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