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('would move into illegal terrain')
87 for t in self.thing.world.things:
88 if t.position == test_pos:
89 raise GameError('would move into other thing')
92 class Thing(game_common.Thing):
94 def __init__(self, *args, **kwargs):
95 super().__init__(*args, **kwargs)
96 self.task = Task(self, 'wait')
97 self.last_task_result = None
103 def task_move(self, direction):
104 self.position = self.world.map_.move(self.position, direction)
107 def decide_task(self):
108 visible_things = self.get_visible_things()
110 for t in visible_things:
111 if t.type_ == 'human':
115 self.set_task('wait')
117 dijkstra_map = type(self.world.map_)(self.world.map_.size)
119 dijkstra_map.terrain = [n_max for i in range(dijkstra_map.size_i)]
120 dijkstra_map[target] = 0
124 for pos in dijkstra_map:
125 if self.world.map_[pos] != '.':
127 neighbors = dijkstra_map.get_neighbors(pos)
129 if yx is not None and dijkstra_map[yx] < dijkstra_map[pos] - 1:
130 dijkstra_map[pos] = dijkstra_map[yx] + 1
132 #with open('log', 'a') as f:
133 # f.write('---------------------------------\n')
134 # for y, line in dijkstra_map.lines():
143 neighbors = dijkstra_map.get_neighbors(self.position)
145 dirs = dijkstra_map.get_directions()
146 #print('DEBUG dirs', dirs)
147 #print('DEBUG neighbors', neighbors)
149 #for pos in neighbors:
151 # debug_scores += [9000]
153 # debug_scores += [dijkstra_map[pos]]
154 #print('DEBUG debug_scores', debug_scores)
156 for i_dir in range(len(neighbors)):
157 pos = neighbors[i_dir]
158 if pos is not None and dijkstra_map[pos] < n:
159 n = dijkstra_map[pos]
160 direction = dirs[i_dir]
161 #print('DEBUG result', direction)
163 self.set_task('move', direction=direction)
164 #self.world.game.io.send('would move ' + direction)
166 self.set_task('wait')
169 def set_task(self, task_name, *args, **kwargs):
170 self.task = Task(self, task_name, args, kwargs)
171 self.task.check() # will throw GameError if necessary
173 def proceed(self, is_AI=True):
174 """Further the thing in its tasks.
176 Decrements .task.todo; if it thus falls to <= 0, enacts method
177 whose name is 'task_' + self.task.name and sets .task =
178 None. If is_AI, calls .decide_task to decide a self.task.
180 Before doing anything, ensures an empty map visibility stencil
181 and checks that task is still possible, and aborts it
182 otherwise (for AI things, decides a new task).
188 except GameError as e:
190 self.last_task_result = e
195 if self.task.todo <= 0:
196 task = getattr(self, 'task_' + self.task.name)
197 self.last_task_result = task(*self.task.args, **self.task.kwargs)
199 if is_AI and self.task is None:
202 def get_stencil(self):
203 if self._stencil is not None:
205 self._stencil = self.world.map_.get_fov_map(self.position)
208 def get_visible_map(self):
209 stencil = self.get_stencil()
210 m = self.world.map_.new_from_shape(' ')
212 if stencil[pos] == '.':
213 m[pos] = self.world.map_[pos]
216 def get_visible_things(self):
217 stencil = self.get_stencil()
219 for thing in self.world.things:
220 if stencil[thing.position] == '.':
221 visible_things += [thing]
222 return visible_things
226 """Calculate n-th Fibonacci number. Very inefficiently."""
230 return fib(n-1) + fib(n-2)
233 class Game(game_common.CommonCommandsMixin):
235 def __init__(self, game_file_name):
237 self.map_manager = server_.map_.map_manager
238 self.world = World(self)
239 self.io = server_.io.GameIO(game_file_name, self)
240 # self.pool and self.pool_result are currently only needed by the FIB
241 # command and the demo of a parallelized game loop in cmd_inc_p.
242 from multiprocessing import Pool
244 self.pool_result = None
246 def send_gamestate(self, connection_id=None):
247 """Send out game state data relevant to clients."""
249 def stringify_yx(tuple_):
250 """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
251 return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])
253 self.io.send('NEW_TURN ' + str(self.world.turn))
254 self.io.send('MAP ' + self.world.map_.geometry +\
255 ' ' + stringify_yx(self.world.map_.size))
256 visible_map = self.world.get_player().get_visible_map()
257 for y, line in visible_map.lines():
258 self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, self.io.quote(line)))
259 visible_things = self.world.get_player().get_visible_things()
260 for thing in visible_things:
261 self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
262 self.io.send('THING_POS %s %s' % (thing.id_,
263 stringify_yx(thing.position)))
264 player = self.world.get_player()
265 self.io.send('PLAYER_POS %s' % (stringify_yx(player.position)))
266 self.io.send('GAME_STATE_COMPLETE')
269 """Send turn finish signal, run game world, send new world data.
271 First sends 'TURN_FINISHED' message, then runs game world
272 until new player input is needed, then sends game state.
274 self.io.send('TURN_FINISHED ' + str(self.world.turn))
275 self.world.proceed_to_next_player_turn()
276 msg = str(self.world.get_player().last_task_result)
277 self.io.send('LAST_PLAYER_TASK_RESULT ' + self.io.quote(msg))
278 self.send_gamestate()
280 def cmd_FIB(self, numbers, connection_id):
281 """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
283 Numbers are calculated in parallel as far as possible, using fib().
284 A 'CALCULATING …' message is sent to caller before the result.
286 self.io.send('CALCULATING …', connection_id)
287 results = self.pool.map(fib, numbers)
288 reply = ' '.join([str(r) for r in results])
289 self.io.send(reply, connection_id)
290 cmd_FIB.argtypes = 'seq:int:nonneg'
292 def cmd_INC_P(self, connection_id):
293 """Increment world.turn, send game turn data to everyone.
295 To simulate game processing waiting times, a one second delay between
296 TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive
297 calculations are started as pool processes that need to be finished
298 until a further INC finishes the turn.
300 This is just a demo structure for how the game loop could work when
301 parallelized. One might imagine a two-step game turn, with a non-action
302 step determining actor tasks (the AI determinations would take the
303 place of the fib calculations here), and an action step wherein these
304 tasks are performed (where now sleep(1) is).
306 from time import sleep
307 if self.pool_result is not None:
308 self.pool_result.wait()
309 self.io.send('TURN_FINISHED ' + str(self.world.turn))
312 self.send_gamestate()
313 self.pool_result = self.pool.map_async(fib, (35, 35))
315 def cmd_MOVE(self, direction):
316 """Set player task to 'move' with direction arg, finish player turn."""
318 legal_directions = self.world.map_.get_directions()
319 if direction not in legal_directions:
320 raise parser.ArgError('Move argument must be one of: ' +
321 ', '.join(legal_directions))
322 self.world.get_player().set_task('move', direction=direction)
324 cmd_MOVE.argtypes = 'string'
327 """Set player task to 'wait', finish player turn."""
328 self.world.get_player().set_task('wait')
331 def cmd_GET_GAMESTATE(self, connection_id):
332 """Send game state to caller."""
333 self.send_gamestate(connection_id)
335 def cmd_ECHO(self, msg, connection_id):
336 """Send msg to caller."""
337 self.io.send(msg, connection_id)
338 cmd_ECHO.argtypes = 'string'
340 def cmd_ALL(self, msg, connection_id):
341 """Send msg to all clients."""
343 cmd_ALL.argtypes = 'string'
345 def cmd_TERRAIN_LINE(self, y, terrain_line):
346 self.world.map_.set_line(y, terrain_line)
347 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'
349 def cmd_GEN_WORLD(self, geometry, yx, seed):
350 legal_grids = self.map_manager.get_map_geometries()
351 if geometry not in legal_grids:
352 raise ArgError('First map argument must be one of: ' +
353 ', '.join(legal_grids))
354 self.world.make_new(geometry, yx, seed)
355 cmd_GEN_WORLD.argtypes = 'string yx_tuple:pos string'