Speed up arrival of the nemesis
[erdslangetjie.git] / erdslangetjie / level.py
1 # The level object
2
3 import os
4 from data import load_image, load, filepath
5
6 WALL = '.'
7 FLOOR = ' '
8 ENTRY = 'E'
9 EXIT = 'X'
10 GATE = 'G'
11 BUTTON = 'B'
12
13
14 class Level(object):
15
16     def __init__(self, levelfile):
17         self._data = []
18         self.exit_pos = []
19         self.enter_pos = None
20         self._tiles = []
21         self._changed = []
22         self._gates = {}
23         self._buttons = {}
24         # Because of how kivy's coordinate system works,
25         # we reverse the lines so things match up between
26         # the file and the display (top of file == top of display)
27         for line in reversed(levelfile.readlines()):
28             self._data.append(list(line.strip('\n')))
29
30     def load_tiles(self):
31         """Load the list of tiles for the level"""
32         self._tiles = []
33         self._gates = {}
34         self._buttons = {}
35         self.exit_pos = []
36         self._changed = []
37         self.enter_pos = None
38         for j, line in enumerate(self._data):
39             tile_line = []
40             for i, c in enumerate(line):
41                 tile_image = self._get_tile_image((i, j), c)
42                 tile_line.append(tile_image)
43             self._tiles.append(tile_line)
44
45     def _get_tile_image(self, pos, c):
46         image = None
47         if c == FLOOR:
48             image = load_image('tiles/floor.png')
49         elif c == WALL:
50             image = self._get_wall_tile(pos)
51         elif c == ENTRY:
52             self.enter_pos = pos
53             image = load_image('tiles/entry.png')
54         elif c == EXIT:
55             self.exit_pos.append(pos)
56             image = load_image('tiles/door.png')
57         elif c == GATE:
58             if pos not in self._gates:
59                 self._gates[pos] = -1  # down
60                 image = load_image('tiles/gate_down.png')
61             else:
62                 state = self._gates[pos]
63                 if state == -1:
64                     image = load_image('tiles/gate_down.png')
65                 elif state == 0:
66                     # destroyed
67                     image = load_image('tiles/floor.png')
68                 elif state == 1:
69                     # badly damaged
70                     image = load_image('tiles/gate_dented.png')
71                 elif state == 2:
72                     # lightly damaged
73                     image = load_image('tiles/gate_bent.png')
74                 else:
75                     # gate up
76                     image = load_image('tiles/gate_up.png')
77         elif c == BUTTON:
78             if not pos in self._buttons:
79                 image = load_image('tiles/button.png')
80                 self._buttons[pos] = 'active'
81             elif self._buttons[pos] == 'active':
82                 image = load_image('tiles/button.png')
83             else:
84                 image = load_image('tiles/floor.png')
85         if image is None:
86             raise RuntimeError('Unknown tile type %s at %s' % (c, pos))
87         return image
88
89     def validate(self):
90         entry_points = 0
91         exit_points = 0
92         gates = 0
93         buttons = 0
94         for line in self._data:
95             if ENTRY in line:
96                 entry_points += line.count(ENTRY)
97             if EXIT in line:
98                 exit_points += line.count(EXIT)
99             if BUTTON in line:
100                 buttons += line.count(BUTTON)
101             if GATE in line:
102                 gates += line.count(GATE)
103         if entry_points == 0:
104             raise RuntimeError('No entry point')
105         if entry_points > 1:
106             raise RuntimeError('Multiple entry points')
107         if exit_points == 0:
108             raise RuntimeError('No exit')
109         if gates != buttons:
110             raise RuntimeError('The number of buttons and gates differ')
111
112     def get_tiles(self):
113         return self._tiles
114
115     def get_single_tile(self, pos):
116         return self._tiles[pos[1]][pos[0]]
117
118     def get_tile_type(self, pos):
119         return self._data[pos[1]][pos[0]]
120
121     def set_tile_type(self, pos, new_type):
122         # Setting the type resets any state anyway, so
123         if pos in self._gates:
124             del self._gates[pos]
125         if pos in self._buttons:
126             del self._buttons[pos]
127         self._data[pos[1]][pos[0]] = new_type
128         new_tile = self._get_tile_image(pos, new_type)
129         self._tiles[pos[1]][pos[0]] = new_tile
130         self._changed.append((pos, new_tile))
131         # Also update neighbourhood for wall types, etc.
132         for new_pos in [(pos[0] - 1, pos[1]), (pos[0] + 1, pos[1]),
133                 (pos[0] - 1, pos[1] - 1), (pos[0] + 1, pos[1] + 1),
134                 (pos[0], pos[1] - 1), (pos[0], pos[1] + 1),
135                 (pos[0] - 1, pos[1] + 1), (pos[0] + 1, pos[1] - 1)]:
136             if not self._in_limits(new_pos):
137                 continue
138             # Update display to changed status
139             self._fix_tile(new_pos)
140
141     def _fix_tile(self, pos):
142         tile = self._data[pos[1]][pos[0]]
143         new_tile = self._get_tile_image(pos, tile)
144         self._tiles[pos[1]][pos[0]] = new_tile
145         self._changed.append((pos, new_tile))
146
147     def get_size(self):
148         return len(self._tiles[0]), len(self._tiles)
149
150     def at_exit(self, pos):
151         return pos in self.exit_pos
152
153     def get_level_data(self):
154         return '\n'.join(reversed([''.join(x) for x in self._data]))
155
156     def _get_wall_tile(self, pos):
157         # Is the neighbour in this direction also a wall?
158         x, y = pos
159         left = right = top = bottom = False
160         if x == 0:
161             left = True
162         elif self._data[y][x - 1] == WALL:
163             left = True
164         if x == len(self._data[0]) - 1:
165             right = True
166         elif self._data[y][x + 1] == WALL:
167             right = True
168         if y == 0:
169             top = True
170         elif self._data[y - 1][x] == WALL:
171             top = True
172         if y == len(self._data) - 1:
173             bottom = True
174         elif self._data[y + 1][x] == WALL:
175             bottom = True
176         if top and bottom and left and right:
177             return load_image('tiles/cwall.png')
178         elif bottom and left and right:
179             return load_image('tiles/bottom_wall.png')
180         elif top and left and right:
181             return load_image('tiles/top_wall.png')
182         elif top and bottom and right:
183             return load_image('tiles/left_wall.png')
184         elif top and bottom and left:
185             return load_image('tiles/right_wall.png')
186         elif top and bottom:
187             return load_image('tiles/vert_wall.png')
188         elif left and right:
189             return load_image('tiles/horiz_wall.png')
190         elif left and top:
191             return load_image('tiles/corner_lt.png')
192         elif left and bottom:
193             return load_image('tiles/corner_lb.png')
194         elif right and top:
195             return load_image('tiles/corner_rt.png')
196         elif right and bottom:
197             return load_image('tiles/corner_rb.png')
198         elif top:
199             return load_image('tiles/end_top.png')
200         elif bottom:
201             return load_image('tiles/end_bottom.png')
202         elif left:
203             return load_image('tiles/end_right.png')
204         elif right:
205             return load_image('tiles/end_left.png')
206         return load_image('tiles/pillar.png')
207
208     def _in_limits(self, pos):
209         if pos[0] < 0:
210             return False
211         if pos[1] < 0:
212             return False
213         try:
214             self._data[pos[1]][pos[0]]
215         except IndexError:
216             return False
217         return True
218
219     def blocked(self, pos):
220         if pos[0] < 0:
221             return True
222         if pos[1] < 0:
223             return True
224         try:
225             tile = self._data[pos[1]][pos[0]]
226         except IndexError:
227             return True
228         if tile == WALL or tile == ENTRY:
229             return True
230         if tile == GATE:
231             if self._gates[pos] > 0:
232                 return True
233         return False
234
235     def calc_dist(self, pos1, pos2):
236         return abs(pos1[0] - pos2[0]) + abs(pos1[1] - pos2[1])
237
238     def is_gate(self, pos):
239         if not self._in_limits(pos):
240             return False
241         return self._data[pos[1]][pos[0]] == GATE
242
243     def is_button(self, pos):
244         if not self._in_limits(pos):
245             return False
246         return self._data[pos[1]][pos[0]] == BUTTON
247
248     def is_wall(self, pos):
249         if not self._in_limits(pos):
250             return True
251         return self._data[pos[1]][pos[0]] == WALL
252
253     def trigger_button(self, pos):
254         if not self.is_button(pos):
255             return False
256         if not self._buttons[pos] == 'active':
257             return False
258         # Find the closest gate down gate and trigger it
259         mindist = 99999
260         gate_pos = None
261         for cand in self._gates:
262             dist = self.calc_dist(pos, cand)
263             if dist < mindist:
264                 gate_pos = cand
265                 mindist = dist
266         if gate_pos:
267             self._buttons[pos] = 'pressed'
268             self._gates[gate_pos] = 3  # Raise gate
269             self._fix_tile(pos)
270             self._fix_tile(gate_pos)
271
272     def damage_gate(self, pos):
273         if not self.is_gate(pos):
274             return
275         if self._gates[pos] == -1 or self._gates[pos] == 0:
276             return
277         self._gates[pos] = self._gates[pos] - 1
278         self._fix_tile(pos)
279
280     def get_changed_tiles(self):
281         ret = self._changed[:]
282         self._changed = []
283         return ret
284
285
286 class LevelList(object):
287
288     LEVELS = 'level_list'
289
290     def __init__(self):
291         self.levels = []
292         self.errors = []
293         level_list = load(self.LEVELS)
294         for line in level_list:
295             line = line.strip()
296             if os.path.exists(filepath(line)):
297                 level_file = load(line)
298                 level = Level(level_file)
299                 level_file.close()
300                 try:
301                     level.validate()
302                     self.levels.append(level)
303                 except RuntimeError as err:
304                     self.errors.append(
305                             'Invalid level %s in level_list: %s' % (line, err))
306             else:
307                 self.errors.append(
308                     'Level list includes non-existant level %s' % line)
309         level_list.close()
310         self._cur_level = 0
311
312     def get_current_level(self):
313         if self._cur_level < len(self.levels):
314             return self.levels[self._cur_level]
315         else:
316             return None
317
318     def get_errors(self):
319         return self.errors
320
321     def advance_to_next_level(self):
322         self._cur_level += 1
323         return self.get_current_level()
324
325     def reset(self):
326         self._cur_level = 0