7 class GameError(Exception):
11 class World(game_common.World):
13 def __init__(self, game):
17 # use extended local classes
20 def proceed_to_next_player_turn(self):
21 """Run game world turns until player can decide their next step.
23 Iterates through all non-player things, on each step
24 furthering them in their tasks (and letting them decide new
25 ones if they finish). The iteration order is: first all things
26 that come after the player in the world things list, then
27 (after incrementing the world turn) all that come before the
28 player; then the player's .proceed() is run, and if it does
29 not finish his task, the loop starts at the beginning. Once
30 the player's task is finished, the loop breaks.
33 player = self.get_player()
34 player_i = self.things.index(player)
35 for thing in self.things[player_i+1:]:
38 for thing in self.things[:player_i]:
40 player.proceed(is_AI=False)
41 if player.task is None:
45 return self.get_thing(self.player_id)
50 def __init__(self, thing, name, args=(), kwargs={}):
58 if self.name == 'move':
59 if len(self.args) > 0:
60 direction = self.args[0]
62 direction = self.kwargs['direction']
63 test_pos = self.thing.world.map_.move(self.thing.position, direction)
64 if self.thing.world.map_[test_pos] != '.':
65 raise GameError('would move into illegal terrain')
66 for t in self.thing.world.things:
67 if t.position == test_pos:
68 raise GameError('would move into other thing')
71 class Thing(game_common.Thing):
73 def __init__(self, *args, **kwargs):
74 super().__init__(*args, **kwargs)
75 self.task = Task(self, 'wait')
76 self.last_task_result = None
82 def task_move(self, direction):
83 self.position = self.world.map_.move(self.position, direction)
86 def decide_task(self):
87 if self.position[1] > 1:
88 self.set_task('move', 'LEFT')
89 elif self.position[1] < 3:
90 self.set_task('move', 'RIGHT')
94 def set_task(self, task_name, *args, **kwargs):
95 self.task = Task(self, task_name, args, kwargs)
98 def proceed(self, is_AI=True):
99 """Further the thing in its tasks.
101 Decrements .task.todo; if it thus falls to <= 0, enacts method
102 whose name is 'task_' + self.task.name and sets .task =
103 None. If is_AI, calls .decide_task to decide a self.task.
105 Before doing anything, ensures an empty map visibility stencil
106 and checks that task is still possible, and aborts it
107 otherwise (for AI things, decides a new task).
113 except GameError as e:
115 self.last_task_result = e
120 if self.task.todo <= 0:
121 task = getattr(self, 'task_' + self.task.name)
122 self.last_task_result = task(*self.task.args, **self.task.kwargs)
124 if is_AI and self.task is None:
127 def get_stencil(self):
128 if self._stencil is not None:
130 m = self.world.map_.new_from_shape('?')
132 if pos == self.position or m.are_neighbors(pos, self.position):
137 def get_visible_map(self):
138 stencil = self.get_stencil()
139 m = self.world.map_.new_from_shape(' ')
141 if stencil[pos] == '.':
142 m[pos] = self.world.map_[pos]
145 def get_visible_things(self):
146 stencil = self.get_stencil()
148 for thing in self.world.things:
149 if stencil[thing.position] == '.':
150 visible_things += [thing]
151 return visible_things
155 """Calculate n-th Fibonacci number. Very inefficiently."""
159 return fib(n-1) + fib(n-2)
162 class Game(game_common.CommonCommandsMixin):
164 def __init__(self, game_file_name):
166 #self.get_map_class = server_.map_.get_map_class
167 self.map_manager = server_.map_.map_manager
168 self.world = World(self)
169 self.io = server_.io.GameIO(game_file_name, self)
170 # self.pool and self.pool_result are currently only needed by the FIB
171 # command and the demo of a parallelized game loop in cmd_inc_p.
172 from multiprocessing import Pool
174 self.pool_result = None
176 def send_gamestate(self, connection_id=None):
177 """Send out game state data relevant to clients."""
179 def stringify_yx(tuple_):
180 """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
181 return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])
183 self.io.send('NEW_TURN ' + str(self.world.turn))
184 grid = self.world.map_.__class__.__name__[3:]
185 self.io.send('MAP ' + grid +' ' + stringify_yx(self.world.map_.size))
186 visible_map = self.world.get_player().get_visible_map()
187 for y, line in visible_map.lines():
188 self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, self.io.quote(line)))
189 visible_things = self.world.get_player().get_visible_things()
190 for thing in visible_things:
191 self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
192 self.io.send('THING_POS %s %s' % (thing.id_,
193 stringify_yx(thing.position)))
196 """Send turn finish signal, run game world, send new world data.
198 First sends 'TURN_FINISHED' message, then runs game world
199 until new player input is needed, then sends game state.
201 self.io.send('TURN_FINISHED ' + str(self.world.turn))
202 self.world.proceed_to_next_player_turn()
203 msg = str(self.world.get_player().last_task_result)
204 self.io.send('LAST_PLAYER_TASK_RESULT ' + self.io.quote(msg))
205 self.send_gamestate()
207 def cmd_FIB(self, numbers, connection_id):
208 """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
210 Numbers are calculated in parallel as far as possible, using fib().
211 A 'CALCULATING …' message is sent to caller before the result.
213 self.io.send('CALCULATING …', connection_id)
214 results = self.pool.map(fib, numbers)
215 reply = ' '.join([str(r) for r in results])
216 self.io.send(reply, connection_id)
217 cmd_FIB.argtypes = 'seq:int:nonneg'
219 def cmd_INC_P(self, connection_id):
220 """Increment world.turn, send game turn data to everyone.
222 To simulate game processing waiting times, a one second delay between
223 TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive
224 calculations are started as pool processes that need to be finished
225 until a further INC finishes the turn.
227 This is just a demo structure for how the game loop could work when
228 parallelized. One might imagine a two-step game turn, with a non-action
229 step determining actor tasks (the AI determinations would take the
230 place of the fib calculations here), and an action step wherein these
231 tasks are performed (where now sleep(1) is).
233 from time import sleep
234 if self.pool_result is not None:
235 self.pool_result.wait()
236 self.io.send('TURN_FINISHED ' + str(self.world.turn))
239 self.send_gamestate()
240 self.pool_result = self.pool.map_async(fib, (35, 35))
242 def cmd_MOVE(self, direction):
243 """Set player task to 'move' with direction arg, finish player turn."""
245 legal_directions = self.world.map_.get_directions()
246 if direction not in legal_directions:
247 raise parser.ArgError('Move argument must be one of: ' +
248 ', '.join(legal_directions))
249 self.world.get_player().set_task('move', direction=direction)
251 cmd_MOVE.argtypes = 'string'
254 """Set player task to 'wait', finish player turn."""
255 self.world.get_player().set_task('wait')
258 def cmd_GET_GAMESTATE(self, connection_id):
259 """Send game state jto caller."""
260 self.send_gamestate(connection_id)
262 def cmd_ECHO(self, msg, connection_id):
263 """Send msg to caller."""
264 self.io.send(msg, connection_id)
265 cmd_ECHO.argtypes = 'string'
267 def cmd_ALL(self, msg, connection_id):
268 """Send msg to all clients."""
270 cmd_ALL.argtypes = 'string'
272 def cmd_TERRAIN_LINE(self, y, terrain_line):
273 self.world.map_.set_line(y, terrain_line)
274 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'