# puzzlegen.core.puzzle_logic

View Source

      1from collections import Counter, deque
      2from .utils import sort_blocks_by_rows, sort_blocks_by_columns
      3class PuzzleLogic():
      4    """
      5    PuzzleLogic Class: Contains methods responsible for the game's core logic.
      6    This includes checking for block elimination, enforcing gravity rules (making blocks fall down after elimination),
      7    and performing player moves (e.g., swapping two adjacent blocks).
      8    """
      9    def __init__(self):
     10        """Initialize the PuzzleLogic class."""
     11        return
     12
     13    def check_all_available_moves(self, current_position, color, set_blocks, grid_size):
     14        """
     15        Check all possible moves for a block at the given position.
     16
     17        Args:
     18            current_position (tuple): (row, col) position of the block.
     19            color (str): Color of the block.
     20            set_blocks (dict): Current set of blocks.
     21            grid_size (tuple): Size of the grid.
     22
     23        Returns:
     24            dict: Dictionary with possible moves as keys and booleans as values.
     25        """
     26        possible_moves = {
     27            'move_left': self.can_move_left(current_position, set_blocks),
     28            'move_right': self.can_move_right(current_position, set_blocks, grid_size),
     29            'exchange_left': self.can_exchange_left(current_position, color, set_blocks),
     30            'exchange_right': self.can_exchange_right(current_position, color, set_blocks, grid_size)
     31        }
     32        return possible_moves
     33
     34    def can_move_left(self, position, set_blocks):
     35        """
     36        Check if a block can move left to an unoccupied space.
     37
     38        Args:
     39            position (tuple): (row, col) position of the block.
     40            set_blocks (dict): Current set of blocks.
     41
     42        Returns:
     43            bool: True if the block can move left, False otherwise.
     44        """
     45        row, col = position
     46        if col > 0:
     47            left_position = (row, col - 1)
     48            return left_position not in set_blocks
     49        return False
     50
     51    def can_move_right(self, position, set_blocks, grid_size):
     52        """
     53        Check if a block can move right to an unoccupied space.
     54
     55        Args:
     56            position (tuple): (row, col) position of the block.
     57            set_blocks (dict): Current set of blocks.
     58            grid_size (tuple): Size of the grid.
     59
     60        Returns:
     61            bool: True if the block can move right, False otherwise.
     62        """
     63        row, col = position
     64        if col < grid_size[1] - 1:
     65            right_position = (row, col + 1)
     66            return right_position not in set_blocks
     67        return False
     68
     69    def can_exchange_left(self, position, color, set_blocks):
     70        """
     71        Check if a block can exchange with the block to its left.
     72
     73        Args:
     74            position (tuple): (row, col) position of the block.
     75            color (str): Color of the block.
     76            set_blocks (dict): Current set of blocks.
     77
     78        Returns:
     79            bool: True if the block can exchange left, False otherwise.
     80        """
     81        row, col = position
     82        if col > 0:
     83            left_position = (row, col - 1)
     84            return left_position in set_blocks and color != set_blocks[left_position].color
     85        return False
     86
     87    def can_exchange_right(self, position, color, set_blocks, grid_size):
     88        """
     89        Check if a block can exchange with the block to its right.
     90
     91        Args:
     92            position (tuple): (row, col) position of the block.
     93            color (str): Color of the block.
     94            set_blocks (dict): Current set of blocks.
     95            grid_size (tuple): Size of the grid.
     96
     97        Returns:
     98            bool: True if the block can exchange right, False otherwise.
     99        """
    100        row, col = position
    101        if col < grid_size[1] - 1:
    102            right_position = (row, col + 1)
    103            return right_position in set_blocks and color != set_blocks[right_position].color
    104        return False
    105
    106    def update_block(self, old_position, new_position, set_blocks):
    107        """
    108        Update the position of a block in the set_blocks dictionary.
    109
    110        Args:
    111            old_position (tuple): Previous (row, col) position.
    112            new_position (tuple): New (row, col) position.
    113            set_blocks (dict): Current set of blocks.
    114
    115        Returns:
    116            dict: Updated set_blocks dictionary.
    117        """
    118        saved_block = set_blocks.pop(old_position)
    119        set_blocks[new_position] = saved_block
    120        set_blocks = sort_blocks_by_rows(set_blocks)
    121        return set_blocks
    122
    123    def exchange_block(self, first_position, second_position, set_blocks):
    124        """
    125        Exchange two blocks in the set_blocks dictionary.
    126
    127        Args:
    128            first_position (tuple): (row, col) of the first block.
    129            second_position (tuple): (row, col) of the second block.
    130            set_blocks (dict): Current set of blocks.
    131
    132        Returns:
    133            dict: Updated set_blocks dictionary.
    134        """
    135        block_1 = set_blocks[first_position]
    136        block_2 = set_blocks[second_position]
    137        set_blocks[first_position] = block_2
    138        set_blocks[second_position] = block_1
    139        set_blocks = sort_blocks_by_rows(set_blocks)
    140        return set_blocks
    141
    142    def has_matches(self, set_blocks):
    143        """
    144        Check if there are any matches (3 or more aligned blocks of the same color).
    145
    146        Args:
    147            set_blocks (dict): Current set of blocks.
    148
    149        Returns:
    150            bool: True if matches exist, False otherwise.
    151        """
    152        return bool(self.find_matches(set_blocks))
    153
    154    def find_matches(self, set_blocks):
    155        """
    156        Find all matches (3 or more aligned blocks of the same color).
    157
    158        Args:
    159            set_blocks (dict): Current set of blocks.
    160
    161        Returns:
    162            set: Set of positions where matches are found.
    163        """
    164        matches = set()
    165
    166        matches |= self.find_matches_by_rows(set_blocks)
    167        matches |= self.find_matches_by_columns(set_blocks)
    168
    169        return matches
    170
    171    def find_matches_by_rows(self, set_blocks):
    172        """
    173        Find all horizontal matches in the set_blocks.
    174
    175        Args:
    176            set_blocks (dict): Current set of blocks.
    177
    178        Returns:
    179            set: Set of positions where horizontal matches are found.
    180        """
    181        matches = set()
    182
    183        set_blocks_row = sort_blocks_by_rows(set_blocks)
    184        matches |= self.find_matches_in_iterator(set_blocks_row.items(), is_same_row=True)
    185
    186        return matches
    187
    188    def find_matches_by_columns(self, set_blocks):
    189        """
    190        Find all vertical matches in the set_blocks.
    191
    192        Args:
    193            set_blocks (dict): Current set of blocks.
    194
    195        Returns:
    196            set: Set of positions where vertical matches are found.
    197        """
    198        matches = set()
    199
    200        set_blocks_column = sort_blocks_by_columns(set_blocks)
    201        matches |= self.find_matches_in_iterator(set_blocks_column.items(), is_same_row=False)
    202
    203        return matches
    204
    205    def find_matches_in_iterator(self, set_blocks, is_same_row):
    206        """
    207        Helper function to find matches in an iterator (row or column).
    208
    209        Args:
    210            set_blocks (iterator): Iterator over set_blocks items.
    211            is_same_row (bool): True for row, False for column.
    212
    213        Returns:
    214            set: Set of positions where matches are found.
    215        """
    216        matches = set()
    217        tmp_matches = set()
    218        nb_aligned = 1
    219
    220        try:
    221            iterator = iter(set_blocks)
    222            (position, block) = next(iterator)
    223            while True:
    224                (next_position, next_block) = next(iterator)
    225                if self.is_aligned(position, next_position, block, next_block, is_same_row):
    226                    nb_aligned += 1
    227                    tmp_matches.add(position)
    228                    tmp_matches.add(next_position)
    229                else:
    230                    nb_aligned = 1
    231                    tmp_matches = set()
    232
    233                if nb_aligned >= 3:
    234                    matches |= tmp_matches
    235
    236                position, block = next_position, next_block
    237
    238        except StopIteration:
    239            pass
    240
    241        return matches
    242
    243    def delete_matches(self, set_blocks, matches):
    244        """
    245        Delete all matched blocks from the set_blocks.
    246
    247        Args:
    248            set_blocks (dict): Current set of blocks.
    249            matches (set): Set of positions to delete.
    250
    251        Returns:
    252            dict: Updated set_blocks dictionary.
    253        """
    254        new_set_blocks = set_blocks.copy()
    255        for key in matches:
    256            if key in new_set_blocks:
    257                del new_set_blocks[key]
    258        return new_set_blocks
    259
    260    def is_aligned(self, position, next_position, block, next_block, is_same_row):
    261        """
    262        Check if two blocks are aligned and of the same color.
    263
    264        Args:
    265            position (tuple): (row, col) of the first block.
    266            next_position (tuple): (row, col) of the second block.
    267            block (Block): First block.
    268            next_block (Block): Second block.
    269            is_same_row (bool): True for row, False for column.
    270
    271        Returns:
    272            bool: True if aligned and same color, False otherwise.
    273        """
    274        if is_same_row:
    275            return position[0] == next_position[0] and position[1] == next_position[1] - 1 and block.color == next_block.color
    276        else:
    277            return position[1] == next_position[1] and position[0] == next_position[0] - 1 and block.color == next_block.color
    278
    279    def has_gravity(self, set_blocks, grid_size):
    280        """
    281        Check if any block can fall due to gravity.
    282
    283        Args:
    284            set_blocks (dict): Current set of blocks.
    285            grid_size (tuple): Size of the grid.
    286
    287        Returns:
    288            bool: True if gravity applies, False otherwise.
    289        """
    290        return bool(self.apply_gravity(set_blocks, grid_size) != set_blocks)
    291
    292    def apply_gravity(self, set_blocks, grid_size):
    293        """
    294        Apply gravity to all blocks (make unsupported blocks fall).
    295
    296        For each block in the grid (processed from bottom to top), this function checks whether
    297        there is empty space directly beneath it. If so, it allows the block to fall vertically
    298        until it lands on either another block or the bottom of the grid.
    299
    300        Args:
    301            set_blocks (dict): Current set of blocks.
    302            grid_size (tuple): Size of the grid.
    303
    304        Returns:
    305            dict: Updated set_blocks dictionary after gravity.
    306        """
    307        new_set_blocks = set_blocks.copy()
    308        for position in reversed(set_blocks):
    309            row, col = position
    310            underneath_position = (row + 1, col)
    311
    312            while underneath_position[0] < grid_size[0] and underneath_position not in new_set_blocks:
    313              underneath_position = (underneath_position[0] + 1, underneath_position[1])
    314
    315            new_position = (underneath_position[0] - 1, underneath_position[1])
    316            if new_position != position:
    317              new_set_blocks = self.update_block(position, new_position, new_set_blocks)
    318        return new_set_blocks
    319
    320    def apply_gravity_and_eliminate_matches(self, set_blocks, grid_size):
    321        """
    322        Apply gravity and eliminate matches repeatedly until stable.
    323
    324        Args:
    325            set_blocks (dict): Current set of blocks.
    326            grid_size (tuple): Size of the grid.
    327
    328        Returns:
    329            dict: Updated set_blocks dictionary after gravity and elimination.
    330        """
    331        new_set_blocks = set_blocks.copy()
    332        while True:
    333            new_set_blocks_before = new_set_blocks.copy()
    334            new_set_blocks = self.apply_gravity(new_set_blocks, grid_size)
    335            new_set_blocks = self.delete_matches(new_set_blocks,self.find_matches(new_set_blocks))
    336            if new_set_blocks == new_set_blocks_before:
    337                break
    338        return new_set_blocks
    339
    340    def make_move(self, move_type, position, set_blocks):
    341        """
    342        Perform a move (either a simple shift or an exchange) on a given block in the grid (set_blocks).
    343
    344        Args:
    345            move_type (str): Type of move ('move_left', 'move_right', etc.).
    346            position (tuple): (row, col) of the block to move.
    347            set_blocks (dict): Current set of blocks.
    348
    349        Returns:
    350            dict: Updated set_blocks dictionary.
    351        """
    352        if move_type == "move_left":
    353            set_blocks = self.make_simple_move(position, "left", set_blocks)
    354        elif move_type == "move_right":
    355            set_blocks = self.make_simple_move(position, "right", set_blocks)
    356        elif move_type == "exchange_left":
    357            set_blocks = self.make_exchange_move(position, "left", set_blocks)
    358        elif move_type == "exchange_right":
    359            set_blocks = self.make_exchange_move(position, "right", set_blocks)
    360        return set_blocks
    361
    362    def make_simple_move(self, position, direction, set_blocks):
    363        """
    364        Perform a simple move (left or right) for a given block.
    365
    366        Args:
    367            position (tuple): (row, col) of the block.
    368            direction (str): 'left' or 'right'.
    369            set_blocks (dict): Current set of blocks.
    370
    371        Returns:
    372            dict: Updated set_blocks dictionary.
    373        """
    374        if direction == "left":
    375          new_position = (position[0], position[1]-1)
    376          set_blocks = self.update_block(position, new_position, set_blocks)
    377        elif direction == "right":
    378          new_position = (position[0], position[1]+1)
    379          set_blocks = self.update_block(position, new_position, set_blocks)
    380        return set_blocks
    381
    382    def make_exchange_move(self, position, direction, set_blocks):
    383        """
    384        Perform an exchange move (left or right) for a given block.
    385
    386        Args:
    387            position (tuple): (row, col) of the block.
    388            direction (str): 'left' or 'right'.
    389            set_blocks (dict): Current set of blocks.
    390
    391        Returns:
    392            dict: Updated set_blocks dictionary.
    393        """
    394        if direction == "left":
    395          new_position = (position[0], position[1]-1)
    396          set_blocks = self.exchange_block(position, new_position, set_blocks)
    397        elif direction == "right":
    398          new_position = (position[0], position[1]+1)
    399          set_blocks = self.exchange_block(position, new_position, set_blocks)
    400        return set_blocks
    401
    402    def get_game_state(self, set_blocks, colors):
    403        """
    404        Get the current game state.
    405
    406        Args:
    407            set_blocks (dict): Current set of blocks.
    408            colors (list): List of block colors.
    409
    410        Returns:
    411            int: 3 for victory, 2 for defeat, 1 for in progress.
    412        """
    413        if not set_blocks:
    414          return 3
    415        elif not self.has_valid_block_color_counts(set_blocks, colors):
    416          return 2
    417        else:
    418          return 1
    419
    420    def has_valid_block_color_counts(self, set_blocks, colors):
    421        """
    422        Check if all colors have at least 3 blocks.
    423
    424        Args:
    425            set_blocks (dict): Current set of blocks.
    426            colors (list): List of block colors.
    427
    428        Returns:
    429            bool: True if valid, False otherwise.
    430        """
    431        color_counts = self.count_block_colors(set_blocks)
    432        for color in colors:
    433            if color_counts[color] == 1 or color_counts[color] == 2:
    434                return False
    435            else:
    436                return True
    437
    438    def count_block_colors(self, set_blocks):
    439        """
    440        Count the number of blocks for each color.
    441
    442        Args:
    443            set_blocks (dict): Current set of blocks.
    444
    445        Returns:
    446            Counter: Counter object with color counts.
    447        """
    448        color_counts = Counter(block.color for block in set_blocks.values())
    449        return color_counts

class PuzzleLogic: View Source

      4class PuzzleLogic():
      5    """
      6    PuzzleLogic Class: Contains methods responsible for the game's core logic.
      7    This includes checking for block elimination, enforcing gravity rules (making blocks fall down after elimination),
      8    and performing player moves (e.g., swapping two adjacent blocks).
      9    """
     10    def __init__(self):
     11        """Initialize the PuzzleLogic class."""
     12        return
     13
     14    def check_all_available_moves(self, current_position, color, set_blocks, grid_size):
     15        """
     16        Check all possible moves for a block at the given position.
     17
     18        Args:
     19            current_position (tuple): (row, col) position of the block.
     20            color (str): Color of the block.
     21            set_blocks (dict): Current set of blocks.
     22            grid_size (tuple): Size of the grid.
     23
     24        Returns:
     25            dict: Dictionary with possible moves as keys and booleans as values.
     26        """
     27        possible_moves = {
     28            'move_left': self.can_move_left(current_position, set_blocks),
     29            'move_right': self.can_move_right(current_position, set_blocks, grid_size),
     30            'exchange_left': self.can_exchange_left(current_position, color, set_blocks),
     31            'exchange_right': self.can_exchange_right(current_position, color, set_blocks, grid_size)
     32        }
     33        return possible_moves
     34
     35    def can_move_left(self, position, set_blocks):
     36        """
     37        Check if a block can move left to an unoccupied space.
     38
     39        Args:
     40            position (tuple): (row, col) position of the block.
     41            set_blocks (dict): Current set of blocks.
     42
     43        Returns:
     44            bool: True if the block can move left, False otherwise.
     45        """
     46        row, col = position
     47        if col > 0:
     48            left_position = (row, col - 1)
     49            return left_position not in set_blocks
     50        return False
     51
     52    def can_move_right(self, position, set_blocks, grid_size):
     53        """
     54        Check if a block can move right to an unoccupied space.
     55
     56        Args:
     57            position (tuple): (row, col) position of the block.
     58            set_blocks (dict): Current set of blocks.
     59            grid_size (tuple): Size of the grid.
     60
     61        Returns:
     62            bool: True if the block can move right, False otherwise.
     63        """
     64        row, col = position
     65        if col < grid_size[1] - 1:
     66            right_position = (row, col + 1)
     67            return right_position not in set_blocks
     68        return False
     69
     70    def can_exchange_left(self, position, color, set_blocks):
     71        """
     72        Check if a block can exchange with the block to its left.
     73
     74        Args:
     75            position (tuple): (row, col) position of the block.
     76            color (str): Color of the block.
     77            set_blocks (dict): Current set of blocks.
     78
     79        Returns:
     80            bool: True if the block can exchange left, False otherwise.
     81        """
     82        row, col = position
     83        if col > 0:
     84            left_position = (row, col - 1)
     85            return left_position in set_blocks and color != set_blocks[left_position].color
     86        return False
     87
     88    def can_exchange_right(self, position, color, set_blocks, grid_size):
     89        """
     90        Check if a block can exchange with the block to its right.
     91
     92        Args:
     93            position (tuple): (row, col) position of the block.
     94            color (str): Color of the block.
     95            set_blocks (dict): Current set of blocks.
     96            grid_size (tuple): Size of the grid.
     97
     98        Returns:
     99            bool: True if the block can exchange right, False otherwise.
    100        """
    101        row, col = position
    102        if col < grid_size[1] - 1:
    103            right_position = (row, col + 1)
    104            return right_position in set_blocks and color != set_blocks[right_position].color
    105        return False
    106
    107    def update_block(self, old_position, new_position, set_blocks):
    108        """
    109        Update the position of a block in the set_blocks dictionary.
    110
    111        Args:
    112            old_position (tuple): Previous (row, col) position.
    113            new_position (tuple): New (row, col) position.
    114            set_blocks (dict): Current set of blocks.
    115
    116        Returns:
    117            dict: Updated set_blocks dictionary.
    118        """
    119        saved_block = set_blocks.pop(old_position)
    120        set_blocks[new_position] = saved_block
    121        set_blocks = sort_blocks_by_rows(set_blocks)
    122        return set_blocks
    123
    124    def exchange_block(self, first_position, second_position, set_blocks):
    125        """
    126        Exchange two blocks in the set_blocks dictionary.
    127
    128        Args:
    129            first_position (tuple): (row, col) of the first block.
    130            second_position (tuple): (row, col) of the second block.
    131            set_blocks (dict): Current set of blocks.
    132
    133        Returns:
    134            dict: Updated set_blocks dictionary.
    135        """
    136        block_1 = set_blocks[first_position]
    137        block_2 = set_blocks[second_position]
    138        set_blocks[first_position] = block_2
    139        set_blocks[second_position] = block_1
    140        set_blocks = sort_blocks_by_rows(set_blocks)
    141        return set_blocks
    142
    143    def has_matches(self, set_blocks):
    144        """
    145        Check if there are any matches (3 or more aligned blocks of the same color).
    146
    147        Args:
    148            set_blocks (dict): Current set of blocks.
    149
    150        Returns:
    151            bool: True if matches exist, False otherwise.
    152        """
    153        return bool(self.find_matches(set_blocks))
    154
    155    def find_matches(self, set_blocks):
    156        """
    157        Find all matches (3 or more aligned blocks of the same color).
    158
    159        Args:
    160            set_blocks (dict): Current set of blocks.
    161
    162        Returns:
    163            set: Set of positions where matches are found.
    164        """
    165        matches = set()
    166
    167        matches |= self.find_matches_by_rows(set_blocks)
    168        matches |= self.find_matches_by_columns(set_blocks)
    169
    170        return matches
    171
    172    def find_matches_by_rows(self, set_blocks):
    173        """
    174        Find all horizontal matches in the set_blocks.
    175
    176        Args:
    177            set_blocks (dict): Current set of blocks.
    178
    179        Returns:
    180            set: Set of positions where horizontal matches are found.
    181        """
    182        matches = set()
    183
    184        set_blocks_row = sort_blocks_by_rows(set_blocks)
    185        matches |= self.find_matches_in_iterator(set_blocks_row.items(), is_same_row=True)
    186
    187        return matches
    188
    189    def find_matches_by_columns(self, set_blocks):
    190        """
    191        Find all vertical matches in the set_blocks.
    192
    193        Args:
    194            set_blocks (dict): Current set of blocks.
    195
    196        Returns:
    197            set: Set of positions where vertical matches are found.
    198        """
    199        matches = set()
    200
    201        set_blocks_column = sort_blocks_by_columns(set_blocks)
    202        matches |= self.find_matches_in_iterator(set_blocks_column.items(), is_same_row=False)
    203
    204        return matches
    205
    206    def find_matches_in_iterator(self, set_blocks, is_same_row):
    207        """
    208        Helper function to find matches in an iterator (row or column).
    209
    210        Args:
    211            set_blocks (iterator): Iterator over set_blocks items.
    212            is_same_row (bool): True for row, False for column.
    213
    214        Returns:
    215            set: Set of positions where matches are found.
    216        """
    217        matches = set()
    218        tmp_matches = set()
    219        nb_aligned = 1
    220
    221        try:
    222            iterator = iter(set_blocks)
    223            (position, block) = next(iterator)
    224            while True:
    225                (next_position, next_block) = next(iterator)
    226                if self.is_aligned(position, next_position, block, next_block, is_same_row):
    227                    nb_aligned += 1
    228                    tmp_matches.add(position)
    229                    tmp_matches.add(next_position)
    230                else:
    231                    nb_aligned = 1
    232                    tmp_matches = set()
    233
    234                if nb_aligned >= 3:
    235                    matches |= tmp_matches
    236
    237                position, block = next_position, next_block
    238
    239        except StopIteration:
    240            pass
    241
    242        return matches
    243
    244    def delete_matches(self, set_blocks, matches):
    245        """
    246        Delete all matched blocks from the set_blocks.
    247
    248        Args:
    249            set_blocks (dict): Current set of blocks.
    250            matches (set): Set of positions to delete.
    251
    252        Returns:
    253            dict: Updated set_blocks dictionary.
    254        """
    255        new_set_blocks = set_blocks.copy()
    256        for key in matches:
    257            if key in new_set_blocks:
    258                del new_set_blocks[key]
    259        return new_set_blocks
    260
    261    def is_aligned(self, position, next_position, block, next_block, is_same_row):
    262        """
    263        Check if two blocks are aligned and of the same color.
    264
    265        Args:
    266            position (tuple): (row, col) of the first block.
    267            next_position (tuple): (row, col) of the second block.
    268            block (Block): First block.
    269            next_block (Block): Second block.
    270            is_same_row (bool): True for row, False for column.
    271
    272        Returns:
    273            bool: True if aligned and same color, False otherwise.
    274        """
    275        if is_same_row:
    276            return position[0] == next_position[0] and position[1] == next_position[1] - 1 and block.color == next_block.color
    277        else:
    278            return position[1] == next_position[1] and position[0] == next_position[0] - 1 and block.color == next_block.color
    279
    280    def has_gravity(self, set_blocks, grid_size):
    281        """
    282        Check if any block can fall due to gravity.
    283
    284        Args:
    285            set_blocks (dict): Current set of blocks.
    286            grid_size (tuple): Size of the grid.
    287
    288        Returns:
    289            bool: True if gravity applies, False otherwise.
    290        """
    291        return bool(self.apply_gravity(set_blocks, grid_size) != set_blocks)
    292
    293    def apply_gravity(self, set_blocks, grid_size):
    294        """
    295        Apply gravity to all blocks (make unsupported blocks fall).
    296
    297        For each block in the grid (processed from bottom to top), this function checks whether
    298        there is empty space directly beneath it. If so, it allows the block to fall vertically
    299        until it lands on either another block or the bottom of the grid.
    300
    301        Args:
    302            set_blocks (dict): Current set of blocks.
    303            grid_size (tuple): Size of the grid.
    304
    305        Returns:
    306            dict: Updated set_blocks dictionary after gravity.
    307        """
    308        new_set_blocks = set_blocks.copy()
    309        for position in reversed(set_blocks):
    310            row, col = position
    311            underneath_position = (row + 1, col)
    312
    313            while underneath_position[0] < grid_size[0] and underneath_position not in new_set_blocks:
    314              underneath_position = (underneath_position[0] + 1, underneath_position[1])
    315
    316            new_position = (underneath_position[0] - 1, underneath_position[1])
    317            if new_position != position:
    318              new_set_blocks = self.update_block(position, new_position, new_set_blocks)
    319        return new_set_blocks
    320
    321    def apply_gravity_and_eliminate_matches(self, set_blocks, grid_size):
    322        """
    323        Apply gravity and eliminate matches repeatedly until stable.
    324
    325        Args:
    326            set_blocks (dict): Current set of blocks.
    327            grid_size (tuple): Size of the grid.
    328
    329        Returns:
    330            dict: Updated set_blocks dictionary after gravity and elimination.
    331        """
    332        new_set_blocks = set_blocks.copy()
    333        while True:
    334            new_set_blocks_before = new_set_blocks.copy()
    335            new_set_blocks = self.apply_gravity(new_set_blocks, grid_size)
    336            new_set_blocks = self.delete_matches(new_set_blocks,self.find_matches(new_set_blocks))
    337            if new_set_blocks == new_set_blocks_before:
    338                break
    339        return new_set_blocks
    340
    341    def make_move(self, move_type, position, set_blocks):
    342        """
    343        Perform a move (either a simple shift or an exchange) on a given block in the grid (set_blocks).
    344
    345        Args:
    346            move_type (str): Type of move ('move_left', 'move_right', etc.).
    347            position (tuple): (row, col) of the block to move.
    348            set_blocks (dict): Current set of blocks.
    349
    350        Returns:
    351            dict: Updated set_blocks dictionary.
    352        """
    353        if move_type == "move_left":
    354            set_blocks = self.make_simple_move(position, "left", set_blocks)
    355        elif move_type == "move_right":
    356            set_blocks = self.make_simple_move(position, "right", set_blocks)
    357        elif move_type == "exchange_left":
    358            set_blocks = self.make_exchange_move(position, "left", set_blocks)
    359        elif move_type == "exchange_right":
    360            set_blocks = self.make_exchange_move(position, "right", set_blocks)
    361        return set_blocks
    362
    363    def make_simple_move(self, position, direction, set_blocks):
    364        """
    365        Perform a simple move (left or right) for a given block.
    366
    367        Args:
    368            position (tuple): (row, col) of the block.
    369            direction (str): 'left' or 'right'.
    370            set_blocks (dict): Current set of blocks.
    371
    372        Returns:
    373            dict: Updated set_blocks dictionary.
    374        """
    375        if direction == "left":
    376          new_position = (position[0], position[1]-1)
    377          set_blocks = self.update_block(position, new_position, set_blocks)
    378        elif direction == "right":
    379          new_position = (position[0], position[1]+1)
    380          set_blocks = self.update_block(position, new_position, set_blocks)
    381        return set_blocks
    382
    383    def make_exchange_move(self, position, direction, set_blocks):
    384        """
    385        Perform an exchange move (left or right) for a given block.
    386
    387        Args:
    388            position (tuple): (row, col) of the block.
    389            direction (str): 'left' or 'right'.
    390            set_blocks (dict): Current set of blocks.
    391
    392        Returns:
    393            dict: Updated set_blocks dictionary.
    394        """
    395        if direction == "left":
    396          new_position = (position[0], position[1]-1)
    397          set_blocks = self.exchange_block(position, new_position, set_blocks)
    398        elif direction == "right":
    399          new_position = (position[0], position[1]+1)
    400          set_blocks = self.exchange_block(position, new_position, set_blocks)
    401        return set_blocks
    402
    403    def get_game_state(self, set_blocks, colors):
    404        """
    405        Get the current game state.
    406
    407        Args:
    408            set_blocks (dict): Current set of blocks.
    409            colors (list): List of block colors.
    410
    411        Returns:
    412            int: 3 for victory, 2 for defeat, 1 for in progress.
    413        """
    414        if not set_blocks:
    415          return 3
    416        elif not self.has_valid_block_color_counts(set_blocks, colors):
    417          return 2
    418        else:
    419          return 1
    420
    421    def has_valid_block_color_counts(self, set_blocks, colors):
    422        """
    423        Check if all colors have at least 3 blocks.
    424
    425        Args:
    426            set_blocks (dict): Current set of blocks.
    427            colors (list): List of block colors.
    428
    429        Returns:
    430            bool: True if valid, False otherwise.
    431        """
    432        color_counts = self.count_block_colors(set_blocks)
    433        for color in colors:
    434            if color_counts[color] == 1 or color_counts[color] == 2:
    435                return False
    436            else:
    437                return True
    438
    439    def count_block_colors(self, set_blocks):
    440        """
    441        Count the number of blocks for each color.
    442
    443        Args:
    444            set_blocks (dict): Current set of blocks.
    445
    446        Returns:
    447            Counter: Counter object with color counts.
    448        """
    449        color_counts = Counter(block.color for block in set_blocks.values())
    450        return color_counts

PuzzleLogic Class: Contains methods responsible for the game's core logic. This includes checking for block elimination, enforcing gravity rules (making blocks fall down after elimination), and performing player moves (e.g., swapping two adjacent blocks).

PuzzleLogic() View Source

    10    def __init__(self):
    11        """Initialize the PuzzleLogic class."""
    12        return

Initialize the PuzzleLogic class.

def check_all_available_moves(self, current_position, color, set_blocks, grid_size): View Source

    14    def check_all_available_moves(self, current_position, color, set_blocks, grid_size):
    15        """
    16        Check all possible moves for a block at the given position.
    17
    18        Args:
    19            current_position (tuple): (row, col) position of the block.
    20            color (str): Color of the block.
    21            set_blocks (dict): Current set of blocks.
    22            grid_size (tuple): Size of the grid.
    23
    24        Returns:
    25            dict: Dictionary with possible moves as keys and booleans as values.
    26        """
    27        possible_moves = {
    28            'move_left': self.can_move_left(current_position, set_blocks),
    29            'move_right': self.can_move_right(current_position, set_blocks, grid_size),
    30            'exchange_left': self.can_exchange_left(current_position, color, set_blocks),
    31            'exchange_right': self.can_exchange_right(current_position, color, set_blocks, grid_size)
    32        }
    33        return possible_moves

Check all possible moves for a block at the given position.

Args: current_position (tuple): (row, col) position of the block. color (str): Color of the block. set_blocks (dict): Current set of blocks. grid_size (tuple): Size of the grid.

Returns: dict: Dictionary with possible moves as keys and booleans as values.

def can_move_left(self, position, set_blocks): View Source

    35    def can_move_left(self, position, set_blocks):
    36        """
    37        Check if a block can move left to an unoccupied space.
    38
    39        Args:
    40            position (tuple): (row, col) position of the block.
    41            set_blocks (dict): Current set of blocks.
    42
    43        Returns:
    44            bool: True if the block can move left, False otherwise.
    45        """
    46        row, col = position
    47        if col > 0:
    48            left_position = (row, col - 1)
    49            return left_position not in set_blocks
    50        return False

Check if a block can move left to an unoccupied space.

Args: position (tuple): (row, col) position of the block. set_blocks (dict): Current set of blocks.

Returns: bool: True if the block can move left, False otherwise.

def can_move_right(self, position, set_blocks, grid_size): View Source

    52    def can_move_right(self, position, set_blocks, grid_size):
    53        """
    54        Check if a block can move right to an unoccupied space.
    55
    56        Args:
    57            position (tuple): (row, col) position of the block.
    58            set_blocks (dict): Current set of blocks.
    59            grid_size (tuple): Size of the grid.
    60
    61        Returns:
    62            bool: True if the block can move right, False otherwise.
    63        """
    64        row, col = position
    65        if col < grid_size[1] - 1:
    66            right_position = (row, col + 1)
    67            return right_position not in set_blocks
    68        return False

Check if a block can move right to an unoccupied space.

Args: position (tuple): (row, col) position of the block. set_blocks (dict): Current set of blocks. grid_size (tuple): Size of the grid.

Returns: bool: True if the block can move right, False otherwise.

def can_exchange_left(self, position, color, set_blocks): View Source

    70    def can_exchange_left(self, position, color, set_blocks):
    71        """
    72        Check if a block can exchange with the block to its left.
    73
    74        Args:
    75            position (tuple): (row, col) position of the block.
    76            color (str): Color of the block.
    77            set_blocks (dict): Current set of blocks.
    78
    79        Returns:
    80            bool: True if the block can exchange left, False otherwise.
    81        """
    82        row, col = position
    83        if col > 0:
    84            left_position = (row, col - 1)
    85            return left_position in set_blocks and color != set_blocks[left_position].color
    86        return False

Check if a block can exchange with the block to its left.

Args: position (tuple): (row, col) position of the block. color (str): Color of the block. set_blocks (dict): Current set of blocks.

Returns: bool: True if the block can exchange left, False otherwise.

def can_exchange_right(self, position, color, set_blocks, grid_size): View Source

     88    def can_exchange_right(self, position, color, set_blocks, grid_size):
     89        """
     90        Check if a block can exchange with the block to its right.
     91
     92        Args:
     93            position (tuple): (row, col) position of the block.
     94            color (str): Color of the block.
     95            set_blocks (dict): Current set of blocks.
     96            grid_size (tuple): Size of the grid.
     97
     98        Returns:
     99            bool: True if the block can exchange right, False otherwise.
    100        """
    101        row, col = position
    102        if col < grid_size[1] - 1:
    103            right_position = (row, col + 1)
    104            return right_position in set_blocks and color != set_blocks[right_position].color
    105        return False

Check if a block can exchange with the block to its right.

Args: position (tuple): (row, col) position of the block. color (str): Color of the block. set_blocks (dict): Current set of blocks. grid_size (tuple): Size of the grid.

Returns: bool: True if the block can exchange right, False otherwise.

def update_block(self, old_position, new_position, set_blocks): View Source

    107    def update_block(self, old_position, new_position, set_blocks):
    108        """
    109        Update the position of a block in the set_blocks dictionary.
    110
    111        Args:
    112            old_position (tuple): Previous (row, col) position.
    113            new_position (tuple): New (row, col) position.
    114            set_blocks (dict): Current set of blocks.
    115
    116        Returns:
    117            dict: Updated set_blocks dictionary.
    118        """
    119        saved_block = set_blocks.pop(old_position)
    120        set_blocks[new_position] = saved_block
    121        set_blocks = sort_blocks_by_rows(set_blocks)
    122        return set_blocks

Update the position of a block in the set_blocks dictionary.

Args: old_position (tuple): Previous (row, col) position. new_position (tuple): New (row, col) position. set_blocks (dict): Current set of blocks.

Returns: dict: Updated set_blocks dictionary.

def exchange_block(self, first_position, second_position, set_blocks): View Source

    124    def exchange_block(self, first_position, second_position, set_blocks):
    125        """
    126        Exchange two blocks in the set_blocks dictionary.
    127
    128        Args:
    129            first_position (tuple): (row, col) of the first block.
    130            second_position (tuple): (row, col) of the second block.
    131            set_blocks (dict): Current set of blocks.
    132
    133        Returns:
    134            dict: Updated set_blocks dictionary.
    135        """
    136        block_1 = set_blocks[first_position]
    137        block_2 = set_blocks[second_position]
    138        set_blocks[first_position] = block_2
    139        set_blocks[second_position] = block_1
    140        set_blocks = sort_blocks_by_rows(set_blocks)
    141        return set_blocks

Exchange two blocks in the set_blocks dictionary.

Args: first_position (tuple): (row, col) of the first block. second_position (tuple): (row, col) of the second block. set_blocks (dict): Current set of blocks.

Returns: dict: Updated set_blocks dictionary.

def has_matches(self, set_blocks): View Source

    143    def has_matches(self, set_blocks):
    144        """
    145        Check if there are any matches (3 or more aligned blocks of the same color).
    146
    147        Args:
    148            set_blocks (dict): Current set of blocks.
    149
    150        Returns:
    151            bool: True if matches exist, False otherwise.
    152        """
    153        return bool(self.find_matches(set_blocks))

Check if there are any matches (3 or more aligned blocks of the same color).

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

Returns: bool: True if matches exist, False otherwise.

def find_matches(self, set_blocks): View Source

    155    def find_matches(self, set_blocks):
    156        """
    157        Find all matches (3 or more aligned blocks of the same color).
    158
    159        Args:
    160            set_blocks (dict): Current set of blocks.
    161
    162        Returns:
    163            set: Set of positions where matches are found.
    164        """
    165        matches = set()
    166
    167        matches |= self.find_matches_by_rows(set_blocks)
    168        matches |= self.find_matches_by_columns(set_blocks)
    169
    170        return matches

Find all matches (3 or more aligned blocks of the same color).

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

Returns: set: Set of positions where matches are found.

def find_matches_by_rows(self, set_blocks): View Source

    172    def find_matches_by_rows(self, set_blocks):
    173        """
    174        Find all horizontal matches in the set_blocks.
    175
    176        Args:
    177            set_blocks (dict): Current set of blocks.
    178
    179        Returns:
    180            set: Set of positions where horizontal matches are found.
    181        """
    182        matches = set()
    183
    184        set_blocks_row = sort_blocks_by_rows(set_blocks)
    185        matches |= self.find_matches_in_iterator(set_blocks_row.items(), is_same_row=True)
    186
    187        return matches

Find all horizontal matches in the set_blocks.

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

Returns: set: Set of positions where horizontal matches are found.

def find_matches_by_columns(self, set_blocks): View Source

    189    def find_matches_by_columns(self, set_blocks):
    190        """
    191        Find all vertical matches in the set_blocks.
    192
    193        Args:
    194            set_blocks (dict): Current set of blocks.
    195
    196        Returns:
    197            set: Set of positions where vertical matches are found.
    198        """
    199        matches = set()
    200
    201        set_blocks_column = sort_blocks_by_columns(set_blocks)
    202        matches |= self.find_matches_in_iterator(set_blocks_column.items(), is_same_row=False)
    203
    204        return matches

Find all vertical matches in the set_blocks.

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

Returns: set: Set of positions where vertical matches are found.

def find_matches_in_iterator(self, set_blocks, is_same_row): View Source

    206    def find_matches_in_iterator(self, set_blocks, is_same_row):
    207        """
    208        Helper function to find matches in an iterator (row or column).
    209
    210        Args:
    211            set_blocks (iterator): Iterator over set_blocks items.
    212            is_same_row (bool): True for row, False for column.
    213
    214        Returns:
    215            set: Set of positions where matches are found.
    216        """
    217        matches = set()
    218        tmp_matches = set()
    219        nb_aligned = 1
    220
    221        try:
    222            iterator = iter(set_blocks)
    223            (position, block) = next(iterator)
    224            while True:
    225                (next_position, next_block) = next(iterator)
    226                if self.is_aligned(position, next_position, block, next_block, is_same_row):
    227                    nb_aligned += 1
    228                    tmp_matches.add(position)
    229                    tmp_matches.add(next_position)
    230                else:
    231                    nb_aligned = 1
    232                    tmp_matches = set()
    233
    234                if nb_aligned >= 3:
    235                    matches |= tmp_matches
    236
    237                position, block = next_position, next_block
    238
    239        except StopIteration:
    240            pass
    241
    242        return matches

Helper function to find matches in an iterator (row or column).

Args: set_blocks (iterator): Iterator over set_blocks items. is_same_row (bool): True for row, False for column.

Returns: set: Set of positions where matches are found.

def delete_matches(self, set_blocks, matches): View Source

    244    def delete_matches(self, set_blocks, matches):
    245        """
    246        Delete all matched blocks from the set_blocks.
    247
    248        Args:
    249            set_blocks (dict): Current set of blocks.
    250            matches (set): Set of positions to delete.
    251
    252        Returns:
    253            dict: Updated set_blocks dictionary.
    254        """
    255        new_set_blocks = set_blocks.copy()
    256        for key in matches:
    257            if key in new_set_blocks:
    258                del new_set_blocks[key]
    259        return new_set_blocks

Delete all matched blocks from the set_blocks.

Args: set_blocks (dict): Current set of blocks. matches (set): Set of positions to delete.

Returns: dict: Updated set_blocks dictionary.

def is_aligned(self, position, next_position, block, next_block, is_same_row): View Source

    261    def is_aligned(self, position, next_position, block, next_block, is_same_row):
    262        """
    263        Check if two blocks are aligned and of the same color.
    264
    265        Args:
    266            position (tuple): (row, col) of the first block.
    267            next_position (tuple): (row, col) of the second block.
    268            block (Block): First block.
    269            next_block (Block): Second block.
    270            is_same_row (bool): True for row, False for column.
    271
    272        Returns:
    273            bool: True if aligned and same color, False otherwise.
    274        """
    275        if is_same_row:
    276            return position[0] == next_position[0] and position[1] == next_position[1] - 1 and block.color == next_block.color
    277        else:
    278            return position[1] == next_position[1] and position[0] == next_position[0] - 1 and block.color == next_block.color

Check if two blocks are aligned and of the same color.

Args: position (tuple): (row, col) of the first block. next_position (tuple): (row, col) of the second block. block (Block): First block. next_block (Block): Second block. is_same_row (bool): True for row, False for column.

Returns: bool: True if aligned and same color, False otherwise.

def has_gravity(self, set_blocks, grid_size): View Source

    280    def has_gravity(self, set_blocks, grid_size):
    281        """
    282        Check if any block can fall due to gravity.
    283
    284        Args:
    285            set_blocks (dict): Current set of blocks.
    286            grid_size (tuple): Size of the grid.
    287
    288        Returns:
    289            bool: True if gravity applies, False otherwise.
    290        """
    291        return bool(self.apply_gravity(set_blocks, grid_size) != set_blocks)

Check if any block can fall due to gravity.

Args: set_blocks (dict): Current set of blocks. grid_size (tuple): Size of the grid.

Returns: bool: True if gravity applies, False otherwise.

def apply_gravity(self, set_blocks, grid_size): View Source

    293    def apply_gravity(self, set_blocks, grid_size):
    294        """
    295        Apply gravity to all blocks (make unsupported blocks fall).
    296
    297        For each block in the grid (processed from bottom to top), this function checks whether
    298        there is empty space directly beneath it. If so, it allows the block to fall vertically
    299        until it lands on either another block or the bottom of the grid.
    300
    301        Args:
    302            set_blocks (dict): Current set of blocks.
    303            grid_size (tuple): Size of the grid.
    304
    305        Returns:
    306            dict: Updated set_blocks dictionary after gravity.
    307        """
    308        new_set_blocks = set_blocks.copy()
    309        for position in reversed(set_blocks):
    310            row, col = position
    311            underneath_position = (row + 1, col)
    312
    313            while underneath_position[0] < grid_size[0] and underneath_position not in new_set_blocks:
    314              underneath_position = (underneath_position[0] + 1, underneath_position[1])
    315
    316            new_position = (underneath_position[0] - 1, underneath_position[1])
    317            if new_position != position:
    318              new_set_blocks = self.update_block(position, new_position, new_set_blocks)
    319        return new_set_blocks

Apply gravity to all blocks (make unsupported blocks fall).

For each block in the grid (processed from bottom to top), this function checks whether there is empty space directly beneath it. If so, it allows the block to fall vertically until it lands on either another block or the bottom of the grid.

Args: set_blocks (dict): Current set of blocks. grid_size (tuple): Size of the grid.

Returns: dict: Updated set_blocks dictionary after gravity.

def apply_gravity_and_eliminate_matches(self, set_blocks, grid_size): View Source

    321    def apply_gravity_and_eliminate_matches(self, set_blocks, grid_size):
    322        """
    323        Apply gravity and eliminate matches repeatedly until stable.
    324
    325        Args:
    326            set_blocks (dict): Current set of blocks.
    327            grid_size (tuple): Size of the grid.
    328
    329        Returns:
    330            dict: Updated set_blocks dictionary after gravity and elimination.
    331        """
    332        new_set_blocks = set_blocks.copy()
    333        while True:
    334            new_set_blocks_before = new_set_blocks.copy()
    335            new_set_blocks = self.apply_gravity(new_set_blocks, grid_size)
    336            new_set_blocks = self.delete_matches(new_set_blocks,self.find_matches(new_set_blocks))
    337            if new_set_blocks == new_set_blocks_before:
    338                break
    339        return new_set_blocks

Apply gravity and eliminate matches repeatedly until stable.

Args: set_blocks (dict): Current set of blocks. grid_size (tuple): Size of the grid.

Returns: dict: Updated set_blocks dictionary after gravity and elimination.

def make_move(self, move_type, position, set_blocks): View Source

    341    def make_move(self, move_type, position, set_blocks):
    342        """
    343        Perform a move (either a simple shift or an exchange) on a given block in the grid (set_blocks).
    344
    345        Args:
    346            move_type (str): Type of move ('move_left', 'move_right', etc.).
    347            position (tuple): (row, col) of the block to move.
    348            set_blocks (dict): Current set of blocks.
    349
    350        Returns:
    351            dict: Updated set_blocks dictionary.
    352        """
    353        if move_type == "move_left":
    354            set_blocks = self.make_simple_move(position, "left", set_blocks)
    355        elif move_type == "move_right":
    356            set_blocks = self.make_simple_move(position, "right", set_blocks)
    357        elif move_type == "exchange_left":
    358            set_blocks = self.make_exchange_move(position, "left", set_blocks)
    359        elif move_type == "exchange_right":
    360            set_blocks = self.make_exchange_move(position, "right", set_blocks)
    361        return set_blocks

Perform a move (either a simple shift or an exchange) on a given block in the grid (set_blocks).

Args: move_type (str): Type of move ('move_left', 'move_right', etc.). position (tuple): (row, col) of the block to move. set_blocks (dict): Current set of blocks.

Returns: dict: Updated set_blocks dictionary.

def make_simple_move(self, position, direction, set_blocks): View Source

    363    def make_simple_move(self, position, direction, set_blocks):
    364        """
    365        Perform a simple move (left or right) for a given block.
    366
    367        Args:
    368            position (tuple): (row, col) of the block.
    369            direction (str): 'left' or 'right'.
    370            set_blocks (dict): Current set of blocks.
    371
    372        Returns:
    373            dict: Updated set_blocks dictionary.
    374        """
    375        if direction == "left":
    376          new_position = (position[0], position[1]-1)
    377          set_blocks = self.update_block(position, new_position, set_blocks)
    378        elif direction == "right":
    379          new_position = (position[0], position[1]+1)
    380          set_blocks = self.update_block(position, new_position, set_blocks)
    381        return set_blocks

Perform a simple move (left or right) for a given block.

Args: position (tuple): (row, col) of the block. direction (str): 'left' or 'right'. set_blocks (dict): Current set of blocks.

Returns: dict: Updated set_blocks dictionary.

def make_exchange_move(self, position, direction, set_blocks): View Source

    383    def make_exchange_move(self, position, direction, set_blocks):
    384        """
    385        Perform an exchange move (left or right) for a given block.
    386
    387        Args:
    388            position (tuple): (row, col) of the block.
    389            direction (str): 'left' or 'right'.
    390            set_blocks (dict): Current set of blocks.
    391
    392        Returns:
    393            dict: Updated set_blocks dictionary.
    394        """
    395        if direction == "left":
    396          new_position = (position[0], position[1]-1)
    397          set_blocks = self.exchange_block(position, new_position, set_blocks)
    398        elif direction == "right":
    399          new_position = (position[0], position[1]+1)
    400          set_blocks = self.exchange_block(position, new_position, set_blocks)
    401        return set_blocks

Perform an exchange move (left or right) for a given block.

Args: position (tuple): (row, col) of the block. direction (str): 'left' or 'right'. set_blocks (dict): Current set of blocks.

Returns: dict: Updated set_blocks dictionary.

def get_game_state(self, set_blocks, colors): View Source

    403    def get_game_state(self, set_blocks, colors):
    404        """
    405        Get the current game state.
    406
    407        Args:
    408            set_blocks (dict): Current set of blocks.
    409            colors (list): List of block colors.
    410
    411        Returns:
    412            int: 3 for victory, 2 for defeat, 1 for in progress.
    413        """
    414        if not set_blocks:
    415          return 3
    416        elif not self.has_valid_block_color_counts(set_blocks, colors):
    417          return 2
    418        else:
    419          return 1

Get the current game state.

Args: set_blocks (dict): Current set of blocks. colors (list): List of block colors.

Returns: int: 3 for victory, 2 for defeat, 1 for in progress.

def has_valid_block_color_counts(self, set_blocks, colors): View Source

    421    def has_valid_block_color_counts(self, set_blocks, colors):
    422        """
    423        Check if all colors have at least 3 blocks.
    424
    425        Args:
    426            set_blocks (dict): Current set of blocks.
    427            colors (list): List of block colors.
    428
    429        Returns:
    430            bool: True if valid, False otherwise.
    431        """
    432        color_counts = self.count_block_colors(set_blocks)
    433        for color in colors:
    434            if color_counts[color] == 1 or color_counts[color] == 2:
    435                return False
    436            else:
    437                return True

Check if all colors have at least 3 blocks.

Args: set_blocks (dict): Current set of blocks. colors (list): List of block colors.

Returns: bool: True if valid, False otherwise.

def count_block_colors(self, set_blocks): View Source

    439    def count_block_colors(self, set_blocks):
    440        """
    441        Count the number of blocks for each color.
    442
    443        Args:
    444            set_blocks (dict): Current set of blocks.
    445
    446        Returns:
    447            Counter: Counter object with color counts.
    448        """
    449        color_counts = Counter(block.color for block in set_blocks.values())
    450        return color_counts

Count the number of blocks for each color.

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

Returns: Counter: Counter object with color counts.