/**
* Tamagotchi for ESP32 WROOM
* Hardware:
* - 0.96" OLED (SSD1306, I2C) — SDA: GPIO21, SCL: GPIO22
* - MPU6050 Gyroscope (I2C, same bus)
* - Button LEFT : GPIO12
* - Button SELECT: GPIO27
* - Button RIGHT : GPIO14
*
* Libraries required (install via Arduino Library Manager):
* - Adafruit SSD1306
* - Adafruit GFX
* - Adafruit MPU6050
* - Adafruit Unified Sensor
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
// ─────────────────────────────────────────────
// Hardware Config
// ─────────────────────────────────────────────
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define OLED_ADDR 0x3C
#define PIN_BTN_LEFT 12
#define PIN_BTN_SEL 27
#define PIN_BTN_RIGHT 14
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
Adafruit_MPU6050 mpu;
// ─────────────────────────────────────────────
// Game States
// ─────────────────────────────────────────────
enum GameState {
STATE_MAIN,
STATE_MENU,
STATE_FEED_MENU,
STATE_PLAY_PONG,
STATE_STATS,
STATE_SLEEP,
STATE_PET_HAPPY
};
GameState currentState = STATE_MAIN;
// ─────────────────────────────────────────────
// Pet Stats (0–100)
// ─────────────────────────────────────────────
struct PetStats {
int hunger; // 100 = full, 0 = starving
int happiness; // 100 = ecstatic, 0 = miserable
int energy; // 100 = rested, 0 = exhausted
int age; // in minutes
bool isSleeping;
bool isDead;
};
PetStats pet = { 70, 70, 80, 0, false, false };
// ─────────────────────────────────────────────
// Button Handling
// ─────────────────────────────────────────────
struct Button {
bool pressed;
bool lastState;
unsigned long lastDebounce;
};
Button btnLeft = {false, HIGH, 0};
Button btnSelect = {false, HIGH, 0};
Button btnRight = {false, HIGH, 0};
#define DEBOUNCE_MS 50
void readButtons() {
auto read = [](Button &b, int pin) {
bool raw = digitalRead(pin) == LOW;
if (raw != b.lastState) b.lastDebounce = millis();
if ((millis() - b.lastDebounce) > DEBOUNCE_MS) {
b.pressed = raw && (b.lastState == false || b.lastState != raw);
b.lastState = raw;
} else {
b.pressed = false;
}
};
// Edge detection — fire once per press
static bool lPrev = HIGH, sPrev = HIGH, rPrev = HIGH;
bool lCur = digitalRead(PIN_BTN_LEFT) == LOW;
bool sCur = digitalRead(PIN_BTN_SEL) == LOW;
bool rCur = digitalRead(PIN_BTN_RIGHT) == LOW;
btnLeft.pressed = lCur && !lPrev;
btnSelect.pressed = sCur && !sPrev;
btnRight.pressed = rCur && !rPrev;
lPrev = lCur; sPrev = sCur; rPrev = rCur;
}
// ─────────────────────────────────────────────
// Timers
// ─────────────────────────────────────────────
unsigned long lastStatDecay = 0;
unsigned long lastAgeIncrease = 0;
unsigned long lastAnimFrame = 0;
unsigned long happyAnimStart = 0;
int animFrame = 0;
#define STAT_DECAY_INTERVAL 15000 // decay stats every 15s
#define AGE_INTERVAL 60000 // age every 60s
#define ANIM_INTERVAL 400 // sprite frame every 400ms
// ─────────────────────────────────────────────
// Menu
// ─────────────────────────────────────────────
const char* menuItems[] = { "Feed", "Play", "Stats", "Sleep" };
const int menuItemCount = 4;
int menuIndex = 0;
const char* feedItems[] = { "Apple", "Rice", "Cake", "Candy" };
// hunger restore, happiness bonus
const int feedHunger[] = { 25, 30, 15, 8 };
const int feedHappy[] = { 5, 5, 20, 25 };
int feedIndex = 0;
// ─────────────────────────────────────────────
// Pong Game
// ─────────────────────────────────────────────
struct Pong {
float ballX, ballY;
float ballVX, ballVY;
int paddleY; // player paddle (right side)
int cpuY; // cpu paddle (left side)
int score;
bool active;
};
Pong pong;
#define PADDLE_H 14
#define PADDLE_W 3
#define BALL_SIZE 3
void initPong() {
pong.ballX = SCREEN_WIDTH / 2;
pong.ballY = SCREEN_HEIGHT / 2;
pong.ballVX = -2.0f;
pong.ballVY = 1.5f;
pong.paddleY = SCREEN_HEIGHT / 2 - PADDLE_H / 2;
pong.cpuY = SCREEN_HEIGHT / 2 - PADDLE_H / 2;
pong.score = 0;
pong.active = true;
}
// ─────────────────────────────────────────────
// Pet Sprite Bitmaps (16×16)
// Frame 0: idle/blink closed eyes
// Frame 1: idle/blink open eyes
// Frame 2: happy
// Frame 3: sleeping
// Frame 4: sad/hungry
// ─────────────────────────────────────────────
// ── Frame 0: neutral, eyes open ──
static const uint8_t PROGMEM sprite_neutral[] = {
0b00000000, 0b00000000,
0b00000111, 0b11100000,
0b00011111, 0b11111000,
0b00111111, 0b11111100,
0b01110000, 0b00001110,
0b01101001, 0b10010110,
0b01101001, 0b10010110,
0b01110000, 0b00001110,
0b01111100, 0b00111110,
0b01111111, 0b11111110,
0b00111110, 0b01111100,
0b00111101, 0b10111100,
0b00111111, 0b11111100,
0b00011111, 0b11111000,
0b00000111, 0b11100000,
0b00000000, 0b00000000
};
// ── Frame 1: blink (eyes closed) ──
static const uint8_t PROGMEM sprite_blink[] = {
0b00000000, 0b00000000,
0b00000111, 0b11100000,
0b00011111, 0b11111000,
0b00111111, 0b11111100,
0b01110000, 0b00001110,
0b01111111, 0b11111110,
0b01111111, 0b11111110,
0b01110000, 0b00001110,
0b01111100, 0b00111110,
0b01111111, 0b11111110,
0b00111110, 0b01111100,
0b00111101, 0b10111100,
0b00111111, 0b11111100,
0b00011111, 0b11111000,
0b00000111, 0b11100000,
0b00000000, 0b00000000
};
// ── Frame 2: happy (wide eyes + smile) ──
static const uint8_t PROGMEM sprite_happy[] = {
0b00000000, 0b00000000,
0b00000111, 0b11100000,
0b00011111, 0b11111000,
0b00111111, 0b11111100,
0b01110000, 0b00001110,
0b01101111, 0b11110110,
0b01101111, 0b11110110,
0b01110000, 0b00001110,
0b01110000, 0b00001110,
0b01111000, 0b00011110,
0b00111111, 0b11111100,
0b00011111, 0b11111000,
0b00001111, 0b11110000,
0b00011111, 0b11111000,
0b00000111, 0b11100000,
0b00000000, 0b00000000
};
// ── Frame 3: sleeping (Zzz) ──
static const uint8_t PROGMEM sprite_sleep[] = {
0b00000000, 0b00000000,
0b00000111, 0b11100000,
0b00011111, 0b11111000,
0b00111111, 0b11111100,
0b01110000, 0b00001110,
0b01111100, 0b00111110,
0b01111100, 0b00111110,
0b01110000, 0b00001110,
0b01111111, 0b11111110,
0b01111111, 0b11111110,
0b00111111, 0b11111100,
0b00110000, 0b00001100,
0b00111111, 0b11111100,
0b00011111, 0b11111000,
0b00000111, 0b11100000,
0b00000000, 0b00000000
};
// ── Frame 4: sad ──
static const uint8_t PROGMEM sprite_sad[] = {
0b00000000, 0b00000000,
0b00000111, 0b11100000,
0b00011111, 0b11111000,
0b00111111, 0b11111100,
0b01110000, 0b00001110,
0b01100110, 0b01100110,
0b01100110, 0b01100110,
0b01110000, 0b00001110,
0b01111111, 0b11111110,
0b01111001, 0b10011110,
0b00111110, 0b01111100,
0b00111100, 0b00111100,
0b00111101, 0b10111100,
0b00011111, 0b11111000,
0b00000111, 0b11100000,
0b00000000, 0b00000000
};
// ─────────────────────────────────────────────
// Draw Pet Sprite
// ─────────────────────────────────────────────
void drawPet(int x, int y, int frame) {
const uint8_t* sprite;
switch (frame) {
case 0: sprite = sprite_neutral; break;
case 1: sprite = sprite_blink; break;
case 2: sprite = sprite_happy; break;
case 3: sprite = sprite_sleep; break;
case 4: sprite = sprite_sad; break;
default: sprite = sprite_neutral;
}
display.drawBitmap(x, y, sprite, 16, 16, SSD1306_WHITE);
}
// ─────────────────────────────────────────────
// Draw Stat Bar
// ─────────────────────────────────────────────
void drawBar(int x, int y, int w, int h, int val, int maxVal) {
display.drawRect(x, y, w, h, SSD1306_WHITE);
int fill = map(val, 0, maxVal, 0, w - 2);
if (fill > 0)
display.fillRect(x + 1, y + 1, fill, h - 2, SSD1306_WHITE);
}
// ─────────────────────────────────────────────
// Get current sprite frame
// ─────────────────────────────────────────────
int getPetFrame() {
if (pet.isDead) return 4;
if (pet.isSleeping) return 3;
if (currentState == STATE_PET_HAPPY) return 2;
if (pet.happiness < 20 || pet.hunger < 20) return 4;
// blink occasionally
if (animFrame % 8 == 0) return 1;
return 0;
}
// ─────────────────────────────────────────────
// Main Screen
// ─────────────────────────────────────────────
void drawMainScreen() {
display.clearDisplay();
// Pet sprite centered
int petX = (SCREEN_WIDTH - 16) / 2;
int petY = 16;
drawPet(petX, petY, getPetFrame());
// Floating "Zzz" when sleeping
if (pet.isSleeping) {
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(petX + 18, petY);
display.print("Zzz");
}
// Age indicator
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print("Age:");
display.print(pet.age);
// Bottom mini bars
// Hunger bar
display.setCursor(0, 54);
display.print("H");
drawBar(10, 55, 32, 6, pet.hunger, 100);
// Happy bar
display.setCursor(44, 54);
display.print("J");
drawBar(54, 55, 32, 6, pet.happiness, 100);
// Energy bar
display.setCursor(88, 54);
display.print("E");
drawBar(98, 55, 30, 6, pet.energy, 100);
// Dead check
if (pet.isDead) {
display.fillRect(0, 20, SCREEN_WIDTH, 24, SSD1306_BLACK);
display.setTextSize(1);
display.setCursor(20, 26);
display.print("* GAME OVER *");
display.setCursor(14, 36);
display.print("SELECT: restart");
}
display.display();
}
// ─────────────────────────────────────────────
// Menu Screen
// ─────────────────────────────────────────────
void drawMenu() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(40, 0);
display.print("- MENU -");
for (int i = 0; i < menuItemCount; i++) {
int y = 14 + i * 12;
if (i == menuIndex) {
display.fillRect(0, y - 1, SCREEN_WIDTH, 11, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
} else {
display.setTextColor(SSD1306_WHITE);
}
display.setCursor(10, y);
display.print(menuItems[i]);
}
display.display();
}
// ─────────────────────────────────────────────
// Feed Menu
// ─────────────────────────────────────────────
void drawFeedMenu() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(35, 0);
display.print("- FEED -");
for (int i = 0; i < 4; i++) {
int y = 14 + i * 12;
if (i == feedIndex) {
display.fillRect(0, y - 1, SCREEN_WIDTH, 11, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
} else {
display.setTextColor(SSD1306_WHITE);
}
display.setCursor(10, y);
display.print(feedItems[i]);
display.setCursor(65, y);
display.print("+");
display.print(feedHunger[i]);
display.print("H +");
display.print(feedHappy[i]);
display.print("J");
}
display.display();
}
// ─────────────────────────────────────────────
// Stats Screen
// ─────────────────────────────────────────────
void drawStats() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(33, 0);
display.print("- STATS -");
auto drawStat = [](const char* label, int val, int y) {
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, y);
display.print(label);
drawBar(40, y, 80, 8, val, 100);
display.setCursor(122 - (val < 10 ? 0 : 6), y);
display.print(val);
};
drawStat("Hungry ", pet.hunger, 14);
drawStat("Happy ", pet.happiness, 27);
drawStat("Energy ", pet.energy, 40);
display.setCursor(0, 54);
display.print("Age: ");
display.print(pet.age);
display.print(" min");
display.display();
}
// ─────────────────────────────────────────────
// Pong Update & Draw
// ─────────────────────────────────────────────
void updatePong() {
// Move ball
pong.ballX += pong.ballVX;
pong.ballY += pong.ballVY;
// Top/bottom wall bounce
if (pong.ballY <= 8) { pong.ballY = 8; pong.ballVY = fabs(pong.ballVY); }
if (pong.ballY >= SCREEN_HEIGHT - 8) { pong.ballY = SCREEN_HEIGHT - 8; pong.ballVY = -fabs(pong.ballVY); }
// CPU paddle tracks ball (with slight delay)
int cpuCenter = pong.cpuY + PADDLE_H / 2;
if (cpuCenter < pong.ballY - 1) pong.cpuY += 2;
if (cpuCenter > pong.ballY + 1) pong.cpuY -= 2;
pong.cpuY = constrain(pong.cpuY, 8, SCREEN_HEIGHT - PADDLE_H - 1);
// Gyroscope moves player paddle
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
float tiltY = a.acceleration.y; // tilt forward/back
pong.paddleY -= (int)(tiltY * 1.5f);
pong.paddleY = constrain(pong.paddleY, 8, SCREEN_HEIGHT - PADDLE_H - 1);
// Right paddle (player) collision
int px = SCREEN_WIDTH - PADDLE_W - 2;
if (pong.ballX + BALL_SIZE >= px &&
pong.ballY >= pong.paddleY &&
pong.ballY <= pong.paddleY + PADDLE_H) {
pong.ballVX = -fabs(pong.ballVX);
pong.score++;
// Increase speed slightly
if (pong.ballVX > -4.5f) pong.ballVX -= 0.1f;
if (pong.ballVY > 0) pong.ballVY += 0.05f;
else pong.ballVY -= 0.05f;
}
// Left paddle (CPU) collision
if (pong.ballX <= PADDLE_W + 2 &&
pong.ballY >= pong.cpuY &&
pong.ballY <= pong.cpuY + PADDLE_H) {
pong.ballVX = fabs(pong.ballVX);
}
// Ball out of bounds
if (pong.ballX < 0) {
// CPU missed — player scores but ball resets
pong.score++;
pong.ballX = SCREEN_WIDTH / 2;
pong.ballY = SCREEN_HEIGHT / 2;
pong.ballVX = -2.0f;
pong.ballVY = 1.5f;
}
if (pong.ballX > SCREEN_WIDTH) {
// Player missed — lose 3 points min 0
pong.score = max(0, pong.score - 3);
pong.ballX = SCREEN_WIDTH / 2;
pong.ballY = SCREEN_HEIGHT / 2;
pong.ballVX = -2.0f;
pong.ballVY = 1.5f;
}
// Win condition: 10 rallies
if (pong.score >= 10) {
pet.happiness = min(100, pet.happiness + 30);
pet.energy = max(0, pet.energy - 10);
currentState = STATE_PET_HAPPY;
happyAnimStart = millis();
pong.active = false;
}
}
void drawPong() {
display.clearDisplay();
// Score
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(55, 0);
display.print(pong.score);
display.print("/10");
// Tilt hint
display.setCursor(0, 0);
display.print("Tilt!");
// Left boundary
display.drawFastVLine(0, 8, SCREEN_HEIGHT - 8, SSD1306_WHITE);
// CPU paddle (left)
display.fillRect(1, pong.cpuY, PADDLE_W, PADDLE_H, SSD1306_WHITE);
// Player paddle (right)
int px = SCREEN_WIDTH - PADDLE_W - 2;
display.fillRect(px, pong.paddleY, PADDLE_W, PADDLE_H, SSD1306_WHITE);
// Ball
display.fillRect((int)pong.ballX, (int)pong.ballY, BALL_SIZE, BALL_SIZE, SSD1306_WHITE);
// Dashed center line
for (int y = 8; y < SCREEN_HEIGHT; y += 5)
display.drawPixel(SCREEN_WIDTH / 2, y, SSD1306_WHITE);
display.display();
}
// ─────────────────────────────────────────────
// Happy Animation
// ─────────────────────────────────────────────
void drawHappyAnim() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
int petX = (SCREEN_WIDTH - 16) / 2;
int petY = 20 + (animFrame % 3); // gentle bounce
drawPet(petX, petY, 2);
display.setCursor(30, 4);
display.print("* GREAT! *");
// Stars
int starPhase = animFrame % 4;
if (starPhase == 0) { display.setCursor(10, 20); display.print("*"); }
if (starPhase == 1) { display.setCursor(100, 22); display.print("*"); }
if (starPhase == 2) { display.setCursor(15, 38); display.print("*"); }
if (starPhase == 3) { display.setCursor(97, 40); display.print("*"); }
display.setCursor(22, 52);
display.print("+30 Happiness!");
display.display();
if (millis() - happyAnimStart > 2500) {
currentState = STATE_MAIN;
}
}
// ─────────────────────────────────────────────
// Stat Decay
// ─────────────────────────────────────────────
void updateStats() {
unsigned long now = millis();
if (now - lastStatDecay > STAT_DECAY_INTERVAL) {
lastStatDecay = now;
if (!pet.isSleeping) {
pet.hunger = max(0, pet.hunger - 3);
pet.happiness = max(0, pet.happiness - 2);
pet.energy = max(0, pet.energy - 2);
} else {
// Sleeping restores energy, hunger still drops slowly
pet.energy = min(100, pet.energy + 5);
pet.hunger = max(0, pet.hunger - 1);
// Wake up if energy full
if (pet.energy >= 100) pet.isSleeping = false;
}
// Gyroscope shake = play interaction (fun boost)
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
float shake = fabs(a.acceleration.x) + fabs(a.acceleration.y) + fabs(a.acceleration.z);
if (shake > 18.0f && !pet.isSleeping) {
pet.happiness = min(100, pet.happiness + 5);
}
}
if (now - lastAgeIncrease > AGE_INTERVAL) {
lastAgeIncrease = now;
pet.age++;
}
// Death check
if (pet.hunger == 0 && pet.happiness == 0) {
pet.isDead = true;
}
}
// ─────────────────────────────────────────────
// Setup
// ─────────────────────────────────────────────
void setup() {
Serial.begin(115200);
pinMode(PIN_BTN_LEFT, INPUT_PULLUP);
pinMode(PIN_BTN_SEL, INPUT_PULLUP);
pinMode(PIN_BTN_RIGHT, INPUT_PULLUP);
Wire.begin(21, 22); // SDA=21, SCL=22 for ESP32 WROOM
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
Serial.println("OLED init failed");
for (;;);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(20, 24);
display.print("Tamagotchi WROOM");
display.display();
delay(1500);
if (!mpu.begin()) {
Serial.println("MPU6050 not found — gyro disabled");
// Continue without gyro
} else {
mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
mpu.setGyroRange(MPU6050_RANGE_500_DEG);
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
}
lastStatDecay = millis();
lastAgeIncrease = millis();
}
// ─────────────────────────────────────────────
// Loop
// ─────────────────────────────────────────────
void loop() {
readButtons();
// Animate frame counter
if (millis() - lastAnimFrame > ANIM_INTERVAL) {
lastAnimFrame = millis();
animFrame++;
}
// Update pet stats passively
if (currentState != STATE_PLAY_PONG) {
updateStats();
}
// ── State Machine ──────────────────────────
switch (currentState) {
// ── MAIN SCREEN ──────────────────────────
case STATE_MAIN:
drawMainScreen();
if (pet.isDead) {
if (btnSelect.pressed) {
// Restart
pet = { 70, 70, 80, 0, false, false };
menuIndex = 0;
}
break;
}
if (btnSelect.pressed) {
currentState = STATE_MENU;
menuIndex = 0;
}
break;
// ── MENU ─────────────────────────────────
case STATE_MENU:
drawMenu();
if (btnLeft.pressed) menuIndex = (menuIndex - 1 + menuItemCount) % menuItemCount;
if (btnRight.pressed) menuIndex = (menuIndex + 1) % menuItemCount;
if (btnSelect.pressed) {
switch (menuIndex) {
case 0: currentState = STATE_FEED_MENU; feedIndex = 0; break;
case 1: currentState = STATE_PLAY_PONG; initPong(); break;
case 2: currentState = STATE_STATS; break;
case 3:
pet.isSleeping = !pet.isSleeping;
currentState = STATE_MAIN;
break;
}
}
// Back
if (btnLeft.pressed && menuIndex == 0) currentState = STATE_MAIN;
break;
// ── FEED MENU ─────────────────────────────
case STATE_FEED_MENU:
drawFeedMenu();
if (btnLeft.pressed) feedIndex = (feedIndex - 1 + 4) % 4;
if (btnRight.pressed) feedIndex = (feedIndex + 1) % 4;
if (btnSelect.pressed) {
pet.hunger = min(100, pet.hunger + feedHunger[feedIndex]);
pet.happiness = min(100, pet.happiness + feedHappy[feedIndex]);
happyAnimStart = millis();
currentState = STATE_PET_HAPPY;
}
// Back with long left
static unsigned long leftHeld = 0;
if (digitalRead(PIN_BTN_LEFT) == LOW) {
if (leftHeld == 0) leftHeld = millis();
if (millis() - leftHeld > 800) { currentState = STATE_MENU; leftHeld = 0; }
} else {
leftHeld = 0;
}
break;
// ── PONG ─────────────────────────────────
case STATE_PLAY_PONG:
updatePong();
drawPong();
// Quit pong with SELECT
if (btnSelect.pressed) {
currentState = STATE_MAIN;
}
break;
// ── STATS ─────────────────────────────────
case STATE_STATS:
drawStats();
if (btnSelect.pressed || btnLeft.pressed || btnRight.pressed) {
currentState = STATE_MENU;
}
break;
// ── HAPPY ANIM ────────────────────────────
case STATE_PET_HAPPY:
drawHappyAnim();
break;
}
delay(16); // ~60fps target
}