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