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