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