614 lines
21 KiB
Python
614 lines
21 KiB
Python
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]}')
|