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]}')