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