home · contact · privacy
Redo world generation with GEN_MAP command.
[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
210     def proceed(self):
211         """Send turn finish signal, run game world, send new world data.
212
213         First sends 'TURN_FINISHED' message, then runs game world
214         until new player input is needed, then sends game state.
215         """
216         self.io.send('TURN_FINISHED ' + str(self.world.turn))
217         self.world.proceed_to_next_player_turn()
218         msg = str(self.world.get_player().last_task_result)
219         self.io.send('LAST_PLAYER_TASK_RESULT ' + self.io.quote(msg))
220         self.send_gamestate()
221
222     def cmd_FIB(self, numbers, connection_id):
223         """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
224
225         Numbers are calculated in parallel as far as possible, using fib().
226         A 'CALCULATING …' message is sent to caller before the result.
227         """
228         self.io.send('CALCULATING …', connection_id)
229         results = self.pool.map(fib, numbers)
230         reply = ' '.join([str(r) for r in results])
231         self.io.send(reply, connection_id)
232     cmd_FIB.argtypes = 'seq:int:nonneg'
233
234     def cmd_INC_P(self, connection_id):
235         """Increment world.turn, send game turn data to everyone.
236
237         To simulate game processing waiting times, a one second delay between
238         TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive
239         calculations are started as pool processes that need to be finished
240         until a further INC finishes the turn.
241
242         This is just a demo structure for how the game loop could work when
243         parallelized. One might imagine a two-step game turn, with a non-action
244         step determining actor tasks (the AI determinations would take the
245         place of the fib calculations here), and an action step wherein these
246         tasks are performed (where now sleep(1) is).
247         """
248         from time import sleep
249         if self.pool_result is not None:
250             self.pool_result.wait()
251         self.io.send('TURN_FINISHED ' + str(self.world.turn))
252         sleep(1)
253         self.world.turn += 1
254         self.send_gamestate()
255         self.pool_result = self.pool.map_async(fib, (35, 35))
256
257     def cmd_MOVE(self, direction):
258         """Set player task to 'move' with direction arg, finish player turn."""
259         import parser
260         legal_directions = self.world.map_.get_directions()
261         if direction not in legal_directions:
262             raise parser.ArgError('Move argument must be one of: ' +
263                                   ', '.join(legal_directions))
264         self.world.get_player().set_task('move', direction=direction)
265         self.proceed()
266     cmd_MOVE.argtypes = 'string'
267
268     def cmd_WAIT(self):
269         """Set player task to 'wait', finish player turn."""
270         self.world.get_player().set_task('wait')
271         self.proceed()
272
273     def cmd_GET_GAMESTATE(self, connection_id):
274         """Send game state jto caller."""
275         self.send_gamestate(connection_id)
276
277     def cmd_ECHO(self, msg, connection_id):
278         """Send msg to caller."""
279         self.io.send(msg, connection_id)
280     cmd_ECHO.argtypes = 'string'
281
282     def cmd_ALL(self, msg, connection_id):
283         """Send msg to all clients."""
284         self.io.send(msg)
285     cmd_ALL.argtypes = 'string'
286
287     def cmd_TERRAIN_LINE(self, y, terrain_line):
288         self.world.map_.set_line(y, terrain_line)
289     cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'
290
291     def cmd_GEN_WORLD(self, geometry, yx, seed):
292         legal_grids = self.map_manager.get_map_geometries()
293         if geometry not in legal_grids:
294             raise ArgError('First map argument must be one of: ' +
295                            ', '.join(legal_grids))
296         self.world.make_new(geometry, yx, seed)
297     cmd_GEN_WORLD.argtypes = 'string yx_tuple:pos string'