Puzzle Game Logic - Python Implementation
Complete Puzzle Game Implementation in Python
This is the complete Python/Pygame implementation featuring multiple puzzle types: Match-3 with cascading combos, sliding tile puzzles, physics-based challenges, pattern memory games, and logic gate puzzles.
Full Puzzle Game Code
import pygame
import random
import math
from typing import List, Tuple, Optional, Dict
from dataclasses import dataclass
from enum import Enum
import copy
class PuzzleType(Enum):
MATCH3 = "match3"
SLIDING = "sliding"
PHYSICS = "physics"
LOGIC = "logic"
PATTERN = "pattern"
# Match-3 Game Implementation
class Match3Game:
"""Match-3 puzzle game"""
def __init__(self, grid_size: int = 8, gem_types: int = 6):
self.grid_size = grid_size
self.gem_types = gem_types
self.grid = []
self.selected = None
self.score = 0
self.combo = 1
self.moves = 0
# Gem colors
self.gem_colors = [
(255, 0, 0), # Red
(0, 255, 0), # Green
(0, 0, 255), # Blue
(255, 255, 0), # Yellow
(255, 0, 255), # Purple
(0, 255, 255) # Cyan
]
self.initialize_grid()
def initialize_grid(self):
"""Create initial game grid"""
self.grid = []
for y in range(self.grid_size):
row = []
for x in range(self.grid_size):
# Ensure no initial matches
valid_gem = False
while not valid_gem:
gem_type = random.randint(0, self.gem_types - 1)
# Check horizontal match
if x >= 2:
if row[x-1] == gem_type and row[x-2] == gem_type:
continue
# Check vertical match
if y >= 2:
if (self.grid[y-1][x] == gem_type and
self.grid[y-2][x] == gem_type):
continue
valid_gem = True
row.append(gem_type)
self.grid.append(row)
def swap_gems(self, x1: int, y1: int, x2: int, y2: int) -> bool:
"""Swap two adjacent gems"""
# Check if adjacent
if abs(x1 - x2) + abs(y1 - y2) != 1:
return False
# Perform swap
self.grid[y1][x1], self.grid[y2][x2] = self.grid[y2][x2], self.grid[y1][x1]
# Check for matches
matches = self.find_matches()
if matches:
self.process_matches(matches)
self.moves += 1
return True
else:
# Swap back if no matches
self.grid[y1][x1], self.grid[y2][x2] = self.grid[y2][x2], self.grid[y1][x1]
return False
def find_matches(self) -> List[Tuple[int, int]]:
"""Find all matching gems"""
matches = set()
# Check horizontal matches
for y in range(self.grid_size):
for x in range(self.grid_size - 2):
if (self.grid[y][x] == self.grid[y][x+1] == self.grid[y][x+2]
and self.grid[y][x] != -1):
matches.add((x, y))
matches.add((x+1, y))
matches.add((x+2, y))
# Check for longer matches
i = x + 3
while i < self.grid_size and self.grid[y][i] == self.grid[y][x]:
matches.add((i, y))
i += 1
# Check vertical matches
for x in range(self.grid_size):
for y in range(self.grid_size - 2):
if (self.grid[y][x] == self.grid[y+1][x] == self.grid[y+2][x]
and self.grid[y][x] != -1):
matches.add((x, y))
matches.add((x, y+1))
matches.add((x, y+2))
# Check for longer matches
i = y + 3
while i < self.grid_size and self.grid[i][x] == self.grid[y][x]:
matches.add((x, i))
i += 1
return list(matches)
def process_matches(self, matches: List[Tuple[int, int]]):
"""Process matched gems"""
# Award points
base_score = len(matches) * 10
self.score += base_score * self.combo
# Remove matched gems
for x, y in matches:
self.grid[y][x] = -1
# Drop gems
self.drop_gems()
# Fill empty spaces
self.fill_empty()
# Check for cascading matches
new_matches = self.find_matches()
if new_matches:
self.combo += 1
self.process_matches(new_matches)
else:
self.combo = 1
def drop_gems(self):
"""Drop gems to fill empty spaces"""
for x in range(self.grid_size):
# Compact column
write_pos = self.grid_size - 1
for y in range(self.grid_size - 1, -1, -1):
if self.grid[y][x] != -1:
if y != write_pos:
self.grid[write_pos][x] = self.grid[y][x]
self.grid[y][x] = -1
write_pos -= 1
def fill_empty(self):
"""Fill empty spaces with new gems"""
for y in range(self.grid_size):
for x in range(self.grid_size):
if self.grid[y][x] == -1:
self.grid[y][x] = random.randint(0, self.gem_types - 1)
def has_possible_moves(self) -> bool:
"""Check if there are any possible moves"""
for y in range(self.grid_size):
for x in range(self.grid_size):
# Try swapping with right neighbor
if x < self.grid_size - 1:
self.grid[y][x], self.grid[y][x+1] = self.grid[y][x+1], self.grid[y][x]
if self.find_matches():
self.grid[y][x], self.grid[y][x+1] = self.grid[y][x+1], self.grid[y][x]
return True
self.grid[y][x], self.grid[y][x+1] = self.grid[y][x+1], self.grid[y][x]
# Try swapping with bottom neighbor
if y < self.grid_size - 1:
self.grid[y][x], self.grid[y+1][x] = self.grid[y+1][x], self.grid[y][x]
if self.find_matches():
self.grid[y][x], self.grid[y+1][x] = self.grid[y+1][x], self.grid[y][x]
return True
self.grid[y][x], self.grid[y+1][x] = self.grid[y+1][x], self.grid[y][x]
return False
def shuffle_board(self):
"""Shuffle the board if no moves available"""
gems = []
for y in range(self.grid_size):
for x in range(self.grid_size):
gems.append(self.grid[y][x])
random.shuffle(gems)
i = 0
for y in range(self.grid_size):
for x in range(self.grid_size):
self.grid[y][x] = gems[i]
i += 1
# Sliding Puzzle Implementation
class SlidingPuzzle:
"""Sliding tile puzzle game"""
def __init__(self, size: int = 4):
self.size = size
self.grid = []
self.empty_pos = (size - 1, size - 1)
self.moves = 0
self.solved = False
self.initialize_puzzle()
self.shuffle()
def initialize_puzzle(self):
"""Create solved puzzle state"""
self.grid = []
num = 1
for y in range(self.size):
row = []
for x in range(self.size):
if y == self.size - 1 and x == self.size - 1:
row.append(0) # Empty tile
else:
row.append(num)
num += 1
self.grid.append(row)
self.empty_pos = (self.size - 1, self.size - 1)
def shuffle(self):
"""Shuffle puzzle with valid moves"""
for _ in range(100):
moves = self.get_valid_moves()
if moves:
move = random.choice(moves)
self.move_tile(move[0], move[1])
self.moves = 0
def get_valid_moves(self) -> List[Tuple[int, int]]:
"""Get list of valid tile positions that can move"""
moves = []
empty_y, empty_x = self.empty_pos
# Check adjacent tiles
for dy, dx in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
new_y, new_x = empty_y + dy, empty_x + dx
if 0 <= new_y < self.size and 0 <= new_x < self.size:
moves.append((new_y, new_x))
return moves
def move_tile(self, y: int, x: int) -> bool:
"""Move tile to empty position"""
empty_y, empty_x = self.empty_pos
# Check if adjacent to empty
if abs(y - empty_y) + abs(x - empty_x) != 1:
return False
# Swap tile with empty
self.grid[empty_y][empty_x] = self.grid[y][x]
self.grid[y][x] = 0
self.empty_pos = (y, x)
self.moves += 1
self.check_win()
return True
def check_win(self):
"""Check if puzzle is solved"""
expected = 1
for y in range(self.size):
for x in range(self.size):
if y == self.size - 1 and x == self.size - 1:
if self.grid[y][x] != 0:
return
else:
if self.grid[y][x] != expected:
return
expected += 1
self.solved = True
# Physics Puzzle Implementation
class PhysicsPuzzle:
"""Physics-based puzzle game"""
def __init__(self, screen_width: int, screen_height: int):
self.width = screen_width
self.height = screen_height
self.gravity = 0.5
self.objects = []
self.projectile = None
self.target = None
self.obstacles = []
self.solved = False
self.create_level()
def create_level(self):
"""Create a physics puzzle level"""
# Create target
self.target = {
'x': self.width - 100,
'y': self.height - 100,
'radius': 30,
'hit': False
}
# Create obstacles
self.obstacles = [
{
'x': self.width // 2,
'y': self.height - 200,
'width': 20,
'height': 150
},
{
'x': self.width // 3,
'y': self.height - 300,
'width': 100,
'height': 20
}
]
# Initialize projectile
self.reset_projectile()
def reset_projectile(self):
"""Reset projectile to launch position"""
self.projectile = {
'x': 50,
'y': self.height - 50,
'vx': 0,
'vy': 0,
'radius': 10,
'launched': False,
'trail': []
}
def launch(self, target_x: float, target_y: float):
"""Launch projectile towards target"""
if self.projectile['launched']:
return
# Calculate launch velocity
dx = target_x - self.projectile['x']
dy = target_y - self.projectile['y']
power = min(math.sqrt(dx**2 + dy**2) / 20, 15)
angle = math.atan2(dy, dx)
self.projectile['vx'] = math.cos(angle) * power
self.projectile['vy'] = math.sin(angle) * power
self.projectile['launched'] = True
def update(self):
"""Update physics simulation"""
if not self.projectile['launched'] or self.solved:
return
# Apply gravity
self.projectile['vy'] += self.gravity
# Update position
self.projectile['x'] += self.projectile['vx']
self.projectile['y'] += self.projectile['vy']
# Add to trail
self.projectile['trail'].append({
'x': self.projectile['x'],
'y': self.projectile['y']
})
if len(self.projectile['trail']) > 50:
self.projectile['trail'].pop(0)
# Check collisions with obstacles
for obstacle in self.obstacles:
if (self.projectile['x'] + self.projectile['radius'] > obstacle['x'] and
self.projectile['x'] - self.projectile['radius'] < obstacle['x'] + obstacle['width'] and
self.projectile['y'] + self.projectile['radius'] > obstacle['y'] and
self.projectile['y'] - self.projectile['radius'] < obstacle['y'] + obstacle['height']):
# Simple bounce
if abs(self.projectile['vx']) > abs(self.projectile['vy']):
self.projectile['vx'] *= -0.8
else:
self.projectile['vy'] *= -0.8
# Check collision with target
dx = self.projectile['x'] - self.target['x']
dy = self.projectile['y'] - self.target['y']
dist = math.sqrt(dx**2 + dy**2)
if dist < self.target['radius'] + self.projectile['radius']:
self.target['hit'] = True
self.solved = True
# Reset if out of bounds
if (self.projectile['y'] > self.height or
self.projectile['x'] < 0 or
self.projectile['x'] > self.width):
self.reset_projectile()
# Pattern Memory Puzzle
class PatternPuzzle:
"""Pattern memory puzzle game"""
def __init__(self, grid_size: int = 4):
self.grid_size = grid_size
self.pattern = []
self.player_pattern = []
self.showing_pattern = False
self.level = 1
self.score = 0
self.generate_pattern()
def generate_pattern(self):
"""Generate a new pattern to memorize"""
pattern_length = 3 + self.level
self.pattern = []
for _ in range(pattern_length):
self.pattern.append({
'x': random.randint(0, self.grid_size - 1),
'y': random.randint(0, self.grid_size - 1)
})
self.player_pattern = []
self.showing_pattern = True
def add_player_input(self, x: int, y: int):
"""Add player's input to pattern"""
if self.showing_pattern:
return
self.player_pattern.append({'x': x, 'y': y})
# Check if pattern is complete
if len(self.player_pattern) == len(self.pattern):
if self.check_pattern():
self.score += 100 * self.level
self.level += 1
self.generate_pattern()
else:
# Wrong pattern
self.player_pattern = []
def check_pattern(self) -> bool:
"""Check if player's pattern matches"""
for i in range(len(self.pattern)):
if (self.pattern[i]['x'] != self.player_pattern[i]['x'] or
self.pattern[i]['y'] != self.player_pattern[i]['y']):
return False
return True
# Logic Gates Puzzle
class LogicGate:
"""Single logic gate"""
def __init__(self, gate_type: str, x: int, y: int):
self.type = gate_type # AND, OR, NOT, XOR, NAND, NOR
self.x = x
self.y = y
self.inputs = []
self.output = False
self.connected_to = None
def evaluate(self):
"""Evaluate gate output based on inputs"""
if self.type == "AND":
self.output = all(self.inputs) if self.inputs else False
elif self.type == "OR":
self.output = any(self.inputs) if self.inputs else False
elif self.type == "NOT":
self.output = not self.inputs[0] if self.inputs else True
elif self.type == "XOR":
if len(self.inputs) == 2:
self.output = self.inputs[0] != self.inputs[1]
else:
self.output = False
elif self.type == "NAND":
self.output = not all(self.inputs) if self.inputs else True
elif self.type == "NOR":
self.output = not any(self.inputs) if self.inputs else True
class LogicPuzzle:
"""Logic gates puzzle game"""
def __init__(self):
self.gates = []
self.inputs = []
self.outputs = []
self.wires = []
self.target_output = []
self.solved = False
self.create_puzzle()
def create_puzzle(self):
"""Create a logic puzzle"""
# Create input switches
self.inputs = [
{'id': 'A', 'value': False, 'x': 50, 'y': 100},
{'id': 'B', 'value': False, 'x': 50, 'y': 200},
{'id': 'C', 'value': False, 'x': 50, 'y': 300}
]
# Create logic gates
self.gates = [
LogicGate("AND", 200, 150),
LogicGate("OR", 200, 250),
LogicGate("NOT", 350, 200)
]
# Set target output
self.target_output = [True] # Example target
# Create output
self.outputs = [
{'id': 'OUT', 'value': False, 'x': 500, 'y': 200}
]
def toggle_input(self, input_id: str):
"""Toggle input switch"""
for inp in self.inputs:
if inp['id'] == input_id:
inp['value'] = not inp['value']
self.evaluate_circuit()
break
def connect_wire(self, from_obj, to_obj):
"""Connect two objects with a wire"""
self.wires.append({
'from': from_obj,
'to': to_obj
})
self.evaluate_circuit()
def evaluate_circuit(self):
"""Evaluate the entire circuit"""
# Propagate signals through gates
for gate in self.gates:
gate.evaluate()
# Check if puzzle is solved
output_values = [out['value'] for out in self.outputs]
if output_values == self.target_output:
self.solved = True
# Main Puzzle Game Manager
class PuzzleGameManager:
"""Manages different puzzle types"""
def __init__(self, screen_width: int = 800, screen_height: int = 600):
pygame.init()
self.screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption("Puzzle Games")
self.clock = pygame.time.Clock()
self.width = screen_width
self.height = screen_height
# Initialize puzzles
self.puzzles = {
PuzzleType.MATCH3: Match3Game(),
PuzzleType.SLIDING: SlidingPuzzle(),
PuzzleType.PHYSICS: PhysicsPuzzle(screen_width, screen_height),
PuzzleType.PATTERN: PatternPuzzle(),
PuzzleType.LOGIC: LogicPuzzle()
}
self.current_puzzle = PuzzleType.MATCH3
self.active_puzzle = self.puzzles[self.current_puzzle]
# UI state
self.score = 0
self.time_elapsed = 0
self.moves = 0
def switch_puzzle(self, puzzle_type: PuzzleType):
"""Switch to a different puzzle type"""
self.current_puzzle = puzzle_type
self.active_puzzle = self.puzzles[puzzle_type]
self.score = 0
self.moves = 0
self.time_elapsed = 0
def handle_click(self, pos: Tuple[int, int]):
"""Handle mouse click"""
x, y = pos
if self.current_puzzle == PuzzleType.MATCH3:
# Convert to grid coordinates
grid_x = x // 60
grid_y = y // 60
if 0 <= grid_x < 8 and 0 <= grid_y < 8:
match3 = self.active_puzzle
if match3.selected:
# Try to swap
prev_x, prev_y = match3.selected
if match3.swap_gems(prev_x, prev_y, grid_x, grid_y):
self.score = match3.score
self.moves = match3.moves
match3.selected = None
else:
match3.selected = (grid_x, grid_y)
elif self.current_puzzle == PuzzleType.SLIDING:
# Convert to grid coordinates
grid_x = (x - 200) // 100
grid_y = (y - 100) // 100
if 0 <= grid_x < 4 and 0 <= grid_y < 4:
if self.active_puzzle.move_tile(grid_y, grid_x):
self.moves = self.active_puzzle.moves
if self.active_puzzle.solved:
self.score += 1000
elif self.current_puzzle == PuzzleType.PHYSICS:
self.active_puzzle.launch(x, y)
elif self.current_puzzle == PuzzleType.PATTERN:
grid_x = x // 100
grid_y = y // 100
if 0 <= grid_x < 4 and 0 <= grid_y < 4:
self.active_puzzle.add_player_input(grid_x, grid_y)
self.score = self.active_puzzle.score
def render_match3(self):
"""Render Match-3 puzzle"""
match3 = self.active_puzzle
for y in range(match3.grid_size):
for x in range(match3.grid_size):
gem_type = match3.grid[y][x]
if gem_type != -1:
color = match3.gem_colors[gem_type]
pygame.draw.circle(
self.screen,
color,
(x * 60 + 30, y * 60 + 30),
25
)
# Highlight selected
if match3.selected == (x, y):
pygame.draw.circle(
self.screen,
(255, 255, 255),
(x * 60 + 30, y * 60 + 30),
25,
3
)
def render_sliding(self):
"""Render sliding puzzle"""
puzzle = self.active_puzzle
for y in range(puzzle.size):
for x in range(puzzle.size):
value = puzzle.grid[y][x]
if value != 0:
rect = pygame.Rect(
200 + x * 100,
100 + y * 100,
95,
95
)
pygame.draw.rect(self.screen, (100, 150, 200), rect)
font = pygame.font.Font(None, 48)
text = font.render(str(value), True, (255, 255, 255))
text_rect = text.get_rect(center=rect.center)
self.screen.blit(text, text_rect)
def render_physics(self):
"""Render physics puzzle"""
physics = self.active_puzzle
# Draw target
color = (0, 255, 0) if physics.target['hit'] else (255, 0, 0)
pygame.draw.circle(
self.screen,
color,
(int(physics.target['x']), int(physics.target['y'])),
physics.target['radius']
)
# Draw obstacles
for obstacle in physics.obstacles:
pygame.draw.rect(
self.screen,
(128, 128, 128),
(obstacle['x'], obstacle['y'], obstacle['width'], obstacle['height'])
)
# Draw projectile
if physics.projectile:
pygame.draw.circle(
self.screen,
(255, 255, 0),
(int(physics.projectile['x']), int(physics.projectile['y'])),
physics.projectile['radius']
)
# Draw trail
for point in physics.projectile['trail']:
pygame.draw.circle(
self.screen,
(255, 255, 0, 128),
(int(point['x']), int(point['y'])),
2
)
def render_ui(self):
"""Render UI elements"""
font = pygame.font.Font(None, 36)
# Score
score_text = font.render(f"Score: {self.score}", True, (255, 255, 255))
self.screen.blit(score_text, (10, 10))
# Moves
moves_text = font.render(f"Moves: {self.moves}", True, (255, 255, 255))
self.screen.blit(moves_text, (10, 50))
# Current puzzle type
puzzle_text = font.render(f"Puzzle: {self.current_puzzle.value}", True, (255, 255, 255))
self.screen.blit(puzzle_text, (10, 90))
def run(self):
"""Main game loop"""
running = True
while running:
dt = self.clock.tick(60) / 1000.0
self.time_elapsed += dt
# Handle events
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1: # Left click
self.handle_click(event.pos)
elif event.type == pygame.KEYDOWN:
# Switch puzzles with number keys
if event.key == pygame.K_1:
self.switch_puzzle(PuzzleType.MATCH3)
elif event.key == pygame.K_2:
self.switch_puzzle(PuzzleType.SLIDING)
elif event.key == pygame.K_3:
self.switch_puzzle(PuzzleType.PHYSICS)
elif event.key == pygame.K_4:
self.switch_puzzle(PuzzleType.PATTERN)
elif event.key == pygame.K_5:
self.switch_puzzle(PuzzleType.LOGIC)
# Update
if self.current_puzzle == PuzzleType.PHYSICS:
self.active_puzzle.update()
# Render
self.screen.fill((30, 30, 50))
if self.current_puzzle == PuzzleType.MATCH3:
self.render_match3()
elif self.current_puzzle == PuzzleType.SLIDING:
self.render_sliding()
elif self.current_puzzle == PuzzleType.PHYSICS:
self.render_physics()
self.render_ui()
pygame.display.flip()
pygame.quit()
if __name__ == "__main__":
game = PuzzleGameManager()
game.run()
Key Features Implemented
✅ Complete Puzzle Systems
- Match-3: Gem swapping, match detection, cascading combos, score multipliers
- Sliding Puzzle: Tile movement, shuffle algorithm, win detection
- Physics Puzzle: Projectile physics, gravity, collision detection, trajectory calculation
- Pattern Memory: Sequence generation, pattern matching, difficulty progression
- Logic Gates: AND/OR/NOT gates, circuit evaluation, wire connections
- Game Manager: Puzzle switching, unified UI, score tracking
- Move Validation: Legal move checking for all puzzle types
- Shuffle System: Board randomization while maintaining solvability
Usage Instructions
🎮 How to Play
- Install Pygame:
pip install pygame - Run the game:
python puzzle_game.py - Controls:
- Number keys 1-5 to switch puzzle types
- Mouse click for all interactions
- Match-3: Click to select, click adjacent to swap
- Sliding: Click tile to move into empty space
- Physics: Click to launch projectile
Customization Options
🔧 Easy Modifications
- Grid Size: Change grid_size parameter in Match3Game and SlidingPuzzle
- Gem Types: Modify gem_types and gem_colors in Match3Game
- Physics Parameters: Adjust gravity, power, and bounce coefficients
- Pattern Difficulty: Change pattern_length calculation in PatternPuzzle
- Logic Gates: Add new gate types (XOR, NAND, NOR) in LogicGate
- Scoring: Modify score calculations for each puzzle type
🏋️♂️ Practice Exercise
🏋️♂️ Exercise 1: Three Pythonic Levers, One Demo — Enum Puzzle Identity + 2D-Array Board State + Recursive Cascade Fixed-Point in One Pygame Window
Objective: Build a runnable pygame program (~110 lines) that distills the lesson's class PuzzleType(Enum) + Match3Game / SlidingPuzzle / PhysicsPuzzle / PatternPuzzle / LogicPuzzle five-class architecture into one cohesive demo where three Python-specific puzzle disciplines are visible per frame on a 1088×540 window split into three labeled vertical panels: LEFT (10–190) renders the five PuzzleType Enum members as an identity selector with the active member highlighted in yellow and keys 1–5 binding to active = PuzzleType.MATCH3 through PuzzleType.LOGIC; MIDDLE (200–740) renders the active puzzle's 2D-array board through direct nested-list reads match3.grid[r][c] and sliding.grid[r][c]; RIGHT (750–1078) renders the recursive-cascade trace built by Match3.process(matches, depth) calling itself when find_matches() returns a non-empty set, with combo and depth counters reflecting the current call-stack frame and a rolling 15-line log of every recursive entry. (a) Enum-based puzzle-type identity at PUZZLE-IDENTITY scope: the dispatch keys ARE PuzzleType.MATCH3 through PuzzleType.LOGIC Enum members, not bare strings; a typo at the dispatch site like PuzzleType.MATC3 raises AttributeError at the import statement of the typo-containing module, while a bare-string typo like puzzles['matc3'] would silently return None at runtime when dispatch happens; mypy can also verify active: PuzzleType annotations at design time and reject any string literal that doesn't match a declared member. Same design-time-validation-vs-runtime-discovery pattern as chat-47 platformer_level_design's validate-vs-is_solid + chat-70 publishing_executables's hiddenimports static-AST-vs-runtime-discovery + chat-72 publishing_performance's profile-bars-vs-intuition + chat-73 publishing_platforms's try-import runtime-capability-detection applied at PUZZLE-IDENTITY scope = FIFTH lesson reinforcing the design-time-validation-vs-runtime-discovery pattern across Phase 8, FIRST applied at PUZZLE-IDENTITY scope. (b) 2D-array board state at BOARD-REPRESENTATION scope: Match3's self.grid is [[int]] 8×8 with values 0–4 plus -1 for cleared cells, and Sliding's self.grid is [[int]] 4×4 with values 0–15 (0 = empty slot); both grid-based puzzle classes share the nested-list-of-ints shape so swap is a two-line index-pair assignment, win-check is a flat scan, match-detect is two nested for-loops over (r, c), and gravity-drop is a single-column list comprehension [self.grid[r][c] for r in range(GRID) if self.grid[r][c] >= 0]; same data-driven externalization pattern as architecture_save_load schema chat-58 + pathfinding terrain-cost dict chat-60 + behavior-trees child-list-order chat-61 + decision-making personality dict chat-62 + procedural generation seed chat-68 + publishing_executables .spec file chat-70 + publishing_marketing pitch templates chat-71 + publishing_performance algorithmic-axis toggles chat-72 + publishing_platforms TARGETS+ADAPTERS dicts chat-73 + publishing_updates CADENCES+MIGRATIONS dicts chat-74 + genres_puzzle 2D-array board chat-75 applied at BOARD-REPRESENTATION scope = EIGHTEENTH lesson reinforcing data-driven externalization across Phase 8, REAFFIRMING chat-75's first BOARD-REPRESENTATION application now in Python where the from dataclasses import dataclass import is unused and the board IS raw [[int]]. (c) Recursive cascade fixed-point at MATCH-CASCADE scope: Match3.process(matches, depth=1) recursively calls itself with new = self.find_matches(); if new: self.combo += 1; self.process(new, depth + 1); the depth parameter threads the call-stack count through to the visualization so each recursive entry is visible as 'process_matches depth=N cleared=K' in the rolling log; same chained-contract pattern as chat-74 publishing_updates' MIGRATIONS chain (each migration step is a frozen contract that may produce a new state requiring another step) applied at MATCH-CASCADE scope where each recursive call is a frozen contract that may produce new matches requiring another call, expressed as Python-natural recursion rather than chat-75's iterative combo = 0; while self.step(): combo += 1 shape; the recursion terminates because clearing strictly reduces the count of non-cleared cells in the finite-state grid so a base case (find_matches() returns empty → no recursive call) must be reached. SPACE plants a deliberate horizontal triple in a random row and triggers a fresh cascade by calling find_matches() on the modified grid then entering recursion if matches exist.
Instructions:
- Set up a 1088×540 pygame window with three vertical panels at x=10–190 (Identity), x=200–740 (Board), x=750–1078 (Recursion Log).
- Define
class PuzzleType(Enum)with five membersMATCH3/SLIDING/PHYSICS/PATTERN/LOGICmatching the lesson's enum. - Define
Match3class withself.grid = [[random.randint(0, 4) for _ in range(8)] for _ in range(8)],find_matches()returning a set of (r, c) tuples by scanning rows then columns for triples,process(matches, depth=1)recursive method that clears matched cells, applies gravity via per-column list comprehension, and self-calls when new matches surface, andtrigger()that plants a deliberate horizontal triple then enters the recursion if matches exist. - Define
Slidingclass withself.grid4×4 holding shuffled 0–15 (0 = empty slot). - Build the
PUZZLES = {PuzzleType.MATCH3: Match3(), PuzzleType.SLIDING: Sliding(), ...}dict keyed by Enum members; anactivevariable tracks current PuzzleType. - In the event loop bind keys 1–5 to set
active = PuzzleType.MATCH3etc. via aKEYSdict that mapspygame.K_1throughpygame.K_5to Enum members, and bind SPACE to callPUZZLES[PuzzleType.MATCH3].trigger()when active is MATCH3. - Per frame render the three panels: LEFT iterates
for i, t in enumerate(PuzzleType)highlightingt == activein yellow; MIDDLE branches onactiverenderingmatch3.gridas colored 50×50 squares for cells withv >= 0andsliding.gridas numbered 90×90 tiles for cells withv != 0; RIGHT showsmatch3.combo,match3.depth, and the rolling 15-linematch3.login 22px line spacing when active is MATCH3.
💡 Hint
The three axes are independent levers visible in three different panels. The LEFT panel iterates for i, t in enumerate(PuzzleType) reading the Enum members directly — switching active is just rebinding active = PuzzleType.SLIDING at the keypress, and the dispatch in the MIDDLE and RIGHT panels reads active to decide what to render. The MIDDLE panel reads the 2D-array boards directly via match3.grid[r][c] and sliding.grid[r][c] — adding a sixth puzzle type with its own 2D-array board would not change how match3 and sliding render. The RIGHT panel's depth counter is set by the depth parameter that recursive process() threads through itself — when SPACE is pressed and find_matches() returns non-empty, process(matches, 1) runs its body once at depth 1 then if find_matches() returns non-empty again calls process(next_matches, 2), and so on until a base case is reached (find_matches() returns empty) — the call stack IS the combo counter without any explicit accumulator outside the recursion.
✅ Example Solution
import pygame, random, sys
from enum import Enum
W, H = 1088, 540
GRID, CELL = 8, 50
class PuzzleType(Enum):
MATCH3 = "match3"
SLIDING = "sliding"
PHYSICS = "physics"
PATTERN = "pattern"
LOGIC = "logic"
class Match3:
def __init__(self):
self.grid = [[random.randint(0, 4) for _ in range(GRID)] for _ in range(GRID)]
self.combo = 0
self.depth = 0
self.log = []
def find_matches(self):
m = set()
for r in range(GRID):
for c in range(GRID - 2):
v = self.grid[r][c]
if v >= 0 and v == self.grid[r][c+1] == self.grid[r][c+2]:
m.update([(r, c), (r, c+1), (r, c+2)])
for c in range(GRID):
for r in range(GRID - 2):
v = self.grid[r][c]
if v >= 0 and v == self.grid[r+1][c] == self.grid[r+2][c]:
m.update([(r, c), (r+1, c), (r+2, c)])
return m
def process(self, matches, depth=1):
self.depth = depth
self.log.append(f"process_matches depth={depth} cleared={len(matches)}")
for r, c in matches:
self.grid[r][c] = -1
for c in range(GRID):
col = [self.grid[r][c] for r in range(GRID) if self.grid[r][c] >= 0]
col = [random.randint(0, 4) for _ in range(GRID - len(col))] + col
for r in range(GRID):
self.grid[r][c] = col[r]
new = self.find_matches()
if new:
self.combo += 1
self.process(new, depth + 1)
def trigger(self):
r = random.randint(0, GRID - 1)
c = random.randint(0, GRID - 3)
v = random.randint(0, 4)
self.grid[r][c] = self.grid[r][c+1] = self.grid[r][c+2] = v
self.combo, self.log = 1, []
m = self.find_matches()
if m:
self.process(m, 1)
class Sliding:
def __init__(self):
n = list(range(1, 16)) + [0]
random.shuffle(n)
self.grid = [n[i*4:(i+1)*4] for i in range(4)]
class Physics: pass
class Pattern: pass
class Logic: pass
PUZZLES = {
PuzzleType.MATCH3: Match3(),
PuzzleType.SLIDING: Sliding(),
PuzzleType.PHYSICS: Physics(),
PuzzleType.PATTERN: Pattern(),
PuzzleType.LOGIC: Logic(),
}
COLORS = [(255,80,80),(80,200,80),(80,160,255),(240,200,60),(200,80,255)]
pygame.init()
scr = pygame.display.set_mode((W, H))
font = pygame.font.SysFont(None, 22)
big = pygame.font.SysFont(None, 28)
clk = pygame.time.Clock()
active = PuzzleType.MATCH3
KEYS = {pygame.K_1: PuzzleType.MATCH3, pygame.K_2: PuzzleType.SLIDING,
pygame.K_3: PuzzleType.PHYSICS, pygame.K_4: PuzzleType.PATTERN,
pygame.K_5: PuzzleType.LOGIC}
run = True
while run:
for e in pygame.event.get():
if e.type == pygame.QUIT:
run = False
elif e.type == pygame.KEYDOWN:
if e.key in KEYS:
active = KEYS[e.key]
elif e.key == pygame.K_SPACE and active == PuzzleType.MATCH3:
PUZZLES[PuzzleType.MATCH3].trigger()
scr.fill((20, 22, 30))
pygame.draw.rect(scr, (40, 44, 55), (10, 10, 180, 520))
scr.blit(big.render("Identity", True, (255,255,255)), (20, 18))
for i, t in enumerate(PuzzleType):
col = (255, 220, 80) if t == active else (180, 180, 180)
scr.blit(font.render(f"{i+1}: {t.name}", True, col), (20, 60 + i*30))
pygame.draw.rect(scr, (40, 44, 55), (200, 10, 540, 520))
scr.blit(big.render(f"Board ({active.name})", True, (255,255,255)), (210, 18))
if active == PuzzleType.MATCH3:
m3 = PUZZLES[PuzzleType.MATCH3]
for r in range(GRID):
for c in range(GRID):
v = m3.grid[r][c]
if v >= 0:
pygame.draw.rect(scr, COLORS[v], (220+c*CELL, 60+r*CELL, CELL-4, CELL-4))
elif active == PuzzleType.SLIDING:
sl = PUZZLES[PuzzleType.SLIDING]
for r in range(4):
for c in range(4):
v = sl.grid[r][c]
if v != 0:
pygame.draw.rect(scr, (80, 160, 255), (260+c*100, 90+r*100, 90, 90))
scr.blit(big.render(str(v), True, (255,255,255)), (260+c*100+30, 90+r*100+30))
pygame.draw.rect(scr, (40, 44, 55), (750, 10, 328, 520))
scr.blit(big.render("Recursion Log", True, (255,255,255)), (760, 18))
if active == PuzzleType.MATCH3:
m3 = PUZZLES[PuzzleType.MATCH3]
scr.blit(font.render(f"combo: {m3.combo}", True, (255,220,80)), (760, 55))
scr.blit(font.render(f"depth: {m3.depth}", True, (255,220,80)), (760, 80))
for i, line in enumerate(m3.log[-15:]):
scr.blit(font.render(line, True, (200,200,200)), (760, 110+i*22))
pygame.display.flip()
clk.tick(60)
pygame.quit()
sys.exit()
🎯 Quick Quiz
Question 1: The lesson's class PuzzleType(Enum) declares MATCH3/SLIDING/PHYSICS/PATTERN/LOGIC and the manager's puzzles dict is keyed by these Enum members rather than by bare strings 'match3'/'sliding'/etc. as in chat-75's JS implementation. What is the architectural reason for choosing Enum keys over string keys for puzzle identifiers?
Question 2: Match3.grid is a nested list of ints (8×8 with values 0–4 plus -1 for cleared cells) and Sliding.grid is a nested list of ints (4×4 with values 0–15 with 0 = empty). Both grid-based puzzle classes use the same [[int]] shape. What is the architectural reason for choosing raw nested lists rather than parallel Cell objects with row/col/color attributes?
Question 3: Match3.process calls itself recursively when find_matches() returns non-empty: new = self.find_matches(); if new: self.combo += 1; self.process(new, depth + 1). The chat-75 JS implementation expressed the same fixed-point iteration as combo = 0; while self.step(): combo += 1. What is the architectural reason this pygame demo expresses the cascade as recursion rather than as iteration?