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