# puzzlegen.core.grid_initializer

View Source

      1import math
      2import matplotlib.pyplot as plt
      3import matplotlib as mpl
      4import random
      5from matplotlib.patches import Rectangle
      6from matplotlib import colors
      7
      8from .utils import assign_blocks_per_color, sort_blocks_by_rows, print_framed
      9from .block import Block
     10
     11mpl.rcParams['figure.max_open_warning'] = 50
     12
     13class GridInitializer:
     14    """GridInitializer Class: This class is responsible for handling the setup of the game board, including generating the initial configuration of blocks.
     15    """
     16    def __init__(self, grid_size, nb_blocks, colors, min_nb_moves, stack_probability, blocks_gap):
     17        """
     18        Initialize the GridInitializer.
     19
     20        Args:
     21
     22            grid_size (tuple): Size of the grid as (rows, columns).
     23
     24            nb_blocks (int): Number of blocks to place.
     25
     26            colors (list): List of block colors.
     27
     28            min_nb_moves (int): Maximum number of moves allowed for the optimal solution.
     29
     30            stack_probability (float): Probability of stacking blocks.
     31
     32            blocks_gap (int, optional): Maximum allowed gap between blocks during generation (e.g., 1 means at most one empty cell). Default is 1.
     33        """
     34        self.grid_size = grid_size
     35        self.nb_blocks = nb_blocks
     36        self.colors = colors
     37        self.set_blocks = {}
     38        self.min_nb_moves = min_nb_moves
     39        self.stack_probability = stack_probability
     40        self.blocks_gap = blocks_gap
     41
     42    def is_valid_position(self, row, col):
     43        """
     44        Check if a position is within the grid bounds.
     45
     46        Args:
     47
     48            row (int): Row index.
     49
     50            col (int): Column index.
     51
     52        Returns:
     53            bool: True if position is valid, False otherwise.
     54        """
     55        if row < 0 or row >= self.grid_size[0] or col < 0 or col >= self.grid_size[1]:
     56            return False
     57        return True
     58
     59    def is_block_present(self, row, col):
     60        """
     61        Check if a block is present at the given position.
     62
     63        Args:
     64
     65            row (int): Row index.
     66
     67            col (int): Column index.
     68
     69        Returns:
     70            list: [True, color] if block is present, [False, None] otherwise.
     71        """
     72        if self.is_valid_position(row, col):
     73            if (row, col) in self.set_blocks:
     74                block = self.set_blocks[(row, col)]
     75                block_color = block.color
     76            else:
     77                block_color = None
     78            return [block_color is not None, block_color]
     79        return [False, None]
     80
     81    def is_similar_block(self, row, col, color):
     82        """
     83        Check if a block of the same color is present at the given position.
     84
     85        Args:
     86
     87            row (int): Row index.
     88
     89            col (int): Column index.
     90
     91            color (str): Color to check.
     92
     93        Returns:
     94            bool: True if a similar block is present, False otherwise.
     95        """
     96        block_present = self.is_block_present(row, col)
     97        if self.is_valid_position(row, col) and block_present[0] and block_present[1] == color:
     98            return True
     99
    100    def check_valid_placement(self, row, col, color):
    101        """
    102        Check if placing a block at the given position is valid according to the rules (no matches are allowed during initial setup).
    103
    104        Args:
    105
    106            row (int): Row index.
    107
    108            col (int): Column index.
    109
    110            color (str): Color of the block.
    111
    112        Returns:
    113            bool: True if placement is valid, False otherwise.
    114        """
    115        if self.is_similar_block(row + 1, col, color) and self.is_similar_block(row + 2, col, color):
    116            return False
    117        if self.is_similar_block(row, col-1, color) and self.is_similar_block(row, col+1, color):
    118            return False
    119        if self.is_similar_block(row, col-1, color) and self.is_similar_block(row, col-2, color):
    120            return False
    121        if self.is_similar_block(row, col+1, color) and self.is_similar_block(row, col+2, color):
    122            return False
    123        return True
    124
    125    def get_highest_positions(self):
    126        """
    127        Get the highest (topmost) block positions for each column.
    128
    129        Returns:
    130            list: List of (row, col) tuples representing the highest block in each column.
    131        """
    132        highest_positions = {}
    133        for position in self.set_blocks:
    134            row, col = position
    135            if col not in highest_positions or row < highest_positions[col][0]:
    136                highest_positions[col] = (row, col)
    137        highest_block_positions = list(highest_positions.values())
    138        return highest_block_positions
    139
    140    def select_random_highest_position(self):
    141        """
    142        Select a random position above the highest block in a random column or return an empty position with a gap.
    143
    144        Returns:
    145            tuple: (row, col) position above a random highest block, or an empty position with gap if none found.
    146        """
    147        highest_block_positions = self.get_highest_positions()
    148        highest_block_positions_filtered = [(row, col) for row, col in highest_block_positions if row != 0]
    149        if not highest_block_positions_filtered:
    150            return self.select_empty_positions_with_gap()
    151        random_position = random.choice(highest_block_positions_filtered)
    152        return (random_position[0]-1,random_position[1])
    153
    154    def get_empty_positions_with_gap(self):
    155        """
    156        Get empty positions in the bottomost row that respect the minimum gap between blocks.
    157
    158        Returns:
    159            list: List of (row, col) tuples for valid empty positions.
    160        """
    161        last_row = self.grid_size[0] - 1
    162        empty_positions_with_gap = []
    163        highest_positions = self.get_highest_positions()
    164
    165        for row, col in highest_positions:
    166            adjacent_cols = [col + self.blocks_gap, col + self.blocks_gap + 1, col - self.blocks_gap, col - self.blocks_gap - 1]
    167            adjacent_cols = [adj_col for adj_col in adjacent_cols if adj_col >= 0 and adj_col < self.grid_size[1]]
    168
    169            for adj_col in adjacent_cols:
    170                adjacent_position = (last_row, adj_col)
    171                if adjacent_position not in self.set_blocks:
    172                    empty_positions_with_gap.append(adjacent_position)
    173
    174        return empty_positions_with_gap
    175
    176    def select_empty_positions_with_gap(self):
    177        """
    178        Select a random empty position in the last row that respects the minimum gap.
    179
    180        Returns:
    181            tuple: (row, col) position.
    182        """
    183        empty_positions_with_gap = self.get_empty_positions_with_gap()
    184        if not empty_positions_with_gap:
    185            return self.select_random_highest_position()
    186        random_position = random.choice(empty_positions_with_gap)
    187        return random_position
    188
    189    def initialize_grid(self):
    190        """
    191        Initialize the grid with blocks according to the rules and constraints.
    192
    193        The method performs the following steps:
    194          - Determines the number of blocks to place per color using `assign_blocks_per_color`.
    195          - Creates a list of blocks to place, respecting the color distribution.
    196          - Iteratively places each block in the grid, either stacking on existing blocks or
    197            placing in empty positions that respect minimum spacing (gap) constraints.
    198          - The first block is placed near the center top of the grid.
    199          - For subsequent blocks, the placement is either stacked on top of existing blocks (with a
    200            probability defined by `stack_probability`) or placed in valid empty positions.
    201          - Each potential placement is validated by `check_valid_placement` to avoid rule violations (e.g., no immediate matches).
    202          - Once placed, blocks are recorded in `self.set_blocks` with their positions as keys.
    203          - Finally, `self.set_blocks` is sorted by row to maintain a consistent order.
    204
    205        This process ensures the initial puzzle grid respects the game's placement rules
    206        and sets up a valid starting configuration for the puzzle.
    207
    208        Returns:
    209            None
    210        """
    211        blocks_per_color = assign_blocks_per_color(self.nb_blocks, self.colors)
    212        blocks_to_place = [color for color, count in blocks_per_color for i in range(count)]
    213        while blocks_to_place:
    214            placed = False
    215            while not(placed):
    216                stacked = False
    217                if random.random() < self.stack_probability:
    218                    stacked = True
    219                random_block_index = random.randrange(len(blocks_to_place))
    220                random_block_color = blocks_to_place[random_block_index]
    221                if not self.set_blocks:
    222                    random_column_grid = math.floor(self.grid_size[1]/2)
    223                    selected_row = self.grid_size[0]-1
    224                elif not stacked:
    225                    random_position = self.select_empty_positions_with_gap()
    226                    random_column_grid = random_position[1]
    227                    selected_row = random_position[0]
    228                if stacked and self.set_blocks:
    229                    random_position = self.select_random_highest_position()
    230                    random_column_grid = random_position[1]
    231                    selected_row = random_position[0]
    232
    233                if self.check_valid_placement(selected_row, random_column_grid, random_block_color):
    234                    block = Block(random_block_color)
    235                    self.set_blocks[(selected_row, random_column_grid)] = block
    236                    placed = True
    237            blocks_to_place.pop(random_block_index)
    238        self.set_blocks = sort_blocks_by_rows(self.set_blocks)
    239
    240    def change_set_blocks(self, set_blocks):
    241        """
    242        Change the current set of blocks to a new configuration.
    243
    244        Args:
    245            set_blocks (dict): New set of blocks.
    246        """
    247        self.set_blocks = set_blocks
    248
    249    @staticmethod
    250    def init_graphical_grid(set_blocks, grid_size):
    251        """
    252        Create a graphical representation of the puzzle grid using matplotlib.
    253
    254        This method builds a 2D color grid from the set of placed blocks, then draws it using
    255        matplotlib by rendering each cell as a colored rectangle. The Y-axis is flipped so that
    256        row 0 appears at the bottom, consistent with a bottom-up grid orientation.
    257
    258        Args:
    259
    260            set_blocks (dict): Dictionary mapping (row, col) positions to Block objects. Each block
    261                                contains a `color` attribute used for rendering.
    262
    263            grid_size (tuple): A (rows, columns) tuple indicating the dimensions of the grid.
    264
    265        Returns:
    266
    267            tuple: (fig, ax, grid)
    268                - fig (matplotlib.figure.Figure): The matplotlib figure object.
    269                - ax (matplotlib.axes.Axes): The matplotlib axis with rendered grid.
    270                - grid (list of lists): 2D list of cell colors representing the grid state.
    271        """
    272        grid = [['white' for _ in range(grid_size[1])] for _ in range(grid_size[0])]
    273        for position, block in set_blocks.items():
    274            row, col = position
    275            grid[row][col] = block.color
    276
    277        fig, ax = plt.subplots()
    278        ax.axis('off')
    279        for row in range(grid_size[0]):
    280            for col in range(grid_size[1]):
    281                color = grid[row][col]
    282                rect = plt.Rectangle((col, grid_size[0] - row - 1), 1, 1, facecolor=color, edgecolor='black')
    283                ax.add_patch(rect)
    284
    285        return fig, ax, grid
    286
    287    def print_initial_grid(self):
    288        """
    289        Print and display the initial grid using matplotlib.
    290        """
    291        fig, ax, grid = GridInitializer.init_graphical_grid(self.set_blocks, self.grid_size)
    292        ax.set_aspect('equal')
    293        ax.set_xlim(-0.5, self.grid_size[1] + 0.5)
    294        ax.set_ylim(-0.5, self.grid_size[0] + 0.5)
    295        header_lines = [
    296            "🎲🎉 Here is your randomly generated puzzle! 🎉🎲",
    297            "──────────────────────────────────────────────"
    298        ]
    299
    300        print_framed(header_lines)
    301        print("🖼️  Close the figure window to continue...")
    302        plt.show()
    303
    304        print_framed([
    305            "🔹 Shorter representation of the puzzle:",
    306            str(self.set_blocks)
    307        ])

class GridInitializer: View Source

     14class GridInitializer:
     15    """GridInitializer Class: This class is responsible for handling the setup of the game board, including generating the initial configuration of blocks.
     16    """
     17    def __init__(self, grid_size, nb_blocks, colors, min_nb_moves, stack_probability, blocks_gap):
     18        """
     19        Initialize the GridInitializer.
     20
     21        Args:
     22
     23            grid_size (tuple): Size of the grid as (rows, columns).
     24
     25            nb_blocks (int): Number of blocks to place.
     26
     27            colors (list): List of block colors.
     28
     29            min_nb_moves (int): Maximum number of moves allowed for the optimal solution.
     30
     31            stack_probability (float): Probability of stacking blocks.
     32
     33            blocks_gap (int, optional): Maximum allowed gap between blocks during generation (e.g., 1 means at most one empty cell). Default is 1.
     34        """
     35        self.grid_size = grid_size
     36        self.nb_blocks = nb_blocks
     37        self.colors = colors
     38        self.set_blocks = {}
     39        self.min_nb_moves = min_nb_moves
     40        self.stack_probability = stack_probability
     41        self.blocks_gap = blocks_gap
     42
     43    def is_valid_position(self, row, col):
     44        """
     45        Check if a position is within the grid bounds.
     46
     47        Args:
     48
     49            row (int): Row index.
     50
     51            col (int): Column index.
     52
     53        Returns:
     54            bool: True if position is valid, False otherwise.
     55        """
     56        if row < 0 or row >= self.grid_size[0] or col < 0 or col >= self.grid_size[1]:
     57            return False
     58        return True
     59
     60    def is_block_present(self, row, col):
     61        """
     62        Check if a block is present at the given position.
     63
     64        Args:
     65
     66            row (int): Row index.
     67
     68            col (int): Column index.
     69
     70        Returns:
     71            list: [True, color] if block is present, [False, None] otherwise.
     72        """
     73        if self.is_valid_position(row, col):
     74            if (row, col) in self.set_blocks:
     75                block = self.set_blocks[(row, col)]
     76                block_color = block.color
     77            else:
     78                block_color = None
     79            return [block_color is not None, block_color]
     80        return [False, None]
     81
     82    def is_similar_block(self, row, col, color):
     83        """
     84        Check if a block of the same color is present at the given position.
     85
     86        Args:
     87
     88            row (int): Row index.
     89
     90            col (int): Column index.
     91
     92            color (str): Color to check.
     93
     94        Returns:
     95            bool: True if a similar block is present, False otherwise.
     96        """
     97        block_present = self.is_block_present(row, col)
     98        if self.is_valid_position(row, col) and block_present[0] and block_present[1] == color:
     99            return True
    100
    101    def check_valid_placement(self, row, col, color):
    102        """
    103        Check if placing a block at the given position is valid according to the rules (no matches are allowed during initial setup).
    104
    105        Args:
    106
    107            row (int): Row index.
    108
    109            col (int): Column index.
    110
    111            color (str): Color of the block.
    112
    113        Returns:
    114            bool: True if placement is valid, False otherwise.
    115        """
    116        if self.is_similar_block(row + 1, col, color) and self.is_similar_block(row + 2, col, color):
    117            return False
    118        if self.is_similar_block(row, col-1, color) and self.is_similar_block(row, col+1, color):
    119            return False
    120        if self.is_similar_block(row, col-1, color) and self.is_similar_block(row, col-2, color):
    121            return False
    122        if self.is_similar_block(row, col+1, color) and self.is_similar_block(row, col+2, color):
    123            return False
    124        return True
    125
    126    def get_highest_positions(self):
    127        """
    128        Get the highest (topmost) block positions for each column.
    129
    130        Returns:
    131            list: List of (row, col) tuples representing the highest block in each column.
    132        """
    133        highest_positions = {}
    134        for position in self.set_blocks:
    135            row, col = position
    136            if col not in highest_positions or row < highest_positions[col][0]:
    137                highest_positions[col] = (row, col)
    138        highest_block_positions = list(highest_positions.values())
    139        return highest_block_positions
    140
    141    def select_random_highest_position(self):
    142        """
    143        Select a random position above the highest block in a random column or return an empty position with a gap.
    144
    145        Returns:
    146            tuple: (row, col) position above a random highest block, or an empty position with gap if none found.
    147        """
    148        highest_block_positions = self.get_highest_positions()
    149        highest_block_positions_filtered = [(row, col) for row, col in highest_block_positions if row != 0]
    150        if not highest_block_positions_filtered:
    151            return self.select_empty_positions_with_gap()
    152        random_position = random.choice(highest_block_positions_filtered)
    153        return (random_position[0]-1,random_position[1])
    154
    155    def get_empty_positions_with_gap(self):
    156        """
    157        Get empty positions in the bottomost row that respect the minimum gap between blocks.
    158
    159        Returns:
    160            list: List of (row, col) tuples for valid empty positions.
    161        """
    162        last_row = self.grid_size[0] - 1
    163        empty_positions_with_gap = []
    164        highest_positions = self.get_highest_positions()
    165
    166        for row, col in highest_positions:
    167            adjacent_cols = [col + self.blocks_gap, col + self.blocks_gap + 1, col - self.blocks_gap, col - self.blocks_gap - 1]
    168            adjacent_cols = [adj_col for adj_col in adjacent_cols if adj_col >= 0 and adj_col < self.grid_size[1]]
    169
    170            for adj_col in adjacent_cols:
    171                adjacent_position = (last_row, adj_col)
    172                if adjacent_position not in self.set_blocks:
    173                    empty_positions_with_gap.append(adjacent_position)
    174
    175        return empty_positions_with_gap
    176
    177    def select_empty_positions_with_gap(self):
    178        """
    179        Select a random empty position in the last row that respects the minimum gap.
    180
    181        Returns:
    182            tuple: (row, col) position.
    183        """
    184        empty_positions_with_gap = self.get_empty_positions_with_gap()
    185        if not empty_positions_with_gap:
    186            return self.select_random_highest_position()
    187        random_position = random.choice(empty_positions_with_gap)
    188        return random_position
    189
    190    def initialize_grid(self):
    191        """
    192        Initialize the grid with blocks according to the rules and constraints.
    193
    194        The method performs the following steps:
    195          - Determines the number of blocks to place per color using `assign_blocks_per_color`.
    196          - Creates a list of blocks to place, respecting the color distribution.
    197          - Iteratively places each block in the grid, either stacking on existing blocks or
    198            placing in empty positions that respect minimum spacing (gap) constraints.
    199          - The first block is placed near the center top of the grid.
    200          - For subsequent blocks, the placement is either stacked on top of existing blocks (with a
    201            probability defined by `stack_probability`) or placed in valid empty positions.
    202          - Each potential placement is validated by `check_valid_placement` to avoid rule violations (e.g., no immediate matches).
    203          - Once placed, blocks are recorded in `self.set_blocks` with their positions as keys.
    204          - Finally, `self.set_blocks` is sorted by row to maintain a consistent order.
    205
    206        This process ensures the initial puzzle grid respects the game's placement rules
    207        and sets up a valid starting configuration for the puzzle.
    208
    209        Returns:
    210            None
    211        """
    212        blocks_per_color = assign_blocks_per_color(self.nb_blocks, self.colors)
    213        blocks_to_place = [color for color, count in blocks_per_color for i in range(count)]
    214        while blocks_to_place:
    215            placed = False
    216            while not(placed):
    217                stacked = False
    218                if random.random() < self.stack_probability:
    219                    stacked = True
    220                random_block_index = random.randrange(len(blocks_to_place))
    221                random_block_color = blocks_to_place[random_block_index]
    222                if not self.set_blocks:
    223                    random_column_grid = math.floor(self.grid_size[1]/2)
    224                    selected_row = self.grid_size[0]-1
    225                elif not stacked:
    226                    random_position = self.select_empty_positions_with_gap()
    227                    random_column_grid = random_position[1]
    228                    selected_row = random_position[0]
    229                if stacked and self.set_blocks:
    230                    random_position = self.select_random_highest_position()
    231                    random_column_grid = random_position[1]
    232                    selected_row = random_position[0]
    233
    234                if self.check_valid_placement(selected_row, random_column_grid, random_block_color):
    235                    block = Block(random_block_color)
    236                    self.set_blocks[(selected_row, random_column_grid)] = block
    237                    placed = True
    238            blocks_to_place.pop(random_block_index)
    239        self.set_blocks = sort_blocks_by_rows(self.set_blocks)
    240
    241    def change_set_blocks(self, set_blocks):
    242        """
    243        Change the current set of blocks to a new configuration.
    244
    245        Args:
    246            set_blocks (dict): New set of blocks.
    247        """
    248        self.set_blocks = set_blocks
    249
    250    @staticmethod
    251    def init_graphical_grid(set_blocks, grid_size):
    252        """
    253        Create a graphical representation of the puzzle grid using matplotlib.
    254
    255        This method builds a 2D color grid from the set of placed blocks, then draws it using
    256        matplotlib by rendering each cell as a colored rectangle. The Y-axis is flipped so that
    257        row 0 appears at the bottom, consistent with a bottom-up grid orientation.
    258
    259        Args:
    260
    261            set_blocks (dict): Dictionary mapping (row, col) positions to Block objects. Each block
    262                                contains a `color` attribute used for rendering.
    263
    264            grid_size (tuple): A (rows, columns) tuple indicating the dimensions of the grid.
    265
    266        Returns:
    267
    268            tuple: (fig, ax, grid)
    269                - fig (matplotlib.figure.Figure): The matplotlib figure object.
    270                - ax (matplotlib.axes.Axes): The matplotlib axis with rendered grid.
    271                - grid (list of lists): 2D list of cell colors representing the grid state.
    272        """
    273        grid = [['white' for _ in range(grid_size[1])] for _ in range(grid_size[0])]
    274        for position, block in set_blocks.items():
    275            row, col = position
    276            grid[row][col] = block.color
    277
    278        fig, ax = plt.subplots()
    279        ax.axis('off')
    280        for row in range(grid_size[0]):
    281            for col in range(grid_size[1]):
    282                color = grid[row][col]
    283                rect = plt.Rectangle((col, grid_size[0] - row - 1), 1, 1, facecolor=color, edgecolor='black')
    284                ax.add_patch(rect)
    285
    286        return fig, ax, grid
    287
    288    def print_initial_grid(self):
    289        """
    290        Print and display the initial grid using matplotlib.
    291        """
    292        fig, ax, grid = GridInitializer.init_graphical_grid(self.set_blocks, self.grid_size)
    293        ax.set_aspect('equal')
    294        ax.set_xlim(-0.5, self.grid_size[1] + 0.5)
    295        ax.set_ylim(-0.5, self.grid_size[0] + 0.5)
    296        header_lines = [
    297            "🎲🎉 Here is your randomly generated puzzle! 🎉🎲",
    298            "──────────────────────────────────────────────"
    299        ]
    300
    301        print_framed(header_lines)
    302        print("🖼️  Close the figure window to continue...")
    303        plt.show()
    304
    305        print_framed([
    306            "🔹 Shorter representation of the puzzle:",
    307            str(self.set_blocks)
    308        ])

GridInitializer Class: This class is responsible for handling the setup of the game board, including generating the initial configuration of blocks.

GridInitializer( grid_size, nb_blocks, colors, min_nb_moves, stack_probability, blocks_gap) View Source

    17    def __init__(self, grid_size, nb_blocks, colors, min_nb_moves, stack_probability, blocks_gap):
    18        """
    19        Initialize the GridInitializer.
    20
    21        Args:
    22
    23            grid_size (tuple): Size of the grid as (rows, columns).
    24
    25            nb_blocks (int): Number of blocks to place.
    26
    27            colors (list): List of block colors.
    28
    29            min_nb_moves (int): Maximum number of moves allowed for the optimal solution.
    30
    31            stack_probability (float): Probability of stacking blocks.
    32
    33            blocks_gap (int, optional): Maximum allowed gap between blocks during generation (e.g., 1 means at most one empty cell). Default is 1.
    34        """
    35        self.grid_size = grid_size
    36        self.nb_blocks = nb_blocks
    37        self.colors = colors
    38        self.set_blocks = {}
    39        self.min_nb_moves = min_nb_moves
    40        self.stack_probability = stack_probability
    41        self.blocks_gap = blocks_gap

Initialize the GridInitializer.

Args:

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

    nb_blocks (int): Number of blocks to place.

    colors (list): List of block colors.

    min_nb_moves (int): Maximum number of moves allowed for the optimal solution.

    stack_probability (float): Probability of stacking blocks.

    blocks_gap (int, optional): Maximum allowed gap between blocks during generation (e.g., 1 means at most one empty cell). Default is 1.

grid_size
nb_blocks
colors
set_blocks
min_nb_moves
stack_probability
blocks_gap
def is_valid_position(self, row, col): View Source

    43    def is_valid_position(self, row, col):
    44        """
    45        Check if a position is within the grid bounds.
    46
    47        Args:
    48
    49            row (int): Row index.
    50
    51            col (int): Column index.
    52
    53        Returns:
    54            bool: True if position is valid, False otherwise.
    55        """
    56        if row < 0 or row >= self.grid_size[0] or col < 0 or col >= self.grid_size[1]:
    57            return False
    58        return True

Check if a position is within the grid bounds.

Args:

    row (int): Row index.

    col (int): Column index.

Returns: bool: True if position is valid, False otherwise.

def is_block_present(self, row, col): View Source

    60    def is_block_present(self, row, col):
    61        """
    62        Check if a block is present at the given position.
    63
    64        Args:
    65
    66            row (int): Row index.
    67
    68            col (int): Column index.
    69
    70        Returns:
    71            list: [True, color] if block is present, [False, None] otherwise.
    72        """
    73        if self.is_valid_position(row, col):
    74            if (row, col) in self.set_blocks:
    75                block = self.set_blocks[(row, col)]
    76                block_color = block.color
    77            else:
    78                block_color = None
    79            return [block_color is not None, block_color]
    80        return [False, None]

Check if a block is present at the given position.

Args:

    row (int): Row index.

    col (int): Column index.

Returns: list: [True, color] if block is present, [False, None] otherwise.

def is_similar_block(self, row, col, color): View Source

    82    def is_similar_block(self, row, col, color):
    83        """
    84        Check if a block of the same color is present at the given position.
    85
    86        Args:
    87
    88            row (int): Row index.
    89
    90            col (int): Column index.
    91
    92            color (str): Color to check.
    93
    94        Returns:
    95            bool: True if a similar block is present, False otherwise.
    96        """
    97        block_present = self.is_block_present(row, col)
    98        if self.is_valid_position(row, col) and block_present[0] and block_present[1] == color:
    99            return True

Check if a block of the same color is present at the given position.

Args:

    row (int): Row index.

    col (int): Column index.

    color (str): Color to check.

Returns: bool: True if a similar block is present, False otherwise.

def check_valid_placement(self, row, col, color): View Source

    101    def check_valid_placement(self, row, col, color):
    102        """
    103        Check if placing a block at the given position is valid according to the rules (no matches are allowed during initial setup).
    104
    105        Args:
    106
    107            row (int): Row index.
    108
    109            col (int): Column index.
    110
    111            color (str): Color of the block.
    112
    113        Returns:
    114            bool: True if placement is valid, False otherwise.
    115        """
    116        if self.is_similar_block(row + 1, col, color) and self.is_similar_block(row + 2, col, color):
    117            return False
    118        if self.is_similar_block(row, col-1, color) and self.is_similar_block(row, col+1, color):
    119            return False
    120        if self.is_similar_block(row, col-1, color) and self.is_similar_block(row, col-2, color):
    121            return False
    122        if self.is_similar_block(row, col+1, color) and self.is_similar_block(row, col+2, color):
    123            return False
    124        return True

Check if placing a block at the given position is valid according to the rules (no matches are allowed during initial setup).

Args:

    row (int): Row index.

    col (int): Column index.

    color (str): Color of the block.

Returns: bool: True if placement is valid, False otherwise.

def get_highest_positions(self): View Source

    126    def get_highest_positions(self):
    127        """
    128        Get the highest (topmost) block positions for each column.
    129
    130        Returns:
    131            list: List of (row, col) tuples representing the highest block in each column.
    132        """
    133        highest_positions = {}
    134        for position in self.set_blocks:
    135            row, col = position
    136            if col not in highest_positions or row < highest_positions[col][0]:
    137                highest_positions[col] = (row, col)
    138        highest_block_positions = list(highest_positions.values())
    139        return highest_block_positions

Get the highest (topmost) block positions for each column.

Returns: list: List of (row, col) tuples representing the highest block in each column.

def select_random_highest_position(self): View Source

    141    def select_random_highest_position(self):
    142        """
    143        Select a random position above the highest block in a random column or return an empty position with a gap.
    144
    145        Returns:
    146            tuple: (row, col) position above a random highest block, or an empty position with gap if none found.
    147        """
    148        highest_block_positions = self.get_highest_positions()
    149        highest_block_positions_filtered = [(row, col) for row, col in highest_block_positions if row != 0]
    150        if not highest_block_positions_filtered:
    151            return self.select_empty_positions_with_gap()
    152        random_position = random.choice(highest_block_positions_filtered)
    153        return (random_position[0]-1,random_position[1])

Select a random position above the highest block in a random column or return an empty position with a gap.

Returns: tuple: (row, col) position above a random highest block, or an empty position with gap if none found.

def get_empty_positions_with_gap(self): View Source

    155    def get_empty_positions_with_gap(self):
    156        """
    157        Get empty positions in the bottomost row that respect the minimum gap between blocks.
    158
    159        Returns:
    160            list: List of (row, col) tuples for valid empty positions.
    161        """
    162        last_row = self.grid_size[0] - 1
    163        empty_positions_with_gap = []
    164        highest_positions = self.get_highest_positions()
    165
    166        for row, col in highest_positions:
    167            adjacent_cols = [col + self.blocks_gap, col + self.blocks_gap + 1, col - self.blocks_gap, col - self.blocks_gap - 1]
    168            adjacent_cols = [adj_col for adj_col in adjacent_cols if adj_col >= 0 and adj_col < self.grid_size[1]]
    169
    170            for adj_col in adjacent_cols:
    171                adjacent_position = (last_row, adj_col)
    172                if adjacent_position not in self.set_blocks:
    173                    empty_positions_with_gap.append(adjacent_position)
    174
    175        return empty_positions_with_gap

Get empty positions in the bottomost row that respect the minimum gap between blocks.

Returns: list: List of (row, col) tuples for valid empty positions.

def select_empty_positions_with_gap(self): View Source

    177    def select_empty_positions_with_gap(self):
    178        """
    179        Select a random empty position in the last row that respects the minimum gap.
    180
    181        Returns:
    182            tuple: (row, col) position.
    183        """
    184        empty_positions_with_gap = self.get_empty_positions_with_gap()
    185        if not empty_positions_with_gap:
    186            return self.select_random_highest_position()
    187        random_position = random.choice(empty_positions_with_gap)
    188        return random_position

Select a random empty position in the last row that respects the minimum gap.

Returns: tuple: (row, col) position.

def initialize_grid(self): View Source

    190    def initialize_grid(self):
    191        """
    192        Initialize the grid with blocks according to the rules and constraints.
    193
    194        The method performs the following steps:
    195          - Determines the number of blocks to place per color using `assign_blocks_per_color`.
    196          - Creates a list of blocks to place, respecting the color distribution.
    197          - Iteratively places each block in the grid, either stacking on existing blocks or
    198            placing in empty positions that respect minimum spacing (gap) constraints.
    199          - The first block is placed near the center top of the grid.
    200          - For subsequent blocks, the placement is either stacked on top of existing blocks (with a
    201            probability defined by `stack_probability`) or placed in valid empty positions.
    202          - Each potential placement is validated by `check_valid_placement` to avoid rule violations (e.g., no immediate matches).
    203          - Once placed, blocks are recorded in `self.set_blocks` with their positions as keys.
    204          - Finally, `self.set_blocks` is sorted by row to maintain a consistent order.
    205
    206        This process ensures the initial puzzle grid respects the game's placement rules
    207        and sets up a valid starting configuration for the puzzle.
    208
    209        Returns:
    210            None
    211        """
    212        blocks_per_color = assign_blocks_per_color(self.nb_blocks, self.colors)
    213        blocks_to_place = [color for color, count in blocks_per_color for i in range(count)]
    214        while blocks_to_place:
    215            placed = False
    216            while not(placed):
    217                stacked = False
    218                if random.random() < self.stack_probability:
    219                    stacked = True
    220                random_block_index = random.randrange(len(blocks_to_place))
    221                random_block_color = blocks_to_place[random_block_index]
    222                if not self.set_blocks:
    223                    random_column_grid = math.floor(self.grid_size[1]/2)
    224                    selected_row = self.grid_size[0]-1
    225                elif not stacked:
    226                    random_position = self.select_empty_positions_with_gap()
    227                    random_column_grid = random_position[1]
    228                    selected_row = random_position[0]
    229                if stacked and self.set_blocks:
    230                    random_position = self.select_random_highest_position()
    231                    random_column_grid = random_position[1]
    232                    selected_row = random_position[0]
    233
    234                if self.check_valid_placement(selected_row, random_column_grid, random_block_color):
    235                    block = Block(random_block_color)
    236                    self.set_blocks[(selected_row, random_column_grid)] = block
    237                    placed = True
    238            blocks_to_place.pop(random_block_index)
    239        self.set_blocks = sort_blocks_by_rows(self.set_blocks)

Initialize the grid with blocks according to the rules and constraints.

The method performs the following steps:

- Determines the number of blocks to place per color using `assign_blocks_per_color`.
- Creates a list of blocks to place, respecting the color distribution.
- Iteratively places each block in the grid, either stacking on existing blocks or placing in empty positions that respect minimum spacing (gap) constraints.
- The first block is placed near the center top of the grid.
- For subsequent blocks, the placement is either stacked on top of existing blocks (with a probability defined by `stack_probability`) or placed in valid empty positions.
- Each potential placement is validated by `check_valid_placement` to avoid rule violations (e.g., no immediate matches).
- Once placed, blocks are recorded in `self.set_blocks` with their positions as keys.
- Finally, `self.set_blocks` is sorted by row to maintain a consistent order.

This process ensures the initial puzzle grid respects the game's placement rules and sets up a valid starting configuration for the puzzle.

Returns: None

def change_set_blocks(self, set_blocks): View Source

    241    def change_set_blocks(self, set_blocks):
    242        """
    243        Change the current set of blocks to a new configuration.
    244
    245        Args:
    246            set_blocks (dict): New set of blocks.
    247        """
    248        self.set_blocks = set_blocks

Change the current set of blocks to a new configuration.

Args: set_blocks (dict): New set of blocks.

@staticmethod
def init_graphical_grid(set_blocks, grid_size): View Source

    250    @staticmethod
    251    def init_graphical_grid(set_blocks, grid_size):
    252        """
    253        Create a graphical representation of the puzzle grid using matplotlib.
    254
    255        This method builds a 2D color grid from the set of placed blocks, then draws it using
    256        matplotlib by rendering each cell as a colored rectangle. The Y-axis is flipped so that
    257        row 0 appears at the bottom, consistent with a bottom-up grid orientation.
    258
    259        Args:
    260
    261            set_blocks (dict): Dictionary mapping (row, col) positions to Block objects. Each block
    262                                contains a `color` attribute used for rendering.
    263
    264            grid_size (tuple): A (rows, columns) tuple indicating the dimensions of the grid.
    265
    266        Returns:
    267
    268            tuple: (fig, ax, grid)
    269                - fig (matplotlib.figure.Figure): The matplotlib figure object.
    270                - ax (matplotlib.axes.Axes): The matplotlib axis with rendered grid.
    271                - grid (list of lists): 2D list of cell colors representing the grid state.
    272        """
    273        grid = [['white' for _ in range(grid_size[1])] for _ in range(grid_size[0])]
    274        for position, block in set_blocks.items():
    275            row, col = position
    276            grid[row][col] = block.color
    277
    278        fig, ax = plt.subplots()
    279        ax.axis('off')
    280        for row in range(grid_size[0]):
    281            for col in range(grid_size[1]):
    282                color = grid[row][col]
    283                rect = plt.Rectangle((col, grid_size[0] - row - 1), 1, 1, facecolor=color, edgecolor='black')
    284                ax.add_patch(rect)
    285
    286        return fig, ax, grid

Create a graphical representation of the puzzle grid using matplotlib.

This method builds a 2D color grid from the set of placed blocks, then draws it using matplotlib by rendering each cell as a colored rectangle. The Y-axis is flipped so that row 0 appears at the bottom, consistent with a bottom-up grid orientation.

Args:

    set_blocks (dict): Dictionary mapping (row, col) positions to Block objects. Each block
                        contains a `color` attribute used for rendering.

    grid_size (tuple): A (rows, columns) tuple indicating the dimensions of the grid.

Returns:

    tuple: (fig, ax, grid)
        - fig (matplotlib.figure.Figure): The matplotlib figure object.
        - ax (matplotlib.axes.Axes): The matplotlib axis with rendered grid.
        - grid (list of lists): 2D list of cell colors representing the grid state.

def print_initial_grid(self): View Source

    288    def print_initial_grid(self):
    289        """
    290        Print and display the initial grid using matplotlib.
    291        """
    292        fig, ax, grid = GridInitializer.init_graphical_grid(self.set_blocks, self.grid_size)
    293        ax.set_aspect('equal')
    294        ax.set_xlim(-0.5, self.grid_size[1] + 0.5)
    295        ax.set_ylim(-0.5, self.grid_size[0] + 0.5)
    296        header_lines = [
    297            "🎲🎉 Here is your randomly generated puzzle! 🎉🎲",
    298            "──────────────────────────────────────────────"
    299        ]
    300
    301        print_framed(header_lines)
    302        print("🖼️  Close the figure window to continue...")
    303        plt.show()
    304
    305        print_framed([
    306            "🔹 Shorter representation of the puzzle:",
    307            str(self.set_blocks)
    308        ])

Print and display the initial grid using matplotlib.