home · contact · privacy
Add very basic pathfinding AI.
[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('would move into illegal terrain')
87             for t in self.thing.world.things:
88                 if t.position == test_pos:
89                     raise GameError('would move into other thing')
90
91
92 class Thing(game_common.Thing):
93
94     def __init__(self, *args, **kwargs):
95         super().__init__(*args, **kwargs)
96         self.task = Task(self, 'wait')
97         self.last_task_result = None
98         self._stencil = None
99
100     def task_wait(self):
101         return 'success'
102
103     def task_move(self, direction):
104         self.position = self.world.map_.move(self.position, direction)
105         return 'success'
106
107     def decide_task(self):
108         visible_things = self.get_visible_things()
109         target = None
110         for t in visible_things:
111             if t.type_ == 'human':
112                 target = t.position
113                 break
114         if target is None:
115             self.set_task('wait')
116             return
117         dijkstra_map = type(self.world.map_)(self.world.map_.size)
118         n_max = 256
119         dijkstra_map.terrain = [n_max for i in range(dijkstra_map.size_i)]
120         dijkstra_map[target] = 0
121         shrunk = True
122         while shrunk:
123             shrunk = False
124             for pos in dijkstra_map:
125                 if self.world.map_[pos] != '.':
126                     continue
127                 neighbors = dijkstra_map.get_neighbors(pos)
128                 for yx in neighbors:
129                     if yx is not None and dijkstra_map[yx] < dijkstra_map[pos] - 1:
130                         dijkstra_map[pos] = dijkstra_map[yx] + 1
131                         shrunk = True
132         #with open('log', 'a') as f:
133         #    f.write('---------------------------------\n')
134         #    for y, line in dijkstra_map.lines():
135         #        for val in line:
136         #            if val < 10:
137         #                f.write(str(val))
138         #            elif val == 256:
139         #                f.write('x')
140         #            else:
141         #                f.write('~')
142         #        f.write('\n')
143         neighbors = dijkstra_map.get_neighbors(self.position)
144         n = n_max
145         dirs = dijkstra_map.get_directions()
146         #print('DEBUG dirs', dirs)
147         #print('DEBUG neighbors', neighbors)
148         #debug_scores = []
149         #for pos in neighbors:
150         #    if pos is None:
151         #        debug_scores += [9000]
152         #    else:
153         #        debug_scores += [dijkstra_map[pos]]
154         #print('DEBUG debug_scores', debug_scores)
155         direction = None
156         for i_dir in range(len(neighbors)):
157             pos = neighbors[i_dir]
158             if pos is not None and dijkstra_map[pos] < n:
159                 n = dijkstra_map[pos]
160                 direction = dirs[i_dir]
161         #print('DEBUG result', direction)
162         if direction:
163             self.set_task('move', direction=direction)
164             #self.world.game.io.send('would move ' + direction)
165         else:
166             self.set_task('wait')
167
168
169     def set_task(self, task_name, *args, **kwargs):
170         self.task = Task(self, task_name, args, kwargs)
171         self.task.check()  # will throw GameError if necessary
172
173     def proceed(self, is_AI=True):
174         """Further the thing in its tasks.
175
176         Decrements .task.todo; if it thus falls to <= 0, enacts method
177         whose name is 'task_' + self.task.name and sets .task =
178         None. If is_AI, calls .decide_task to decide a self.task.
179
180         Before doing anything, ensures an empty map visibility stencil
181         and checks that task is still possible, and aborts it
182         otherwise (for AI things, decides a new task).
183
184         """
185         self._stencil = None
186         try:
187             self.task.check()
188         except GameError as e:
189             self.task = None
190             self.last_task_result = e
191             if is_AI:
192                 self.decide_task()
193             return
194         self.task.todo -= 1
195         if self.task.todo <= 0:
196             task = getattr(self, 'task_' + self.task.name)
197             self.last_task_result = task(*self.task.args, **self.task.kwargs)
198             self.task = None
199         if is_AI and self.task is None:
200             self.decide_task()
201
202     def get_stencil(self):
203         if self._stencil is not None:
204             return self._stencil
205         self._stencil = self.world.map_.get_fov_map(self.position)
206         return self._stencil
207
208     def get_visible_map(self):
209         stencil = self.get_stencil()
210         m = self.world.map_.new_from_shape(' ')
211         for pos in m:
212             if stencil[pos] == '.':
213                 m[pos] = self.world.map_[pos]
214         return m
215
216     def get_visible_things(self):
217         stencil = self.get_stencil()
218         visible_things = []
219         for thing in self.world.things:
220             if stencil[thing.position] == '.':
221                 visible_things += [thing]
222         return visible_things
223
224
225 def fib(n):
226     """Calculate n-th Fibonacci number. Very inefficiently."""
227     if n in (1, 2):
228         return 1
229     else:
230         return fib(n-1) + fib(n-2)
231
232
233 class Game(game_common.CommonCommandsMixin):
234
235     def __init__(self, game_file_name):
236         import server_.io
237         self.map_manager = server_.map_.map_manager
238         self.world = World(self)
239         self.io = server_.io.GameIO(game_file_name, self)
240         # self.pool and self.pool_result are currently only needed by the FIB
241         # command and the demo of a parallelized game loop in cmd_inc_p.
242         from multiprocessing import Pool
243         self.pool = Pool()
244         self.pool_result = None
245
246     def send_gamestate(self, connection_id=None):
247         """Send out game state data relevant to clients."""
248
249         def stringify_yx(tuple_):
250             """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
251             return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])
252
253         self.io.send('NEW_TURN ' + str(self.world.turn))
254         self.io.send('MAP ' + self.world.map_.geometry +\
255                      ' ' + stringify_yx(self.world.map_.size))
256         visible_map = self.world.get_player().get_visible_map()
257         for y, line in visible_map.lines():
258             self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, self.io.quote(line)))
259         visible_things = self.world.get_player().get_visible_things()
260         for thing in visible_things:
261             self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
262             self.io.send('THING_POS %s %s' % (thing.id_,
263                                               stringify_yx(thing.position)))
264         player = self.world.get_player()
265         self.io.send('PLAYER_POS %s' % (stringify_yx(player.position)))
266         self.io.send('GAME_STATE_COMPLETE')
267
268     def proceed(self):
269         """Send turn finish signal, run game world, send new world data.
270
271         First sends 'TURN_FINISHED' message, then runs game world
272         until new player input is needed, then sends game state.
273         """
274         self.io.send('TURN_FINISHED ' + str(self.world.turn))
275         self.world.proceed_to_next_player_turn()
276         msg = str(self.world.get_player().last_task_result)
277         self.io.send('LAST_PLAYER_TASK_RESULT ' + self.io.quote(msg))
278         self.send_gamestate()
279
280     def cmd_FIB(self, numbers, connection_id):
281         """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
282
283         Numbers are calculated in parallel as far as possible, using fib().
284         A 'CALCULATING …' message is sent to caller before the result.
285         """
286         self.io.send('CALCULATING …', connection_id)
287         results = self.pool.map(fib, numbers)
288         reply = ' '.join([str(r) for r in results])
289         self.io.send(reply, connection_id)
290     cmd_FIB.argtypes = 'seq:int:nonneg'
291
292     def cmd_INC_P(self, connection_id):
293         """Increment world.turn, send game turn data to everyone.
294
295         To simulate game processing waiting times, a one second delay between
296         TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive
297         calculations are started as pool processes that need to be finished
298         until a further INC finishes the turn.
299
300         This is just a demo structure for how the game loop could work when
301         parallelized. One might imagine a two-step game turn, with a non-action
302         step determining actor tasks (the AI determinations would take the
303         place of the fib calculations here), and an action step wherein these
304         tasks are performed (where now sleep(1) is).
305         """
306         from time import sleep
307         if self.pool_result is not None:
308             self.pool_result.wait()
309         self.io.send('TURN_FINISHED ' + str(self.world.turn))
310         sleep(1)
311         self.world.turn += 1
312         self.send_gamestate()
313         self.pool_result = self.pool.map_async(fib, (35, 35))
314
315     def cmd_MOVE(self, direction):
316         """Set player task to 'move' with direction arg, finish player turn."""
317         import parser
318         legal_directions = self.world.map_.get_directions()
319         if direction not in legal_directions:
320             raise parser.ArgError('Move argument must be one of: ' +
321                                   ', '.join(legal_directions))
322         self.world.get_player().set_task('move', direction=direction)
323         self.proceed()
324     cmd_MOVE.argtypes = 'string'
325
326     def cmd_WAIT(self):
327         """Set player task to 'wait', finish player turn."""
328         self.world.get_player().set_task('wait')
329         self.proceed()
330
331     def cmd_GET_GAMESTATE(self, connection_id):
332         """Send game state to caller."""
333         self.send_gamestate(connection_id)
334
335     def cmd_ECHO(self, msg, connection_id):
336         """Send msg to caller."""
337         self.io.send(msg, connection_id)
338     cmd_ECHO.argtypes = 'string'
339
340     def cmd_ALL(self, msg, connection_id):
341         """Send msg to all clients."""
342         self.io.send(msg)
343     cmd_ALL.argtypes = 'string'
344
345     def cmd_TERRAIN_LINE(self, y, terrain_line):
346         self.world.map_.set_line(y, terrain_line)
347     cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'
348
349     def cmd_GEN_WORLD(self, geometry, yx, seed):
350         legal_grids = self.map_manager.get_map_geometries()
351         if geometry not in legal_grids:
352             raise ArgError('First map argument must be one of: ' +
353                            ', '.join(legal_grids))
354         self.world.make_new(geometry, yx, seed)
355     cmd_GEN_WORLD.argtypes = 'string yx_tuple:pos string'