home · contact · privacy
Fix save game corruption due to insufficient .in_inventory handling.
[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,
9                                 cmd_TERRAIN_LINE, cmd_PLAYER_ID,
10                                 cmd_TURN, cmd_SWITCH_PLAYER, cmd_SAVE)
11 from plomrogue.mapping import MapHex
12 from plomrogue.parser import Parser
13 from plomrogue.io import GameIO
14 from plomrogue.misc import quote, stringify_yx
15 from plomrogue.things import Thing, ThingMonster, ThingHuman, ThingFood
16 import random
17
18
19
20 class PRNGod(random.Random):
21
22     def seed(self, seed):
23         self.prngod_seed = seed
24
25     def getstate(self):
26         return self.prngod_seed
27
28     def setstate(seed):
29         self.seed(seed)
30
31     def random(self):
32         self.prngod_seed = ((self.prngod_seed * 1103515245) + 12345) % 2**32
33         return (self.prngod_seed >> 16) / (2**16 - 1)
34
35
36
37 class WorldBase:
38
39     def __init__(self, game):
40         self.turn = 0
41         self.things = []
42         self.game = game
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.game.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 World(WorldBase):
64
65     def __init__(self, *args, **kwargs):
66         super().__init__(*args, **kwargs)
67         self.player_id = 0
68         self.player_is_alive = True
69         self.maps = {}
70         self.rand = PRNGod(0)
71
72     @property
73     def player(self):
74         return self.get_thing(self.player_id)
75
76     def new_thing_id(self):
77         if len(self.things) == 0:
78             return 0
79         return self.things[-1].id_ + 1
80
81     def new_map(self, map_pos, size):
82         self.maps[map_pos] = self.game.map_type(size)
83
84     def proceed_to_next_player_turn(self):
85         """Run game world turns until player can decide their next step.
86
87         Iterates through all non-player things, on each step
88         furthering them in their tasks (and letting them decide new
89         ones if they finish). The iteration order is: first all things
90         that come after the player in the world things list, then
91         (after incrementing the world turn) all that come before the
92         player; then the player's .proceed() is run, and if it does
93         not finish his task, the loop starts at the beginning. Once
94         the player's task is finished, or the player is dead, the loop
95         breaks.
96
97         """
98         while True:
99             player_i = self.things.index(self.player)
100             for thing in self.things[player_i+1:]:
101                 thing.proceed()
102             self.turn += 1
103             for pos in self.maps[(0,0)]:
104                 if self.maps[(0,0)][pos] == '.' and \
105                    len(self.things_at_pos(((0,0), pos))) == 0 and \
106                    self.rand.random() > 0.999:
107                     self.add_thing_at('food', ((0,0), pos))
108             for thing in self.things[:player_i]:
109                 thing.proceed()
110             self.player.proceed(is_AI=False)
111             if self.player.task is None or not self.player_is_alive:
112                 break
113
114     def add_thing_at(self, type_, pos):
115         t = self.game.thing_types[type_](self)
116         t.position = pos
117         self.things += [t]
118         return t
119
120     def make_new(self, yx, seed):
121
122         def add_thing_at_random(type_):
123             while True:
124                 new_pos = ((0,0),
125                            (self.rand.randint(0, yx[0] -1),
126                             self.rand.randint(0, yx[1] -1)))
127                 if self.maps[new_pos[0]][new_pos[1]] != '.':
128                     continue
129                 if len(self.things_at_pos(new_pos)) > 0:
130                     continue
131                 return self.add_thing_at(type_, new_pos)
132
133         self.things = []
134         self.rand.seed(seed)
135         self.turn = 0
136         self.maps = {}
137         self.new_map((0,0), yx)
138         self.new_map((0,1), yx)
139         self.new_map((1,1), yx)
140         self.new_map((1,0), yx)
141         self.new_map((1,-1), yx)
142         self.new_map((0,-1), yx)
143         self.new_map((-1,-1), yx)
144         self.new_map((-1,0), yx)
145         self.new_map((-1,1), yx)
146         for map_pos in self.maps:
147             map_ = self.maps[map_pos]
148             if (0,0) == map_pos:
149                 for pos in map_:
150                     map_[pos] = self.rand.choice(('.', '.', '.', '.', 'x'))
151             else:
152                 for pos in map_:
153                     map_[pos] = '~'
154         player = add_thing_at_random('human')
155         self.player_id = player.id_
156         add_thing_at_random('monster')
157         add_thing_at_random('monster')
158         add_thing_at_random('food')
159         add_thing_at_random('food')
160         add_thing_at_random('food')
161         add_thing_at_random('food')
162         return 'success'
163
164
165
166 class Game:
167
168     def __init__(self, game_file_name):
169         self.io = GameIO(game_file_name, self)
170         self.map_type = MapHex
171         self.tasks = {'WAIT': Task_WAIT,
172                       'MOVE': Task_MOVE,
173                       'PICKUP': Task_PICKUP,
174                       'EAT': Task_EAT,
175                       'DROP': Task_DROP}
176         self.commands = {'GEN_WORLD': cmd_GEN_WORLD,
177                          'GET_GAMESTATE': cmd_GET_GAMESTATE,
178                          'SEED': cmd_SEED,
179                          'MAP': cmd_MAP,
180                          'THING_TYPE': cmd_THING_TYPE,
181                          'THING_POS': cmd_THING_POS,
182                          'THING_HEALTH': cmd_THING_HEALTH,
183                          'THING_INVENTORY': cmd_THING_INVENTORY,
184                          'TERRAIN_LINE': cmd_TERRAIN_LINE,
185                          'GET_PICKABLE_ITEMS': cmd_GET_PICKABLE_ITEMS,
186                          'PLAYER_ID': cmd_PLAYER_ID,
187                          'TURN': cmd_TURN,
188                          'SWITCH_PLAYER': cmd_SWITCH_PLAYER,
189                          'SAVE': cmd_SAVE}
190         self.world_type = World
191         self.world = self.world_type(self)
192         self.thing_type = Thing
193         self.thing_types = {'human': ThingHuman,
194                             'monster': ThingMonster,
195                             'food': ThingFood}
196
197     def get_string_options(self, string_option_type):
198         if string_option_type == 'direction':
199             return self.world.maps[(0,0)].get_directions()
200         elif string_option_type == 'thingtype':
201             return list(self.thing_types.keys())
202         return None
203
204     def send_gamestate(self, connection_id=None):
205         """Send out game state data relevant to clients."""
206
207         def send_thing(offset, thing):
208             offset_pos = (thing.position[1][0] - offset[0],
209                           thing.position[1][1] - offset[1])
210             self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
211             self.io.send('THING_POS %s %s' % (thing.id_,
212                                               stringify_yx(offset_pos)))
213
214         self.io.send('TURN ' + str(self.world.turn))
215         visible_map = self.world.player.get_visible_map()
216         offset = self.world.player.get_surroundings_offset()
217         self.io.send('VISIBLE_MAP ' + stringify_yx(offset) + ' ' + stringify_yx(visible_map.size))
218         for y, line in visible_map.lines():
219             self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, quote(line)))
220         visible_things = self.world.player.get_visible_things()
221         for thing in visible_things:
222             send_thing(offset, thing)
223             if hasattr(thing, 'health'):
224                 self.io.send('THING_HEALTH %s %s' % (thing.id_,
225                                                      thing.health))
226         if len(self.world.player.inventory) > 0:
227             self.io.send('PLAYER_INVENTORY %s' %
228                          ','.join([str(i) for i in self.world.player.inventory]))
229         else:
230             self.io.send('PLAYER_INVENTORY ,')
231         for id_ in self.world.player.inventory:
232             thing = self.world.get_thing(id_)
233             send_thing(offset, thing)
234         self.io.send('GAME_STATE_COMPLETE')
235
236     def proceed(self):
237         """Send turn finish signal, run game world, send new world data.
238
239         First sends 'TURN_FINISHED' message, then runs game world
240         until new player input is needed, then sends game state.
241         """
242         self.io.send('TURN_FINISHED ' + str(self.world.turn))
243         self.world.proceed_to_next_player_turn()
244         msg = str(self.world.player._last_task_result)
245         self.io.send('LAST_PLAYER_TASK_RESULT ' + quote(msg))
246         self.send_gamestate()
247
248     def get_command(self, command_name):
249
250         def partial_with_attrs(f, *args, **kwargs):
251             from functools import partial
252             p = partial(f, *args, **kwargs)
253             p.__dict__.update(f.__dict__)
254             return p
255
256         def cmd_TASK_colon(task_name, game, *args):
257             if not game.world.player_is_alive:
258                 raise GameError('You are dead.')
259             game.world.player.set_task(task_name, args)
260             game.proceed()
261
262         def cmd_SET_TASK_colon(task_name, game, thing_id, todo, *args):
263             t = game.world.get_thing(thing_id, False)
264             if t is None:
265                 raise ArgError('No such Thing.')
266             task_class = game.tasks[task_name]
267             t.task = task_class(t, args)
268             t.task.todo = todo
269
270         def task_prefixed(command_name, task_prefix, task_command,
271                           argtypes_prefix=None):
272             if command_name[:len(task_prefix)] == task_prefix:
273                 task_name = command_name[len(task_prefix):]
274                 if task_name in self.tasks:
275                     f = partial_with_attrs(task_command, task_name, self)
276                     task = self.tasks[task_name]
277                     if argtypes_prefix:
278                         f.argtypes = argtypes_prefix + ' ' + task.argtypes
279                     else:
280                         f.argtypes = task.argtypes
281                     return f
282             return None
283
284         command = task_prefixed(command_name, 'TASK:', cmd_TASK_colon)
285         if command:
286             return command
287         command = task_prefixed(command_name, 'SET_TASK:', cmd_SET_TASK_colon,
288                                 'int:nonneg int:nonneg ')
289         if command:
290             return command
291         if command_name in self.commands:
292             f = partial_with_attrs(self.commands[command_name], self)
293             return f
294         return None