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