#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <sys/unistd.h>
#include <sys/stat.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_system.h"
#include "esp_log.h"
#include "driver/i2c.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "driver/ledc.h"
#include "esp_vfs_fat.h"
#include "sdmmc_cmd.h"
#include "driver/sdmmc_host.h"
static const char *TAG = "GAME_SYSTEM";
// Definições de Hardware
#define I2C_MPU_MASTER_SCL_IO 9
#define I2C_MPU_MASTER_SDA_IO 8
#define I2C_OLED_MASTER_SCL_IO 17
#define I2C_OLED_MASTER_SDA_IO 18
#define I2C_MASTER_FREQ_HZ 400000
#define I2C_MASTER_TX_BUF_DISABLE 0
#define I2C_MASTER_RX_BUF_DISABLE 0
#define I2C_MASTER_TIMEOUT_MS 1000
#define MPU6050_ADDR 0x68
#define SSD1306_ADDR 0x3C
#define SD_MISO_PIN 13
#define SD_MOSI_PIN 11
#define SD_CLK_PIN 12
#define SD_CS_PIN 10
#define BUZZER_PIN 6
#define BUTTON1_PIN 3
#define BUTTON2_PIN 4
#define LEDC_TIMER LEDC_TIMER_0
#define LEDC_MODE LEDC_LOW_SPEED_MODE
#define LEDC_CHANNEL LEDC_CHANNEL_0
#define LEDC_DUTY_RES LEDC_TIMER_13_BIT
#define LEDC_FREQUENCY (4000)
// Definições do Display
#define SSD1306_WIDTH 128
#define SSD1306_HEIGHT 64
#define SSD1306_PAGES 8
// Estados do Sistema
typedef enum {
STATE_MENU,
STATE_GAME1_TILT,
STATE_GAME2_SHAKE,
STATE_GAME3_BALANCE,
STATE_GAME4_REACTION,
STATE_SHOW_RECORD,
STATE_GAME_OVER
} system_state_t;
// Estrutura de dados do MPU6050
typedef struct {
float accel_x;
float accel_y;
float accel_z;
float gyro_x;
float gyro_y;
float gyro_z;
} mpu6050_data_t;
// Estrutura de dados dos jogos
typedef struct {
int score;
int high_score;
bool game_active;
uint32_t start_time;
uint32_t game_duration;
} game_data_t;
// Variáveis globais
static system_state_t current_state = STATE_MENU;
static int menu_selection = 0;
static game_data_t games[4] = {0};
static mpu6050_data_t mpu_data = {0};
static spi_device_handle_t spi_sd;
static bool button1_pressed = false;
static bool button2_pressed = false;
static QueueHandle_t button_queue;
// Buffer do display
static uint8_t display_buffer[SSD1306_WIDTH * SSD1306_PAGES];
// Nomes dos jogos
static const char* game_names[] = {
"Tilt Control",
"Shake Master",
"Balance Pro",
"Quick React"
};
// ==================== FUNÇÕES DE HARDWARE ====================
// Inicialização I2C para MPU6050
static esp_err_t i2c_mpu_init(void) {
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_MPU_MASTER_SDA_IO,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_io_num = I2C_MPU_MASTER_SCL_IO,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = I2C_MASTER_FREQ_HZ,
};
esp_err_t err = i2c_param_config(I2C_NUM_0, &conf);
if (err != ESP_OK) return err;
return i2c_driver_install(I2C_NUM_0, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0);
}
// Inicialização I2C para OLED
static esp_err_t i2c_oled_init(void) {
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_OLED_MASTER_SDA_IO,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_io_num = I2C_OLED_MASTER_SCL_IO,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = I2C_MASTER_FREQ_HZ,
};
esp_err_t err = i2c_param_config(I2C_NUM_1, &conf);
if (err != ESP_OK) return err;
return i2c_driver_install(I2C_NUM_1, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0);
}
// Escrever no MPU6050
static esp_err_t mpu6050_write_reg(uint8_t reg, uint8_t data) {
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (MPU6050_ADDR << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, reg, true);
i2c_master_write_byte(cmd, data, true);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(I2C_NUM_0, cmd, I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
return ret;
}
// Ler do MPU6050
static esp_err_t mpu6050_read_reg(uint8_t reg, uint8_t *data, size_t len) {
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (MPU6050_ADDR << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, reg, true);
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (MPU6050_ADDR << 1) | I2C_MASTER_READ, true);
if (len > 1) {
i2c_master_read(cmd, data, len - 1, I2C_MASTER_ACK);
}
i2c_master_read_byte(cmd, data + len - 1, I2C_MASTER_NACK);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(I2C_NUM_0, cmd, I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
return ret;
}
// Inicializar MPU6050
static esp_err_t mpu6050_init(void) {
esp_err_t ret = mpu6050_write_reg(0x6B, 0x00); // Wake up
if (ret != ESP_OK) return ret;
vTaskDelay(100 / portTICK_PERIOD_MS);
ret = mpu6050_write_reg(0x1C, 0x00); // Accel config ±2g
if (ret != ESP_OK) return ret;
ret = mpu6050_write_reg(0x1B, 0x00); // Gyro config ±250°/s
return ret;
}
// Ler dados do MPU6050
static void mpu6050_read_data(mpu6050_data_t *data) {
uint8_t raw_data[14];
if (mpu6050_read_reg(0x3B, raw_data, 14) == ESP_OK) {
int16_t accel_x = (raw_data[0] << 8) | raw_data[1];
int16_t accel_y = (raw_data[2] << 8) | raw_data[3];
int16_t accel_z = (raw_data[4] << 8) | raw_data[5];
int16_t gyro_x = (raw_data[8] << 8) | raw_data[9];
int16_t gyro_y = (raw_data[10] << 8) | raw_data[11];
int16_t gyro_z = (raw_data[12] << 8) | raw_data[13];
data->accel_x = accel_x / 16384.0;
data->accel_y = accel_y / 16384.0;
data->accel_z = accel_z / 16384.0;
data->gyro_x = gyro_x / 131.0;
data->gyro_y = gyro_y / 131.0;
data->gyro_z = gyro_z / 131.0;
}
}
// Comando para SSD1306
static esp_err_t ssd1306_write_command(uint8_t cmd) {
i2c_cmd_handle_t i2c_cmd = i2c_cmd_link_create();
i2c_master_start(i2c_cmd);
i2c_master_write_byte(i2c_cmd, (SSD1306_ADDR << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(i2c_cmd, 0x00, true); // Command mode
i2c_master_write_byte(i2c_cmd, cmd, true);
i2c_master_stop(i2c_cmd);
esp_err_t ret = i2c_master_cmd_begin(I2C_NUM_1, i2c_cmd, I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS);
i2c_cmd_link_delete(i2c_cmd);
return ret;
}
// Inicializar SSD1306
static esp_err_t ssd1306_init(void) {
esp_err_t ret;
ret = ssd1306_write_command(0xAE); // Display off
ret |= ssd1306_write_command(0xD5); // Set display clock divide ratio
ret |= ssd1306_write_command(0x80);
ret |= ssd1306_write_command(0xA8); // Set multiplex ratio
ret |= ssd1306_write_command(0x3F);
ret |= ssd1306_write_command(0xD3); // Set display offset
ret |= ssd1306_write_command(0x00);
ret |= ssd1306_write_command(0x40); // Set start line
ret |= ssd1306_write_command(0x8D); // Charge pump
ret |= ssd1306_write_command(0x14);
ret |= ssd1306_write_command(0x20); // Memory mode
ret |= ssd1306_write_command(0x00);
ret |= ssd1306_write_command(0xA1); // Set segment re-map
ret |= ssd1306_write_command(0xC8); // Set COM output scan direction
ret |= ssd1306_write_command(0xDA); // Set COM pins
ret |= ssd1306_write_command(0x12);
ret |= ssd1306_write_command(0x81); // Set contrast
ret |= ssd1306_write_command(0xCF);
ret |= ssd1306_write_command(0xD9); // Set pre-charge period
ret |= ssd1306_write_command(0xF1);
ret |= ssd1306_write_command(0xDB); // Set VCOMH
ret |= ssd1306_write_command(0x40);
ret |= ssd1306_write_command(0xA4); // Entire display on
ret |= ssd1306_write_command(0xA6); // Set normal display
ret |= ssd1306_write_command(0xAF); // Display on
return ret;
}
// Limpar display
static void ssd1306_clear(void) {
memset(display_buffer, 0, sizeof(display_buffer));
}
// Atualizar display
static esp_err_t ssd1306_update_display(void) {
esp_err_t ret = ESP_OK;
for (int page = 0; page < SSD1306_PAGES; page++) {
ret |= ssd1306_write_command(0xB0 + page); // Set page
ret |= ssd1306_write_command(0x00); // Set lower column
ret |= ssd1306_write_command(0x10); // Set higher column
i2c_cmd_handle_t i2c_cmd = i2c_cmd_link_create();
i2c_master_start(i2c_cmd);
i2c_master_write_byte(i2c_cmd, (SSD1306_ADDR << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(i2c_cmd, 0x40, true); // Data mode
i2c_master_write(i2c_cmd, &display_buffer[page * SSD1306_WIDTH], SSD1306_WIDTH, true);
i2c_master_stop(i2c_cmd);
ret |= i2c_master_cmd_begin(I2C_NUM_1, i2c_cmd, I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS);
i2c_cmd_link_delete(i2c_cmd);
}
return ret;
}
// Desenhar pixel
static void ssd1306_draw_pixel(int x, int y, bool color) {
if (x >= 0 && x < SSD1306_WIDTH && y >= 0 && y < SSD1306_HEIGHT) {
if (color) {
display_buffer[x + (y / 8) * SSD1306_WIDTH] |= (1 << (y % 8));
} else {
display_buffer[x + (y / 8) * SSD1306_WIDTH] &= ~(1 << (y % 8));
}
}
}
// Desenhar linha
static void ssd1306_draw_line(int x0, int y0, int x1, int y1, bool color) {
int dx = abs(x1 - x0);
int dy = abs(y1 - y0);
int sx = x0 < x1 ? 1 : -1;
int sy = y0 < y1 ? 1 : -1;
int err = dx - dy;
while (true) {
ssd1306_draw_pixel(x0, y0, color);
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
}
}
// Font 5x8 básica (apenas alguns caracteres)
static const uint8_t font5x8[][5] = {
{0x3E, 0x51, 0x49, 0x45, 0x3E}, // 0
{0x00, 0x42, 0x7F, 0x40, 0x00}, // 1
{0x42, 0x61, 0x51, 0x49, 0x46}, // 2
{0x21, 0x41, 0x45, 0x4B, 0x31}, // 3
{0x18, 0x14, 0x12, 0x7F, 0x10}, // 4
{0x27, 0x45, 0x45, 0x45, 0x39}, // 5
{0x3C, 0x4A, 0x49, 0x49, 0x30}, // 6
{0x01, 0x71, 0x09, 0x05, 0x03}, // 7
{0x36, 0x49, 0x49, 0x49, 0x36}, // 8
{0x06, 0x49, 0x49, 0x29, 0x1E}, // 9
{0x7E, 0x11, 0x11, 0x11, 0x7E}, // A
{0x7F, 0x49, 0x49, 0x49, 0x36}, // B
{0x3E, 0x41, 0x41, 0x41, 0x22}, // C
{0x7F, 0x41, 0x41, 0x22, 0x1C}, // D
{0x7F, 0x49, 0x49, 0x49, 0x41}, // E
{0x7F, 0x09, 0x09, 0x09, 0x01}, // F
{0x3E, 0x41, 0x49, 0x49, 0x7A}, // G
{0x7F, 0x08, 0x08, 0x08, 0x7F}, // H
{0x00, 0x41, 0x7F, 0x41, 0x00}, // I
{0x20, 0x40, 0x41, 0x3F, 0x01}, // J
{0x7F, 0x08, 0x14, 0x22, 0x41}, // K
{0x7F, 0x40, 0x40, 0x40, 0x40}, // L
{0x7F, 0x02, 0x0C, 0x02, 0x7F}, // M
{0x7F, 0x04, 0x08, 0x10, 0x7F}, // N
{0x3E, 0x41, 0x41, 0x41, 0x3E}, // O
{0x7F, 0x09, 0x09, 0x09, 0x06}, // P
{0x3E, 0x41, 0x51, 0x21, 0x5E}, // Q
{0x7F, 0x09, 0x19, 0x29, 0x46}, // R
{0x46, 0x49, 0x49, 0x49, 0x31}, // S
{0x01, 0x01, 0x7F, 0x01, 0x01}, // T
{0x3F, 0x40, 0x40, 0x40, 0x3F}, // U
{0x1F, 0x20, 0x40, 0x20, 0x1F}, // V
{0x3F, 0x40, 0x38, 0x40, 0x3F}, // W
{0x63, 0x14, 0x08, 0x14, 0x63}, // X
{0x07, 0x08, 0x70, 0x08, 0x07}, // Y
{0x61, 0x51, 0x49, 0x45, 0x43}, // Z
{0x00, 0x00, 0x00, 0x00, 0x00}, // Space
{0x00, 0x00, 0x5F, 0x00, 0x00}, // !
{0x00, 0x36, 0x36, 0x00, 0x00}, // :
};
// Desenhar caractere
static void ssd1306_draw_char(int x, int y, char c, bool color) {
int index;
if (c >= '0' && c <= '9') {
index = c - '0';
} else if (c >= 'A' && c <= 'Z') {
index = c - 'A' + 10;
} else if (c >= 'a' && c <= 'z') {
index = c - 'a' + 10;
} else if (c == ' ') {
index = 36;
} else if (c == '!') {
index = 37;
} else if (c == ':') {
index = 38;
} else {
return;
}
for (int i = 0; i < 5; i++) {
uint8_t line = font5x8[index][i];
for (int j = 0; j < 8; j++) {
if (line & (1 << j)) {
ssd1306_draw_pixel(x + i, y + j, color);
}
}
}
}
// Desenhar string
static void ssd1306_draw_string(int x, int y, const char* str, bool color) {
while (*str) {
ssd1306_draw_char(x, y, *str, color);
x += 6;
str++;
}
}
// Inicializar buzzer
static void buzzer_init(void) {
ledc_timer_config_t ledc_timer = {
.duty_resolution = LEDC_DUTY_RES,
.freq_hz = LEDC_FREQUENCY,
.speed_mode = LEDC_MODE,
.timer_num = LEDC_TIMER,
.clk_cfg = LEDC_AUTO_CLK,
};
ledc_timer_config(&ledc_timer);
ledc_channel_config_t ledc_channel = {
.channel = LEDC_CHANNEL,
.duty = 0,
.gpio_num = BUZZER_PIN,
.speed_mode = LEDC_MODE,
.timer_sel = LEDC_TIMER
};
ledc_channel_config(&ledc_channel);
}
// Tocar som
static void play_tone(int frequency, int duration_ms) {
ledc_set_freq(LEDC_MODE, LEDC_TIMER, frequency);
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, 4096); // 50% duty cycle
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
vTaskDelay(duration_ms / portTICK_PERIOD_MS);
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, 0);
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
}
// Música de vitória
static void play_victory_sound(void) {
play_tone(523, 200); // C
play_tone(659, 200); // E
play_tone(784, 200); // G
play_tone(1047, 400); // C
}
// Som de erro
static void play_error_sound(void) {
play_tone(200, 500);
}
// Som de menu
static void play_menu_sound(void) {
play_tone(800, 100);
}
// Inicializar botões
static void buttons_init(void) {
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << BUTTON1_PIN) | (1ULL << BUTTON2_PIN),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_NEGEDGE
};
gpio_config(&io_conf);
button_queue = xQueueCreate(10, sizeof(int));
gpio_install_isr_service(0);
}
// ISR dos botões
static void IRAM_ATTR button_isr_handler(void* arg) {
int button_num = (int) arg;
xQueueSendFromISR(button_queue, &button_num, NULL);
}
// Configurar interrupções dos botões
static void setup_button_interrupts(void) {
gpio_isr_handler_add(BUTTON1_PIN, button_isr_handler, (void*) 1);
gpio_isr_handler_add(BUTTON2_PIN, button_isr_handler, (void*) 2);
}
// Inicializar SD Card
static esp_err_t sd_card_init(void) {
esp_err_t ret;
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = true,
.max_files = 5,
.allocation_unit_size = 16 * 1024
};
sdmmc_card_t *card;
const char mount_point[] = "/sdcard";
sdmmc_host_t host = SDSPI_HOST_DEFAULT();
spi_bus_config_t bus_cfg = {
.mosi_io_num = SD_MOSI_PIN,
.miso_io_num = SD_MISO_PIN,
.sclk_io_num = SD_CLK_PIN,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 4000,
};
ret = spi_bus_initialize(host.slot, &bus_cfg, SDSPI_DEFAULT_DMA);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize bus.");
return ret;
}
sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
slot_config.gpio_cs = SD_CS_PIN;
slot_config.host_id = host.slot;
ret = esp_vfs_fat_sdspi_mount(mount_point, &host, &slot_config, &mount_config, &card);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to mount filesystem.");
return ret;
}
ESP_LOGI(TAG, "SD card mounted successfully");
return ESP_OK;
}
// Salvar recorde
static void save_high_score(int game_index, int score) {
char filename[32];
snprintf(filename, sizeof(filename), "/sdcard/game%d.txt", game_index);
FILE *f = fopen(filename, "w");
if (f == NULL) {
ESP_LOGE(TAG, "Failed to open file for writing");
return;
}
fprintf(f, "%d\n", score);
fclose(f);
ESP_LOGI(TAG, "High score %d saved for game %d", score, game_index);
}
// Carregar recorde
static int load_high_score(int game_index) {
char filename[32];
snprintf(filename, sizeof(filename), "/sdcard/game%d.txt", game_index);
FILE *f = fopen(filename, "r");
if (f == NULL) {
ESP_LOGW(TAG, "No high score file found for game %d", game_index);
return 0;
}
int score = 0;
fscanf(f, "%d", &score);
fclose(f);
ESP_LOGI(TAG, "High score %d loaded for game %d", score, game_index);
return score;
}
// ==================== FUNÇÕES DOS JOGOS ====================
// Jogo 1: Tilt Control - Controlar um ponto com inclinação
static void game1_tilt_control(void) {
static int ball_x = 64, ball_y = 32;
static int target_x = 100, target_y = 20;
static bool target_hit = false;
if (!games[0].game_active) {
games[0].game_active = true;
games[0].start_time = xTaskGetTickCount();
games[0].score = 0;
ball_x = 64;
ball_y = 32;
target_x = 20 + (rand() % 80);
target_y = 10 + (rand() % 40);
target_hit = false;
}
// Atualizar posição da bola baseado na inclinação
ball_x += (int)(mpu_data.accel_y * 5);
ball_y += (int)(mpu_data.accel_x * 5);
// Limitar posição da bola
if (ball_x < 2) ball_x = 2;
if (ball_x > 125) ball_x = 125;
if (ball_y < 2) ball_y = 2;
if (ball_y > 61) ball_y = 61;
// Verificar colisão com alvo
if (abs(ball_x - target_x) < 5 && abs(ball_y - target_y) < 5 && !target_hit) {
games[0].score += 10;
target_x = 20 + (rand() % 80);
target_y = 10 + (rand() % 40);
target_hit = true;
play_tone(1000, 100);
vTaskDelay(100 / portTICK_PERIOD_MS);
target_hit = false;
}
// Desenhar jogo
ssd1306_clear();
// Desenhar bola (círculo)
for (int i = -2; i <= 2; i++) {
for (int j = -2; j <= 2; j++) {
if (i*i + j*j <= 4) {
ssd1306_draw_pixel(ball_x + i, ball_y + j, true);
}
}
}
// Desenhar alvo (quadrado)
for (int i = -3; i <= 3; i++) {
ssd1306_draw_pixel(target_x + i, target_y - 3, true);
ssd1306_draw_pixel(target_x + i, target_y + 3, true);
ssd1306_draw_pixel(target_x - 3, target_y + i, true);
ssd1306_draw_pixel(target_x + 3, target_y + i, true);
}
// Mostrar pontuação
char score_str[16];
snprintf(score_str, sizeof(score_str), "SCORE: %d", games[0].score);
ssd1306_draw_string(2, 2, score_str, true);
// Verificar tempo limite (30 segundos)
if ((xTaskGetTickCount() - games[0].start_time) > (30000 / portTICK_PERIOD_MS)) {
games[0].game_active = false;
if (games[0].score > games[0].high_score) {
games[0].high_score = games[0].score;
save_high_score(0, games[0].score);
current_state = STATE_SHOW_RECORD;
} else {
current_state = STATE_GAME_OVER;
}
play_victory_sound();
}
}
// Jogo 2: Shake Master - Contar movimentos de shake
static void game2_shake_master(void) {
static float last_accel_magnitude = 0;
static int shake_count = 0;
static uint32_t last_shake_time = 0;
if (!games[1].game_active) {
games[1].game_active = true;
games[1].start_time = xTaskGetTickCount();
games[1].score = 0;
shake_count = 0;
last_accel_magnitude = 0;
}
// Calcular magnitude da aceleração
float accel_magnitude = sqrt(mpu_data.accel_x * mpu_data.accel_x +
mpu_data.accel_y * mpu_data.accel_y +
mpu_data.accel_z * mpu_data.accel_z);
// Detectar shake (mudança brusca na aceleração)
uint32_t current_time = xTaskGetTickCount();
if (fabs(accel_magnitude - last_accel_magnitude) > 0.5 &&
(current_time - last_shake_time) > (200 / portTICK_PERIOD_MS)) {
shake_count++;
games[1].score = shake_count;
last_shake_time = current_time;
play_tone(500, 50);
}
last_accel_magnitude = accel_magnitude;
// Desenhar jogo
ssd1306_clear();
// Título
ssd1306_draw_string(20, 5, "SHAKE MASTER", true);
// Mostrar contagem de shakes
char shake_str[16];
snprintf(shake_str, sizeof(shake_str), "SHAKES: %d", shake_count);
ssd1306_draw_string(20, 20, shake_str, true);
// Barra de progresso do tempo
int time_elapsed = (xTaskGetTickCount() - games[1].start_time) * portTICK_PERIOD_MS;
int progress = (time_elapsed * 100) / 15000; // 15 segundos
if (progress > 100) progress = 100;
ssd1306_draw_line(10, 40, 118, 40, true);
ssd1306_draw_line(10, 38, 10, 42, true);
ssd1306_draw_line(118, 38, 118, 42, true);
int bar_width = (progress * 108) / 100;
for (int i = 0; i < bar_width; i++) {
ssd1306_draw_line(10 + i, 39, 10 + i, 41, true);
}
// Instrução
ssd1306_draw_string(30, 50, "SHAKE IT!", true);
// Verificar tempo limite (15 segundos)
if (time_elapsed > 15000) {
games[1].game_active = false;
if (games[1].score > games[1].high_score) {
games[1].high_score = games[1].score;
save_high_score(1, games[1].score);
current_state = STATE_SHOW_RECORD;
} else {
current_state = STATE_GAME_OVER;
}
play_victory_sound();
}
}
// Jogo 3: Balance Pro - Manter equilíbrio
static void game3_balance_pro(void) {
static float balance_score = 50.0; // Começa no meio
static int bonus_timer = 0;
if (!games[2].game_active) {
games[2].game_active = true;
games[2].start_time = xTaskGetTickCount();
games[2].score = 0;
balance_score = 50.0;
bonus_timer = 0;
}
// Calcular inclinação total
float tilt = sqrt(mpu_data.accel_x * mpu_data.accel_x + mpu_data.accel_y * mpu_data.accel_y);
// Se estiver bem equilibrado (pouca inclinação)
if (tilt < 0.3) {
bonus_timer++;
if (bonus_timer > 10) { // Meio segundo de equilíbrio
games[2].score += 5;
bonus_timer = 0;
play_tone(800, 30);
}
// Recuperar equilíbrio
if (balance_score < 50) balance_score += 0.5;
if (balance_score > 50) balance_score -= 0.5;
} else {
bonus_timer = 0;
// Perder equilíbrio baseado na inclinação
balance_score += (mpu_data.accel_x * 2);
if (balance_score < 0) balance_score = 0;
if (balance_score > 100) balance_score = 100;
}
// Desenhar jogo
ssd1306_clear();
// Título
ssd1306_draw_string(25, 2, "BALANCE PRO", true);
// Barra de equilíbrio
ssd1306_draw_line(10, 25, 118, 25, true);
ssd1306_draw_line(64, 20, 64, 30, true); // Linha central
// Indicador de equilíbrio
int balance_pos = 10 + (balance_score * 108) / 100;
for (int i = -2; i <= 2; i++) {
ssd1306_draw_line(balance_pos + i, 22, balance_pos + i, 28, true);
}
// Zona de segurança (verde)
ssd1306_draw_line(54, 26, 74, 26, true);
ssd1306_draw_line(54, 24, 74, 24, true);
// Mostrar pontuação
char score_str[16];
snprintf(score_str, sizeof(score_str), "SCORE: %d", games[2].score);
ssd1306_draw_string(2, 35, score_str, true);
// Mostrar inclinação
char tilt_str[16];
snprintf(tilt_str, sizeof(tilt_str), "TILT: %.2f", tilt);
ssd1306_draw_string(2, 45, tilt_str, true);
// Instrução
ssd1306_draw_string(20, 55, "STAY BALANCED!", true);
// Game over se sair muito do equilíbrio
if (balance_score <= 5 || balance_score >= 95) {
games[2].game_active = false;
if (games[2].score > games[2].high_score) {
games[2].high_score = games[2].score;
save_high_score(2, games[2].score);
current_state = STATE_SHOW_RECORD;
} else {
current_state = STATE_GAME_OVER;
}
play_error_sound();
return;
}
// Verificar tempo limite (20 segundos)
int time_elapsed = (xTaskGetTickCount() - games[2].start_time) * portTICK_PERIOD_MS;
if (time_elapsed > 20000) {
games[2].game_active = false;
if (games[2].score > games[2].high_score) {
games[2].high_score = games[2].score;
save_high_score(2, games[2].score);
current_state = STATE_SHOW_RECORD;
} else {
current_state = STATE_GAME_OVER;
}
play_victory_sound();
}
}
// Jogo 4: Quick React - Teste de reflexos com acelerômetro
static void game4_quick_react(void) {
static int reaction_phase = 0; // 0=waiting, 1=go, 2=moved
static uint32_t go_time = 0;
static uint32_t react_time = 0;
static int round_count = 0;
static float initial_accel = 0;
if (!games[3].game_active) {
games[3].game_active = true;
games[3].start_time = xTaskGetTickCount();
games[3].score = 0;
reaction_phase = 0;
round_count = 0;
initial_accel = sqrt(mpu_data.accel_x * mpu_data.accel_x + mpu_data.accel_y * mpu_data.accel_y);
}
uint32_t current_time = xTaskGetTickCount();
float current_accel = sqrt(mpu_data.accel_x * mpu_data.accel_x + mpu_data.accel_y * mpu_data.accel_y);
switch (reaction_phase) {
case 0: // Esperando
if ((current_time - games[3].start_time) > ((2000 + rand() % 3000) / portTICK_PERIOD_MS)) {
reaction_phase = 1;
go_time = current_time;
play_tone(1000, 200);
initial_accel = current_accel;
}
break;
case 1: // GO! - Esperando movimento
if (fabs(current_accel - initial_accel) > 0.4) {
react_time = current_time - go_time;
reaction_phase = 2;
// Calcular pontuação (quanto mais rápido, mais pontos)
int time_ms = react_time * portTICK_PERIOD_MS;
if (time_ms < 300) {
games[3].score += 100;
} else if (time_ms < 500) {
games[3].score += 75;
} else if (time_ms < 800) {
games[3].score += 50;
} else {
games[3].score += 25;
}
play_tone(800, 100);
}
// Timeout (muito lento)
if ((current_time - go_time) > (2000 / portTICK_PERIOD_MS)) {
reaction_phase = 2;
react_time = 2000 / portTICK_PERIOD_MS;
games[3].score += 5;
play_error_sound();
}
break;
case 2: // Mostrar resultado
if ((current_time - go_time) > (2000 / portTICK_PERIOD_MS)) {
round_count++;
if (round_count >= 5) {
games[3].game_active = false;
if (games[3].score > games[3].high_score) {
games[3].high_score = games[3].score;
save_high_score(3, games[3].score);
current_state = STATE_SHOW_RECORD;
} else {
current_state = STATE_GAME_OVER;
}
play_victory_sound();
return;
} else {
reaction_phase = 0;
games[3].start_time = current_time;
}
}
break;
}
// Desenhar jogo
ssd1306_clear();
// Título
ssd1306_draw_string(25, 2, "QUICK REACT", true);
// Round counter
char round_str[16];
snprintf(round_str, sizeof(round_str), "ROUND: %d/5", round_count + 1);
ssd1306_draw_string(30, 12, round_str, true);
switch (reaction_phase) {
case 0:
ssd1306_draw_string(40, 30, "WAIT...", true);
break;
case 1:
ssd1306_draw_string(50, 25, "GO!", true);
ssd1306_draw_string(25, 35, "MOVE NOW!", true);
break;
case 2:
char time_str[16];
int time_ms = react_time * portTICK_PERIOD_MS;
snprintf(time_str, sizeof(time_str), "%dms", time_ms);
ssd1306_draw_string(45, 30, time_str, true);
break;
}
// Mostrar pontuação
char score_str[16];
snprintf(score_str, sizeof(score_str), "SCORE: %d", games[3].score);
ssd1306_draw_string(2, 50, score_str, true);
}
// ==================== INTERFACE E MENU ====================
// Desenhar menu principal
static void draw_menu(void) {
ssd1306_clear();
// Título
ssd1306_draw_string(30, 2, "GAME SYSTEM", true);
// Lista de jogos
for (int i = 0; i < 4; i++) {
int y_pos = 15 + i * 10;
// Destacar seleção atual
if (i == menu_selection) {
// Desenhar seletor
ssd1306_draw_string(2, y_pos, ">", true);
// Inverter cores do texto selecionado
for (int x = 10; x < 120; x++) {
for (int y = y_pos; y < y_pos + 8; y++) {
if (x < 10 + strlen(game_names[i]) * 6) {
ssd1306_draw_pixel(x, y, true);
}
}
}
ssd1306_draw_string(12, y_pos, game_names[i], false);
} else {
ssd1306_draw_string(12, y_pos, game_names[i], true);
}
// Mostrar high score
char hs_str[16];
snprintf(hs_str, sizeof(hs_str), "HI:%d", games[i].high_score);
ssd1306_draw_string(85, y_pos, hs_str, true);
}
// Instruções
ssd1306_draw_string(5, 57, "BTN1:NAV BTN2:SELECT", true);
}
// Desenhar tela de novo recorde
static void draw_new_record(int game_index) {
ssd1306_clear();
// Título
ssd1306_draw_string(25, 10, "NEW RECORD!", true);
// Nome do jogo
ssd1306_draw_string(20, 25, game_names[game_index], true);
// Pontuação
char score_str[32];
snprintf(score_str, sizeof(score_str), "SCORE: %d", games[game_index].score);
ssd1306_draw_string(25, 35, score_str, true);
// Animação de celebração (estrelas piscando)
static int blink_counter = 0;
blink_counter++;
if ((blink_counter / 10) % 2) {
ssd1306_draw_string(10, 2, "*", true);
ssd1306_draw_string(110, 2, "*", true);
ssd1306_draw_string(5, 50, "*", true);
ssd1306_draw_string(115, 50, "*", true);
}
ssd1306_draw_string(20, 55, "PRESS BTN2 TO MENU", true);
}
// Desenhar tela de game over
static void draw_game_over(int game_index) {
ssd1306_clear();
// Título
ssd1306_draw_string(35, 10, "GAME OVER", true);
// Nome do jogo
ssd1306_draw_string(20, 25, game_names[game_index], true);
// Pontuação final
char score_str[32];
snprintf(score_str, sizeof(score_str), "SCORE: %d", games[game_index].score);
ssd1306_draw_string(25, 35, score_str, true);
// High score
char hs_str[32];
snprintf(hs_str, sizeof(hs_str), "BEST: %d", games[game_index].high_score);
ssd1306_draw_string(30, 45, hs_str, true);
ssd1306_draw_string(20, 55, "PRESS BTN2 TO MENU", true);
}
// ==================== TASK PRINCIPAL ====================
// Task para processar botões
static void button_task(void *pvParameters) {
int button_num;
while (1) {
if (xQueueReceive(button_queue, &button_num, portMAX_DELAY)) {
vTaskDelay(50 / portTICK_PERIOD_MS); // Debounce
if (button_num == 1) {
button1_pressed = true;
} else if (button_num == 2) {
button2_pressed = true;
}
}
}
}
// Task principal do jogo
static void game_task(void *pvParameters) {
while (1) {
// Ler dados do MPU6050
mpu6050_read_data(&mpu_data);
// Processar botões
if (button1_pressed) {
button1_pressed = false;
play_menu_sound();
switch (current_state) {
case STATE_MENU:
menu_selection = (menu_selection + 1) % 4;
break;
default:
break;
}
}
if (button2_pressed) {
button2_pressed = false;
play_menu_sound();
switch (current_state) {
case STATE_MENU:
// Iniciar jogo selecionado
switch (menu_selection) {
case 0: current_state = STATE_GAME1_TILT; break;
case 1: current_state = STATE_GAME2_SHAKE; break;
case 2: current_state = STATE_GAME3_BALANCE; break;
case 3: current_state = STATE_GAME4_REACTION; break;
}
games[menu_selection].game_active = false; // Reset para inicializar
break;
case STATE_SHOW_RECORD:
case STATE_GAME_OVER:
current_state = STATE_MENU;
break;
default:
break;
}
}
// Atualizar estado do jogo
switch (current_state) {
case STATE_MENU:
draw_menu();
break;
case STATE_GAME1_TILT:
game1_tilt_control();
break;
case STATE_GAME2_SHAKE:
game2_shake_master();
break;
case STATE_GAME3_BALANCE:
game3_balance_pro();
break;
case STATE_GAME4_REACTION:
game4_quick_react();
break;
case STATE_SHOW_RECORD:
draw_new_record(menu_selection);
break;
case STATE_GAME_OVER:
draw_game_over(menu_selection);
break;
}
// Atualizar display
ssd1306_update_display();
// Delay para controle de frame rate
vTaskDelay(50 / portTICK_PERIOD_MS);
}
}
// ==================== FUNÇÃO PRINCIPAL ====================
void app_main(void) {
ESP_LOGI(TAG, "Iniciando Sistema de Jogos ESP32-S3");
// Inicializar componentes de hardware
ESP_ERROR_CHECK(i2c_mpu_init());
ESP_ERROR_CHECK(i2c_oled_init());
ESP_ERROR_CHECK(mpu6050_init());
ESP_ERROR_CHECK(ssd1306_init());
buzzer_init();
buttons_init();
setup_button_interrupts();
// Inicializar SD Card
if (sd_card_init() == ESP_OK) {
// Carregar high scores
for (int i = 0; i < 4; i++) {
games[i].high_score = load_high_score(i);
}
ESP_LOGI(TAG, "High scores carregados do SD Card");
} else {
ESP_LOGW(TAG, "SD Card não disponível - usando valores padrão");
for (int i = 0; i < 4; i++) {
games[i].high_score = 0;
}
}
// Tela de inicialização
ssd1306_clear();
ssd1306_draw_string(25, 20, "ESP32 GAMES", true);
ssd1306_draw_string(35, 35, "LOADING...", true);
ssd1306_update_display();
// Som de inicialização
play_tone(523, 200);
play_tone(659, 200);
play_tone(784, 200);
vTaskDelay(1000 / portTICK_PERIOD_MS);
// Criar tasks
xTaskCreate(button_task, "button_task", 2048, NULL, 10, NULL);
xTaskCreate(game_task, "game_task", 4096, NULL, 5, NULL);
ESP_LOGI(TAG, "Sistema de jogos iniciado com sucesso!");
// Task principal não precisa fazer mais nada
while (1) {
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}