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 extended child class
117 def proceed_to_next_player_turn(self):
118 """Run game world turns until player can decide their next step.
120 Iterates through all non-player things, on each step
121 furthering them in their tasks (and letting them decide new
122 ones if they finish). The iteration order is: first all things
123 that come after the player in the world things list, then
124 (after incrementing the world turn) all that come before the
125 player; then the player's .proceed() is run, and if it does
126 not finish his task, the loop starts at the beginning. Once
127 the player's task is finished, the loop breaks.
130 player = self.get_player()
131 player_i = self.things.index(player)
132 for thing in self.things[player_i+1:]:
135 for thing in self.things[:player_i]:
137 player.proceed(is_AI=False)
138 if player.task is None:
141 def get_player(self):
142 return self.get_thing(self.player_id)
147 def __init__(self, thing, name, args=(), kwargs={}):
155 if self.name == 'move':
156 if len(self.args) > 0:
157 direction = self.args[0]
159 direction = self.kwargs['direction']
160 test_pos = self.thing.world.map_.move(self.thing.position, direction)
161 if self.thing.world.map_[test_pos] != '.':
162 raise GameError('would move into illegal terrain')
163 for t in self.thing.world.things:
164 if t.position == test_pos:
165 raise GameError('would move into other thing')
168 class Thing(game_common.Thing):
170 def __init__(self, *args, **kwargs):
171 super().__init__(*args, **kwargs)
172 self.task = Task(self, 'wait')
173 self.last_task_result = None
179 def task_move(self, direction):
180 self.position = self.world.map_.move(self.position, direction)
183 def decide_task(self):
184 if self.position[1] > 1:
185 self.set_task('move', 'LEFT')
186 elif self.position[1] < 3:
187 self.set_task('move', 'RIGHT')
189 self.set_task('wait')
191 def set_task(self, task_name, *args, **kwargs):
192 self.task = Task(self, task_name, args, kwargs)
195 def proceed(self, is_AI=True):
196 """Further the thing in its tasks.
198 Decrements .task.todo; if it thus falls to <= 0, enacts method
199 whose name is 'task_' + self.task.name and sets .task =
200 None. If is_AI, calls .decide_task to decide a self.task.
202 Before doing anything, ensures an empty map visibility stencil
203 and checks that task is still possible, and aborts it
204 otherwise (for AI things, decides a new task).
210 except GameError as e:
212 self.last_task_result = e
217 if self.task.todo <= 0:
218 task = getattr(self, 'task_' + self.task.name)
219 self.last_task_result = task(*self.task.args, **self.task.kwargs)
221 if is_AI and self.task is None:
224 def get_stencil(self):
225 if self._stencil is not None:
227 m = self.world.map_.new_from_shape('?')
229 if pos == self.position or m.are_neighbors(pos, self.position):
234 def get_visible_map(self):
235 stencil = self.get_stencil()
236 m = self.world.map_.new_from_shape(' ')
238 if stencil[pos] == '.':
239 m[pos] = self.world.map_[pos]
242 def get_visible_things(self):
243 stencil = self.get_stencil()
245 for thing in self.world.things:
246 if stencil[thing.position] == '.':
247 visible_things += [thing]
248 return visible_things
252 """Calculate n-th Fibonacci number. Very inefficiently."""
256 return fib(n-1) + fib(n-2)
259 class Game(game_common.CommonCommandsMixin):
261 def __init__(self, game_file_name):
264 self.io = server_.io.GameIO(game_file_name, self)
265 # self.pool and self.pool_result are currently only needed by the FIB
266 # command and the demo of a parallelized game loop in cmd_inc_p.
267 from multiprocessing import Pool
269 self.pool_result = None
271 def send_gamestate(self, connection_id=None):
272 """Send out game state data relevant to clients."""
274 def stringify_yx(tuple_):
275 """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
276 return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])
278 self.io.send('NEW_TURN ' + str(self.world.turn))
279 self.io.send('MAP_SIZE ' + stringify_yx(self.world.map_.size))
280 visible_map = self.world.get_player().get_visible_map()
281 for y, line in visible_map.lines():
282 self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, self.io.quote(line)))
283 visible_things = self.world.get_player().get_visible_things()
284 for thing in visible_things:
285 self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
286 self.io.send('THING_POS %s %s' % (thing.id_,
287 stringify_yx(thing.position)))
290 """Send turn finish signal, run game world, send new world data.
292 First sends 'TURN_FINISHED' message, then runs game world
293 until new player input is needed, then sends game state.
295 self.io.send('TURN_FINISHED ' + str(self.world.turn))
296 self.world.proceed_to_next_player_turn()
297 msg = str(self.world.get_player().last_task_result)
298 self.io.send('LAST_PLAYER_TASK_RESULT ' + self.io.quote(msg))
299 self.send_gamestate()
301 def cmd_FIB(self, numbers, connection_id):
302 """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
304 Numbers are calculated in parallel as far as possible, using fib().
305 A 'CALCULATING …' message is sent to caller before the result.
307 self.io.send('CALCULATING …', connection_id)
308 results = self.pool.map(fib, numbers)
309 reply = ' '.join([str(r) for r in results])
310 self.io.send(reply, connection_id)
311 cmd_FIB.argtypes = 'seq:int:nonneg'
313 def cmd_INC_P(self, connection_id):
314 """Increment world.turn, send game turn data to everyone.
316 To simulate game processing waiting times, a one second delay between
317 TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive
318 calculations are started as pool processes that need to be finished
319 until a further INC finishes the turn.
321 This is just a demo structure for how the game loop could work when
322 parallelized. One might imagine a two-step game turn, with a non-action
323 step determining actor tasks (the AI determinations would take the
324 place of the fib calculations here), and an action step wherein these
325 tasks are performed (where now sleep(1) is).
327 from time import sleep
328 if self.pool_result is not None:
329 self.pool_result.wait()
330 self.io.send('TURN_FINISHED ' + str(self.world.turn))
333 self.send_gamestate()
334 self.pool_result = self.pool.map_async(fib, (35, 35))
336 def cmd_MOVE(self, direction):
337 """Set player task to 'move' with direction arg, finish player turn."""
339 legal_directions = self.world.map_.get_directions()
340 if direction not in legal_directions:
341 raise parser.ArgError('Move argument must be one of: ' +
342 ', '.join(legal_directions))
343 self.world.get_player().set_task('move', direction=direction)
345 cmd_MOVE.argtypes = 'string'
348 """Set player task to 'wait', finish player turn."""
349 self.world.get_player().set_task('wait')
352 def cmd_GET_GAMESTATE(self, connection_id):
353 """Send game state jto caller."""
354 self.send_gamestate(connection_id)
356 def cmd_ECHO(self, msg, connection_id):
357 """Send msg to caller."""
358 self.io.send(msg, connection_id)
359 cmd_ECHO.argtypes = 'string'
361 def cmd_ALL(self, msg, connection_id):
362 """Send msg to all clients."""
364 cmd_ALL.argtypes = 'string'
366 def cmd_TERRAIN_LINE(self, y, terrain_line):
367 self.world.map_.set_line(y, terrain_line)
368 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'