First level solver!

This commit is contained in:
Alberto De Nardis 2026-10-13 00:50:51 +02:00
parent f44d7c7858
commit 84f1c9d548
Signed by: AlbYoda
GPG key ID: C6F60C5234D5FB62
2 changed files with 615 additions and 0 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
*pycache* *pycache*
*guy_originale.py* *guy_originale.py*
*venv*

View file

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