home · contact · privacy
Refactor and optimize FOV algorithm.
[plomrogue2-experiments] / server_ / game.py
1 import sys
2 sys.path.append('../')
3 import game_common
4 import server_.map_
5 import server_.io
6 from parser import ArgError
7
8
9 class GameError(Exception):
10     pass
11
12
13 class World(game_common.World):
14
15     def __init__(self, game):
16         super().__init__()
17         self.game = game
18         self.player_id = 0
19         # use extended local classes
20         self.Thing = Thing
21
22     def proceed_to_next_player_turn(self):
23         """Run game world turns until player can decide their next step.
24
25         Iterates through all non-player things, on each step
26         furthering them in their tasks (and letting them decide new
27         ones if they finish). The iteration order is: first all things
28         that come after the player in the world things list, then
29         (after incrementing the world turn) all that come before the
30         player; then the player's .proceed() is run, and if it does
31         not finish his task, the loop starts at the beginning. Once
32         the player's task is finished, the loop breaks.
33         """
34         while True:
35             player = self.get_player()
36             player_i = self.things.index(player)
37             for thing in self.things[player_i+1:]:
38                 thing.proceed()
39             self.turn += 1
40             for thing in self.things[:player_i]:
41                 thing.proceed()
42             player.proceed(is_AI=False)
43             if player.task is None:
44                 break
45
46     def get_player(self):
47         return self.get_thing(self.player_id)
48
49     def make_new(self, geometry, yx, seed):
50         import random
51         random.seed(seed)
52         self.turn = 0
53         self.new_map(geometry, yx)
54         for pos in self.map_:
55             if 0 in pos or (yx[0] - 1) == pos[0] or (yx[1] - 1) == pos[1]:
56                 self.map_[pos] = '#'
57                 continue
58             self.map_[pos] = random.choice(('.', '.', '.', '.', 'x'))
59         player = self.Thing(self, 0)
60         player.type_ = 'human'
61         player.position = [random.randint(0, yx[0] -1),
62                            random.randint(0, yx[1] - 1)]
63         npc = self.Thing(self, 1)
64         npc.type_ = 'monster'
65         npc.position = [random.randint(0, yx[0] -1),
66                         random.randint(0, yx[1] -1)]
67         self.things = [player, npc]
68
69
70 class Task:
71
72     def __init__(self, thing, name, args=()):
73         self.name = name
74         self.thing = thing
75         self.args = args
76         self.todo = 3
77
78     def check(self):
79         if self.name == 'MOVE':
80             test_pos = self.thing.world.map_.move(self.thing.position, self.args[0])
81             if self.thing.world.map_[test_pos] != '.':
82                 raise GameError(str(self.thing.id_) +
83                                 ' would move into illegal terrain')
84             for t in self.thing.world.things:
85                 if t.position == test_pos:
86                     raise GameError(str(self.thing.id_) +
87                                     ' would move into other thing')
88
89     def get_args_string(self):
90         stringed_args = []
91         for arg in self.args:
92             if type(arg) == 'string':
93                 stringed_args += [server_.io.quote(arg)]
94             else:
95                 raise GameError('stringifying arg type not implemented')
96         return ' '.join(stringed_args)
97
98
99 class Thing(game_common.Thing):
100
101     def __init__(self, *args, **kwargs):
102         super().__init__(*args, **kwargs)
103         self.task = Task(self, 'WAIT')
104         self._last_task_result = None
105         self._stencil = None
106
107     def task_WAIT(self):
108         return 'success'
109
110     def task_MOVE(self, direction):
111         self.position = self.world.map_.move(self.position, direction)
112         return 'success'
113     task_MOVE.argtypes = 'string:direction'
114
115     def move_towards_target(self, target):
116         dijkstra_map = type(self.world.map_)(self.world.map_.size)
117         n_max = 256
118         dijkstra_map.terrain = [n_max for i in range(dijkstra_map.size_i)]
119         dijkstra_map[target] = 0
120         shrunk = True
121         stencil = self.get_stencil()
122         while shrunk:
123             shrunk = False
124             for pos in dijkstra_map:
125                 if stencil[pos] != '.':
126                     continue
127                 neighbors = dijkstra_map.get_neighbors(tuple(pos))
128                 for direction in neighbors:
129                     yx = neighbors[direction]
130                     if yx is not None and dijkstra_map[yx] < dijkstra_map[pos] - 1:
131                         dijkstra_map[pos] = dijkstra_map[yx] + 1
132                         shrunk = True
133         #with open('log', 'a') as f:
134         #    f.write('---------------------------------\n')
135         #    for y, line in dijkstra_map.lines():
136         #        for val in line:
137         #            if val < 10:
138         #                f.write(str(val))
139         #            elif val == 256:
140         #                f.write('x')
141         #            else:
142         #                f.write('~')
143         #        f.write('\n')
144         neighbors = dijkstra_map.get_neighbors(tuple(self.position))
145         n = n_max
146         #print('DEBUG', self.position, neighbors)
147         #dirs = dijkstra_map.get_directions()
148         #print('DEBUG dirs', dirs)
149         #print('DEBUG neighbors', neighbors)
150         #debug_scores = []
151         #for pos in neighbors:
152         #    if pos is None:
153         #        debug_scores += [9000]
154         #    else:
155         #        debug_scores += [dijkstra_map[pos]]
156         #print('DEBUG debug_scores', debug_scores)
157         target_direction = None
158         for direction in neighbors:
159             yx = neighbors[direction]
160             if yx is not None:
161                 n_new = dijkstra_map[yx]
162                 if n_new < n:
163                     n = n_new
164                     target_direction = direction
165         #print('DEBUG result', direction)
166         if target_direction:
167             self.set_task('MOVE', (target_direction,))
168             #self.world.game.io.send('would move ' + direction)
169
170     def decide_task(self):
171         visible_things = self.get_visible_things()
172         target = None
173         for t in visible_things:
174             if t.type_ == 'human':
175                 target = t.position
176                 break
177         if target is not None:
178             try:
179                 self.move_towards_target(target)
180                 return
181             except GameError:
182                 pass
183         self.set_task('WAIT')
184
185
186     def set_task(self, task_name, args=()):
187         self.task = Task(self, task_name, args)
188         self.task.check()  # will throw GameError if necessary
189
190     def proceed(self, is_AI=True):
191         """Further the thing in its tasks.
192
193         Decrements .task.todo; if it thus falls to <= 0, enacts method
194         whose name is 'task_' + self.task.name and sets .task =
195         None. If is_AI, calls .decide_task to decide a self.task.
196
197         Before doing anything, ensures an empty map visibility stencil
198         and checks that task is still possible, and aborts it
199         otherwise (for AI things, decides a new task).
200
201         """
202         self._stencil = None
203         try:
204             self.task.check()
205         except GameError as e:
206             self.task = None
207             self._last_task_result = e
208             if is_AI:
209                 try:
210                     self.decide_task()
211                 except GameError:
212                     self.set_task('WAIT')
213             return
214         self.task.todo -= 1
215         if self.task.todo <= 0:
216             task = getattr(self, 'task_' + self.task.name)
217             self._last_task_result = task(*self.task.args)
218             self.task = None
219         if is_AI and self.task is None:
220             try:
221                 self.decide_task()
222             except GameError:
223                 self.set_task('WAIT')
224
225     def get_stencil(self):
226         if self._stencil is not None:
227             return self._stencil
228         self._stencil = self.world.map_.get_fov_map(self.position)
229         return self._stencil
230
231     def get_visible_map(self):
232         stencil = self.get_stencil()
233         m = self.world.map_.new_from_shape(' ')
234         for pos in m:
235             if stencil[pos] == '.':
236                 m[pos] = self.world.map_[pos]
237         return m
238
239     def get_visible_things(self):
240         stencil = self.get_stencil()
241         visible_things = []
242         for thing in self.world.things:
243             if stencil[thing.position] == '.':
244                 visible_things += [thing]
245         return visible_things
246
247
248 def fib(n):
249     """Calculate n-th Fibonacci number. Very inefficiently."""
250     if n in (1, 2):
251         return 1
252     else:
253         return fib(n-1) + fib(n-2)
254
255
256 class Game(game_common.CommonCommandsMixin):
257
258     def __init__(self, game_file_name):
259         self.map_manager = server_.map_.map_manager
260         self.world = World(self)
261         self.io = server_.io.GameIO(game_file_name, self)
262         # self.pool and self.pool_result are currently only needed by the FIB
263         # command and the demo of a parallelized game loop in cmd_inc_p.
264         from multiprocessing import Pool
265         self.pool = Pool()
266         self.pool_result = None
267
268     def send_gamestate(self, connection_id=None):
269         """Send out game state data relevant to clients."""
270
271         self.io.send('TURN ' + str(self.world.turn))
272         self.io.send('MAP ' + self.world.map_.geometry +\
273                      ' ' + server_.io.stringify_yx(self.world.map_.size))
274         visible_map = self.world.get_player().get_visible_map()
275         for y, line in visible_map.lines():
276             self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, server_.io.quote(line)))
277         visible_things = self.world.get_player().get_visible_things()
278         for thing in visible_things:
279             self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
280             self.io.send('THING_POS %s %s' % (thing.id_,
281                                               server_.io.stringify_yx(thing.position)))
282         player = self.world.get_player()
283         self.io.send('PLAYER_POS %s' % (server_.io.stringify_yx(player.position)))
284         self.io.send('GAME_STATE_COMPLETE')
285
286     def proceed(self):
287         """Send turn finish signal, run game world, send new world data.
288
289         First sends 'TURN_FINISHED' message, then runs game world
290         until new player input is needed, then sends game state.
291         """
292         self.io.send('TURN_FINISHED ' + str(self.world.turn))
293         self.world.proceed_to_next_player_turn()
294         msg = str(self.world.get_player()._last_task_result)
295         self.io.send('LAST_PLAYER_TASK_RESULT ' + server_.io.quote(msg))
296         self.send_gamestate()
297
298     def cmd_FIB(self, numbers, connection_id):
299         """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
300
301         Numbers are calculated in parallel as far as possible, using fib().
302         A 'CALCULATING …' message is sent to caller before the result.
303         """
304         self.io.send('CALCULATING …', connection_id)
305         results = self.pool.map(fib, numbers)
306         reply = ' '.join([str(r) for r in results])
307         self.io.send(reply, connection_id)
308     cmd_FIB.argtypes = 'seq:int:nonneg'
309
310     def cmd_INC_P(self, connection_id):
311         """Increment world.turn, send game turn data to everyone.
312
313         To simulate game processing waiting times, a one second delay
314         between TURN_FINISHED and TURN occurs; after TURN, some
315         expensive calculations are started as pool processes that need
316         to be finished until a further INC finishes the turn.
317
318         This is just a demo structure for how the game loop could work
319         when parallelized. One might imagine a two-step game turn,
320         with a non-action step determining actor tasks (the AI
321         determinations would take the place of the fib calculations
322         here), and an action step wherein these tasks are performed
323         (where now sleep(1) is).
324
325         """
326         from time import sleep
327         if self.pool_result is not None:
328             self.pool_result.wait()
329         self.io.send('TURN_FINISHED ' + str(self.world.turn))
330         sleep(1)
331         self.world.turn += 1
332         self.send_gamestate()
333         self.pool_result = self.pool.map_async(fib, (35, 35))
334
335     def cmd_SWITCH_PLAYER(self):
336         player = self.world.get_player()
337         player.set_task('WAIT')
338         thing_ids = [t.id_ for t in self.world.things]
339         player_index = thing_ids.index(player.id_)
340         if player_index == len(thing_ids) - 1:
341             self.world.player_id = thing_ids[0]
342         else:
343             self.world.player_id = thing_ids[player_index + 1]
344         self.proceed()
345
346     def cmd_GET_GAMESTATE(self, connection_id):
347         """Send game state to caller."""
348         self.send_gamestate(connection_id)
349
350     def cmd_ECHO(self, msg, connection_id):
351         """Send msg to caller."""
352         self.io.send(msg, connection_id)
353     cmd_ECHO.argtypes = 'string'
354
355     def cmd_ALL(self, msg, connection_id):
356         """Send msg to all clients."""
357         self.io.send(msg)
358     cmd_ALL.argtypes = 'string'
359
360     def cmd_TERRAIN_LINE(self, y, terrain_line):
361         self.world.map_.set_line(y, terrain_line)
362     cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'
363
364     def cmd_GEN_WORLD(self, geometry, yx, seed):
365         self.world.make_new(geometry, yx, seed)
366     cmd_GEN_WORLD.argtypes = 'string:geometry yx_tuple:pos string'
367
368     def get_command_signature(self, command_name):
369         from functools import partial
370
371         def cmd_TASK_colon(task_name, *args):
372             self.world.get_player().set_task(task_name, args)
373             self.proceed()
374
375         def cmd_SET_TASK_colon(task_name, thing_id, todo, *args):
376             t = self.world.get_thing(thing_id, False)
377             if t is None:
378                 raiseArgError('No such Thing.')
379             t.task = Task(t, task_name, args)
380             t.task.todo = todo
381
382         def task_prefixed(command_name, task_prefix, task_command,
383                           argtypes_prefix=''):
384             method = None
385             argtypes = ''
386             if command_name[:len(task_prefix)] == task_prefix:
387                 task_name = command_name[len(task_prefix):]
388                 task_method_candidate = 'task_' + task_name
389                 if hasattr(Thing, task_method_candidate):
390                     method = partial(task_command, task_name)
391                     task_method = getattr(Thing, task_method_candidate)
392                     if hasattr(task_method, 'argtypes'):
393                         argtypes = task_method.argtypes
394             if method is not None:
395                 return method, argtypes_prefix + argtypes
396             return None, argtypes
397
398         method, argtypes = task_prefixed(command_name, 'TASK:', cmd_TASK_colon)
399         if method:
400             return method, argtypes
401         method, argtypes = task_prefixed(command_name, 'SET_TASK:',
402                                          cmd_SET_TASK_colon,
403                                          'int:nonneg int:nonneg')
404         if method:
405             return method, argtypes
406         method_candidate = 'cmd_' + command_name
407         if hasattr(self, method_candidate):
408             method = getattr(self, method_candidate)
409             if hasattr(method, 'argtypes'):
410                 argtypes = method.argtypes
411         return method, argtypes
412
413     def get_string_options(self, string_option_type):
414         if string_option_type == 'geometry':
415             return self.map_manager.get_map_geometries()
416         elif string_option_type == 'direction':
417             return self.world.map_.get_directions()
418         return None
419
420     def cmd_PLAYER_ID(self, id_):
421         # TODO: test whether valid thing ID
422         self.world.player_id = id_
423     cmd_PLAYER_ID.argtypes = 'int:nonneg'
424
425     def cmd_TURN(self, n):
426         self.world.turn = n
427     cmd_TURN.argtypes = 'int:nonneg'
428
429     def cmd_SAVE(self):
430
431         def write(f, msg):
432             f.write(msg + '\n')
433
434         save_file_name = self.io.game_file_name + '.save'
435         with open(save_file_name, 'w') as f:
436             write(f, 'TURN %s' % self.world.turn)
437             write(f, 'MAP ' + self.world.map_.geometry + ' ' + server_.io.stringify_yx(self.world.map_.size))
438             for y, line in self.world.map_.lines():
439                 write(f, 'TERRAIN_LINE %5s %s' % (y, server_.io.quote(line)))
440             for thing in self.world.things:
441                 write(f, 'THING_TYPE %s %s' % (thing.id_, thing.type_))
442                 write(f, 'THING_POS %s %s' % (thing.id_,
443                                               server_.io.stringify_yx(thing.position)))
444                 task = thing.task
445                 if task is not None:
446                     task_args = task.get_args_string()
447                     write(f, 'SET_TASK:%s %s %s %s' % (task.name, thing.id_,
448                                                     task.todo, task_args))
449             write(f, 'PLAYER_ID %s' % self.world.player_id)
450     cmd_SAVE.dont_save = True