home · contact · privacy
669fae3dd0b42483d3800e5ba989bdce37428daf
[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 get_map(self, map_pos, create_unfound=True):
83         if not (map_pos in self.maps and
84                 self.maps[map_pos].size == self.map_size):
85             if create_unfound:
86                 self.maps[map_pos] = self.game.map_type(self.map_size)
87                 for pos in self.maps[map_pos]:
88                     self.maps[map_pos][pos] = '~'
89             else:
90                 return None
91         return self.maps[map_pos]
92
93     def proceed_to_next_player_turn(self):
94         """Run game world turns until player can decide their next step.
95
96         Iterates through all non-player things, on each step
97         furthering them in their tasks (and letting them decide new
98         ones if they finish). The iteration order is: first all things
99         that come after the player in the world things list, then
100         (after incrementing the world turn) all that come before the
101         player; then the player's .proceed() is run, and if it does
102         not finish his task, the loop starts at the beginning. Once
103         the player's task is finished, or the player is dead, the loop
104         breaks.
105
106         """
107         while True:
108             player_i = self.things.index(self.player)
109             for thing in self.things[player_i+1:]:
110                 thing.proceed()
111             self.turn += 1
112             for pos in self.maps[YX(0,0)]:
113                 if self.maps[YX(0,0)][pos] == '.' and \
114                    len(self.things_at_pos((YX(0,0), pos))) == 0 and \
115                    self.rand.random() > 0.999:
116                     self.add_thing_at('food', (YX(0,0), pos))
117             for thing in self.things[:player_i]:
118                 thing.proceed()
119             self.player.proceed(is_AI=False)
120             if self.player.task is None or not self.player_is_alive:
121                 break
122
123     def add_thing_at(self, type_, pos):
124         t = self.game.thing_types[type_](self)
125         t.position = pos
126         self.things += [t]
127         return t
128
129     def make_new(self, yx, seed):
130
131         def add_thing_at_random(type_):
132             while True:
133                 new_pos = (YX(0,0),
134                            YX(self.rand.randint(0, yx.y - 1),
135                               self.rand.randint(0, yx.x - 1)))
136                 if self.maps[new_pos[0]][new_pos[1]] != '.':
137                     continue
138                 if len(self.things_at_pos(new_pos)) > 0:
139                     continue
140                 return self.add_thing_at(type_, new_pos)
141
142         self.things = []
143         self.rand.seed(seed)
144         self.turn = 0
145         self.maps = {}
146         self.map_size = yx
147         map_ = self.get_map(YX(0,0))
148         for pos in map_:
149             map_[pos] = self.rand.choice(('.', '.', '.', '.', 'x'))
150         player = add_thing_at_random('human')
151         self.player_id = player.id_
152         add_thing_at_random('monster')
153         add_thing_at_random('monster')
154         add_thing_at_random('food')
155         add_thing_at_random('food')
156         add_thing_at_random('food')
157         add_thing_at_random('food')
158         return 'success'
159
160
161
162 class Game:
163
164     def __init__(self, game_file_name):
165         self.io = GameIO(game_file_name, self)
166         self.map_type = MapHex
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_type().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 = (thing.position[1] - offset)
206             self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
207             self.io.send('THING_POS %s %s' % (thing.id_, offset_pos))
208
209         self.io.send('TURN ' + str(self.world.turn))
210         visible_map = self.world.player.get_visible_map()
211         offset = self.world.player.get_surroundings_offset()
212         self.io.send('VISIBLE_MAP %s %s' % (offset, visible_map.size))
213         for y, line in visible_map.lines():
214             self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, quote(line)))
215         visible_things = self.world.player.get_visible_things()
216         for thing in visible_things:
217             send_thing(offset, thing)
218             if hasattr(thing, 'health'):
219                 self.io.send('THING_HEALTH %s %s' % (thing.id_,
220                                                      thing.health))
221         if len(self.world.player.inventory) > 0:
222             self.io.send('PLAYER_INVENTORY %s' %
223                          ','.join([str(i) for i in self.world.player.inventory]))
224         else:
225             self.io.send('PLAYER_INVENTORY ,')
226         for id_ in self.world.player.inventory:
227             thing = self.world.get_thing(id_)
228             send_thing(offset, thing)
229         self.io.send('GAME_STATE_COMPLETE')
230
231     def proceed(self):
232         """Send turn finish signal, run game world, send new world data.
233
234         First sends 'TURN_FINISHED' message, then runs game world
235         until new player input is needed, then sends game state.
236         """
237         self.io.send('TURN_FINISHED ' + str(self.world.turn))
238         self.world.proceed_to_next_player_turn()
239         msg = str(self.world.player._last_task_result)
240         self.io.send('LAST_PLAYER_TASK_RESULT ' + quote(msg))
241         self.send_gamestate()
242
243     def get_command(self, command_name):
244
245         def partial_with_attrs(f, *args, **kwargs):
246             from functools import partial
247             p = partial(f, *args, **kwargs)
248             p.__dict__.update(f.__dict__)
249             return p
250
251         def cmd_TASK_colon(task_name, game, *args):
252             if not game.world.player_is_alive:
253                 raise GameError('You are dead.')
254             game.world.player.set_task(task_name, args)
255             game.proceed()
256
257         def cmd_SET_TASK_colon(task_name, game, thing_id, todo, *args):
258             t = game.world.get_thing(thing_id, False)
259             if t is None:
260                 raise ArgError('No such Thing.')
261             task_class = game.tasks[task_name]
262             t.task = task_class(t, args)
263             t.task.todo = todo
264
265         def task_prefixed(command_name, task_prefix, task_command,
266                           argtypes_prefix=None):
267             if command_name[:len(task_prefix)] == task_prefix:
268                 task_name = command_name[len(task_prefix):]
269                 if task_name in self.tasks:
270                     f = partial_with_attrs(task_command, task_name, self)
271                     task = self.tasks[task_name]
272                     if argtypes_prefix:
273                         f.argtypes = argtypes_prefix + ' ' + task.argtypes
274                     else:
275                         f.argtypes = task.argtypes
276                     return f
277             return None
278
279         command = task_prefixed(command_name, 'TASK:', cmd_TASK_colon)
280         if command:
281             return command
282         command = task_prefixed(command_name, 'SET_TASK:', cmd_SET_TASK_colon,
283                                 'int:nonneg int:nonneg ')
284         if command:
285             return command
286         if command_name in self.commands:
287             f = partial_with_attrs(self.commands[command_name], self)
288             return f
289         return None