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=(), kwargs={}):
79 if self.name == 'move':
80 if len(self.args) > 0:
81 direction = self.args[0]
83 direction = self.kwargs['direction']
84 test_pos = self.thing.world.map_.move(self.thing.position, direction)
85 if self.thing.world.map_[test_pos] != '.':
86 raise GameError(str(self.thing.id_) +
87 ' would move into illegal terrain')
88 for t in self.thing.world.things:
89 if t.position == test_pos:
90 raise GameError(str(self.thing.id_) +
91 ' would move into other thing')
94 class Thing(game_common.Thing):
96 def __init__(self, *args, **kwargs):
97 super().__init__(*args, **kwargs)
98 self.task = Task(self, 'wait')
99 self.last_task_result = None
105 def task_move(self, direction):
106 self.position = self.world.map_.move(self.position, direction)
109 def move_towards_target(self, target):
110 dijkstra_map = type(self.world.map_)(self.world.map_.size)
112 dijkstra_map.terrain = [n_max for i in range(dijkstra_map.size_i)]
113 dijkstra_map[target] = 0
117 for pos in dijkstra_map:
118 if self.world.map_[pos] != '.':
120 neighbors = dijkstra_map.get_neighbors(pos)
122 if yx is not None and dijkstra_map[yx] < dijkstra_map[pos] - 1:
123 dijkstra_map[pos] = dijkstra_map[yx] + 1
125 #with open('log', 'a') as f:
126 # f.write('---------------------------------\n')
127 # for y, line in dijkstra_map.lines():
136 neighbors = dijkstra_map.get_neighbors(self.position)
138 dirs = dijkstra_map.get_directions()
139 #print('DEBUG dirs', dirs)
140 #print('DEBUG neighbors', neighbors)
142 #for pos in neighbors:
144 # debug_scores += [9000]
146 # debug_scores += [dijkstra_map[pos]]
147 #print('DEBUG debug_scores', debug_scores)
149 for i_dir in range(len(neighbors)):
150 pos = neighbors[i_dir]
151 if pos is not None and dijkstra_map[pos] < n:
152 n = dijkstra_map[pos]
153 direction = dirs[i_dir]
154 #print('DEBUG result', direction)
156 self.set_task('move', direction=direction)
157 #self.world.game.io.send('would move ' + direction)
159 def decide_task(self):
160 visible_things = self.get_visible_things()
162 for t in visible_things:
163 if t.type_ == 'human':
166 if target is not None:
168 self.move_towards_target(target)
172 self.set_task('wait')
175 def set_task(self, task_name, *args, **kwargs):
176 self.task = Task(self, task_name, args, kwargs)
177 self.task.check() # will throw GameError if necessary
179 def proceed(self, is_AI=True):
180 """Further the thing in its tasks.
182 Decrements .task.todo; if it thus falls to <= 0, enacts method
183 whose name is 'task_' + self.task.name and sets .task =
184 None. If is_AI, calls .decide_task to decide a self.task.
186 Before doing anything, ensures an empty map visibility stencil
187 and checks that task is still possible, and aborts it
188 otherwise (for AI things, decides a new task).
194 except GameError as e:
196 self.last_task_result = e
201 self.set_task('wait')
204 if self.task.todo <= 0:
205 task = getattr(self, 'task_' + self.task.name)
206 self.last_task_result = task(*self.task.args, **self.task.kwargs)
208 if is_AI and self.task is None:
212 self.set_task('wait')
214 def get_stencil(self):
215 if self._stencil is not None:
217 self._stencil = self.world.map_.get_fov_map(self.position)
220 def get_visible_map(self):
221 stencil = self.get_stencil()
222 m = self.world.map_.new_from_shape(' ')
224 if stencil[pos] == '.':
225 m[pos] = self.world.map_[pos]
228 def get_visible_things(self):
229 stencil = self.get_stencil()
231 for thing in self.world.things:
232 if stencil[thing.position] == '.':
233 visible_things += [thing]
234 return visible_things
238 """Calculate n-th Fibonacci number. Very inefficiently."""
242 return fib(n-1) + fib(n-2)
245 class Game(game_common.CommonCommandsMixin):
247 def __init__(self, game_file_name):
249 self.map_manager = server_.map_.map_manager
250 self.world = World(self)
251 self.io = server_.io.GameIO(game_file_name, self)
252 # self.pool and self.pool_result are currently only needed by the FIB
253 # command and the demo of a parallelized game loop in cmd_inc_p.
254 from multiprocessing import Pool
256 self.pool_result = None
258 def send_gamestate(self, connection_id=None):
259 """Send out game state data relevant to clients."""
261 def stringify_yx(tuple_):
262 """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
263 return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])
265 self.io.send('NEW_TURN ' + str(self.world.turn))
266 self.io.send('MAP ' + self.world.map_.geometry +\
267 ' ' + stringify_yx(self.world.map_.size))
268 visible_map = self.world.get_player().get_visible_map()
269 for y, line in visible_map.lines():
270 self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, self.io.quote(line)))
271 visible_things = self.world.get_player().get_visible_things()
272 for thing in visible_things:
273 self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
274 self.io.send('THING_POS %s %s' % (thing.id_,
275 stringify_yx(thing.position)))
276 player = self.world.get_player()
277 self.io.send('PLAYER_POS %s' % (stringify_yx(player.position)))
278 self.io.send('GAME_STATE_COMPLETE')
281 """Send turn finish signal, run game world, send new world data.
283 First sends 'TURN_FINISHED' message, then runs game world
284 until new player input is needed, then sends game state.
286 self.io.send('TURN_FINISHED ' + str(self.world.turn))
287 self.world.proceed_to_next_player_turn()
288 msg = str(self.world.get_player().last_task_result)
289 self.io.send('LAST_PLAYER_TASK_RESULT ' + self.io.quote(msg))
290 self.send_gamestate()
292 def cmd_FIB(self, numbers, connection_id):
293 """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
295 Numbers are calculated in parallel as far as possible, using fib().
296 A 'CALCULATING …' message is sent to caller before the result.
298 self.io.send('CALCULATING …', connection_id)
299 results = self.pool.map(fib, numbers)
300 reply = ' '.join([str(r) for r in results])
301 self.io.send(reply, connection_id)
302 cmd_FIB.argtypes = 'seq:int:nonneg'
304 def cmd_INC_P(self, connection_id):
305 """Increment world.turn, send game turn data to everyone.
307 To simulate game processing waiting times, a one second delay between
308 TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive
309 calculations are started as pool processes that need to be finished
310 until a further INC finishes the turn.
312 This is just a demo structure for how the game loop could work when
313 parallelized. One might imagine a two-step game turn, with a non-action
314 step determining actor tasks (the AI determinations would take the
315 place of the fib calculations here), and an action step wherein these
316 tasks are performed (where now sleep(1) is).
318 from time import sleep
319 if self.pool_result is not None:
320 self.pool_result.wait()
321 self.io.send('TURN_FINISHED ' + str(self.world.turn))
324 self.send_gamestate()
325 self.pool_result = self.pool.map_async(fib, (35, 35))
327 def cmd_MOVE(self, direction):
328 """Set player task to 'move' with direction arg, finish player turn."""
330 legal_directions = self.world.map_.get_directions()
331 if direction not in legal_directions:
332 raise parser.ArgError('Move argument must be one of: ' +
333 ', '.join(legal_directions))
334 self.world.get_player().set_task('move', direction=direction)
336 cmd_MOVE.argtypes = 'string'
338 def cmd_SWITCH_PLAYER(self):
339 player = self.world.get_player()
340 player.set_task('wait')
341 thing_ids = [t.id_ for t in self.world.things]
342 player_index = thing_ids.index(player.id_)
343 if player_index == len(thing_ids) - 1:
344 self.world.player_id = thing_ids[0]
346 self.world.player_id = thing_ids[player_index + 1]
350 """Set player task to 'wait', finish player turn."""
351 self.world.get_player().set_task('wait')
354 def cmd_GET_GAMESTATE(self, connection_id):
355 """Send game state to caller."""
356 self.send_gamestate(connection_id)
358 def cmd_ECHO(self, msg, connection_id):
359 """Send msg to caller."""
360 self.io.send(msg, connection_id)
361 cmd_ECHO.argtypes = 'string'
363 def cmd_ALL(self, msg, connection_id):
364 """Send msg to all clients."""
366 cmd_ALL.argtypes = 'string'
368 def cmd_TERRAIN_LINE(self, y, terrain_line):
369 self.world.map_.set_line(y, terrain_line)
370 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'
372 def cmd_GEN_WORLD(self, geometry, yx, seed):
373 legal_grids = self.map_manager.get_map_geometries()
374 if geometry not in legal_grids:
375 raise ArgError('First map argument must be one of: ' +
376 ', '.join(legal_grids))
377 self.world.make_new(geometry, yx, seed)
378 cmd_GEN_WORLD.argtypes = 'string yx_tuple:pos string'