5 from parser import ArgError
8 class GameError(Exception):
12 class World(game_common.World):
14 def __init__(self, game):
18 # use extended local classes
21 def proceed_to_next_player_turn(self):
22 """Run game world turns until player can decide their next step.
24 Iterates through all non-player things, on each step
25 furthering them in their tasks (and letting them decide new
26 ones if they finish). The iteration order is: first all things
27 that come after the player in the world things list, then
28 (after incrementing the world turn) all that come before the
29 player; then the player's .proceed() is run, and if it does
30 not finish his task, the loop starts at the beginning. Once
31 the player's task is finished, the loop breaks.
34 player = self.get_player()
35 player_i = self.things.index(player)
36 for thing in self.things[player_i+1:]:
39 for thing in self.things[:player_i]:
41 player.proceed(is_AI=False)
42 if player.task is None:
46 return self.get_thing(self.player_id)
48 def make_new(self, geometry, yx, seed):
52 self.new_map(geometry, yx)
54 if 0 in pos or (yx[0] - 1) == pos[0] or (yx[1] - 1) == pos[1]:
57 self.map_[pos] = random.choice(('.', '.', '.', '.', 'x'))
58 player = self.Thing(self, 0)
59 player.type_ = 'human'
60 player.position = [random.randint(0, yx[0] -1),
61 random.randint(0, yx[1] - 1)]
62 npc = self.Thing(self, 1)
64 npc.position = [random.randint(0, yx[0] -1),
65 random.randint(0, yx[1] -1)]
66 self.things = [player, npc]
71 def __init__(self, thing, name, args=(), kwargs={}):
79 if self.name == 'move':
80 if len(self.args) > 0:
81 direction = self.args[0]
83 direction = self.kwargs['direction']
84 test_pos = self.thing.world.map_.move(self.thing.position, direction)
85 if self.thing.world.map_[test_pos] != '.':
86 raise GameError('would move into illegal terrain')
87 for t in self.thing.world.things:
88 if t.position == test_pos:
89 raise GameError('would move into other thing')
92 class Thing(game_common.Thing):
94 def __init__(self, *args, **kwargs):
95 super().__init__(*args, **kwargs)
96 self.task = Task(self, 'wait')
97 self.last_task_result = None
103 def task_move(self, direction):
104 self.position = self.world.map_.move(self.position, direction)
107 def decide_task(self):
108 #if self.position[1] > 1:
109 # self.set_task('move', 'LEFT')
110 #elif self.position[1] < 3:
111 # self.set_task('move', 'RIGHT')
113 self.set_task('wait')
115 def set_task(self, task_name, *args, **kwargs):
116 self.task = Task(self, task_name, args, kwargs)
119 def proceed(self, is_AI=True):
120 """Further the thing in its tasks.
122 Decrements .task.todo; if it thus falls to <= 0, enacts method
123 whose name is 'task_' + self.task.name and sets .task =
124 None. If is_AI, calls .decide_task to decide a self.task.
126 Before doing anything, ensures an empty map visibility stencil
127 and checks that task is still possible, and aborts it
128 otherwise (for AI things, decides a new task).
134 except GameError as e:
136 self.last_task_result = e
141 if self.task.todo <= 0:
142 task = getattr(self, 'task_' + self.task.name)
143 self.last_task_result = task(*self.task.args, **self.task.kwargs)
145 if is_AI and self.task is None:
148 def get_stencil(self):
149 if self._stencil is not None:
151 self._stencil = self.world.map_.get_fov_map(self.position)
154 def get_visible_map(self):
155 stencil = self.get_stencil()
156 m = self.world.map_.new_from_shape(' ')
158 if stencil[pos] == '.':
159 m[pos] = self.world.map_[pos]
162 def get_visible_things(self):
163 stencil = self.get_stencil()
165 for thing in self.world.things:
166 if stencil[thing.position] == '.':
167 visible_things += [thing]
168 return visible_things
172 """Calculate n-th Fibonacci number. Very inefficiently."""
176 return fib(n-1) + fib(n-2)
179 class Game(game_common.CommonCommandsMixin):
181 def __init__(self, game_file_name):
183 self.map_manager = server_.map_.map_manager
184 self.world = World(self)
185 self.io = server_.io.GameIO(game_file_name, self)
186 # self.pool and self.pool_result are currently only needed by the FIB
187 # command and the demo of a parallelized game loop in cmd_inc_p.
188 from multiprocessing import Pool
190 self.pool_result = None
192 def send_gamestate(self, connection_id=None):
193 """Send out game state data relevant to clients."""
195 def stringify_yx(tuple_):
196 """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
197 return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])
199 self.io.send('NEW_TURN ' + str(self.world.turn))
200 self.io.send('MAP ' + self.world.map_.geometry +\
201 ' ' + stringify_yx(self.world.map_.size))
202 visible_map = self.world.get_player().get_visible_map()
203 for y, line in visible_map.lines():
204 self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, self.io.quote(line)))
205 visible_things = self.world.get_player().get_visible_things()
206 for thing in visible_things:
207 self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
208 self.io.send('THING_POS %s %s' % (thing.id_,
209 stringify_yx(thing.position)))
210 player = self.world.get_player()
211 self.io.send('PLAYER_POS %s' % (stringify_yx(player.position)))
212 self.io.send('GAME_STATE_COMPLETE')
215 """Send turn finish signal, run game world, send new world data.
217 First sends 'TURN_FINISHED' message, then runs game world
218 until new player input is needed, then sends game state.
220 self.io.send('TURN_FINISHED ' + str(self.world.turn))
221 self.world.proceed_to_next_player_turn()
222 msg = str(self.world.get_player().last_task_result)
223 self.io.send('LAST_PLAYER_TASK_RESULT ' + self.io.quote(msg))
224 self.send_gamestate()
226 def cmd_FIB(self, numbers, connection_id):
227 """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
229 Numbers are calculated in parallel as far as possible, using fib().
230 A 'CALCULATING …' message is sent to caller before the result.
232 self.io.send('CALCULATING …', connection_id)
233 results = self.pool.map(fib, numbers)
234 reply = ' '.join([str(r) for r in results])
235 self.io.send(reply, connection_id)
236 cmd_FIB.argtypes = 'seq:int:nonneg'
238 def cmd_INC_P(self, connection_id):
239 """Increment world.turn, send game turn data to everyone.
241 To simulate game processing waiting times, a one second delay between
242 TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive
243 calculations are started as pool processes that need to be finished
244 until a further INC finishes the turn.
246 This is just a demo structure for how the game loop could work when
247 parallelized. One might imagine a two-step game turn, with a non-action
248 step determining actor tasks (the AI determinations would take the
249 place of the fib calculations here), and an action step wherein these
250 tasks are performed (where now sleep(1) is).
252 from time import sleep
253 if self.pool_result is not None:
254 self.pool_result.wait()
255 self.io.send('TURN_FINISHED ' + str(self.world.turn))
258 self.send_gamestate()
259 self.pool_result = self.pool.map_async(fib, (35, 35))
261 def cmd_MOVE(self, direction):
262 """Set player task to 'move' with direction arg, finish player turn."""
264 legal_directions = self.world.map_.get_directions()
265 if direction not in legal_directions:
266 raise parser.ArgError('Move argument must be one of: ' +
267 ', '.join(legal_directions))
268 self.world.get_player().set_task('move', direction=direction)
270 cmd_MOVE.argtypes = 'string'
273 """Set player task to 'wait', finish player turn."""
274 self.world.get_player().set_task('wait')
277 def cmd_GET_GAMESTATE(self, connection_id):
278 """Send game state to caller."""
279 self.send_gamestate(connection_id)
281 def cmd_ECHO(self, msg, connection_id):
282 """Send msg to caller."""
283 self.io.send(msg, connection_id)
284 cmd_ECHO.argtypes = 'string'
286 def cmd_ALL(self, msg, connection_id):
287 """Send msg to all clients."""
289 cmd_ALL.argtypes = 'string'
291 def cmd_TERRAIN_LINE(self, y, terrain_line):
292 self.world.map_.set_line(y, terrain_line)
293 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'
295 def cmd_GEN_WORLD(self, geometry, yx, seed):
296 legal_grids = self.map_manager.get_map_geometries()
297 if geometry not in legal_grids:
298 raise ArgError('First map argument must be one of: ' +
299 ', '.join(legal_grids))
300 self.world.make_new(geometry, yx, seed)
301 cmd_GEN_WORLD.argtypes = 'string yx_tuple:pos string'