6 class GameError(Exception):
10 class Map(game_common.Map):
14 return self.size[0] * self.size[1]
16 def get_line(self, y):
18 return self.terrain[y * width:(y + 1) * width]
20 def get_directions(self):
22 for name in dir(self):
23 if name[:5] == 'move_':
24 directions += [name[5:]]
27 def get_pos_i(self, yx):
28 return yx[0] * self.size[1] + yx[1]
30 def set_terrain_at(self, pos, c):
31 pos_i = self.get_pos_i(pos)
32 self.terrain = self.terrain[:pos_i] + c + self.terrain[pos_i + 1:]
34 def get_terrain_at(self, yx):
35 return self.terrain[self.get_pos_i(yx)]
37 def new_from_shape(self, init_char):
38 return Map(self.size, init_char*self.size_i)
41 for y in range(self.size[0]):
42 for x in range(self.size[1]):
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,
122 map_tile = self.thing.world.map_.get_terrain_at(test_pos)
124 raise GameError('would move into illegal terrain')
125 for t in self.thing.world.things:
126 if t.position == test_pos:
127 raise GameError('would move into other thing')
130 class Thing(game_common.Thing):
132 def __init__(self, *args, **kwargs):
133 super().__init__(*args, **kwargs)
134 self.task = Task(self, 'wait')
135 self.last_task_result = None
141 def task_move(self, direction):
142 self.position = self.world.map_.move(self.position, direction)
145 def decide_task(self):
146 if self.position[1] > 1:
147 self.set_task('move', 'LEFT')
148 elif self.position[1] < 3:
149 self.set_task('move', 'RIGHT')
151 self.set_task('wait')
153 def set_task(self, task_name, *args, **kwargs):
154 self.task = Task(self, task_name, args, kwargs)
157 def proceed(self, is_AI=True):
158 """Further the thing in its tasks.
160 Decrements .task.todo; if it thus falls to <= 0, enacts method
161 whose name is 'task_' + self.task.name and sets .task =
162 None. If is_AI, calls .decide_task to decide a self.task.
164 Before doing anything, ensures an empty map visibility stencil
165 and checks that task is still possible, and aborts it
166 otherwise (for AI things, decides a new task).
172 except GameError as e:
174 self.last_task_result = e
179 if self.task.todo <= 0:
180 task = getattr(self, 'task_' + self.task.name)
181 self.last_task_result = task(*self.task.args, **self.task.kwargs)
183 if is_AI and self.task is None:
186 def get_stencil(self):
187 if self._stencil is not None:
189 m = self.world.map_.new_from_shape('?')
190 for pos in m.iterate():
191 if pos == self.position or m.are_neighbors(pos, self.position):
192 m.set_terrain_at(pos, '.')
196 def get_visible_map(self):
197 stencil = self.get_stencil()
198 m = self.world.map_.new_from_shape(' ')
199 for pos in m.iterate():
200 if stencil.get_terrain_at(pos) == '.':
201 m.set_terrain_at(pos, self.world.map_.get_terrain_at(pos))
204 def get_visible_things(self):
205 stencil = self.get_stencil()
207 for thing in self.world.things:
208 if stencil.get_terrain_at(thing.position) == '.':
209 visible_things += [thing]
210 return visible_things
214 """Calculate n-th Fibonacci number. Very inefficiently."""
218 return fib(n-1) + fib(n-2)
221 class Game(game_common.CommonCommandsMixin):
223 def __init__(self, game_file_name):
226 self.io = server_.io.GameIO(game_file_name, self)
227 # self.pool and self.pool_result are currently only needed by the FIB
228 # command and the demo of a parallelized game loop in cmd_inc_p.
229 from multiprocessing import Pool
231 self.pool_result = None
233 def send_gamestate(self, connection_id=None):
234 """Send out game state data relevant to clients."""
236 def stringify_yx(tuple_):
237 """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
238 return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])
240 self.io.send('NEW_TURN ' + str(self.world.turn))
241 self.io.send('MAP_SIZE ' + stringify_yx(self.world.map_.size))
242 visible_map = self.world.get_player().get_visible_map()
243 for y in range(self.world.map_.size[0]):
244 self.io.send('VISIBLE_MAP_LINE %5s %s' %
245 (y, self.io.quote(visible_map.get_line(y))))
246 visible_things = self.world.get_player().get_visible_things()
247 for thing in visible_things:
248 self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
249 self.io.send('THING_POS %s %s' % (thing.id_,
250 stringify_yx(thing.position)))
253 """Send turn finish signal, run game world, send new world data.
255 First sends 'TURN_FINISHED' message, then runs game world
256 until new player input is needed, then sends game state.
258 self.io.send('TURN_FINISHED ' + str(self.world.turn))
259 self.world.proceed_to_next_player_turn()
260 msg = str(self.world.get_player().last_task_result)
261 self.io.send('LAST_PLAYER_TASK_RESULT ' + self.io.quote(msg))
262 self.send_gamestate()
264 def cmd_FIB(self, numbers, connection_id):
265 """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
267 Numbers are calculated in parallel as far as possible, using fib().
268 A 'CALCULATING …' message is sent to caller before the result.
270 self.io.send('CALCULATING …', connection_id)
271 results = self.pool.map(fib, numbers)
272 reply = ' '.join([str(r) for r in results])
273 self.io.send(reply, connection_id)
274 cmd_FIB.argtypes = 'seq:int:nonneg'
276 def cmd_INC_P(self, connection_id):
277 """Increment world.turn, send game turn data to everyone.
279 To simulate game processing waiting times, a one second delay between
280 TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive
281 calculations are started as pool processes that need to be finished
282 until a further INC finishes the turn.
284 This is just a demo structure for how the game loop could work when
285 parallelized. One might imagine a two-step game turn, with a non-action
286 step determining actor tasks (the AI determinations would take the
287 place of the fib calculations here), and an action step wherein these
288 tasks are performed (where now sleep(1) is).
290 from time import sleep
291 if self.pool_result is not None:
292 self.pool_result.wait()
293 self.io.send('TURN_FINISHED ' + str(self.world.turn))
296 self.send_gamestate()
297 self.pool_result = self.pool.map_async(fib, (35, 35))
299 def cmd_MOVE(self, direction):
300 """Set player task to 'move' with direction arg, finish player turn."""
302 legal_directions = self.world.map_.get_directions()
303 if direction not in legal_directions:
304 raise parser.ArgError('Move argument must be one of: ' +
305 ', '.join(legal_directions))
306 self.world.get_player().set_task('move', direction=direction)
308 cmd_MOVE.argtypes = 'string'
311 """Set player task to 'wait', finish player turn."""
312 self.world.get_player().set_task('wait')
315 def cmd_GET_GAMESTATE(self, connection_id):
316 """Send game state jto caller."""
317 self.send_gamestate(connection_id)
319 def cmd_ECHO(self, msg, connection_id):
320 """Send msg to caller."""
321 self.io.send(msg, connection_id)
322 cmd_ECHO.argtypes = 'string'
324 def cmd_ALL(self, msg, connection_id):
325 """Send msg to all clients."""
327 cmd_ALL.argtypes = 'string'
329 def cmd_TERRAIN_LINE(self, y, terrain_line):
330 self.world.map_.set_line(y, terrain_line)
331 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'