6 from parser import ArgError
9 class GameError(Exception):
13 class World(game_common.World):
15 def __init__(self, game):
19 # use extended local classes
22 def proceed_to_next_player_turn(self):
23 """Run game world turns until player can decide their next step.
25 Iterates through all non-player things, on each step
26 furthering them in their tasks (and letting them decide new
27 ones if they finish). The iteration order is: first all things
28 that come after the player in the world things list, then
29 (after incrementing the world turn) all that come before the
30 player; then the player's .proceed() is run, and if it does
31 not finish his task, the loop starts at the beginning. Once
32 the player's task is finished, the loop breaks.
35 player = self.get_player()
36 player_i = self.things.index(player)
37 for thing in self.things[player_i+1:]:
40 for thing in self.things[:player_i]:
42 player.proceed(is_AI=False)
43 if player.task is None:
47 return self.get_thing(self.player_id)
49 def make_new(self, geometry, yx, seed):
53 self.new_map(geometry, yx)
55 if 0 in pos or (yx[0] - 1) == pos[0] or (yx[1] - 1) == pos[1]:
58 self.map_[pos] = random.choice(('.', '.', '.', '.', 'x'))
59 player = self.Thing(self, 0)
60 player.type_ = 'human'
61 player.position = [random.randint(0, yx[0] -1),
62 random.randint(0, yx[1] - 1)]
63 npc = self.Thing(self, 1)
65 npc.position = [random.randint(0, yx[0] -1),
66 random.randint(0, yx[1] -1)]
67 self.things = [player, npc]
73 def __init__(self, thing, args=()):
81 class_name = self.__class__.__name__
82 return class_name[len(prefix):]
87 def get_args_string(self):
91 stringed_args += [server_.io.quote(arg)]
93 raise GameError('stringifying arg type not implemented')
94 return ' '.join(stringed_args)
98 class Task_WAIT(Task):
105 class Task_MOVE(Task):
106 argtypes = 'string:direction'
109 test_pos = self.thing.world.map_.move(self.thing.position, self.args[0])
110 if self.thing.world.map_[test_pos] != '.':
111 raise GameError('%s would move into illegal terrain' % self.thing.id_)
112 for t in self.thing.world.things:
113 if t.position == test_pos:
114 raise GameError('%s would move into other thing' % self.thing.id_)
117 self.thing.position = self.thing.world.map_.move(self.thing.position,
123 class Thing(game_common.Thing):
125 def __init__(self, *args, **kwargs):
126 super().__init__(*args, **kwargs)
127 self.task = Task_WAIT(self)
128 self._last_task_result = None
131 def move_towards_target(self, target):
132 dijkstra_map = type(self.world.map_)(self.world.map_.size)
134 dijkstra_map.terrain = [n_max for i in range(dijkstra_map.size_i)]
135 dijkstra_map[target] = 0
137 visible_map = self.get_visible_map()
140 for pos in dijkstra_map:
141 if visible_map[pos] != '.':
143 neighbors = dijkstra_map.get_neighbors(tuple(pos))
144 for direction in neighbors:
145 yx = neighbors[direction]
146 if yx is not None and dijkstra_map[yx] < dijkstra_map[pos] - 1:
147 dijkstra_map[pos] = dijkstra_map[yx] + 1
149 #with open('log', 'a') as f:
150 # f.write('---------------------------------\n')
151 # for y, line in dijkstra_map.lines():
160 neighbors = dijkstra_map.get_neighbors(tuple(self.position))
162 #print('DEBUG', self.position, neighbors)
163 #dirs = dijkstra_map.get_directions()
164 #print('DEBUG dirs', dirs)
165 #print('DEBUG neighbors', neighbors)
167 #for pos in neighbors:
169 # debug_scores += [9000]
171 # debug_scores += [dijkstra_map[pos]]
172 #print('DEBUG debug_scores', debug_scores)
173 target_direction = None
174 for direction in neighbors:
175 yx = neighbors[direction]
177 n_new = dijkstra_map[yx]
180 target_direction = direction
181 #print('DEBUG result', direction)
183 self.set_task('MOVE', (target_direction,))
185 def decide_task(self):
186 # TODO: Check if monster can follow player too well (even when they should lose them)
187 visible_things = self.get_visible_things()
189 for t in visible_things:
190 if t.type_ == 'human':
193 if target is not None:
195 self.move_towards_target(target)
199 self.set_task('WAIT')
202 def set_task(self, task_name, args=()):
203 task_class = globals()['Task_' + task_name]
204 self.task = task_class(self, args)
205 self.task.check() # will throw GameError if necessary
207 def proceed(self, is_AI=True):
208 """Further the thing in its tasks.
210 Decrements .task.todo; if it thus falls to <= 0, enacts method
211 whose name is 'task_' + self.task.name and sets .task =
212 None. If is_AI, calls .decide_task to decide a self.task.
214 Before doing anything, ensures an empty map visibility stencil
215 and checks that task is still possible, and aborts it
216 otherwise (for AI things, decides a new task).
222 except GameError as e:
224 self._last_task_result = e
229 self.set_task('WAIT')
232 if self.task.todo <= 0:
233 self._last_task_result = self.task.do()
235 if is_AI and self.task is None:
239 self.set_task('WAIT')
241 def get_stencil(self):
242 if self._stencil is not None:
244 self._stencil = self.world.map_.get_fov_map(self.position)
247 def get_visible_map(self):
248 stencil = self.get_stencil()
249 m = self.world.map_.new_from_shape(' ')
251 if stencil[pos] == '.':
252 m[pos] = self.world.map_[pos]
255 def get_visible_things(self):
256 stencil = self.get_stencil()
258 for thing in self.world.things:
259 if stencil[thing.position] == '.':
260 visible_things += [thing]
261 return visible_things
265 """Calculate n-th Fibonacci number. Very inefficiently."""
269 return fib(n-1) + fib(n-2)
272 class Game(game_common.CommonCommandsMixin):
274 def __init__(self, game_file_name):
275 self.map_manager = server_.map_.map_manager
276 self.world = World(self)
277 self.io = server_.io.GameIO(game_file_name, self)
278 # self.pool and self.pool_result are currently only needed by the FIB
279 # command and the demo of a parallelized game loop in cmd_inc_p.
280 from multiprocessing import Pool
282 self.pool_result = None
284 def send_gamestate(self, connection_id=None):
285 """Send out game state data relevant to clients."""
287 self.io.send('TURN ' + str(self.world.turn))
288 self.io.send('MAP ' + self.world.map_.geometry +\
289 ' ' + server_.io.stringify_yx(self.world.map_.size))
290 visible_map = self.world.get_player().get_visible_map()
291 for y, line in visible_map.lines():
292 self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, server_.io.quote(line)))
293 visible_things = self.world.get_player().get_visible_things()
294 for thing in visible_things:
295 self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
296 self.io.send('THING_POS %s %s' % (thing.id_,
297 server_.io.stringify_yx(thing.position)))
298 player = self.world.get_player()
299 self.io.send('PLAYER_POS %s' % (server_.io.stringify_yx(player.position)))
300 self.io.send('GAME_STATE_COMPLETE')
303 """Send turn finish signal, run game world, send new world data.
305 First sends 'TURN_FINISHED' message, then runs game world
306 until new player input is needed, then sends game state.
308 self.io.send('TURN_FINISHED ' + str(self.world.turn))
309 self.world.proceed_to_next_player_turn()
310 msg = str(self.world.get_player()._last_task_result)
311 self.io.send('LAST_PLAYER_TASK_RESULT ' + server_.io.quote(msg))
312 self.send_gamestate()
314 def cmd_FIB(self, numbers, connection_id):
315 """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
317 Numbers are calculated in parallel as far as possible, using fib().
318 A 'CALCULATING …' message is sent to caller before the result.
320 self.io.send('CALCULATING …', connection_id)
321 results = self.pool.map(fib, numbers)
322 reply = ' '.join([str(r) for r in results])
323 self.io.send(reply, connection_id)
324 cmd_FIB.argtypes = 'seq:int:nonneg'
326 def cmd_INC_P(self, connection_id):
327 """Increment world.turn, send game turn data to everyone.
329 To simulate game processing waiting times, a one second delay
330 between TURN_FINISHED and TURN occurs; after TURN, some
331 expensive calculations are started as pool processes that need
332 to be finished until a further INC finishes the turn.
334 This is just a demo structure for how the game loop could work
335 when parallelized. One might imagine a two-step game turn,
336 with a non-action step determining actor tasks (the AI
337 determinations would take the place of the fib calculations
338 here), and an action step wherein these tasks are performed
339 (where now sleep(1) is).
342 from time import sleep
343 if self.pool_result is not None:
344 self.pool_result.wait()
345 self.io.send('TURN_FINISHED ' + str(self.world.turn))
348 self.send_gamestate()
349 self.pool_result = self.pool.map_async(fib, (35, 35))
351 def cmd_SWITCH_PLAYER(self):
352 player = self.world.get_player()
353 player.set_task('WAIT')
354 thing_ids = [t.id_ for t in self.world.things]
355 player_index = thing_ids.index(player.id_)
356 if player_index == len(thing_ids) - 1:
357 self.world.player_id = thing_ids[0]
359 self.world.player_id = thing_ids[player_index + 1]
362 def cmd_GET_GAMESTATE(self, connection_id):
363 """Send game state to caller."""
364 self.send_gamestate(connection_id)
366 def cmd_ECHO(self, msg, connection_id):
367 """Send msg to caller."""
368 self.io.send(msg, connection_id)
369 cmd_ECHO.argtypes = 'string'
371 def cmd_ALL(self, msg, connection_id):
372 """Send msg to all clients."""
374 cmd_ALL.argtypes = 'string'
376 def cmd_TERRAIN_LINE(self, y, terrain_line):
377 self.world.map_.set_line(y, terrain_line)
378 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'
380 def cmd_GEN_WORLD(self, geometry, yx, seed):
381 self.world.make_new(geometry, yx, seed)
382 cmd_GEN_WORLD.argtypes = 'string:geometry yx_tuple:pos string'
384 def get_command_signature(self, command_name):
385 from functools import partial
387 def cmd_TASK_colon(task_name, *args):
388 self.world.get_player().set_task(task_name, args)
391 def cmd_SET_TASK_colon(task_name, thing_id, todo, *args):
392 t = self.world.get_thing(thing_id, False)
394 raiseArgError('No such Thing.')
395 task_class = globals()['Task_' + task_name]
396 t.task = task_class(t, args)
399 def task_prefixed(command_name, task_prefix, task_command,
403 if command_name[:len(task_prefix)] == task_prefix:
404 task_name = command_name[len(task_prefix):]
405 task_class_candidate = 'Task_' + task_name
406 if task_class_candidate in globals():
407 func = partial(task_command, task_name)
408 task_class = globals()[task_class_candidate]
409 argtypes = task_class.argtypes
411 return func, argtypes_prefix + argtypes
412 return None, argtypes
414 func, argtypes = task_prefixed(command_name, 'TASK:', cmd_TASK_colon)
416 return func, argtypes
417 func, argtypes = task_prefixed(command_name, 'SET_TASK:',
419 'int:nonneg int:nonneg ')
421 return func, argtypes
422 func_candidate = 'cmd_' + command_name
423 if hasattr(self, func_candidate):
424 func = getattr(self, func_candidate)
425 if hasattr(func, 'argtypes'):
426 argtypes = func.argtypes
427 return func, argtypes
429 def get_string_options(self, string_option_type):
430 if string_option_type == 'geometry':
431 return self.map_manager.get_map_geometries()
432 elif string_option_type == 'direction':
433 return self.world.map_.get_directions()
436 def cmd_PLAYER_ID(self, id_):
437 # TODO: test whether valid thing ID
438 self.world.player_id = id_
439 cmd_PLAYER_ID.argtypes = 'int:nonneg'
441 def cmd_TURN(self, n):
443 cmd_TURN.argtypes = 'int:nonneg'
450 save_file_name = self.io.game_file_name + '.save'
451 with open(save_file_name, 'w') as f:
452 write(f, 'TURN %s' % self.world.turn)
453 write(f, 'MAP ' + self.world.map_.geometry + ' ' + server_.io.stringify_yx(self.world.map_.size))
454 for y, line in self.world.map_.lines():
455 write(f, 'TERRAIN_LINE %5s %s' % (y, server_.io.quote(line)))
456 for thing in self.world.things:
457 write(f, 'THING_TYPE %s %s' % (thing.id_, thing.type_))
458 write(f, 'THING_POS %s %s' % (thing.id_,
459 server_.io.stringify_yx(thing.position)))
462 task_args = task.get_args_string()
463 write(f, 'SET_TASK:%s %s %s %s' % (task.name, thing.id_,
464 task.todo, task_args))
465 write(f, 'PLAYER_ID %s' % self.world.player_id)
466 cmd_SAVE.dont_save = True