Build Games Challenge: Build Tower of Hanoi with Amazon Q Developer CLI
Martin Nanchev

Martin Nanchev @martinnanchev

About: DevOps with wide variety of projects experience 11 AWS Certificates AWS authorised instructor AWS Community Builder

Joined:
Sep 17, 2022

Build Games Challenge: Build Tower of Hanoi with Amazon Q Developer CLI

Publish Date: Jul 2
0 0

Table of Contents

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
Enter fullscreen mode Exit fullscreen mode

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:

Tower of Hanoi - First Version

Tower of Hanoi - First Version winning

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

  1. Zero-shot works best for complete, well-defined tasks
  2. Incremental changes can be trickier than starting fresh
  3. Always review and test AI-generated code
  4. 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:

Tower of Hanoi - Material Design

Tower of Hanoi - Material Design winning

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:

Tower of Hanoi - Final Polish

Tower of Hanoi - Final Polish winning

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:

  1. Complete game logic implementation
  2. Pygame framework integration
  3. Object-oriented design patterns
  4. Mathematical algorithm implementation (recursive Hanoi solver)
  5. Event handling and game state management

Where It Struggled:

  1. Incremental design changes to existing code
  2. Complex prompt continuation when code was cut off
  3. 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()

Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()

Enter fullscreen mode Exit fullscreen mode

Comments 0 total

    Add comment