c722b563873c76034e73548e2a723c1ba0cfdf14
[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             image = load_image('tiles/button.png')
79             self._buttons[pos] = 'active'
80         if image is None:
81             raise RuntimeError('Unknown tile type %s at %s' % (c, pos))
82         return image
83
84     def validate(self):
85         entry_points = 0
86         exit_points = 0
87         for line in self._data:
88             if ENTRY in line:
89                 entry_points += line.count(ENTRY)
90             if EXIT in line:
91                 exit_points += line.count(EXIT)
92         if entry_points == 0:
93             raise RuntimeError('No entry point')
94         if entry_points > 1:
95             raise RuntimeError('Multiple entry points')
96         if exit_points == 0:
97             raise RuntimeError('No exit')
98
99     def get_tiles(self):
100         return self._tiles
101
102     def get_single_tile(self, pos):
103         return self._tiles[pos[1]][pos[0]]
104
105     def get_tile_type(self, pos):
106         return self._data[pos[1]][pos[0]]
107
108     def set_tile_type(self, pos, new_type):
109         # Setting the type resets any state anyway, so
110         if pos in self._gates:
111             del self._gates[pos]
112         if pos in self._buttons:
113             del self._buttons[pos]
114         self._data[pos[1]][pos[0]] = new_type
115         new_tile = self._get_tile_image(pos, new_type)
116         self._tiles[pos[1]][pos[0]] = new_tile
117         self._changed.append((pos, new_tile))
118         # Also update neighbourhood for wall types, etc.
119         for new_pos in [(pos[0] - 1, pos[1]), (pos[0] + 1, pos[1]),
120                 (pos[0] - 1, pos[1] - 1), (pos[0] + 1, pos[1] + 1),
121                 (pos[0], pos[1] - 1), (pos[0], pos[1] + 1),
122                 (pos[0] - 1, pos[1] + 1), (pos[0] + 1, pos[1] - 1)]:
123             if not self._in_limits(new_pos):
124                 continue
125             tile = self._data[new_pos[1]][new_pos[0]]
126             new_tile = self._get_tile_image(new_pos, tile)
127             self._tiles[new_pos[1]][new_pos[0]] = new_tile
128             self._changed.append((new_pos, new_tile))
129
130     def get_size(self):
131         return len(self._tiles[0]), len(self._tiles)
132
133     def at_exit(self, pos):
134         return pos in self.exit_pos
135
136     def get_level_data(self):
137         return '\n'.join(reversed([''.join(x) for x in self._data]))
138
139     def _get_wall_tile(self, pos):
140         # Is the neighbour in this direction also a wall?
141         x, y = pos
142         left = right = top = bottom = False
143         if x == 0:
144             left = True
145         elif self._data[y][x - 1] == WALL:
146             left = True
147         if x == len(self._data[0]) - 1:
148             right = True
149         elif self._data[y][x + 1] == WALL:
150             right = True
151         if y == 0:
152             top = True
153         elif self._data[y - 1][x] == WALL:
154             top = True
155         if y == len(self._data) - 1:
156             bottom = True
157         elif self._data[y + 1][x] == WALL:
158             bottom = True
159         if top and bottom and left and right:
160             return load_image('tiles/cwall.png')
161         elif bottom and left and right:
162             return load_image('tiles/bottom_wall.png')
163         elif top and left and right:
164             return load_image('tiles/top_wall.png')
165         elif top and bottom and right:
166             return load_image('tiles/left_wall.png')
167         elif top and bottom and left:
168             return load_image('tiles/right_wall.png')
169         elif top and bottom:
170             return load_image('tiles/vert_wall.png')
171         elif left and right:
172             return load_image('tiles/horiz_wall.png')
173         elif left and top:
174             return load_image('tiles/corner_lt.png')
175         elif left and bottom:
176             return load_image('tiles/corner_lb.png')
177         elif right and top:
178             return load_image('tiles/corner_rt.png')
179         elif right and bottom:
180             return load_image('tiles/corner_rb.png')
181         elif top:
182             return load_image('tiles/end_top.png')
183         elif bottom:
184             return load_image('tiles/end_bottom.png')
185         elif left:
186             return load_image('tiles/end_right.png')
187         elif right:
188             return load_image('tiles/end_left.png')
189         return load_image('tiles/pillar.png')
190
191     def _in_limits(self, pos):
192         if pos[0] < 0:
193             return False
194         if pos[1] < 0:
195             return False
196         try:
197             self._data[pos[1]][pos[0]]
198         except IndexError:
199             return False
200         return True
201
202     def blocked(self, pos):
203         if pos[0] < 0:
204             return True
205         if pos[1] < 0:
206             return True
207         try:
208             tile = self._data[pos[1]][pos[0]]
209         except IndexError:
210             return True
211         if tile == WALL or tile == ENTRY:
212             return True
213         if tile == GATE:
214             if self._gates[pos] != -1:
215                 return True
216         return False
217
218     def is_gate(self, pos):
219         return self._data[pos[1]][pos[0]] == GATE
220
221     def is_button(self, pos):
222         return self._data[pos[1]][pos[0]] == BUTTON
223
224     def trigger_button(self, pos):
225         if not self.is_button(pos):
226             return False
227         if not self._buttons[pos] == 'active':
228             return False
229         # Find the closest gate down gate and trigger it
230         gate_pos = pos
231
232         self._changed.append((pos, self.get_single_tile(pos)))
233         self._changed.append((gate_pos, self.get_single_tile(pos)))
234
235     def damage_gate(self, pos):
236         if not self.is_gate(pos):
237             return False
238         if self._gates[pos] == -1 or self._gates[pos] == 0:
239             return False
240         self._gates[pos] = self._gates[pos] - 1
241         self._fix_gate_tile(pos)
242         self._changed.append((pos, self.get_single_tile(pos)))
243         return True
244
245     def get_changed_tiles(self):
246         ret = self._changed[:]
247         self._changed = []
248         return ret
249
250
251 class LevelList(object):
252
253     LEVELS = 'level_list'
254
255     def __init__(self):
256         self.levels = []
257         self.errors = []
258         level_list = load(self.LEVELS)
259         for line in level_list:
260             line = line.strip()
261             if os.path.exists(filepath(line)):
262                 level_file = load(line)
263                 level = Level(level_file)
264                 level_file.close()
265                 try:
266                     level.validate()
267                     self.levels.append(level)
268                 except RuntimeError as err:
269                     self.errors.append(
270                             'Invalid level %s in level_list: %s' % (line, err))
271             else:
272                 self.errors.append(
273                     'Level list includes non-existant level %s' % line)
274         level_list.close()
275         self._cur_level = 0
276
277     def get_current_level(self):
278         if self._cur_level < len(self.levels):
279             return self.levels[self._cur_level]
280         else:
281             return None
282
283     def get_errors(self):
284         return self.errors
285
286     def advance_to_next_level(self):
287         self._cur_level += 1
288         return self.get_current_level()
289
290     def reset(self):
291         self._cur_level = 0