home · contact · privacy
Add FOV algorithm for Hex grids.
[plomrogue2-experiments] / server_ / game.py
1 import sys
2 sys.path.append('../')
3 import game_common
4 import server_.map_
5
6
7 class GameError(Exception):
8     pass
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
48 class Task:
49
50     def __init__(self, thing, name, args=(), kwargs={}):
51         self.name = name
52         self.thing = thing
53         self.args = args
54         self.kwargs = kwargs
55         self.todo = 3
56
57     def check(self):
58         if self.name == 'move':
59             if len(self.args) > 0:
60                 direction = self.args[0]
61             else:
62                 direction = self.kwargs['direction']
63             test_pos = self.thing.world.map_.move(self.thing.position, direction)
64             if self.thing.world.map_[test_pos] != '.':
65                 raise GameError('would move into illegal terrain')
66             for t in self.thing.world.things:
67                 if t.position == test_pos:
68                     raise GameError('would move into other thing')
69
70
71 class Thing(game_common.Thing):
72
73     def __init__(self, *args, **kwargs):
74         super().__init__(*args, **kwargs)
75         self.task = Task(self, 'wait')
76         self.last_task_result = None
77         self._stencil = None
78
79     def task_wait(self):
80         return 'success'
81
82     def task_move(self, direction):
83         self.position = self.world.map_.move(self.position, direction)
84         return 'success'
85
86     def decide_task(self):
87         if self.position[1] > 1:
88             self.set_task('move', 'LEFT')
89         elif self.position[1] < 3:
90             self.set_task('move', 'RIGHT')
91         else:
92             self.set_task('wait')
93
94     def set_task(self, task_name, *args, **kwargs):
95         self.task = Task(self, task_name, args, kwargs)
96         self.task.check()
97
98     def proceed(self, is_AI=True):
99         """Further the thing in its tasks.
100
101         Decrements .task.todo; if it thus falls to <= 0, enacts method
102         whose name is 'task_' + self.task.name and sets .task =
103         None. If is_AI, calls .decide_task to decide a self.task.
104
105         Before doing anything, ensures an empty map visibility stencil
106         and checks that task is still possible, and aborts it
107         otherwise (for AI things, decides a new task).
108
109         """
110         self._stencil = None
111         try:
112             self.task.check()
113         except GameError as e:
114             self.task = None
115             self.last_task_result = e
116             if is_AI:
117                 self.decide_task()
118             return
119         self.task.todo -= 1
120         if self.task.todo <= 0:
121             task = getattr(self, 'task_' + self.task.name)
122             self.last_task_result = task(*self.task.args, **self.task.kwargs)
123             self.task = None
124         if is_AI and self.task is None:
125             self.decide_task()
126
127     def get_stencil(self):
128         if self._stencil is not None:
129             return self._stencil
130         self._stencil = self.world.map_.get_fov_map(self.position)
131         return self._stencil
132
133     def get_visible_map(self):
134         stencil = self.get_stencil()
135         m = self.world.map_.new_from_shape(' ')
136         for pos in m:
137             if stencil[pos] == '.':
138                 m[pos] = self.world.map_[pos]
139         return m
140
141     def get_visible_things(self):
142         stencil = self.get_stencil()
143         visible_things = []
144         for thing in self.world.things:
145             if stencil[thing.position] == '.':
146                 visible_things += [thing]
147         return visible_things
148
149
150 def fib(n):
151     """Calculate n-th Fibonacci number. Very inefficiently."""
152     if n in (1, 2):
153         return 1
154     else:
155         return fib(n-1) + fib(n-2)
156
157
158 class Game(game_common.CommonCommandsMixin):
159
160     def __init__(self, game_file_name):
161         import server_.io
162         self.map_manager = server_.map_.map_manager
163         self.world = World(self)
164         self.io = server_.io.GameIO(game_file_name, self)
165         # self.pool and self.pool_result are currently only needed by the FIB
166         # command and the demo of a parallelized game loop in cmd_inc_p.
167         from multiprocessing import Pool
168         self.pool = Pool()
169         self.pool_result = None
170
171     def send_gamestate(self, connection_id=None):
172         """Send out game state data relevant to clients."""
173
174         def stringify_yx(tuple_):
175             """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
176             return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])
177
178         self.io.send('NEW_TURN ' + str(self.world.turn))
179         self.io.send('MAP ' + self.world.map_.geometry +\
180                      ' ' + stringify_yx(self.world.map_.size))
181         visible_map = self.world.get_player().get_visible_map()
182         for y, line in visible_map.lines():
183             self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, self.io.quote(line)))
184         visible_things = self.world.get_player().get_visible_things()
185         for thing in visible_things:
186             self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
187             self.io.send('THING_POS %s %s' % (thing.id_,
188                                               stringify_yx(thing.position)))
189
190     def proceed(self):
191         """Send turn finish signal, run game world, send new world data.
192
193         First sends 'TURN_FINISHED' message, then runs game world
194         until new player input is needed, then sends game state.
195         """
196         self.io.send('TURN_FINISHED ' + str(self.world.turn))
197         self.world.proceed_to_next_player_turn()
198         msg = str(self.world.get_player().last_task_result)
199         self.io.send('LAST_PLAYER_TASK_RESULT ' + self.io.quote(msg))
200         self.send_gamestate()
201
202     def cmd_FIB(self, numbers, connection_id):
203         """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
204
205         Numbers are calculated in parallel as far as possible, using fib().
206         A 'CALCULATING …' message is sent to caller before the result.
207         """
208         self.io.send('CALCULATING …', connection_id)
209         results = self.pool.map(fib, numbers)
210         reply = ' '.join([str(r) for r in results])
211         self.io.send(reply, connection_id)
212     cmd_FIB.argtypes = 'seq:int:nonneg'
213
214     def cmd_INC_P(self, connection_id):
215         """Increment world.turn, send game turn data to everyone.
216
217         To simulate game processing waiting times, a one second delay between
218         TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive
219         calculations are started as pool processes that need to be finished
220         until a further INC finishes the turn.
221
222         This is just a demo structure for how the game loop could work when
223         parallelized. One might imagine a two-step game turn, with a non-action
224         step determining actor tasks (the AI determinations would take the
225         place of the fib calculations here), and an action step wherein these
226         tasks are performed (where now sleep(1) is).
227         """
228         from time import sleep
229         if self.pool_result is not None:
230             self.pool_result.wait()
231         self.io.send('TURN_FINISHED ' + str(self.world.turn))
232         sleep(1)
233         self.world.turn += 1
234         self.send_gamestate()
235         self.pool_result = self.pool.map_async(fib, (35, 35))
236
237     def cmd_MOVE(self, direction):
238         """Set player task to 'move' with direction arg, finish player turn."""
239         import parser
240         legal_directions = self.world.map_.get_directions()
241         if direction not in legal_directions:
242             raise parser.ArgError('Move argument must be one of: ' +
243                                   ', '.join(legal_directions))
244         self.world.get_player().set_task('move', direction=direction)
245         self.proceed()
246     cmd_MOVE.argtypes = 'string'
247
248     def cmd_WAIT(self):
249         """Set player task to 'wait', finish player turn."""
250         self.world.get_player().set_task('wait')
251         self.proceed()
252
253     def cmd_GET_GAMESTATE(self, connection_id):
254         """Send game state jto caller."""
255         self.send_gamestate(connection_id)
256
257     def cmd_ECHO(self, msg, connection_id):
258         """Send msg to caller."""
259         self.io.send(msg, connection_id)
260     cmd_ECHO.argtypes = 'string'
261
262     def cmd_ALL(self, msg, connection_id):
263         """Send msg to all clients."""
264         self.io.send(msg)
265     cmd_ALL.argtypes = 'string'
266
267     def cmd_TERRAIN_LINE(self, y, terrain_line):
268         self.world.map_.set_line(y, terrain_line)
269     cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'