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