home · contact · privacy
Use smarter YX class for y,x coordinates/sizes.
[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 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[YX(0,0)]:
105                 if self.maps[YX(0,0)][pos] == '.' and \
106                    len(self.things_at_pos((YX(0,0), pos))) == 0 and \
107                    self.rand.random() > 0.999:
108                     self.add_thing_at('food', (YX(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 = (YX(0,0),
126                            YX(self.rand.randint(0, yx.y - 1),
127                               self.rand.randint(0, yx.x - 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(YX(0,0))
140         self.new_map(YX(0,1))
141         self.new_map(YX(1,1))
142         self.new_map(YX(1,0))
143         self.new_map(YX(1,-1))
144         self.new_map(YX(0,-1))
145         self.new_map(YX(-1,-1))
146         self.new_map(YX(-1,0))
147         self.new_map(YX(-1,1))
148         for map_pos in self.maps:
149             map_ = self.maps[map_pos]
150             if YX(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[YX(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] - offset)
212             self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
213             self.io.send('THING_POS %s %s' % (thing.id_, offset_pos))
214
215         self.io.send('TURN ' + str(self.world.turn))
216         visible_map = self.world.player.get_visible_map()
217         offset = self.world.player.get_surroundings_offset()
218         self.io.send('VISIBLE_MAP %s %s' % (offset, visible_map.size))
219         for y, line in visible_map.lines():
220             self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, quote(line)))
221         visible_things = self.world.player.get_visible_things()
222         for thing in visible_things:
223             send_thing(offset, thing)
224             if hasattr(thing, 'health'):
225                 self.io.send('THING_HEALTH %s %s' % (thing.id_,
226                                                      thing.health))
227         if len(self.world.player.inventory) > 0:
228             self.io.send('PLAYER_INVENTORY %s' %
229                          ','.join([str(i) for i in self.world.player.inventory]))
230         else:
231             self.io.send('PLAYER_INVENTORY ,')
232         for id_ in self.world.player.inventory:
233             thing = self.world.get_thing(id_)
234             send_thing(offset, thing)
235         self.io.send('GAME_STATE_COMPLETE')
236
237     def proceed(self):
238         """Send turn finish signal, run game world, send new world data.
239
240         First sends 'TURN_FINISHED' message, then runs game world
241         until new player input is needed, then sends game state.
242         """
243         self.io.send('TURN_FINISHED ' + str(self.world.turn))
244         self.world.proceed_to_next_player_turn()
245         msg = str(self.world.player._last_task_result)
246         self.io.send('LAST_PLAYER_TASK_RESULT ' + quote(msg))
247         self.send_gamestate()
248
249     def get_command(self, command_name):
250
251         def partial_with_attrs(f, *args, **kwargs):
252             from functools import partial
253             p = partial(f, *args, **kwargs)
254             p.__dict__.update(f.__dict__)
255             return p
256
257         def cmd_TASK_colon(task_name, game, *args):
258             if not game.world.player_is_alive:
259                 raise GameError('You are dead.')
260             game.world.player.set_task(task_name, args)
261             game.proceed()
262
263         def cmd_SET_TASK_colon(task_name, game, thing_id, todo, *args):
264             t = game.world.get_thing(thing_id, False)
265             if t is None:
266                 raise ArgError('No such Thing.')
267             task_class = game.tasks[task_name]
268             t.task = task_class(t, args)
269             t.task.todo = todo
270
271         def task_prefixed(command_name, task_prefix, task_command,
272                           argtypes_prefix=None):
273             if command_name[:len(task_prefix)] == task_prefix:
274                 task_name = command_name[len(task_prefix):]
275                 if task_name in self.tasks:
276                     f = partial_with_attrs(task_command, task_name, self)
277                     task = self.tasks[task_name]
278                     if argtypes_prefix:
279                         f.argtypes = argtypes_prefix + ' ' + task.argtypes
280                     else:
281                         f.argtypes = task.argtypes
282                     return f
283             return None
284
285         command = task_prefixed(command_name, 'TASK:', cmd_TASK_colon)
286         if command:
287             return command
288         command = task_prefixed(command_name, 'SET_TASK:', cmd_SET_TASK_colon,
289                                 'int:nonneg int:nonneg ')
290         if command:
291             return command
292         if command_name in self.commands:
293             f = partial_with_attrs(self.commands[command_name], self)
294             return f
295         return None