6 class GameError(Exception):
10 def move_pos(direction, pos_yx):
13 elif direction == 'DOWN':
15 elif direction == 'RIGHT':
17 elif direction == 'LEFT':
21 class Map(game_common.Map):
23 def get_line(self, y):
25 return self.terrain[y * width:(y + 1) * width]
28 class World(game_common.World):
32 self.Thing = Thing # use local Thing class instead of game_common's
33 self.map_ = Map() # use extended child class
36 def proceed_to_next_player_turn(self):
37 """Run game world turns until player can decide their next step.
39 Iterates through all non-player things, on each step
40 furthering them in their tasks (and letting them decide new
41 ones if they finish). The iteration order is: first all things
42 that come after the player in the world things list, then
43 (after incrementing the world turn) all that come before the
44 player; then the player's .proceed() is run, and if it does
45 not finish his task, the loop starts at the beginning. Once
46 the player's task is finished, the loop breaks.
49 player = self.get_player()
50 player_i = self.things.index(player)
51 for thing in self.things[player_i+1:]:
54 for thing in self.things[:player_i]:
56 player.proceed(is_AI=False)
57 if player.task is None:
61 return self.get_thing(self.player_id)
66 def __init__(self, thing, name, args=(), kwargs={}):
74 if self.name == 'move':
75 if len(self.args) > 0:
76 direction = self.args[0]
78 direction = self.kwargs['direction']
79 test_pos = self.thing.position[:]
80 move_pos(direction, test_pos)
81 if test_pos[0] < 0 or test_pos[1] < 0 or \
82 test_pos[0] >= self.thing.world.map_.size[0] or \
83 test_pos[1] >= self.thing.world.map_.size[1]:
84 raise GameError('would move outside map bounds')
85 pos_i = test_pos[0] * self.thing.world.map_.size[1] + test_pos[1]
86 map_tile = self.thing.world.map_.terrain[pos_i]
88 raise GameError('would move into illegal terrain')
89 for t in self.thing.world.things:
90 if t.position == test_pos:
91 raise GameError('would move into other thing')
94 class Thing(game_common.Thing):
96 def __init__(self, *args, **kwargs):
97 super().__init__(*args, **kwargs)
98 self.task = Task(self, 'wait')
99 self.last_task_result = None
105 def task_move(self, direction):
106 move_pos(direction, self.position)
109 def decide_task(self):
110 if self.position[1] > 1:
111 self.set_task('move', 'LEFT')
112 elif self.position[1] < 3:
113 self.set_task('move', 'RIGHT')
115 self.set_task('wait')
117 def set_task(self, task_name, *args, **kwargs):
118 self.task = Task(self, task_name, args, kwargs)
121 def proceed(self, is_AI=True):
122 """Further the thing in its tasks.
124 Decrements .task.todo; if it thus falls to <= 0, enacts method
125 whose name is 'task_' + self.task.name and sets .task =
126 None. If is_AI, calls .decide_task to decide a self.task.
128 Before doing anything, ensures an empty map visibility stencil
129 and checks that task is still possible, and aborts it
130 otherwise (for AI things, decides a new task).
136 except GameError as e:
138 self.last_task_result = e
143 if self.task.todo <= 0:
144 task = getattr(self, 'task_' + self.task.name)
145 self.last_task_result = task(*self.task.args, **self.task.kwargs)
147 if is_AI and self.task is None:
150 def get_stencil(self):
151 if self._stencil is not None:
153 size = self.world.map_.size
154 m = Map(self.world.map_.size, '?'*size[0]*size[1])
155 y_me = self.position[0]
156 x_me = self.position[1]
157 for y in range(m.size[0]):
158 if y in (y_me - 1, y_me, y_me + 1):
159 for x in range(m.size[1]):
160 if x in (x_me - 1, x_me, x_me + 1):
161 pos = y * size[1] + x
162 m.terrain = m.terrain[:pos] + '.' + m.terrain[pos+1:]
166 def get_visible_map(self):
167 stencil = self.get_stencil()
168 size = self.world.map_.size
169 size_i = self.world.map_.size[0] * self.world.map_.size[1]
170 m = Map(size, ' '*size_i)
171 for i in range(size_i):
172 if stencil.terrain[i] == '.':
173 c = self.world.map_.terrain[i]
174 m.terrain = m.terrain[:i] + c + m.terrain[i+1:]
177 def get_visible_things(self):
178 stencil = self.get_stencil()
180 for thing in self.world.things:
181 width = self.world.map_.size[1]
182 pos_i = thing.position[0] * width + thing.position[1]
183 if stencil.terrain[pos_i] == '.':
184 visible_things += [thing]
185 return visible_things
189 """Calculate n-th Fibonacci number. Very inefficiently."""
193 return fib(n-1) + fib(n-2)
196 class Game(game_common.CommonCommandsMixin):
198 def __init__(self, game_file_name):
201 self.io = server_.io.GameIO(game_file_name, self)
202 # self.pool and self.pool_result are currently only needed by the FIB
203 # command and the demo of a parallelized game loop in cmd_inc_p.
204 from multiprocessing import Pool
206 self.pool_result = None
208 def send_gamestate(self, connection_id=None):
209 """Send out game state data relevant to clients."""
211 def stringify_yx(tuple_):
212 """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
213 return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])
215 self.io.send('NEW_TURN ' + str(self.world.turn))
216 self.io.send('MAP_SIZE ' + stringify_yx(self.world.map_.size))
217 visible_map = self.world.get_player().get_visible_map()
218 for y in range(self.world.map_.size[0]):
219 self.io.send('VISIBLE_MAP_LINE %5s %s' %
220 (y, self.io.quote(visible_map.get_line(y))))
221 visible_things = self.world.get_player().get_visible_things()
222 for thing in visible_things:
223 self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
224 self.io.send('THING_POS %s %s' % (thing.id_,
225 stringify_yx(thing.position)))
228 """Send turn finish signal, run game world, send new world data.
230 First sends 'TURN_FINISHED' message, then runs game world
231 until new player input is needed, then sends game state.
233 self.io.send('TURN_FINISHED ' + str(self.world.turn))
234 self.world.proceed_to_next_player_turn()
235 msg = str(self.world.get_player().last_task_result)
236 self.io.send('LAST_PLAYER_TASK_RESULT ' + self.io.quote(msg))
237 self.send_gamestate()
239 def cmd_FIB(self, numbers, connection_id):
240 """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
242 Numbers are calculated in parallel as far as possible, using fib().
243 A 'CALCULATING …' message is sent to caller before the result.
245 self.io.send('CALCULATING …', connection_id)
246 results = self.pool.map(fib, numbers)
247 reply = ' '.join([str(r) for r in results])
248 self.io.send(reply, connection_id)
249 cmd_FIB.argtypes = 'seq:int:nonneg'
251 def cmd_INC_P(self, connection_id):
252 """Increment world.turn, send game turn data to everyone.
254 To simulate game processing waiting times, a one second delay between
255 TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive
256 calculations are started as pool processes that need to be finished
257 until a further INC finishes the turn.
259 This is just a demo structure for how the game loop could work when
260 parallelized. One might imagine a two-step game turn, with a non-action
261 step determining actor tasks (the AI determinations would take the
262 place of the fib calculations here), and an action step wherein these
263 tasks are performed (where now sleep(1) is).
265 from time import sleep
266 if self.pool_result is not None:
267 self.pool_result.wait()
268 self.io.send('TURN_FINISHED ' + str(self.world.turn))
271 self.send_gamestate()
272 self.pool_result = self.pool.map_async(fib, (35, 35))
274 def cmd_MOVE(self, direction):
275 """Set player task to 'move' with direction arg, finish player turn."""
277 if direction not in {'UP', 'DOWN', 'RIGHT', 'LEFT'}:
278 raise parser.ArgError('Move argument must be one of: '
279 'UP, DOWN, RIGHT, LEFT')
280 self.world.get_player().set_task('move', direction=direction)
282 cmd_MOVE.argtypes = 'string'
285 """Set player task to 'wait', finish player turn."""
286 self.world.get_player().set_task('wait')
289 def cmd_GET_GAMESTATE(self, connection_id):
290 """Send game state jto caller."""
291 self.send_gamestate(connection_id)
293 def cmd_ECHO(self, msg, connection_id):
294 """Send msg to caller."""
295 self.io.send(msg, connection_id)
296 cmd_ECHO.argtypes = 'string'
298 def cmd_ALL(self, msg, connection_id):
299 """Send msg to all clients."""
301 cmd_ALL.argtypes = 'string'
303 def cmd_TERRAIN_LINE(self, y, terrain_line):
304 self.world.map_.set_line(y, terrain_line)
305 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'