home · contact · privacy
Fix bug that created multiple objects of same ID.
[plomrogue2-experiments] / new / plomrogue / game.py
1 from plomrogue.tasks import (Task_WAIT, Task_MOVE, Task_PICKUP,
2                              Task_DROP, Task_EAT)
3 from plomrogue.errors import ArgError, GameError
4 from plomrogue.commands import (cmd_GEN_WORLD, cmd_GET_GAMESTATE,
5                                 cmd_MAP, cmd_MAP, cmd_THING_TYPE,
6                                 cmd_THING_POS, cmd_THING_INVENTORY,
7                                 cmd_THING_HEALTH, cmd_SEED,
8                                 cmd_GET_PICKABLE_ITEMS, cmd_MAP_SIZE,
9                                 cmd_TERRAIN_LINE, cmd_PLAYER_ID,
10                                 cmd_TURN, cmd_SWITCH_PLAYER, cmd_SAVE)
11 from plomrogue.mapping import MapGeometryHex, MapChunk, YX
12 from plomrogue.parser import Parser
13 from plomrogue.io import GameIO
14 from plomrogue.misc import quote
15 from plomrogue.things import (Thing, ThingMonster, ThingHuman, ThingFood,
16                               ThingAnimate)
17 import random
18
19
20
21 class PRNGod(random.Random):
22
23     def seed(self, seed):
24         self.prngod_seed = seed
25
26     def getstate(self):
27         return self.prngod_seed
28
29     def setstate(seed):
30         self.seed(seed)
31
32     def random(self):
33         self.prngod_seed = ((self.prngod_seed * 1103515245) + 12345) % 2**32
34         return (self.prngod_seed >> 16) / (2**16 - 1)
35
36
37
38 class GameBase:
39
40     def __init__(self):
41         self.turn = 0
42         self.things = []
43
44     def get_thing(self, id_, create_unfound=True):
45         for thing in self.things:
46             if id_ == thing.id_:
47                 return thing
48         if create_unfound:
49             t = self.thing_type(self, id_)
50             self.things += [t]
51             return t
52         return None
53
54     def things_at_pos(self, pos):
55         things = []
56         for t in self.things:
57             if t.position == pos:
58                 things += [t]
59         return things
60
61
62
63 class Game(GameBase):
64
65     def __init__(self, game_file_name, *args, **kwargs):
66         super().__init__(*args, **kwargs)
67         self.io = GameIO(game_file_name, self)
68         self.map_size = None
69         self.map_geometry = MapGeometryHex()
70         self.tasks = {'WAIT': Task_WAIT,
71                       'MOVE': Task_MOVE,
72                       'PICKUP': Task_PICKUP,
73                       'EAT': Task_EAT,
74                       'DROP': Task_DROP}
75         self.commands = {'GEN_WORLD': cmd_GEN_WORLD,
76                          'GET_GAMESTATE': cmd_GET_GAMESTATE,
77                          'SEED': cmd_SEED,
78                          'MAP_SIZE': cmd_MAP_SIZE,
79                          'MAP': cmd_MAP,
80                          'THING_TYPE': cmd_THING_TYPE,
81                          'THING_POS': cmd_THING_POS,
82                          'THING_HEALTH': cmd_THING_HEALTH,
83                          'THING_INVENTORY': cmd_THING_INVENTORY,
84                          'TERRAIN_LINE': cmd_TERRAIN_LINE,
85                          'GET_PICKABLE_ITEMS': cmd_GET_PICKABLE_ITEMS,
86                          'PLAYER_ID': cmd_PLAYER_ID,
87                          'TURN': cmd_TURN,
88                          'SWITCH_PLAYER': cmd_SWITCH_PLAYER,
89                          'SAVE': cmd_SAVE}
90         self.thing_type = Thing
91         self.thing_types = {'human': ThingHuman,
92                             'monster': ThingMonster,
93                             'food': ThingFood}
94         self.player_id = 0
95         self.player_is_alive = True
96         self.maps = {}
97         self.max_map_awakeness = 100
98         self.rand = PRNGod(0)
99
100     def get_string_options(self, string_option_type):
101         if string_option_type == 'direction':
102             return self.map_geometry.get_directions()
103         elif string_option_type == 'thingtype':
104             return list(self.thing_types.keys())
105         return None
106
107     def send_gamestate(self, connection_id=None):
108         """Send out game state data relevant to clients."""
109
110         def send_thing(thing):
111             view_pos = self.map_geometry.pos_in_view(thing.position,
112                                                      self.player.view_offset,
113                                                      self.map_size)
114             self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
115             self.io.send('THING_POS %s %s' % (thing.id_, view_pos))
116
117         self.io.send('PLAYER_ID ' + str(self.player_id))
118         self.io.send('TURN ' + str(self.turn))
119         visible_map = self.player.get_visible_map()
120         self.io.send('VISIBLE_MAP %s %s' % (visible_map.size,
121                                             visible_map.start_indented))
122         for y, line in visible_map.lines():
123             self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, quote(line)))
124         visible_things = self.player.get_visible_things()
125         for thing in visible_things:
126             send_thing(thing)
127             if hasattr(thing, 'health'):
128                 self.io.send('THING_HEALTH %s %s' % (thing.id_,
129                                                      thing.health))
130         if len(self.player.inventory) > 0:
131             self.io.send('PLAYER_INVENTORY %s' %
132                          ','.join([str(i) for i in self.player.inventory]))
133         else:
134             self.io.send('PLAYER_INVENTORY ,')
135         for id_ in self.player.inventory:
136             thing = self.get_thing(id_)
137             send_thing(thing)
138         self.io.send('GAME_STATE_COMPLETE')
139
140     def proceed(self):
141         """Send turn finish signal, run game world, send new world data.
142
143         First sends 'TURN_FINISHED' message, then runs game world
144         until new player input is needed, then sends game state.
145         """
146         self.io.send('TURN_FINISHED ' + str(self.turn))
147         self.proceed_to_next_player_turn()
148         msg = str(self.player._last_task_result)
149         self.io.send('LAST_PLAYER_TASK_RESULT ' + quote(msg))
150         self.send_gamestate()
151
152     def get_command(self, command_name):
153
154         def partial_with_attrs(f, *args, **kwargs):
155             from functools import partial
156             p = partial(f, *args, **kwargs)
157             p.__dict__.update(f.__dict__)
158             return p
159
160         def cmd_TASK_colon(task_name, game, *args):
161             if not game.player_is_alive:
162                 raise GameError('You are dead.')
163             game.player.set_task(task_name, args)
164             game.proceed()
165
166         def cmd_SET_TASK_colon(task_name, game, thing_id, todo, *args):
167             t = game.get_thing(thing_id, False)
168             if t is None:
169                 raise ArgError('No such Thing.')
170             task_class = game.tasks[task_name]
171             t.task = task_class(t, args)
172             t.task.todo = todo
173
174         def task_prefixed(command_name, task_prefix, task_command,
175                           argtypes_prefix=None):
176             if command_name[:len(task_prefix)] == task_prefix:
177                 task_name = command_name[len(task_prefix):]
178                 if task_name in self.tasks:
179                     f = partial_with_attrs(task_command, task_name, self)
180                     task = self.tasks[task_name]
181                     if argtypes_prefix:
182                         f.argtypes = argtypes_prefix + ' ' + task.argtypes
183                     else:
184                         f.argtypes = task.argtypes
185                     return f
186             return None
187
188         command = task_prefixed(command_name, 'TASK:', cmd_TASK_colon)
189         if command:
190             return command
191         command = task_prefixed(command_name, 'SET_TASK:', cmd_SET_TASK_colon,
192                                 'int:nonneg int:nonneg ')
193         if command:
194             return command
195         if command_name in self.commands:
196             f = partial_with_attrs(self.commands[command_name], self)
197             return f
198         return None
199
200     @property
201     def player(self):
202         return self.get_thing(self.player_id)
203
204     def new_thing_id(self):
205         if len(self.things) == 0:
206             return 0
207         # DANGEROUS – if anywhere we append a thing to the list of lower
208         # ID than the highest-value ID, this might lead to re-using an
209         # already active ID.  This should not happen anywhere in the
210         # code, but a break here might be more visible.
211         return self.things[-1].id_ + 1
212
213     def get_map(self, map_pos):
214         if not (map_pos in self.maps and
215                 self.maps[map_pos].size == self.map_size):
216             self.maps[map_pos] = MapChunk(self.map_size)
217             self.maps[map_pos].awake = self.max_map_awakeness
218             for pos in self.maps[map_pos]:
219                 self.maps[map_pos][pos] = '.'
220         return self.maps[map_pos]
221
222     def proceed_to_next_player_turn(self):
223         """Run game world turns until player can decide their next step.
224
225         All things and processes inside the player's reality bubble
226         are worked through. Things are furthered in their tasks and,
227         if finished, decide new ones. The iteration order is: first
228         all things that come after the player in the world things
229         list, then (after incrementing the world turn) all that come
230         before the player; then the player's .proceed() is run.
231
232         Next, parts of the game world are put to sleep or woken up
233         based on how close they are to the player's position, or how
234         short ago the player visited them.
235
236         If the player's last task is finished at the end of the loop,
237         it breaks; otherwise it starts again.
238
239         """
240
241         def proceed_world():
242             for thing in self.things[player_i+1:]:
243                 thing.proceed()
244             self.turn += 1
245             for map_pos in self.maps:
246                 if self.maps[map_pos].awake:
247                     for pos in self.maps[map_pos]:
248                         if self.rand.random() > 0.999 and \
249                            self.maps[map_pos][pos] == '.' and \
250                            len(self.things_at_pos((map_pos, pos))) == 0:
251                             self.add_thing_at('food', (map_pos, pos))
252             for thing in self.things[:player_i]:
253                 thing.proceed()
254             self.player.proceed(is_AI=False)
255
256         def reality_bubble():
257
258             def regenerate_chunk_from_map_stats(map_):
259                 import math
260                 max_stat = self.max_map_awakeness
261                 for t_type in map_.stats:
262                     stat = map_.stats[t_type]
263                     to_create = stat['population'] // max_stat
264                     mod_created = int(self.rand.randint(0, max_stat - 1) <
265                                       (stat['population'] % max_stat))
266                     to_create = (stat['population'] // max_stat) + mod_created
267                     if to_create == 0:
268                         continue
269                     average_health = None
270                     if stat['health'] > 0:
271                         average_health = math.ceil(stat['health'] /
272                                                    stat['population'])
273                     for i in range(to_create):
274                         t = self.add_thing_at_random(map_pos, t_type)
275                         if average_health:
276                             t.health = average_health
277                         #if hasattr(t, 'health'):
278                         #    print('DEBUG create', t.type_, t.health)
279
280             for map_pos in self.maps:
281                 m = self.maps[map_pos]
282                 if map_pos in self.player.close_maps:
283
284                     # Newly inside chunks are regenerated from .stats.
285                     if not m.awake:
286                         #print('DEBUG regen stats', map_pos, m.stats)
287                         regenerate_chunk_from_map_stats(m)
288
289                     # Inside chunks are set to max .awake and don't collect
290                     # stats.
291                     m.awake = self.max_map_awakeness
292                     m.stats = {}
293
294                 # Outside chunks grow distant through .awake decremention.
295                 # They collect .stats until they fall asleep – then any things
296                 # inside are disappeared.
297                 elif m.awake > 0:
298                     m.awake -= 1
299                     # We iterate over a list comprehension of self.things,
300                     # since we might delete elements of self.things.
301                     for t in [t for t in self.things]:
302                         if t.position[0] == map_pos:
303                             if not t.type_ in m.stats:
304                                 m.stats[t.type_] = {'population': 0,
305                                                     'health': 0}
306                             m.stats[t.type_]['population'] += 1
307                             if isinstance(t, ThingAnimate):
308                                 m.stats[t.type_]['health'] += t.health
309                             if not m.awake:
310                                 # TODO: Handle inventory.
311                                 del self.things[self.things.index(t)]
312                     #if not m.awake:
313                     #    print('DEBUG sleep stats', map_pos, m.stats)
314
315         while True:
316             player_i = self.things.index(self.player)
317             proceed_world()
318             reality_bubble()
319             if self.player.task is None or not self.player_is_alive:
320                 break
321
322     def add_thing_at(self, type_, pos):
323         t = self.thing_types[type_](self)
324         t.position = pos
325         self.things += [t]
326         return t
327
328     def add_thing_at_random(self, big_yx, type_):
329         while True:
330             new_pos = (big_yx,
331                        YX(self.rand.randint(0, self.map_size.y - 1),
332                           self.rand.randint(0, self.map_size.x - 1)))
333             if self.maps[new_pos[0]][new_pos[1]] != '.':
334                 continue
335             if len(self.things_at_pos(new_pos)) > 0:
336                 continue
337             return self.add_thing_at(type_, new_pos)
338
339     def make_map_chunk(self, big_yx):
340         map_ = self.get_map(big_yx)
341         for pos in map_:
342             map_[pos] = self.rand.choice(('.', '.', '.', '~', 'x'))
343         self.add_thing_at_random(big_yx, 'monster')
344         self.add_thing_at_random(big_yx, 'monster')
345         self.add_thing_at_random(big_yx, 'monster')
346         self.add_thing_at_random(big_yx, 'monster')
347         self.add_thing_at_random(big_yx, 'monster')
348         self.add_thing_at_random(big_yx, 'monster')
349         self.add_thing_at_random(big_yx, 'monster')
350         self.add_thing_at_random(big_yx, 'monster')
351         self.add_thing_at_random(big_yx, 'food')
352         self.add_thing_at_random(big_yx, 'food')
353         self.add_thing_at_random(big_yx, 'food')
354         self.add_thing_at_random(big_yx, 'food')
355
356     def make_new_world(self, size, seed):
357         self.things = []
358         self.rand.seed(seed)
359         self.turn = 0
360         self.maps = {}
361         self.map_size = size
362         self.make_map_chunk(YX(0,0))
363         player = self.add_thing_at_random(YX(0,0), 'human')
364         player.surroundings  # To help initializing reality bubble, see
365                              # comment on ThingAnimate._position_set
366         self.player_id = player.id_
367         return 'success'