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