1 from plomrogue.tasks import Task_WAIT, Task_MOVE
2 from plomrogue.errors import GameError, ArgError
3 from plomrogue.commands import (cmd_GEN_WORLD, cmd_GET_GAMESTATE, cmd_MAP,
4 cmd_MAP, cmd_THING_TYPE, cmd_THING_POS,
5 cmd_TERRAIN_LINE, cmd_PLAYER_ID, cmd_TURN,
6 cmd_SWITCH_PLAYER, cmd_SAVE)
7 from plomrogue.mapping import MapHex
8 from plomrogue.parser import Parser
9 from plomrogue.io import GameIO
10 from plomrogue.misc import quote, stringify_yx
16 def __init__(self, world, id_, type_='?', position=[0,0]):
20 self.position = position
24 class Thing(ThingBase):
26 def __init__(self, *args, **kwargs):
27 super().__init__(*args, **kwargs)
29 self._last_task_result = None
32 def move_towards_target(self, target):
33 dijkstra_map = type(self.world.map_)(self.world.map_.size)
35 dijkstra_map.terrain = [n_max for i in range(dijkstra_map.size_i)]
36 dijkstra_map[target] = 0
38 visible_map = self.get_visible_map()
41 for pos in dijkstra_map:
42 if visible_map[pos] != '.':
44 neighbors = dijkstra_map.get_neighbors(tuple(pos))
45 for direction in neighbors:
46 yx = neighbors[direction]
47 if yx is not None and dijkstra_map[yx] < dijkstra_map[pos] - 1:
48 dijkstra_map[pos] = dijkstra_map[yx] + 1
50 #with open('log', 'a') as f:
51 # f.write('---------------------------------\n')
52 # for y, line in dijkstra_map.lines():
61 neighbors = dijkstra_map.get_neighbors(tuple(self.position))
63 #print('DEBUG', self.position, neighbors)
64 #dirs = dijkstra_map.get_directions()
65 #print('DEBUG dirs', dirs)
66 #print('DEBUG neighbors', neighbors)
68 #for pos in neighbors:
70 # debug_scores += [9000]
72 # debug_scores += [dijkstra_map[pos]]
73 #print('DEBUG debug_scores', debug_scores)
74 target_direction = None
75 for direction in neighbors:
76 yx = neighbors[direction]
78 n_new = dijkstra_map[yx]
81 target_direction = direction
82 #print('DEBUG result', direction)
84 self.set_task('MOVE', (target_direction,))
86 def decide_task(self):
87 # TODO: Check if monster can follow player too well (even when they should lose them)
88 visible_things = self.get_visible_things()
90 for t in visible_things:
91 if t.type_ == 'human':
94 if target is not None:
96 self.move_towards_target(target)
100 self.set_task('WAIT')
102 def set_task(self, task_name, args=()):
103 task_class = self.world.game.tasks[task_name]
104 self.task = task_class(self, args)
105 self.task.check() # will throw GameError if necessary
107 def proceed(self, is_AI=True):
108 """Further the thing in its tasks.
110 Decrements .task.todo; if it thus falls to <= 0, enacts method
111 whose name is 'task_' + self.task.name and sets .task =
112 None. If is_AI, calls .decide_task to decide a self.task.
114 Before doing anything, ensures an empty map visibility stencil
115 and checks that task is still possible, and aborts it
116 otherwise (for AI things, decides a new task).
122 except GameError as e:
124 self._last_task_result = e
129 self.set_task('WAIT')
132 if self.task.todo <= 0:
133 self._last_task_result = self.task.do()
135 if is_AI and self.task is None:
139 self.set_task('WAIT')
141 def get_stencil(self):
142 if self._stencil is not None:
144 self._stencil = self.world.map_.get_fov_map(self.position)
147 def get_visible_map(self):
148 stencil = self.get_stencil()
149 m = self.world.map_.new_from_shape(' ')
151 if stencil[pos] == '.':
152 m[pos] = self.world.map_[pos]
155 def get_visible_things(self):
156 stencil = self.get_stencil()
158 for thing in self.world.things:
159 if stencil[thing.position] == '.':
160 visible_things += [thing]
161 return visible_things
167 def __init__(self, game):
172 def get_thing(self, id_, create_unfound=True):
173 for thing in self.things:
177 t = self.game.thing_type(self, id_)
184 class World(WorldBase):
186 def __init__(self, *args, **kwargs):
187 super().__init__(*args, **kwargs)
190 def new_map(self, yx):
191 self.map_ = self.game.map_type(yx)
193 def proceed_to_next_player_turn(self):
194 """Run game world turns until player can decide their next step.
196 Iterates through all non-player things, on each step
197 furthering them in their tasks (and letting them decide new
198 ones if they finish). The iteration order is: first all things
199 that come after the player in the world things list, then
200 (after incrementing the world turn) all that come before the
201 player; then the player's .proceed() is run, and if it does
202 not finish his task, the loop starts at the beginning. Once
203 the player's task is finished, the loop breaks.
206 player = self.get_player()
207 player_i = self.things.index(player)
208 for thing in self.things[player_i+1:]:
211 for thing in self.things[:player_i]:
213 player.proceed(is_AI=False)
214 if player.task is None:
217 def get_player(self):
218 return self.get_thing(self.player_id)
220 def make_new(self, yx, seed):
225 for pos in self.map_:
226 if 0 in pos or (yx[0] - 1) == pos[0] or (yx[1] - 1) == pos[1]:
229 self.map_[pos] = random.choice(('.', '.', '.', '.', 'x'))
230 player = self.game.thing_type(self, 0)
231 player.type_ = 'human'
232 player.position = [random.randint(0, yx[0] -1),
233 random.randint(0, yx[1] - 1)]
234 npc = self.game.thing_type(self, 1)
235 npc.type_ = 'monster'
236 npc.position = [random.randint(0, yx[0] -1),
237 random.randint(0, yx[1] -1)]
238 self.things = [player, npc]
245 def __init__(self, game_file_name):
246 self.io = GameIO(game_file_name, self)
247 self.map_type = MapHex
248 self.tasks = {'WAIT': Task_WAIT, 'MOVE': Task_MOVE}
249 self.commands = {'GEN_WORLD': cmd_GEN_WORLD,
250 'GET_GAMESTATE': cmd_GET_GAMESTATE,
252 'THING_TYPE': cmd_THING_TYPE,
253 'THING_POS': cmd_THING_POS,
254 'TERRAIN_LINE': cmd_TERRAIN_LINE,
255 'PLAYER_ID': cmd_PLAYER_ID,
257 'SWITCH_PLAYER': cmd_SWITCH_PLAYER,
259 self.world_type = World
260 self.world = self.world_type(self)
261 self.thing_type = Thing
263 def get_string_options(self, string_option_type):
264 if string_option_type == 'direction':
265 return self.world.map_.get_directions()
268 def send_gamestate(self, connection_id=None):
269 """Send out game state data relevant to clients."""
271 self.io.send('TURN ' + str(self.world.turn))
272 self.io.send('MAP ' + stringify_yx(self.world.map_.size))
273 visible_map = self.world.get_player().get_visible_map()
274 for y, line in visible_map.lines():
275 self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, quote(line)))
276 visible_things = self.world.get_player().get_visible_things()
277 for thing in visible_things:
278 self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
279 self.io.send('THING_POS %s %s' % (thing.id_,
280 stringify_yx(thing.position)))
281 player = self.world.get_player()
282 self.io.send('PLAYER_POS %s' % (stringify_yx(player.position)))
283 self.io.send('GAME_STATE_COMPLETE')
286 """Send turn finish signal, run game world, send new world data.
288 First sends 'TURN_FINISHED' message, then runs game world
289 until new player input is needed, then sends game state.
291 self.io.send('TURN_FINISHED ' + str(self.world.turn))
292 self.world.proceed_to_next_player_turn()
293 msg = str(self.world.get_player()._last_task_result)
294 self.io.send('LAST_PLAYER_TASK_RESULT ' + quote(msg))
295 self.send_gamestate()
297 def get_command(self, command_name):
299 def partial_with_attrs(f, *args, **kwargs):
300 from functools import partial
301 p = partial(f, *args, **kwargs)
302 p.__dict__.update(f.__dict__)
305 def cmd_TASK_colon(task_name, game, *args):
306 game.world.get_player().set_task(task_name, args)
309 def cmd_SET_TASK_colon(task_name, game, thing_id, todo, *args):
310 t = game.world.get_thing(thing_id, False)
312 raise ArgError('No such Thing.')
313 task_class = game.tasks[task_name]
314 t.task = task_class(t, args)
317 def task_prefixed(command_name, task_prefix, task_command,
318 argtypes_prefix=None):
319 if command_name[:len(task_prefix)] == task_prefix:
320 task_name = command_name[len(task_prefix):]
321 if task_name in self.tasks:
322 f = partial_with_attrs(task_command, task_name, self)
323 task = self.tasks[task_name]
325 f.argtypes = argtypes_prefix + ' ' + task.argtypes
327 f.argtypes = task.argtypes
331 command = task_prefixed(command_name, 'TASK:', cmd_TASK_colon)
334 command = task_prefixed(command_name, 'SET_TASK:', cmd_SET_TASK_colon,
335 'int:nonneg int:nonneg ')
338 if command_name in self.commands:
339 f = partial_with_attrs(self.commands[command_name], self)