Table of Contents
- The Frog and the Ox: Why I Started This Project
- Why Tower of Hanoi?
- Getting Started with Amazon Q Developer CLI
- The Zero-Shot Miracle
- The Design Challenge
- The Material Design Evolution
- The Final Polish: Advanced Zero-Shot
- Technical Insights
- Code Architecture Highlights
- Performance and User Experience
- Reflections on AI-Assisted Development
- Conclusion: The Frog's Success
- Try It Yourself
Building Tower of Hanoi with Amazon Q Developer CLI: A Journey from Zero-Shot to Polished Game
The Frog and the Ox: Why I Started This Project
A few days ago, I stumbled upon an AWS Community post about the Build Games Challenge using Amazon Q Developer CLI. It reminded me of a Bulgarian proverb: "The frog saw the ox being shod and lifted her leg too." This saying captures the essence of someone copying others blindly, trying to be part of something they clearly don't belong to—like a frog thinking she's an ox just because she saw the ox getting horseshoes.
That's exactly why I decided to build the Tower of Hanoi game and participate in the challenge. Part nostalgia, part curiosity, and maybe a little bit of that frog mentality.
Why Tower of Hanoi?
Tower of Hanoi holds a special place in my programming journey. I first coded it back in university, and it's stuck with me ever since. The game's elegant simplicity masks its mathematical complexity—moving a set of disks from one pole to another while following just a few rules:
- Three rods/poles total
- Move only one disk at a time
- Never place a larger disk on top of a smaller one
The mathematical beauty lies in its solution: the minimum number of moves needed is 2^n - 1, where n is the number of disks.
This time, I wanted to bring it to life in a new way—powered by Amazon Q Developer CLI, without the hard memories of Java classes that haunted my university days.
Getting Started with Amazon Q Developer CLI
Setting up Amazon Q Developer CLI is refreshingly simple. If you're on macOS with Homebrew:
brew install amazon-q
Once installed, just type q
in your terminal to start chatting with your AI coding companion.
The Zero-Shot Miracle
My first prompt was deliberately naive:
help me build tower of hanoi using pygame framework
The results absolutely stunned me. Without any examples, rules, or detailed specifications, Amazon Q Developer CLI generated a fully functional Tower of Hanoi game using pygame. Zero-shot prompting at its finest.
The First Success
The initial game was surprisingly complete:
- ✅ Interactive gameplay with mouse controls
- ✅ Visual feedback and animations
- ✅ Rules enforcement
- ✅ Move counter
- ✅ Auto-solve functionality
- ✅ Variable disk counts
Here's what that first iteration looked like:
Clean, functional, but lacking visual polish
The Design Challenge
Emboldened by this success, I decided to push further. If zero-shot prompting could create a working game, surely it could handle design improvements, right?
My next prompt:
make it more polished using modern ui design patterns like material design
Plot twist: This didn't work as smoothly. The generated code was incomplete, cutting off mid-function. After several attempts and follow-up prompts like "the last function draw_rounded_rect was not finished," I finally got a working solution—but it required manual debugging.
Lessons Learned
- Zero-shot works best for complete, well-defined tasks
- Incremental changes can be trickier than starting fresh
- Always review and test AI-generated code
- Be prepared to debug and iterate
The Material Design Evolution
After some back-and-forth, I achieved a much more polished version with Material Design principles:
Modern Material Design aesthetic with elevated cards, shadows, and proper color palette
Key Material Design Elements Added:
- Elevated surfaces with subtle shadows
- Material color palette (Blue 500, Orange 500, etc.)
- Rounded corners and proper spacing
- Card-based UI for controls
- Visual hierarchy with proper typography
- Hover states and interactive feedback
The Final Polish: Advanced Zero-Shot
For my final iteration, I crafted a comprehensive prompt that specified exactly what I wanted:
Create a complete, polished Tower of Hanoi game using Python Pygame framework, styled according to Google's Material Design principles. Include:
- Visually appealing, responsive UI with Material Design elements
- Smooth animations and visual feedback
- Drag-and-drop or click-to-move functionality
- Rules enforcement and user-friendly error handling
- Move counter, elapsed timer, and reset functionality
- Level selection (3-7 disks)
- Light/dark theme toggle
- Modular, well-documented OOP code
- Material Design color palettes and shadows
This comprehensive prompt yielded the most impressive result:
The final version with enhanced responsiveness and refined interactions
New Features in the Final Version:
- Enhanced mouse interactions with better responsiveness
- Improved visual feedback for user actions
- Smoother animations and transitions
- Better error handling and edge case management
- More intuitive UI with clearer visual hierarchy
Technical Insights
What Amazon Q Developer CLI Excelled At:
- Complete game logic implementation
- Pygame framework integration
- Object-oriented design patterns
- Mathematical algorithm implementation (recursive Hanoi solver)
- Event handling and game state management
Where It Struggled:
- Incremental design changes to existing code
- Complex prompt continuation when code was cut off
- Fine-tuning visual details without explicit guidance
The Sweet Spot:
The most effective approach was comprehensive, specific prompts that clearly defined the entire scope of what I wanted. This worked better than trying to modify existing code piecemeal.
Code Architecture Highlights
The final implementation showcased excellent software engineering practices:
Key architectural decisions:
- Separation of concerns between game logic and rendering
- Event-driven architecture for user interactions
- State management for game progression
- Modular design for easy extensibility
Performance and User Experience
The final game delivers on multiple fronts:
- Responsive controls with immediate visual feedback
- Intuitive interface following Material Design principles
- Accessibility considerations with clear visual hierarchy
- Error prevention through smart UI design
Reflections on AI-Assisted Development
This project revealed fascinating insights about working with AI coding assistants:
The Good:
- Rapid prototyping from concept to working game
- Best practices implementation without explicit instruction
- Complex algorithm generation (recursive Hanoi solver)
- Framework expertise beyond what I could have written alone
The Challenging:
- Iteration difficulties when making incremental changes
- Need for human oversight and debugging
- Prompt engineering importance for optimal results
The Surprising:
- Zero-shot capability exceeded expectations for complete tasks
- Code quality was consistently high and well-structured
- Documentation and comments were thoughtfully included
Conclusion: The Frog's Success
Looking back at that Bulgarian proverb, maybe sometimes it's okay to be the frog lifting her leg when she sees the ox being shod. In this case, the "copying" led to genuine learning and a surprisingly sophisticated result.
Amazon Q Developer CLI proved to be an impressive coding companion, especially for:
- Complete project generation from clear specifications
- Framework-specific implementations with best practices
- Algorithm implementation with proper optimization
The key lesson? Be specific, be comprehensive, and be prepared to iterate. The most successful prompts were those that painted a complete picture of the desired outcome rather than asking for incremental modifications.
Try It Yourself
Want to experiment with Amazon Q Developer CLI? Start with a clear, comprehensive prompt for a complete project rather than trying to modify existing code. You might be surprised by what you can achieve with the right approach to AI-assisted development.
The Tower of Hanoi might be an ancient puzzle, but building it with modern AI tools offers fresh insights into both game development and the evolving landscape of programming assistance.
Have you tried building games with AI coding assistants? What was your experience? Share your own "frog and ox" moments in the comments below.
Code initial version:
import pygame
import sys
import time
# Initialize pygame
pygame.init()
# Constants
WIDTH, HEIGHT = 800, 600
DISK_HEIGHT = 20
MAX_DISKS = 5
ANIMATION_SPEED = 5
# Colors
BACKGROUND = (50, 50, 50)
TOWER_COLOR = (139, 69, 19)
DISK_COLORS = [
(255, 0, 0), # Red
(255, 165, 0), # Orange
(255, 255, 0), # Yellow
(0, 255, 0), # Green
(0, 0, 255), # Blue
]
# Set up the display
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Tower of Hanoi")
clock = pygame.time.Clock()
class Disk:
def __init__(self, size, color):
self.size = size
self.color = color
self.x = 0
self.y = 0
self.moving = False
self.target_x = 0
self.target_y = 0
def draw(self):
width = (self.size + 1) * 30
pygame.draw.rect(screen, self.color, (self.x - width // 2, self.y - DISK_HEIGHT // 2, width, DISK_HEIGHT), 0, 5)
def move_towards_target(self):
dx = self.target_x - self.x
dy = self.target_y - self.y
if abs(dx) < ANIMATION_SPEED and abs(dy) < ANIMATION_SPEED:
self.x = self.target_x
self.y = self.target_y
self.moving = False
return True
if abs(dx) > 0:
self.x += ANIMATION_SPEED if dx > 0 else -ANIMATION_SPEED
if abs(dy) > 0:
self.y += ANIMATION_SPEED if dy > 0 else -ANIMATION_SPEED
return False
class Tower:
def __init__(self, x):
self.x = x
self.y = HEIGHT - 100
self.disks = []
def draw(self):
# Draw tower base
pygame.draw.rect(screen, TOWER_COLOR, (self.x - 10, self.y, 20, 20))
pygame.draw.rect(screen, TOWER_COLOR, (self.x - 50, self.y + 20, 100, 10))
# Draw tower pole
pygame.draw.rect(screen, TOWER_COLOR, (self.x - 5, self.y - 200, 10, 200))
def add_disk(self, disk):
disk_y = self.y - len(self.disks) * DISK_HEIGHT - DISK_HEIGHT // 2
disk.x = self.x
disk.y = disk_y
self.disks.append(disk)
def remove_top_disk(self):
if self.disks:
return self.disks.pop()
return None
def can_add_disk(self, disk):
if not self.disks:
return True
return disk.size < self.disks[-1].size
class Game:
def __init__(self, num_disks=3):
self.num_disks = min(num_disks, MAX_DISKS)
self.towers = [
Tower(WIDTH // 4),
Tower(WIDTH // 2),
Tower(3 * WIDTH // 4)
]
self.moves = 0
self.selected_tower = None
self.selected_disk = None
self.moving_disk = None
self.auto_solving = False
self.solution_steps = []
self.solution_index = 0
self.last_move_time = 0
self.game_won = False
# Initialize the first tower with disks
for i in range(self.num_disks, 0, -1):
disk = Disk(i - 1, DISK_COLORS[(i - 1) % len(DISK_COLORS)])
self.towers[0].add_disk(disk)
def draw(self):
screen.fill(BACKGROUND)
# Draw towers
for tower in self.towers:
tower.draw()
# Draw disks
for tower in self.towers:
for disk in tower.disks:
disk.draw()
# Draw moving disk
if self.moving_disk:
self.moving_disk.draw()
# Draw move counter
font = pygame.font.SysFont('Arial', 24)
moves_text = font.render(f"Moves: {self.moves}", True, (255, 255, 255))
screen.blit(moves_text, (20, 20))
# Draw win message
if self.game_won:
win_font = pygame.font.SysFont('Arial', 48)
win_text = win_font.render("You Win!", True, (255, 215, 0))
screen.blit(win_text, (WIDTH // 2 - win_text.get_width() // 2, 50))
# Draw buttons
self.draw_buttons()
pygame.display.flip()
def draw_buttons(self):
# Reset button
pygame.draw.rect(screen, (200, 200, 200), (20, HEIGHT - 60, 100, 40), 0, 5)
font = pygame.font.SysFont('Arial', 20)
reset_text = font.render("Reset", True, (0, 0, 0))
screen.blit(reset_text, (45, HEIGHT - 50))
# Auto Solve button
pygame.draw.rect(screen, (200, 200, 200), (140, HEIGHT - 60, 120, 40), 0, 5)
solve_text = font.render("Auto Solve", True, (0, 0, 0))
screen.blit(solve_text, (155, HEIGHT - 50))
# Disk count buttons
for i in range(1, MAX_DISKS + 1):
pygame.draw.rect(screen, (200, 200, 200), (280 + (i-1)*60, HEIGHT - 60, 50, 40), 0, 5)
disk_text = font.render(str(i), True, (0, 0, 0))
screen.blit(disk_text, (300 + (i-1)*60, HEIGHT - 50))
def handle_click(self, pos):
x, y = pos
# Check if a tower was clicked
if not self.auto_solving and y < HEIGHT - 70:
for i, tower in enumerate(self.towers):
if abs(x - tower.x) < 50:
self.handle_tower_click(i)
return
# Check if reset button was clicked
if 20 <= x <= 120 and HEIGHT - 60 <= y <= HEIGHT - 20:
self.reset()
return
# Check if auto solve button was clicked
if 140 <= x <= 260 and HEIGHT - 60 <= y <= HEIGHT - 20:
self.start_auto_solve()
return
# Check if disk count buttons were clicked
for i in range(1, MAX_DISKS + 1):
if 280 + (i-1)*60 <= x <= 330 + (i-1)*60 and HEIGHT - 60 <= y <= HEIGHT - 20:
self.reset(i)
return
def handle_tower_click(self, tower_index):
if self.selected_tower is None:
# No tower selected yet, try to select this one
if self.towers[tower_index].disks:
self.selected_tower = tower_index
self.selected_disk = self.towers[tower_index].remove_top_disk()
self.moving_disk = self.selected_disk
self.moving_disk.moving = True
self.moving_disk.target_x = self.towers[tower_index].x
self.moving_disk.target_y = 100 # Move up
else:
# A tower was already selected, try to move the disk
if tower_index == self.selected_tower:
# Put the disk back
self.towers[tower_index].add_disk(self.selected_disk)
elif self.towers[tower_index].can_add_disk(self.selected_disk):
# Move the disk to the new tower
self.moving_disk.target_x = self.towers[tower_index].x
self.moving_disk.target_y = self.towers[tower_index].y - len(self.towers[tower_index].disks) * DISK_HEIGHT - DISK_HEIGHT // 2
self.towers[tower_index].add_disk(self.selected_disk)
self.moves += 1
# Check if the game is won
if len(self.towers[2].disks) == self.num_disks:
self.game_won = True
else:
# Invalid move, put the disk back
self.towers[self.selected_tower].add_disk(self.selected_disk)
self.selected_tower = None
self.selected_disk = None
self.moving_disk = None
def reset(self, num_disks=None):
if num_disks is not None:
self.num_disks = min(num_disks, MAX_DISKS)
self.towers = [
Tower(WIDTH // 4),
Tower(WIDTH // 2),
Tower(3 * WIDTH // 4)
]
for i in range(self.num_disks, 0, -1):
disk = Disk(i - 1, DISK_COLORS[(i - 1) % len(DISK_COLORS)])
self.towers[0].add_disk(disk)
self.moves = 0
self.selected_tower = None
self.selected_disk = None
self.moving_disk = None
self.auto_solving = False
self.solution_steps = []
self.solution_index = 0
self.game_won = False
def solve_hanoi(self, n, source, target, auxiliary, steps):
if n > 0:
self.solve_hanoi(n-1, source, auxiliary, target, steps)
steps.append((source, target))
self.solve_hanoi(n-1, auxiliary, target, source, steps)
def start_auto_solve(self):
if self.auto_solving:
return
# Reset the game first
self.reset(self.num_disks)
# Generate solution steps
self.solution_steps = []
self.solve_hanoi(self.num_disks, 0, 2, 1, self.solution_steps)
self.solution_index = 0
self.auto_solving = True
self.last_move_time = time.time()
def update_auto_solve(self):
if not self.auto_solving or self.solution_index >= len(self.solution_steps):
return
current_time = time.time()
if current_time - self.last_move_time < 1.0: # Wait 1 second between moves
return
source, target = self.solution_steps[self.solution_index]
# Make the move
if self.towers[source].disks:
disk = self.towers[source].remove_top_disk()
self.towers[target].add_disk(disk)
self.moves += 1
# Check if the game is won
if len(self.towers[2].disks) == self.num_disks:
self.game_won = True
self.auto_solving = False
self.solution_index += 1
self.last_move_time = current_time
if self.solution_index >= len(self.solution_steps):
self.auto_solving = False
def main():
game = Game(3)
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1: # Left mouse button
game.handle_click(event.pos)
game.update_auto_solve()
game.draw()
clock.tick(60)
pygame.quit()
sys.exit()
if __name__ == "__main__":
main()
Code result material non polished:
import pygame
import sys
import time
# Initialize pygame
pygame.init()
# Constants
WIDTH, HEIGHT = 800, 600
DISK_HEIGHT = 24
MAX_DISKS = 5
ANIMATION_SPEED = 5
# Material Design Colors
BACKGROUND = (245, 245, 245) # Grey 100
PRIMARY_COLOR = (33, 150, 243) # Blue 500
ACCENT_COLOR = (255, 152, 0) # Orange 500
TOWER_COLOR = (96, 125, 139) # Blue Grey 500
TEXT_PRIMARY = (33, 33, 33) # Grey 900
TEXT_SECONDARY = (117, 117, 117) # Grey 600
BUTTON_COLOR = (255, 255, 255) # White
BUTTON_HOVER = (238, 238, 238) # Grey 200
WIN_COLOR = (76, 175, 80) # Green 500
# Material Design Disk Colors
DISK_COLORS = [
(244, 67, 54), # Red 500
(156, 39, 176), # Purple 500
(76, 175, 80), # Green 500
(3, 169, 244), # Light Blue 500
(255, 193, 7), # Amber 500
]
# Set up the display
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Tower of Hanoi - Material Design")
clock = pygame.time.Clock()
# Helper function for drawing rounded rectangles with shadow effect
def draw_material_rect(surface, color, rect, radius=8, elevation=2):
x, y, width, height = rect
# Draw shadow (if elevation > 0)
if elevation > 0:
shadow_rect = (x, y + elevation, width, height)
pygame.draw.rect(surface, (0, 0, 0, 50), shadow_rect, 0, radius)
# Draw the rounded rectangle
pygame.draw.rect(surface, color, (x, y, width, height), 0, radius)
# Helper function for drawing material buttons
def draw_material_button(surface, rect, text, font, color=BUTTON_COLOR, text_color=TEXT_PRIMARY, elevation=2):
draw_material_rect(surface, color, rect, 4, elevation)
text_surf = font.render(text, True, text_color)
text_rect = text_surf.get_rect(center=(rect[0] + rect[2]//2, rect[1] + rect[3]//2))
surface.blit(text_surf, text_rect)
class Disk:
def __init__(self, size, color):
self.size = size
self.color = color
self.x = 0
self.y = 0
self.moving = False
self.target_x = 0
self.target_y = 0
def draw(self):
width = (self.size + 1) * 30
# Draw disk with elevation effect
shadow_rect = (self.x - width // 2, self.y - DISK_HEIGHT // 2 + 2, width, DISK_HEIGHT)
pygame.draw.rect(screen, (0, 0, 0, 30), shadow_rect, 0, DISK_HEIGHT // 2)
# Draw main disk
disk_rect = (self.x - width // 2, self.y - DISK_HEIGHT // 2, width, DISK_HEIGHT)
pygame.draw.rect(screen, self.color, disk_rect, 0, DISK_HEIGHT // 2)
# Add a subtle highlight on top
highlight_rect = (self.x - width // 2, self.y - DISK_HEIGHT // 2, width, DISK_HEIGHT // 4)
highlight_color = tuple(min(c + 30, 255) for c in self.color[:3])
pygame.draw.rect(screen, highlight_color, highlight_rect, 0, DISK_HEIGHT // 2)
def move_towards_target(self):
dx = self.target_x - self.x
dy = self.target_y - self.y
if abs(dx) < ANIMATION_SPEED and abs(dy) < ANIMATION_SPEED:
self.x = self.target_x
self.y = self.target_y
self.moving = False
return True
if abs(dx) > 0:
self.x += ANIMATION_SPEED if dx > 0 else -ANIMATION_SPEED
if abs(dy) > 0:
self.y += ANIMATION_SPEED if dy > 0 else -ANIMATION_SPEED
return False
class Tower:
def __init__(self, x):
self.x = x
self.y = HEIGHT - 100
self.disks = []
def draw(self):
# Draw tower base with material design
base_width = 120
# Draw base shadow
pygame.draw.rect(screen, (0, 0, 0, 30), (self.x - base_width//2, self.y + 22, base_width, 8), 0, 4)
# Draw base
pygame.draw.rect(screen, TOWER_COLOR, (self.x - base_width//2, self.y + 20, base_width, 8), 0, 4)
# Draw tower pole with subtle gradient
pole_height = 220
for i in range(pole_height):
# Create subtle gradient effect
shade = max(0, min(20, i // 10))
color = tuple(max(0, min(255, c + shade)) for c in TOWER_COLOR[:3])
pygame.draw.rect(screen, color, (self.x - 4, self.y - pole_height + i, 8, 1))
def add_disk(self, disk):
disk_y = self.y - len(self.disks) * DISK_HEIGHT - DISK_HEIGHT // 2
disk.x = self.x
disk.y = disk_y
self.disks.append(disk)
def remove_top_disk(self):
if self.disks:
return self.disks.pop()
return None
def can_add_disk(self, disk):
if not self.disks:
return True
return disk.size < self.disks[-1].size
class Game:
def __init__(self, num_disks=3):
self.num_disks = min(num_disks, MAX_DISKS)
self.towers = [
Tower(WIDTH // 4),
Tower(WIDTH // 2),
Tower(3 * WIDTH // 4)
]
self.moves = 0
self.selected_tower = None
self.selected_disk = None
self.moving_disk = None
self.auto_solving = False
self.solution_steps = []
self.solution_index = 0
self.last_move_time = 0
self.game_won = False
# Load fonts with fallbacks
try:
self.title_font = pygame.font.SysFont('Roboto', 36)
self.main_font = pygame.font.SysFont('Roboto', 24)
self.button_font = pygame.font.SysFont('Roboto', 18)
except:
self.title_font = pygame.font.SysFont(None, 36)
self.main_font = pygame.font.SysFont(None, 24)
self.button_font = pygame.font.SysFont(None, 18)
# Initialize the first tower with disks
for i in range(self.num_disks, 0, -1):
disk = Disk(i - 1, DISK_COLORS[(i - 1) % len(DISK_COLORS)])
self.towers[0].add_disk(disk)
def draw(self):
screen.fill(BACKGROUND)
# Draw app bar
pygame.draw.rect(screen, PRIMARY_COLOR, (0, 0, WIDTH, 60))
title_text = self.title_font.render("Tower of Hanoi", True, (255, 255, 255))
screen.blit(title_text, (20, 15))
# Draw towers
for tower in self.towers:
tower.draw()
# Draw disks
for tower in self.towers:
for disk in tower.disks:
disk.draw()
# Draw moving disk
if self.moving_disk:
self.moving_disk.draw()
# Draw move counter with card style
counter_rect = (WIDTH - 150, 70, 130, 50)
draw_material_rect(screen, (255, 255, 255), counter_rect, 4, 2)
moves_text = self.main_font.render(f"Moves: {self.moves}", True, TEXT_PRIMARY)
screen.blit(moves_text, (WIDTH - 140, 85))
# Draw win message with material card
if self.game_won:
win_rect = (WIDTH // 2 - 150, 70, 300, 60)
draw_material_rect(screen, WIN_COLOR, win_rect, 4, 3)
win_text = self.title_font.render("You Win!", True, (255, 255, 255))
screen.blit(win_text, (WIDTH // 2 - win_text.get_width() // 2, 85))
# Draw buttons
self.draw_buttons()
pygame.display.flip()
def draw_buttons(self):
# Create a card for the controls
control_card_rect = (20, HEIGHT - 80, WIDTH - 40, 60)
draw_material_rect(screen, (255, 255, 255), control_card_rect, 4, 3)
# Reset button
reset_rect = (40, HEIGHT - 70, 100, 40)
draw_material_button(screen, reset_rect, "Reset", self.button_font, PRIMARY_COLOR, (255, 255, 255))
# Auto Solve button
solve_rect = (160, HEIGHT - 70, 120, 40)
draw_material_button(screen, solve_rect, "Auto Solve", self.button_font, ACCENT_COLOR, (255, 255, 255))
# Disk count buttons
for i in range(1, MAX_DISKS + 1):
disk_rect = (300 + (i-1)*80, HEIGHT - 70, 60, 40)
draw_material_button(screen, disk_rect, str(i), self.button_font,
BUTTON_COLOR if i != self.num_disks else PRIMARY_COLOR,
TEXT_PRIMARY if i != self.num_disks else (255, 255, 255))
def handle_click(self, pos):
x, y = pos
# Check if a tower was clicked
if not self.auto_solving and y < HEIGHT - 90 and y > 60:
for i, tower in enumerate(self.towers):
if abs(x - tower.x) < 60:
self.handle_tower_click(i)
return
# Check if reset button was clicked
if 40 <= x <= 140 and HEIGHT - 70 <= y <= HEIGHT - 30:
self.reset()
return
# Check if auto solve button was clicked
if 160 <= x <= 280 and HEIGHT - 70 <= y <= HEIGHT - 30:
self.start_auto_solve()
return
# Check if disk count buttons were clicked
for i in range(1, MAX_DISKS + 1):
if 300 + (i-1)*80 <= x <= 360 + (i-1)*80 and HEIGHT - 70 <= y <= HEIGHT - 30:
self.reset(i)
return
def handle_tower_click(self, tower_index):
if self.selected_tower is None:
# No tower selected yet, try to select this one
if self.towers[tower_index].disks:
self.selected_tower = tower_index
self.selected_disk = self.towers[tower_index].remove_top_disk()
self.moving_disk = self.selected_disk
self.moving_disk.moving = True
self.moving_disk.target_x = self.towers[tower_index].x
self.moving_disk.target_y = 100 # Move up
else:
# A tower was already selected, try to move the disk
if tower_index == self.selected_tower:
# Put the disk back
self.towers[tower_index].add_disk(self.selected_disk)
elif self.towers[tower_index].can_add_disk(self.selected_disk):
# Move the disk to the new tower
self.moving_disk.target_x = self.towers[tower_index].x
self.moving_disk.target_y = self.towers[tower_index].y - len(self.towers[tower_index].disks) * DISK_HEIGHT - DISK_HEIGHT // 2
self.towers[tower_index].add_disk(self.selected_disk)
self.moves += 1
# Check if the game is won
if len(self.towers[2].disks) == self.num_disks:
self.game_won = True
else:
# Invalid move, put the disk back
self.towers[self.selected_tower].add_disk(self.selected_disk)
self.selected_tower = None
self.selected_disk = None
self.moving_disk = None
def reset(self, num_disks=None):
if num_disks is not None:
self.num_disks = min(num_disks, MAX_DISKS)
self.towers = [
Tower(WIDTH // 4),
Tower(WIDTH // 2),
Tower(3 * WIDTH // 4)
]
for i in range(self.num_disks, 0, -1):
disk = Disk(i - 1, DISK_COLORS[(i - 1) % len(DISK_COLORS)])
self.towers[0].add_disk(disk)
self.moves = 0
self.selected_tower = None
self.selected_disk = None
self.moving_disk = None
self.auto_solving = False
self.solution_steps = []
self.solution_index = 0
self.game_won = False
def solve_hanoi(self, n, source, target, auxiliary, steps):
if n > 0:
self.solve_hanoi(n-1, source, auxiliary, target, steps)
steps.append((source, target))
self.solve_hanoi(n-1, auxiliary, target, source, steps)
def start_auto_solve(self):
if self.auto_solving:
return
# Reset the game first
self.reset(self.num_disks)
# Generate solution steps
self.solution_steps = []
self.solve_hanoi(self.num_disks, 0, 2, 1, self.solution_steps)
self.solution_index = 0
self.auto_solving = True
self.last_move_time = time.time()
def update_auto_solve(self):
if not self.auto_solving or self.solution_index >= len(self.solution_steps):
return
current_time = time.time()
if current_time - self.last_move_time < 1.0: # Wait 1 second between moves
return
source, target = self.solution_steps[self.solution_index]
# Make the move
if self.towers[source].disks:
disk = self.towers[source].remove_top_disk()
self.towers[target].add_disk(disk)
self.moves += 1
# Check if the game is won
if len(self.towers[2].disks) == self.num_disks:
self.game_won = True
self.auto_solving = False
self.solution_index += 1
self.last_move_time = current_time
if self.solution_index >= len(self.solution_steps):
self.auto_solving = False
def main():
game = Game(3)
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1: # Left mouse button
game.handle_click(event.pos)
game.update_auto_solve()
game.draw()
clock.tick(60)
pygame.quit()
sys.exit()
if __name__ == "__main__":
main()
Code result material polished:
import random
import pygame
import sys
import time
import math
# Initialize pygame
pygame.init()
# Constants
WIDTH, HEIGHT = 800, 600
FPS = 60
MIN_DISKS = 3
MAX_DISKS = 7
# Material Design Colors
LIGHT_THEME = {
"background": (245, 245, 245), # Grey 100
"surface": (255, 255, 255), # White
"primary": (33, 150, 243), # Blue 500
"primary_dark": (25, 118, 210), # Blue 700
"primary_light": (100, 181, 246), # Blue 300
"secondary": (255, 152, 0), # Orange 500
"text_primary": (33, 33, 33), # Grey 900
"text_secondary": (117, 117, 117), # Grey 600
"tower": (96, 125, 139), # Blue Grey 500
"error": (244, 67, 54), # Red 500
"success": (76, 175, 80), # Green 500
}
DARK_THEME = {
"background": (48, 48, 48), # Grey 900
"surface": (66, 66, 66), # Grey 800
"primary": (33, 150, 243), # Blue 500
"primary_dark": (25, 118, 210), # Blue 700
"primary_light": (100, 181, 246), # Blue 300
"secondary": (255, 152, 0), # Orange 500
"text_primary": (255, 255, 255),# White
"text_secondary": (189, 189, 189), # Grey 400
"tower": (176, 190, 197), # Blue Grey 300
"error": (244, 67, 54), # Red 500
"success": (76, 175, 80), # Green 500
}
# Material Design Disk Colors - Light Theme
DISK_COLORS_LIGHT = [
(244, 67, 54), # Red 500
(156, 39, 176), # Purple 500
(33, 150, 243), # Blue 500
(76, 175, 80), # Green 500
(255, 193, 7), # Amber 500
(255, 87, 34), # Deep Orange 500
(0, 188, 212), # Cyan 500
]
# Material Design Disk Colors - Dark Theme
DISK_COLORS_DARK = [
(229, 115, 115), # Red 300
(186, 104, 200), # Purple 300
(100, 181, 246), # Blue 300
(129, 199, 132), # Green 300
(255, 213, 79), # Amber 300
(255, 138, 101), # Deep Orange 300
(77, 208, 225), # Cyan 300
]
# Set up the display
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Tower of Hanoi - Material Design")
clock = pygame.time.Clock()
# Helper function for drawing rounded rectangles
def draw_rounded_rect(surface, color, rect, radius=10, border=0, border_color=(0, 0, 0)):
"""Draw a rounded rectangle with optional border"""
x, y, width, height = rect
if border > 0:
# Draw border first (as a slightly larger rectangle)
pygame.draw.rect(surface, border_color,
(x-border, y-border, width+2*border, height+2*border),
0, radius+border)
# Draw the main rectangle
pygame.draw.rect(surface, color, (x, y, width, height), 0, radius)
# Helper function for drawing material shadows
def draw_shadow(surface, rect, radius=10, alpha=30, offset=(0, 4), blur=4):
"""Draw a soft shadow under a rectangle"""
shadow_surf = pygame.Surface((rect[2] + blur * 2, rect[3] + blur * 2), pygame.SRCALPHA)
pygame.draw.rect(shadow_surf, (0, 0, 0, alpha),
(blur, blur, rect[2], rect[3]), 0, radius)
# Apply simple blur by scaling down and up
scale_factor = 0.5
small_surf = pygame.transform.smoothscale(shadow_surf,
(int(shadow_surf.get_width() * scale_factor),
int(shadow_surf.get_height() * scale_factor)))
blurred = pygame.transform.smoothscale(small_surf, shadow_surf.get_size())
# Blit the shadow
surface.blit(blurred, (rect[0] - blur + offset[0], rect[1] - blur + offset[1]))
# Helper function for drawing material buttons
def draw_material_button(surface, rect, text, font, colors, is_hover=False, is_active=False):
"""Draw a material design button with hover and active states"""
base_color = colors["primary"] if is_active else colors["surface"]
text_color = colors["text_primary"] if not is_active else (255, 255, 255)
# Draw shadow
if not is_hover:
draw_shadow(surface, rect, radius=4, offset=(0, 2), blur=4)
else:
draw_shadow(surface, rect, radius=4, offset=(0, 4), blur=6)
# Draw button
draw_rounded_rect(surface, base_color, rect, radius=4)
# Draw ripple effect if hovering
if is_hover and not is_active:
hover_color = (*base_color[:3], 20) # Semi-transparent overlay
hover_surf = pygame.Surface((rect[2], rect[3]), pygame.SRCALPHA)
pygame.draw.rect(hover_surf, hover_color, (0, 0, rect[2], rect[3]), 0, 4)
surface.blit(hover_surf, (rect[0], rect[1]))
# Draw text
text_surf = font.render(text, True, text_color)
text_rect = text_surf.get_rect(center=(rect[0] + rect[2]//2, rect[1] + rect[3]//2))
surface.blit(text_surf, text_rect)
# Helper function for drawing material cards
def draw_material_card(surface, rect, colors, elevation=2):
"""Draw a material design card with elevation"""
# Draw shadow
draw_shadow(surface, rect, radius=8, offset=(0, elevation), blur=elevation*2)
# Draw card
draw_rounded_rect(surface, colors["surface"], rect, radius=8)
class Disk:
def __init__(self, size, color, theme_colors):
self.size = size
self.base_color = color
self.theme_colors = theme_colors
self.x = 0
self.y = 0
self.target_x = 0
self.target_y = 0
self.moving = False
self.dragging = False
self.drag_offset_x = 0
self.drag_offset_y = 0
self.width = (size + 1) * 30
self.height = 24
self.animation_progress = 0
self.animation_duration = 0.3 # seconds
self.animation_start_time = 0
self.animation_start_pos = (0, 0)
def draw(self, surface):
# Calculate disk rectangle
rect = (self.x - self.width // 2, self.y - self.height // 2, self.width, self.height)
# Draw shadow
if not self.dragging:
shadow_offset = 2
draw_shadow(surface, rect, radius=self.height//2, offset=(0, shadow_offset), blur=4)
else:
# Larger shadow when dragging
shadow_offset = 6
draw_shadow(surface, rect, radius=self.height//2, offset=(0, shadow_offset), blur=8, alpha=40)
# Draw disk with rounded corners
draw_rounded_rect(surface, self.base_color, rect, radius=self.height//2)
# Add a subtle highlight on top for 3D effect
highlight_rect = (self.x - self.width // 2, self.y - self.height // 2, self.width, self.height // 3)
highlight_color = tuple(min(c + 30, 255) for c in self.base_color[:3])
draw_rounded_rect(surface, highlight_color, highlight_rect, radius=self.height//2)
def contains_point(self, point):
"""Check if the disk contains the given point"""
return (abs(point[0] - self.x) <= self.width // 2 and
abs(point[1] - self.y) <= self.height // 2)
def start_drag(self, mouse_pos):
"""Start dragging the disk"""
self.dragging = True
self.drag_offset_x = self.x - mouse_pos[0]
self.drag_offset_y = self.y - mouse_pos[1]
def update_drag(self, mouse_pos):
"""Update the disk position while dragging"""
if self.dragging:
self.x = mouse_pos[0] + self.drag_offset_x
self.y = mouse_pos[1] + self.drag_offset_y
def end_drag(self):
"""End dragging the disk"""
self.dragging = False
def start_animation(self, target_x, target_y):
"""Start animating the disk to a new position"""
self.moving = True
self.animation_start_time = time.time()
self.animation_progress = 0
self.animation_start_pos = (self.x, self.y)
self.target_x = target_x
self.target_y = target_y
def update_animation(self):
"""Update the disk animation"""
if not self.moving:
return False
current_time = time.time()
elapsed = current_time - self.animation_start_time
self.animation_progress = min(elapsed / self.animation_duration, 1.0)
# Use easeOutCubic easing function for smooth animation
progress = 1 - (1 - self.animation_progress) ** 3
# Update position
self.x = self.animation_start_pos[0] + (self.target_x - self.animation_start_pos[0]) * progress
self.y = self.animation_start_pos[1] + (self.target_y - self.animation_start_pos[1]) * progress
# Check if animation is complete
if self.animation_progress >= 1.0:
self.x = self.target_x
self.y = self.target_y
self.moving = False
return True
return False
class Tower:
def __init__(self, x, y, theme_colors):
self.x = x
self.y = y
self.theme_colors = theme_colors
self.disks = []
self.base_width = 120
self.pole_height = 220
self.pole_width = 8
self.highlight = False
self.highlight_alpha = 0
self.highlight_direction = 1
def draw(self, surface):
tower_color = self.theme_colors["tower"]
# Draw base shadow
base_rect = (self.x - self.base_width//2, self.y, self.base_width, 10)
draw_shadow(surface, base_rect, radius=5, offset=(0, 2), blur=4)
# Draw base
draw_rounded_rect(surface, tower_color, base_rect, radius=5)
# Draw tower pole with subtle gradient
for i in range(self.pole_height):
# Create subtle gradient effect
shade = max(0, min(20, i // 10))
color = tuple(max(0, min(255, c + shade)) for c in tower_color[:3])
pygame.draw.rect(surface, color,
(self.x - self.pole_width//2, self.y - self.pole_height + i,
self.pole_width, 1))
# Draw highlight if this tower is a valid drop target
if self.highlight:
self.highlight_alpha += self.highlight_direction * 5
if self.highlight_alpha >= 60:
self.highlight_alpha = 60
self.highlight_direction = -1
elif self.highlight_alpha <= 20:
self.highlight_alpha = 20
self.highlight_direction = 1
highlight_color = (*self.theme_colors["primary"][:3], self.highlight_alpha)
highlight_surf = pygame.Surface((self.base_width + 20, self.pole_height + 10), pygame.SRCALPHA)
pygame.draw.rect(highlight_surf, highlight_color,
(0, 0, self.base_width + 20, self.pole_height + 10), 0, 10)
surface.blit(highlight_surf,
(self.x - (self.base_width + 20)//2, self.y - self.pole_height - 5))
def add_disk(self, disk):
"""Add a disk to this tower"""
disk_y = self.y - len(self.disks) * disk.height - disk.height // 2
disk.x = self.x
disk.y = disk_y
self.disks.append(disk)
def remove_top_disk(self):
"""Remove and return the top disk from this tower"""
if self.disks:
return self.disks.pop()
return None
def can_add_disk(self, disk):
"""Check if a disk can be added to this tower"""
if not self.disks:
return True
return disk.size < self.disks[-1].size
def get_top_disk(self):
"""Get the top disk without removing it"""
if self.disks:
return self.disks[-1]
return None
def contains_point(self, point):
"""Check if the tower contains the given point for dropping"""
return abs(point[0] - self.x) < self.base_width // 2
def get_top_position(self):
"""Get the position for a new disk at the top of the tower"""
disk_y = self.y - len(self.disks) * 24 - 24 // 2
return self.x, disk_y
def set_highlight(self, highlight):
"""Set whether this tower should be highlighted as a valid drop target"""
self.highlight = highlight
class Button:
def __init__(self, x, y, width, height, text, theme_colors, action=None):
self.rect = (x, y, width, height)
self.text = text
self.theme_colors = theme_colors
self.action = action
self.hover = False
self.active = False
# Load font
try:
self.font = pygame.font.SysFont('Roboto', 18)
except:
self.font = pygame.font.SysFont(None, 18)
def draw(self, surface):
draw_material_button(surface, self.rect, self.text, self.font,
self.theme_colors, self.hover, self.active)
def contains_point(self, point):
x, y, width, height = self.rect
return (x <= point[0] <= x + width and y <= point[1] <= y + height)
def set_hover(self, hover):
self.hover = hover
def set_active(self, active):
self.active = active
def click(self):
if self.action:
self.action()
class Game:
def __init__(self, num_disks=3, dark_mode=False):
self.num_disks = min(max(num_disks, MIN_DISKS), MAX_DISKS)
self.dark_mode = dark_mode
self.theme = DARK_THEME if dark_mode else LIGHT_THEME
self.disk_colors = DISK_COLORS_DARK if dark_mode else DISK_COLORS_LIGHT
# Game state
self.moves = 0
self.start_time = time.time()
self.elapsed_time = 0
self.game_won = False
self.show_win_animation = False
self.win_animation_start = 0
self.win_particles = []
# Tower setup
tower_y = HEIGHT - 100
self.towers = [
Tower(WIDTH // 4, tower_y, self.theme),
Tower(WIDTH // 2, tower_y, self.theme),
Tower(3 * WIDTH // 4, tower_y, self.theme)
]
# Disk interaction state
self.selected_disk = None
self.source_tower = None
self.last_valid_position = (0, 0)
# Initialize the first tower with disks
for i in range(self.num_disks, 0, -1):
disk = Disk(i - 1, self.disk_colors[(i - 1) % len(self.disk_colors)], self.theme)
self.towers[0].add_disk(disk)
# Load fonts
try:
self.title_font = pygame.font.SysFont('Roboto', 36)
self.main_font = pygame.font.SysFont('Roboto', 24)
self.button_font = pygame.font.SysFont('Roboto', 18)
except:
self.title_font = pygame.font.SysFont(None, 36)
self.main_font = pygame.font.SysFont(None, 24)
self.button_font = pygame.font.SysFont(None, 18)
# Create UI buttons
self.create_buttons()
# Feedback message
self.feedback_message = ""
self.feedback_color = self.theme["text_primary"]
self.feedback_timer = 0
def create_buttons(self):
"""Create all UI buttons"""
self.buttons = []
# Reset button
self.buttons.append(Button(20, HEIGHT - 60, 100, 40, "Reset", self.theme,
action=lambda: self.reset()))
# Theme toggle button
theme_text = "Light Mode" if self.dark_mode else "Dark Mode"
self.buttons.append(Button(140, HEIGHT - 60, 120, 40, theme_text, self.theme,
action=lambda: self.toggle_theme()))
# Disk count buttons
for i in range(MIN_DISKS, MAX_DISKS + 1):
x_pos = 280 + (i - MIN_DISKS) * 70
self.buttons.append(Button(x_pos, HEIGHT - 60, 60, 40, str(i), self.theme,
action=lambda i=i: self.reset(i)))
def toggle_theme(self):
"""Toggle between light and dark themes"""
self.dark_mode = not self.dark_mode
self.theme = DARK_THEME if self.dark_mode else LIGHT_THEME
self.disk_colors = DISK_COLORS_DARK if self.dark_mode else DISK_COLORS_LIGHT
# Update tower colors
for tower in self.towers:
tower.theme_colors = self.theme
# Update disk colors
for tower in self.towers:
for i, disk in enumerate(tower.disks):
disk.theme_colors = self.theme
disk.base_color = self.disk_colors[(disk.size) % len(self.disk_colors)]
if self.selected_disk:
self.selected_disk.theme_colors = self.theme
self.selected_disk.base_color = self.disk_colors[(self.selected_disk.size) % len(self.disk_colors)]
# Recreate buttons with new theme
self.create_buttons()
def draw(self, surface):
# Fill background
surface.fill(self.theme["background"])
# Draw app bar
pygame.draw.rect(surface, self.theme["primary"], (0, 0, WIDTH, 60))
title_text = self.title_font.render("Tower of Hanoi", True, (255, 255, 255))
surface.blit(title_text, (20, 15))
# Draw towers
for tower in self.towers:
tower.draw(surface)
# Draw disks on towers
for tower in self.towers:
for disk in tower.disks:
disk.draw(surface)
# Draw selected disk (being dragged) on top
if self.selected_disk:
self.selected_disk.draw(surface)
# Draw move counter and timer
self.draw_stats(surface)
# Draw buttons
for button in self.buttons:
button.draw(surface)
# Draw disk count indicator
self.draw_disk_count_indicator(surface)
# Draw feedback message
if self.feedback_message and time.time() < self.feedback_timer:
self.draw_feedback(surface)
# Draw win animation
if self.show_win_animation:
self.draw_win_animation(surface)
# Draw win message
if self.game_won:
self.draw_win_message(surface)
def draw_stats(self, surface):
# Create a card for stats
stats_rect = (WIDTH - 200, 70, 180, 80)
draw_material_card(surface, stats_rect, self.theme)
# Draw move counter
moves_text = self.main_font.render(f"Moves: {self.moves}", True, self.theme["text_primary"])
surface.blit(moves_text, (WIDTH - 180, 85))
# Draw timer
minutes = int(self.elapsed_time) // 60
seconds = int(self.elapsed_time) % 60
time_text = self.main_font.render(f"Time: {minutes:02d}:{seconds:02d}", True,
self.theme["text_primary"])
surface.blit(time_text, (WIDTH - 180, 115))
def draw_disk_count_indicator(self, surface):
# Draw text above disk count buttons
text = self.button_font.render("Number of Disks:", True, self.theme["text_primary"])
surface.blit(text, (280, HEIGHT - 90))
# Highlight the current disk count button
for button in self.buttons[2:]: # Skip Reset and Theme buttons
button.set_active(button.text == str(self.num_disks))
def draw_feedback(self, surface):
text = self.main_font.render(self.feedback_message, True, self.feedback_color)
text_rect = text.get_rect(center=(WIDTH // 2, HEIGHT - 100))
surface.blit(text, text_rect)
def draw_win_message(self, surface):
# Create a card for the win message
win_rect = (WIDTH // 2 - 150, 70, 300, 60)
draw_material_card(surface, win_rect, self.theme, elevation=4)
# Draw win text
win_text = self.title_font.render("You Win!", True, self.theme["success"])
text_rect = win_text.get_rect(center=(WIDTH // 2, 100))
surface.blit(win_text, text_rect)
def draw_win_animation(self, surface):
# Generate particles
if time.time() - self.win_animation_start < 2.0:
if len(self.win_particles) < 100 and random.random() < 0.3:
self.win_particles.append({
'x': random.randint(0, WIDTH),
'y': random.randint(0, HEIGHT // 2),
'size': random.randint(5, 15),
'color': random.choice([self.theme["primary"], self.theme["secondary"],
self.theme["success"]]),
'speed': random.uniform(1, 3),
'angle': random.uniform(0, 2 * math.pi)
})
# Update and draw particles
particles_to_keep = []
for particle in self.win_particles:
# Update position
particle['y'] += particle['speed']
particle['x'] += math.sin(particle['angle']) * 0.5
# Draw particle
pygame.draw.circle(surface, particle['color'],
(int(particle['x']), int(particle['y'])),
particle['size'])
# Keep particles that are still on screen
if particle['y'] < HEIGHT:
particles_to_keep.append(particle)
self.win_particles = particles_to_keep
# End animation after a while
if time.time() - self.win_animation_start > 3.0:
self.show_win_animation = False
def update(self):
# Update elapsed time if game is not won
if not self.game_won:
self.elapsed_time = time.time() - self.start_time
# Update disk animations
for tower in self.towers:
for disk in tower.disks:
disk.update_animation()
def handle_click(self, pos):
# Check if a button was clicked
for button in self.buttons:
if button.contains_point(pos):
button.click()
return
# Don't allow moves if game is won
if self.game_won:
return
# Check if a tower was clicked
if not self.selected_disk:
# Try to select a disk
for i, tower in enumerate(self.towers):
if tower.disks and tower.get_top_disk().contains_point(pos):
self.source_tower = i
self.selected_disk = tower.remove_top_disk()
self.selected_disk.start_drag(pos)
self.last_valid_position = (self.selected_disk.x, self.selected_disk.y)
return
else:
# Try to place the disk
self.end_disk_drag()
def handle_mouse_motion(self, pos):
# Update button hover states
for button in self.buttons:
button.set_hover(button.contains_point(pos))
# Update selected disk position
if self.selected_disk:
self.selected_disk.update_drag(pos)
# Highlight valid drop towers
for i, tower in enumerate(self.towers):
can_drop = tower.contains_point(pos) and tower.can_add_disk(self.selected_disk)
tower.set_highlight(can_drop)
def handle_mouse_up(self, pos):
if self.selected_disk:
self.end_disk_drag()
def end_disk_drag(self):
"""End dragging the selected disk and place it on a tower if valid"""
if not self.selected_disk:
return
valid_drop = False
# Check if the disk is over a valid tower
for i, tower in enumerate(self.towers):
if tower.contains_point((self.selected_disk.x, self.selected_disk.y)):
if tower.can_add_disk(self.selected_disk):
# Valid move
target_x, target_y = tower.get_top_position()
self.selected_disk.start_animation(target_x, target_y)
tower.add_disk(self.selected_disk)
self.selected_disk = None
# Update move counter
if i != self.source_tower:
self.moves += 1
# Check if the game is won
if len(self.towers[2].disks) == self.num_disks:
self.game_won = True
self.show_win_animation = True
self.win_animation_start = time.time()
import random
self.win_particles = []
valid_drop = True
break
else:
# Invalid move - show feedback
self.show_feedback("Invalid move: Can't place larger disk on smaller disk",
self.theme["error"])
# If no valid drop, return the disk to its original tower
if not valid_drop:
self.towers[self.source_tower].add_disk(self.selected_disk)
target_x, target_y = self.last_valid_position
self.selected_disk.start_animation(target_x, target_y)
self.selected_disk = None
# Clear tower highlights
for tower in self.towers:
tower.set_highlight(False)
def show_feedback(self, message, color=None):
"""Show a feedback message for a short time"""
self.feedback_message = message
self.feedback_color = color if color else self.theme["text_primary"]
self.feedback_timer = time.time() + 2.0 # Show for 2 seconds
def reset(self, num_disks=None):
"""Reset the game with the specified number of disks"""
if num_disks is not None:
self.num_disks = min(max(num_disks, MIN_DISKS), MAX_DISKS)
# Reset game state
self.moves = 0
self.start_time = time.time()
self.elapsed_time = 0
self.game_won = False
self.show_win_animation = False
self.selected_disk = None
self.source_tower = None
# Reset towers
self.towers = [
Tower(WIDTH // 4, HEIGHT - 100, self.theme),
Tower(WIDTH // 2, HEIGHT - 100, self.theme),
Tower(3 * WIDTH // 4, HEIGHT - 100, self.theme)
]
# Initialize the first tower with disks
for i in range(self.num_disks, 0, -1):
disk = Disk(i - 1, self.disk_colors[(i - 1) % len(self.disk_colors)], self.theme)
self.towers[0].add_disk(disk)
# Update disk count indicator
for button in self.buttons[2:]: # Skip Reset and Theme buttons
button.set_active(button.text == str(self.num_disks))
def main():
# Import needed only for win animation
import random
# Initialize game
game = Game(3)
# Main game loop
running = True
while running:
# 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 mouse button
game.handle_click(event.pos)
elif event.type == pygame.MOUSEMOTION:
game.handle_mouse_motion(event.pos)
elif event.type == pygame.MOUSEBUTTONUP:
if event.button == 1: # Left mouse button
game.handle_mouse_up(event.pos)
# Update game state
game.update()
# Draw everything
game.draw(screen)
# Update display
pygame.display.flip()
# Cap the frame rate
clock.tick(FPS)
# Clean up
pygame.quit()
sys.exit()
if __name__ == "__main__":
main()