6 class GameError(Exception):
10 class Map(game_common.Map):
12 def __getitem__(self, yx):
13 return self.terrain[self.get_position_index(yx)]
15 def __setitem__(self, yx, c):
16 pos_i = self.get_position_index(yx)
17 self.terrain = self.terrain[:pos_i] + c + self.terrain[pos_i + 1:]
20 """Iterate over YX position coordinates."""
21 for y in range(self.size[0]):
22 for x in range(self.size[1]):
27 for y in range(self.size[0]):
28 yield (y, self.terrain[y * width:(y + 1) * width])
30 # The following is used nowhere, so not implemented.
32 # for y in range(self.size[0]):
33 # for x in range(self.size[1]):
34 # yield ([y, x], self.terrain[self.get_position_index([y, x])])
36 def get_directions(self):
38 for name in dir(self):
39 if name[:5] == 'move_':
40 directions += [name[5:]]
43 def new_from_shape(self, init_char):
45 new_map = copy.deepcopy(self)
47 new_map[pos] = init_char
50 def move(self, start_pos, direction):
51 mover = getattr(self, 'move_' + direction)
52 new_pos = mover(start_pos)
53 if new_pos[0] < 0 or new_pos[1] < 0 or \
54 new_pos[0] >= self.size[0] or new_pos[1] >= self.size[1]:
55 raise GameError('would move outside map bounds')
58 def move_LEFT(self, start_pos):
59 return [start_pos[0], start_pos[1] - 1]
61 def move_RIGHT(self, start_pos):
62 return [start_pos[0], start_pos[1] + 1]
67 def are_neighbors(self, pos_1, pos_2):
68 if pos_1[0] == pos_2[0] and abs(pos_1[1] - pos_2[1]) <= 1:
70 elif abs(pos_1[0] - pos_2[0]) == 1:
72 if pos_2[1] in (pos_1[1], pos_1[1] - 1):
74 elif pos_2[1] in (pos_1[1], pos_1[1] + 1):
78 def move_UPLEFT(self, start_pos):
79 if start_pos[0] % 2 == 0:
80 return [start_pos[0] - 1, start_pos[1] - 1]
82 return [start_pos[0] - 1, start_pos[1]]
84 def move_UPRIGHT(self, start_pos):
85 if start_pos[0] % 2 == 0:
86 return [start_pos[0] - 1, start_pos[1]]
88 return [start_pos[0] - 1, start_pos[1] + 1]
90 def move_DOWNLEFT(self, start_pos):
91 if start_pos[0] % 2 == 0:
92 return [start_pos[0] + 1, start_pos[1] - 1]
94 return [start_pos[0] + 1, start_pos[1]]
96 def move_DOWNRIGHT(self, start_pos):
97 if start_pos[0] % 2 == 0:
98 return [start_pos[0] + 1, start_pos[1]]
100 return [start_pos[0] + 1, start_pos[1] + 1]
103 class MapSquare(Map):
105 def are_neighbors(self, pos_1, pos_2):
106 return abs(pos_1[0] - pos_2[0]) <= 1 and abs(pos_1[1] - pos_2[1] <= 1)
108 def move_UP(self, start_pos):
109 return [start_pos[0] - 1, start_pos[1]]
111 def move_DOWN(self, start_pos):
112 return [start_pos[0] + 1, start_pos[1]]
115 class World(game_common.World):
120 # use extended local classes
123 self.MapSquare = MapSquare
125 def proceed_to_next_player_turn(self):
126 """Run game world turns until player can decide their next step.
128 Iterates through all non-player things, on each step
129 furthering them in their tasks (and letting them decide new
130 ones if they finish). The iteration order is: first all things
131 that come after the player in the world things list, then
132 (after incrementing the world turn) all that come before the
133 player; then the player's .proceed() is run, and if it does
134 not finish his task, the loop starts at the beginning. Once
135 the player's task is finished, the loop breaks.
138 player = self.get_player()
139 player_i = self.things.index(player)
140 for thing in self.things[player_i+1:]:
143 for thing in self.things[:player_i]:
145 player.proceed(is_AI=False)
146 if player.task is None:
149 def get_player(self):
150 return self.get_thing(self.player_id)
155 def __init__(self, thing, name, args=(), kwargs={}):
163 if self.name == 'move':
164 if len(self.args) > 0:
165 direction = self.args[0]
167 direction = self.kwargs['direction']
168 test_pos = self.thing.world.map_.move(self.thing.position, direction)
169 if self.thing.world.map_[test_pos] != '.':
170 raise GameError('would move into illegal terrain')
171 for t in self.thing.world.things:
172 if t.position == test_pos:
173 raise GameError('would move into other thing')
176 class Thing(game_common.Thing):
178 def __init__(self, *args, **kwargs):
179 super().__init__(*args, **kwargs)
180 self.task = Task(self, 'wait')
181 self.last_task_result = None
187 def task_move(self, direction):
188 self.position = self.world.map_.move(self.position, direction)
191 def decide_task(self):
192 if self.position[1] > 1:
193 self.set_task('move', 'LEFT')
194 elif self.position[1] < 3:
195 self.set_task('move', 'RIGHT')
197 self.set_task('wait')
199 def set_task(self, task_name, *args, **kwargs):
200 self.task = Task(self, task_name, args, kwargs)
203 def proceed(self, is_AI=True):
204 """Further the thing in its tasks.
206 Decrements .task.todo; if it thus falls to <= 0, enacts method
207 whose name is 'task_' + self.task.name and sets .task =
208 None. If is_AI, calls .decide_task to decide a self.task.
210 Before doing anything, ensures an empty map visibility stencil
211 and checks that task is still possible, and aborts it
212 otherwise (for AI things, decides a new task).
218 except GameError as e:
220 self.last_task_result = e
225 if self.task.todo <= 0:
226 task = getattr(self, 'task_' + self.task.name)
227 self.last_task_result = task(*self.task.args, **self.task.kwargs)
229 if is_AI and self.task is None:
232 def get_stencil(self):
233 if self._stencil is not None:
235 m = self.world.map_.new_from_shape('?')
237 if pos == self.position or m.are_neighbors(pos, self.position):
242 def get_visible_map(self):
243 stencil = self.get_stencil()
244 m = self.world.map_.new_from_shape(' ')
246 if stencil[pos] == '.':
247 m[pos] = self.world.map_[pos]
250 def get_visible_things(self):
251 stencil = self.get_stencil()
253 for thing in self.world.things:
254 if stencil[thing.position] == '.':
255 visible_things += [thing]
256 return visible_things
260 """Calculate n-th Fibonacci number. Very inefficiently."""
264 return fib(n-1) + fib(n-2)
267 class Game(game_common.CommonCommandsMixin):
269 def __init__(self, game_file_name):
272 self.io = server_.io.GameIO(game_file_name, self)
273 # self.pool and self.pool_result are currently only needed by the FIB
274 # command and the demo of a parallelized game loop in cmd_inc_p.
275 from multiprocessing import Pool
277 self.pool_result = None
279 def send_gamestate(self, connection_id=None):
280 """Send out game state data relevant to clients."""
282 def stringify_yx(tuple_):
283 """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
284 return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])
286 self.io.send('NEW_TURN ' + str(self.world.turn))
287 grid = self.world.map_.__class__.__name__[3:]
288 self.io.send('MAP ' + grid +' ' + stringify_yx(self.world.map_.size))
289 visible_map = self.world.get_player().get_visible_map()
290 for y, line in visible_map.lines():
291 self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, self.io.quote(line)))
292 visible_things = self.world.get_player().get_visible_things()
293 for thing in visible_things:
294 self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
295 self.io.send('THING_POS %s %s' % (thing.id_,
296 stringify_yx(thing.position)))
299 """Send turn finish signal, run game world, send new world data.
301 First sends 'TURN_FINISHED' message, then runs game world
302 until new player input is needed, then sends game state.
304 self.io.send('TURN_FINISHED ' + str(self.world.turn))
305 self.world.proceed_to_next_player_turn()
306 msg = str(self.world.get_player().last_task_result)
307 self.io.send('LAST_PLAYER_TASK_RESULT ' + self.io.quote(msg))
308 self.send_gamestate()
310 def cmd_FIB(self, numbers, connection_id):
311 """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
313 Numbers are calculated in parallel as far as possible, using fib().
314 A 'CALCULATING …' message is sent to caller before the result.
316 self.io.send('CALCULATING …', connection_id)
317 results = self.pool.map(fib, numbers)
318 reply = ' '.join([str(r) for r in results])
319 self.io.send(reply, connection_id)
320 cmd_FIB.argtypes = 'seq:int:nonneg'
322 def cmd_INC_P(self, connection_id):
323 """Increment world.turn, send game turn data to everyone.
325 To simulate game processing waiting times, a one second delay between
326 TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive
327 calculations are started as pool processes that need to be finished
328 until a further INC finishes the turn.
330 This is just a demo structure for how the game loop could work when
331 parallelized. One might imagine a two-step game turn, with a non-action
332 step determining actor tasks (the AI determinations would take the
333 place of the fib calculations here), and an action step wherein these
334 tasks are performed (where now sleep(1) is).
336 from time import sleep
337 if self.pool_result is not None:
338 self.pool_result.wait()
339 self.io.send('TURN_FINISHED ' + str(self.world.turn))
342 self.send_gamestate()
343 self.pool_result = self.pool.map_async(fib, (35, 35))
345 def cmd_MOVE(self, direction):
346 """Set player task to 'move' with direction arg, finish player turn."""
348 legal_directions = self.world.map_.get_directions()
349 if direction not in legal_directions:
350 raise parser.ArgError('Move argument must be one of: ' +
351 ', '.join(legal_directions))
352 self.world.get_player().set_task('move', direction=direction)
354 cmd_MOVE.argtypes = 'string'
357 """Set player task to 'wait', finish player turn."""
358 self.world.get_player().set_task('wait')
361 def cmd_GET_GAMESTATE(self, connection_id):
362 """Send game state jto caller."""
363 self.send_gamestate(connection_id)
365 def cmd_ECHO(self, msg, connection_id):
366 """Send msg to caller."""
367 self.io.send(msg, connection_id)
368 cmd_ECHO.argtypes = 'string'
370 def cmd_ALL(self, msg, connection_id):
371 """Send msg to all clients."""
373 cmd_ALL.argtypes = 'string'
375 def cmd_TERRAIN_LINE(self, y, terrain_line):
376 self.world.map_.set_line(y, terrain_line)
377 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'