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