diff --git a/iq_mini_4489/README.md b/iq_mini_4489/README.md new file mode 100644 index 0000000..004ff30 --- /dev/null +++ b/iq_mini_4489/README.md @@ -0,0 +1,105 @@ +

+ +

+ +## Description + + +### Game + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +It's a 5x5 grid of holes, connected as shown in the diagram. Each colored path contains one peg which can be moved around to any other square with the same color. The point of the game is to fit the 6 uniquely shaped pieces in the puzzle such that all of them fit (and subsequently fill up the entire matrix). + +### Pieces +Very similar to tetris: +![Tetris](https://static.vecteezy.com/system/resources/previews/009/102/301/original/set-of-colorful-blocks-for-tetris-game-illustration-vector.jpg) + + +The pieces are as follows: + +#### 3-in-a-row +Effectively 2 different ways to place it. It's just 3 consecutive squares. + +#### L shape +There are 4 different ways to place it. It's a 3-in-a-row with an extra square attached to one of the ends. +You can also use the third dimension and flip it, leading to 8 different ways to place it. + +#### T shape +There are 4 different ways to place it. It's a 3-in-a-row with an extra square attached to the middle. + +#### Square +1 way to place it. It's a 2x2 square. + +#### Smaller L shape +There are 4 different ways to place it. It's a 2-in-a-row with an extra square attached to one of the ends. + +#### 2 lines offset by 1 +4 different ways. + +## Optimization +- [ ] Bitmasking \ No newline at end of file diff --git a/iq_mini_4489/src/__pycache__/board.cpython-312.pyc b/iq_mini_4489/src/__pycache__/board.cpython-312.pyc new file mode 100644 index 0000000..00c46ef Binary files /dev/null and b/iq_mini_4489/src/__pycache__/board.cpython-312.pyc differ diff --git a/iq_mini_4489/src/__pycache__/pieces.cpython-312.pyc b/iq_mini_4489/src/__pycache__/pieces.cpython-312.pyc new file mode 100644 index 0000000..5b86030 Binary files /dev/null and b/iq_mini_4489/src/__pycache__/pieces.cpython-312.pyc differ diff --git a/iq_mini_4489/src/board.py b/iq_mini_4489/src/board.py new file mode 100644 index 0000000..1af2617 --- /dev/null +++ b/iq_mini_4489/src/board.py @@ -0,0 +1,77 @@ +import numpy as np +from typing import Tuple, List +import pieces +import colorama + + +def generate_orientations(piece: np.ndarray) -> List[np.ndarray]: + """ + Generate all unique orientations of a piece. + """ + orientations = [] + + # Rotations + current = piece.copy() + for _ in range(4): + current = np.rot90(current) + if not any(np.array_equal(current, o) for o in orientations): + orientations.append(current.copy()) + + # Flip + rotations + flipped = np.flip(piece, axis=0) + for _ in range(4): + flipped = np.rot90(flipped) + if not any(np.array_equal(flipped, o) for o in orientations): + orientations.append(flipped.copy()) + + return orientations + + +class Board: + def __init__(self, size: Tuple[int, int] = (5, 5)): + self.rows, self.cols = size + self.grid = np.zeros((self.rows, self.cols), dtype=int) + + def in_bounds(self, r: int, c: int) -> bool: + return 0 <= r < self.rows and 0 <= c < self.cols + + def placeable(self, shape: np.ndarray, top_left: Tuple[int,int]) -> bool: + """ + check if valid + """ + (r_offset, c_offset) = top_left + shape_rows, shape_cols = shape.shape + + for i in range(shape_rows): + for j in range(shape_cols): + if shape[i, j] == 1: + r = r_offset + i + c = c_offset + j + if not self.in_bounds(r, c) or self.grid[r, c] != 0: + return False + return True + + def place(self, shape: np.ndarray, top_left: Tuple[int,int], piece_id: int) -> None: + """ + place the piece + """ + (r_offset, c_offset) = top_left + shape_rows, shape_cols = shape.shape + for i in range(shape_rows): + for j in range(shape_cols): + if shape[i, j] == 1: + self.grid[r_offset + i, c_offset + j] = piece_id + + def remove(self, shape: np.ndarray, top_left: Tuple[int,int]) -> None: + (r_offset, c_offset) = top_left + shape_rows, shape_cols = shape.shape + for i in range(shape_rows): + for j in range(shape_cols): + if shape[i, j] == 1: + self.grid[r_offset + i, c_offset + j] = 0 + + def is_full(self) -> bool: + return np.all(self.grid != 0) + + def __str__(self): + return str(self.grid) diff --git a/iq_mini_4489/src/main.py b/iq_mini_4489/src/main.py new file mode 100644 index 0000000..dff506f --- /dev/null +++ b/iq_mini_4489/src/main.py @@ -0,0 +1,96 @@ +from board import Board, generate_orientations +from typing import Tuple, List +import pieces +import numpy as np +import colorama + +def backtrack(board: Board, pieces_list: List[str], piece_index: int, + orientations: dict, solutions: List[np.ndarray]) -> None: + """ + Backtracking to place each piece in the board in all possible ways. + """ + + # if full, check if solution + if piece_index == len(pieces_list): + if board.is_full(): + solutions.append(board.grid.copy()) + return + + piece_name = pieces_list[piece_index] + all_orientations = orientations[piece_name] + + for orientation in all_orientations: + rows, cols = orientation.shape + for row in range(board.rows - rows + 1): + for col in range(board.cols - cols + 1): + if board.placeable(orientation, (row, col)): + board.place(orientation, (row, col), piece_index + 1) + backtrack(board, pieces_list, piece_index + 1, orientations, solutions) + board.remove(orientation, (row, col)) + + +def colorful_solution(solution: np.ndarray) -> str: + """ + Return a colorful representation of the solution. + """ + rows, cols = solution.shape + result = "" + piece_id_to_color = { + -1: colorama.Back.BLACK, + 1: colorama.Back.GREEN, + 2: colorama.Back.BLUE, + 3: colorama.Back.YELLOW, + 4: colorama.Back.MAGENTA, + 5: colorama.Back.CYAN, + 6: colorama.Back.RED, + } + for r in range(rows): + for c in range(cols): + piece_id = solution[r, c] + if piece_id == 0: + result += colorama.Back.LIGHTWHITE_EX + " " + else: + result += piece_id_to_color[piece_id] + " " + + result += colorama.Back.RESET + result += "\n" + return result + +def solve(xPin: Tuple[int, int], yPin: Tuple[int, int], zPin: Tuple[int, int]): + """ + solve the puzzle, avoiding the pins at the specified positions. + + Args: + xPin (Tuple[int, int]): row, col of the 'x' pin + yPin (Tuple[int, int]): row, col of the 'y' pin + zPin (Tuple[int, int]): row, col of the 'z' pin + """ + + board = Board((5, 5)) + + pinned_positions = [xPin, yPin, zPin] + for (pr, pc) in pinned_positions: + board.grid[pr, pc] = -1 + + orientations_map = {} + for piece_name, piece_matrix in pieces.all_pieces.items(): + orientations_map[piece_name] = generate_orientations(piece_matrix) + piece_names = list(pieces.all_pieces.keys()) + solutions = [] + backtrack(board, piece_names, 0, orientations_map, solutions) + if solutions: + print(f"Found {len(solutions)} solution(s).") + for idx, sol in enumerate(solutions, start=1): + print(f"Solution #{idx}:") + print(colorful_solution(sol)) + print("------------------") + print(f"Total: {len(solutions)} solution{"s" if len(solutions) else ""}.") + return + + print("No solution found.") + + + + +if __name__ == "__main__": + solve((0, 2), (2, 0), (3, 3)) \ No newline at end of file diff --git a/iq_mini_4489/src/pieces.py b/iq_mini_4489/src/pieces.py new file mode 100644 index 0000000..cfa5d48 --- /dev/null +++ b/iq_mini_4489/src/pieces.py @@ -0,0 +1,99 @@ +import numpy as np + +three_in_a_row = np.array([ + [1, 1, 1] +]) + +l_shape = np.array([ + [1, 0], + [1, 0], + [1, 1] +]) + +t_shape = np.array([ + [1, 1, 1], + [0, 1, 0], +]) + +square = np.array([ + [1, 1], + [1, 1] +]) + +smaller_l_shape = np.array([ + [1, 0], + [1, 1] +]) + +tetris_z = np.array([ + [1, 1, 0], + [0, 1, 1] +]) + +all_pieces = { + "Consecutive 3s": three_in_a_row, + "L": l_shape, + "T": t_shape, + "Square": square, + "Smaller L": smaller_l_shape, + "Z": tetris_z +} + +def sanity_check(): + """Check all possible ways to position the pieces(including 3d rotation)""" + for piece_name, piece in all_pieces.items(): + all_placements = [] + + # Check original rotations + for _ in range(4): + piece = np.rot90(piece) + if piece.tolist() not in all_placements: + all_placements.append(piece.tolist()) + + # Check flipped rotations + flipped_piece = np.flip(piece, axis=0) + for _ in range(4): + flipped_piece = np.rot90(flipped_piece) + if flipped_piece.tolist() not in all_placements: + all_placements.append(flipped_piece.tolist()) + + print(f"Piece: {piece_name}") + print(f"Distinct Placements (including flips and rotations): {len(all_placements)}") + print("--------------------") + +def count_combinations_on_matrix(matrix_size): + """Count all possible combinations of pieces on a matrix of given size.""" + matrix = np.zeros(matrix_size, dtype=int) + total_combinations = 0 + + for piece_name, piece in all_pieces.items(): + piece_rows, piece_cols = piece.shape + placements = [] + + # Generate all distinct placements of the piece + for _ in range(4): + piece = np.rot90(piece) + if piece.tolist() not in placements: + placements.append(piece.tolist()) + + flipped_piece = np.flip(piece, axis=0) + for _ in range(4): + flipped_piece = np.rot90(flipped_piece) + if flipped_piece.tolist() not in placements: + placements.append(flipped_piece.tolist()) + + # Count valid placements on the matrix + for placement in placements: + rows, cols = len(placement), len(placement[0]) + for i in range(matrix_size[0] - rows + 1): + for j in range(matrix_size[1] - cols + 1): + sub_matrix = matrix[i:i + rows, j:j + cols] + if not sub_matrix.any(): # Check if the space is empty + total_combinations += 1 + + print(f"Total combinations on {matrix_size[0]}x{matrix_size[1]} matrix: {total_combinations}") + + +if __name__ == "__main__": + sanity_check() + count_combinations_on_matrix((5, 5)) \ No newline at end of file diff --git a/README.md b/othello/README.md similarity index 100% rename from README.md rename to othello/README.md diff --git a/src/game.py b/othello/src/game.py similarity index 100% rename from src/game.py rename to othello/src/game.py
xxoyy
xxxxy
oxzzy
zxzoy
zzzyy