Newer
Older
ESP32-RetroPlay / main / tasks / game / arkanoid_game.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h> // For rand(), srand()
#include <math.h>   // For roundf, fmaxf, fminf
#include "esp_system.h" // For esp_random() for better seeding
#include "esp_random.h" // Explicit include for esp_random
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "freertos/queue.h" // Needed for QueueHandle_t
#include "esp_log.h"        // For ESP_LOG macros

#include "ssd1306.h"        // OLED driver functions
#include "game_framework.h" // Include game framework
#include "game_drawing.h"   // Include common drawing functions

#define GAME_TAG "ARKANOID_GAME"

// --- Game Specific Definitions ---
// Paddle
#define PADDLE_WIDTH_NORMAL 24
#define PADDLE_WIDTH_ENLARGED 36 // Larger paddle width
// #define PADDLE_WIDTH_SHORTER 16 // Shorter paddle width (REMOVED)
#define PADDLE_HEIGHT 4
#define PADDLE_Y (OLED_HEIGHT - PADDLE_HEIGHT - 2) // A bit above the bottom edge
#define PADDLE_SPEED 4.0f // Pixels per frame for paddle movement

// Ball
#define BALL_RADIUS 2
#define INITIAL_BALL_SPEED 1.5f // Pixels per frame
#define MAX_BALL_SPEED 3.0f     // Cap ball speed
#define MAX_BALLS 2             // Max number of balls active at once

// Bricks
#define BRICK_ROWS 3
#define BRICK_COLS 8
#define BRICK_PADDING_X 1 // 1 pixel gap between bricks and from edges
#define BRICK_WIDTH ((OLED_WIDTH - (BRICK_PADDING_X * (BRICK_COLS + 1))) / BRICK_COLS)
#define BRICK_HEIGHT 3 // Made smaller as requested
#define BRICK_START_Y (GAME_PLAY_AREA_START_Y + 2) // Start below score area, with some padding
#define BRICK_PADDING_Y 1

// Movable Obstacles
#define NUM_MOVABLE_OBSTACLES 2 // Number of movable, unbreakable obstacles
#define MOVABLE_OBSTACLE_WIDTH 8 // Smaller as requested
#define MOVABLE_OBSTACLE_HEIGHT 8 // Smaller as requested
#define MOVABLE_OBSTACLE_INITIAL_SPEED 1.0f // Speed for movable obstacles
#define MOVABLE_OBSTACLE_MIN_Y_ABOVE_PADDLE (PADDLE_Y - MOVABLE_OBSTACLE_HEIGHT - 5) // 5 pixels above paddle

// Boost Items
#define MAX_BOOST_ITEMS 3 // Max number of boost items falling at once
#define BOOST_ITEM_WIDTH 6
#define BOOST_ITEM_HEIGHT 6
#define BOOST_ITEM_FALL_SPEED 0.5f // Pixels per frame
#define BOOST_ITEM_DROP_CHANCE_PERCENT 25 // 25% chance to drop a boost item (0-99)

#define PADDLE_ENLARGE_DURATION_FRAMES 300 // Duration for enlarged paddle (e.g., 5 seconds at 60 FPS)
// #define PADDLE_SHORTER_DURATION_FRAMES 300 // Duration for shorter paddle (REMOVED)
#define OBSTACLE_PAUSE_DURATION_FRAMES 300 // Duration for obstacle pause (NEW)


// Game State
#define INITIAL_LIVES 3

// Structure for a brick
typedef struct {
    int x, y;
    bool active;
} brick_t;

// Structure for a movable obstacle
typedef struct {
    float x, y;
    float vx, vy;
    bool active;
} movable_obstacle_t;

// Structure for a ball (now supports multiple balls)
typedef struct {
    float x, y;
    float vx, vy;
    bool active; // Is this ball currently in play?
} ball_t;

// Enum for boost item types (UPDATED)
typedef enum {
    BOOST_PADDLE_ENLARGE,
    BOOST_DOUBLE_BALL,
    BOOST_PAUSE_OBSTACLES, // NEW
    // BOOST_SHORTER_PADDLE,  // REMOVED
    // Add more boost types here
    NUM_BOOST_TYPES // Keep this last to count total types
} boost_item_type_t;

// Structure for a boost item
typedef struct {
    float x, y;
    float vy; // Only falls downwards
    bool active;
    boost_item_type_t type;
} boost_item_t;


// --- Game State Variables (static to this file) ---
static float s_paddle_x_float; // Paddle X position (float for smoother movement if needed)
static int s_paddle_x_int;     // Integer position for drawing
static int s_current_paddle_direction; // -1 for left, 0 for stop, 1 for right
static int s_current_paddle_width; // Dynamic paddle width

static ball_t s_balls[MAX_BALLS]; // Array of balls
static int s_active_balls_count; // Number of balls currently in play

static brick_t s_bricks[BRICK_ROWS][BRICK_COLS];
static movable_obstacle_t s_movable_obstacles[NUM_MOVABLE_OBSTACLES];
static boost_item_t s_boost_items[MAX_BOOST_ITEMS]; // Array for boost items

static int s_score;
static int s_lives;
static int s_bricks_remaining;
static bool s_game_over;
static bool s_game_is_active;

static int s_paddle_enlarge_timer; // Timer for paddle enlargement effect
// static int s_paddle_shorter_timer; // Timer for paddle shortening effect (REMOVED)
static int s_obstacle_pause_timer; // Timer for obstacle pause effect (NEW)


// Global input queue for this game instance, set externally
static QueueHandle_t s_game_input_queue_global = NULL;

// Helper functions (draw_filled_rect and draw_rectangle_outline are now in game_drawing.h/c)

/**
 * @brief Initializes the Arkanoid game state.
 */
static void arkanoid_init_game_impl() {
    ESP_LOGI(GAME_TAG, "Initializing Arkanoid Game state...");
    srand((unsigned int)esp_random());

    // If queue exists (e.g., restarting game), reset it.
    if (s_game_input_queue_global != NULL) {
        xQueueReset(s_game_input_queue_global);
        ESP_LOGI(GAME_TAG, "Game input queue reset for Arkanoid.");
    } else {
        ESP_LOGE(GAME_TAG, "Global game input queue is NULL during init!");
    }

    // Initialize paddle
    s_paddle_x_float = (OLED_WIDTH / 2) - (PADDLE_WIDTH_NORMAL / 2);
    s_paddle_x_int = (int)s_paddle_x_float;
    s_current_paddle_direction = 0; // Initialize paddle as stationary
    s_current_paddle_width = PADDLE_WIDTH_NORMAL; // Start with normal width
    s_paddle_enlarge_timer = 0; // Reset paddle enlarge timer
    // s_paddle_shorter_timer = 0; // Reset paddle shorter timer (REMOVED)
    s_obstacle_pause_timer = 0; // Reset obstacle pause timer (NEW)


    // Initialize balls
    for (int i = 0; i < MAX_BALLS; i++) {
        s_balls[i].active = false;
    }
    // Activate the first ball
    s_balls[0].active = true;
    s_balls[0].x = s_paddle_x_float + (PADDLE_WIDTH_NORMAL / 2);
    s_balls[0].y = PADDLE_Y - BALL_RADIUS - 1;
    s_balls[0].vx = INITIAL_BALL_SPEED; // Start moving right
    s_balls[0].vy = -INITIAL_BALL_SPEED; // Start moving up
    s_active_balls_count = 1;

    // Initialize bricks (all active)
    s_bricks_remaining = 0;
    for (int r = 0; r < BRICK_ROWS; r++) {
        for (int c = 0; c < BRICK_COLS; c++) {
            s_bricks[r][c].x = c * (BRICK_WIDTH + BRICK_PADDING_X) + BRICK_PADDING_X;
            s_bricks[r][c].y = BRICK_START_Y + r * (BRICK_HEIGHT + BRICK_PADDING_Y);
            s_bricks[r][c].active = true; // All bricks are active
            s_bricks_remaining++;
        }
    }

    // Initialize movable obstacles
    for (int i = 0; i < NUM_MOVABLE_OBSTACLES; i++) {
        s_movable_obstacles[i].active = true;
        int min_y_spawn = BRICK_START_Y + BRICK_ROWS * (BRICK_HEIGHT + BRICK_PADDING_Y) + 5;
        int max_y_spawn = MOVABLE_OBSTACLE_MIN_Y_ABOVE_PADDLE - MOVABLE_OBSTACLE_HEIGHT;

        if (max_y_spawn < min_y_spawn) {
            max_y_spawn = min_y_spawn;
        }

        s_movable_obstacles[i].x = (float)(esp_random() % (OLED_WIDTH - MOVABLE_OBSTACLE_WIDTH));
        s_movable_obstacles[i].y = (float)(min_y_spawn + (esp_random() % (max_y_spawn - min_y_spawn + 1)));

        s_movable_obstacles[i].vx = (esp_random() % 2 == 0 ? 1 : -1) * MOVABLE_OBSTACLE_INITIAL_SPEED;
        s_movable_obstacles[i].vy = (esp_random() % 2 == 0 ? 1 : -1) * MOVABLE_OBSTACLE_INITIAL_SPEED;
    }

    // Initialize boost items
    for (int i = 0; i < MAX_BOOST_ITEMS; i++) {
        s_boost_items[i].active = false;
    }
    
    s_score = 0;
    s_lives = INITIAL_LIVES;
    s_game_over = false;
    s_game_is_active = true;
}

/**
 * @brief Handles player input for Arkanoid (paddle movement).
 * Reads the latest desired direction from the input queue.
 */
static void arkanoid_handle_input_impl() {
    int input_direction_from_queue = 0;
    if (s_game_input_queue_global != NULL && xQueueReceive(s_game_input_queue_global, &input_direction_from_queue, 0) == pdTRUE) {
        s_current_paddle_direction = input_direction_from_queue;
    } else {
        s_current_paddle_direction = 0;
    }
}

/**
 * @brief Updates the Arkanoid game state for one frame.
 * @return true if the game is over, false otherwise.
 */
static bool arkanoid_update_game_state_impl() {
    if (s_game_over) return true;

    // Update paddle enlargement timer
    if (s_paddle_enlarge_timer > 0) {
        s_paddle_enlarge_timer--;
        s_current_paddle_width = PADDLE_WIDTH_ENLARGED;
    } else { // Revert to normal size if no enlarge boost is active
        s_current_paddle_width = PADDLE_WIDTH_NORMAL; 
    }

    // NEW: Update obstacle pause timer
    bool obstacles_are_paused = false;
    if (s_obstacle_pause_timer > 0) {
        s_obstacle_pause_timer--;
        obstacles_are_paused = true;
    }

    // 1. Update Paddle Position based on s_current_paddle_direction
    s_paddle_x_float += (float)s_current_paddle_direction * PADDLE_SPEED;

    // Clamp paddle position to screen bounds
    if (s_paddle_x_float < 0) {
        s_paddle_x_float = 0;
    } else if (s_paddle_x_float + s_current_paddle_width > OLED_WIDTH) { // Use dynamic width
        s_paddle_x_float = OLED_WIDTH - s_current_paddle_width;
    }

    // 2. Update Ball Positions and handle collisions
    for (int ball_idx = 0; ball_idx < MAX_BALLS; ball_idx++) {
        if (!s_balls[ball_idx].active) continue;

        s_balls[ball_idx].x += s_balls[ball_idx].vx;
        s_balls[ball_idx].y += s_balls[ball_idx].vy;

        // Ball-Wall Collisions
        // Top wall
        if (s_balls[ball_idx].y - BALL_RADIUS < GAME_PLAY_AREA_START_Y) {
            s_balls[ball_idx].y = GAME_PLAY_AREA_START_Y + BALL_RADIUS;
            s_balls[ball_idx].vy *= -1;
        }
        // Left wall
        if (s_balls[ball_idx].x - BALL_RADIUS < 0) {
            s_balls[ball_idx].x = BALL_RADIUS;
            s_balls[ball_idx].vx *= -1;
        }
        // Right wall
        if (s_balls[ball_idx].x + BALL_RADIUS >= OLED_WIDTH) {
            s_balls[ball_idx].x = OLED_WIDTH - BALL_RADIUS - 1;
            s_balls[ball_idx].vx *= -1;
        }
        // Bottom wall (ball lost)
        if (s_balls[ball_idx].y + BALL_RADIUS >= OLED_HEIGHT) {
            s_balls[ball_idx].active = false; // Deactivate this ball
            s_active_balls_count--;
            ESP_LOGI(GAME_TAG, "Ball %d lost. Active balls: %d", ball_idx, s_active_balls_count);

            if (s_active_balls_count <= 0) {
                s_lives--;
                ESP_LOGI(GAME_TAG, "Life lost! Lives remaining: %d", s_lives);
                if (s_lives <= 0) {
                    s_game_over = true;
                    ESP_LOGI(GAME_TAG, "Game Over! No lives left.");
                    return true; // Game is over
                } else {
                    // Reset one ball to paddle if lives remain
                    s_balls[0].active = true;
                    s_balls[0].x = s_paddle_x_float + (s_current_paddle_width / 2);
                    s_balls[0].y = PADDLE_Y - BALL_RADIUS - 1;
                    s_balls[0].vx = INITIAL_BALL_SPEED; // Start moving right
                    s_balls[0].vy = -INITIAL_BALL_SPEED; // Start moving up
                    s_active_balls_count = 1;
                    ESP_LOGI(GAME_TAG, "New ball spawned after losing a life.");
                }
            }
            continue; // Skip further collision checks for this lost ball
        }

        // Ball-Paddle Collision
        if (s_balls[ball_idx].x + BALL_RADIUS > s_paddle_x_float &&
            s_balls[ball_idx].x - BALL_RADIUS < s_paddle_x_float + s_current_paddle_width) { // Use dynamic width
            if (s_balls[ball_idx].y + BALL_RADIUS >= PADDLE_Y && s_balls[ball_idx].vy > 0) {
                s_balls[ball_idx].y = PADDLE_Y - BALL_RADIUS - 1;
                s_balls[ball_idx].vy *= -1;

                float hit_point_relative_to_paddle = (s_balls[ball_idx].x - s_paddle_x_float) / s_current_paddle_width;
                s_balls[ball_idx].vx = (hit_point_relative_to_paddle - 0.5f) * 2.0f * MAX_BALL_SPEED;
            }
        }

        // Ball-Brick Collisions
        for (int r = 0; r < BRICK_ROWS; r++) {
            for (int c = 0; c < BRICK_COLS; c++) {
                if (s_bricks[r][c].active) {
                    int brick_left = s_bricks[r][c].x;
                    int brick_right = s_bricks[r][c].x + BRICK_WIDTH;
                    int brick_top = s_bricks[r][c].y;
                    int brick_bottom = s_bricks[r][c].y + BRICK_HEIGHT;

                    bool collision_x = s_balls[ball_idx].x + BALL_RADIUS > brick_left && s_balls[ball_idx].x - BALL_RADIUS < brick_right;
                    bool collision_y = s_balls[ball_idx].y + BALL_RADIUS > brick_top && s_balls[ball_idx].y - BALL_RADIUS < brick_bottom;

                    if (collision_x && collision_y) {
                        s_bricks[r][c].active = false; // Deactivate brick
                        s_bricks_remaining--;
                        s_score += 10; // Score for breaking a brick
                        ESP_LOGI(GAME_TAG, "Brick broken! Score: %d, Bricks remaining: %d", s_score, s_bricks_remaining);

                        // Boost Item Drop Chance (NEW)
                        if ((esp_random() % 100) < BOOST_ITEM_DROP_CHANCE_PERCENT) {
                            for (int i = 0; i < MAX_BOOST_ITEMS; i++) {
                                if (!s_boost_items[i].active) {
                                    s_boost_items[i].active = true;
                                    s_boost_items[i].x = (float)brick_left + (BRICK_WIDTH / 2.0f);
                                    s_boost_items[i].y = (float)brick_bottom + (BOOST_ITEM_HEIGHT / 2.0f);
                                    s_boost_items[i].vy = BOOST_ITEM_FALL_SPEED;
                                    // Randomly assign boost type from all available types (excluding NUM_BOOST_TYPES)
                                    s_boost_items[i].type = (boost_item_type_t)(esp_random() % NUM_BOOST_TYPES);
                                    ESP_LOGI(GAME_TAG, "Boost item dropped! Type: %d", s_boost_items[i].type);
                                    break; // Only drop one item per broken brick
                                }
                            }
                        }

                        // Determine which side of the brick was hit to reverse ball direction
                        float overlap_left = (s_balls[ball_idx].x + BALL_RADIUS) - brick_left;
                        float overlap_right = brick_right - (s_balls[ball_idx].x - BALL_RADIUS);
                        float overlap_top = (s_balls[ball_idx].y + BALL_RADIUS) - brick_top;
                        float overlap_bottom = brick_bottom - (s_balls[ball_idx].y - BALL_RADIUS);

                        bool hit_from_left = (s_balls[ball_idx].vx > 0) && (overlap_left < overlap_right) && (overlap_left < overlap_top) && (overlap_left < overlap_bottom);
                        bool hit_from_right = (s_balls[ball_idx].vx < 0) && (overlap_right < overlap_left) && (overlap_right < overlap_top) && (overlap_right < overlap_bottom);
                        bool hit_from_top = (s_balls[ball_idx].vy > 0) && (overlap_top < overlap_bottom) && (overlap_top < overlap_left) && (overlap_top < overlap_right);
                        bool hit_from_bottom = (s_balls[ball_idx].vy < 0) && (overlap_bottom < overlap_top) && (overlap_bottom < overlap_left) && (overlap_bottom < overlap_right);

                        if (hit_from_left || hit_from_right) {
                            s_balls[ball_idx].vx *= -1;
                        } else if (hit_from_top || hit_from_bottom) {
                            s_balls[ball_idx].vy *= -1;
                        } else {
                            s_balls[ball_idx].vx *= -1;
                            s_balls[ball_idx].vy *= -1;
                        }
                        break; // Only hit one brick per frame per ball
                    }
                }
            }
        }

        // Ball-Movable Obstacle Collision
        for (int i = 0; i < NUM_MOVABLE_OBSTACLES; i++) {
            if (s_movable_obstacles[i].active) {
                int obs_left = (int)s_movable_obstacles[i].x;
                int obs_right = (int)(s_movable_obstacles[i].x + MOVABLE_OBSTACLE_WIDTH);
                int obs_top = (int)s_movable_obstacles[i].y;
                int obs_bottom = (int)(s_movable_obstacles[i].y + MOVABLE_OBSTACLE_HEIGHT);

                bool collision_x = s_balls[ball_idx].x + BALL_RADIUS > obs_left && s_balls[ball_idx].x - BALL_RADIUS < obs_right;
                bool collision_y = s_balls[ball_idx].y + BALL_RADIUS > obs_top && s_balls[ball_idx].y - BALL_RADIUS < obs_bottom;

                if (collision_x && collision_y) {
                    float overlap_left = (s_balls[ball_idx].x + BALL_RADIUS) - obs_left;
                    float overlap_right = obs_right - (s_balls[ball_idx].x - BALL_RADIUS);
                    float overlap_top = (s_balls[ball_idx].y + BALL_RADIUS) - obs_top;
                    float overlap_bottom = obs_bottom - (s_balls[ball_idx].y - BALL_RADIUS);

                    bool hit_from_left = (s_balls[ball_idx].vx > 0) && (overlap_left < overlap_right) && (overlap_left < overlap_top) && (overlap_left < overlap_bottom);
                    bool hit_from_right = (s_balls[ball_idx].vx < 0) && (overlap_right < overlap_left) && (overlap_right < overlap_top) && (overlap_right < overlap_bottom);
                    bool hit_from_top = (s_balls[ball_idx].vy > 0) && (overlap_top < overlap_bottom) && (overlap_top < overlap_left) && (overlap_top < overlap_right);
                    bool hit_from_bottom = (s_balls[ball_idx].vy < 0) && (overlap_bottom < overlap_top) && (overlap_bottom < overlap_left) && (overlap_bottom < overlap_right);

                    if (hit_from_left || hit_from_right) {
                        s_balls[ball_idx].vx *= -1;
                    } else if (hit_from_top || hit_from_bottom) {
                        s_balls[ball_idx].vy *= -1;
                    } else {
                        s_balls[ball_idx].vx *= -1;
                        s_balls[ball_idx].vy *= -1;
                    }
                    break; // Only hit one obstacle per frame per ball
                }
            }
        }
    }

    // 3. Update Movable Obstacles (NEW: Check if paused)
    for (int i = 0; i < NUM_MOVABLE_OBSTACLES; i++) {
        if (s_movable_obstacles[i].active && !obstacles_are_paused) { // Only move if not paused
            s_movable_obstacles[i].x += s_movable_obstacles[i].vx;
            s_movable_obstacles[i].y += s_movable_obstacles[i].vy;

            // Obstacle-Wall Collisions (reflect)
            if (s_movable_obstacles[i].x < 0) {
                s_movable_obstacles[i].x = 0;
                s_movable_obstacles[i].vx *= -1;
            } else if (s_movable_obstacles[i].x + MOVABLE_OBSTACLE_WIDTH > OLED_WIDTH) {
                s_movable_obstacles[i].x = (float)(OLED_WIDTH - MOVABLE_OBSTACLE_WIDTH);
                s_movable_obstacles[i].vx *= -1;
            }

            // Obstacle-Vertical Boundary Collisions
            if (s_movable_obstacles[i].y < GAME_PLAY_AREA_START_Y) { // Top boundary
                s_movable_obstacles[i].y = (float)GAME_PLAY_AREA_START_Y;
                s_movable_obstacles[i].vy *= -1;
            } else if (s_movable_obstacles[i].y + MOVABLE_OBSTACLE_HEIGHT > MOVABLE_OBSTACLE_MIN_Y_ABOVE_PADDLE) { // Lower boundary
                s_movable_obstacles[i].y = (float)(MOVABLE_OBSTACLE_MIN_Y_ABOVE_PADDLE - MOVABLE_OBSTACLE_HEIGHT);
                s_movable_obstacles[i].vy *= -1;
            }
        }
    }

    // 4. Update and Handle Boost Item Collisions
    for (int i = 0; i < MAX_BOOST_ITEMS; i++) {
        if (s_boost_items[i].active) {
            s_boost_items[i].y += s_boost_items[i].vy; // Boost items fall downwards

            // If boost item goes off screen, deactivate it
            if (s_boost_items[i].y > OLED_HEIGHT) {
                s_boost_items[i].active = false;
                continue;
            }

            // Paddle-Boost Item Collision
            int item_left = (int)s_boost_items[i].x - (BOOST_ITEM_WIDTH / 2);
            int item_right = (int)s_boost_items[i].x + (BOOST_ITEM_WIDTH / 2);
            int item_top = (int)s_boost_items[i].y - (BOOST_ITEM_HEIGHT / 2);
            int item_bottom = (int)s_boost_items[i].y + (BOOST_ITEM_HEIGHT / 2);

            int paddle_left = (int)s_paddle_x_float;
            int paddle_right = (int)s_paddle_x_float + s_current_paddle_width;
            int paddle_top = PADDLE_Y;
            int paddle_bottom = PADDLE_Y + PADDLE_HEIGHT;

            bool collision_x = item_right > paddle_left && item_left < paddle_right;
            bool collision_y = item_bottom > paddle_top && item_top < paddle_bottom;

            if (collision_x && collision_y) {
                s_boost_items[i].active = false; // Deactivate collected item
                ESP_LOGI(GAME_TAG, "Boost item collected! Type: %d", s_boost_items[i].type);

                switch (s_boost_items[i].type) {
                    case BOOST_PADDLE_ENLARGE:
                        s_paddle_enlarge_timer = PADDLE_ENLARGE_DURATION_FRAMES;
                        // s_paddle_shorter_timer = 0; // No longer needed
                        ESP_LOGI(GAME_TAG, "Paddle enlarged!");
                        break;
                    case BOOST_DOUBLE_BALL:
                        if (s_active_balls_count < MAX_BALLS) {
                            for (int j = 0; j < MAX_BALLS; j++) {
                                if (!s_balls[j].active) {
                                    s_balls[j].active = true;
                                    // Spawn new ball near the first active ball, or paddle
                                    int source_ball_idx = 0; // Default to first ball
                                    for(int k=0; k<MAX_BALLS; k++) { // Find an active ball to copy from
                                        if(s_balls[k].active) {
                                            source_ball_idx = k;
                                            break;
                                        }
                                    }
                                    s_balls[j].x = s_balls[source_ball_idx].x + (esp_random() % 5) - 2; // Slight offset
                                    s_balls[j].y = s_balls[source_ball_idx].y + (esp_random() % 5) - 2;
                                    s_balls[j].vx = -s_balls[source_ball_idx].vx; // Opposite horizontal direction
                                    s_balls[j].vy = s_balls[source_ball_idx].vy;
                                    s_active_balls_count++;
                                    ESP_LOGI(GAME_TAG, "New ball spawned! Active balls: %d", s_active_balls_count);
                                    break; // Only spawn one new ball per power-up
                                }
                            }
                        } else {
                            ESP_LOGI(GAME_TAG, "Cannot double ball, max balls already active.");
                        }
                        break;
                    case BOOST_PAUSE_OBSTACLES: // NEW
                        s_obstacle_pause_timer = OBSTACLE_PAUSE_DURATION_FRAMES;
                        ESP_LOGI(GAME_TAG, "Obstacles paused!");
                        break;
                    // case BOOST_SHORTER_PADDLE: // REMOVED
                    //     s_paddle_shorter_timer = PADDLE_SHORTER_DURATION_FRAMES;
                    //     s_paddle_enlarge_timer = 0; // Cancel enlarge paddle if active
                    //     ESP_LOGI(GAME_TAG, "Paddle shortened!");
                    //     break;
                    default: // Handle NUM_BOOST_TYPES or any unexpected value
                        ESP_LOGW(GAME_TAG, "Unhandled boost item type: %d", s_boost_items[i].type);
                        break;
                }
            }
        }
    }

    // 5. Check Win/Lose Conditions
    if (s_bricks_remaining == 0) {
        s_game_over = true;
        ESP_LOGI(GAME_TAG, "You Win! All bricks cleared.");
        return true;
    }

    return s_game_over;
}

/**
 * @brief Draws the current Arkanoid game state to the OLED internal buffer.
 * @param dev Pointer to the SSD1306 device structure.
 */
static void arkanoid_draw_game_impl(SSD1306_t *dev) {
    // Clear the game play area
    draw_filled_rect(dev, 0, GAME_PLAY_AREA_START_Y, OLED_WIDTH - 1, OLED_HEIGHT - 1, false);

    // Draw the screen border around the game play area
    draw_rectangle_outline(dev, 0, GAME_PLAY_AREA_START_Y - 1, OLED_WIDTH - 1, OLED_HEIGHT - 1, true); // White border

    // Draw paddle
    s_paddle_x_int = (int)roundf(s_paddle_x_float); // Update integer position for drawing
    draw_filled_rect(dev, s_paddle_x_int, PADDLE_Y, s_paddle_x_int + s_current_paddle_width - 1, PADDLE_Y + PADDLE_HEIGHT - 1, true);

    // Draw balls
    for (int i = 0; i < MAX_BALLS; i++) {
        if (s_balls[i].active) {
            draw_filled_rect(dev, (int)roundf(s_balls[i].x) - BALL_RADIUS, (int)roundf(s_balls[i].y) - BALL_RADIUS,
                             (int)roundf(s_balls[i].x) + BALL_RADIUS - 1, (int)roundf(s_balls[i].y) + BALL_RADIUS - 1, true);
        }
    }

    // Draw bricks
    for (int r = 0; r < BRICK_ROWS; r++) {
        for (int c = 0; c < BRICK_COLS; c++) {
            if (s_bricks[r][c].active) {
                draw_filled_rect(dev, s_bricks[r][c].x, s_bricks[r][c].y,
                                 s_bricks[r][c].x + BRICK_WIDTH - 1, s_bricks[r][c].y + BRICK_HEIGHT - 1, true);
            }
        }
    }

    // Draw movable obstacles
    for (int i = 0; i < NUM_MOVABLE_OBSTACLES; i++) {
        if (s_movable_obstacles[i].active) {
            draw_filled_rect(dev, (int)roundf(s_movable_obstacles[i].x), (int)roundf(s_movable_obstacles[i].y),
                             (int)roundf(s_movable_obstacles[i].x) + MOVABLE_OBSTACLE_WIDTH - 1, (int)roundf(s_movable_obstacles[i].y) + MOVABLE_OBSTACLE_HEIGHT - 1, true);
        }
    }

    // Draw boost items
    for (int i = 0; i < MAX_BOOST_ITEMS; i++) {
        if (s_boost_items[i].active) {
            // Draw a square for the boost item
            draw_filled_rect(dev, (int)roundf(s_boost_items[i].x) - (BOOST_ITEM_WIDTH / 2), (int)roundf(s_boost_items[i].y) - (BOOST_ITEM_HEIGHT / 2),
                             (int)roundf(s_boost_items[i].x) + (BOOST_ITEM_WIDTH / 2) - 1, (int)roundf(s_boost_items[i].y) + (BOOST_ITEM_HEIGHT / 2) - 1, true);
            
            // Draw a character on the boost item to indicate its type
            char boost_char = '?';
            switch (s_boost_items[i].type) {
                case BOOST_PADDLE_ENLARGE:
                    boost_char = 'P'; // For Paddle
                    break;
                case BOOST_DOUBLE_BALL:
                    boost_char = 'B'; // For Ball
                    break;
                case BOOST_PAUSE_OBSTACLES:
                    boost_char = 'S'; // For Stop/Pause
                    break;
                // case BOOST_SHORTER_PADDLE: // REMOVED
                //     boost_char = 'L'; // For Less/Smaller
                //     break;
                default:
                    break;
            }
            ssd1306_draw_char(dev, (int)roundf(s_boost_items[i].x) - 2, (int)roundf(s_boost_items[i].y) - 4, boost_char, 1, false); // Draw char in black on white item
        }
    }
}

/**
 * @brief Gets the current Arkanoid score.
 * @return The current game score.
 */
static int arkanoid_get_score_impl() {
    return s_score;
}

/**
 * @brief Checks if the Arkanoid game is currently active.
 * @return True if the game is active, false otherwise.
 */
static bool arkanoid_is_active_impl() {
    return s_game_is_active && !s_game_over;
}

/**
 * @brief Sets whether the Arkanoid game should be active.
 * @param active True to make the game active, false otherwise.
 */
static void arkanoid_set_active_impl(bool active) {
    s_game_is_active = active;
}

/**
 * @brief Returns the FreeRTOS QueueHandle_t for Arkanoid's input.
 */
static QueueHandle_t arkanoid_get_input_queue_impl(void) {
    return s_game_input_queue_global;
}

/**
 * @brief Sets the global input queue for this game instance.
 * @param queue The QueueHandle_t to be used for game input.
 */
static void arkanoid_set_input_queue_impl(QueueHandle_t queue) {
    s_game_input_queue_global = queue;
    ESP_LOGI(GAME_TAG, "Arkanoid input queue set.");
}

// Define the Arkanoid game instance, implementing the generic game_t interface
const game_t ArkanoidGame = {
    .init_game = arkanoid_init_game_impl,
    .handle_input = arkanoid_handle_input_impl,
    .update_game_state = arkanoid_update_game_state_impl,
    .draw_game = arkanoid_draw_game_impl,
    .get_score = arkanoid_get_score_impl,
    .is_active = arkanoid_is_active_impl,
    .set_active = arkanoid_set_active_impl,
    .get_input_queue = arkanoid_get_input_queue_impl,
    .set_input_queue = arkanoid_set_input_queue_impl,
};