# puzzlegen.core.bfs_solver

View Source

      1import logging
      2from collections import deque
      3import math
      4from matplotlib import pyplot as plt
      5from matplotlib.patches import FancyArrowPatch
      6from tqdm import tqdm
      7from .utils import print_framed
      8import textwrap
      9logging.getLogger().setLevel(logging.INFO)
     10logger = logging.getLogger(__name__)
     11
     12class BFSSolver:
     13    """
     14    A solver that uses Breadth-First Search (BFS) to explore possible move sequences
     15    and determine whether a puzzle configuration can be solved.
     16
     17    This class delegates puzzle-specific logic (gravity, move execution, match detection)
     18    to external components provided via `grid_initializer` and `puzzle_logic`.
     19    """
     20    def __init__(self, grid_initializer, puzzle_logic):
     21        """
     22        Initialize the BFSSolver with a grid and puzzle logic instance.
     23
     24        Args:
     25
     26            grid_initializer (GridInitializer): Handles initial puzzle setup and parameters.
     27
     28            puzzle_logic (PuzzleLogic): Provides core puzzle mechanics (moves, gravity, match detection).
     29        """
     30        self.grid_initializer = grid_initializer
     31        self.puzzle_logic = puzzle_logic
     32
     33    def perform_all_blocks_moves(self, display_progress: bool = True):
     34        """
     35        Attempt to solve the puzzle using Breadth-First Search (BFS) within a limited number of moves.
     36
     37        This function explores all possible sequences of valid block moves (simple or exchange)
     38        using a BFS strategy to determine whether the current puzzle configuration can be solved
     39        in `min_nb_moves` steps or fewer.
     40
     41        It starts from the initial grid state and simulates possible actions at each round. Each node
     42        in the search tree tracks the history of block positions, moves performed, and puzzle states.
     43        The exploration stops when either a solution is found or the search depth exceeds the allowed limit.
     44
     45        Args:
     46
     47            display_progress (bool): Whether to show a progress bar during the search.
     48
     49        Returns:
     50
     51            tuple:
     52                - is_solvable (bool): True if a valid solution is found within the move limit, False otherwise.
     53                - solution (dict): A dictionary containing the move history and block states leading to the solution,
     54                                or an empty dict if no solution was found.
     55        """
     56        min_moves_to_solve = self.grid_initializer.min_nb_moves
     57        initial_set_blocks = self.grid_initializer.set_blocks.copy()
     58        grid_size = self.grid_initializer.grid_size
     59        colors = self.grid_initializer.colors
     60
     61        print_framed([
     62            f"Attempting to solve puzzle in ≤ {min_moves_to_solve} moves...",
     63            "Using Breadth-First Search (BFS) strategy."
     64        ])
     65
     66        queue = deque()
     67        current_round = 0
     68        initial_node = {
     69            "set_blocks_history": [initial_set_blocks],
     70            "moves_types_history": [],
     71            "moved_blocks_history" : [],
     72            "rounds_number_history": [1],
     73            "puzzle_states_history" : [1]
     74        }
     75        queue.append(initial_node)
     76
     77        processed = 0
     78        last_reported_round = 0
     79
     80        if display_progress:
     81            pbar = tqdm(total=min_moves_to_solve, desc="Solving Puzzle Progress")
     82        else:
     83            pbar = None
     84
     85        found_solution = False
     86        solution = {}
     87        while queue:
     88            current_item = queue.popleft()
     89            processed += 1
     90
     91            set_blocks = current_item["set_blocks_history"][-1].copy()
     92            current_round = current_item["rounds_number_history"][-1]
     93
     94            if current_round > min_moves_to_solve:
     95                break
     96
     97            if display_progress and current_round > last_reported_round:
     98                pbar.update(current_round - last_reported_round)
     99                last_reported_round = current_round
    100                pbar.set_postfix({
    101                    "Processed": processed,
    102                    "Queue Size": len(queue)
    103                })
    104
    105            puzzle_state = current_item["puzzle_states_history"][-1]
    106
    107            if puzzle_state == 1:
    108                is_solvable, solution = self.process_possible_moves(
    109                    set_blocks, current_item, current_round, grid_size, colors, queue
    110                )
    111                if is_solvable:
    112                    found_solution = True
    113                    break
    114
    115        if display_progress:
    116            if last_reported_round < min_moves_to_solve:
    117                pbar.update(min_moves_to_solve - last_reported_round)
    118            pbar.close()
    119
    120        if found_solution:
    121            return True, solution
    122
    123        logger.info(f"Did not find a solution in {current_round - 1} moves.")
    124        return False, {}
    125
    126    def process_possible_moves(self, set_blocks, current_item, current_round, grid_size, colors, queue):
    127        """
    128        Explore and enqueue all valid moves from the current puzzle state in the BFS process.
    129
    130        For each block in the current grid state, this method checks all possible move types
    131        (simple moves and exchanges to the left and right). For each valid move:
    132        - The move is applied to a copy of the current block set.
    133        - Gravity and match elimination are applied to the updated grid.
    134        - The new state is evaluated and stored in a new BFS node.
    135        - This new node is appended to the BFS queue for further exploration.
    136
    137        If a terminal game state is reached (e.g., state == 3), a solution is extracted and returned immediately.
    138
    139        Args:
    140
    141            set_blocks (dict): Dictionary of current block positions and Block objects.
    142
    143            current_item (dict): Current BFS node containing history of previous states and moves.
    144
    145            current_round (int): The current depth (round) in the BFS search tree.
    146
    147            grid_size (tuple): Tuple indicating grid dimensions (rows, columns).
    148
    149            colors (list): List of allowed block colors.
    150
    151            queue (deque): The BFS queue, to which new valid states will be appended.
    152
    153        Returns:
    154
    155            tuple:
    156                - is_solvable (bool): True if a solution is found in this branch, False otherwise.
    157                - solution (dict): Dictionary containing the history of moves and states leading to the solution,
    158                                or an empty dict if no solution is found at this level.
    159        """
    160        for position, block in set_blocks.items():
    161          possible_moves = self.puzzle_logic.check_all_available_moves(position, block.color, set_blocks, grid_size)
    162          for move_type in ['move_left', 'move_right', 'exchange_left', 'exchange_right']:
    163            updated_blocks = set_blocks.copy()
    164            if possible_moves[move_type]:
    165              updated_blocks = self.puzzle_logic.make_move(move_type, position, updated_blocks)
    166              updated_blocks = self.puzzle_logic.apply_gravity_and_eliminate_matches(updated_blocks, grid_size)
    167              state = self.puzzle_logic.get_game_state(updated_blocks, colors)
    168
    169              next_node = {
    170                        "set_blocks_history": current_item["set_blocks_history"] + [updated_blocks],
    171                        "moves_types_history": current_item["moves_types_history"] + [move_type],
    172                        "moved_blocks_history" : current_item["moved_blocks_history"] + [position],
    173                        "rounds_number_history": current_item["rounds_number_history"] + [current_round+1],
    174                        "puzzle_states_history" : current_item["puzzle_states_history"] + [state]
    175              }
    176              queue.append(next_node)
    177              if state == 3:
    178                solution = self.process_solution(next_node, current_round)
    179                return True, solution
    180        return False, {}
    181
    182    def process_solution(self, solved_puzzle_dict, current_round):
    183        """
    184        Finalize and return the solution after the puzzle is solved.
    185
    186        This function adds empty entries to the move history to mark the end,
    187        prints the solution path, and returns it.
    188
    189        Args:
    190
    191            solved_puzzle_dict (dict): Contains the steps that led to the solution.
    192
    193            current_round (int): Number of moves it took to solve the puzzle.
    194
    195        Returns:
    196            dict: The completed solution dictionary.
    197        """
    198        solved_puzzle_dict["moves_types_history"].append('')
    199        solved_puzzle_dict["moved_blocks_history"].append(())
    200
    201        print(f"Found a solution: {solved_puzzle_dict['set_blocks_history']} in {current_round} moves \n")
    202        return solved_puzzle_dict
    203
    204    @staticmethod
    205    def print_history(solved_puzzle_dict, grid_size, show=True, ax=None, max_cols=3):
    206        """
    207        Visualize the full sequence of moves that solved the puzzle.
    208
    209        This function creates a series of side-by-side grid plots, one for each step
    210        in the solution. It highlights moved blocks and shows arrows indicating moves.
    211        The initial and final grid states are labeled clearly.
    212
    213        Args:
    214
    215            solved_puzzle_dict (dict): Dictionary containing the history of grid states,
    216                                    moves, and block positions from the solver.
    217
    218            grid_size (tuple): Size of the puzzle grid (rows, columns).
    219
    220            show (bool): If True, the plot is displayed using matplotlib.
    221
    222            ax (matplotlib.axes.Axes, optional): Optional axis to plot on (useful for embedding).
    223
    224        Returns:
    225            None
    226        """
    227        set_blocks_history = solved_puzzle_dict["set_blocks_history"]
    228        moved_blocks_history = solved_puzzle_dict["moved_blocks_history"]
    229        moves_types_history = solved_puzzle_dict["moves_types_history"]
    230        rounds_number_history = solved_puzzle_dict["rounds_number_history"]
    231
    232        num_subplots = len(set_blocks_history)
    233        cols = min(max_cols, num_subplots)
    234        rows = math.ceil(num_subplots / cols)
    235
    236        if ax is None:
    237            fig_width = cols * 8
    238            fig_height = rows * 8
    239            fig, ax = plt.subplots(rows, cols, figsize=(fig_width, fig_height), squeeze=False)
    240
    241        for i in range(num_subplots):
    242            row_idx = i // cols
    243            col_idx = i % cols
    244
    245            set_blocks = set_blocks_history[i]
    246            move_type = moves_types_history[i]
    247            position = moved_blocks_history[i]
    248            round = rounds_number_history[i]
    249            title = ""
    250
    251            grid = [['white' for _ in range(grid_size[1])] for _ in range(grid_size[0])]
    252            for pos, block in set_blocks.items():
    253                r, c = pos
    254                grid[r][c] = block.color
    255
    256            for r in range(grid_size[0]):
    257                for c in range(grid_size[1]):
    258                    color = grid[r][c]
    259                    rect = plt.Rectangle((c, grid_size[0] - r - 1), 1, 1, facecolor=color, edgecolor='black')
    260                    ax[row_idx, col_idx].add_patch(rect)
    261
    262            if i == 0:
    263                title = f"..::SOLUTION::..: Solvable in {rounds_number_history[-2]} moves\nInitial State"
    264            if i != num_subplots - 1:
    265                title += f"\n(Move {round})\nPerform: {move_type} at position: {position}"
    266                ax[row_idx, col_idx], grid = BFSSolver.highlight_moved_block(position, ax[row_idx, col_idx], grid, grid_size)
    267                ax[row_idx, col_idx], grid = BFSSolver.draw_arrow(position, move_type, ax[row_idx, col_idx], grid, grid_size)
    268            else:
    269                title = "Final State"
    270
    271            wrapped_title = "\n".join(textwrap.wrap(title, width=40))
    272            ax[row_idx, col_idx].set_title(wrapped_title, loc='left')
    273
    274            ax[row_idx, col_idx].set_aspect('equal')
    275            ax[row_idx, col_idx].axis('off')
    276            ax[row_idx, col_idx].set_xlim(-0.5, grid_size[1] + 0.5)
    277            ax[row_idx, col_idx].set_ylim(-0.5, grid_size[0] + 0.5)
    278
    279        for idx in range(num_subplots, rows * cols):
    280            r = idx // cols
    281            c = idx % cols
    282            ax[r, c].axis('off')
    283
    284        plt.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=0.9, wspace=0.4, hspace=0.4)
    285
    286        if show:
    287            print("🖼️  Close the figure window to continue...")
    288            plt.show()
    289
    290    @staticmethod
    291    def highlight_moved_block(position, ax, grid, grid_size):
    292        """
    293        Visually highlight the block that was moved by drawing a lime-colored border around it.
    294
    295        Args:
    296
    297            position (tuple): (row, col) coordinates of the moved block.
    298
    299            ax (matplotlib.axes.Axes): The matplotlib axis on which to draw.
    300
    301            grid (list): 2D list representing the grid colors.
    302
    303            grid_size (tuple): Size of the grid (rows, columns).
    304
    305        Returns:
    306            tuple: (ax, grid) — The updated axis and grid, unchanged except for visualization.
    307        """
    308        arrow_pos_row = position[0]
    309        arrow_pos_col = position[1]
    310        color = grid[arrow_pos_row][arrow_pos_col]
    311        rect = plt.Rectangle((arrow_pos_col, grid_size[0] - arrow_pos_row - 1), 1, 1, facecolor=color, edgecolor='lime', linewidth=2)
    312        ax.add_patch(rect)
    313        return ax, grid
    314
    315    @staticmethod
    316    def draw_arrow(position, move_type, ax, grid, grid_size):
    317        """
    318        Draw an arrow on the plot to indicate the direction of a block’s move.
    319
    320        Args:
    321
    322            position (tuple): (row, col) coordinates of the moved block.
    323
    324            move_type (str): Type of move performed (e.g., 'move_left', 'exchange_right').
    325
    326            ax (matplotlib.axes.Axes): The matplotlib axis on which to draw the arrow.
    327
    328            grid (list): 2D list representing the grid colors.
    329
    330            grid_size (tuple): Size of the grid (rows, columns).
    331
    332        Returns:
    333            tuple: (ax, grid) — The updated axis and grid, with the arrow added for visualization.
    334        """
    335        arrow_pos_row = position[0]
    336        arrow_pos_col = position[1]
    337        linecol = "lime"
    338        arrow = BFSSolver.create_fancy_arrow_patch(arrow_pos_col, arrow_pos_row, move_type, linecol, grid_size)
    339        ax.add_patch(arrow)
    340        return ax, grid
    341
    342    @staticmethod
    343    def create_fancy_arrow_patch(arrow_pos_col, arrow_pos_row, move_type, linecol, grid_size):
    344        """
    345        Create a FancyArrowPatch for move visualization.
    346
    347        Args:
    348
    349            arrow_pos_col (int): Column of the arrow start.
    350
    351            arrow_pos_row (int): Row of the arrow start.
    352
    353            move_type (str): Type of move.
    354
    355            linecol (str): Color of the arrow.
    356
    357            grid_size (tuple): Size of the grid.
    358
    359        Returns:
    360            FancyArrowPatch: The arrow patch object.
    361        """
    362        if move_type == "move_left":
    363            arrow_start = (arrow_pos_col - 0.5, grid_size[0] - arrow_pos_row - 0.5)
    364            arrow_end = (arrow_pos_col + 0.5, grid_size[0] - arrow_pos_row - 0.5)
    365            arrow = FancyArrowPatch(arrow_start, arrow_end, arrowstyle='<-', mutation_scale=30, linewidth=2, color=linecol)
    366        elif move_type == "move_right":
    367            arrow_start = (arrow_pos_col + 0.5, grid_size[0] - arrow_pos_row - 0.5)
    368            arrow_end = (arrow_pos_col + 1.5, grid_size[0] - arrow_pos_row - 0.5)
    369            arrow = FancyArrowPatch(arrow_start, arrow_end, arrowstyle='->', mutation_scale=30, linewidth=2, color=linecol)
    370        elif move_type == "exchange_left":
    371            arrow_start = (arrow_pos_col - 0.5, grid_size[0] - arrow_pos_row - 0.5)
    372            arrow_end = (arrow_pos_col + 0.5, grid_size[0] - arrow_pos_row - 0.5)
    373            arrow = FancyArrowPatch(arrow_start, arrow_end, arrowstyle='<->', mutation_scale=30, linewidth=2, color=linecol)
    374        elif move_type == "exchange_right":
    375            arrow_start = (arrow_pos_col + 0.5, grid_size[0] - arrow_pos_row - 0.5)
    376            arrow_end = (arrow_pos_col + 1.5, grid_size[0] - arrow_pos_row - 0.5)
    377            arrow = FancyArrowPatch(arrow_start, arrow_end, arrowstyle='<->', mutation_scale=30, linewidth=2, color=linecol)
    378        return arrow

logger = <Logger puzzlegen.core.bfs_solver (INFO)>
class BFSSolver: View Source

     13class BFSSolver:
     14    """
     15    A solver that uses Breadth-First Search (BFS) to explore possible move sequences
     16    and determine whether a puzzle configuration can be solved.
     17
     18    This class delegates puzzle-specific logic (gravity, move execution, match detection)
     19    to external components provided via `grid_initializer` and `puzzle_logic`.
     20    """
     21    def __init__(self, grid_initializer, puzzle_logic):
     22        """
     23        Initialize the BFSSolver with a grid and puzzle logic instance.
     24
     25        Args:
     26
     27            grid_initializer (GridInitializer): Handles initial puzzle setup and parameters.
     28
     29            puzzle_logic (PuzzleLogic): Provides core puzzle mechanics (moves, gravity, match detection).
     30        """
     31        self.grid_initializer = grid_initializer
     32        self.puzzle_logic = puzzle_logic
     33
     34    def perform_all_blocks_moves(self, display_progress: bool = True):
     35        """
     36        Attempt to solve the puzzle using Breadth-First Search (BFS) within a limited number of moves.
     37
     38        This function explores all possible sequences of valid block moves (simple or exchange)
     39        using a BFS strategy to determine whether the current puzzle configuration can be solved
     40        in `min_nb_moves` steps or fewer.
     41
     42        It starts from the initial grid state and simulates possible actions at each round. Each node
     43        in the search tree tracks the history of block positions, moves performed, and puzzle states.
     44        The exploration stops when either a solution is found or the search depth exceeds the allowed limit.
     45
     46        Args:
     47
     48            display_progress (bool): Whether to show a progress bar during the search.
     49
     50        Returns:
     51
     52            tuple:
     53                - is_solvable (bool): True if a valid solution is found within the move limit, False otherwise.
     54                - solution (dict): A dictionary containing the move history and block states leading to the solution,
     55                                or an empty dict if no solution was found.
     56        """
     57        min_moves_to_solve = self.grid_initializer.min_nb_moves
     58        initial_set_blocks = self.grid_initializer.set_blocks.copy()
     59        grid_size = self.grid_initializer.grid_size
     60        colors = self.grid_initializer.colors
     61
     62        print_framed([
     63            f"Attempting to solve puzzle in ≤ {min_moves_to_solve} moves...",
     64            "Using Breadth-First Search (BFS) strategy."
     65        ])
     66
     67        queue = deque()
     68        current_round = 0
     69        initial_node = {
     70            "set_blocks_history": [initial_set_blocks],
     71            "moves_types_history": [],
     72            "moved_blocks_history" : [],
     73            "rounds_number_history": [1],
     74            "puzzle_states_history" : [1]
     75        }
     76        queue.append(initial_node)
     77
     78        processed = 0
     79        last_reported_round = 0
     80
     81        if display_progress:
     82            pbar = tqdm(total=min_moves_to_solve, desc="Solving Puzzle Progress")
     83        else:
     84            pbar = None
     85
     86        found_solution = False
     87        solution = {}
     88        while queue:
     89            current_item = queue.popleft()
     90            processed += 1
     91
     92            set_blocks = current_item["set_blocks_history"][-1].copy()
     93            current_round = current_item["rounds_number_history"][-1]
     94
     95            if current_round > min_moves_to_solve:
     96                break
     97
     98            if display_progress and current_round > last_reported_round:
     99                pbar.update(current_round - last_reported_round)
    100                last_reported_round = current_round
    101                pbar.set_postfix({
    102                    "Processed": processed,
    103                    "Queue Size": len(queue)
    104                })
    105
    106            puzzle_state = current_item["puzzle_states_history"][-1]
    107
    108            if puzzle_state == 1:
    109                is_solvable, solution = self.process_possible_moves(
    110                    set_blocks, current_item, current_round, grid_size, colors, queue
    111                )
    112                if is_solvable:
    113                    found_solution = True
    114                    break
    115
    116        if display_progress:
    117            if last_reported_round < min_moves_to_solve:
    118                pbar.update(min_moves_to_solve - last_reported_round)
    119            pbar.close()
    120
    121        if found_solution:
    122            return True, solution
    123
    124        logger.info(f"Did not find a solution in {current_round - 1} moves.")
    125        return False, {}
    126
    127    def process_possible_moves(self, set_blocks, current_item, current_round, grid_size, colors, queue):
    128        """
    129        Explore and enqueue all valid moves from the current puzzle state in the BFS process.
    130
    131        For each block in the current grid state, this method checks all possible move types
    132        (simple moves and exchanges to the left and right). For each valid move:
    133        - The move is applied to a copy of the current block set.
    134        - Gravity and match elimination are applied to the updated grid.
    135        - The new state is evaluated and stored in a new BFS node.
    136        - This new node is appended to the BFS queue for further exploration.
    137
    138        If a terminal game state is reached (e.g., state == 3), a solution is extracted and returned immediately.
    139
    140        Args:
    141
    142            set_blocks (dict): Dictionary of current block positions and Block objects.
    143
    144            current_item (dict): Current BFS node containing history of previous states and moves.
    145
    146            current_round (int): The current depth (round) in the BFS search tree.
    147
    148            grid_size (tuple): Tuple indicating grid dimensions (rows, columns).
    149
    150            colors (list): List of allowed block colors.
    151
    152            queue (deque): The BFS queue, to which new valid states will be appended.
    153
    154        Returns:
    155
    156            tuple:
    157                - is_solvable (bool): True if a solution is found in this branch, False otherwise.
    158                - solution (dict): Dictionary containing the history of moves and states leading to the solution,
    159                                or an empty dict if no solution is found at this level.
    160        """
    161        for position, block in set_blocks.items():
    162          possible_moves = self.puzzle_logic.check_all_available_moves(position, block.color, set_blocks, grid_size)
    163          for move_type in ['move_left', 'move_right', 'exchange_left', 'exchange_right']:
    164            updated_blocks = set_blocks.copy()
    165            if possible_moves[move_type]:
    166              updated_blocks = self.puzzle_logic.make_move(move_type, position, updated_blocks)
    167              updated_blocks = self.puzzle_logic.apply_gravity_and_eliminate_matches(updated_blocks, grid_size)
    168              state = self.puzzle_logic.get_game_state(updated_blocks, colors)
    169
    170              next_node = {
    171                        "set_blocks_history": current_item["set_blocks_history"] + [updated_blocks],
    172                        "moves_types_history": current_item["moves_types_history"] + [move_type],
    173                        "moved_blocks_history" : current_item["moved_blocks_history"] + [position],
    174                        "rounds_number_history": current_item["rounds_number_history"] + [current_round+1],
    175                        "puzzle_states_history" : current_item["puzzle_states_history"] + [state]
    176              }
    177              queue.append(next_node)
    178              if state == 3:
    179                solution = self.process_solution(next_node, current_round)
    180                return True, solution
    181        return False, {}
    182
    183    def process_solution(self, solved_puzzle_dict, current_round):
    184        """
    185        Finalize and return the solution after the puzzle is solved.
    186
    187        This function adds empty entries to the move history to mark the end,
    188        prints the solution path, and returns it.
    189
    190        Args:
    191
    192            solved_puzzle_dict (dict): Contains the steps that led to the solution.
    193
    194            current_round (int): Number of moves it took to solve the puzzle.
    195
    196        Returns:
    197            dict: The completed solution dictionary.
    198        """
    199        solved_puzzle_dict["moves_types_history"].append('')
    200        solved_puzzle_dict["moved_blocks_history"].append(())
    201
    202        print(f"Found a solution: {solved_puzzle_dict['set_blocks_history']} in {current_round} moves \n")
    203        return solved_puzzle_dict
    204
    205    @staticmethod
    206    def print_history(solved_puzzle_dict, grid_size, show=True, ax=None, max_cols=3):
    207        """
    208        Visualize the full sequence of moves that solved the puzzle.
    209
    210        This function creates a series of side-by-side grid plots, one for each step
    211        in the solution. It highlights moved blocks and shows arrows indicating moves.
    212        The initial and final grid states are labeled clearly.
    213
    214        Args:
    215
    216            solved_puzzle_dict (dict): Dictionary containing the history of grid states,
    217                                    moves, and block positions from the solver.
    218
    219            grid_size (tuple): Size of the puzzle grid (rows, columns).
    220
    221            show (bool): If True, the plot is displayed using matplotlib.
    222
    223            ax (matplotlib.axes.Axes, optional): Optional axis to plot on (useful for embedding).
    224
    225        Returns:
    226            None
    227        """
    228        set_blocks_history = solved_puzzle_dict["set_blocks_history"]
    229        moved_blocks_history = solved_puzzle_dict["moved_blocks_history"]
    230        moves_types_history = solved_puzzle_dict["moves_types_history"]
    231        rounds_number_history = solved_puzzle_dict["rounds_number_history"]
    232
    233        num_subplots = len(set_blocks_history)
    234        cols = min(max_cols, num_subplots)
    235        rows = math.ceil(num_subplots / cols)
    236
    237        if ax is None:
    238            fig_width = cols * 8
    239            fig_height = rows * 8
    240            fig, ax = plt.subplots(rows, cols, figsize=(fig_width, fig_height), squeeze=False)
    241
    242        for i in range(num_subplots):
    243            row_idx = i // cols
    244            col_idx = i % cols
    245
    246            set_blocks = set_blocks_history[i]
    247            move_type = moves_types_history[i]
    248            position = moved_blocks_history[i]
    249            round = rounds_number_history[i]
    250            title = ""
    251
    252            grid = [['white' for _ in range(grid_size[1])] for _ in range(grid_size[0])]
    253            for pos, block in set_blocks.items():
    254                r, c = pos
    255                grid[r][c] = block.color
    256
    257            for r in range(grid_size[0]):
    258                for c in range(grid_size[1]):
    259                    color = grid[r][c]
    260                    rect = plt.Rectangle((c, grid_size[0] - r - 1), 1, 1, facecolor=color, edgecolor='black')
    261                    ax[row_idx, col_idx].add_patch(rect)
    262
    263            if i == 0:
    264                title = f"..::SOLUTION::..: Solvable in {rounds_number_history[-2]} moves\nInitial State"
    265            if i != num_subplots - 1:
    266                title += f"\n(Move {round})\nPerform: {move_type} at position: {position}"
    267                ax[row_idx, col_idx], grid = BFSSolver.highlight_moved_block(position, ax[row_idx, col_idx], grid, grid_size)
    268                ax[row_idx, col_idx], grid = BFSSolver.draw_arrow(position, move_type, ax[row_idx, col_idx], grid, grid_size)
    269            else:
    270                title = "Final State"
    271
    272            wrapped_title = "\n".join(textwrap.wrap(title, width=40))
    273            ax[row_idx, col_idx].set_title(wrapped_title, loc='left')
    274
    275            ax[row_idx, col_idx].set_aspect('equal')
    276            ax[row_idx, col_idx].axis('off')
    277            ax[row_idx, col_idx].set_xlim(-0.5, grid_size[1] + 0.5)
    278            ax[row_idx, col_idx].set_ylim(-0.5, grid_size[0] + 0.5)
    279
    280        for idx in range(num_subplots, rows * cols):
    281            r = idx // cols
    282            c = idx % cols
    283            ax[r, c].axis('off')
    284
    285        plt.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=0.9, wspace=0.4, hspace=0.4)
    286
    287        if show:
    288            print("🖼️  Close the figure window to continue...")
    289            plt.show()
    290
    291    @staticmethod
    292    def highlight_moved_block(position, ax, grid, grid_size):
    293        """
    294        Visually highlight the block that was moved by drawing a lime-colored border around it.
    295
    296        Args:
    297
    298            position (tuple): (row, col) coordinates of the moved block.
    299
    300            ax (matplotlib.axes.Axes): The matplotlib axis on which to draw.
    301
    302            grid (list): 2D list representing the grid colors.
    303
    304            grid_size (tuple): Size of the grid (rows, columns).
    305
    306        Returns:
    307            tuple: (ax, grid) — The updated axis and grid, unchanged except for visualization.
    308        """
    309        arrow_pos_row = position[0]
    310        arrow_pos_col = position[1]
    311        color = grid[arrow_pos_row][arrow_pos_col]
    312        rect = plt.Rectangle((arrow_pos_col, grid_size[0] - arrow_pos_row - 1), 1, 1, facecolor=color, edgecolor='lime', linewidth=2)
    313        ax.add_patch(rect)
    314        return ax, grid
    315
    316    @staticmethod
    317    def draw_arrow(position, move_type, ax, grid, grid_size):
    318        """
    319        Draw an arrow on the plot to indicate the direction of a block’s move.
    320
    321        Args:
    322
    323            position (tuple): (row, col) coordinates of the moved block.
    324
    325            move_type (str): Type of move performed (e.g., 'move_left', 'exchange_right').
    326
    327            ax (matplotlib.axes.Axes): The matplotlib axis on which to draw the arrow.
    328
    329            grid (list): 2D list representing the grid colors.
    330
    331            grid_size (tuple): Size of the grid (rows, columns).
    332
    333        Returns:
    334            tuple: (ax, grid) — The updated axis and grid, with the arrow added for visualization.
    335        """
    336        arrow_pos_row = position[0]
    337        arrow_pos_col = position[1]
    338        linecol = "lime"
    339        arrow = BFSSolver.create_fancy_arrow_patch(arrow_pos_col, arrow_pos_row, move_type, linecol, grid_size)
    340        ax.add_patch(arrow)
    341        return ax, grid
    342
    343    @staticmethod
    344    def create_fancy_arrow_patch(arrow_pos_col, arrow_pos_row, move_type, linecol, grid_size):
    345        """
    346        Create a FancyArrowPatch for move visualization.
    347
    348        Args:
    349
    350            arrow_pos_col (int): Column of the arrow start.
    351
    352            arrow_pos_row (int): Row of the arrow start.
    353
    354            move_type (str): Type of move.
    355
    356            linecol (str): Color of the arrow.
    357
    358            grid_size (tuple): Size of the grid.
    359
    360        Returns:
    361            FancyArrowPatch: The arrow patch object.
    362        """
    363        if move_type == "move_left":
    364            arrow_start = (arrow_pos_col - 0.5, grid_size[0] - arrow_pos_row - 0.5)
    365            arrow_end = (arrow_pos_col + 0.5, grid_size[0] - arrow_pos_row - 0.5)
    366            arrow = FancyArrowPatch(arrow_start, arrow_end, arrowstyle='<-', mutation_scale=30, linewidth=2, color=linecol)
    367        elif move_type == "move_right":
    368            arrow_start = (arrow_pos_col + 0.5, grid_size[0] - arrow_pos_row - 0.5)
    369            arrow_end = (arrow_pos_col + 1.5, grid_size[0] - arrow_pos_row - 0.5)
    370            arrow = FancyArrowPatch(arrow_start, arrow_end, arrowstyle='->', mutation_scale=30, linewidth=2, color=linecol)
    371        elif move_type == "exchange_left":
    372            arrow_start = (arrow_pos_col - 0.5, grid_size[0] - arrow_pos_row - 0.5)
    373            arrow_end = (arrow_pos_col + 0.5, grid_size[0] - arrow_pos_row - 0.5)
    374            arrow = FancyArrowPatch(arrow_start, arrow_end, arrowstyle='<->', mutation_scale=30, linewidth=2, color=linecol)
    375        elif move_type == "exchange_right":
    376            arrow_start = (arrow_pos_col + 0.5, grid_size[0] - arrow_pos_row - 0.5)
    377            arrow_end = (arrow_pos_col + 1.5, grid_size[0] - arrow_pos_row - 0.5)
    378            arrow = FancyArrowPatch(arrow_start, arrow_end, arrowstyle='<->', mutation_scale=30, linewidth=2, color=linecol)
    379        return arrow

A solver that uses Breadth-First Search (BFS) to explore possible move sequences and determine whether a puzzle configuration can be solved.

This class delegates puzzle-specific logic (gravity, move execution, match detection) to external components provided via `grid_initializer` and `puzzle_logic`.

BFSSolver(grid_initializer, puzzle_logic) View Source

    21    def __init__(self, grid_initializer, puzzle_logic):
    22        """
    23        Initialize the BFSSolver with a grid and puzzle logic instance.
    24
    25        Args:
    26
    27            grid_initializer (GridInitializer): Handles initial puzzle setup and parameters.
    28
    29            puzzle_logic (PuzzleLogic): Provides core puzzle mechanics (moves, gravity, match detection).
    30        """
    31        self.grid_initializer = grid_initializer
    32        self.puzzle_logic = puzzle_logic

Initialize the BFSSolver with a grid and puzzle logic instance.

Args:

    grid_initializer (GridInitializer): Handles initial puzzle setup and parameters.

    puzzle_logic (PuzzleLogic): Provides core puzzle mechanics (moves, gravity, match detection).

grid_initializer
puzzle_logic
def perform_all_blocks_moves(self, display_progress: bool = True): View Source

     34    def perform_all_blocks_moves(self, display_progress: bool = True):
     35        """
     36        Attempt to solve the puzzle using Breadth-First Search (BFS) within a limited number of moves.
     37
     38        This function explores all possible sequences of valid block moves (simple or exchange)
     39        using a BFS strategy to determine whether the current puzzle configuration can be solved
     40        in `min_nb_moves` steps or fewer.
     41
     42        It starts from the initial grid state and simulates possible actions at each round. Each node
     43        in the search tree tracks the history of block positions, moves performed, and puzzle states.
     44        The exploration stops when either a solution is found or the search depth exceeds the allowed limit.
     45
     46        Args:
     47
     48            display_progress (bool): Whether to show a progress bar during the search.
     49
     50        Returns:
     51
     52            tuple:
     53                - is_solvable (bool): True if a valid solution is found within the move limit, False otherwise.
     54                - solution (dict): A dictionary containing the move history and block states leading to the solution,
     55                                or an empty dict if no solution was found.
     56        """
     57        min_moves_to_solve = self.grid_initializer.min_nb_moves
     58        initial_set_blocks = self.grid_initializer.set_blocks.copy()
     59        grid_size = self.grid_initializer.grid_size
     60        colors = self.grid_initializer.colors
     61
     62        print_framed([
     63            f"Attempting to solve puzzle in ≤ {min_moves_to_solve} moves...",
     64            "Using Breadth-First Search (BFS) strategy."
     65        ])
     66
     67        queue = deque()
     68        current_round = 0
     69        initial_node = {
     70            "set_blocks_history": [initial_set_blocks],
     71            "moves_types_history": [],
     72            "moved_blocks_history" : [],
     73            "rounds_number_history": [1],
     74            "puzzle_states_history" : [1]
     75        }
     76        queue.append(initial_node)
     77
     78        processed = 0
     79        last_reported_round = 0
     80
     81        if display_progress:
     82            pbar = tqdm(total=min_moves_to_solve, desc="Solving Puzzle Progress")
     83        else:
     84            pbar = None
     85
     86        found_solution = False
     87        solution = {}
     88        while queue:
     89            current_item = queue.popleft()
     90            processed += 1
     91
     92            set_blocks = current_item["set_blocks_history"][-1].copy()
     93            current_round = current_item["rounds_number_history"][-1]
     94
     95            if current_round > min_moves_to_solve:
     96                break
     97
     98            if display_progress and current_round > last_reported_round:
     99                pbar.update(current_round - last_reported_round)
    100                last_reported_round = current_round
    101                pbar.set_postfix({
    102                    "Processed": processed,
    103                    "Queue Size": len(queue)
    104                })
    105
    106            puzzle_state = current_item["puzzle_states_history"][-1]
    107
    108            if puzzle_state == 1:
    109                is_solvable, solution = self.process_possible_moves(
    110                    set_blocks, current_item, current_round, grid_size, colors, queue
    111                )
    112                if is_solvable:
    113                    found_solution = True
    114                    break
    115
    116        if display_progress:
    117            if last_reported_round < min_moves_to_solve:
    118                pbar.update(min_moves_to_solve - last_reported_round)
    119            pbar.close()
    120
    121        if found_solution:
    122            return True, solution
    123
    124        logger.info(f"Did not find a solution in {current_round - 1} moves.")
    125        return False, {}

Attempt to solve the puzzle using Breadth-First Search (BFS) within a limited number of moves.

This function explores all possible sequences of valid block moves (simple or exchange) using a BFS strategy to determine whether the current puzzle configuration can be solved in `min_nb_moves` steps or fewer.

It starts from the initial grid state and simulates possible actions at each round. Each node in the search tree tracks the history of block positions, moves performed, and puzzle states. The exploration stops when either a solution is found or the search depth exceeds the allowed limit.

Args:

    display_progress (bool): Whether to show a progress bar during the search.

Returns:

    tuple:
        - is_solvable (bool): True if a valid solution is found within the move limit, False otherwise.
        - solution (dict): A dictionary containing the move history and block states leading to the solution,
                        or an empty dict if no solution was found.

def process_possible_moves( self, set_blocks, current_item, current_round, grid_size, colors, queue): View Source

    127    def process_possible_moves(self, set_blocks, current_item, current_round, grid_size, colors, queue):
    128        """
    129        Explore and enqueue all valid moves from the current puzzle state in the BFS process.
    130
    131        For each block in the current grid state, this method checks all possible move types
    132        (simple moves and exchanges to the left and right). For each valid move:
    133        - The move is applied to a copy of the current block set.
    134        - Gravity and match elimination are applied to the updated grid.
    135        - The new state is evaluated and stored in a new BFS node.
    136        - This new node is appended to the BFS queue for further exploration.
    137
    138        If a terminal game state is reached (e.g., state == 3), a solution is extracted and returned immediately.
    139
    140        Args:
    141
    142            set_blocks (dict): Dictionary of current block positions and Block objects.
    143
    144            current_item (dict): Current BFS node containing history of previous states and moves.
    145
    146            current_round (int): The current depth (round) in the BFS search tree.
    147
    148            grid_size (tuple): Tuple indicating grid dimensions (rows, columns).
    149
    150            colors (list): List of allowed block colors.
    151
    152            queue (deque): The BFS queue, to which new valid states will be appended.
    153
    154        Returns:
    155
    156            tuple:
    157                - is_solvable (bool): True if a solution is found in this branch, False otherwise.
    158                - solution (dict): Dictionary containing the history of moves and states leading to the solution,
    159                                or an empty dict if no solution is found at this level.
    160        """
    161        for position, block in set_blocks.items():
    162          possible_moves = self.puzzle_logic.check_all_available_moves(position, block.color, set_blocks, grid_size)
    163          for move_type in ['move_left', 'move_right', 'exchange_left', 'exchange_right']:
    164            updated_blocks = set_blocks.copy()
    165            if possible_moves[move_type]:
    166              updated_blocks = self.puzzle_logic.make_move(move_type, position, updated_blocks)
    167              updated_blocks = self.puzzle_logic.apply_gravity_and_eliminate_matches(updated_blocks, grid_size)
    168              state = self.puzzle_logic.get_game_state(updated_blocks, colors)
    169
    170              next_node = {
    171                        "set_blocks_history": current_item["set_blocks_history"] + [updated_blocks],
    172                        "moves_types_history": current_item["moves_types_history"] + [move_type],
    173                        "moved_blocks_history" : current_item["moved_blocks_history"] + [position],
    174                        "rounds_number_history": current_item["rounds_number_history"] + [current_round+1],
    175                        "puzzle_states_history" : current_item["puzzle_states_history"] + [state]
    176              }
    177              queue.append(next_node)
    178              if state == 3:
    179                solution = self.process_solution(next_node, current_round)
    180                return True, solution
    181        return False, {}

Explore and enqueue all valid moves from the current puzzle state in the BFS process.

For each block in the current grid state, this method checks all possible move types (simple moves and exchanges to the left and right). For each valid move:

- The move is applied to a copy of the current block set.
- Gravity and match elimination are applied to the updated grid.
- The new state is evaluated and stored in a new BFS node.
- This new node is appended to the BFS queue for further exploration.

If a terminal game state is reached (e.g., state == 3), a solution is extracted and returned immediately.

Args:

    set_blocks (dict): Dictionary of current block positions and Block objects.

    current_item (dict): Current BFS node containing history of previous states and moves.

    current_round (int): The current depth (round) in the BFS search tree.

    grid_size (tuple): Tuple indicating grid dimensions (rows, columns).

    colors (list): List of allowed block colors.

    queue (deque): The BFS queue, to which new valid states will be appended.

Returns:

    tuple:
        - is_solvable (bool): True if a solution is found in this branch, False otherwise.
        - solution (dict): Dictionary containing the history of moves and states leading to the solution,
                        or an empty dict if no solution is found at this level.

def process_solution(self, solved_puzzle_dict, current_round): View Source

    183    def process_solution(self, solved_puzzle_dict, current_round):
    184        """
    185        Finalize and return the solution after the puzzle is solved.
    186
    187        This function adds empty entries to the move history to mark the end,
    188        prints the solution path, and returns it.
    189
    190        Args:
    191
    192            solved_puzzle_dict (dict): Contains the steps that led to the solution.
    193
    194            current_round (int): Number of moves it took to solve the puzzle.
    195
    196        Returns:
    197            dict: The completed solution dictionary.
    198        """
    199        solved_puzzle_dict["moves_types_history"].append('')
    200        solved_puzzle_dict["moved_blocks_history"].append(())
    201
    202        print(f"Found a solution: {solved_puzzle_dict['set_blocks_history']} in {current_round} moves \n")
    203        return solved_puzzle_dict

Finalize and return the solution after the puzzle is solved.

This function adds empty entries to the move history to mark the end, prints the solution path, and returns it.

Args:

    solved_puzzle_dict (dict): Contains the steps that led to the solution.

    current_round (int): Number of moves it took to solve the puzzle.

Returns: dict: The completed solution dictionary.

@staticmethod
def print_history(solved_puzzle_dict, grid_size, show=True, ax=None, max_cols=3): View Source

    205    @staticmethod
    206    def print_history(solved_puzzle_dict, grid_size, show=True, ax=None, max_cols=3):
    207        """
    208        Visualize the full sequence of moves that solved the puzzle.
    209
    210        This function creates a series of side-by-side grid plots, one for each step
    211        in the solution. It highlights moved blocks and shows arrows indicating moves.
    212        The initial and final grid states are labeled clearly.
    213
    214        Args:
    215
    216            solved_puzzle_dict (dict): Dictionary containing the history of grid states,
    217                                    moves, and block positions from the solver.
    218
    219            grid_size (tuple): Size of the puzzle grid (rows, columns).
    220
    221            show (bool): If True, the plot is displayed using matplotlib.
    222
    223            ax (matplotlib.axes.Axes, optional): Optional axis to plot on (useful for embedding).
    224
    225        Returns:
    226            None
    227        """
    228        set_blocks_history = solved_puzzle_dict["set_blocks_history"]
    229        moved_blocks_history = solved_puzzle_dict["moved_blocks_history"]
    230        moves_types_history = solved_puzzle_dict["moves_types_history"]
    231        rounds_number_history = solved_puzzle_dict["rounds_number_history"]
    232
    233        num_subplots = len(set_blocks_history)
    234        cols = min(max_cols, num_subplots)
    235        rows = math.ceil(num_subplots / cols)
    236
    237        if ax is None:
    238            fig_width = cols * 8
    239            fig_height = rows * 8
    240            fig, ax = plt.subplots(rows, cols, figsize=(fig_width, fig_height), squeeze=False)
    241
    242        for i in range(num_subplots):
    243            row_idx = i // cols
    244            col_idx = i % cols
    245
    246            set_blocks = set_blocks_history[i]
    247            move_type = moves_types_history[i]
    248            position = moved_blocks_history[i]
    249            round = rounds_number_history[i]
    250            title = ""
    251
    252            grid = [['white' for _ in range(grid_size[1])] for _ in range(grid_size[0])]
    253            for pos, block in set_blocks.items():
    254                r, c = pos
    255                grid[r][c] = block.color
    256
    257            for r in range(grid_size[0]):
    258                for c in range(grid_size[1]):
    259                    color = grid[r][c]
    260                    rect = plt.Rectangle((c, grid_size[0] - r - 1), 1, 1, facecolor=color, edgecolor='black')
    261                    ax[row_idx, col_idx].add_patch(rect)
    262
    263            if i == 0:
    264                title = f"..::SOLUTION::..: Solvable in {rounds_number_history[-2]} moves\nInitial State"
    265            if i != num_subplots - 1:
    266                title += f"\n(Move {round})\nPerform: {move_type} at position: {position}"
    267                ax[row_idx, col_idx], grid = BFSSolver.highlight_moved_block(position, ax[row_idx, col_idx], grid, grid_size)
    268                ax[row_idx, col_idx], grid = BFSSolver.draw_arrow(position, move_type, ax[row_idx, col_idx], grid, grid_size)
    269            else:
    270                title = "Final State"
    271
    272            wrapped_title = "\n".join(textwrap.wrap(title, width=40))
    273            ax[row_idx, col_idx].set_title(wrapped_title, loc='left')
    274
    275            ax[row_idx, col_idx].set_aspect('equal')
    276            ax[row_idx, col_idx].axis('off')
    277            ax[row_idx, col_idx].set_xlim(-0.5, grid_size[1] + 0.5)
    278            ax[row_idx, col_idx].set_ylim(-0.5, grid_size[0] + 0.5)
    279
    280        for idx in range(num_subplots, rows * cols):
    281            r = idx // cols
    282            c = idx % cols
    283            ax[r, c].axis('off')
    284
    285        plt.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=0.9, wspace=0.4, hspace=0.4)
    286
    287        if show:
    288            print("🖼️  Close the figure window to continue...")
    289            plt.show()

Visualize the full sequence of moves that solved the puzzle.

This function creates a series of side-by-side grid plots, one for each step in the solution. It highlights moved blocks and shows arrows indicating moves. The initial and final grid states are labeled clearly.

Args:

    solved_puzzle_dict (dict): Dictionary containing the history of grid states,
                            moves, and block positions from the solver.

    grid_size (tuple): Size of the puzzle grid (rows, columns).

    show (bool): If True, the plot is displayed using matplotlib.

    ax (matplotlib.axes.Axes, optional): Optional axis to plot on (useful for embedding).

Returns: None

@staticmethod
def highlight_moved_block(position, ax, grid, grid_size): View Source

    291    @staticmethod
    292    def highlight_moved_block(position, ax, grid, grid_size):
    293        """
    294        Visually highlight the block that was moved by drawing a lime-colored border around it.
    295
    296        Args:
    297
    298            position (tuple): (row, col) coordinates of the moved block.
    299
    300            ax (matplotlib.axes.Axes): The matplotlib axis on which to draw.
    301
    302            grid (list): 2D list representing the grid colors.
    303
    304            grid_size (tuple): Size of the grid (rows, columns).
    305
    306        Returns:
    307            tuple: (ax, grid) — The updated axis and grid, unchanged except for visualization.
    308        """
    309        arrow_pos_row = position[0]
    310        arrow_pos_col = position[1]
    311        color = grid[arrow_pos_row][arrow_pos_col]
    312        rect = plt.Rectangle((arrow_pos_col, grid_size[0] - arrow_pos_row - 1), 1, 1, facecolor=color, edgecolor='lime', linewidth=2)
    313        ax.add_patch(rect)
    314        return ax, grid

Visually highlight the block that was moved by drawing a lime-colored border around it.

Args:

    position (tuple): (row, col) coordinates of the moved block.

    ax (matplotlib.axes.Axes): The matplotlib axis on which to draw.

    grid (list): 2D list representing the grid colors.

    grid_size (tuple): Size of the grid (rows, columns).

Returns: tuple: (ax, grid) — The updated axis and grid, unchanged except for visualization.

@staticmethod
def draw_arrow(position, move_type, ax, grid, grid_size): View Source

    316    @staticmethod
    317    def draw_arrow(position, move_type, ax, grid, grid_size):
    318        """
    319        Draw an arrow on the plot to indicate the direction of a block’s move.
    320
    321        Args:
    322
    323            position (tuple): (row, col) coordinates of the moved block.
    324
    325            move_type (str): Type of move performed (e.g., 'move_left', 'exchange_right').
    326
    327            ax (matplotlib.axes.Axes): The matplotlib axis on which to draw the arrow.
    328
    329            grid (list): 2D list representing the grid colors.
    330
    331            grid_size (tuple): Size of the grid (rows, columns).
    332
    333        Returns:
    334            tuple: (ax, grid) — The updated axis and grid, with the arrow added for visualization.
    335        """
    336        arrow_pos_row = position[0]
    337        arrow_pos_col = position[1]
    338        linecol = "lime"
    339        arrow = BFSSolver.create_fancy_arrow_patch(arrow_pos_col, arrow_pos_row, move_type, linecol, grid_size)
    340        ax.add_patch(arrow)
    341        return ax, grid

Draw an arrow on the plot to indicate the direction of a block’s move.

Args:

    position (tuple): (row, col) coordinates of the moved block.

    move_type (str): Type of move performed (e.g., 'move_left', 'exchange_right').

    ax (matplotlib.axes.Axes): The matplotlib axis on which to draw the arrow.

    grid (list): 2D list representing the grid colors.

    grid_size (tuple): Size of the grid (rows, columns).

Returns: tuple: (ax, grid) — The updated axis and grid, with the arrow added for visualization.

@staticmethod
def create_fancy_arrow_patch(arrow_pos_col, arrow_pos_row, move_type, linecol, grid_size): View Source

    343    @staticmethod
    344    def create_fancy_arrow_patch(arrow_pos_col, arrow_pos_row, move_type, linecol, grid_size):
    345        """
    346        Create a FancyArrowPatch for move visualization.
    347
    348        Args:
    349
    350            arrow_pos_col (int): Column of the arrow start.
    351
    352            arrow_pos_row (int): Row of the arrow start.
    353
    354            move_type (str): Type of move.
    355
    356            linecol (str): Color of the arrow.
    357
    358            grid_size (tuple): Size of the grid.
    359
    360        Returns:
    361            FancyArrowPatch: The arrow patch object.
    362        """
    363        if move_type == "move_left":
    364            arrow_start = (arrow_pos_col - 0.5, grid_size[0] - arrow_pos_row - 0.5)
    365            arrow_end = (arrow_pos_col + 0.5, grid_size[0] - arrow_pos_row - 0.5)
    366            arrow = FancyArrowPatch(arrow_start, arrow_end, arrowstyle='<-', mutation_scale=30, linewidth=2, color=linecol)
    367        elif move_type == "move_right":
    368            arrow_start = (arrow_pos_col + 0.5, grid_size[0] - arrow_pos_row - 0.5)
    369            arrow_end = (arrow_pos_col + 1.5, grid_size[0] - arrow_pos_row - 0.5)
    370            arrow = FancyArrowPatch(arrow_start, arrow_end, arrowstyle='->', mutation_scale=30, linewidth=2, color=linecol)
    371        elif move_type == "exchange_left":
    372            arrow_start = (arrow_pos_col - 0.5, grid_size[0] - arrow_pos_row - 0.5)
    373            arrow_end = (arrow_pos_col + 0.5, grid_size[0] - arrow_pos_row - 0.5)
    374            arrow = FancyArrowPatch(arrow_start, arrow_end, arrowstyle='<->', mutation_scale=30, linewidth=2, color=linecol)
    375        elif move_type == "exchange_right":
    376            arrow_start = (arrow_pos_col + 0.5, grid_size[0] - arrow_pos_row - 0.5)
    377            arrow_end = (arrow_pos_col + 1.5, grid_size[0] - arrow_pos_row - 0.5)
    378            arrow = FancyArrowPatch(arrow_start, arrow_end, arrowstyle='<->', mutation_scale=30, linewidth=2, color=linecol)
    379        return arrow

Create a FancyArrowPatch for move visualization.

Args:

    arrow_pos_col (int): Column of the arrow start.

    arrow_pos_row (int): Row of the arrow start.

    move_type (str): Type of move.

    linecol (str): Color of the arrow.

    grid_size (tuple): Size of the grid.

Returns: FancyArrowPatch: The arrow patch object.