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
123 for pos in dijkstra_map:
124 if self.world.map_[pos] != '.':
126 neighbors = dijkstra_map.get_neighbors(pos)
128 if yx is not None and dijkstra_map[yx] < dijkstra_map[pos] - 1:
129 dijkstra_map[pos] = dijkstra_map[yx] + 1
131 #with open('log', 'a') as f:
132 # f.write('---------------------------------\n')
133 # for y, line in dijkstra_map.lines():
142 neighbors = dijkstra_map.get_neighbors(self.position)
144 dirs = dijkstra_map.get_directions()
145 #print('DEBUG dirs', dirs)
146 #print('DEBUG neighbors', neighbors)
148 #for pos in neighbors:
150 # debug_scores += [9000]
152 # debug_scores += [dijkstra_map[pos]]
153 #print('DEBUG debug_scores', debug_scores)
155 for i_dir in range(len(neighbors)):
156 pos = neighbors[i_dir]
157 if pos is not None and dijkstra_map[pos] < n:
158 n = dijkstra_map[pos]
159 direction = dirs[i_dir]
160 #print('DEBUG result', direction)
162 self.set_task('MOVE', (direction,))
163 #self.world.game.io.send('would move ' + direction)
165 def decide_task(self):
166 visible_things = self.get_visible_things()
168 for t in visible_things:
169 if t.type_ == 'human':
172 if target is not None:
174 self.move_towards_target(target)
178 self.set_task('WAIT')
181 def set_task(self, task_name, args=()):
182 self.task = Task(self, task_name, args)
183 self.task.check() # will throw GameError if necessary
185 def proceed(self, is_AI=True):
186 """Further the thing in its tasks.
188 Decrements .task.todo; if it thus falls to <= 0, enacts method
189 whose name is 'task_' + self.task.name and sets .task =
190 None. If is_AI, calls .decide_task to decide a self.task.
192 Before doing anything, ensures an empty map visibility stencil
193 and checks that task is still possible, and aborts it
194 otherwise (for AI things, decides a new task).
200 except GameError as e:
202 self._last_task_result = e
207 self.set_task('WAIT')
210 if self.task.todo <= 0:
211 task = getattr(self, 'task_' + self.task.name)
212 self._last_task_result = task(*self.task.args)
214 if is_AI and self.task is None:
218 self.set_task('WAIT')
220 def get_stencil(self):
221 if self._stencil is not None:
223 self._stencil = self.world.map_.get_fov_map(self.position)
226 def get_visible_map(self):
227 stencil = self.get_stencil()
228 m = self.world.map_.new_from_shape(' ')
230 if stencil[pos] == '.':
231 m[pos] = self.world.map_[pos]
234 def get_visible_things(self):
235 stencil = self.get_stencil()
237 for thing in self.world.things:
238 if stencil[thing.position] == '.':
239 visible_things += [thing]
240 return visible_things
244 """Calculate n-th Fibonacci number. Very inefficiently."""
248 return fib(n-1) + fib(n-2)
251 class Game(game_common.CommonCommandsMixin):
253 def __init__(self, game_file_name):
254 self.map_manager = server_.map_.map_manager
255 self.world = World(self)
256 self.io = server_.io.GameIO(game_file_name, self)
257 # self.pool and self.pool_result are currently only needed by the FIB
258 # command and the demo of a parallelized game loop in cmd_inc_p.
259 from multiprocessing import Pool
261 self.pool_result = None
263 def send_gamestate(self, connection_id=None):
264 """Send out game state data relevant to clients."""
266 self.io.send('TURN ' + str(self.world.turn))
267 self.io.send('MAP ' + self.world.map_.geometry +\
268 ' ' + server_.io.stringify_yx(self.world.map_.size))
269 visible_map = self.world.get_player().get_visible_map()
270 for y, line in visible_map.lines():
271 self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, server_.io.quote(line)))
272 visible_things = self.world.get_player().get_visible_things()
273 for thing in visible_things:
274 self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
275 self.io.send('THING_POS %s %s' % (thing.id_,
276 server_.io.stringify_yx(thing.position)))
277 player = self.world.get_player()
278 self.io.send('PLAYER_POS %s' % (server_.io.stringify_yx(player.position)))
279 self.io.send('GAME_STATE_COMPLETE')
282 """Send turn finish signal, run game world, send new world data.
284 First sends 'TURN_FINISHED' message, then runs game world
285 until new player input is needed, then sends game state.
287 self.io.send('TURN_FINISHED ' + str(self.world.turn))
288 self.world.proceed_to_next_player_turn()
289 msg = str(self.world.get_player()._last_task_result)
290 self.io.send('LAST_PLAYER_TASK_RESULT ' + server_.io.quote(msg))
291 self.send_gamestate()
293 def cmd_FIB(self, numbers, connection_id):
294 """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
296 Numbers are calculated in parallel as far as possible, using fib().
297 A 'CALCULATING …' message is sent to caller before the result.
299 self.io.send('CALCULATING …', connection_id)
300 results = self.pool.map(fib, numbers)
301 reply = ' '.join([str(r) for r in results])
302 self.io.send(reply, connection_id)
303 cmd_FIB.argtypes = 'seq:int:nonneg'
305 def cmd_INC_P(self, connection_id):
306 """Increment world.turn, send game turn data to everyone.
308 To simulate game processing waiting times, a one second delay
309 between TURN_FINISHED and TURN occurs; after TURN, some
310 expensive calculations are started as pool processes that need
311 to be finished until a further INC finishes the turn.
313 This is just a demo structure for how the game loop could work
314 when parallelized. One might imagine a two-step game turn,
315 with a non-action step determining actor tasks (the AI
316 determinations would take the place of the fib calculations
317 here), and an action step wherein these tasks are performed
318 (where now sleep(1) is).
321 from time import sleep
322 if self.pool_result is not None:
323 self.pool_result.wait()
324 self.io.send('TURN_FINISHED ' + str(self.world.turn))
327 self.send_gamestate()
328 self.pool_result = self.pool.map_async(fib, (35, 35))
330 def cmd_SWITCH_PLAYER(self):
331 player = self.world.get_player()
332 player.set_task('WAIT')
333 thing_ids = [t.id_ for t in self.world.things]
334 player_index = thing_ids.index(player.id_)
335 if player_index == len(thing_ids) - 1:
336 self.world.player_id = thing_ids[0]
338 self.world.player_id = thing_ids[player_index + 1]
341 def cmd_GET_GAMESTATE(self, connection_id):
342 """Send game state to caller."""
343 self.send_gamestate(connection_id)
345 def cmd_ECHO(self, msg, connection_id):
346 """Send msg to caller."""
347 self.io.send(msg, connection_id)
348 cmd_ECHO.argtypes = 'string'
350 def cmd_ALL(self, msg, connection_id):
351 """Send msg to all clients."""
353 cmd_ALL.argtypes = 'string'
355 def cmd_TERRAIN_LINE(self, y, terrain_line):
356 self.world.map_.set_line(y, terrain_line)
357 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'
359 def cmd_GEN_WORLD(self, geometry, yx, seed):
360 self.world.make_new(geometry, yx, seed)
361 cmd_GEN_WORLD.argtypes = 'string:geometry yx_tuple:pos string'
363 def get_command_signature(self, command_name):
364 from functools import partial
366 def cmd_TASK_colon(task_name, *args):
367 self.world.get_player().set_task(task_name, args)
370 def cmd_SET_TASK_colon(task_name, thing_id, todo, *args):
371 t = self.world.get_thing(thing_id, False)
373 raiseArgError('No such Thing.')
374 t.task = Task(t, task_name, args)
377 def task_prefixed(command_name, task_prefix, task_command,
381 if command_name[:len(task_prefix)] == task_prefix:
382 task_name = command_name[len(task_prefix):]
383 task_method_candidate = 'task_' + task_name
384 if hasattr(Thing, task_method_candidate):
385 method = partial(task_command, task_name)
386 task_method = getattr(Thing, task_method_candidate)
387 if hasattr(task_method, 'argtypes'):
388 argtypes = task_method.argtypes
389 if method is not None:
390 return method, argtypes_prefix + argtypes
391 return None, argtypes
393 method, argtypes = task_prefixed(command_name, 'TASK:', cmd_TASK_colon)
395 return method, argtypes
396 method, argtypes = task_prefixed(command_name, 'SET_TASK:',
398 'int:nonneg int:nonneg')
400 return method, argtypes
401 method_candidate = 'cmd_' + command_name
402 if hasattr(self, method_candidate):
403 method = getattr(self, method_candidate)
404 if hasattr(method, 'argtypes'):
405 argtypes = method.argtypes
406 return method, argtypes
408 def get_string_options(self, string_option_type):
409 if string_option_type == 'geometry':
410 return self.map_manager.get_map_geometries()
411 elif string_option_type == 'direction':
412 return self.world.map_.get_directions()
415 def cmd_PLAYER_ID(self, id_):
416 # TODO: test whether valid thing ID
417 self.world.player_id = id_
418 cmd_PLAYER_ID.argtypes = 'int:nonneg'
420 def cmd_TURN(self, n):
422 cmd_TURN.argtypes = 'int:nonneg'
429 save_file_name = self.io.game_file_name + '.save'
430 with open(save_file_name, 'w') as f:
431 write(f, 'TURN %s' % self.world.turn)
432 write(f, 'MAP ' + self.world.map_.geometry + ' ' + server_.io.stringify_yx(self.world.map_.size))
433 for y, line in self.world.map_.lines():
434 write(f, 'TERRAIN_LINE %5s %s' % (y, server_.io.quote(line)))
435 for thing in self.world.things:
436 write(f, 'THING_TYPE %s %s' % (thing.id_, thing.type_))
437 write(f, 'THING_POS %s %s' % (thing.id_,
438 server_.io.stringify_yx(thing.position)))
441 task_args = task.get_args_string()
442 write(f, 'SET_TASK:%s %s %s %s' % (task.name, thing.id_,
443 task.todo, task_args))
444 write(f, 'PLAYER_ID %s' % self.world.player_id)
445 cmd_SAVE.dont_save = True