#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 }