5 from parser import ArgError
8 class GameError(Exception):
12 class World(game_common.World):
14 def __init__(self, game):
18 # use extended local classes
21 def proceed_to_next_player_turn(self):
22 """Run game world turns until player can decide their next step.
24 Iterates through all non-player things, on each step
25 furthering them in their tasks (and letting them decide new
26 ones if they finish). The iteration order is: first all things
27 that come after the player in the world things list, then
28 (after incrementing the world turn) all that come before the
29 player; then the player's .proceed() is run, and if it does
30 not finish his task, the loop starts at the beginning. Once
31 the player's task is finished, the loop breaks.
34 player = self.get_player()
35 player_i = self.things.index(player)
36 for thing in self.things[player_i+1:]:
39 for thing in self.things[:player_i]:
41 player.proceed(is_AI=False)
42 if player.task is None:
46 return self.get_thing(self.player_id)
48 def make_new(self, geometry, yx, seed):
52 self.new_map(geometry, yx)
54 if 0 in pos or (yx[0] - 1) == pos[0] or (yx[1] - 1) == pos[1]:
57 self.map_[pos] = random.choice(('.', '.', '.', '.', 'x'))
58 player = self.Thing(self, 0)
59 player.type_ = 'human'
60 player.position = [random.randint(0, yx[0] -1),
61 random.randint(0, yx[1] - 1)]
62 npc = self.Thing(self, 1)
64 npc.position = [random.randint(0, yx[0] -1),
65 random.randint(0, yx[1] -1)]
66 self.things = [player, npc]
71 def __init__(self, thing, name, args=()):
78 if self.name == 'MOVE':
79 test_pos = self.thing.world.map_.move(self.thing.position, self.args[0])
80 if self.thing.world.map_[test_pos] != '.':
81 raise GameError(str(self.thing.id_) +
82 ' would move into illegal terrain')
83 for t in self.thing.world.things:
84 if t.position == test_pos:
85 raise GameError(str(self.thing.id_) +
86 ' would move into other thing')
89 class Thing(game_common.Thing):
91 def __init__(self, *args, **kwargs):
92 super().__init__(*args, **kwargs)
93 self.task = Task(self, 'WAIT')
94 self.last_task_result = None
100 def task_MOVE(self, direction):
101 self.position = self.world.map_.move(self.position, direction)
103 task_MOVE.argtypes = 'string:direction'
105 def move_towards_target(self, target):
106 dijkstra_map = type(self.world.map_)(self.world.map_.size)
108 dijkstra_map.terrain = [n_max for i in range(dijkstra_map.size_i)]
109 dijkstra_map[target] = 0
113 for pos in dijkstra_map:
114 if self.world.map_[pos] != '.':
116 neighbors = dijkstra_map.get_neighbors(pos)
118 if yx is not None and dijkstra_map[yx] < dijkstra_map[pos] - 1:
119 dijkstra_map[pos] = dijkstra_map[yx] + 1
121 #with open('log', 'a') as f:
122 # f.write('---------------------------------\n')
123 # for y, line in dijkstra_map.lines():
132 neighbors = dijkstra_map.get_neighbors(self.position)
134 dirs = dijkstra_map.get_directions()
135 #print('DEBUG dirs', dirs)
136 #print('DEBUG neighbors', neighbors)
138 #for pos in neighbors:
140 # debug_scores += [9000]
142 # debug_scores += [dijkstra_map[pos]]
143 #print('DEBUG debug_scores', debug_scores)
145 for i_dir in range(len(neighbors)):
146 pos = neighbors[i_dir]
147 if pos is not None and dijkstra_map[pos] < n:
148 n = dijkstra_map[pos]
149 direction = dirs[i_dir]
150 #print('DEBUG result', direction)
152 self.set_task('MOVE', (direction,))
153 #self.world.game.io.send('would move ' + direction)
155 def decide_task(self):
156 visible_things = self.get_visible_things()
158 for t in visible_things:
159 if t.type_ == 'human':
162 if target is not None:
164 self.move_towards_target(target)
168 self.set_task('WAIT')
171 def set_task(self, task_name, args=()):
172 self.task = Task(self, task_name, args)
173 self.task.check() # will throw GameError if necessary
175 def proceed(self, is_AI=True):
176 """Further the thing in its tasks.
178 Decrements .task.todo; if it thus falls to <= 0, enacts method
179 whose name is 'task_' + self.task.name and sets .task =
180 None. If is_AI, calls .decide_task to decide a self.task.
182 Before doing anything, ensures an empty map visibility stencil
183 and checks that task is still possible, and aborts it
184 otherwise (for AI things, decides a new task).
190 except GameError as e:
192 self.last_task_result = e
197 self.set_task('WAIT')
200 if self.task.todo <= 0:
201 task = getattr(self, 'task_' + self.task.name)
202 self.last_task_result = task(*self.task.args)
204 if is_AI and self.task is None:
208 self.set_task('WAIT')
210 def get_stencil(self):
211 if self._stencil is not None:
213 self._stencil = self.world.map_.get_fov_map(self.position)
216 def get_visible_map(self):
217 stencil = self.get_stencil()
218 m = self.world.map_.new_from_shape(' ')
220 if stencil[pos] == '.':
221 m[pos] = self.world.map_[pos]
224 def get_visible_things(self):
225 stencil = self.get_stencil()
227 for thing in self.world.things:
228 if stencil[thing.position] == '.':
229 visible_things += [thing]
230 return visible_things
234 """Calculate n-th Fibonacci number. Very inefficiently."""
238 return fib(n-1) + fib(n-2)
241 class Game(game_common.CommonCommandsMixin):
243 def __init__(self, game_file_name):
245 self.map_manager = server_.map_.map_manager
246 self.world = World(self)
247 self.io = server_.io.GameIO(game_file_name, self)
248 # self.pool and self.pool_result are currently only needed by the FIB
249 # command and the demo of a parallelized game loop in cmd_inc_p.
250 from multiprocessing import Pool
252 self.pool_result = None
254 def send_gamestate(self, connection_id=None):
255 """Send out game state data relevant to clients."""
257 def stringify_yx(tuple_):
258 """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
259 return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])
261 self.io.send('NEW_TURN ' + str(self.world.turn))
262 self.io.send('MAP ' + self.world.map_.geometry +\
263 ' ' + stringify_yx(self.world.map_.size))
264 visible_map = self.world.get_player().get_visible_map()
265 for y, line in visible_map.lines():
266 self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, self.io.quote(line)))
267 visible_things = self.world.get_player().get_visible_things()
268 for thing in visible_things:
269 self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
270 self.io.send('THING_POS %s %s' % (thing.id_,
271 stringify_yx(thing.position)))
272 player = self.world.get_player()
273 self.io.send('PLAYER_POS %s' % (stringify_yx(player.position)))
274 self.io.send('GAME_STATE_COMPLETE')
277 """Send turn finish signal, run game world, send new world data.
279 First sends 'TURN_FINISHED' message, then runs game world
280 until new player input is needed, then sends game state.
282 self.io.send('TURN_FINISHED ' + str(self.world.turn))
283 self.world.proceed_to_next_player_turn()
284 msg = str(self.world.get_player().last_task_result)
285 self.io.send('LAST_PLAYER_TASK_RESULT ' + self.io.quote(msg))
286 self.send_gamestate()
288 def cmd_FIB(self, numbers, connection_id):
289 """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
291 Numbers are calculated in parallel as far as possible, using fib().
292 A 'CALCULATING …' message is sent to caller before the result.
294 self.io.send('CALCULATING …', connection_id)
295 results = self.pool.map(fib, numbers)
296 reply = ' '.join([str(r) for r in results])
297 self.io.send(reply, connection_id)
298 cmd_FIB.argtypes = 'seq:int:nonneg'
300 def cmd_INC_P(self, connection_id):
301 """Increment world.turn, send game turn data to everyone.
303 To simulate game processing waiting times, a one second delay between
304 TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive
305 calculations are started as pool processes that need to be finished
306 until a further INC finishes the turn.
308 This is just a demo structure for how the game loop could work when
309 parallelized. One might imagine a two-step game turn, with a non-action
310 step determining actor tasks (the AI determinations would take the
311 place of the fib calculations here), and an action step wherein these
312 tasks are performed (where now sleep(1) is).
314 from time import sleep
315 if self.pool_result is not None:
316 self.pool_result.wait()
317 self.io.send('TURN_FINISHED ' + str(self.world.turn))
320 self.send_gamestate()
321 self.pool_result = self.pool.map_async(fib, (35, 35))
323 def cmd_SWITCH_PLAYER(self):
324 player = self.world.get_player()
325 player.set_task('WAIT')
326 thing_ids = [t.id_ for t in self.world.things]
327 player_index = thing_ids.index(player.id_)
328 if player_index == len(thing_ids) - 1:
329 self.world.player_id = thing_ids[0]
331 self.world.player_id = thing_ids[player_index + 1]
334 def cmd_GET_GAMESTATE(self, connection_id):
335 """Send game state to caller."""
336 self.send_gamestate(connection_id)
338 def cmd_ECHO(self, msg, connection_id):
339 """Send msg to caller."""
340 self.io.send(msg, connection_id)
341 cmd_ECHO.argtypes = 'string'
343 def cmd_ALL(self, msg, connection_id):
344 """Send msg to all clients."""
346 cmd_ALL.argtypes = 'string'
348 def cmd_TERRAIN_LINE(self, y, terrain_line):
349 self.world.map_.set_line(y, terrain_line)
350 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'
352 def cmd_GEN_WORLD(self, geometry, yx, seed):
353 self.world.make_new(geometry, yx, seed)
354 cmd_GEN_WORLD.argtypes = 'string:geometry yx_tuple:pos string'
356 def get_command_signature(self, command_name):
357 from functools import partial
359 def cmd_TASK_colon(task_name, *args):
360 self.world.get_player().set_task(task_name, args)
365 task_prefix = 'TASK:'
366 if command_name[:len(task_prefix)] == task_prefix:
367 task_name = command_name[len(task_prefix):]
368 task_method_candidate = 'task_' + task_name
369 if hasattr(Thing, task_method_candidate):
370 method = partial(cmd_TASK_colon, task_name)
371 task_method = getattr(Thing, task_method_candidate)
372 if hasattr(task_method, 'argtypes'):
373 argtypes = task_method.argtypes
374 return method, argtypes
375 method_candidate = 'cmd_' + command_name
376 if hasattr(self, method_candidate):
377 method = getattr(self, method_candidate)
378 if hasattr(method, 'argtypes'):
379 argtypes = method.argtypes
380 return method, argtypes
382 def get_string_options(self, string_option_type):
383 if string_option_type == 'geometry':
384 return self.map_manager.get_map_geometries()
385 elif string_option_type == 'direction':
386 return self.world.map_.get_directions()