added exercise 4
All checks were successful
Python unit tests / unittests (push) Successful in 5s

This commit is contained in:
Louis Heredero 2025-01-24 15:51:12 +01:00
parent c7c36f7717
commit 307c46d86a
Signed by: HEL
GPG Key ID: 8D83DE470F8544E7
2 changed files with 259 additions and 0 deletions

208
src/ex4_furniture.py Normal file
View File

@ -0,0 +1,208 @@
"""
Question 4 - Le beau, la bête et les meubles mouvants
But : calculer le nombre minimum de déplacements (et lesquels) pour passer d'un
agencement de meubles à un autre
Approche :
Nous pouvons représenter la situation par un graphe d'états, dans lequel chaque
nœud correspond à un agencement de meuble, et chaque arête à un déplacement.
Nous pouvons ensuite partir de la situation initiale et explore cet arbre
(par exemple en BFS) jusqu'à trouver la disposition finale. Afin d'optimiser un
peu le BFS naïf, nous pouvons trier les nœuds selon la somme des distances entre
la position actuelle de chaque meuble et sa position finale.
Par la nature du BFS, la première solution que nous trouverons sera la moins
profonde, c'est-à-dire celle comportant le moins de déplacements.
"""
from __future__ import annotations
from typing import Optional
OFFSETS = [
(0, -1),
(-1, 0),
(0, 1),
(1, 0),
]
class Furniture:
def __init__(self, id: int, pos: tuple[int, int], tiles: list[tuple[int, int]]):
self.id: int = id
self.pos: tuple[int, int] = pos
self.tiles: list[tuple[int, int]] = tiles
@staticmethod
def from_list(id: int, tiles: list[tuple[int, int]]) -> Furniture:
x = [tile[0] for tile in tiles]
y = [tile[1] for tile in tiles]
ox = min(x)
oy = min(y)
tiles2 = [(tile[0] - ox, tile[1] - oy) for tile in tiles]
return Furniture(id, (ox, oy), tiles2)
def move(self, offset: tuple[int, int]) -> Furniture:
return Furniture(self.id, (self.pos[0] + offset[0], self.pos[1] + offset[1]), self.tiles)
def copy(self) -> Furniture:
return Furniture(self.id, self.pos, self.tiles)
def get_tiles(self) -> list[tuple[int, int]]:
ox, oy = self.pos
return [
(ox + dx, oy + dy)
for dx, dy in self.tiles
]
def dist(self, f2: Furniture) -> int:
return abs(self.pos[0] - f2.pos[0]) + abs(self.pos[1] - f2.pos[1])
class State:
def __init__(
self,
#plan: list[list[int]],
width: int,
height: int,
furniture: dict[id, Furniture],
parent: Optional[State] = None
):
#self.plan: list[list[int]] = plan
self.furniture: dict[id, Furniture] = furniture
self.parent: Optional[State] = parent
#self.width: int = len(plan[0])
#self.height: int = len(plan)
self.width: int = width
self.height: int = height
@staticmethod
def from_list(plan: list[list[int]]) -> State:
furniture: dict[int, list[tuple[int, int]]] = {}
width = len(plan[0])
height = len(plan)
for y in range(height):
for x in range(width):
tile: int = plan[y][x]
if tile != 0:
if tile not in furniture:
furniture[tile] = []
furniture[tile].append((x, y))
furniture2: dict[int, Furniture] = {
i: Furniture.from_list(i, tiles)
for i, tiles in furniture.items()
}
#return State(plan, furniture2)
return State(width, height, furniture2)
def to_list(self) -> list[list[int]]:
plan: list[list[int]] = [
[0 for _ in range(self.width)]
for _ in range(self.height)
]
for furniture in self.furniture.values():
for tx, ty in furniture.get_tiles():
plan[ty][tx] = furniture.id
return plan
def get_depth(self) -> int:
if self.parent is None:
return 0
return self.parent.get_depth() + 1
def apply_move(self, id: int, offset: tuple[int, int]) -> Optional[State]:
#new_plan: list[list[int]] = [[0] * self.width for _ in range(self.height)]
"""
for y, row in enumerate(self.plan):
for x, tile in enumerate(row):
if tile == id:
x2, y2 = x + offset[0], y + offset[1]
if x2 < 0 or x2 >= self.width or y2 < 0 or y2 >= self.height:
return None
if new_plan[y2][x2] not in (0, id):
return None
new_plan[y2][x2] = id
new_plan[y][x] = 0
else:
new_plan[y][x] = tile
"""
plan: list[list[int]] = self.to_list()
furn2: Furniture = self.furniture[id].move(offset)
for tx, ty in furn2.get_tiles():
if tx < 0 or tx >= self.width or ty < 0 or ty >= self.height:
return None
if plan[ty][tx] not in (0, id):
return None
new_furn: dict[int, Furniture] = {
i: f.copy()
for i, f in self.furniture.items()
}
new_furn[id] = furn2
return State(self.width, self.height, new_furn, self)
def get_children(self) -> list[State]:
moves: list[tuple[int, tuple[int, int]]] = self.get_moves()
children: list[State] = [
self.apply_move(move[0], move[1])
for move in moves
]
children = list(filter(lambda child: child is not None, children))
return children
def get_moves(self) -> list[tuple[int, tuple[int, int]]]:
moves: list[tuple[int, tuple[int, int]]] = []
for id in self.furniture.keys():
for offset in OFFSETS:
moves.append((id, offset))
return moves
def matches(self, state2: State) -> bool:
for id in self.furniture.keys():
f1: Furniture = self.furniture[id]
f2: Furniture = state2.furniture[id]
if f1.pos != f2.pos:
return False
return True
def get_path_list(self) -> list[list[list[int]]]:
path = []
if self.parent is not None:
path = self.parent.get_path_list()
return path + [self.to_list()]
def get_dist(self, state2: State) -> int:
total_dist: int = 0
for id in self.furniture.keys():
f1: Furniture = self.furniture[id]
f2: Furniture = state2.furniture[id]
total_dist += f1.dist(f2)
return total_dist
def minimumMoves(current_plan: list[list[int]], target_plan: list[list[int]]) -> list[list[list[int]]]:
current_state = State.from_list(current_plan)
target_state = State.from_list(target_plan)
states: list[State] = [current_state]
while len(states) != 0:
new_states: list[State] = []
for state in states:
if state.matches(target_state):
return state.get_path_list()
children = state.get_children()
new_states += children
states = sorted(new_states, key=lambda s: s.get_dist(target_state))
print("Could not find a way to rearrange the room")
return []

51
tests/test_ex4.py Normal file
View File

@ -0,0 +1,51 @@
import unittest
from src.ex4_furniture import minimumMoves
class MyTestCase(unittest.TestCase):
def test_simple1(self):
current_plan = [
[0, 0, 0, 0],
[2, 0, 1, 0],
[0, 0, 1, 0]
]
target_plan = [
[0, 0, 2, 0],
[0, 1, 0, 0],
[0, 1, 0 ,0]
]
self.assertEqual(
minimumMoves(current_plan, target_plan),
[
[
[0, 0, 0, 0],
[2, 0, 1, 0],
[0, 0, 1, 0]
],
[
[2, 0, 0, 0],
[0, 0, 1, 0],
[0, 0, 1, 0]
],
[
[0, 2, 0, 0],
[0, 0, 1, 0],
[0, 0, 1, 0]
],
[
[0, 0, 2, 0],
[0, 0, 1, 0],
[0, 0, 1, 0]
],
[
[0, 0, 2, 0],
[0, 1, 0, 0],
[0, 1, 0, 0]
],
]
)
if __name__ == '__main__':
unittest.main()