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