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