home · contact · privacy
Class renaming.
[plomrogue2-experiments] / server_ / game.py
1 import sys
2 sys.path.append('../')
3 import game_common
4
5
6 class GameError(Exception):
7     pass
8
9
10 def move_pos(direction, pos_yx):
11     if direction == 'UP':
12         pos_yx[0] -= 1
13     elif direction == 'DOWN':
14         pos_yx[0] += 1
15     elif direction == 'RIGHT':
16         pos_yx[1] += 1
17     elif direction == 'LEFT':
18         pos_yx[1] -= 1
19
20
21 class Map(game_common.Map):
22
23     def get_line(self, y):
24         width = self.size[1]
25         return self.terrain[y * width:(y + 1) * width]
26
27
28 class World(game_common.World):
29
30     def __init__(self):
31         super().__init__()
32         self.Thing = Thing  # use local Thing class instead of game_common's
33         self.map_ = Map()  # use extended child class
34         self.player_id = 0
35
36     def proceed_to_next_player_turn(self):
37         """Run game world turns until player can decide their next step.
38
39         Iterates through all non-player things, on each step
40         furthering them in their tasks (and letting them decide new
41         ones if they finish). The iteration order is: first all things
42         that come after the player in the world things list, then
43         (after incrementing the world turn) all that come before the
44         player; then the player's .proceed() is run, and if it does
45         not finish his task, the loop starts at the beginning. Once
46         the player's task is finished, the loop breaks.
47         """
48         while True:
49             player = self.get_player()
50             player_i = self.things.index(player)
51             for thing in self.things[player_i+1:]:
52                 thing.proceed()
53             self.turn += 1
54             for thing in self.things[:player_i]:
55                 thing.proceed()
56             player.proceed(is_AI=False)
57             if player.task is None:
58                 break
59
60     def get_player(self):
61         return self.get_thing(self.player_id)
62
63
64 class Task:
65
66     def __init__(self, thing, name, args=(), kwargs={}):
67         self.name = name
68         self.thing = thing
69         self.args = args
70         self.kwargs = kwargs
71         self.todo = 3
72
73     def check(self):
74         if self.name == 'move':
75             if len(self.args) > 0:
76                 direction = self.args[0]
77             else:
78                 direction = self.kwargs['direction']
79             test_pos = self.thing.position[:]
80             move_pos(direction, test_pos)
81             if test_pos[0] < 0 or test_pos[1] < 0 or \
82                test_pos[0] >= self.thing.world.map_.size[0] or \
83                test_pos[1] >= self.thing.world.map_.size[1]:
84                 raise GameError('would move outside map bounds')
85             pos_i = test_pos[0] * self.thing.world.map_.size[1] + test_pos[1]
86             map_tile = self.thing.world.map_.terrain[pos_i]
87             if map_tile != '.':
88                 raise GameError('would move into illegal terrain')
89             for t in self.thing.world.things:
90                 if t.position == test_pos:
91                     raise GameError('would move into other thing')
92
93
94 class Thing(game_common.Thing):
95
96     def __init__(self, *args, **kwargs):
97         super().__init__(*args, **kwargs)
98         self.task = Task(self, 'wait')
99         self.last_task_result = None
100         self._stencil = None
101
102     def task_wait(self):
103         return 'success'
104
105     def task_move(self, direction):
106         move_pos(direction, self.position)
107         return 'success'
108
109     def decide_task(self):
110         if self.position[1] > 1:
111             self.set_task('move', 'LEFT')
112         elif self.position[1] < 3:
113             self.set_task('move', 'RIGHT')
114         else:
115             self.set_task('wait')
116
117     def set_task(self, task_name, *args, **kwargs):
118         self.task = Task(self, task_name, args, kwargs)
119         self.task.check()
120
121     def proceed(self, is_AI=True):
122         """Further the thing in its tasks.
123
124         Decrements .task.todo; if it thus falls to <= 0, enacts method
125         whose name is 'task_' + self.task.name and sets .task =
126         None. If is_AI, calls .decide_task to decide a self.task.
127
128         Before doing anything, ensures an empty map visibility stencil
129         and checks that task is still possible, and aborts it
130         otherwise (for AI things, decides a new task).
131
132         """
133         self._stencil = None
134         try:
135             self.task.check()
136         except GameError as e:
137             self.task = None
138             self.last_task_result = e
139             if is_AI:
140                 self.decide_task()
141             return
142         self.task.todo -= 1
143         if self.task.todo <= 0:
144             task = getattr(self, 'task_' + self.task.name)
145             self.last_task_result = task(*self.task.args, **self.task.kwargs)
146             self.task = None
147         if is_AI and self.task is None:
148             self.decide_task()
149
150     def get_stencil(self):
151         if self._stencil is not None:
152             return self._stencil
153         size = self.world.map_.size
154         m = Map(self.world.map_.size, '?'*size[0]*size[1])
155         y_me = self.position[0]
156         x_me = self.position[1]
157         for y in range(m.size[0]):
158             if y in (y_me - 1, y_me, y_me + 1):
159                 for x in range(m.size[1]):
160                     if x in (x_me - 1, x_me, x_me + 1):
161                         pos = y * size[1] + x
162                         m.terrain = m.terrain[:pos] + '.' + m.terrain[pos+1:]
163         self._stencil = m
164         return self._stencil
165
166     def get_visible_map(self):
167         stencil = self.get_stencil()
168         size = self.world.map_.size
169         size_i = self.world.map_.size[0] * self.world.map_.size[1]
170         m = Map(size, ' '*size_i)
171         for i in range(size_i):
172             if stencil.terrain[i] == '.':
173                 c = self.world.map_.terrain[i]
174                 m.terrain = m.terrain[:i] + c + m.terrain[i+1:]
175         return m
176
177     def get_visible_things(self):
178         stencil = self.get_stencil()
179         visible_things = []
180         for thing in self.world.things:
181             width = self.world.map_.size[1]
182             pos_i = thing.position[0] * width + thing.position[1]
183             if stencil.terrain[pos_i] == '.':
184                 visible_things += [thing]
185         return visible_things
186
187
188 def fib(n):
189     """Calculate n-th Fibonacci number. Very inefficiently."""
190     if n in (1, 2):
191         return 1
192     else:
193         return fib(n-1) + fib(n-2)
194
195
196 class Game(game_common.CommonCommandsMixin):
197
198     def __init__(self, game_file_name):
199         import server_.io
200         self.world = World()
201         self.io = server_.io.GameIO(game_file_name, self)
202         # self.pool and self.pool_result are currently only needed by the FIB
203         # command and the demo of a parallelized game loop in cmd_inc_p.
204         from multiprocessing import Pool
205         self.pool = Pool()
206         self.pool_result = None
207
208     def send_gamestate(self, connection_id=None):
209         """Send out game state data relevant to clients."""
210
211         def stringify_yx(tuple_):
212             """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
213             return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])
214
215         self.io.send('NEW_TURN ' + str(self.world.turn))
216         self.io.send('MAP_SIZE ' + stringify_yx(self.world.map_.size))
217         visible_map = self.world.get_player().get_visible_map()
218         for y in range(self.world.map_.size[0]):
219             self.io.send('VISIBLE_MAP_LINE %5s %s' %
220                          (y, self.io.quote(visible_map.get_line(y))))
221         visible_things = self.world.get_player().get_visible_things()
222         for thing in visible_things:
223             self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
224             self.io.send('THING_POS %s %s' % (thing.id_,
225                                               stringify_yx(thing.position)))
226
227     def proceed(self):
228         """Send turn finish signal, run game world, send new world data.
229
230         First sends 'TURN_FINISHED' message, then runs game world
231         until new player input is needed, then sends game state.
232         """
233         self.io.send('TURN_FINISHED ' + str(self.world.turn))
234         self.world.proceed_to_next_player_turn()
235         msg = str(self.world.get_player().last_task_result)
236         self.io.send('LAST_PLAYER_TASK_RESULT ' + self.io.quote(msg))
237         self.send_gamestate()
238
239     def cmd_FIB(self, numbers, connection_id):
240         """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
241
242         Numbers are calculated in parallel as far as possible, using fib().
243         A 'CALCULATING …' message is sent to caller before the result.
244         """
245         self.io.send('CALCULATING …', connection_id)
246         results = self.pool.map(fib, numbers)
247         reply = ' '.join([str(r) for r in results])
248         self.io.send(reply, connection_id)
249     cmd_FIB.argtypes = 'seq:int:nonneg'
250
251     def cmd_INC_P(self, connection_id):
252         """Increment world.turn, send game turn data to everyone.
253
254         To simulate game processing waiting times, a one second delay between
255         TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive
256         calculations are started as pool processes that need to be finished
257         until a further INC finishes the turn.
258
259         This is just a demo structure for how the game loop could work when
260         parallelized. One might imagine a two-step game turn, with a non-action
261         step determining actor tasks (the AI determinations would take the
262         place of the fib calculations here), and an action step wherein these
263         tasks are performed (where now sleep(1) is).
264         """
265         from time import sleep
266         if self.pool_result is not None:
267             self.pool_result.wait()
268         self.io.send('TURN_FINISHED ' + str(self.world.turn))
269         sleep(1)
270         self.world.turn += 1
271         self.send_gamestate()
272         self.pool_result = self.pool.map_async(fib, (35, 35))
273
274     def cmd_MOVE(self, direction):
275         """Set player task to 'move' with direction arg, finish player turn."""
276         import parser
277         if direction not in {'UP', 'DOWN', 'RIGHT', 'LEFT'}:
278             raise parser.ArgError('Move argument must be one of: '
279                                   'UP, DOWN, RIGHT, LEFT')
280         self.world.get_player().set_task('move', direction=direction)
281         self.proceed()
282     cmd_MOVE.argtypes = 'string'
283
284     def cmd_WAIT(self):
285         """Set player task to 'wait', finish player turn."""
286         self.world.get_player().set_task('wait')
287         self.proceed()
288
289     def cmd_GET_GAMESTATE(self, connection_id):
290         """Send game state jto caller."""
291         self.send_gamestate(connection_id)
292
293     def cmd_ECHO(self, msg, connection_id):
294         """Send msg to caller."""
295         self.io.send(msg, connection_id)
296     cmd_ECHO.argtypes = 'string'
297
298     def cmd_ALL(self, msg, connection_id):
299         """Send msg to all clients."""
300         self.io.send(msg)
301     cmd_ALL.argtypes = 'string'
302
303     def cmd_TERRAIN_LINE(self, y, terrain_line):
304         self.world.map_.set_line(y, terrain_line)
305     cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'