Less buggy Kivy 1.7 hackery
[erdslangetjie.git] / erdslangetjie / level.py
index 645a62f7678aca49f89f85a024dbaf4cc9ecd327..9ebb6f4912525a3f0f34c7589badbb80cf3525af 100644 (file)
 # The level object
 
-from data import load_image
+import os
+from data import load_image, load, filepath
+
+
+from kivy.logger import Logger
+
+WALL = '.'
+FLOOR = ' '
+ENTRY = 'E'
+EXIT = 'X'
+GATE = 'G'
+BUTTON = 'B'
 
 
 class Level(object):
 
-    def __init__(self):
-        self.data = []
+    def __init__(self, levelfile, name):
+        self._data = []
         self.exit_pos = []
         self.enter_pos = None
-        self.tiles = []
-
-    def load(self, levelfile):
-        """Load the level"""
-        self.data = []
+        self._tiles = []
+        self._changed = []
+        self._gates = {}
+        self._buttons = {}
+        self._name = name
         # Because of how kivy's coordinate system works,
         # we reverse the lines so things match up between
         # the file and the display (top of file == top of display)
         for line in reversed(levelfile.readlines()):
-            self.data.append(list(line.strip('\n')))
+            self._data.append(list(line.strip('\n')))
 
     def load_tiles(self):
         """Load the list of tiles for the level"""
-        self.tiles = []
+        Logger.info('%s: load tiles' % self._name)
+        self._tiles = []
+        self._gates = {}
+        self._buttons = {}
         self.exit_pos = []
+        self._changed = []
         self.enter_pos = None
-        for j, line in enumerate(self.data):
+        for j, line in enumerate(self._data):
             tile_line = []
             for i, c in enumerate(line):
-                if c == ' ':
-                    tile_line.append(load_image('tiles/floor.bmp'))
-                elif c == '.':
-                    tile_line.append(load_image('tiles/wall.bmp'))
-                elif c == 'E' or c == 'X':
-                    if c == 'E':
-                        if self.enter_pos:
-                            raise RuntimeError('Multiple entry points')
-                        self.enter_pos = (i, j)
-                    else:
-                        self.exit_pos.append((i, j))
-                    tile_line.append(load_image('tiles/door.bmp'))
-            self.tiles.append(tile_line)
+                tile_image = self._get_tile_image((i, j), c)
+                tile_line.append(tile_image)
+            self._tiles.append(tile_line)
+
+    def _get_tile_image(self, pos, c):
+        image = None
+        if c == FLOOR:
+            image = load_image('tiles/floor.png')
+        elif c == WALL:
+            image = self._get_wall_tile(pos)
+        elif c == ENTRY:
+            self.enter_pos = pos
+            image = load_image('tiles/entry.png')
+        elif c == EXIT:
+            self.exit_pos.append(pos)
+            image = load_image('tiles/door.png')
+        elif c == GATE:
+            if pos not in self._gates:
+                self._gates[pos] = -1  # down
+                image = load_image('tiles/gate_down.png')
+            else:
+                state = self._gates[pos]
+                if state == -1:
+                    image = load_image('tiles/gate_down.png')
+                elif state == 0:
+                    # destroyed
+                    image = load_image('tiles/floor.png')
+                elif state == 1:
+                    # badly damaged
+                    image = load_image('tiles/gate_dented.png')
+                elif state == 2:
+                    # lightly damaged
+                    image = load_image('tiles/gate_bent.png')
+                else:
+                    # gate up
+                    image = load_image('tiles/gate_up.png')
+        elif c == BUTTON:
+            if not pos in self._buttons:
+                image = load_image('tiles/button.png')
+                self._buttons[pos] = 'active'
+            elif self._buttons[pos] == 'active':
+                image = load_image('tiles/button.png')
+            else:
+                image = load_image('tiles/floor.png')
+        if image is None:
+            raise RuntimeError('Unknown tile type %s at %s' % (c, pos))
+        return image
+
+    def validate(self):
+        entry_points = 0
+        exit_points = 0
+        gates = 0
+        buttons = 0
+        for line in self._data:
+            if ENTRY in line:
+                entry_points += line.count(ENTRY)
+            if EXIT in line:
+                exit_points += line.count(EXIT)
+            if BUTTON in line:
+                buttons += line.count(BUTTON)
+            if GATE in line:
+                gates += line.count(GATE)
+        if entry_points == 0:
+            raise RuntimeError('No entry point')
+        if entry_points > 1:
+            raise RuntimeError('Multiple entry points')
+        if exit_points == 0:
+            raise RuntimeError('No exit')
+        if gates != buttons:
+            raise RuntimeError('The number of buttons and gates differ')
 
     def get_tiles(self):
-        return self.tiles
+        return self._tiles
+
+    def get_single_tile(self, pos):
+        return self._tiles[pos[1]][pos[0]]
+
+    def get_tile_type(self, pos):
+        return self._data[pos[1]][pos[0]]
+
+    def set_tile_type(self, pos, new_type):
+        # Setting the type resets any state anyway, so
+        if pos in self._gates:
+            del self._gates[pos]
+        if pos in self._buttons:
+            del self._buttons[pos]
+        self._data[pos[1]][pos[0]] = new_type
+        new_tile = self._get_tile_image(pos, new_type)
+        self._tiles[pos[1]][pos[0]] = new_tile
+        self._changed.append((pos, new_tile))
+        # Also update neighbourhood for wall types, etc.
+        for new_pos in [(pos[0] - 1, pos[1]), (pos[0] + 1, pos[1]),
+                (pos[0] - 1, pos[1] - 1), (pos[0] + 1, pos[1] + 1),
+                (pos[0], pos[1] - 1), (pos[0], pos[1] + 1),
+                (pos[0] - 1, pos[1] + 1), (pos[0] + 1, pos[1] - 1)]:
+            if not self._in_limits(new_pos):
+                continue
+            # Update display to changed status
+            self._fix_tile(new_pos)
+
+    def _fix_tile(self, pos):
+        tile = self._data[pos[1]][pos[0]]
+        new_tile = self._get_tile_image(pos, tile)
+        self._tiles[pos[1]][pos[0]] = new_tile
+        self._changed.append((pos, new_tile))
+
+    def get_size(self):
+        return len(self._tiles[0]), len(self._tiles)
 
     def at_exit(self, pos):
         return pos in self.exit_pos
 
+    def get_level_data(self):
+        return '\n'.join(reversed([''.join(x) for x in self._data]))
+
+    def _get_wall_tile(self, pos):
+        # Is the neighbour in this direction also a wall?
+        x, y = pos
+        left = right = top = bottom = False
+        if x == 0:
+            left = True
+        elif self._data[y][x - 1] == WALL:
+            left = True
+        if x == len(self._data[0]) - 1:
+            right = True
+        elif self._data[y][x + 1] == WALL:
+            right = True
+        if y == 0:
+            top = True
+        elif self._data[y - 1][x] == WALL:
+            top = True
+        if y == len(self._data) - 1:
+            bottom = True
+        elif self._data[y + 1][x] == WALL:
+            bottom = True
+        if top and bottom and left and right:
+            return load_image('tiles/cwall.png')
+        elif bottom and left and right:
+            return load_image('tiles/bottom_wall.png')
+        elif top and left and right:
+            return load_image('tiles/top_wall.png')
+        elif top and bottom and right:
+            return load_image('tiles/left_wall.png')
+        elif top and bottom and left:
+            return load_image('tiles/right_wall.png')
+        elif top and bottom:
+            return load_image('tiles/vert_wall.png')
+        elif left and right:
+            return load_image('tiles/horiz_wall.png')
+        elif left and top:
+            return load_image('tiles/corner_lt.png')
+        elif left and bottom:
+            return load_image('tiles/corner_lb.png')
+        elif right and top:
+            return load_image('tiles/corner_rt.png')
+        elif right and bottom:
+            return load_image('tiles/corner_rb.png')
+        elif top:
+            return load_image('tiles/end_top.png')
+        elif bottom:
+            return load_image('tiles/end_bottom.png')
+        elif left:
+            return load_image('tiles/end_right.png')
+        elif right:
+            return load_image('tiles/end_left.png')
+        return load_image('tiles/pillar.png')
+
+    def _in_limits(self, pos):
+        if pos[0] < 0:
+            return False
+        if pos[1] < 0:
+            return False
+        try:
+            self._data[pos[1]][pos[0]]
+        except IndexError:
+            return False
+        return True
+
     def blocked(self, pos):
         if pos[0] < 0:
             return True
         if pos[1] < 0:
             return True
         try:
-            tile = self.data[pos[1]][pos[0]]
+            tile = self._data[pos[1]][pos[0]]
         except IndexError:
             return True
-        if tile == '.':
+        if tile == WALL or tile == ENTRY:
             return True
+        if tile == GATE:
+            if self._gates[pos] > 0:
+                return True
         return False
+
+    def calc_dist(self, pos1, pos2):
+        return abs(pos1[0] - pos2[0]) + abs(pos1[1] - pos2[1])
+
+    def is_gate(self, pos):
+        if not self._in_limits(pos):
+            return False
+        return self._data[pos[1]][pos[0]] == GATE
+
+    def is_button(self, pos):
+        if not self._in_limits(pos):
+            return False
+        return self._data[pos[1]][pos[0]] == BUTTON
+
+    def is_wall(self, pos):
+        if not self._in_limits(pos):
+            return True
+        return self._data[pos[1]][pos[0]] == WALL
+
+    def trigger_button(self, pos):
+        if not self.is_button(pos):
+            return False
+        if not self._buttons[pos] == 'active':
+            return False
+        # Find the closest gate down gate and trigger it
+        mindist = 99999
+        gate_pos = None
+        for cand in self._gates:
+            dist = self.calc_dist(pos, cand)
+            if dist < mindist:
+                gate_pos = cand
+                mindist = dist
+        if gate_pos:
+            self._buttons[pos] = 'pressed'
+            self._gates[gate_pos] = 3  # Raise gate
+            self._fix_tile(pos)
+            self._fix_tile(gate_pos)
+
+    def damage_gate(self, pos):
+        if not self.is_gate(pos):
+            return
+        if self._gates[pos] == -1 or self._gates[pos] == 0:
+            return
+        self._gates[pos] = self._gates[pos] - 1
+        self._fix_tile(pos)
+
+    def get_changed_tiles(self):
+        ret = self._changed[:]
+        self._changed = []
+        return ret
+
+
+class LevelList(object):
+
+    LEVELS = 'level_list'
+
+    def __init__(self):
+        self._levels = []
+        self._level_names = []
+        self._errors = []
+        level_list = load(self.LEVELS)
+        for line in level_list:
+            line = line.strip()
+            if os.path.exists(filepath(line)):
+                level_file = load(line)
+                level = Level(level_file, line)
+                level_file.close()
+                try:
+                    level.validate()
+                    self._levels.append(level)
+                    self._level_names.append(line)
+                except RuntimeError as err:
+                    self._errors.append(
+                            'Invalid level %s in level_list: %s' % (line, err))
+            else:
+                self._errors.append(
+                    'Level list includes non-existant level %s' % line)
+        level_list.close()
+        self._cur_level = 0
+
+    def get_current_level(self):
+        if self._cur_level < len(self._levels):
+            return self._levels[self._cur_level]
+        else:
+            return None
+
+    def get_errors(self):
+        return self._errors
+
+    def get_level_names(self):
+        return self._level_names
+
+    def set_level_to(self, level_name):
+        if level_name in self._level_names:
+            self._cur_level = self._level_names.index(level_name)
+
+    def advance_to_next_level(self):
+        self._cur_level += 1
+        return self.get_current_level()
+
+    def reset(self):
+        self._cur_level = 0