home · contact · privacy
Add basic reality bubble mechanism.
[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.rand = PRNGod(0)
98
99     def get_string_options(self, string_option_type):
100         if string_option_type == 'direction':
101             return self.map_geometry.get_directions()
102         elif string_option_type == 'thingtype':
103             return list(self.thing_types.keys())
104         return None
105
106     def send_gamestate(self, connection_id=None):
107         """Send out game state data relevant to clients."""
108
109         def send_thing(thing):
110             view_pos = self.map_geometry.pos_in_view(thing.position,
111                                                      self.player.view_offset,
112                                                      self.map_size)
113             self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
114             self.io.send('THING_POS %s %s' % (thing.id_, view_pos))
115
116         self.io.send('PLAYER_ID ' + str(self.player_id))
117         self.io.send('TURN ' + str(self.turn))
118         visible_map = self.player.get_visible_map()
119         self.io.send('VISIBLE_MAP %s %s' % (visible_map.size,
120                                             visible_map.start_indented))
121         for y, line in visible_map.lines():
122             self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, quote(line)))
123         visible_things = self.player.get_visible_things()
124         for thing in visible_things:
125             send_thing(thing)
126             if hasattr(thing, 'health'):
127                 self.io.send('THING_HEALTH %s %s' % (thing.id_,
128                                                      thing.health))
129         if len(self.player.inventory) > 0:
130             self.io.send('PLAYER_INVENTORY %s' %
131                          ','.join([str(i) for i in self.player.inventory]))
132         else:
133             self.io.send('PLAYER_INVENTORY ,')
134         for id_ in self.player.inventory:
135             thing = self.get_thing(id_)
136             send_thing(thing)
137         self.io.send('GAME_STATE_COMPLETE')
138
139     def proceed(self):
140         """Send turn finish signal, run game world, send new world data.
141
142         First sends 'TURN_FINISHED' message, then runs game world
143         until new player input is needed, then sends game state.
144         """
145         self.io.send('TURN_FINISHED ' + str(self.turn))
146         self.proceed_to_next_player_turn()
147         msg = str(self.player._last_task_result)
148         self.io.send('LAST_PLAYER_TASK_RESULT ' + quote(msg))
149         self.send_gamestate()
150
151     def get_command(self, command_name):
152
153         def partial_with_attrs(f, *args, **kwargs):
154             from functools import partial
155             p = partial(f, *args, **kwargs)
156             p.__dict__.update(f.__dict__)
157             return p
158
159         def cmd_TASK_colon(task_name, game, *args):
160             if not game.player_is_alive:
161                 raise GameError('You are dead.')
162             game.player.set_task(task_name, args)
163             game.proceed()
164
165         def cmd_SET_TASK_colon(task_name, game, thing_id, todo, *args):
166             t = game.get_thing(thing_id, False)
167             if t is None:
168                 raise ArgError('No such Thing.')
169             task_class = game.tasks[task_name]
170             t.task = task_class(t, args)
171             t.task.todo = todo
172
173         def task_prefixed(command_name, task_prefix, task_command,
174                           argtypes_prefix=None):
175             if command_name[:len(task_prefix)] == task_prefix:
176                 task_name = command_name[len(task_prefix):]
177                 if task_name in self.tasks:
178                     f = partial_with_attrs(task_command, task_name, self)
179                     task = self.tasks[task_name]
180                     if argtypes_prefix:
181                         f.argtypes = argtypes_prefix + ' ' + task.argtypes
182                     else:
183                         f.argtypes = task.argtypes
184                     return f
185             return None
186
187         command = task_prefixed(command_name, 'TASK:', cmd_TASK_colon)
188         if command:
189             return command
190         command = task_prefixed(command_name, 'SET_TASK:', cmd_SET_TASK_colon,
191                                 'int:nonneg int:nonneg ')
192         if command:
193             return command
194         if command_name in self.commands:
195             f = partial_with_attrs(self.commands[command_name], self)
196             return f
197         return None
198
199     @property
200     def player(self):
201         return self.get_thing(self.player_id)
202
203     def new_thing_id(self):
204         if len(self.things) == 0:
205             return 0
206         return self.things[-1].id_ + 1
207
208     def get_map(self, map_pos):
209         if not (map_pos in self.maps and
210                 self.maps[map_pos].size == self.map_size):
211             self.maps[map_pos] = Map(self.map_size)
212             for pos in self.maps[map_pos]:
213                 self.maps[map_pos][pos] = '.'
214         return self.maps[map_pos]
215
216     def proceed_to_next_player_turn(self):
217         """Run game world turns until player can decide their next step.
218
219         All things and processes inside the player's reality bubble
220         are worked through. Things are furthered in their tasks and,
221         if finished, decide new ones. The iteration order is: first
222         all things that come after the player in the world things
223         list, then (after incrementing the world turn) all that come
224         before the player; then the player's .proceed() is run.
225
226         Next, parts of the game world are put to sleep or woken up
227         based on how close they are to the player's position, or how
228         short ago the player visited them.
229
230         If the player's last task is finished at the end of the loop,
231         it breaks; otherwise it starts again.
232
233         """
234
235         def proceed_world():
236             for thing in self.things[player_i+1:]:
237                 thing.proceed()
238             self.turn += 1
239             for map_pos in self.maps:
240                 if self.maps[map_pos].awake:
241                     for pos in self.maps[map_pos]:
242                         if self.rand.random() > 0.999 and \
243                            self.maps[map_pos][pos] == '.' and \
244                            len(self.things_at_pos((map_pos, pos))) == 0:
245                             self.add_thing_at('food', (map_pos, pos))
246             for thing in self.things[:player_i]:
247                 thing.proceed()
248             self.player.proceed(is_AI=False)
249
250         def reality_bubble():
251             import math
252             for map_pos in self.maps:
253                 m = self.maps[map_pos]
254                 if map_pos in self.player.close_maps:
255
256                     # Newly inside chunks are regenerated from .stats.
257                     if not m.awake:
258                         for t_type in m.stats:
259                             stat = m.stats[t_type]
260                             to_create = stat['population'] // 100
261                             to_create = stat['population'] // 100 +\
262                                 int(self.rand.randint(0, 99) < (stat['population'] % 100))
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                     # Inside chunks are set to max .awake and don't collect
275                     # stats.
276                     m.awake = 100
277                     m.stats = {}
278
279                 # Outside chunks grow distant through .awake decremention.
280                 # They collect .stats until they fall asleep – then any things
281                 # inside are disappeared.
282                 elif m.awake > 0:
283                     m.awake -= 1
284                     for t in self.things:
285                         if t.position[0] == map_pos:
286                             if not t.type_ in m.stats:
287                                 m.stats[t.type_] = {'population': 0,
288                                                     'health': 0}
289                             m.stats[t.type_]['population'] += 1
290                             if isinstance(t, ThingAnimate):
291                                 m.stats[t.type_]['health'] += t.health
292                                 if not m.awake:
293                             if not m.awake:
294                                 del self.things[self.things.index(t)]
295
296         while True:
297             player_i = self.things.index(self.player)
298             proceed_world()
299             reality_bubble()
300             if self.player.task is None or not self.player_is_alive:
301                 break
302
303     def add_thing_at(self, type_, pos):
304         t = self.thing_types[type_](self)
305         t.position = pos
306         self.things += [t]
307         return t
308
309     def add_thing_at_random(self, big_yx, type_):
310         while True:
311             new_pos = (big_yx,
312                        YX(self.rand.randint(0, self.map_size.y - 1),
313                           self.rand.randint(0, self.map_size.x - 1)))
314             if self.maps[new_pos[0]][new_pos[1]] != '.':
315                 continue
316             if len(self.things_at_pos(new_pos)) > 0:
317                 continue
318             return self.add_thing_at(type_, new_pos)
319
320     def make_map_chunk(self, big_yx):
321         map_ = self.get_map(big_yx)
322         for pos in map_:
323             map_[pos] = self.rand.choice(('.', '.', '.', '~', 'x'))
324         self.add_thing_at_random(big_yx, 'monster')
325         self.add_thing_at_random(big_yx, 'monster')
326         self.add_thing_at_random(big_yx, 'monster')
327         self.add_thing_at_random(big_yx, 'monster')
328         self.add_thing_at_random(big_yx, 'monster')
329         self.add_thing_at_random(big_yx, 'monster')
330         self.add_thing_at_random(big_yx, 'monster')
331         self.add_thing_at_random(big_yx, 'monster')
332         self.add_thing_at_random(big_yx, 'food')
333         self.add_thing_at_random(big_yx, 'food')
334         self.add_thing_at_random(big_yx, 'food')
335         self.add_thing_at_random(big_yx, 'food')
336
337     def make_new_world(self, size, seed):
338         self.things = []
339         self.rand.seed(seed)
340         self.turn = 0
341         self.maps = {}
342         self.map_size = size
343         self.make_map_chunk(YX(0,0))
344         player = self.add_thing_at_random(YX(0,0), 'human')
345         player.surroundings  # To help initializing reality bubble, see
346                              # comment on ThingAnimate._position_set
347         self.player_id = player.id_
348         return 'success'