Puzzle Game Logic
Building Engaging Puzzle Game Mechanics
Master puzzle game design! Create match-3 mechanics, sliding puzzles, physics-based challenges, pattern matching, and logic gates! 🧩💎🔮
Understanding Puzzle Game Systems
🎯 Core Puzzle Mechanics
Great puzzle games combine simple rules with emergent complexity:
- Match-3: Pattern matching and cascading combos
- Sliding Puzzles: Spatial reasoning and planning
- Physics Puzzles: Real-world mechanics and experimentation
- Logic Puzzles: Deduction and systematic thinking
- Pattern Memory: Observation and recall
- Word Puzzles: Language and vocabulary
Interactive Multi-Puzzle Demo
Experience different puzzle mechanics - Match gems, slide tiles, launch projectiles, and solve logic gates!
Select Puzzle Type:
Match-3: Click to select, click adjacent gem to swap
Core Puzzle Types Explained
🎮 Essential Puzzle Mechanics
1. Match-3 Mechanics
- Grid-based gem/tile matching
- Gravity and cascading effects
- Special tiles and power-ups
- Combo multipliers
2. Sliding Puzzles
- 15-puzzle and variants
- Optimal move counting
- Solvability detection
- A* pathfinding for hints
3. Physics Puzzles
- Projectile mechanics
- Collision detection
- Force and momentum
- Environmental interactions
4. Pattern Matching
- Sequence memorization
- Progressive difficulty
- Visual and audio cues
- Time-based challenges
5. Logic Gates
- Boolean operations
- Circuit simulation
- Truth tables
- Complex gate combinations
Puzzle Game Implementation in Python
🐍 Complete Python Implementation
For the full Python implementation with Pygame including all puzzle types, advanced features, and procedural generation, Python code.
Key Implementation Features:
- Object-oriented puzzle system
- Modular game states
- Animation framework
- Score and combo system
- Level progression
- Save/load functionality
- Procedural puzzle generation
- Hint and auto-solve algorithms
Best Practices
✨ Puzzle Game Best Practices
- Clear Rules: Players should understand mechanics immediately
- Progressive Difficulty: Smooth learning curve
- Visual Feedback: Immediate response to actions
- No Dead Ends: Always have possible moves
- Hint Systems: Help stuck players
- Satisfying Effects: Reward successful moves
- Multiple Solutions: Allow creative problem solving
- Undo Feature: Reduce frustration
Key Takeaways
- 💎 Match-3 mechanics create addictive gameplay
- 🧩 Sliding puzzles test spatial reasoning
- ⚙️ Physics puzzles reward experimentation
- 🔌 Logic puzzles teach systematic thinking
- 🎨 Pattern games improve memory
- 📈 Difficulty progression keeps players engaged
- ✨ Polish and juice make puzzles satisfying
- 🔄 Procedural generation adds replayability
🏋️♂️ Practice Exercise
🏋️♂️ Exercise 1: Five Genres, One Interface — Polymorphic Puzzle-Genre Dispatch + 2D-Array Board State + Match-Cascade Fixed-Point Loop in One Pygame Window
Objective: Build a runnable pygame window in roughly 95 lines that shows three orthogonal puzzle-game disciplines visible per frame on a 1088×540 window with three labeled regions: LEFT panel renders a 5-row genre selector with the active genre highlighted; MIDDLE panel renders the active puzzle's board state via the same render(surface, ox, oy) method on every genre class; RIGHT panel shows the live combo counter and a rolling 8-line dispatch log of every event routed through the polymorphic interface. (a) Polymorphic puzzle-genre dispatch at PUZZLE-GENRE scope — a PUZZLES = {'match3': Match3(), 'sliding': Sliding(), 'physics': Physics(), 'pattern': Pattern(), 'logic': Logic()} dict plus a uniform handle_click / step / render interface across all five classes lets the controller call PUZZLES[active].handle_click(mx, my) and PUZZLES[active].render(screen, ox, oy) with zero if active == 'match3': ... elif active == 'sliding': ... branches; adding a sixth genre (e.g., word puzzles) is one new class plus one new dict entry with no controller edits. Same UIElement-protocol pattern from chat-67 graphics_ui_hud and PlatformAdapter pattern from chat-73 publishing_platforms applied at PUZZLE-GENRE scope = SECOND polymorphic-dispatch lesson reinforcing the shared-protocol pattern at a new domain scope. (b) Grid-state-as-2D-array at BOARD-REPRESENTATION scope — match-3 stores its 8×8 board as grid[r][c] = color_int with values 0–5 plus −1 for cleared cells, and sliding stores its 4×4 board as grid[r][c] = number with values 1–15 plus 0 for the empty cell; both genres share the same 2D-array shape so swap (match-3) and slide (sliding) are both two-line index-pair assignments, win-check is a flat scan, match-detect is two nested for-loops over (r, c), and serializing the board for save/undo is one json.dumps call. 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 dict (chat-73), and publishing_updates CADENCES + MIGRATIONS dicts (chat-74) applied at BOARD-REPRESENTATION scope = SEVENTEENTH lesson reinforcing data-driven externalization across Phase 8, FIRST applied at BOARD-REPRESENTATION scope where the board IS the data the puzzle's algorithms (swap, slide, match-detect, cascade) read and mutate. (c) Match-cascade-as-fixed-point-loop at MATCH-CASCADE scope — match-3's handle_click ends with combo = 0; while self.step(): combo += 1 where step() performs {detect runs of 3+ → mark cleared cells → drop survivors → spawn new from top → return True if any change happened, False if state stabilized}; the loop terminates when no further matches exist and the combo multiplier IS the iteration count without any extra accumulator. Same chained-contract pattern as chat-74 publishing_updates' MIGRATIONS dict (each iteration is a frozen contract that may produce a new state requiring another iteration) applied at MATCH-CASCADE scope. Keys 1/2/3/4/5 switch the active genre; mouse clicks dispatch through the active class via PUZZLES[active].handle_click(...); HUD shows combo counter and dispatch log — three orthogonal puzzle-game disciplines visible per frame as concrete dict entries, board values, and iteration counts.
Instructions:
- Open a 1088×540 pygame window with three labeled regions (LEFT genre selector at x=10–190, MIDDLE puzzle board at x=200–760, RIGHT combo+log at x=770–1078).
- Define five puzzle classes Match3 / Sliding / Physics / Pattern / Logic, each implementing the same
handle_click(local_x, local_y)andrender(surf, ox, oy)method names, plusstep()on every class returning True if a cascade-style state change occurred (only Match3 returns True; the other four always return False). - Store the match-3 board as a 2D list
grid[r][c]with int values 0–5 (color index) and −1 for cleared cells; store the sliding board as a 2D listgrid[r][c]with int values 0–15 (0 = empty) — same 2D-array shape, different value domain. - In
Match3.step(), scan the grid for horizontal and vertical runs of 3+ same color, mark them, set marked cells to −1, drop surviving cells with column-rebuilding, spawn random colors into the empty slots at the top of each column, return True if any cells were marked this pass and False otherwise. - In
Match3.handle_click, after a valid adjacent swap, runcombo = 0; while self.step(): combo += 1and store the finalcomboasself.last_combofor the HUD to display — the combo multiplier IS the number of cascade iterations. - Build a controller dict
PUZZLES = {'match3': Match3(), 'sliding': Sliding(), 'physics': Physics(), 'pattern': Pattern(), 'logic': Logic()}and dispatch every controller event withPUZZLES[active].method(...)— never branch on theactivestring anywhere in the controller. - On keys 1–5 switch
activeto the corresponding genre name and append a 'switch <name>' line to the rolling 8-line log; on mouse click in the middle panel, dispatch throughPUZZLES[active].handle_click(mx-200, my-60)and append a 'click on <name>' line.
💡 Hint
The three axes are independent levers: the dispatch dict (axis a) controls WHICH class handles an event with no controller branching; the 2D-array board shape (axis b) controls HOW each board's state is stored and gives match-3 and sliding the same algorithmic primitive (index-pair assignment) for swap and slide; the fixed-point cascade loop (axis c) lets match-3 run multi-step combos with the iteration count itself acting as the combo multiplier without any extra accumulator. Keep the controller code (event loop + render orchestrator) free of any if active == '...' branches — every controller-side operation goes through PUZZLES[active].method(...). The step() method's return value is the loop's halting condition AND the source of the combo count: the loop runs until step() returns False, and every True return increments the combo by exactly one.
✅ Example Solution
import pygame, sys, random
pygame.init()
W, H = 1088, 540
screen = pygame.display.set_mode((W, H))
pygame.display.set_caption('Five Genres, One Interface')
font = pygame.font.SysFont('consolas', 14)
big = pygame.font.SysFont('consolas', 22, bold=True)
clock = pygame.time.Clock()
COLORS = [(220,80,80),(80,200,80),(80,140,220),(220,200,80),(200,80,200),(80,200,200)]
class Match3:
def __init__(self):
self.grid = [[random.randint(0,5) for _ in range(8)] for _ in range(8)]
self.sel = None; self.last_combo = 0
def handle_click(self, lx, ly):
c, r = lx // 40, ly // 40
if not (0 <= c < 8 and 0 <= r < 8): return
if self.sel is None: self.sel = (r, c); return
r0, c0 = self.sel; self.sel = None
if abs(r0-r) + abs(c0-c) != 1: return
self.grid[r0][c0], self.grid[r][c] = self.grid[r][c], self.grid[r0][c0]
combo = 0
while self.step(): combo += 1
self.last_combo = combo
def step(self):
m = [[False]*8 for _ in range(8)]; hit = False
for r in range(8):
for c in range(6):
v = self.grid[r][c]
if v >= 0 and v == self.grid[r][c+1] == self.grid[r][c+2]:
m[r][c] = m[r][c+1] = m[r][c+2] = True; hit = True
for c in range(8):
for r in range(6):
v = self.grid[r][c]
if v >= 0 and v == self.grid[r+1][c] == self.grid[r+2][c]:
m[r][c] = m[r+1][c] = m[r+2][c] = True; hit = True
if not hit: return False
for r in range(8):
for c in range(8):
if m[r][c]: self.grid[r][c] = -1
for c in range(8):
col = [self.grid[r][c] for r in range(8) if self.grid[r][c] != -1]
col = [-1]*(8-len(col)) + col
for r in range(8): self.grid[r][c] = col[r]
for r in range(8):
for c in range(8):
if self.grid[r][c] == -1: self.grid[r][c] = random.randint(0,5)
return True
def render(self, surf, ox, oy):
for r in range(8):
for c in range(8):
v = self.grid[r][c]
col = COLORS[v%6] if v >= 0 else (40,40,40)
pygame.draw.rect(surf, col, (ox+c*40+1, oy+r*40+1, 38, 38))
if self.sel:
r, c = self.sel
pygame.draw.rect(surf, (255,255,255), (ox+c*40, oy+r*40, 40, 40), 2)
class Sliding:
def __init__(self):
self.grid = [[r*4+c for c in range(4)] for r in range(4)]
for _ in range(100): self._slide_random()
self.last_combo = 0
def _empty(self):
for r in range(4):
for c in range(4):
if self.grid[r][c] == 0: return r, c
def _slide_random(self):
er, ec = self._empty()
opts = [(r,c) for r,c in [(er-1,ec),(er+1,ec),(er,ec-1),(er,ec+1)] if 0<=r<4 and 0<=c<4]
nr, nc = random.choice(opts)
self.grid[er][ec], self.grid[nr][nc] = self.grid[nr][nc], self.grid[er][ec]
def handle_click(self, lx, ly):
c, r = lx // 80, ly // 80
if not (0<=c<4 and 0<=r<4): return
er, ec = self._empty()
if abs(er-r) + abs(ec-c) == 1:
self.grid[er][ec], self.grid[r][c] = self.grid[r][c], self.grid[er][ec]
def step(self): return False
def render(self, surf, ox, oy):
for r in range(4):
for c in range(4):
v = self.grid[r][c]
col = (40,40,40) if v == 0 else (100,160,220)
pygame.draw.rect(surf, col, (ox+c*80+1, oy+r*80+1, 78, 78))
if v != 0:
surf.blit(big.render(str(v), True, (255,255,255)), (ox+c*80+28, oy+r*80+28))
class Physics:
def __init__(self):
self.x, self.y, self.vx, self.vy, self.flying, self.last_combo = 50, 250, 0, 0, False, 0
def handle_click(self, lx, ly):
if not self.flying:
self.vx, self.vy, self.flying = (lx-50)*0.05, (ly-250)*0.05, True
def step(self): return False
def update(self):
if self.flying:
self.x += self.vx; self.y += self.vy; self.vy += 0.3
if self.y > 480 or self.x > 540 or self.x < 0:
self.x, self.y, self.vx, self.vy, self.flying = 50, 250, 0, 0, False
def render(self, surf, ox, oy):
pygame.draw.circle(surf, (255,200,80), (ox+int(self.x), oy+int(self.y)), 12)
for tx, ty in [(300, 100),(450, 200),(380, 350)]:
pygame.draw.circle(surf, (220,80,80), (ox+tx, oy+ty), 16, 2)
class Pattern:
def __init__(self):
self.seq = [random.randint(0,3)]; self.user = []; self.last_combo = 0
def handle_click(self, lx, ly):
c = lx // 135
if not (0 <= c < 4): return
self.user.append(c)
if len(self.user) == len(self.seq):
if self.user == self.seq:
self.seq.append(random.randint(0,3)); self.last_combo = len(self.seq)-1
else:
self.seq = [random.randint(0,3)]; self.last_combo = 0
self.user = []
def step(self): return False
def render(self, surf, ox, oy):
cols = [(220,80,80),(80,200,80),(80,140,220),(220,200,80)]
for i in range(4):
pygame.draw.rect(surf, cols[i], (ox+i*135+5, oy+200, 125, 80))
surf.blit(font.render(f'len(seq)={len(self.seq)} progress={len(self.user)}', True, (255,255,255)), (ox+5, oy+5))
class Logic:
def __init__(self): self.inp = [False, False, False]; self.last_combo = 0
def handle_click(self, lx, ly):
for i in range(3):
if (lx-80)**2 + (ly-100-i*100)**2 < 400: self.inp[i] = not self.inp[i]
def step(self): return False
def render(self, surf, ox, oy):
for i in range(3):
col = (80,200,80) if self.inp[i] else (60,60,60)
pygame.draw.circle(surf, col, (ox+80, oy+100+i*100), 20)
surf.blit(font.render(['A','B','C'][i], True, (255,255,255)), (ox+72, oy+92+i*100))
out = self.inp[0] and self.inp[1] and self.inp[2]
pygame.draw.rect(surf, (60,60,60), (ox+200, oy+170, 100, 60))
surf.blit(big.render('AND', True, (255,255,255)), (ox+220, oy+185))
pygame.draw.circle(surf, (80,200,80) if out else (60,60,60), (ox+360, oy+200), 25)
PUZZLES = {'match3': Match3(), 'sliding': Sliding(), 'physics': Physics(), 'pattern': Pattern(), 'logic': Logic()}
NAMES = ['match3', 'sliding', 'physics', 'pattern', 'logic']
active = 'match3'; log = []
running = True
while running:
for e in pygame.event.get():
if e.type == pygame.QUIT: running = False
elif e.type == pygame.KEYDOWN and pygame.K_1 <= e.key <= pygame.K_5:
active = NAMES[e.key - pygame.K_1]; log.append(f'switch {active}')
elif e.type == pygame.MOUSEBUTTONDOWN:
mx, my = e.pos
if 200 <= mx <= 760 and 60 <= my <= 480:
PUZZLES[active].handle_click(mx-200, my-60); log.append(f'click on {active}')
if hasattr(PUZZLES[active], 'update'): PUZZLES[active].update()
log = log[-8:]
screen.fill((20,20,30))
for i, n in enumerate(NAMES):
col = (80,160,220) if n == active else (60,60,60)
pygame.draw.rect(screen, col, (10, 60+i*60, 180, 50))
screen.blit(big.render(f'{i+1}: {n}', True, (255,255,255)), (20, 75+i*60))
pygame.draw.rect(screen, (10,10,15), (200, 60, 560, 420))
PUZZLES[active].render(screen, 200, 60)
pygame.draw.rect(screen, (10,10,15), (770, 60, 308, 420))
screen.blit(big.render(f'combo: {PUZZLES[active].last_combo}', True, (255,255,255)), (780, 70))
for i, line in enumerate(log):
screen.blit(font.render(line, True, (200,200,200)), (780, 120+i*20))
screen.blit(big.render('Five Genres, One Interface — keys 1-5 switch genre', True, (255,255,255)), (10, 10))
pygame.display.flip()
clock.tick(60)
pygame.quit(); sys.exit()
🎯 Quick Quiz
Question 1: The controller dispatches every event with PUZZLES[active].handle_click(...) and PUZZLES[active].render(...) rather than branching if active == 'match3': match3.handle_click(...) elif active == 'sliding': sliding.handle_click(...) .... Why is the dict-dispatch approach the architectural fit at PUZZLE-GENRE scope?
Question 2: Both Match3 and Sliding store their boards as grid[r][c] = int 2D arrays — match-3 with values 0–5 plus −1 for cleared cells on an 8×8 board, sliding with values 0–15 (0 = empty) on a 4×4 board. Why is the 2D-array shape the architectural fit at BOARD-REPRESENTATION scope?
Question 3: After a valid match-3 swap, the code runs combo = 0 then while self.step(): combo += 1, where step() performs {detect runs of 3+ → mark cleared → drop survivors → spawn new from top → return True if any change happened, False if state stabilized}. Why is the fixed-point loop with iteration-count-as-combo-multiplier the architectural fit at MATCH-CASCADE scope?
What's Next?
Now that you've mastered puzzle game logic, next we'll explore racing game physics!