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