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