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