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 get_position_index(self, yx):
48 return yx[0] * self.size[1] + yx[1]
50 def new_from_shape(self, init_char):
51 return Map(self.size, init_char*self.size_i)
53 def are_neighbors(self, pos_1, pos_2):
54 return abs(pos_1[0] - pos_2[0]) <= 1 and abs(pos_1[1] - pos_2[1] <= 1)
56 def move(self, start_pos, direction):
57 mover = getattr(self, 'move_' + direction)
58 new_pos = mover(start_pos)
59 if new_pos[0] < 0 or new_pos[1] < 0 or \
60 new_pos[0] >= self.size[0] or new_pos[1] >= self.size[1]:
61 raise GameError('would move outside map bounds')
64 def move_UP(self, start_pos):
65 return [start_pos[0] - 1, start_pos[1]]
67 def move_DOWN(self, start_pos):
68 return [start_pos[0] + 1, start_pos[1]]
70 def move_LEFT(self, start_pos):
71 return [start_pos[0], start_pos[1] - 1]
73 def move_RIGHT(self, start_pos):
74 return [start_pos[0], start_pos[1] + 1]
77 class World(game_common.World):
81 self.Thing = Thing # use local Thing class instead of game_common's
82 self.map_ = Map() # use extended child class
85 def proceed_to_next_player_turn(self):
86 """Run game world turns until player can decide their next step.
88 Iterates through all non-player things, on each step
89 furthering them in their tasks (and letting them decide new
90 ones if they finish). The iteration order is: first all things
91 that come after the player in the world things list, then
92 (after incrementing the world turn) all that come before the
93 player; then the player's .proceed() is run, and if it does
94 not finish his task, the loop starts at the beginning. Once
95 the player's task is finished, the loop breaks.
98 player = self.get_player()
99 player_i = self.things.index(player)
100 for thing in self.things[player_i+1:]:
103 for thing in self.things[:player_i]:
105 player.proceed(is_AI=False)
106 if player.task is None:
109 def get_player(self):
110 return self.get_thing(self.player_id)
115 def __init__(self, thing, name, args=(), kwargs={}):
123 if self.name == 'move':
124 if len(self.args) > 0:
125 direction = self.args[0]
127 direction = self.kwargs['direction']
128 test_pos = self.thing.world.map_.move(self.thing.position, direction)
129 if self.thing.world.map_[test_pos] != '.':
130 raise GameError('would move into illegal terrain')
131 for t in self.thing.world.things:
132 if t.position == test_pos:
133 raise GameError('would move into other thing')
136 class Thing(game_common.Thing):
138 def __init__(self, *args, **kwargs):
139 super().__init__(*args, **kwargs)
140 self.task = Task(self, 'wait')
141 self.last_task_result = None
147 def task_move(self, direction):
148 self.position = self.world.map_.move(self.position, direction)
151 def decide_task(self):
152 if self.position[1] > 1:
153 self.set_task('move', 'LEFT')
154 elif self.position[1] < 3:
155 self.set_task('move', 'RIGHT')
157 self.set_task('wait')
159 def set_task(self, task_name, *args, **kwargs):
160 self.task = Task(self, task_name, args, kwargs)
163 def proceed(self, is_AI=True):
164 """Further the thing in its tasks.
166 Decrements .task.todo; if it thus falls to <= 0, enacts method
167 whose name is 'task_' + self.task.name and sets .task =
168 None. If is_AI, calls .decide_task to decide a self.task.
170 Before doing anything, ensures an empty map visibility stencil
171 and checks that task is still possible, and aborts it
172 otherwise (for AI things, decides a new task).
178 except GameError as e:
180 self.last_task_result = e
185 if self.task.todo <= 0:
186 task = getattr(self, 'task_' + self.task.name)
187 self.last_task_result = task(*self.task.args, **self.task.kwargs)
189 if is_AI and self.task is None:
192 def get_stencil(self):
193 if self._stencil is not None:
195 m = self.world.map_.new_from_shape('?')
197 if pos == self.position or m.are_neighbors(pos, self.position):
202 def get_visible_map(self):
203 stencil = self.get_stencil()
204 m = self.world.map_.new_from_shape(' ')
206 if stencil[pos] == '.':
207 m[pos] = self.world.map_[pos]
210 def get_visible_things(self):
211 stencil = self.get_stencil()
213 for thing in self.world.things:
214 if stencil[thing.position] == '.':
215 visible_things += [thing]
216 return visible_things
220 """Calculate n-th Fibonacci number. Very inefficiently."""
224 return fib(n-1) + fib(n-2)
227 class Game(game_common.CommonCommandsMixin):
229 def __init__(self, game_file_name):
232 self.io = server_.io.GameIO(game_file_name, self)
233 # self.pool and self.pool_result are currently only needed by the FIB
234 # command and the demo of a parallelized game loop in cmd_inc_p.
235 from multiprocessing import Pool
237 self.pool_result = None
239 def send_gamestate(self, connection_id=None):
240 """Send out game state data relevant to clients."""
242 def stringify_yx(tuple_):
243 """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
244 return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])
246 self.io.send('NEW_TURN ' + str(self.world.turn))
247 self.io.send('MAP_SIZE ' + stringify_yx(self.world.map_.size))
248 visible_map = self.world.get_player().get_visible_map()
249 for y, line in visible_map.lines():
250 self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, self.io.quote(line)))
251 visible_things = self.world.get_player().get_visible_things()
252 for thing in visible_things:
253 self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
254 self.io.send('THING_POS %s %s' % (thing.id_,
255 stringify_yx(thing.position)))
258 """Send turn finish signal, run game world, send new world data.
260 First sends 'TURN_FINISHED' message, then runs game world
261 until new player input is needed, then sends game state.
263 self.io.send('TURN_FINISHED ' + str(self.world.turn))
264 self.world.proceed_to_next_player_turn()
265 msg = str(self.world.get_player().last_task_result)
266 self.io.send('LAST_PLAYER_TASK_RESULT ' + self.io.quote(msg))
267 self.send_gamestate()
269 def cmd_FIB(self, numbers, connection_id):
270 """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
272 Numbers are calculated in parallel as far as possible, using fib().
273 A 'CALCULATING …' message is sent to caller before the result.
275 self.io.send('CALCULATING …', connection_id)
276 results = self.pool.map(fib, numbers)
277 reply = ' '.join([str(r) for r in results])
278 self.io.send(reply, connection_id)
279 cmd_FIB.argtypes = 'seq:int:nonneg'
281 def cmd_INC_P(self, connection_id):
282 """Increment world.turn, send game turn data to everyone.
284 To simulate game processing waiting times, a one second delay between
285 TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive
286 calculations are started as pool processes that need to be finished
287 until a further INC finishes the turn.
289 This is just a demo structure for how the game loop could work when
290 parallelized. One might imagine a two-step game turn, with a non-action
291 step determining actor tasks (the AI determinations would take the
292 place of the fib calculations here), and an action step wherein these
293 tasks are performed (where now sleep(1) is).
295 from time import sleep
296 if self.pool_result is not None:
297 self.pool_result.wait()
298 self.io.send('TURN_FINISHED ' + str(self.world.turn))
301 self.send_gamestate()
302 self.pool_result = self.pool.map_async(fib, (35, 35))
304 def cmd_MOVE(self, direction):
305 """Set player task to 'move' with direction arg, finish player turn."""
307 legal_directions = self.world.map_.get_directions()
308 if direction not in legal_directions:
309 raise parser.ArgError('Move argument must be one of: ' +
310 ', '.join(legal_directions))
311 self.world.get_player().set_task('move', direction=direction)
313 cmd_MOVE.argtypes = 'string'
316 """Set player task to 'wait', finish player turn."""
317 self.world.get_player().set_task('wait')
320 def cmd_GET_GAMESTATE(self, connection_id):
321 """Send game state jto caller."""
322 self.send_gamestate(connection_id)
324 def cmd_ECHO(self, msg, connection_id):
325 """Send msg to caller."""
326 self.io.send(msg, connection_id)
327 cmd_ECHO.argtypes = 'string'
329 def cmd_ALL(self, msg, connection_id):
330 """Send msg to all clients."""
332 cmd_ALL.argtypes = 'string'
334 def cmd_TERRAIN_LINE(self, y, terrain_line):
335 self.world.map_.set_line(y, terrain_line)
336 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'