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]
72 def __init__(self, thing, name, args=()):
79 if self.name == 'MOVE':
80 test_pos = self.thing.world.map_.move(self.thing.position, self.args[0])
81 if self.thing.world.map_[test_pos] != '.':
82 raise GameError(str(self.thing.id_) +
83 ' would move into illegal terrain')
84 for t in self.thing.world.things:
85 if t.position == test_pos:
86 raise GameError(str(self.thing.id_) +
87 ' would move into other thing')
89 def get_args_string(self):
92 if type(arg) == 'string':
93 stringed_args += [server_.io.quote(arg)]
95 raise GameError('stringifying arg type not implemented')
96 return ' '.join(stringed_args)
99 class Thing(game_common.Thing):
101 def __init__(self, *args, **kwargs):
102 super().__init__(*args, **kwargs)
103 self.task = Task(self, 'WAIT')
104 self._last_task_result = None
110 def task_MOVE(self, direction):
111 self.position = self.world.map_.move(self.position, direction)
113 task_MOVE.argtypes = 'string:direction'
115 def move_towards_target(self, target):
116 dijkstra_map = type(self.world.map_)(self.world.map_.size)
118 dijkstra_map.terrain = [n_max for i in range(dijkstra_map.size_i)]
119 dijkstra_map[target] = 0
121 stencil = self.get_stencil()
124 for pos in dijkstra_map:
125 if stencil[pos] != '.':
127 neighbors = dijkstra_map.get_neighbors(tuple(pos))
128 for direction in neighbors:
129 yx = neighbors[direction]
130 if yx is not None and dijkstra_map[yx] < dijkstra_map[pos] - 1:
131 dijkstra_map[pos] = dijkstra_map[yx] + 1
133 #with open('log', 'a') as f:
134 # f.write('---------------------------------\n')
135 # for y, line in dijkstra_map.lines():
144 neighbors = dijkstra_map.get_neighbors(tuple(self.position))
146 #print('DEBUG', self.position, neighbors)
147 #dirs = dijkstra_map.get_directions()
148 #print('DEBUG dirs', dirs)
149 #print('DEBUG neighbors', neighbors)
151 #for pos in neighbors:
153 # debug_scores += [9000]
155 # debug_scores += [dijkstra_map[pos]]
156 #print('DEBUG debug_scores', debug_scores)
157 target_direction = None
158 for direction in neighbors:
159 yx = neighbors[direction]
161 n_new = dijkstra_map[yx]
164 target_direction = direction
165 #print('DEBUG result', direction)
167 self.set_task('MOVE', (target_direction,))
168 #self.world.game.io.send('would move ' + direction)
170 def decide_task(self):
171 visible_things = self.get_visible_things()
173 for t in visible_things:
174 if t.type_ == 'human':
177 if target is not None:
179 self.move_towards_target(target)
183 self.set_task('WAIT')
186 def set_task(self, task_name, args=()):
187 self.task = Task(self, task_name, args)
188 self.task.check() # will throw GameError if necessary
190 def proceed(self, is_AI=True):
191 """Further the thing in its tasks.
193 Decrements .task.todo; if it thus falls to <= 0, enacts method
194 whose name is 'task_' + self.task.name and sets .task =
195 None. If is_AI, calls .decide_task to decide a self.task.
197 Before doing anything, ensures an empty map visibility stencil
198 and checks that task is still possible, and aborts it
199 otherwise (for AI things, decides a new task).
205 except GameError as e:
207 self._last_task_result = e
212 self.set_task('WAIT')
215 if self.task.todo <= 0:
216 task = getattr(self, 'task_' + self.task.name)
217 self._last_task_result = task(*self.task.args)
219 if is_AI and self.task is None:
223 self.set_task('WAIT')
225 def get_stencil(self):
226 if self._stencil is not None:
228 self._stencil = self.world.map_.get_fov_map(self.position)
231 def get_visible_map(self):
232 stencil = self.get_stencil()
233 m = self.world.map_.new_from_shape(' ')
235 if stencil[pos] == '.':
236 m[pos] = self.world.map_[pos]
239 def get_visible_things(self):
240 stencil = self.get_stencil()
242 for thing in self.world.things:
243 if stencil[thing.position] == '.':
244 visible_things += [thing]
245 return visible_things
249 """Calculate n-th Fibonacci number. Very inefficiently."""
253 return fib(n-1) + fib(n-2)
256 class Game(game_common.CommonCommandsMixin):
258 def __init__(self, game_file_name):
259 self.map_manager = server_.map_.map_manager
260 self.world = World(self)
261 self.io = server_.io.GameIO(game_file_name, self)
262 # self.pool and self.pool_result are currently only needed by the FIB
263 # command and the demo of a parallelized game loop in cmd_inc_p.
264 from multiprocessing import Pool
266 self.pool_result = None
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 ' + self.world.map_.geometry +\
273 ' ' + server_.io.stringify_yx(self.world.map_.size))
274 visible_map = self.world.get_player().get_visible_map()
275 for y, line in visible_map.lines():
276 self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, server_.io.quote(line)))
277 visible_things = self.world.get_player().get_visible_things()
278 for thing in visible_things:
279 self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
280 self.io.send('THING_POS %s %s' % (thing.id_,
281 server_.io.stringify_yx(thing.position)))
282 player = self.world.get_player()
283 self.io.send('PLAYER_POS %s' % (server_.io.stringify_yx(player.position)))
284 self.io.send('GAME_STATE_COMPLETE')
287 """Send turn finish signal, run game world, send new world data.
289 First sends 'TURN_FINISHED' message, then runs game world
290 until new player input is needed, then sends game state.
292 self.io.send('TURN_FINISHED ' + str(self.world.turn))
293 self.world.proceed_to_next_player_turn()
294 msg = str(self.world.get_player()._last_task_result)
295 self.io.send('LAST_PLAYER_TASK_RESULT ' + server_.io.quote(msg))
296 self.send_gamestate()
298 def cmd_FIB(self, numbers, connection_id):
299 """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
301 Numbers are calculated in parallel as far as possible, using fib().
302 A 'CALCULATING …' message is sent to caller before the result.
304 self.io.send('CALCULATING …', connection_id)
305 results = self.pool.map(fib, numbers)
306 reply = ' '.join([str(r) for r in results])
307 self.io.send(reply, connection_id)
308 cmd_FIB.argtypes = 'seq:int:nonneg'
310 def cmd_INC_P(self, connection_id):
311 """Increment world.turn, send game turn data to everyone.
313 To simulate game processing waiting times, a one second delay
314 between TURN_FINISHED and TURN occurs; after TURN, some
315 expensive calculations are started as pool processes that need
316 to be finished until a further INC finishes the turn.
318 This is just a demo structure for how the game loop could work
319 when parallelized. One might imagine a two-step game turn,
320 with a non-action step determining actor tasks (the AI
321 determinations would take the place of the fib calculations
322 here), and an action step wherein these tasks are performed
323 (where now sleep(1) is).
326 from time import sleep
327 if self.pool_result is not None:
328 self.pool_result.wait()
329 self.io.send('TURN_FINISHED ' + str(self.world.turn))
332 self.send_gamestate()
333 self.pool_result = self.pool.map_async(fib, (35, 35))
335 def cmd_SWITCH_PLAYER(self):
336 player = self.world.get_player()
337 player.set_task('WAIT')
338 thing_ids = [t.id_ for t in self.world.things]
339 player_index = thing_ids.index(player.id_)
340 if player_index == len(thing_ids) - 1:
341 self.world.player_id = thing_ids[0]
343 self.world.player_id = thing_ids[player_index + 1]
346 def cmd_GET_GAMESTATE(self, connection_id):
347 """Send game state to caller."""
348 self.send_gamestate(connection_id)
350 def cmd_ECHO(self, msg, connection_id):
351 """Send msg to caller."""
352 self.io.send(msg, connection_id)
353 cmd_ECHO.argtypes = 'string'
355 def cmd_ALL(self, msg, connection_id):
356 """Send msg to all clients."""
358 cmd_ALL.argtypes = 'string'
360 def cmd_TERRAIN_LINE(self, y, terrain_line):
361 self.world.map_.set_line(y, terrain_line)
362 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'
364 def cmd_GEN_WORLD(self, geometry, yx, seed):
365 self.world.make_new(geometry, yx, seed)
366 cmd_GEN_WORLD.argtypes = 'string:geometry yx_tuple:pos string'
368 def get_command_signature(self, command_name):
369 from functools import partial
371 def cmd_TASK_colon(task_name, *args):
372 self.world.get_player().set_task(task_name, args)
375 def cmd_SET_TASK_colon(task_name, thing_id, todo, *args):
376 t = self.world.get_thing(thing_id, False)
378 raiseArgError('No such Thing.')
379 t.task = Task(t, task_name, args)
382 def task_prefixed(command_name, task_prefix, task_command,
386 if command_name[:len(task_prefix)] == task_prefix:
387 task_name = command_name[len(task_prefix):]
388 task_method_candidate = 'task_' + task_name
389 if hasattr(Thing, task_method_candidate):
390 method = partial(task_command, task_name)
391 task_method = getattr(Thing, task_method_candidate)
392 if hasattr(task_method, 'argtypes'):
393 argtypes = task_method.argtypes
394 if method is not None:
395 return method, argtypes_prefix + argtypes
396 return None, argtypes
398 method, argtypes = task_prefixed(command_name, 'TASK:', cmd_TASK_colon)
400 return method, argtypes
401 method, argtypes = task_prefixed(command_name, 'SET_TASK:',
403 'int:nonneg int:nonneg')
405 return method, argtypes
406 method_candidate = 'cmd_' + command_name
407 if hasattr(self, method_candidate):
408 method = getattr(self, method_candidate)
409 if hasattr(method, 'argtypes'):
410 argtypes = method.argtypes
411 return method, argtypes
413 def get_string_options(self, string_option_type):
414 if string_option_type == 'geometry':
415 return self.map_manager.get_map_geometries()
416 elif string_option_type == 'direction':
417 return self.world.map_.get_directions()
420 def cmd_PLAYER_ID(self, id_):
421 # TODO: test whether valid thing ID
422 self.world.player_id = id_
423 cmd_PLAYER_ID.argtypes = 'int:nonneg'
425 def cmd_TURN(self, n):
427 cmd_TURN.argtypes = 'int:nonneg'
434 save_file_name = self.io.game_file_name + '.save'
435 with open(save_file_name, 'w') as f:
436 write(f, 'TURN %s' % self.world.turn)
437 write(f, 'MAP ' + self.world.map_.geometry + ' ' + server_.io.stringify_yx(self.world.map_.size))
438 for y, line in self.world.map_.lines():
439 write(f, 'TERRAIN_LINE %5s %s' % (y, server_.io.quote(line)))
440 for thing in self.world.things:
441 write(f, 'THING_TYPE %s %s' % (thing.id_, thing.type_))
442 write(f, 'THING_POS %s %s' % (thing.id_,
443 server_.io.stringify_yx(thing.position)))
446 task_args = task.get_args_string()
447 write(f, 'SET_TASK:%s %s %s %s' % (task.name, thing.id_,
448 task.todo, task_args))
449 write(f, 'PLAYER_ID %s' % self.world.player_id)
450 cmd_SAVE.dont_save = True