Newer
Older
ESP32-RetroPlay / main / tasks / game / jump_bird_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"
#include "esp_log.h"

#include "ssd1306.h"        // OLED driver functions
#include "font8x8_basic.h" // Font for text

#include "jump_bird_game.h" // Header for this game module
#include "jump_bird_game_input_queue.h" // For xJumpBirdGameInputQueue

#define GAME_TAG "JUMP_BIRD_GAME"

// OLED dimensions (copied from display_task for clarity)
#define OLED_WIDTH 128
#define OLED_HEIGHT 64

// --- Game Specific Definitions ---
#define BIRD_WIDTH 8      // Bird sprite width in pixels
#define BIRD_HEIGHT 8     // Bird sprite height in pixels
#define BIRD_START_X 16   // Starting X position of the bird

#define OBSTACLE_WIDTH 12       // Width of each pipe segment
#define OBSTACLE_GAP_HEIGHT 24  // Vertical gap for the bird to fly through
#define INITIAL_OBSTACLE_SPEED 2        // Initial pixels per frame obstacles move left
#define OBSTACLE_SPEED_INCREMENT 0.2f // How much speed increases per score increment
#define MAX_OBSTACLE_SPEED 5.0f // Maximum speed for obstacles

#define OBSTACLE_SPAWN_INTERVAL_MIN 40 // Min frames between new obstacle spawns (approx 0.4s at 10ms frame)
#define OBSTACLE_SPAWN_INTERVAL_MAX 80 // Max frames between new obstacle spawns (approx 0.8s at 10ms frame)

// New defines for vertical obstacle movement
#define OBSTACLE_VERTICAL_AMPLITUDE_PX 8.0f // Total vertical swing of the gap (e.g., 8px up, 8px down from center)
#define OBSTACLE_VERTICAL_SPEED_PX_PER_FRAME 0.2f // Speed of vertical movement in pixels per frame
#define SCORE_TO_ACTIVATE_VERTICAL_MOVEMENT 5 // Score at which vertical obstacle movement begins

// Define the reserved area for the score
#define SCORE_DISPLAY_HEIGHT 16 // Height of two font lines (16 pixels for font8x8_basic) - Increased for more space
#define GAME_PLAY_AREA_START_Y SCORE_DISPLAY_HEIGHT
#define GAME_PLAY_AREA_HEIGHT (OLED_HEIGHT - SCORE_DISPLAY_HEIGHT)

// Simple bird bitmap (8x8 pixels)
const uint8_t bird_bitmap[8] = {
    0b00011000,
    0b00111100,
    0b01111110,
    0b11111111,
    0b01111110,
    0b00111100,
    0b00011000,
    0b00000000
};

// Structure to define an obstacle (pipe)
typedef struct {
    int x;            // X position of the obstacle (left edge)
    int prev_x;       // Previous X position for erasing
    int gap_y_center; // Y center of the gap (base position without vertical movement)
    bool passed;      // True if bird has passed this obstacle for scoring
    bool active;      // True if obstacle is currently on screen
    float current_vertical_offset; // Current vertical offset from gap_y_center
    float vertical_move_velocity; // Speed and direction of vertical movement (+ for up, - for down)
} obstacle_t;

#define MAX_OBSTACLES 3 // Max number of obstacles on screen at once

// --- Game State Variables (static to this file) ---
static float s_bird_y_float; // Changed to float for smoother sub-pixel movement
static int s_bird_prev_y;    // Previous Y position for erasing (integer for drawing bounds)
static float s_bird_velocity;

// Adjusted values for a more natural feel
static float s_gravity = 0.25f;      // Increased gravity slightly
static float s_jump_strength = -2.0f; // Significantly stronger jump (negative for up)

// Added velocity clamping to prevent excessive speeds
static const float MAX_FALL_SPEED = 3.5f; // Maximum downward velocity
static const float MAX_JUMP_SPEED = -6.0f; // Maximum upward velocity (absolute value)

static float s_current_obstacle_speed; // Dynamic obstacle speed

static obstacle_t s_obstacles[MAX_OBSTACLES];
static int s_score;
static bool s_game_over;
static int s_frames_since_last_obstacle;
static int s_next_obstacle_spawn_frames;

static bool s_game_is_active = false; // New flag to control if the game loop should run

// Helper function to draw the bird bitmap at a given position and color
void draw_bird_bitmap(SSD1306_t *dev, int x, int y, bool color) {
    for (int y_offset = 0; y_offset < BIRD_HEIGHT; y_offset++) {
        for (int x_offset = 0; x_offset < BIRD_WIDTH; x_offset++) {
            if ((bird_bitmap[y_offset] >> (BIRD_WIDTH - 1 - x_offset)) & 0x01) {
                // Only draw the pixel if it's part of the bitmap, and with the specified color
                _ssd1306_pixel(dev, x + x_offset, y + y_offset, color);
            } else {
                // If the bitmap pixel is OFF, ensure it's set to background color (black)
                // This is crucial for proper erasure when the bird moves.
                _ssd1306_pixel(dev, x + x_offset, y + x_offset, false);
            }
        }
    }
}

// Function to draw a filled rectangle (already exists, but included for context)
void draw_filled_rect(SSD1306_t *dev, int x1, int y1, int x2, int y2, bool color) {
    if (x1 > x2) { int t = x1; x1 = x2; x2 = t; }
    if (y1 > y2) { int t = y1; y1 = y2; y2 = t; }

    x1 = (x1 < 0) ? 0 : x1;
    y1 = (y1 < 0) ? 0 : y1;
    x2 = (x2 >= dev->_width)  ? dev->_width - 1 : x2;
    y2 = (y2 >= dev->_height) ? dev->_height - 1 : y2;

    for (int y = y1; y <= y2; y++) {
        for (int x = x1; x <= x2; x++) {
            _ssd1306_pixel(dev, x, y, color);
        }
    }
}

// Reverted: Function to draw an outline rectangle (draws all 4 sides)
void draw_rectangle_outline(SSD1306_t *dev, int x1, int y1, int x2, int y2, bool color) {
    // Ensure coordinates are ordered correctly
    if (x1 > x2) { int t = x1; x1 = x2; x2 = t; }
    if (y1 > y2) { int t = y1; y1 = y2; y2 = t; }

    // Draw top line
    for (int x = x1; x <= x2; x++) {
        _ssd1306_pixel(dev, x, y1, color);
    }
    // Draw bottom line
    for (int x = x1; x <= x2; x++) {
        _ssd1306_pixel(dev, x, y2, color);
    }
    // Draw left line
    for (int y = y1; y <= y2; y++) {
        _ssd1306_pixel(dev, x1, y, color);
    }
    // Draw right line
    for (int y = y1; y <= y2; y++) {
        _ssd1306_pixel(dev, x2, y, color);
    }
}

/**
 * @brief Initializes the game state (bird position, obstacles, score).
 * This should be called once before starting the game loop.
 */
void jump_bird_init_game() {
    ESP_LOGI(GAME_TAG, "Initializing Jump Bird Game state...");
    // Seed the random number generator
    srand((unsigned int)esp_random());

    // Bird starts in the middle of the PLAY AREA (using float for position)
    s_bird_y_float = (float)(GAME_PLAY_AREA_START_Y + (GAME_PLAY_AREA_HEIGHT / 2) - (BIRD_HEIGHT / 2));
    s_bird_prev_y = (int)s_bird_y_float; // Initialize previous Y as int for drawing reference
    s_bird_velocity = 0;

    for (int i = 0; i < MAX_OBSTACLES; i++) {
        s_obstacles[i].active = false;
        s_obstacles[i].prev_x = -999; // Initialize to an impossible value
        s_obstacles[i].current_vertical_offset = 0.0f; // Initialize vertical offset
        s_obstacles[i].vertical_move_velocity = OBSTACLE_VERTICAL_SPEED_PX_PER_FRAME; // Start moving up
    }

    s_score = 0;
    s_game_over = false;
    s_frames_since_last_obstacle = 0;
    s_next_obstacle_spawn_frames = (rand() % (OBSTACLE_SPAWN_INTERVAL_MAX - OBSTACLE_SPAWN_INTERVAL_MIN + 1)) + OBSTACLE_SPAWN_INTERVAL_MIN;
    s_current_obstacle_speed = INITIAL_OBSTACLE_SPEED; // Initialize obstacle speed

    // Clear any pending jump signals from previous game runs
    if (xJumpBirdGameInputQueue != NULL) {
        xQueueReset(xJumpBirdGameInputQueue);
        ESP_LOGI(GAME_TAG, "xJumpBirdGameInputQueue reset during game initialization.");
    } else {
        ESP_LOGE(GAME_TAG, "xJumpBirdGameInputQueue is NULL during init! Cannot reset.");
    }
    s_game_is_active = true; // Set game as active after initialization
}

/**
 * @brief Handles player input for the game.
 * Reads jump signals from the input queue and applies to bird.
 */
void jump_bird_handle_input() {
    bool jump_signal = false;
    // Check if a jump signal is in the queue without blocking
    if (xJumpBirdGameInputQueue != NULL) {
        if (xQueueReceive(xJumpBirdGameInputQueue, &jump_signal, 0) == pdTRUE) {
            if (jump_signal) {
                // Apply jump strength. Flappy bird typically allows flapping even if going up.
                s_bird_velocity = s_jump_strength;
                ESP_LOGD(GAME_TAG, "Jump triggered! Velocity: %.2f. Ticks: %lu", s_bird_velocity, xTaskGetTickCount());
            }
        } else {
            ESP_LOGD(GAME_TAG, "No jump signal in queue (handle_input). Ticks: %lu", xTaskGetTickCount());
        }
    } else {
        ESP_LOGE(GAME_TAG, "xJumpBirdGameInputQueue is NULL in jump_bird_handle_input! Cannot receive signal.");
    }
}

/**
 * @brief Updates the game state for one frame (bird physics, obstacle movement, collision detection, scoring).
 * @return true if the game is over, false otherwise.
 */
bool jump_bird_update_game_state() {
    if (s_game_over) return true; // If already game over, just return true

    // Store previous bird Y before updating its position (for drawing purposes)
    s_bird_prev_y = (int)s_bird_y_float;

    // Log bird position before update
    ESP_LOGD(GAME_TAG, "Bird Y_float before update: %.2f, Velocity: %.2f", s_bird_y_float, s_bird_velocity);

    // 1. Update Bird Physics
    s_bird_velocity += s_gravity;
    s_bird_y_float += s_bird_velocity; // Update bird position using float velocity

    // Apply velocity clamping
    if (s_bird_velocity > MAX_FALL_SPEED) {
        s_bird_velocity = MAX_FALL_SPEED;
    }
    if (s_bird_velocity < MAX_JUMP_SPEED) {
        s_bird_velocity = MAX_JUMP_SPEED;
    }

    // Keep bird within screen bounds (top and bottom of GAME PLAY AREA)
    if (s_bird_y_float < GAME_PLAY_AREA_START_Y) { // Collision with top of game area (score line)
        s_bird_y_float = (float)GAME_PLAY_AREA_START_Y;
        s_bird_velocity = 0; // Stop vertical movement
        ESP_LOGD(GAME_TAG, "Bird hit top game boundary. Y_float: %.2f", s_bird_y_float);
    }
    if (s_bird_y_float + BIRD_HEIGHT > OLED_HEIGHT) { // Collision with bottom of screen
        s_bird_y_float = (float)(OLED_HEIGHT - BIRD_HEIGHT);
        s_game_over = true; // Hit the ground
        ESP_LOGI(GAME_TAG, "Bird hit ground. Game Over.");
        return true;
    }
    // Log bird position after update
    ESP_LOGD(GAME_TAG, "Bird Y_float after update: %.2f, Velocity: %.2f", s_bird_y_float, s_bird_velocity);


    // 2. Update Obstacles and Spawn New Ones
    s_frames_since_last_obstacle++;
    for (int i = 0; i < MAX_OBSTACLES; i++) {
        if (s_obstacles[i].active) {
            s_obstacles[i].prev_x = s_obstacles[i].x; // Store previous X for obstacle
            s_obstacles[i].x -= (int)s_current_obstacle_speed; // Use dynamic speed

            // Apply vertical movement to obstacle gap if score is high enough
            if (s_score >= SCORE_TO_ACTIVATE_VERTICAL_MOVEMENT) {
                s_obstacles[i].current_vertical_offset += s_obstacles[i].vertical_move_velocity;

                // Reverse direction if hitting amplitude limits
                if (s_obstacles[i].current_vertical_offset >= (OBSTACLE_VERTICAL_AMPLITUDE_PX / 2.0f)) {
                    s_obstacles[i].current_vertical_offset = (OBSTACLE_VERTICAL_AMPLITUDE_PX / 2.0f); // Clamp
                    s_obstacles[i].vertical_move_velocity *= -1.0f;
                } else if (s_obstacles[i].current_vertical_offset <= -(OBSTACLE_VERTICAL_AMPLITUDE_PX / 2.0f)) {
                    s_obstacles[i].current_vertical_offset = -(OBSTACLE_VERTICAL_AMPLITUDE_PX / 2.0f); // Clamp
                    s_obstacles[i].vertical_move_velocity *= -1.0f;
                }
            }


            // If obstacle moves off screen, deactivate it
            if (s_obstacles[i].x + OBSTACLE_WIDTH < 0) {
                s_obstacles[i].active = false;
                // No need to set prev_x here as it's deactivated (will be -999 for erase)
                ESP_LOGD(GAME_TAG, "Obstacle %d moved off screen, deactivated.", i);
            }
        }
    }

    // Spawn new obstacle if needed
    if (s_frames_since_last_obstacle >= s_next_obstacle_spawn_frames) {
        for (int i = 0; i < MAX_OBSTACLES; i++) {
            if (!s_obstacles[i].active) {
                s_obstacles[i].x = OLED_WIDTH; // Start from right edge
                s_obstacles[i].prev_x = OLED_WIDTH; // Initialize prev_x for new obstacle

                // Calculate playable vertical range for gap center, considering vertical movement amplitude
                int playable_min_gap_center_y = GAME_PLAY_AREA_START_Y + OBSTACLE_GAP_HEIGHT / 2;
                int playable_max_gap_center_y = OLED_HEIGHT - OBSTACLE_GAP_HEIGHT / 2;

                int min_gap_y_center_for_spawn = playable_min_gap_center_y + (int)roundf(OBSTACLE_VERTICAL_AMPLITUDE_PX / 2.0f);
                int max_gap_y_center_for_spawn = playable_max_gap_center_y - (int)roundf(OBSTACLE_VERTICAL_AMPLITUDE_PX / 2.0f);

                // Ensure the spawn range is valid
                if (min_gap_y_center_for_spawn > max_gap_y_center_for_spawn) {
                    min_gap_y_center_for_spawn = max_gap_y_center_for_spawn; // Fallback to a single point if range is too small
                }

                s_obstacles[i].gap_y_center = (rand() % (max_gap_y_center_for_spawn - min_gap_y_center_for_spawn + 1)) + min_gap_y_center_for_spawn;

                s_obstacles[i].passed = false;
                s_obstacles[i].active = true;
                s_obstacles[i].current_vertical_offset = 0.0f; // Reset vertical offset for new obstacle
                // Randomize initial vertical direction for new obstacles
                s_obstacles[i].vertical_move_velocity = (rand() % 2 == 0) ? OBSTACLE_VERTICAL_SPEED_PX_PER_FRAME : -OBSTACLE_VERTICAL_SPEED_PX_PER_FRAME;

                s_frames_since_last_obstacle = 0;
                s_next_obstacle_spawn_frames = (rand() % (OBSTACLE_SPAWN_INTERVAL_MAX - OBSTACLE_SPAWN_INTERVAL_MIN + 1)) + OBSTACLE_SPAWN_INTERVAL_MIN;
                ESP_LOGD(GAME_TAG, "New obstacle spawned at x=%d, gap_y_center=%d", s_obstacles[i].x, s_obstacles[i].gap_y_center);
                break; // Only spawn one new obstacle at a time
            }
        }
    }

    // 3. Collision Detection & Scoring
    for (int i = 0; i < MAX_OBSTACLES; i++) {
        if (!s_obstacles[i].active) continue; // Only check active obstacles

        // Calculate effective gap center for collision and drawing
        int effective_gap_y_center = s_obstacles[i].gap_y_center + (int)roundf(s_obstacles[i].current_vertical_offset);

        // Bird Bounding Box: (BIRD_START_X, (int)s_bird_y_float, BIRD_WIDTH, BIRD_HEIGHT)
        // Obstacle Bounding Box: (s_obstacles[i].x, 0 to top_pipe_height, OBSTACLE_WIDTH, OLED_HEIGHT from bottom_pipe_y)

        // Check horizontal overlap
        bool horizontal_overlap = (BIRD_START_X < s_obstacles[i].x + OBSTACLE_WIDTH) &&
                                  (BIRD_START_X + BIRD_WIDTH > s_obstacles[i].x);

        if (horizontal_overlap) {
            // Calculate pipe boundaries based on effective gap center
            int top_pipe_bottom_y = effective_gap_y_center - OBSTACLE_GAP_HEIGHT / 2;
            int bottom_pipe_top_y = effective_gap_y_center + OBSTACLE_GAP_HEIGHT / 2;

            // Clamp pipe boundaries to the game play area for accurate collision
            if (top_pipe_bottom_y < GAME_PLAY_AREA_START_Y) top_pipe_bottom_y = GAME_PLAY_AREA_START_Y;
            if (bottom_pipe_top_y > OLED_HEIGHT) bottom_pipe_top_y = OLED_HEIGHT;

            // Check vertical overlap with top pipe
            if ((int)s_bird_y_float < top_pipe_bottom_y) { // Bird's top pixel is above pipe bottom
                s_game_over = true;
                ESP_LOGI(GAME_TAG, "Collision with top pipe. Game Over.");
                return true;
            }
            // Check vertical overlap with bottom pipe
            if ((int)s_bird_y_float + BIRD_HEIGHT > bottom_pipe_top_y) { // Bird's bottom pixel is below pipe top
                s_game_over = true;
                ESP_LOGI(GAME_TAG, "Collision with bottom pipe. Game Over.");
                return true;
            }
        }

        // Scoring
        // If bird has passed the obstacle's X position and hasn't scored yet for this obstacle
        if (BIRD_START_X > s_obstacles[i].x + OBSTACLE_WIDTH && !s_obstacles[i].passed) {
            s_score++;
            s_obstacles[i].passed = true;
            ESP_LOGI(GAME_TAG, "Score: %d", s_score);

            // Increase obstacle speed, but cap it at MAX_OBSTACLE_SPEED
            s_current_obstacle_speed += OBSTACLE_SPEED_INCREMENT;
            if (s_current_obstacle_speed > MAX_OBSTACLE_SPEED) {
                s_current_obstacle_speed = MAX_OBSTACLE_SPEED;
            }
            ESP_LOGD(GAME_TAG, "Obstacle speed increased to %.2f", s_current_obstacle_speed);
        }
    }
    return s_game_over;
}

/**
 * @brief Draws the current game state to the OLED internal buffer.
 * Does not call ssd1306_show_buffer().
 * @param dev Pointer to the SSD1306 device structure.
 */
void jump_bird_draw_game(SSD1306_t *dev) {
    ESP_LOGD(GAME_TAG, "jump_bird_draw_game entered. Bird Y: %d (Prev Y: %d). Score: %d.", (int)s_bird_y_float, s_bird_prev_y, s_score);

    // 1. Clear the game play area for dynamic elements (bird, obstacles)
    // This is done *before* drawing anything else in the game area.
    // NOTE: This rect starts from GAME_PLAY_AREA_START_Y (16 pixels down),
    // leaving the top 16 pixels (lines 0 and 1) untouched for the score.
    draw_filled_rect(dev, 0, GAME_PLAY_AREA_START_Y, OLED_WIDTH - 1, OLED_HEIGHT - 1, false);

    // 2. Draw the screen border (outline)
    // Draw this after clearing the game area, but before drawing game elements,
    // so game elements draw on top of the border.
    // Draw border around the game play area (from GAME_PLAY_AREA_START_Y - 1, to OLED_HEIGHT - 1)
    draw_rectangle_outline(dev, 0, GAME_PLAY_AREA_START_Y - 1, OLED_WIDTH - 1, OLED_HEIGHT - 1, true); // White border

    // 3. Draw bird
    draw_bird_bitmap(dev, BIRD_START_X, (int)s_bird_y_float, true); // Draw (draw with white), cast float to int for drawing


    // 4. Draw obstacles
    for (int i = 0; i < MAX_OBSTACLES; i++) {
        if (!s_obstacles[i].active) {
            continue;
        }

        // Calculate effective gap center for drawing
        int effective_gap_y_center = s_obstacles[i].gap_y_center + (int)roundf(s_obstacles[i].current_vertical_offset);

        ESP_LOGD(GAME_TAG, "Drawing obstacle %d: active=%d, x=%d (prev_x=%d), gap_y_center=%d (effective=%d)",
                             i, s_obstacles[i].active, s_obstacles[i].x, s_obstacles[i].prev_x, s_obstacles[i].gap_y_center, effective_gap_y_center);

        // Top pipe (current position)
        int top_pipe_height = effective_gap_y_center - OBSTACLE_GAP_HEIGHT / 2;
        // Clamp top pipe to not go into score area
        if (top_pipe_height < GAME_PLAY_AREA_START_Y) top_pipe_height = GAME_PLAY_AREA_START_Y;

        if (top_pipe_height >= GAME_PLAY_AREA_START_Y) { // Only draw if it's within or below the score area
            ESP_LOGD(GAME_TAG, "Drawing top pipe: (%d, %d) to (%d, %d)",
                                 s_obstacles[i].x, GAME_PLAY_AREA_START_Y, s_obstacles[i].x + OBSTACLE_WIDTH - 1, top_pipe_height - 1);
            draw_filled_rect(dev, s_obstacles[i].x, GAME_PLAY_AREA_START_Y, s_obstacles[i].x + OBSTACLE_WIDTH - 1, top_pipe_height - 1, true);
        }

        // Bottom pipe (current position)
        int bottom_pipe_y = effective_gap_y_center + OBSTACLE_GAP_HEIGHT / 2;
        if (bottom_pipe_y < OLED_HEIGHT) {
            ESP_LOGD(GAME_TAG, "Drawing bottom pipe: (%d, %d) to (%d, %d)",
                                 s_obstacles[i].x, bottom_pipe_y, s_obstacles[i].x + OBSTACLE_WIDTH - 1, OLED_HEIGHT - 1);
            draw_filled_rect(dev, s_obstacles[i].x, bottom_pipe_y, s_obstacles[i].x + OBSTACLE_WIDTH - 1, OLED_HEIGHT - 1, true);
        }
    }

    // IMPORTANT: Score drawing logic has been moved to display_task.c
    // This function should only draw game elements (bird, obstacles, game area border).
}

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

/**
 * @brief Sets whether the game should be active.
 * Used to start/stop the game loop in the display manager.
 * @param active True to make the game active, false otherwise.
 */
void jump_bird_set_active(bool active) {
    s_game_is_active = active;
}

/**
 * @brief Checks if the game is currently active.
 * @return True if the game is active, false otherwise.
 */
bool jump_bird_is_active() {
    return s_game_is_active && !s_game_over; // Game is active if flag is true AND not game over
}