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