/*
Tami (MiniGatchi v7)
- Arduino Nano + SSD1306 (128x64 I2C, yellow/blue)
- Buttons: D7 = Left, D8 = Right, D9 = OK (Select)
- Independent timers for hunger, happiness, poop
- Hunger bar: full when pet is full, empties as pet gets hungry
- Pet named "Tami" above its sprite
- Cuter, smaller pet with poop mechanic
- No sound, no splash
Libraries required:
Adafruit SSD1306
Adafruit GFX
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// -------- Display setup --------
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
const uint8_t I2C_ADDR = 0x3C;
// -------- Buttons --------
const uint8_t BTN_LEFT = 2; // cycle left
const uint8_t BTN_RIGHT = 6; // cycle right
const uint8_t BTN_SELECT = 5; // OK
// -------- Game intervals --------
// Times are in milliseconds
const uint32_t HUNGER_INTERVAL = 600000; // 10 minutes
const uint32_t HAPPY_INTERVAL = 900000; // 15 minutes
const uint32_t POOP_INTERVAL = 300000; // 5 minutes
// -------- Pet state --------
int hunger = 0; // 0 = full, 100 = starving
int happiness = 100; // 0 = sad, 100 = very happy
int hygiene = 100;
// Poop blobs
struct Poop { bool active; uint8_t x; uint8_t y; };
const uint8_t MAX_POOPS = 5;
Poop poops[MAX_POOPS];
// -------- Timers --------
uint32_t lastHunger = 0;
uint32_t lastHappy = 0;
uint32_t lastPoop = 0;
// -------- Actions --------
enum Action { ACT_FEED = 0, ACT_PLAY = 1, ACT_CLEAN = 2, ACT_COUNT = 3 };
uint8_t selectedAction = 0;
// -------- Button debouncer --------
struct DebouncedButton {
uint8_t pin;
bool lastStable;
bool lastRead;
uint32_t lastChange;
uint16_t debounceMs;
DebouncedButton(uint8_t p, uint16_t d=30) : pin(p), lastStable(HIGH), lastRead(HIGH), lastChange(0), debounceMs(d) {}
void begin() { pinMode(pin, INPUT_PULLUP); lastStable = lastRead = digitalRead(pin); lastChange = millis(); }
bool fell() {
bool r = digitalRead(pin);
uint32_t now = millis();
if (r != lastRead) { lastRead = r; lastChange = now; }
if ((now - lastChange) > debounceMs && r != lastStable) {
lastStable = r;
if (lastStable == LOW) return true;
}
return false;
}
};
DebouncedButton btnLeft(BTN_LEFT), btnRight(BTN_RIGHT), btnSelect(BTN_SELECT);
// -------- Utility --------
int clampi(int v, int lo, int hi){ if(v<lo) return lo; if(v>hi) return hi; return v; }
uint8_t rand8() { return (uint8_t)((millis() * 1103515245UL + 12345UL) >> 24); }
uint8_t activePoopCount() { uint8_t n=0; for(uint8_t i=0;i<MAX_POOPS;i++) if(poops[i].active) n++; return n; }
void clearAllPoop(){ for(uint8_t i=0;i<MAX_POOPS;i++) poops[i].active=false; }
void addPoop() {
for (uint8_t i=0;i<MAX_POOPS;i++) {
if (!poops[i].active) {
poops[i].active = true;
poops[i].x = 88 + (rand8() % 20);
poops[i].y = 38 + (rand8() % 6);
return;
}
}
}
// -------- Actions --------
void performAction(Action a) {
switch(a) {
case ACT_FEED:
hunger = clampi(hunger - 20, 0, 100); // feeding fills stomach
break;
case ACT_PLAY:
happiness = clampi(happiness + 15, 0, 100);
break;
case ACT_CLEAN:
clearAllPoop();
hygiene = clampi(hygiene + 10, 0, 100);
break;
}
}
// -------- Drawing --------
void drawBar(int x, int y, int w, int h, int value/*0..100*/) {
display.drawRect(x, y, w, h, WHITE);
int fillW = map(value, 0, 100, 0, w-2);
display.fillRect(x+1, y+1, fillW, h-2, WHITE);
}
void drawPoop(uint8_t x, uint8_t y) {
display.fillCircle(x, y, 3, WHITE);
display.fillCircle(x-2, y, 2, WHITE);
}
void drawPet(int x, int y) {
// ---- helper: outlined rectangle (1px stroke) + corner dots ----
auto rectOutline = [&](int tlx, int tly, int w, int h) {
// top & bottom
//display.drawLine(tlx, tly, tlx + w -1, tly, WHITE);
//display.drawLine(tlx, tly + h -1, tlx + w - 1, tly + h - 1, WHITE);
// left & right
//display.drawLine(tlx, tly, tlx, tly + h - 1, WHITE);
//display.drawLine(tlx + w -1, tly, tlx + w - 1, tly + h - 1, WHITE);
// corner dots (extra emphasis like in the reference pic)
//display.drawPixel(tlx, tly, WHITE);
//display.drawPixel(tlx + w - 1, tly, WHITE);
//display.drawPixel(tlx, tly + h - 1, WHITE);
//display.drawPixel(tlx + w - 1, tly + h - 1, WHITE);
display.drawPixel(x-10, y+10, WHITE);
display.drawPixel(x-9, y+10, WHITE);
display.drawPixel(x-8, y+10, WHITE);
display.drawPixel(x-7, y+10, WHITE);
display.drawPixel(x-6, y+10, WHITE);
display.drawPixel(x-5, y+10, WHITE);
display.drawPixel(x-4, y+10, WHITE);
display.drawPixel(x-3, y+10, WHITE);
display.drawPixel(x-2, y+10, WHITE);
display.drawPixel(x-1, y+10, WHITE);
display.drawPixel(x, y+10, WHITE);
display.drawPixel(x+1, y+10, WHITE);
display.drawPixel(x+2, y+10, WHITE);
display.drawPixel(x+3, y+10, WHITE);
display.drawPixel(x+4, y+10, WHITE);
display.drawPixel(x+5, y+10, WHITE);
display.drawPixel(x+6, y+10, WHITE);
display.drawPixel(x+7, y+10, WHITE);
display.drawPixel(x+8, y+10, WHITE);
display.drawPixel(x+9, y+10, WHITE);
display.drawPixel(x+10, y+10, WHITE);
display.drawPixel(x-11, y+10, WHITE);
display.drawPixel(x-12, y+9, WHITE);
display.drawPixel(x-12, y+8, WHITE);
display.drawPixel(x-12, y+7, WHITE);
display.drawPixel(x-12, y+6, WHITE);
display.drawPixel(x+11, y+9, WHITE);
display.drawPixel(x+11, y+8, WHITE);
display.drawPixel(x+11, y+7, WHITE);
display.drawPixel(x+11, y+6, WHITE);
display.drawPixel(x-10, y+5, WHITE);
display.drawPixel(x-9, y+5, WHITE);
display.drawPixel(x-8, y+5, WHITE);
display.drawPixel(x-7, y+5, WHITE);
display.drawPixel(x-6, y+5, WHITE);
display.drawPixel(x-5, y+5, WHITE);
display.drawPixel(x-4, y+5, WHITE);
display.drawPixel(x-3, y+5, WHITE);
display.drawPixel(x-2, y+5, WHITE);
display.drawPixel(x-1, y+5, WHITE);
display.drawPixel(x, y+5, WHITE);
display.drawPixel(x+1, y+5, WHITE);
display.drawPixel(x+2, y+5, WHITE);
display.drawPixel(x+3, y+5, WHITE);
display.drawPixel(x+4, y+5, WHITE);
display.drawPixel(x+5, y+5, WHITE);
display.drawPixel(x+6, y+5, WHITE);
display.drawPixel(x+7, y+5, WHITE);
display.drawPixel(x+8, y+5, WHITE);
display.drawPixel(x+9, y+5, WHITE);
display.drawPixel(x+10, y+5, WHITE);
display.drawPixel(x-11, y+5, WHITE);
//second tier
display.drawPixel(x-9, y+4, WHITE);
display.drawPixel(x-10, y+3, WHITE);
display.drawPixel(x-10, y+2, WHITE);
display.drawPixel(x-10, y+1, WHITE);
display.drawPixel(x-10, y, WHITE);
display.drawPixel(x+8, y+4, WHITE);
display.drawPixel(x+9, y+3, WHITE);
display.drawPixel(x+9, y+2, WHITE);
display.drawPixel(x+9, y+1, WHITE);
display.drawPixel(x+9, y, WHITE);
display.drawPixel(x-9, y-1, WHITE);
display.drawPixel(x-8, y-1, WHITE);
display.drawPixel(x-7, y-1, WHITE);
display.drawPixel(x-6, y-1, WHITE);
display.drawPixel(x-5, y-1, WHITE);
display.drawPixel(x-4, y-1, WHITE);
display.drawPixel(x-3, y-1, WHITE);
display.drawPixel(x-2, y-1, WHITE);
display.drawPixel(x-1, y-1, WHITE);
display.drawPixel(x, y-1, WHITE);
display.drawPixel(x+1, y-1, WHITE);
display.drawPixel(x+2, y-1, WHITE);
display.drawPixel(x+3, y-1, WHITE);
display.drawPixel(x+4, y-1, WHITE);
display.drawPixel(x+5, y-1, WHITE);
display.drawPixel(x+6, y-1, WHITE);
display.drawPixel(x+7, y-1, WHITE);
display.drawPixel(x+8, y-1, WHITE);
//third tier
display.drawPixel(x-7, y-2, WHITE);
display.drawPixel(x-8, y-3, WHITE);
display.drawPixel(x-8, y-4, WHITE);
display.drawPixel(x-8, y-5, WHITE);
display.drawPixel(x+6, y-2, WHITE);
display.drawPixel(x+7, y-3, WHITE);
display.drawPixel(x+7, y-4, WHITE);
display.drawPixel(x+7, y-5, WHITE);
display.drawPixel(x-7, y-6, WHITE);
display.drawPixel(x-6, y-6, WHITE);
display.drawPixel(x-5, y-6, WHITE);
display.drawPixel(x-4, y-6, WHITE);
display.drawPixel(x-3, y-6, WHITE);
display.drawPixel(x-2, y-6, WHITE);
display.drawPixel(x-1, y-6, WHITE);
display.drawPixel(x, y-6, WHITE);
display.drawPixel(x+1, y-6, WHITE);
display.drawPixel(x+2, y-6, WHITE);
display.drawPixel(x+3, y-6, WHITE);
display.drawPixel(x+4, y-6, WHITE);
display.drawPixel(x+5, y-6, WHITE);
display.drawPixel(x+6, y-6, WHITE);
};
// ---- tier sizes (kept to fit 26×20 nicely) ----
// Bottom tier
int bX = x - 11, bY = y + 6, bW = 22, bH = 6;
// Middle tier (eyes live here)
int mX = x - 10, mY = y + 0, mW = 20, mH = 6;
// Top tier
int tX = x - 8, tY = y - 6, tW = 16, tH = 6;
// ---- draw the three tiers with lines + pixels only ----
rectOutline(bX, bY, bW, bH);
rectOutline(mX, mY, mW, mH);
rectOutline(tX, tY, tW, tH);
// ---- triangle topper (left vertical, base, hypotenuse) ----
int baseY = tY ;
display.drawLine(x - 4, baseY, x - 2, baseY - 5, WHITE); // left side
display.drawLine(x - 2, baseY, x + 4, baseY, WHITE); // base
display.drawLine(x - 2, baseY - 6, x + 4, baseY, WHITE); // hypotenuse
// ---- eyes: pixel-outline ovals + black pupils (no circles used) ----
// Left eye outline (5×3-ish)
display.drawPixel(x-5,y+2,WHITE);
display.drawPixel(x-4,y+2,WHITE);
int ey = mY + (mH / 2) - 1;
int lx = x - 4;
//display.drawPixel(lx - 2, ey, WHITE);
//display.drawPixel(lx + 2, ey, WHITE);
//display.drawPixel(lx - 1, ey - 1, WHITE);
//display.drawPixel(lx + 1, ey - 1, WHITE);
//display.drawPixel(lx - 1, ey + 1, WHITE);
//display.drawPixel(lx + 1, ey + 1, WHITE);
//display.drawLine (lx - 2, ey + 0, lx - 2, ey + 0, WHITE); // keep single-line count
//display.drawLine (lx + 2, ey + 0, lx + 2, ey + 0, WHITE);
// Right eye outline
int rx = x + 4;
display.drawPixel(x+3,y+2,WHITE);
display.drawPixel(x+4,y+2,WHITE);
//display.drawPixel(rx - 2, ey, WHITE);
//display.drawPixel(rx + 2, ey, WHITE);
//display.drawPixel(rx - 1, ey - 1, WHITE);
//display.drawPixel(rx + 1, ey - 1, WHITE);
//display.drawPixel(rx - 1, ey + 1, WHITE);
//display.drawPixel(rx + 1, ey + 1, WHITE);
//display.drawLine (rx - 2, ey + 0, rx - 2, ey + 0, WHITE);
//display.drawLine (rx + 2, ey + 0, rx + 2, ey + 0, WHITE);
// Pupils (black pixels)
// display.drawPixel(lx, ey, BLACK);
// display.drawPixel(rx, ey, BLACK);
}
void drawActionRow() {
const int BASE_Y = 55;
display.drawLine(0, 46, 127, 46, WHITE);
// Left/Right arrows
display.setCursor(2, BASE_Y); display.print(F("<"));
display.setCursor(22, BASE_Y); display.print(F(">"));
// Action text
const char* actionTxt =
(selectedAction == ACT_FEED) ? "FEED" :
(selectedAction == ACT_PLAY) ? "PLAY" : "CLEAN";
display.setCursor(34, BASE_Y); display.print(actionTxt);
// Fixed OK on right
display.setCursor(SCREEN_WIDTH - 18, BASE_Y);
display.print(F("OK"));
}
void drawUI() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(WHITE);
// Stats (left column)
display.setCursor(2, 6);
display.print(F("full "));
drawBar(35, 6, 52, 8, 100 - hunger);
display.setCursor(2, 20);
display.print(F("happy "));
drawBar(35, 20, 52, 8, happiness);
// Pet name "Tami" above the pet
const char* petName = "Nugget";
int16_t bx, by; uint16_t bw, bh;
display.getTextBounds(petName, 0, 0, &bx, &by, &bw, &bh);
int petNameX = 78 + (26 - bw) / 2; // center text over pet body (width 26)
display.setCursor(petNameX, 4); // around y=8, just above pet
display.print(petName);
// Pet on right side
drawPet(105, 23);
// Poops
for(uint8_t i=0;i<MAX_POOPS;i++) if(poops[i].active) drawPoop(poops[i].x, poops[i].y);
// Bottom actions
drawActionRow();
display.display();
}
// -------- Setup / Loop --------
void setup() {
if (!display.begin(SSD1306_SWITCHCAPVCC, I2C_ADDR)) { for(;;); }
display.setRotation(2); // <-- rotate everything 180°
display.clearDisplay();
display.display();
btnLeft.begin(); btnRight.begin(); btnSelect.begin();
clearAllPoop();
drawUI();
lastHunger = lastHappy = lastPoop = millis();
}
void loop() {
uint32_t now = millis();
// Timers
if (now - lastHunger >= HUNGER_INTERVAL) {
hunger = clampi(hunger + 1, 0, 100);
lastHunger = now;
drawUI();
}
if (now - lastHappy >= HAPPY_INTERVAL) {
happiness = clampi(happiness - 1, 0, 100);
lastHappy = now;
drawUI();
}
if (now - lastPoop >= POOP_INTERVAL) {
addPoop();
lastPoop = now;
drawUI();
}
// Navigation
if (btnLeft.fell()) { selectedAction = (selectedAction + ACT_COUNT - 1) % ACT_COUNT; drawUI(); }
if (btnRight.fell()) { selectedAction = (selectedAction + 1) % ACT_COUNT; drawUI(); }
if (btnSelect.fell()) { performAction((Action)selectedAction); drawUI(); }
}