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