7 from server_.game_error import GameError
8 from parser import ArgError
11 class World(game_common.World):
13 def __init__(self, game):
17 # use extended local classes
20 def proceed_to_next_player_turn(self):
21 """Run game world turns until player can decide their next step.
23 Iterates through all non-player things, on each step
24 furthering them in their tasks (and letting them decide new
25 ones if they finish). The iteration order is: first all things
26 that come after the player in the world things list, then
27 (after incrementing the world turn) all that come before the
28 player; then the player's .proceed() is run, and if it does
29 not finish his task, the loop starts at the beginning. Once
30 the player's task is finished, the loop breaks.
33 player = self.get_player()
34 player_i = self.things.index(player)
35 for thing in self.things[player_i+1:]:
38 for thing in self.things[:player_i]:
40 player.proceed(is_AI=False)
41 if player.task is None:
45 return self.get_thing(self.player_id)
47 def make_new(self, geometry, yx, seed):
51 self.new_map(geometry, yx)
53 if 0 in pos or (yx[0] - 1) == pos[0] or (yx[1] - 1) == pos[1]:
56 self.map_[pos] = random.choice(('.', '.', '.', '.', 'x'))
57 player = self.Thing(self, 0)
58 player.type_ = 'human'
59 player.position = [random.randint(0, yx[0] -1),
60 random.randint(0, yx[1] - 1)]
61 npc = self.Thing(self, 1)
63 npc.position = [random.randint(0, yx[0] -1),
64 random.randint(0, yx[1] -1)]
65 self.things = [player, npc]
72 class Thing(game_common.Thing):
74 def __init__(self, *args, **kwargs):
75 super().__init__(*args, **kwargs)
76 self.task = self.world.game.task_manager.get_task_class('WAIT')(self)
77 self._last_task_result = None
80 def move_towards_target(self, target):
81 dijkstra_map = type(self.world.map_)(self.world.map_.size)
83 dijkstra_map.terrain = [n_max for i in range(dijkstra_map.size_i)]
84 dijkstra_map[target] = 0
86 visible_map = self.get_visible_map()
89 for pos in dijkstra_map:
90 if visible_map[pos] != '.':
92 neighbors = dijkstra_map.get_neighbors(tuple(pos))
93 for direction in neighbors:
94 yx = neighbors[direction]
95 if yx is not None and dijkstra_map[yx] < dijkstra_map[pos] - 1:
96 dijkstra_map[pos] = dijkstra_map[yx] + 1
98 #with open('log', 'a') as f:
99 # f.write('---------------------------------\n')
100 # for y, line in dijkstra_map.lines():
109 neighbors = dijkstra_map.get_neighbors(tuple(self.position))
111 #print('DEBUG', self.position, neighbors)
112 #dirs = dijkstra_map.get_directions()
113 #print('DEBUG dirs', dirs)
114 #print('DEBUG neighbors', neighbors)
116 #for pos in neighbors:
118 # debug_scores += [9000]
120 # debug_scores += [dijkstra_map[pos]]
121 #print('DEBUG debug_scores', debug_scores)
122 target_direction = None
123 for direction in neighbors:
124 yx = neighbors[direction]
126 n_new = dijkstra_map[yx]
129 target_direction = direction
130 #print('DEBUG result', direction)
132 self.set_task('MOVE', (target_direction,))
134 def decide_task(self):
135 # TODO: Check if monster can follow player too well (even when they should lose them)
136 visible_things = self.get_visible_things()
138 for t in visible_things:
139 if t.type_ == 'human':
142 if target is not None:
144 self.move_towards_target(target)
148 self.set_task('WAIT')
151 def set_task(self, task_name, args=()):
152 task_class = self.world.game.task_manager.get_task_class(task_name)
153 self.task = task_class(self, args)
154 self.task.check() # will throw GameError if necessary
156 def proceed(self, is_AI=True):
157 """Further the thing in its tasks.
159 Decrements .task.todo; if it thus falls to <= 0, enacts method
160 whose name is 'task_' + self.task.name and sets .task =
161 None. If is_AI, calls .decide_task to decide a self.task.
163 Before doing anything, ensures an empty map visibility stencil
164 and checks that task is still possible, and aborts it
165 otherwise (for AI things, decides a new task).
171 except GameError as e:
173 self._last_task_result = e
178 self.set_task('WAIT')
181 if self.task.todo <= 0:
182 self._last_task_result = self.task.do()
184 if is_AI and self.task is None:
188 self.set_task('WAIT')
190 def get_stencil(self):
191 if self._stencil is not None:
193 self._stencil = self.world.map_.get_fov_map(self.position)
196 def get_visible_map(self):
197 stencil = self.get_stencil()
198 m = self.world.map_.new_from_shape(' ')
200 if stencil[pos] == '.':
201 m[pos] = self.world.map_[pos]
204 def get_visible_things(self):
205 stencil = self.get_stencil()
207 for thing in self.world.things:
208 if stencil[thing.position] == '.':
209 visible_things += [thing]
210 return visible_things
214 """Calculate n-th Fibonacci number. Very inefficiently."""
218 return fib(n-1) + fib(n-2)
221 class Game(game_common.CommonCommandsMixin):
223 def __init__(self, game_file_name):
224 self.map_manager = server_.map_.map_manager
225 self.task_manager = server_.tasks.task_manager
226 self.world = World(self)
227 self.io = server_.io.GameIO(game_file_name, self)
228 # self.pool and self.pool_result are currently only needed by the FIB
229 # command and the demo of a parallelized game loop in cmd_inc_p.
230 from multiprocessing import Pool
232 self.pool_result = None
234 def send_gamestate(self, connection_id=None):
235 """Send out game state data relevant to clients."""
237 self.io.send('TURN ' + str(self.world.turn))
238 self.io.send('MAP ' + self.world.map_.geometry +\
239 ' ' + server_.io.stringify_yx(self.world.map_.size))
240 visible_map = self.world.get_player().get_visible_map()
241 for y, line in visible_map.lines():
242 self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, server_.io.quote(line)))
243 visible_things = self.world.get_player().get_visible_things()
244 for thing in visible_things:
245 self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
246 self.io.send('THING_POS %s %s' % (thing.id_,
247 server_.io.stringify_yx(thing.position)))
248 player = self.world.get_player()
249 self.io.send('PLAYER_POS %s' % (server_.io.stringify_yx(player.position)))
250 self.io.send('GAME_STATE_COMPLETE')
253 """Send turn finish signal, run game world, send new world data.
255 First sends 'TURN_FINISHED' message, then runs game world
256 until new player input is needed, then sends game state.
258 self.io.send('TURN_FINISHED ' + str(self.world.turn))
259 self.world.proceed_to_next_player_turn()
260 msg = str(self.world.get_player()._last_task_result)
261 self.io.send('LAST_PLAYER_TASK_RESULT ' + server_.io.quote(msg))
262 self.send_gamestate()
264 def cmd_FIB(self, numbers, connection_id):
265 """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
267 Numbers are calculated in parallel as far as possible, using fib().
268 A 'CALCULATING …' message is sent to caller before the result.
270 self.io.send('CALCULATING …', connection_id)
271 results = self.pool.map(fib, numbers)
272 reply = ' '.join([str(r) for r in results])
273 self.io.send(reply, connection_id)
274 cmd_FIB.argtypes = 'seq:int:nonneg'
276 def cmd_INC_P(self, connection_id):
277 """Increment world.turn, send game turn data to everyone.
279 To simulate game processing waiting times, a one second delay
280 between TURN_FINISHED and TURN occurs; after TURN, some
281 expensive calculations are started as pool processes that need
282 to be finished until a further INC finishes the turn.
284 This is just a demo structure for how the game loop could work
285 when parallelized. One might imagine a two-step game turn,
286 with a non-action step determining actor tasks (the AI
287 determinations would take the place of the fib calculations
288 here), and an action step wherein these tasks are performed
289 (where now sleep(1) is).
292 from time import sleep
293 if self.pool_result is not None:
294 self.pool_result.wait()
295 self.io.send('TURN_FINISHED ' + str(self.world.turn))
298 self.send_gamestate()
299 self.pool_result = self.pool.map_async(fib, (35, 35))
301 def cmd_SWITCH_PLAYER(self):
302 player = self.world.get_player()
303 player.set_task('WAIT')
304 thing_ids = [t.id_ for t in self.world.things]
305 player_index = thing_ids.index(player.id_)
306 if player_index == len(thing_ids) - 1:
307 self.world.player_id = thing_ids[0]
309 self.world.player_id = thing_ids[player_index + 1]
312 def cmd_GET_GAMESTATE(self, connection_id):
313 """Send game state to caller."""
314 self.send_gamestate(connection_id)
316 def cmd_ECHO(self, msg, connection_id):
317 """Send msg to caller."""
318 self.io.send(msg, connection_id)
319 cmd_ECHO.argtypes = 'string'
321 def cmd_ALL(self, msg, connection_id):
322 """Send msg to all clients."""
324 cmd_ALL.argtypes = 'string'
326 def cmd_TERRAIN_LINE(self, y, terrain_line):
327 self.world.map_.set_line(y, terrain_line)
328 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'
330 def cmd_GEN_WORLD(self, geometry, yx, seed):
331 self.world.make_new(geometry, yx, seed)
332 cmd_GEN_WORLD.argtypes = 'string:geometry yx_tuple:pos string'
334 def get_command_signature(self, command_name):
335 from functools import partial
337 def cmd_TASK_colon(task_name, *args):
338 self.world.get_player().set_task(task_name, args)
341 def cmd_SET_TASK_colon(task_name, thing_id, todo, *args):
342 t = self.world.get_thing(thing_id, False)
344 raiseArgError('No such Thing.')
345 task_class = self.task_manager.get_task_class(task_name)
346 t.task = task_class(t, args)
349 def task_prefixed(command_name, task_prefix, task_command,
353 if command_name[:len(task_prefix)] == task_prefix:
354 task_name = command_name[len(task_prefix):]
355 task_manager_reply = self.task_manager.get_task_class(task_name)
356 if task_manager_reply is not None:
357 func = partial(task_command, task_name)
358 task_class = task_manager_reply
359 argtypes = task_class.argtypes
361 return func, argtypes_prefix + argtypes
362 return None, argtypes
364 func, argtypes = task_prefixed(command_name, 'TASK:', cmd_TASK_colon)
366 return func, argtypes
367 func, argtypes = task_prefixed(command_name, 'SET_TASK:',
369 'int:nonneg int:nonneg ')
371 return func, argtypes
372 func_candidate = 'cmd_' + command_name
373 if hasattr(self, func_candidate):
374 func = getattr(self, func_candidate)
375 if hasattr(func, 'argtypes'):
376 argtypes = func.argtypes
377 return func, argtypes
379 def get_string_options(self, string_option_type):
380 if string_option_type == 'geometry':
381 return self.map_manager.get_map_geometries()
382 elif string_option_type == 'direction':
383 return self.world.map_.get_directions()
386 def cmd_PLAYER_ID(self, id_):
387 # TODO: test whether valid thing ID
388 self.world.player_id = id_
389 cmd_PLAYER_ID.argtypes = 'int:nonneg'
391 def cmd_TURN(self, n):
393 cmd_TURN.argtypes = 'int:nonneg'
400 save_file_name = self.io.game_file_name + '.save'
401 with open(save_file_name, 'w') as f:
402 write(f, 'TURN %s' % self.world.turn)
403 write(f, 'MAP ' + self.world.map_.geometry + ' ' + server_.io.stringify_yx(self.world.map_.size))
404 for y, line in self.world.map_.lines():
405 write(f, 'TERRAIN_LINE %5s %s' % (y, server_.io.quote(line)))
406 for thing in self.world.things:
407 write(f, 'THING_TYPE %s %s' % (thing.id_, thing.type_))
408 write(f, 'THING_POS %s %s' % (thing.id_,
409 server_.io.stringify_yx(thing.position)))
412 task_args = task.get_args_string()
413 write(f, 'SET_TASK:%s %s %s %s' % (task.name, thing.id_,
414 task.todo, task_args))
415 write(f, 'PLAYER_ID %s' % self.world.player_id)
416 cmd_SAVE.dont_save = True