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 "game_drawing.h"
#include "ssd1306.h"        // OLED driver functions
#include "game_framework.h" // Include game framework
#include "game_drawing.h" // For drawing functions
#define GAME_TAG "ARKANOID_GAME"

// --- Game Specific Definitions ---
// Paddle
#define PADDLE_WIDTH 24
#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

// Bricks
#define BRICK_ROWS 3
#define BRICK_COLS 8
#define BRICK_WIDTH (OLED_WIDTH / BRICK_COLS) - 1 // Leave a 1-pixel gap
#define BRICK_HEIGHT 4
#define BRICK_START_Y (GAME_PLAY_AREA_START_Y + 2) // Start below score area, with some padding
#define BRICK_PADDING_X 1
#define BRICK_PADDING_Y 1

// Game State
#define INITIAL_LIVES 3

// Structure for a brick
typedef struct {
    int x, y;
    bool active;
} brick_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 float s_ball_x_float, s_ball_y_float;
static float s_ball_vx_float, s_ball_vy_float;

static brick_t s_bricks[BRICK_ROWS][BRICK_COLS];
static int s_score;
static int s_lives;
static int s_bricks_remaining;
static bool s_game_over;
static bool s_game_is_active;

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


/**
 * @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.
    // The queue itself is created and managed externally (in main.c)
    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 / 2);
    s_paddle_x_int = (int)s_paddle_x_float;
    s_current_paddle_direction = 0; // Initialize paddle as stationary

    // Initialize ball (start on top of paddle)
    s_ball_x_float = s_paddle_x_float + (PADDLE_WIDTH / 2);
    s_ball_y_float = PADDLE_Y - BALL_RADIUS - 1;
    s_ball_vx_float = INITIAL_BALL_SPEED; // Start moving right
    s_ball_vy_float = -INITIAL_BALL_SPEED; // Start moving up

    // Initialize bricks
    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;
            s_bricks_remaining++;
        }
    }

    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;
    // Receive the latest value from the queue without blocking.
    // This will consume the item from the queue, ensuring the queue is ready for the next update.
    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;
        ESP_LOGD(GAME_TAG, "Arkanoid input received: %d, current_paddle_direction: %d", input_direction_from_queue, s_current_paddle_direction);
    } else {
        // If no new input, the paddle should stop moving unless a button is held down.
        // The button_task now continuously sends 0 when no buttons are pressed.
        // So, if xQueueReceive returns pdFALSE, it means the queue is empty,
        // which implies the last sent signal was likely 0 (no movement).
        // We ensure s_current_paddle_direction is 0 if no new input is available.
        s_current_paddle_direction = 0;
        ESP_LOGD(GAME_TAG, "No new Arkanoid input. Current paddle direction set to 0 (stationary).");
    }
}

/**
 * @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;

    // 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 + PADDLE_WIDTH > OLED_WIDTH) {
        s_paddle_x_float = OLED_WIDTH - PADDLE_WIDTH;
    }

    // 2. Update Ball Position
    s_ball_x_float += s_ball_vx_float;
    s_ball_y_float += s_ball_vy_float;

    // 3. Ball-Wall Collisions
    // Top wall (GAME_PLAY_AREA_START_Y is the top boundary for game elements)
    if (s_ball_y_float - BALL_RADIUS < GAME_PLAY_AREA_START_Y) {
        s_ball_y_float = GAME_PLAY_AREA_START_Y + BALL_RADIUS;
        s_ball_vy_float *= -1;
    }
    // Left wall
    if (s_ball_x_float - BALL_RADIUS < 0) {
        s_ball_x_float = BALL_RADIUS;
        s_ball_vx_float *= -1;
    }
    // Right wall
    if (s_ball_x_float + BALL_RADIUS >= OLED_WIDTH) {
        s_ball_x_float = OLED_WIDTH - BALL_RADIUS - 1;
        s_ball_vx_float *= -1;
    }
    // Bottom wall (lose a life)
    if (s_ball_y_float + BALL_RADIUS >= OLED_HEIGHT) {
        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;
        } else {
            // Reset ball to paddle
            s_ball_x_float = s_paddle_x_float + (PADDLE_WIDTH / 2);
            s_ball_y_float = PADDLE_Y - BALL_RADIUS - 1;
            s_ball_vx_float = INITIAL_BALL_SPEED;
            s_ball_vy_float = -INITIAL_BALL_SPEED;
        }
    }

    // 4. Ball-Paddle Collision
    // Check if ball is horizontally aligned with paddle
    if (s_ball_x_float + BALL_RADIUS > s_paddle_x_float &&
        s_ball_x_float - BALL_RADIUS < s_paddle_x_float + PADDLE_WIDTH) {
        // Check if ball is vertically colliding with paddle from above
        if (s_ball_y_float + BALL_RADIUS >= PADDLE_Y && s_ball_vy_float > 0) {
            s_ball_y_float = PADDLE_Y - BALL_RADIUS - 1; // Move ball above paddle
            s_ball_vy_float *= -1; // Reverse vertical direction

            // Adjust horizontal velocity based on where it hit the paddle
            float hit_point_relative_to_paddle = (s_ball_x_float - s_paddle_x_float) / PADDLE_WIDTH; // 0 to 1
            s_ball_vx_float = (hit_point_relative_to_paddle - 0.5f) * 2.0f * MAX_BALL_SPEED; // -MAX_BALL_SPEED to +MAX_BALL_SPEED
        }
    }

    // 5. 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;

                // Simple AABB collision check
                bool collision_x = s_ball_x_float + BALL_RADIUS > brick_left && s_ball_x_float - BALL_RADIUS < brick_right;
                bool collision_y = s_ball_y_float + BALL_RADIUS > brick_top && s_ball_y_float - 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);

                    // Determine which side of the brick was hit to reverse ball direction
                    // This is a simplified reflection logic
                    float overlap_left = (s_ball_x_float + BALL_RADIUS) - brick_left;
                    float overlap_right = brick_right - (s_ball_x_float - BALL_RADIUS);
                    float overlap_top = (s_ball_y_float + BALL_RADIUS) - brick_top;
                    float overlap_bottom = brick_bottom - (s_ball_y_float - BALL_RADIUS);

                    bool hit_from_left = (s_ball_vx_float > 0) && (overlap_left < overlap_right) && (overlap_left < overlap_top) && (overlap_left < overlap_bottom);
                    bool hit_from_right = (s_ball_vx_float < 0) && (overlap_right < overlap_left) && (overlap_right < overlap_top) && (overlap_right < overlap_bottom);
                    bool hit_from_top = (s_ball_vy_float > 0) && (overlap_top < overlap_bottom) && (overlap_top < overlap_left) && (overlap_top < overlap_right);
                    bool hit_from_bottom = (s_ball_vy_float < 0) && (overlap_bottom < overlap_top) && (overlap_bottom < overlap_left) && (overlap_bottom < overlap_right);

                    if (hit_from_left || hit_from_right) {
                        s_ball_vx_float *= -1;
                    } else if (hit_from_top || hit_from_bottom) {
                        s_ball_vy_float *= -1;
                    } else {
                        // Corner hit or complex collision, just reverse both for simplicity
                        s_ball_vx_float *= -1;
                        s_ball_vy_float *= -1;
                    }
                    break; // Only hit one brick per frame
                }
            }
        }
    }

    // 6. 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 + PADDLE_WIDTH - 1, PADDLE_Y + PADDLE_HEIGHT - 1, true);

    // Draw ball
    draw_filled_rect(dev, (int)roundf(s_ball_x_float) - BALL_RADIUS, (int)roundf(s_ball_y_float) - BALL_RADIUS,
                     (int)roundf(s_ball_x_float) + BALL_RADIUS - 1, (int)roundf(s_ball_y_float) + 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);
            }
        }
    }
}

/**
 * @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, // NEW: Pointer to the set input queue function
};