home · contact · privacy
Decouple awakeness/sleep stats from Map to MapChunk.
[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         return self.things[-1].id_ + 1
208
209     def get_map(self, map_pos):
210         if not (map_pos in self.maps and
211                 self.maps[map_pos].size == self.map_size):
212             self.maps[map_pos] = MapChunk(self.map_size)
213             self.maps[map_pos].awake = self.max_map_awakeness
214             for pos in self.maps[map_pos]:
215                 self.maps[map_pos][pos] = '.'
216         return self.maps[map_pos]
217
218     def proceed_to_next_player_turn(self):
219         """Run game world turns until player can decide their next step.
220
221         All things and processes inside the player's reality bubble
222         are worked through. Things are furthered in their tasks and,
223         if finished, decide new ones. The iteration order is: first
224         all things that come after the player in the world things
225         list, then (after incrementing the world turn) all that come
226         before the player; then the player's .proceed() is run.
227
228         Next, parts of the game world are put to sleep or woken up
229         based on how close they are to the player's position, or how
230         short ago the player visited them.
231
232         If the player's last task is finished at the end of the loop,
233         it breaks; otherwise it starts again.
234
235         """
236
237         def proceed_world():
238             for thing in self.things[player_i+1:]:
239                 thing.proceed()
240             self.turn += 1
241             for map_pos in self.maps:
242                 if self.maps[map_pos].awake:
243                     for pos in self.maps[map_pos]:
244                         if self.rand.random() > 0.999 and \
245                            self.maps[map_pos][pos] == '.' and \
246                            len(self.things_at_pos((map_pos, pos))) == 0:
247                             self.add_thing_at('food', (map_pos, pos))
248             for thing in self.things[:player_i]:
249                 thing.proceed()
250             self.player.proceed(is_AI=False)
251
252         def reality_bubble():
253
254             def regenerate_chunk_from_map_stats(map_):
255                 import math
256                 max_stat = self.max_map_awakeness
257                 for t_type in map_.stats:
258                     stat = map_.stats[t_type]
259                     to_create = stat['population'] // max_stat
260                     mod_created = int(self.rand.randint(0, max_stat - 1) <
261                                       (stat['population'] % max_stat))
262                     to_create = (stat['population'] // max_stat) + mod_created
263                     if to_create == 0:
264                         continue
265                     average_health = None
266                     if stat['health'] > 0:
267                         average_health = math.ceil(stat['health'] /
268                                                    stat['population'])
269                     for i in range(to_create):
270                         t = self.add_thing_at_random(map_pos, t_type)
271                         if average_health:
272                             t.health = average_health
273                         #if hasattr(t, 'health'):
274                         #    print('DEBUG create', t.type_, t.health)
275
276             for map_pos in self.maps:
277                 m = self.maps[map_pos]
278                 if map_pos in self.player.close_maps:
279
280                     # Newly inside chunks are regenerated from .stats.
281                     if not m.awake:
282                         #print('DEBUG regen stats', map_pos, m.stats)
283                         regenerate_chunk_from_map_stats(m)
284
285                     # Inside chunks are set to max .awake and don't collect
286                     # stats.
287                     m.awake = self.max_map_awakeness
288                     m.stats = {}
289
290                 # Outside chunks grow distant through .awake decremention.
291                 # They collect .stats until they fall asleep – then any things
292                 # inside are disappeared.
293                 elif m.awake > 0:
294                     m.awake -= 1
295                     for t in self.things:
296                         if t.position[0] == map_pos:
297                             if not t.type_ in m.stats:
298                                 m.stats[t.type_] = {'population': 0,
299                                                     'health': 0}
300                             m.stats[t.type_]['population'] += 1
301                             if isinstance(t, ThingAnimate):
302                                 m.stats[t.type_]['health'] += t.health
303                             if not m.awake:
304                                 del self.things[self.things.index(t)]
305                     #if not m.awake:
306                     #    print('DEBUG sleep stats', map_pos, m.stats)
307
308         while True:
309             player_i = self.things.index(self.player)
310             proceed_world()
311             reality_bubble()
312             if self.player.task is None or not self.player_is_alive:
313                 break
314
315     def add_thing_at(self, type_, pos):
316         t = self.thing_types[type_](self)
317         t.position = pos
318         self.things += [t]
319         return t
320
321     def add_thing_at_random(self, big_yx, type_):
322         while True:
323             new_pos = (big_yx,
324                        YX(self.rand.randint(0, self.map_size.y - 1),
325                           self.rand.randint(0, self.map_size.x - 1)))
326             if self.maps[new_pos[0]][new_pos[1]] != '.':
327                 continue
328             if len(self.things_at_pos(new_pos)) > 0:
329                 continue
330             return self.add_thing_at(type_, new_pos)
331
332     def make_map_chunk(self, big_yx):
333         map_ = self.get_map(big_yx)
334         for pos in map_:
335             map_[pos] = self.rand.choice(('.', '.', '.', '~', 'x'))
336         self.add_thing_at_random(big_yx, 'monster')
337         self.add_thing_at_random(big_yx, 'monster')
338         self.add_thing_at_random(big_yx, 'monster')
339         self.add_thing_at_random(big_yx, 'monster')
340         self.add_thing_at_random(big_yx, 'monster')
341         self.add_thing_at_random(big_yx, 'monster')
342         self.add_thing_at_random(big_yx, 'monster')
343         self.add_thing_at_random(big_yx, 'monster')
344         self.add_thing_at_random(big_yx, 'food')
345         self.add_thing_at_random(big_yx, 'food')
346         self.add_thing_at_random(big_yx, 'food')
347         self.add_thing_at_random(big_yx, 'food')
348
349     def make_new_world(self, size, seed):
350         self.things = []
351         self.rand.seed(seed)
352         self.turn = 0
353         self.maps = {}
354         self.map_size = size
355         self.make_map_chunk(YX(0,0))
356         player = self.add_thing_at_random(YX(0,0), 'human')
357         player.surroundings  # To help initializing reality bubble, see
358                              # comment on ThingAnimate._position_set
359         self.player_id = player.id_
360         return 'success'