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