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])])
38 return self.size[0] * self.size[1]
40 def get_directions(self):
42 for name in dir(self):
43 if name[:5] == 'move_':
44 directions += [name[5:]]
47 def new_from_shape(self, init_char):
48 return Map(self.size, init_char*self.size_i)
50 #def are_neighbors(self, pos_1, pos_2):
51 # return abs(pos_1[0] - pos_2[0]) <= 1 and abs(pos_1[1] - pos_2[1] <= 1)
53 def are_neighbors(self, pos_1, pos_2):
54 if pos_1[0] == pos_2[0] and abs(pos_1[1] - pos_2[1]) <= 1:
56 elif abs(pos_1[0] - pos_2[0]) == 1:
58 if pos_2[1] in (pos_1[1], pos_1[1] - 1):
60 elif pos_2[1] in (pos_1[1], pos_1[1] + 1):
64 def move(self, start_pos, direction):
65 mover = getattr(self, 'move_' + direction)
66 new_pos = mover(start_pos)
67 if new_pos[0] < 0 or new_pos[1] < 0 or \
68 new_pos[0] >= self.size[0] or new_pos[1] >= self.size[1]:
69 raise GameError('would move outside map bounds')
72 def move_LEFT(self, start_pos):
73 return [start_pos[0], start_pos[1] - 1]
75 def move_RIGHT(self, start_pos):
76 return [start_pos[0], start_pos[1] + 1]
78 #def move_UP(self, start_pos):
79 # return [start_pos[0] - 1, start_pos[1]]
81 #def move_DOWN(self, start_pos):
82 # return [start_pos[0] + 1, start_pos[1]]
84 def move_UPLEFT(self, start_pos):
85 if start_pos[0] % 2 == 0:
86 return [start_pos[0] - 1, start_pos[1] - 1]
88 return [start_pos[0] - 1, start_pos[1]]
90 def move_UPRIGHT(self, start_pos):
91 if start_pos[0] % 2 == 0:
92 return [start_pos[0] - 1, start_pos[1]]
94 return [start_pos[0] - 1, start_pos[1] + 1]
96 def move_DOWNLEFT(self, start_pos):
97 if start_pos[0] % 2 == 0:
98 return [start_pos[0] + 1, start_pos[1] - 1]
100 return [start_pos[0] + 1, start_pos[1]]
102 def move_DOWNRIGHT(self, start_pos):
103 if start_pos[0] % 2 == 0:
104 return [start_pos[0] + 1, start_pos[1]]
106 return [start_pos[0] + 1, start_pos[1] + 1]
109 class World(game_common.World):
113 self.Thing = Thing # use local Thing class instead of game_common's
114 self.Map = Map # use local Map class instead of game_common's
115 self.map_ = Map() # use extended child class
118 def proceed_to_next_player_turn(self):
119 """Run game world turns until player can decide their next step.
121 Iterates through all non-player things, on each step
122 furthering them in their tasks (and letting them decide new
123 ones if they finish). The iteration order is: first all things
124 that come after the player in the world things list, then
125 (after incrementing the world turn) all that come before the
126 player; then the player's .proceed() is run, and if it does
127 not finish his task, the loop starts at the beginning. Once
128 the player's task is finished, the loop breaks.
131 player = self.get_player()
132 player_i = self.things.index(player)
133 for thing in self.things[player_i+1:]:
136 for thing in self.things[:player_i]:
138 player.proceed(is_AI=False)
139 if player.task is None:
142 def get_player(self):
143 return self.get_thing(self.player_id)
148 def __init__(self, thing, name, args=(), kwargs={}):
156 if self.name == 'move':
157 if len(self.args) > 0:
158 direction = self.args[0]
160 direction = self.kwargs['direction']
161 test_pos = self.thing.world.map_.move(self.thing.position, direction)
162 if self.thing.world.map_[test_pos] != '.':
163 raise GameError('would move into illegal terrain')
164 for t in self.thing.world.things:
165 if t.position == test_pos:
166 raise GameError('would move into other thing')
169 class Thing(game_common.Thing):
171 def __init__(self, *args, **kwargs):
172 super().__init__(*args, **kwargs)
173 self.task = Task(self, 'wait')
174 self.last_task_result = None
180 def task_move(self, direction):
181 self.position = self.world.map_.move(self.position, direction)
184 def decide_task(self):
185 if self.position[1] > 1:
186 self.set_task('move', 'LEFT')
187 elif self.position[1] < 3:
188 self.set_task('move', 'RIGHT')
190 self.set_task('wait')
192 def set_task(self, task_name, *args, **kwargs):
193 self.task = Task(self, task_name, args, kwargs)
196 def proceed(self, is_AI=True):
197 """Further the thing in its tasks.
199 Decrements .task.todo; if it thus falls to <= 0, enacts method
200 whose name is 'task_' + self.task.name and sets .task =
201 None. If is_AI, calls .decide_task to decide a self.task.
203 Before doing anything, ensures an empty map visibility stencil
204 and checks that task is still possible, and aborts it
205 otherwise (for AI things, decides a new task).
211 except GameError as e:
213 self.last_task_result = e
218 if self.task.todo <= 0:
219 task = getattr(self, 'task_' + self.task.name)
220 self.last_task_result = task(*self.task.args, **self.task.kwargs)
222 if is_AI and self.task is None:
225 def get_stencil(self):
226 if self._stencil is not None:
228 m = self.world.map_.new_from_shape('?')
230 if pos == self.position or m.are_neighbors(pos, self.position):
235 def get_visible_map(self):
236 stencil = self.get_stencil()
237 m = self.world.map_.new_from_shape(' ')
239 if stencil[pos] == '.':
240 m[pos] = self.world.map_[pos]
243 def get_visible_things(self):
244 stencil = self.get_stencil()
246 for thing in self.world.things:
247 if stencil[thing.position] == '.':
248 visible_things += [thing]
249 return visible_things
253 """Calculate n-th Fibonacci number. Very inefficiently."""
257 return fib(n-1) + fib(n-2)
260 class Game(game_common.CommonCommandsMixin):
262 def __init__(self, game_file_name):
265 self.io = server_.io.GameIO(game_file_name, self)
266 # self.pool and self.pool_result are currently only needed by the FIB
267 # command and the demo of a parallelized game loop in cmd_inc_p.
268 from multiprocessing import Pool
270 self.pool_result = None
272 def send_gamestate(self, connection_id=None):
273 """Send out game state data relevant to clients."""
275 def stringify_yx(tuple_):
276 """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
277 return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])
279 self.io.send('NEW_TURN ' + str(self.world.turn))
280 self.io.send('MAP ' + stringify_yx(self.world.map_.size))
281 visible_map = self.world.get_player().get_visible_map()
282 for y, line in visible_map.lines():
283 self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, self.io.quote(line)))
284 visible_things = self.world.get_player().get_visible_things()
285 for thing in visible_things:
286 self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
287 self.io.send('THING_POS %s %s' % (thing.id_,
288 stringify_yx(thing.position)))
291 """Send turn finish signal, run game world, send new world data.
293 First sends 'TURN_FINISHED' message, then runs game world
294 until new player input is needed, then sends game state.
296 self.io.send('TURN_FINISHED ' + str(self.world.turn))
297 self.world.proceed_to_next_player_turn()
298 msg = str(self.world.get_player().last_task_result)
299 self.io.send('LAST_PLAYER_TASK_RESULT ' + self.io.quote(msg))
300 self.send_gamestate()
302 def cmd_FIB(self, numbers, connection_id):
303 """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
305 Numbers are calculated in parallel as far as possible, using fib().
306 A 'CALCULATING …' message is sent to caller before the result.
308 self.io.send('CALCULATING …', connection_id)
309 results = self.pool.map(fib, numbers)
310 reply = ' '.join([str(r) for r in results])
311 self.io.send(reply, connection_id)
312 cmd_FIB.argtypes = 'seq:int:nonneg'
314 def cmd_INC_P(self, connection_id):
315 """Increment world.turn, send game turn data to everyone.
317 To simulate game processing waiting times, a one second delay between
318 TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive
319 calculations are started as pool processes that need to be finished
320 until a further INC finishes the turn.
322 This is just a demo structure for how the game loop could work when
323 parallelized. One might imagine a two-step game turn, with a non-action
324 step determining actor tasks (the AI determinations would take the
325 place of the fib calculations here), and an action step wherein these
326 tasks are performed (where now sleep(1) is).
328 from time import sleep
329 if self.pool_result is not None:
330 self.pool_result.wait()
331 self.io.send('TURN_FINISHED ' + str(self.world.turn))
334 self.send_gamestate()
335 self.pool_result = self.pool.map_async(fib, (35, 35))
337 def cmd_MOVE(self, direction):
338 """Set player task to 'move' with direction arg, finish player turn."""
340 legal_directions = self.world.map_.get_directions()
341 if direction not in legal_directions:
342 raise parser.ArgError('Move argument must be one of: ' +
343 ', '.join(legal_directions))
344 self.world.get_player().set_task('move', direction=direction)
346 cmd_MOVE.argtypes = 'string'
349 """Set player task to 'wait', finish player turn."""
350 self.world.get_player().set_task('wait')
353 def cmd_GET_GAMESTATE(self, connection_id):
354 """Send game state jto caller."""
355 self.send_gamestate(connection_id)
357 def cmd_ECHO(self, msg, connection_id):
358 """Send msg to caller."""
359 self.io.send(msg, connection_id)
360 cmd_ECHO.argtypes = 'string'
362 def cmd_ALL(self, msg, connection_id):
363 """Send msg to all clients."""
365 cmd_ALL.argtypes = 'string'
367 def cmd_TERRAIN_LINE(self, y, terrain_line):
368 self.world.map_.set_line(y, terrain_line)
369 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'