home · contact · privacy
071290019a4c2f58f552bdbf51f28b66887cb1da
[plomrogue2-experiments] / new / plomrogue / game.py
1 from plomrogue.tasks import Task_WAIT, Task_MOVE
2 from plomrogue.errors import GameError, ArgError
3 from plomrogue.commands import (cmd_GEN_WORLD, cmd_GET_GAMESTATE, cmd_MAP,
4                                 cmd_MAP, cmd_THING_TYPE, cmd_THING_POS,
5                                 cmd_TERRAIN_LINE, cmd_PLAYER_ID, cmd_TURN,
6                                 cmd_SWITCH_PLAYER, cmd_SAVE)
7 from plomrogue.mapping import MapHex
8 from plomrogue.parser import Parser
9 from plomrogue.io import GameIO
10 from plomrogue.misc import quote, stringify_yx
11
12
13
14 class ThingBase:
15
16     def __init__(self, world, id_, type_='?', position=[0,0]):
17         self.world = world
18         self.id_ = id_
19         self.type_ = type_
20         self.position = position
21
22
23
24 class Thing(ThingBase):
25
26     def __init__(self, *args, **kwargs):
27         super().__init__(*args, **kwargs)
28         self.set_task('WAIT')
29         self._last_task_result = None
30         self._stencil = None
31
32     def move_towards_target(self, target):
33         dijkstra_map = type(self.world.map_)(self.world.map_.size)
34         n_max = 256
35         dijkstra_map.terrain = [n_max for i in range(dijkstra_map.size_i)]
36         dijkstra_map[target] = 0
37         shrunk = True
38         visible_map = self.get_visible_map()
39         while shrunk:
40             shrunk = False
41             for pos in dijkstra_map:
42                 if visible_map[pos] != '.':
43                     continue
44                 neighbors = dijkstra_map.get_neighbors(tuple(pos))
45                 for direction in neighbors:
46                     yx = neighbors[direction]
47                     if yx is not None and dijkstra_map[yx] < dijkstra_map[pos] - 1:
48                         dijkstra_map[pos] = dijkstra_map[yx] + 1
49                         shrunk = True
50         #with open('log', 'a') as f:
51         #    f.write('---------------------------------\n')
52         #    for y, line in dijkstra_map.lines():
53         #        for val in line:
54         #            if val < 10:
55         #                f.write(str(val))
56         #            elif val == 256:
57         #                f.write('x')
58         #            else:
59         #                f.write('~')
60         #        f.write('\n')
61         neighbors = dijkstra_map.get_neighbors(tuple(self.position))
62         n = n_max
63         #print('DEBUG', self.position, neighbors)
64         #dirs = dijkstra_map.get_directions()
65         #print('DEBUG dirs', dirs)
66         #print('DEBUG neighbors', neighbors)
67         #debug_scores = []
68         #for pos in neighbors:
69         #    if pos is None:
70         #        debug_scores += [9000]
71         #    else:
72         #        debug_scores += [dijkstra_map[pos]]
73         #print('DEBUG debug_scores', debug_scores)
74         target_direction = None
75         for direction in neighbors:
76             yx = neighbors[direction]
77             if yx is not None:
78                 n_new = dijkstra_map[yx]
79                 if n_new < n:
80                     n = n_new
81                     target_direction = direction
82         #print('DEBUG result', direction)
83         if target_direction:
84             self.set_task('MOVE', (target_direction,))
85
86     def decide_task(self):
87         # TODO: Check if monster can follow player too well (even when they should lose them)
88         visible_things = self.get_visible_things()
89         target = None
90         for t in visible_things:
91             if t.type_ == 'human':
92                 target = t.position
93                 break
94         if target is not None:
95             try:
96                 self.move_towards_target(target)
97                 return
98             except GameError:
99                 pass
100         self.set_task('WAIT')
101
102     def set_task(self, task_name, args=()):
103         task_class = self.world.game.tasks[task_name]
104         self.task = task_class(self, args)
105         self.task.check()  # will throw GameError if necessary
106
107     def proceed(self, is_AI=True):
108         """Further the thing in its tasks.
109
110         Decrements .task.todo; if it thus falls to <= 0, enacts method
111         whose name is 'task_' + self.task.name and sets .task =
112         None. If is_AI, calls .decide_task to decide a self.task.
113
114         Before doing anything, ensures an empty map visibility stencil
115         and checks that task is still possible, and aborts it
116         otherwise (for AI things, decides a new task).
117
118         """
119         self._stencil = None
120         try:
121             self.task.check()
122         except GameError as e:
123             self.task = None
124             self._last_task_result = e
125             if is_AI:
126                 try:
127                     self.decide_task()
128                 except GameError:
129                     self.set_task('WAIT')
130             return
131         self.task.todo -= 1
132         if self.task.todo <= 0:
133             self._last_task_result = self.task.do()
134             self.task = None
135         if is_AI and self.task is None:
136             try:
137                 self.decide_task()
138             except GameError:
139                 self.set_task('WAIT')
140
141     def get_stencil(self):
142         if self._stencil is not None:
143             return self._stencil
144         self._stencil = self.world.map_.get_fov_map(self.position)
145         return self._stencil
146
147     def get_visible_map(self):
148         stencil = self.get_stencil()
149         m = self.world.map_.new_from_shape(' ')
150         for pos in m:
151             if stencil[pos] == '.':
152                 m[pos] = self.world.map_[pos]
153         return m
154
155     def get_visible_things(self):
156         stencil = self.get_stencil()
157         visible_things = []
158         for thing in self.world.things:
159             if stencil[thing.position] == '.':
160                 visible_things += [thing]
161         return visible_things
162
163
164
165 class WorldBase:
166
167     def __init__(self, game):
168         self.turn = 0
169         self.things = []
170         self.game = game
171
172     def get_thing(self, id_, create_unfound=True):
173         for thing in self.things:
174             if id_ == thing.id_:
175                 return thing
176         if create_unfound:
177             t = self.game.thing_type(self, id_)
178             self.things += [t]
179             return t
180         return None
181
182
183
184 class World(WorldBase):
185
186     def __init__(self, *args, **kwargs):
187         super().__init__(*args, **kwargs)
188         self.player_id = 0
189
190     def new_map(self, yx):
191         self.map_ = self.game.map_type(yx)
192
193     def proceed_to_next_player_turn(self):
194         """Run game world turns until player can decide their next step.
195
196         Iterates through all non-player things, on each step
197         furthering them in their tasks (and letting them decide new
198         ones if they finish). The iteration order is: first all things
199         that come after the player in the world things list, then
200         (after incrementing the world turn) all that come before the
201         player; then the player's .proceed() is run, and if it does
202         not finish his task, the loop starts at the beginning. Once
203         the player's task is finished, the loop breaks.
204         """
205         while True:
206             player = self.get_player()
207             player_i = self.things.index(player)
208             for thing in self.things[player_i+1:]:
209                 thing.proceed()
210             self.turn += 1
211             for thing in self.things[:player_i]:
212                 thing.proceed()
213             player.proceed(is_AI=False)
214             if player.task is None:
215                 break
216
217     def get_player(self):
218         return self.get_thing(self.player_id)
219
220     def make_new(self, yx, seed):
221         import random
222         random.seed(seed)
223         self.turn = 0
224         self.new_map(yx)
225         for pos in self.map_:
226             if 0 in pos or (yx[0] - 1) == pos[0] or (yx[1] - 1) == pos[1]:
227                 self.map_[pos] = '#'
228                 continue
229             self.map_[pos] = random.choice(('.', '.', '.', '.', 'x'))
230         player = self.game.thing_type(self, 0)
231         player.type_ = 'human'
232         player.position = [random.randint(0, yx[0] -1),
233                            random.randint(0, yx[1] - 1)]
234         npc = self.game.thing_type(self, 1)
235         npc.type_ = 'monster'
236         npc.position = [random.randint(0, yx[0] -1),
237                         random.randint(0, yx[1] -1)]
238         self.things = [player, npc]
239         return 'success'
240
241
242
243 class Game:
244
245     def __init__(self, game_file_name):
246         self.io = GameIO(game_file_name, self)
247         self.map_type = MapHex
248         self.tasks = {'WAIT': Task_WAIT, 'MOVE': Task_MOVE}
249         self.commands = {'GEN_WORLD': cmd_GEN_WORLD,
250                          'GET_GAMESTATE': cmd_GET_GAMESTATE,
251                          'MAP': cmd_MAP,
252                          'THING_TYPE': cmd_THING_TYPE,
253                          'THING_POS': cmd_THING_POS,
254                          'TERRAIN_LINE': cmd_TERRAIN_LINE,
255                          'PLAYER_ID': cmd_PLAYER_ID,
256                          'TURN': cmd_TURN,
257                          'SWITCH_PLAYER': cmd_SWITCH_PLAYER,
258                          'SAVE': cmd_SAVE}
259         self.world_type = World
260         self.world = self.world_type(self)
261         self.thing_type = Thing
262
263     def get_string_options(self, string_option_type):
264         if string_option_type == 'direction':
265             return self.world.map_.get_directions()
266         return None
267
268     def send_gamestate(self, connection_id=None):
269         """Send out game state data relevant to clients."""
270
271         self.io.send('TURN ' + str(self.world.turn))
272         self.io.send('MAP ' + stringify_yx(self.world.map_.size))
273         visible_map = self.world.get_player().get_visible_map()
274         for y, line in visible_map.lines():
275             self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, quote(line)))
276         visible_things = self.world.get_player().get_visible_things()
277         for thing in visible_things:
278             self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
279             self.io.send('THING_POS %s %s' % (thing.id_,
280                                               stringify_yx(thing.position)))
281         player = self.world.get_player()
282         self.io.send('PLAYER_POS %s' % (stringify_yx(player.position)))
283         self.io.send('GAME_STATE_COMPLETE')
284
285     def proceed(self):
286         """Send turn finish signal, run game world, send new world data.
287
288         First sends 'TURN_FINISHED' message, then runs game world
289         until new player input is needed, then sends game state.
290         """
291         self.io.send('TURN_FINISHED ' + str(self.world.turn))
292         self.world.proceed_to_next_player_turn()
293         msg = str(self.world.get_player()._last_task_result)
294         self.io.send('LAST_PLAYER_TASK_RESULT ' + quote(msg))
295         self.send_gamestate()
296
297     def get_command(self, command_name):
298
299         def partial_with_attrs(f, *args, **kwargs):
300             from functools import partial
301             p = partial(f, *args, **kwargs)
302             p.__dict__.update(f.__dict__)
303             return p
304
305         def cmd_TASK_colon(task_name, game, *args):
306             game.world.get_player().set_task(task_name, args)
307             game.proceed()
308
309         def cmd_SET_TASK_colon(task_name, game, thing_id, todo, *args):
310             t = game.world.get_thing(thing_id, False)
311             if t is None:
312                 raise ArgError('No such Thing.')
313             task_class = game.tasks[task_name]
314             t.task = task_class(t, args)
315             t.task.todo = todo
316
317         def task_prefixed(command_name, task_prefix, task_command,
318                           argtypes_prefix=None):
319             if command_name[:len(task_prefix)] == task_prefix:
320                 task_name = command_name[len(task_prefix):]
321                 if task_name in self.tasks:
322                     f = partial_with_attrs(task_command, task_name, self)
323                     task = self.tasks[task_name]
324                     if argtypes_prefix:
325                         f.argtypes = argtypes_prefix + ' ' + task.argtypes
326                     else:
327                         f.argtypes = task.argtypes
328                     return f
329             return None
330
331         command = task_prefixed(command_name, 'TASK:', cmd_TASK_colon)
332         if command:
333             return command
334         command = task_prefixed(command_name, 'SET_TASK:', cmd_SET_TASK_colon,
335                                 'int:nonneg int:nonneg ')
336         if command:
337             return command
338         if command_name in self.commands:
339             f = partial_with_attrs(self.commands[command_name], self)
340             return f
341         return None