From 84f1c9d54877d6a5db319e2af4a37b1e7d8a24c2 Mon Sep 17 00:00:00 2001 From: AlbYoda <168586436+AlbYoda@users.noreply.github.com> Date: Tue, 13 Oct 2026 00:50:51 +0200 Subject: [PATCH] First level solver! --- .gitignore | 1 + level_generator/level_model.py | 614 +++++++++++++++++++++++++++++++++ 2 files changed, 615 insertions(+) create mode 100644 level_generator/level_model.py diff --git a/.gitignore b/.gitignore index 88de813..2bdb328 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *pycache* *guy_originale.py* +*venv* diff --git a/level_generator/level_model.py b/level_generator/level_model.py new file mode 100644 index 0000000..6dfc6a2 --- /dev/null +++ b/level_generator/level_model.py @@ -0,0 +1,614 @@ +import json +import numpy as np +import copy + +VERBOSE = 0 + +class MapElement: + + def __init__(self, x, y): + self._x = x + self._y = y + + def get_pos(self): + return self._x, self._y + + def __copy__(self): + return MapElement( + self._x, self._y + ) + + def __deepcopy__(self, memo): + return MapElement( + self._x, self._y + ) + + def __hash__(self): + return hash((self._x, self._y)) + + def __str__(self): + return f'{self.__class__.__name__}@{self._x, self._y}' + + def __repr__(self): + return self.__str__() + + +class Door(MapElement): + + def __init__(self, x, y, door_open=False, connecting={}): + super().__init__(x, y) + self._open = door_open + + # the areas that get in contact when the door is opened + # they are saved in a dictionary {area_index: position} + # where area index is the index of the connected area + # in the connected_areas array of the LevelData object + # and position is the first point of the new connected + # area that you land on if you go through the door + self._connecting = connecting + + def set_open(self, value): + self._open = value + + def is_open(self): + return self._open + + def add_connecting(self, idx, pos): + self._connecting[idx] = pos + + def get_connecting(self): + return self._connecting + + def is_connecting(self, idx1, idx2): + return idx1 in self._connecting and idx2 in self._connecting + + def __copy__(self): + return Door( + self._x, self._y, + self._open, self._connecting + ) + + def __deepcopy__(self, memo): + return Door( + self._x, self._y, + self._open, copy.deepcopy(self._connecting, memo) + ) + + def __hash__(self): + return hash((super().__hash__(), self._open)) + + def __str__(self): + if VERBOSE < 1: + return f'{self.__class__.__name__}@{self._x, self._y}' + elif VERBOSE == 1: + return f'{self.__class__.__name__}@{self._x, self._y}, status={"open" if self._open else "closed"}' + else: + return f'{self.__class__.__name__}@{self._x, self._y}, status={"open" if self._open else "closed"}, connecting areas {", ".join([f"{k}@{self._connecting[k]}" for k in self._connecting])}' + + def __repr__(self): + return self.__str__() + +class PressurePlate(MapElement): + + def __init__(self, x, y, pressed=False, door=None): + super().__init__(x, y) + self._pressed = pressed + self._door = door + + def set_pressed(self, value): + self._pressed = value + self.sync_door() + + def is_pressed(self, value): + return self._pressed + + def sync_door(self): + if self._door is not None: + self._door.set_open(self._pressed) + + def set_door(self, door): + self._door = door + + def get_door(self): + return self._door + + def __copy__(self): + return PressurePlate( + self._x, self._y, + self._pressed, self._door + ) + + def __deepcopy__(self, memo): + return PressurePlate( + self._x, self._y, + self._pressed, copy.deepcopy(self._door, memo) + ) + + def __hash__(self): + return hash((super().__hash__(), self._pressed, self._door)) + + def __str__(self): + if VERBOSE < 1: + return f'{self.__class__.__name__}@{self._x, self._y}' + else: + return f'{self.__class__.__name__}@{self._x, self._y}, status={"pressed" if self._pressed else "not pressed"}' + + def __repr__(self): + return self.__str__() + + +class MovableMapElement(MapElement): + + def set_pos(self, x = None, y = None): + out = False + if x is not None: + self._x = x + out = True + if y is not None: + self._y = y + out = True + return out + + def move(self, x, y): + self.set_pos(x, y) + # return minimum number of moves to get to new position, + # does not check for move to actually be allowed + # not used yet + return abs(self._x - x) + abs(self._y - y) + + def __copy__(self): + return MovableMapElement( + self._x, self._y + ) + + def __deepcopy__(self, memo): + return MovableMapElement( + self._x, self._y + ) + +class LockableMapElement(MovableMapElement): + + def __init__(self, x, y, locked=False): + super().__init__(x, y) + self._locked = locked + + def set_locked(self, locked): + self._locked = locked + + def is_locked(self): + return self._locked + + def set_pos(self, x = None, y = None): + if self._locked: + return False + else: + return super().set_pos(x, y) + + def move(self, x, y): + if not self.set_pos(x, y): + return -1 + else: + return abs(self._x - x) + abs(self._y - y) + + def __copy__(self): + return LockableMapElement( + self._x, self._y, self._locked + ) + + def __deepcopy__(self, memo): + return LockableMapElement( + self._x, self._y, self._locked + ) + + def __hash__(self): + return hash((super().__hash__(), self._locked)) + +class End(MapElement): + + def __init__(self, x, y, standing_on=False): + super().__init__(x, y) + self._standing_on = standing_on + + def set_standing_on(self, standing_on): + self._standing_on = standing_on + + def is_standing_on(self): + return self._standing_on + + def __copy__(self): + return End( + self._x, self._y, self._standing_on + ) + + def __deepcopy__(self, memo): + return End( + self._x, self._y, self._standing_on + ) + + def __hash__(self): + return hash((super().__hash__(), self._standing_on)) + +class Player(LockableMapElement): + + def __init__(self, x, y, locked=False, end=None): + super().__init__(x, y, locked) + self._end = end + + def set_end(self, end): + self._end = end + + def get_end(self): + return self._end + + def is_on_end(self): + if self._end is not None: + return self.get_pos() == self._end.get_pos() + else: + return False + + def __copy__(self): + return Player( + self._x, self._y, + self._locked, copy.copy(self._end) + ) + + def __deepcopy__(self, memo): + return Player( + self._x, self._y, + self._locked, copy.deepcopy(self._end, memo) + ) + + def __hash__(self): + return hash((super().__hash__(), self._end)) + + def my_hash(self): + return hash((self._locked, self._end)) + + +ALLOWED_PLATES = ('r', 't', 'o') +ALLOWED_DOORS = tuple(p.upper() for p in ALLOWED_PLATES) +ALLOWED_PLAYERS = ('b', 'g') +ALLOWED_ENDS = tuple(p.upper() for p in ALLOWED_PLAYERS) + +MOVABLE_THROUGH = tuple({'.'}.union(set(ALLOWED_PLATES + ALLOWED_PLAYERS + ALLOWED_ENDS))) +# print(MOVABLE_THROUGH) + +class LevelData: + def __init__(self, path=None, level_number=None, level_map=None, level_id=None, doors=None, plates=None, players=None, ends=None, connected_areas=None, connecting_map=None, move_path=[]): + # Saving the order number of the level + if path is not None: + self.level_number = int(path.name.split("_")[0]) + + # Level data file loading + with open(path, "r") as file: + level_data = json.load(file) + self._map = np.array(level_data["map"]) + self._id = level_data["id"] + + # # LOADING TILE IMAGES + # target_size = (TILE_SIZE, TILE_SIZE) + + self.doors, self.plates, self.players, self.ends = self.load_objs() + + self.connected_areas = self.load_all_connected_areas() + + self.connecting_map = self.build_connecting_map() + + self.connect_doors_to_areas() + else: + self.level_number = level_number + self._map = level_map + self._id = level_id + self.doors = doors + self.plates = plates + self.players = players + self.ends = ends + self.connected_areas = connected_areas + self.connecting_map = connecting_map + + self.move_path = move_path + + def is_winning(self): + return all(map(lambda p: self.players[p].is_on_end(), self.players)) + + # For now this method does this with the map, in the future they will be encoded differently in the json + # maybe already in arrays to make everything easier + def load_objs(self): + doors = {} + plates = {} + players = {} + ends = {} + for (row, col), character in np.ndenumerate(self._map): + if character in ALLOWED_DOORS: + doors[character] = Door(row, col) + if character.lower() in plates: + plates[character.lower()].set_door(doors[character]) + elif character in ALLOWED_PLATES: + plates[character] = PressurePlate(row, col) + if character.upper() in doors: + plates[character].set_door(doors[character.upper()]) + elif character in ALLOWED_PLAYERS: + players[character] = Player(row, col) + if character.upper() in ends: + players[character].set_end(ends[character.upper()]) + elif character in ALLOWED_ENDS: + ends[character] = End(row, col) + if character.lower() in players: + players[character.lower()].set_end(ends[character]) + + return doors, plates, players, ends + + def can_get_off_pressure_plate(self, player): + pos = player.get_pos() + plate = self.plates.get(self._map[pos[0]][pos[1]]) + if plate is None: + # there is no pressure plate + return True + else: + door = plate.get_door() + for p in self.players: + if self.players[p].get_pos() == door.get_pos(): + return False + return True + + def build_connecting_map(self): + out = self._map.copy() + for i, ca in enumerate(self.connected_areas): + connected_area = np.array(list(ca)) + # new_arr[connected[:, 0], connected[:, 1]] = 'c' + out[connected_area[:, 0], connected_area[:, 1]] = str(i) + return out + + def can_move_through(self, pos): + if self._map[pos[0]][pos[1]] in MOVABLE_THROUGH: + return False + else: + door = self.doors.get(self._map[pos[0]][pos[1]]) + if door is None: + return False + else: + return door.is_open() + + def get_adjacents(self, pos): + out = set() + if pos[0] < self._map.shape[0] - 1: + out.add((pos[0] + 1, pos[1])) + if pos[0] > 0: + out.add((pos[0] - 1, pos[1])) + if pos[1] < self._map.shape[1] - 1: + out.add((pos[0], pos[1] + 1)) + if pos[1] > 0: + out.add((pos[0], pos[1] - 1)) + return out + + def get_connected(self, initial_pos): + connected = {initial_pos} + # new_positions = set() + stop = False + while not stop: + stop = True + to_append = set() + for p in connected: + to_append.update(self.get_adjacents(p)) + if len(to_append) == 0: + stop = True + + for p in to_append: + if p in connected: + continue + elif self._map[p[0]][p[1]] in MOVABLE_THROUGH: + connected.add(p) + stop = False + else: + pass + return connected + + def position_already_analyzed(self, position, connected_areas): + for ca in connected_areas: + if position in ca: + return True + return False + + def load_all_connected_areas(self): + connected_areas = [] + for row in range(self._map.shape[0]): + for col in range(self._map.shape[1]): + if self._map[row][col] in MOVABLE_THROUGH: + if not self.position_already_analyzed((row, col), connected_areas): + connected_areas.append(self.get_connected((row, col))) + return connected_areas + + def connect_doors_to_areas(self): + for d in self.doors: + for p in self.get_adjacents(self.doors[d].get_pos()): + for (i, ca) in enumerate(self.connected_areas): + if p in ca: + self.doors[d].add_connecting(i, p) + + def get_possible_moves(self): + moves = set() + am_there = False + for player in self.players: + player_pos = self.players[player].get_pos() + player_connected_area_idx = next((i for i, ca in enumerate(self.connected_areas) if player_pos in ca), None) + player_connected_area = self.connected_areas[player_connected_area_idx] + # this should NEVER happen: player is not in a connected area + if player_connected_area is None: + print(f'FATAL ERROR: player at {player_pos}') + continue + + if self.players[player].is_locked(): + unlocking_move = next( + (pos for pos in self.get_adjacents(player_pos) if pos in player_connected_area), None + ) + # print(unlocking_move) + if unlocking_move is None: + pass + elif self._map[player_pos[0]][player_pos[1]] in ALLOWED_PLATES: + new_position = copy.deepcopy(self) + new_position.plates[self._map[player_pos[0]][player_pos[1]]].set_pressed(False) + new_position.players[player].set_locked(False) + new_position.players[player].set_pos(unlocking_move[0], unlocking_move[1]) + new_position.move_path.append({player: unlocking_move}) + moves.add(new_position) + elif self._map[player_pos[0]][player_pos[1]] in ALLOWED_ENDS: + new_position = copy.deepcopy(self) + new_position.ends[self._map[player_pos[0]][player_pos[1]]].set_standing_on(False) + new_position.players[player].set_locked(False) + new_position.players[player].set_pos(unlocking_move[0], unlocking_move[1]) + new_position.move_path.append({player: unlocking_move}) + moves.add(new_position) + + else: + adjacent_positions = set() + for connected_pos in player_connected_area: + adjacent_positions.update(self.get_adjacents(connected_pos)) + + # adjacent_positions -= player_connected_area + for adj_pos in adjacent_positions: + if self._map[adj_pos[0]][adj_pos[1]] == '#': + continue + overlapping = False + for pp in self.players: + if pp != player and self.players[pp].get_pos() == adj_pos: + overlapping = True + if overlapping: + continue + if self._map[adj_pos[0]][adj_pos[1]] in ALLOWED_PLATES: + new_position = copy.deepcopy(self) + new_position.plates[self._map[adj_pos[0]][adj_pos[1]]].set_pressed(True) + new_position.players[player].set_pos(adj_pos[0], adj_pos[1]) + new_position.players[player].set_locked(True) + new_position.move_path.append({player: adj_pos}) + moves.add(new_position) + elif self._map[adj_pos[0]][adj_pos[1]] in ALLOWED_DOORS: + cur_door = self.doors[self._map[adj_pos[0]][adj_pos[1]]] + if cur_door.is_open(): + for conn_area_idx in cur_door.get_connecting(): + if conn_area_idx != player_connected_area_idx: + new_position = copy.deepcopy(self) + new_player_pos = cur_door.get_connecting()[conn_area_idx] + new_position.players[player].set_pos(new_player_pos[0], new_player_pos[1]) + new_position.move_path.append({player: new_player_pos}) + moves.add(new_position) + + elif self._map[adj_pos[0]][adj_pos[1]] in ALLOWED_ENDS: + cur_end = self.players[player].get_end() + if cur_end.get_pos() == adj_pos: + new_position = copy.deepcopy(self) + new_position.ends[self._map[adj_pos[0]][adj_pos[1]]].set_standing_on(True) + new_position.players[player].set_pos(adj_pos[0], adj_pos[1]) + new_position.players[player].set_locked(True) + new_position.move_path.append({player: adj_pos}) + moves.add(new_position) + return moves + + def __copy__(self): + return LevelData( + path=None, level_number=self.level_number, level_map=self._map, + level_id=self._id, doors=self.doors, plates=self.plates, + players=self.players, ends=self.ends, + connected_areas=self.connected_areas, connecting_map=self.connecting_map, + move_path=self.move_path + ) + + def __deepcopy__(self, memo): + new_plates = {} + new_doors = {} + for k in self.plates: + new_plates[k] = copy.deepcopy(self.plates[k], memo) + new_doors[k.upper()] = new_plates[k].get_door() + + new_players = {} + new_ends = {} + for k in self.players: + new_players[k] = copy.deepcopy(self.players[k], memo) + new_ends[k.upper()] = new_players[k].get_end() + + return LevelData( + path=None, + level_number=self.level_number, + level_map=self._map, + level_id=self._id, + doors=new_doors, plates=new_plates, + players=new_players, ends=new_ends, + connected_areas=self.connected_areas, connecting_map=self.connecting_map, + move_path=copy.deepcopy(self.move_path, memo), + ) + + def __str__(self): + players_areas = {p: next((i for i, ca in enumerate(self.connected_areas) if self.players[p].get_pos() in ca), None) for p in self.players} + return f'{self._id}: ' + \ + f'doors: {', '.join(tuple(map(str, sorted(self.doors.items(), key=lambda d: d[0]))))}, ' + \ + f'players: {', '.join(tuple(map(str, sorted(self.players.items(), key=lambda p: p[0]))))}, ' + \ + f'plates: {', '.join(tuple(map(str, sorted(self.plates.items(), key=lambda p: p[0]))))}, ' + \ + f'player_areas: {', '.join(tuple(map(str, sorted(players_areas.items(), key=lambda pa: pa[0]))))}, ' + \ + f'ends: {', '.join(tuple(map(str, sorted(self.ends.items(), key=lambda e: e[0]))))}, ' + + def __hash__(self): + players_areas = {p: next((i for i, ca in enumerate(self.connected_areas) if self.players[p].get_pos() in ca), None) for p in self.players} + for player in players_areas: + if players_areas[player] is None: + print(f'Player {player} is in position {self.players[player].get_pos()}, which is in no connected area!') + exit(-1) + hashed_players = {} + for player in self.players: + hashed_players[player] = self.players[player].my_hash() + return hash(( + self._id, + tuple(sorted(self.doors.items(), key=lambda d: d[0])), + tuple(sorted(hashed_players.items(), key=lambda p: p[0])), + tuple(sorted(self.plates.items(), key=lambda p: p[0])), + tuple(sorted(players_areas.items(), key=lambda pa: pa[0])), + tuple(sorted(self.ends.items(), key=lambda e: e[0]))) + ) + + def __eq__(self, o): + if not isinstance(o, LevelData): + return False + else: + return hash(self) == hash(o) + + +# for execution only, to be removed +from pathlib import Path + +if __name__ == '__main__': + # level = LevelData(Path('levels/00_tutorial.json')) + level = LevelData(Path('levels/01_livello1.json')) + + to_explore = {level} + explored = set() + stop = False + winning_state = None + while not stop: + to_add = set() + for state in to_explore: + to_add.update(state.get_possible_moves()) + + to_add -= explored + + if len(to_add) == 0: + stop = True + + to_explore.update(to_add) + + for neighbour in to_explore: + explored.add(neighbour) + if neighbour.is_winning(): + if winning_state is None or len(winning_state.move_path) > len(neighbour.move_path): + winning_state = neighbour + stop = True + + if winning_state is not None: + print('Found winning strategy!') + for move in winning_state.move_path: + for k in move: + print(f'Player {k} to {move[k]}') + else: + print('No winning states!') + for i, state in enumerate(sorted(explored, key=lambda e: len(e.move_path))): + print(f'State {i}:') + for move in state.move_path: + for k in move: + print(f'Player {k} to {move[k]}')