home · contact · privacy
Use self.max_map_awakeness to recude magic numbering.
[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, Map, 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] = Map(self.map_size,
213                                      awakeness=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
274             for map_pos in self.maps:
275                 m = self.maps[map_pos]
276                 if map_pos in self.player.close_maps:
277
278                     # Newly inside chunks are regenerated from .stats.
279                     if not m.awake:
280                         regenerate_chunk_from_map_stats(m)
281
282                     # Inside chunks are set to max .awake and don't collect
283                     # stats.
284                     m.awake = self.max_map_awakeness
285                     m.stats = {}
286
287                 # Outside chunks grow distant through .awake decremention.
288                 # They collect .stats until they fall asleep – then any things
289                 # inside are disappeared.
290                 elif m.awake > 0:
291                     m.awake -= 1
292                     for t in self.things:
293                         if t.position[0] == map_pos:
294                             if not t.type_ in m.stats:
295                                 m.stats[t.type_] = {'population': 0,
296                                                     'health': 0}
297                             m.stats[t.type_]['population'] += 1
298                             if isinstance(t, ThingAnimate):
299                                 m.stats[t.type_]['health'] += t.health
300                             if not m.awake:
301                                 del self.things[self.things.index(t)]
302
303         while True:
304             player_i = self.things.index(self.player)
305             proceed_world()
306             reality_bubble()
307             if self.player.task is None or not self.player_is_alive:
308                 break
309
310     def add_thing_at(self, type_, pos):
311         t = self.thing_types[type_](self)
312         t.position = pos
313         self.things += [t]
314         return t
315
316     def add_thing_at_random(self, big_yx, type_):
317         while True:
318             new_pos = (big_yx,
319                        YX(self.rand.randint(0, self.map_size.y - 1),
320                           self.rand.randint(0, self.map_size.x - 1)))
321             if self.maps[new_pos[0]][new_pos[1]] != '.':
322                 continue
323             if len(self.things_at_pos(new_pos)) > 0:
324                 continue
325             return self.add_thing_at(type_, new_pos)
326
327     def make_map_chunk(self, big_yx):
328         map_ = self.get_map(big_yx)
329         for pos in map_:
330             map_[pos] = self.rand.choice(('.', '.', '.', '~', 'x'))
331         self.add_thing_at_random(big_yx, 'monster')
332         self.add_thing_at_random(big_yx, 'monster')
333         self.add_thing_at_random(big_yx, 'monster')
334         self.add_thing_at_random(big_yx, 'monster')
335         self.add_thing_at_random(big_yx, 'monster')
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, 'food')
340         self.add_thing_at_random(big_yx, 'food')
341         self.add_thing_at_random(big_yx, 'food')
342         self.add_thing_at_random(big_yx, 'food')
343
344     def make_new_world(self, size, seed):
345         self.things = []
346         self.rand.seed(seed)
347         self.turn = 0
348         self.maps = {}
349         self.map_size = size
350         self.make_map_chunk(YX(0,0))
351         player = self.add_thing_at_random(YX(0,0), 'human')
352         player.surroundings  # To help initializing reality bubble, see
353                              # comment on ThingAnimate._position_set
354         self.player_id = player.id_
355         return 'success'