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