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)
47 def make_new(self, geometry, yx, seed):
51 self.new_map(geometry, yx)
53 if 0 in pos or (yx[0] - 1) == pos[0] or (yx[1] - 1) == pos[1]:
56 self.map_[pos] = random.choice(('.', '.', '.', '.', 'x'))
57 player = self.Thing(self, 0)
58 player.type_ = 'human'
59 player.position = [random.randint(0, yx[0] -1),
60 random.randint(0, yx[1] - 1)]
61 npc = self.Thing(self, 1)
63 npc.position = [random.randint(0, yx[0] -1),
64 random.randint(0, yx[1] -1)]
65 self.things = [player, npc]
70 def __init__(self, thing, name, args=(), kwargs={}):
78 if self.name == 'move':
79 if len(self.args) > 0:
80 direction = self.args[0]
82 direction = self.kwargs['direction']
83 test_pos = self.thing.world.map_.move(self.thing.position, direction)
84 if self.thing.world.map_[test_pos] != '.':
85 raise GameError('would move into illegal terrain')
86 for t in self.thing.world.things:
87 if t.position == test_pos:
88 raise GameError('would move into other thing')
91 class Thing(game_common.Thing):
93 def __init__(self, *args, **kwargs):
94 super().__init__(*args, **kwargs)
95 self.task = Task(self, 'wait')
96 self.last_task_result = None
102 def task_move(self, direction):
103 self.position = self.world.map_.move(self.position, direction)
106 def decide_task(self):
107 #if self.position[1] > 1:
108 # self.set_task('move', 'LEFT')
109 #elif self.position[1] < 3:
110 # self.set_task('move', 'RIGHT')
112 self.set_task('wait')
114 def set_task(self, task_name, *args, **kwargs):
115 self.task = Task(self, task_name, args, kwargs)
118 def proceed(self, is_AI=True):
119 """Further the thing in its tasks.
121 Decrements .task.todo; if it thus falls to <= 0, enacts method
122 whose name is 'task_' + self.task.name and sets .task =
123 None. If is_AI, calls .decide_task to decide a self.task.
125 Before doing anything, ensures an empty map visibility stencil
126 and checks that task is still possible, and aborts it
127 otherwise (for AI things, decides a new task).
133 except GameError as e:
135 self.last_task_result = e
140 if self.task.todo <= 0:
141 task = getattr(self, 'task_' + self.task.name)
142 self.last_task_result = task(*self.task.args, **self.task.kwargs)
144 if is_AI and self.task is None:
147 def get_stencil(self):
148 if self._stencil is not None:
150 self._stencil = self.world.map_.get_fov_map(self.position)
153 def get_visible_map(self):
154 stencil = self.get_stencil()
155 m = self.world.map_.new_from_shape(' ')
157 if stencil[pos] == '.':
158 m[pos] = self.world.map_[pos]
161 def get_visible_things(self):
162 stencil = self.get_stencil()
164 for thing in self.world.things:
165 if stencil[thing.position] == '.':
166 visible_things += [thing]
167 return visible_things
171 """Calculate n-th Fibonacci number. Very inefficiently."""
175 return fib(n-1) + fib(n-2)
178 class Game(game_common.CommonCommandsMixin):
180 def __init__(self, game_file_name):
182 self.map_manager = server_.map_.map_manager
183 self.world = World(self)
184 self.io = server_.io.GameIO(game_file_name, self)
185 # self.pool and self.pool_result are currently only needed by the FIB
186 # command and the demo of a parallelized game loop in cmd_inc_p.
187 from multiprocessing import Pool
189 self.pool_result = None
191 def send_gamestate(self, connection_id=None):
192 """Send out game state data relevant to clients."""
194 def stringify_yx(tuple_):
195 """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
196 return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])
198 self.io.send('NEW_TURN ' + str(self.world.turn))
199 self.io.send('MAP ' + self.world.map_.geometry +\
200 ' ' + stringify_yx(self.world.map_.size))
201 visible_map = self.world.get_player().get_visible_map()
202 for y, line in visible_map.lines():
203 self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, self.io.quote(line)))
204 visible_things = self.world.get_player().get_visible_things()
205 for thing in visible_things:
206 self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
207 self.io.send('THING_POS %s %s' % (thing.id_,
208 stringify_yx(thing.position)))
209 self.io.send('VISIBLE_MAP_COMPLETE')
212 """Send turn finish signal, run game world, send new world data.
214 First sends 'TURN_FINISHED' message, then runs game world
215 until new player input is needed, then sends game state.
217 self.io.send('TURN_FINISHED ' + str(self.world.turn))
218 self.world.proceed_to_next_player_turn()
219 msg = str(self.world.get_player().last_task_result)
220 self.io.send('LAST_PLAYER_TASK_RESULT ' + self.io.quote(msg))
221 self.send_gamestate()
223 def cmd_FIB(self, numbers, connection_id):
224 """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
226 Numbers are calculated in parallel as far as possible, using fib().
227 A 'CALCULATING …' message is sent to caller before the result.
229 self.io.send('CALCULATING …', connection_id)
230 results = self.pool.map(fib, numbers)
231 reply = ' '.join([str(r) for r in results])
232 self.io.send(reply, connection_id)
233 cmd_FIB.argtypes = 'seq:int:nonneg'
235 def cmd_INC_P(self, connection_id):
236 """Increment world.turn, send game turn data to everyone.
238 To simulate game processing waiting times, a one second delay between
239 TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive
240 calculations are started as pool processes that need to be finished
241 until a further INC finishes the turn.
243 This is just a demo structure for how the game loop could work when
244 parallelized. One might imagine a two-step game turn, with a non-action
245 step determining actor tasks (the AI determinations would take the
246 place of the fib calculations here), and an action step wherein these
247 tasks are performed (where now sleep(1) is).
249 from time import sleep
250 if self.pool_result is not None:
251 self.pool_result.wait()
252 self.io.send('TURN_FINISHED ' + str(self.world.turn))
255 self.send_gamestate()
256 self.pool_result = self.pool.map_async(fib, (35, 35))
258 def cmd_MOVE(self, direction):
259 """Set player task to 'move' with direction arg, finish player turn."""
261 legal_directions = self.world.map_.get_directions()
262 if direction not in legal_directions:
263 raise parser.ArgError('Move argument must be one of: ' +
264 ', '.join(legal_directions))
265 self.world.get_player().set_task('move', direction=direction)
267 cmd_MOVE.argtypes = 'string'
270 """Set player task to 'wait', finish player turn."""
271 self.world.get_player().set_task('wait')
274 def cmd_GET_GAMESTATE(self, connection_id):
275 """Send game state jto caller."""
276 self.send_gamestate(connection_id)
278 def cmd_ECHO(self, msg, connection_id):
279 """Send msg to caller."""
280 self.io.send(msg, connection_id)
281 cmd_ECHO.argtypes = 'string'
283 def cmd_ALL(self, msg, connection_id):
284 """Send msg to all clients."""
286 cmd_ALL.argtypes = 'string'
288 def cmd_TERRAIN_LINE(self, y, terrain_line):
289 self.world.map_.set_line(y, terrain_line)
290 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'
292 def cmd_GEN_WORLD(self, geometry, yx, seed):
293 legal_grids = self.map_manager.get_map_geometries()
294 if geometry not in legal_grids:
295 raise ArgError('First map argument must be one of: ' +
296 ', '.join(legal_grids))
297 self.world.make_new(geometry, yx, seed)
298 cmd_GEN_WORLD.argtypes = 'string yx_tuple:pos string'