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