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