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