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