7 class GameError(Exception):
11 def move_pos(direction, pos_yx):
14 elif direction == 'DOWN':
16 elif direction == 'RIGHT':
18 elif direction == 'LEFT':
22 class Map(game_common.Map):
24 def get_line(self, y):
26 return self.terrain[y * width:(y + 1) * width]
29 class World(game_common.World):
33 self.Thing = Thing # use local Thing class instead of game_common's
34 self.map_ = Map() # use extended child class
37 def proceed_to_next_player_turn(self):
38 """Run game world turns until player can decide their next step.
40 Iterates through all non-player things, on each step
41 furthering them in their tasks (and letting them decide new
42 ones if they finish). The iteration order is: first all things
43 that come after the player in the world things list, then
44 (after incrementing the world turn) all that come before the
45 player; then the player's .proceed() is run, and if it does
46 not finish his task, the loop starts at the beginning. Once
47 the player's task is finished, the loop breaks.
50 player = self.get_player()
51 player_i = self.things.index(player)
52 for thing in self.things[player_i+1:]:
55 for thing in self.things[:player_i]:
57 player.proceed(is_AI=False)
58 if player.task is None:
62 return self.get_thing(self.player_id)
67 def __init__(self, thing, name, args=(), kwargs={}):
75 if self.name == 'move':
76 if len(self.args) > 0:
77 direction = self.args[0]
79 direction = self.kwargs['direction']
80 test_pos = self.thing.position[:]
81 move_pos(direction, test_pos)
82 if test_pos[0] < 0 or test_pos[1] < 0 or \
83 test_pos[0] >= self.thing.world.map_.size[0] or \
84 test_pos[1] >= self.thing.world.map_.size[1]:
85 raise GameError('would move outside map bounds')
86 pos_i = test_pos[0] * self.thing.world.map_.size[1] + test_pos[1]
87 map_tile = self.thing.world.map_.terrain[pos_i]
89 raise GameError('would move into illegal terrain')
90 for t in self.thing.world.things:
91 if t.position == test_pos:
92 raise GameError('would move into other thing')
95 class Thing(game_common.Thing):
97 def __init__(self, *args, **kwargs):
98 super().__init__(*args, **kwargs)
99 self.task = Task(self, 'wait')
100 self.last_task_result = None
106 def task_move(self, direction):
107 move_pos(direction, self.position)
110 def decide_task(self):
111 if self.position[1] > 1:
112 self.set_task('move', 'LEFT')
113 elif self.position[1] < 3:
114 self.set_task('move', 'RIGHT')
116 self.set_task('wait')
118 def set_task(self, task_name, *args, **kwargs):
119 self.task = Task(self, task_name, args, kwargs)
122 def proceed(self, is_AI=True):
123 """Further the thing in its tasks.
125 Decrements .task.todo; if it thus falls to <= 0, enacts method
126 whose name is 'task_' + self.task.name and sets .task =
127 None. If is_AI, calls .decide_task to decide a self.task.
129 Before doing anything, ensures an empty map visibility stencil
130 and checks that task is still possible, and aborts it
131 otherwise (for AI things, decides a new task).
137 except GameError as e:
139 self.last_task_result = e
144 if self.task.todo <= 0:
145 task = getattr(self, 'task_' + self.task.name)
146 self.last_task_result = task(*self.task.args, **self.task.kwargs)
148 if is_AI and self.task is None:
151 def get_stencil(self):
152 if self._stencil is not None:
154 size = self.world.map_.size
155 m = Map(self.world.map_.size, '?'*size[0]*size[1])
156 y_me = self.position[0]
157 x_me = self.position[1]
158 for y in range(m.size[0]):
159 if y in (y_me - 1, y_me, y_me + 1):
160 for x in range(m.size[1]):
161 if x in (x_me - 1, x_me, x_me + 1):
162 pos = y * size[1] + x
163 m.terrain = m.terrain[:pos] + '.' + m.terrain[pos+1:]
167 def get_visible_map(self):
168 stencil = self.get_stencil()
169 size = self.world.map_.size
170 size_i = self.world.map_.size[0] * self.world.map_.size[1]
171 m = Map(size, ' '*size_i)
172 for i in range(size_i):
173 if stencil.terrain[i] == '.':
174 c = self.world.map_.terrain[i]
175 m.terrain = m.terrain[:i] + c + m.terrain[i+1:]
178 def get_visible_things(self):
179 stencil = self.get_stencil()
181 for thing in self.world.things:
182 width = self.world.map_.size[1]
183 pos_i = thing.position[0] * width + thing.position[1]
184 if stencil.terrain[pos_i] == '.':
185 visible_things += [thing]
186 return visible_things
190 """Calculate n-th Fibonacci number. Very inefficiently."""
194 return fib(n-1) + fib(n-2)
197 class CommandHandler(game_common.Commander):
199 def __init__(self, game_file_name):
202 self.parser = parser.Parser(self)
203 self.game_file_name = game_file_name
204 # self.pool and self.pool_result are currently only needed by the FIB
205 # command and the demo of a parallelized game loop in cmd_inc_p.
206 from multiprocessing import Pool
208 self.pool_result = None
210 def send(self, msg, connection_id=None):
211 """Send message msg to server's client(s) via self.queues_out.
213 If a specific client is identified by connection_id, only
214 sends msg to that one. Else, sends it to all clients
215 identified in self.queues_out.
219 self.queues_out[connection_id].put(msg)
221 for connection_id in self.queues_out:
222 self.queues_out[connection_id].put(msg)
224 def handle_input(self, input_, connection_id=None, store=True):
225 """Process input_ to command grammar, call command handler if found."""
226 from inspect import signature
228 def answer(connection_id, msg):
230 self.send(msg, connection_id)
235 command = self.parser.parse(input_)
237 answer(connection_id, 'UNHANDLED_INPUT')
239 if 'connection_id' in list(signature(command).parameters):
240 command(connection_id=connection_id)
244 with open(self.game_file_name, 'a') as f:
245 f.write(input_ + '\n')
246 except parser.ArgError as e:
247 answer(connection_id, 'ARGUMENT_ERROR ' + self.quote(str(e)))
248 except game.GameError as e:
249 answer(connection_id, 'GAME_ERROR ' + self.quote(str(e)))
251 def quote(self, string):
252 """Quote & escape string so client interprets it as single token."""
260 return ''.join(quoted)
262 def send_gamestate(self, connection_id=None):
263 """Send out game state data relevant to clients."""
265 def stringify_yx(tuple_):
266 """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
267 return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])
269 self.send('NEW_TURN ' + str(self.world.turn))
270 self.send('MAP_SIZE ' + stringify_yx(self.world.map_.size))
271 visible_map = self.world.get_player().get_visible_map()
272 for y in range(self.world.map_.size[0]):
273 self.send('VISIBLE_MAP_LINE %5s %s' %
274 (y, self.quote(visible_map.get_line(y))))
275 visible_things = self.world.get_player().get_visible_things()
276 for thing in visible_things:
277 self.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
278 self.send('THING_POS %s %s' % (thing.id_,
279 stringify_yx(thing.position)))
282 """Send turn finish signal, run game world, send new world data.
284 First sends 'TURN_FINISHED' message, then runs game world
285 until new player input is needed, then sends game state.
287 self.send('TURN_FINISHED ' + str(self.world.turn))
288 self.world.proceed_to_next_player_turn()
289 msg = str(self.world.get_player().last_task_result)
290 self.send('LAST_PLAYER_TASK_RESULT ' + self.quote(msg))
291 self.send_gamestate()
293 def cmd_FIB(self, numbers, connection_id):
294 """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
296 Numbers are calculated in parallel as far as possible, using fib().
297 A 'CALCULATING …' message is sent to caller before the result.
299 self.send('CALCULATING …', connection_id)
300 results = self.pool.map(fib, numbers)
301 reply = ' '.join([str(r) for r in results])
302 self.send(reply, connection_id)
303 cmd_FIB.argtypes = 'seq:int:nonneg'
305 def cmd_INC_P(self, connection_id):
306 """Increment world.turn, send game turn data to everyone.
308 To simulate game processing waiting times, a one second delay between
309 TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive
310 calculations are started as pool processes that need to be finished
311 until a further INC finishes the turn.
313 This is just a demo structure for how the game loop could work when
314 parallelized. One might imagine a two-step game turn, with a non-action
315 step determining actor tasks (the AI determinations would take the
316 place of the fib calculations here), and an action step wherein these
317 tasks are performed (where now sleep(1) is).
319 from time import sleep
320 if self.pool_result is not None:
321 self.pool_result.wait()
322 self.send('TURN_FINISHED ' + str(self.world.turn))
325 self.send_gamestate()
326 self.pool_result = self.pool.map_async(fib, (35, 35))
328 def cmd_MOVE(self, direction):
329 """Set player task to 'move' with direction arg, finish player turn."""
330 if direction not in {'UP', 'DOWN', 'RIGHT', 'LEFT'}:
331 raise parser.ArgError('Move argument must be one of: '
332 'UP, DOWN, RIGHT, LEFT')
333 self.world.get_player().set_task('move', direction=direction)
335 cmd_MOVE.argtypes = 'string'
338 """Set player task to 'wait', finish player turn."""
339 self.world.get_player().set_task('wait')
342 def cmd_GET_GAMESTATE(self, connection_id):
343 """Send game state jto caller."""
344 self.send_gamestate(connection_id)
346 def cmd_ECHO(self, msg, connection_id):
347 """Send msg to caller."""
348 self.send(msg, connection_id)
349 cmd_ECHO.argtypes = 'string'
351 def cmd_ALL(self, msg, connection_id):
352 """Send msg to all clients."""
354 cmd_ALL.argtypes = 'string'
356 def cmd_TERRAIN_LINE(self, y, terrain_line):
357 self.world.map_.set_line(y, terrain_line)
358 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'