6 class GameError(Exception):
10 class Map(game_common.Map):
12 def __getitem__(self, yx):
13 return self.terrain[self.get_pos_i(yx)]
15 def __setitem__(self, yx, c):
16 pos_i = self.get_pos_i(yx)
17 self.terrain = self.terrain[:pos_i] + c + self.terrain[pos_i + 1:]
20 for y in range(self.size[0]):
21 for x in range(self.size[1]):
26 return self.size[0] * self.size[1]
28 def get_line(self, y):
30 return self.terrain[y * width:(y + 1) * width]
32 def get_directions(self):
34 for name in dir(self):
35 if name[:5] == 'move_':
36 directions += [name[5:]]
39 def get_pos_i(self, yx):
40 return yx[0] * self.size[1] + yx[1]
42 def new_from_shape(self, init_char):
43 return Map(self.size, init_char*self.size_i)
45 def are_neighbors(self, pos_1, pos_2):
46 return abs(pos_1[0] - pos_2[0]) <= 1 and abs(pos_1[1] - pos_2[1] <= 1)
48 def move(self, start_pos, direction):
49 mover = getattr(self, 'move_' + direction)
50 new_pos = mover(start_pos)
51 if new_pos[0] < 0 or new_pos[1] < 0 or \
52 new_pos[0] >= self.size[0] or new_pos[1] >= self.size[1]:
53 raise GameError('would move outside map bounds')
56 def move_UP(self, start_pos):
57 return [start_pos[0] - 1, start_pos[1]]
59 def move_DOWN(self, start_pos):
60 return [start_pos[0] + 1, start_pos[1]]
62 def move_LEFT(self, start_pos):
63 return [start_pos[0], start_pos[1] - 1]
65 def move_RIGHT(self, start_pos):
66 return [start_pos[0], start_pos[1] + 1]
69 class World(game_common.World):
73 self.Thing = Thing # use local Thing class instead of game_common's
74 self.map_ = Map() # use extended child class
77 def proceed_to_next_player_turn(self):
78 """Run game world turns until player can decide their next step.
80 Iterates through all non-player things, on each step
81 furthering them in their tasks (and letting them decide new
82 ones if they finish). The iteration order is: first all things
83 that come after the player in the world things list, then
84 (after incrementing the world turn) all that come before the
85 player; then the player's .proceed() is run, and if it does
86 not finish his task, the loop starts at the beginning. Once
87 the player's task is finished, the loop breaks.
90 player = self.get_player()
91 player_i = self.things.index(player)
92 for thing in self.things[player_i+1:]:
95 for thing in self.things[:player_i]:
97 player.proceed(is_AI=False)
98 if player.task is None:
101 def get_player(self):
102 return self.get_thing(self.player_id)
107 def __init__(self, thing, name, args=(), kwargs={}):
115 if self.name == 'move':
116 if len(self.args) > 0:
117 direction = self.args[0]
119 direction = self.kwargs['direction']
120 test_pos = self.thing.world.map_.move(self.thing.position, direction)
121 if self.thing.world.map_[test_pos] != '.':
122 raise GameError('would move into illegal terrain')
123 for t in self.thing.world.things:
124 if t.position == test_pos:
125 raise GameError('would move into other thing')
128 class Thing(game_common.Thing):
130 def __init__(self, *args, **kwargs):
131 super().__init__(*args, **kwargs)
132 self.task = Task(self, 'wait')
133 self.last_task_result = None
139 def task_move(self, direction):
140 self.position = self.world.map_.move(self.position, direction)
143 def decide_task(self):
144 if self.position[1] > 1:
145 self.set_task('move', 'LEFT')
146 elif self.position[1] < 3:
147 self.set_task('move', 'RIGHT')
149 self.set_task('wait')
151 def set_task(self, task_name, *args, **kwargs):
152 self.task = Task(self, task_name, args, kwargs)
155 def proceed(self, is_AI=True):
156 """Further the thing in its tasks.
158 Decrements .task.todo; if it thus falls to <= 0, enacts method
159 whose name is 'task_' + self.task.name and sets .task =
160 None. If is_AI, calls .decide_task to decide a self.task.
162 Before doing anything, ensures an empty map visibility stencil
163 and checks that task is still possible, and aborts it
164 otherwise (for AI things, decides a new task).
170 except GameError as e:
172 self.last_task_result = e
177 if self.task.todo <= 0:
178 task = getattr(self, 'task_' + self.task.name)
179 self.last_task_result = task(*self.task.args, **self.task.kwargs)
181 if is_AI and self.task is None:
184 def get_stencil(self):
185 if self._stencil is not None:
187 m = self.world.map_.new_from_shape('?')
189 if pos == self.position or m.are_neighbors(pos, self.position):
194 def get_visible_map(self):
195 stencil = self.get_stencil()
196 m = self.world.map_.new_from_shape(' ')
198 if stencil[pos] == '.':
199 m[pos] = self.world.map_[pos]
202 def get_visible_things(self):
203 stencil = self.get_stencil()
205 for thing in self.world.things:
206 if stencil[thing.position] == '.':
207 visible_things += [thing]
208 return visible_things
212 """Calculate n-th Fibonacci number. Very inefficiently."""
216 return fib(n-1) + fib(n-2)
219 class Game(game_common.CommonCommandsMixin):
221 def __init__(self, game_file_name):
224 self.io = server_.io.GameIO(game_file_name, self)
225 # self.pool and self.pool_result are currently only needed by the FIB
226 # command and the demo of a parallelized game loop in cmd_inc_p.
227 from multiprocessing import Pool
229 self.pool_result = None
231 def send_gamestate(self, connection_id=None):
232 """Send out game state data relevant to clients."""
234 def stringify_yx(tuple_):
235 """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
236 return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])
238 self.io.send('NEW_TURN ' + str(self.world.turn))
239 self.io.send('MAP_SIZE ' + stringify_yx(self.world.map_.size))
240 visible_map = self.world.get_player().get_visible_map()
241 for y in range(self.world.map_.size[0]):
242 self.io.send('VISIBLE_MAP_LINE %5s %s' %
243 (y, self.io.quote(visible_map.get_line(y))))
244 visible_things = self.world.get_player().get_visible_things()
245 for thing in visible_things:
246 self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
247 self.io.send('THING_POS %s %s' % (thing.id_,
248 stringify_yx(thing.position)))
251 """Send turn finish signal, run game world, send new world data.
253 First sends 'TURN_FINISHED' message, then runs game world
254 until new player input is needed, then sends game state.
256 self.io.send('TURN_FINISHED ' + str(self.world.turn))
257 self.world.proceed_to_next_player_turn()
258 msg = str(self.world.get_player().last_task_result)
259 self.io.send('LAST_PLAYER_TASK_RESULT ' + self.io.quote(msg))
260 self.send_gamestate()
262 def cmd_FIB(self, numbers, connection_id):
263 """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
265 Numbers are calculated in parallel as far as possible, using fib().
266 A 'CALCULATING …' message is sent to caller before the result.
268 self.io.send('CALCULATING …', connection_id)
269 results = self.pool.map(fib, numbers)
270 reply = ' '.join([str(r) for r in results])
271 self.io.send(reply, connection_id)
272 cmd_FIB.argtypes = 'seq:int:nonneg'
274 def cmd_INC_P(self, connection_id):
275 """Increment world.turn, send game turn data to everyone.
277 To simulate game processing waiting times, a one second delay between
278 TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive
279 calculations are started as pool processes that need to be finished
280 until a further INC finishes the turn.
282 This is just a demo structure for how the game loop could work when
283 parallelized. One might imagine a two-step game turn, with a non-action
284 step determining actor tasks (the AI determinations would take the
285 place of the fib calculations here), and an action step wherein these
286 tasks are performed (where now sleep(1) is).
288 from time import sleep
289 if self.pool_result is not None:
290 self.pool_result.wait()
291 self.io.send('TURN_FINISHED ' + str(self.world.turn))
294 self.send_gamestate()
295 self.pool_result = self.pool.map_async(fib, (35, 35))
297 def cmd_MOVE(self, direction):
298 """Set player task to 'move' with direction arg, finish player turn."""
300 legal_directions = self.world.map_.get_directions()
301 if direction not in legal_directions:
302 raise parser.ArgError('Move argument must be one of: ' +
303 ', '.join(legal_directions))
304 self.world.get_player().set_task('move', direction=direction)
306 cmd_MOVE.argtypes = 'string'
309 """Set player task to 'wait', finish player turn."""
310 self.world.get_player().set_task('wait')
313 def cmd_GET_GAMESTATE(self, connection_id):
314 """Send game state jto caller."""
315 self.send_gamestate(connection_id)
317 def cmd_ECHO(self, msg, connection_id):
318 """Send msg to caller."""
319 self.io.send(msg, connection_id)
320 cmd_ECHO.argtypes = 'string'
322 def cmd_ALL(self, msg, connection_id):
323 """Send msg to all clients."""
325 cmd_ALL.argtypes = 'string'
327 def cmd_TERRAIN_LINE(self, y, terrain_line):
328 self.world.map_.set_line(y, terrain_line)
329 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'