home · contact · privacy
Refactor / encapsulate Map stuff into own module.
[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):
14         super().__init__()
15         self.player_id = 0
16         # use extended local classes
17         self.Thing = Thing
18         self.get_map_class = server_.map_.get_map_class
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         m = self.world.map_.new_from_shape('?')
131         for pos in m:
132             if pos == self.position or m.are_neighbors(pos, self.position):
133                 m[pos] = '.'
134         self._stencil = m
135         return self._stencil
136
137     def get_visible_map(self):
138         stencil = self.get_stencil()
139         m = self.world.map_.new_from_shape(' ')
140         for pos in m:
141             if stencil[pos] == '.':
142                 m[pos] = self.world.map_[pos]
143         return m
144
145     def get_visible_things(self):
146         stencil = self.get_stencil()
147         visible_things = []
148         for thing in self.world.things:
149             if stencil[thing.position] == '.':
150                 visible_things += [thing]
151         return visible_things
152
153
154 def fib(n):
155     """Calculate n-th Fibonacci number. Very inefficiently."""
156     if n in (1, 2):
157         return 1
158     else:
159         return fib(n-1) + fib(n-2)
160
161
162 class Game(game_common.CommonCommandsMixin):
163
164     def __init__(self, game_file_name):
165         import server_.io
166         self.world = World()
167         self.io = server_.io.GameIO(game_file_name, self)
168         # self.pool and self.pool_result are currently only needed by the FIB
169         # command and the demo of a parallelized game loop in cmd_inc_p.
170         from multiprocessing import Pool
171         self.pool = Pool()
172         self.pool_result = None
173
174     def send_gamestate(self, connection_id=None):
175         """Send out game state data relevant to clients."""
176
177         def stringify_yx(tuple_):
178             """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
179             return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])
180
181         self.io.send('NEW_TURN ' + str(self.world.turn))
182         grid = self.world.map_.__class__.__name__[3:]
183         self.io.send('MAP ' + grid +' ' + stringify_yx(self.world.map_.size))
184         visible_map = self.world.get_player().get_visible_map()
185         for y, line in visible_map.lines():
186             self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, self.io.quote(line)))
187         visible_things = self.world.get_player().get_visible_things()
188         for thing in visible_things:
189             self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
190             self.io.send('THING_POS %s %s' % (thing.id_,
191                                               stringify_yx(thing.position)))
192
193     def proceed(self):
194         """Send turn finish signal, run game world, send new world data.
195
196         First sends 'TURN_FINISHED' message, then runs game world
197         until new player input is needed, then sends game state.
198         """
199         self.io.send('TURN_FINISHED ' + str(self.world.turn))
200         self.world.proceed_to_next_player_turn()
201         msg = str(self.world.get_player().last_task_result)
202         self.io.send('LAST_PLAYER_TASK_RESULT ' + self.io.quote(msg))
203         self.send_gamestate()
204
205     def cmd_FIB(self, numbers, connection_id):
206         """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
207
208         Numbers are calculated in parallel as far as possible, using fib().
209         A 'CALCULATING …' message is sent to caller before the result.
210         """
211         self.io.send('CALCULATING …', connection_id)
212         results = self.pool.map(fib, numbers)
213         reply = ' '.join([str(r) for r in results])
214         self.io.send(reply, connection_id)
215     cmd_FIB.argtypes = 'seq:int:nonneg'
216
217     def cmd_INC_P(self, connection_id):
218         """Increment world.turn, send game turn data to everyone.
219
220         To simulate game processing waiting times, a one second delay between
221         TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive
222         calculations are started as pool processes that need to be finished
223         until a further INC finishes the turn.
224
225         This is just a demo structure for how the game loop could work when
226         parallelized. One might imagine a two-step game turn, with a non-action
227         step determining actor tasks (the AI determinations would take the
228         place of the fib calculations here), and an action step wherein these
229         tasks are performed (where now sleep(1) is).
230         """
231         from time import sleep
232         if self.pool_result is not None:
233             self.pool_result.wait()
234         self.io.send('TURN_FINISHED ' + str(self.world.turn))
235         sleep(1)
236         self.world.turn += 1
237         self.send_gamestate()
238         self.pool_result = self.pool.map_async(fib, (35, 35))
239
240     def cmd_MOVE(self, direction):
241         """Set player task to 'move' with direction arg, finish player turn."""
242         import parser
243         legal_directions = self.world.map_.get_directions()
244         if direction not in legal_directions:
245             raise parser.ArgError('Move argument must be one of: ' +
246                                   ', '.join(legal_directions))
247         self.world.get_player().set_task('move', direction=direction)
248         self.proceed()
249     cmd_MOVE.argtypes = 'string'
250
251     def cmd_WAIT(self):
252         """Set player task to 'wait', finish player turn."""
253         self.world.get_player().set_task('wait')
254         self.proceed()
255
256     def cmd_GET_GAMESTATE(self, connection_id):
257         """Send game state jto caller."""
258         self.send_gamestate(connection_id)
259
260     def cmd_ECHO(self, msg, connection_id):
261         """Send msg to caller."""
262         self.io.send(msg, connection_id)
263     cmd_ECHO.argtypes = 'string'
264
265     def cmd_ALL(self, msg, connection_id):
266         """Send msg to all clients."""
267         self.io.send(msg)
268     cmd_ALL.argtypes = 'string'
269
270     def cmd_TERRAIN_LINE(self, y, terrain_line):
271         self.world.map_.set_line(y, terrain_line)
272     cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'