home · contact · privacy
Improve Map class.
[plomrogue2-experiments] / server_ / game.py
1 import sys
2 sys.path.append('../')
3 import game_common
4
5
6 class GameError(Exception):
7     pass
8
9
10 class Map(game_common.Map):
11
12     def __getitem__(self, yx):
13         return self.terrain[self.get_position_index(yx)]
14
15     def __setitem__(self, yx, c):
16         pos_i = self.get_position_index(yx)
17         self.terrain = self.terrain[:pos_i] + c + self.terrain[pos_i + 1:]
18
19     def __iter__(self):
20         """Iterate over YX position coordinates."""
21         for y in range(self.size[0]):
22             for x in range(self.size[1]):
23                 yield [y, x]
24
25     def lines(self):
26         width = self.size[1]
27         for y in range(self.size[0]):
28             yield (y, self.terrain[y * width:(y + 1) * width])
29
30     # The following is used nowhere, so not implemented.
31     #def items(self):
32     #    for y in range(self.size[0]):
33     #        for x in range(self.size[1]):
34     #            yield ([y, x], self.terrain[self.get_position_index([y, x])])
35
36     @property
37     def size_i(self):
38         return self.size[0] * self.size[1]
39
40     def get_directions(self):
41         directions = []
42         for name in dir(self):
43             if name[:5] == 'move_':
44                 directions += [name[5:]]
45         return directions
46
47     def get_position_index(self, yx):
48         return yx[0] * self.size[1] + yx[1]
49
50     def new_from_shape(self, init_char):
51         return Map(self.size, init_char*self.size_i)
52
53     def are_neighbors(self, pos_1, pos_2):
54         return abs(pos_1[0] - pos_2[0]) <= 1 and abs(pos_1[1] - pos_2[1] <= 1)
55
56     def move(self, start_pos, direction):
57         mover = getattr(self, 'move_' + direction)
58         new_pos = mover(start_pos)
59         if new_pos[0] < 0 or new_pos[1] < 0 or \
60                 new_pos[0] >= self.size[0] or new_pos[1] >= self.size[1]:
61             raise GameError('would move outside map bounds')
62         return new_pos
63
64     def move_UP(self, start_pos):
65         return [start_pos[0] - 1, start_pos[1]]
66
67     def move_DOWN(self, start_pos):
68         return [start_pos[0] + 1, start_pos[1]]
69
70     def move_LEFT(self, start_pos):
71         return [start_pos[0], start_pos[1] - 1]
72
73     def move_RIGHT(self, start_pos):
74         return [start_pos[0], start_pos[1] + 1]
75
76
77 class World(game_common.World):
78
79     def __init__(self):
80         super().__init__()
81         self.Thing = Thing  # use local Thing class instead of game_common's
82         self.map_ = Map()  # use extended child class
83         self.player_id = 0
84
85     def proceed_to_next_player_turn(self):
86         """Run game world turns until player can decide their next step.
87
88         Iterates through all non-player things, on each step
89         furthering them in their tasks (and letting them decide new
90         ones if they finish). The iteration order is: first all things
91         that come after the player in the world things list, then
92         (after incrementing the world turn) all that come before the
93         player; then the player's .proceed() is run, and if it does
94         not finish his task, the loop starts at the beginning. Once
95         the player's task is finished, the loop breaks.
96         """
97         while True:
98             player = self.get_player()
99             player_i = self.things.index(player)
100             for thing in self.things[player_i+1:]:
101                 thing.proceed()
102             self.turn += 1
103             for thing in self.things[:player_i]:
104                 thing.proceed()
105             player.proceed(is_AI=False)
106             if player.task is None:
107                 break
108
109     def get_player(self):
110         return self.get_thing(self.player_id)
111
112
113 class Task:
114
115     def __init__(self, thing, name, args=(), kwargs={}):
116         self.name = name
117         self.thing = thing
118         self.args = args
119         self.kwargs = kwargs
120         self.todo = 3
121
122     def check(self):
123         if self.name == 'move':
124             if len(self.args) > 0:
125                 direction = self.args[0]
126             else:
127                 direction = self.kwargs['direction']
128             test_pos = self.thing.world.map_.move(self.thing.position, direction)
129             if self.thing.world.map_[test_pos] != '.':
130                 raise GameError('would move into illegal terrain')
131             for t in self.thing.world.things:
132                 if t.position == test_pos:
133                     raise GameError('would move into other thing')
134
135
136 class Thing(game_common.Thing):
137
138     def __init__(self, *args, **kwargs):
139         super().__init__(*args, **kwargs)
140         self.task = Task(self, 'wait')
141         self.last_task_result = None
142         self._stencil = None
143
144     def task_wait(self):
145         return 'success'
146
147     def task_move(self, direction):
148         self.position = self.world.map_.move(self.position, direction)
149         return 'success'
150
151     def decide_task(self):
152         if self.position[1] > 1:
153             self.set_task('move', 'LEFT')
154         elif self.position[1] < 3:
155             self.set_task('move', 'RIGHT')
156         else:
157             self.set_task('wait')
158
159     def set_task(self, task_name, *args, **kwargs):
160         self.task = Task(self, task_name, args, kwargs)
161         self.task.check()
162
163     def proceed(self, is_AI=True):
164         """Further the thing in its tasks.
165
166         Decrements .task.todo; if it thus falls to <= 0, enacts method
167         whose name is 'task_' + self.task.name and sets .task =
168         None. If is_AI, calls .decide_task to decide a self.task.
169
170         Before doing anything, ensures an empty map visibility stencil
171         and checks that task is still possible, and aborts it
172         otherwise (for AI things, decides a new task).
173
174         """
175         self._stencil = None
176         try:
177             self.task.check()
178         except GameError as e:
179             self.task = None
180             self.last_task_result = e
181             if is_AI:
182                 self.decide_task()
183             return
184         self.task.todo -= 1
185         if self.task.todo <= 0:
186             task = getattr(self, 'task_' + self.task.name)
187             self.last_task_result = task(*self.task.args, **self.task.kwargs)
188             self.task = None
189         if is_AI and self.task is None:
190             self.decide_task()
191
192     def get_stencil(self):
193         if self._stencil is not None:
194             return self._stencil
195         m = self.world.map_.new_from_shape('?')
196         for pos in m:
197             if pos == self.position or m.are_neighbors(pos, self.position):
198                 m[pos] = '.'
199         self._stencil = m
200         return self._stencil
201
202     def get_visible_map(self):
203         stencil = self.get_stencil()
204         m = self.world.map_.new_from_shape(' ')
205         for pos in m:
206             if stencil[pos] == '.':
207                 m[pos] = self.world.map_[pos]
208         return m
209
210     def get_visible_things(self):
211         stencil = self.get_stencil()
212         visible_things = []
213         for thing in self.world.things:
214             if stencil[thing.position] == '.':
215                 visible_things += [thing]
216         return visible_things
217
218
219 def fib(n):
220     """Calculate n-th Fibonacci number. Very inefficiently."""
221     if n in (1, 2):
222         return 1
223     else:
224         return fib(n-1) + fib(n-2)
225
226
227 class Game(game_common.CommonCommandsMixin):
228
229     def __init__(self, game_file_name):
230         import server_.io
231         self.world = World()
232         self.io = server_.io.GameIO(game_file_name, self)
233         # self.pool and self.pool_result are currently only needed by the FIB
234         # command and the demo of a parallelized game loop in cmd_inc_p.
235         from multiprocessing import Pool
236         self.pool = Pool()
237         self.pool_result = None
238
239     def send_gamestate(self, connection_id=None):
240         """Send out game state data relevant to clients."""
241
242         def stringify_yx(tuple_):
243             """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
244             return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])
245
246         self.io.send('NEW_TURN ' + str(self.world.turn))
247         self.io.send('MAP_SIZE ' + stringify_yx(self.world.map_.size))
248         visible_map = self.world.get_player().get_visible_map()
249         for y, line in visible_map.lines():
250             self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, self.io.quote(line)))
251         visible_things = self.world.get_player().get_visible_things()
252         for thing in visible_things:
253             self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
254             self.io.send('THING_POS %s %s' % (thing.id_,
255                                               stringify_yx(thing.position)))
256
257     def proceed(self):
258         """Send turn finish signal, run game world, send new world data.
259
260         First sends 'TURN_FINISHED' message, then runs game world
261         until new player input is needed, then sends game state.
262         """
263         self.io.send('TURN_FINISHED ' + str(self.world.turn))
264         self.world.proceed_to_next_player_turn()
265         msg = str(self.world.get_player().last_task_result)
266         self.io.send('LAST_PLAYER_TASK_RESULT ' + self.io.quote(msg))
267         self.send_gamestate()
268
269     def cmd_FIB(self, numbers, connection_id):
270         """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
271
272         Numbers are calculated in parallel as far as possible, using fib().
273         A 'CALCULATING …' message is sent to caller before the result.
274         """
275         self.io.send('CALCULATING …', connection_id)
276         results = self.pool.map(fib, numbers)
277         reply = ' '.join([str(r) for r in results])
278         self.io.send(reply, connection_id)
279     cmd_FIB.argtypes = 'seq:int:nonneg'
280
281     def cmd_INC_P(self, connection_id):
282         """Increment world.turn, send game turn data to everyone.
283
284         To simulate game processing waiting times, a one second delay between
285         TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive
286         calculations are started as pool processes that need to be finished
287         until a further INC finishes the turn.
288
289         This is just a demo structure for how the game loop could work when
290         parallelized. One might imagine a two-step game turn, with a non-action
291         step determining actor tasks (the AI determinations would take the
292         place of the fib calculations here), and an action step wherein these
293         tasks are performed (where now sleep(1) is).
294         """
295         from time import sleep
296         if self.pool_result is not None:
297             self.pool_result.wait()
298         self.io.send('TURN_FINISHED ' + str(self.world.turn))
299         sleep(1)
300         self.world.turn += 1
301         self.send_gamestate()
302         self.pool_result = self.pool.map_async(fib, (35, 35))
303
304     def cmd_MOVE(self, direction):
305         """Set player task to 'move' with direction arg, finish player turn."""
306         import parser
307         legal_directions = self.world.map_.get_directions()
308         if direction not in legal_directions:
309             raise parser.ArgError('Move argument must be one of: ' +
310                                   ', '.join(legal_directions))
311         self.world.get_player().set_task('move', direction=direction)
312         self.proceed()
313     cmd_MOVE.argtypes = 'string'
314
315     def cmd_WAIT(self):
316         """Set player task to 'wait', finish player turn."""
317         self.world.get_player().set_task('wait')
318         self.proceed()
319
320     def cmd_GET_GAMESTATE(self, connection_id):
321         """Send game state jto caller."""
322         self.send_gamestate(connection_id)
323
324     def cmd_ECHO(self, msg, connection_id):
325         """Send msg to caller."""
326         self.io.send(msg, connection_id)
327     cmd_ECHO.argtypes = 'string'
328
329     def cmd_ALL(self, msg, connection_id):
330         """Send msg to all clients."""
331         self.io.send(msg)
332     cmd_ALL.argtypes = 'string'
333
334     def cmd_TERRAIN_LINE(self, y, terrain_line):
335         self.world.map_.set_line(y, terrain_line)
336     cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'