From 307c46d86aa30d1571ebd8a9d995a41a83245c46 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Fri, 24 Jan 2025 15:51:12 +0100 Subject: [PATCH] added exercise 4 --- src/ex4_furniture.py | 208 +++++++++++++++++++++++++++++++++++++++++++ tests/test_ex4.py | 51 +++++++++++ 2 files changed, 259 insertions(+) create mode 100644 src/ex4_furniture.py create mode 100644 tests/test_ex4.py diff --git a/src/ex4_furniture.py b/src/ex4_furniture.py new file mode 100644 index 0000000..bd6e0ce --- /dev/null +++ b/src/ex4_furniture.py @@ -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 [] diff --git a/tests/test_ex4.py b/tests/test_ex4.py new file mode 100644 index 0000000..a29bf2a --- /dev/null +++ b/tests/test_ex4.py @@ -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()