Pocket pet on an Arduino Uno
A Tamagotchi-style pet on a 128x64 OLED: hatch it, feed it, play two mini-games, clean up, and it remembers you after a reset. The whole build, an Arduino Uno and 5 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Advanced About 45 minutes to build and run. Runs in your browser. Free, and no account needed.
Build it as the Pocket Pet Kit: the parts in numbered bags and a booklet that checks every step. Retails for about $37, free on Mokxi.
What you'll learn
- Saving state to EEPROM with a checksum
- Pin-change interrupts for catching button presses
- Keeping images and text in flash with PROGMEM
- Driving an SSD1306 OLED with Adafruit_GFX
- Playing tunes from a piezo without freezing the animation
- Difficulty: Advanced
- Time: About 45 minutes
- Board: Arduino Uno R3
- Parts: 7
How it works
An Arduino Uno drives a 128x64 OLED over I2C (SDA on A4, SCL on A5) and reads three buttons on pins 2, 3 and 4. Pip starts as an egg, hatches after 12 pet minutes, then grows into a kid and an adult.
Time is counted in pet minutes. One pet minute lasts TICK_MS, which is 250 ms here, so a pet day passes in about six real minutes. Each pet minute, hunger, joy and energy drift down, and Pip may get sick.
SELECT moves along six icons, CONFIRM picks one, and CANCEL backs out. Pip's state is saved to EEPROM, so a reset brings Pip back. Hold CANCEL at power-up for a new egg.
Parts list
7 parts, plus the jumper wires. They retail for about $37; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Full-size breadboard
- 1 × OLED display, 128x64In Fight Stick Kit, Pocket Pet Kit, Slot Machine Kit.
- 3 × PushbuttonIn Starter Kit, Christmas Lights Kit, Logic Lab Kit.
- 1 × Piezo speakerIn Starter Kit, Claw Machine Kit, Vending Machine Kit.
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| Piezo speaker pin 1 | Arduino Uno R3 pin 8 |
| Pushbutton (3) pin 1a | Arduino Uno R3 pin 4 |
| Pushbutton (2) pin 1a | Arduino Uno R3 pin 3 |
| Pushbutton (1) pin 1a | Arduino Uno R3 pin 2 |
| OLED display, 128x64 pin VCC | Arduino Uno R3 pin 5V |
| OLED display, 128x64 pin GND | Arduino Uno R3 pin GND |
| Pushbutton (1) pin 2a | Arduino Uno R3 pin GND |
| Pushbutton (2) pin 2a | Arduino Uno R3 pin GND |
| Pushbutton (3) pin 2a | Arduino Uno R3 pin GND |
| Piezo speaker pin 2 | Arduino Uno R3 pin GND |
| OLED display, 128x64 pin SDA | Arduino Uno R3 pin A4 |
| OLED display, 128x64 pin SCL | Arduino Uno R3 pin A5 |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Pocket pet: a virtual pet in the spirit of the 1996 keychain toys, on a
// 128x64 OLED with three buttons and a piezo.
//
// SSD1306 SCL -> A5 SDA -> A4 VCC -> 5 V GND -> GND
// SELECT pin 2 to GND CONFIRM pin 3 to GND CANCEL pin 4 to GND
// Piezo pin 8 to GND
//
// Meet Pip. Pip starts as an egg, hatches into a baby, grows into a kid and
// then an adult. Pip gets hungry, bored, tired and sometimes sick, and leaves
// the odd poop for you to clean up. Look after Pip and Pip stays happy.
//
// # The buttons
//
// SELECT moves along the six icons at the bottom of the screen, CONFIRM picks
// the one that is lit and CANCEL backs out. The icons are Feed, Play, Clean,
// Light, Medicine and Stats.
//
// # Time
//
// Everything Pip does is counted in pet minutes, and TICK_MS says how long a
// pet minute lasts. The demo runs one every quarter of a second, so a pet day
// goes by in six real minutes and you can watch Pip grow up over a coffee.
// Set TICK_MS to 60000 and Pip lives in real time: hungry every few hours,
// asleep for the night, an adult after a day. Nothing else needs to change,
// because every rate below is written in pet minutes.
//
// # Remembering you
//
// Pip's whole state is one small struct saved to the Uno's EEPROM every
// fifteen pet minutes and after anything important. EEPROM.put() only writes
// the bytes that changed, which matters: each EEPROM cell is good for about
// 100,000 writes. Press the Uno's reset button and Pip is still there.
// Hold CANCEL while the board starts to begin again with a new egg.
//
// # Memory
//
// The display's frame is 1 KB of the Uno's 2 KB, so every picture and every
// word lives in flash (PROGMEM) and is read out a byte at a time.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <EEPROM.h>
#include <avr/interrupt.h>
const uint8_t BTN_SELECT = 2;
const uint8_t BTN_CONFIRM = 3;
const uint8_t BTN_CANCEL = 4;
const uint8_t BUZZER = 8;
Adafruit_SSD1306 display(128, 64, &Wire, -1);
// ------------------------------------------------------------------ time
const unsigned long TICK_MS = 250; // one pet minute; 60000 is real time
const uint16_t DAY = 1440; // pet minutes in a pet day
const uint16_t HATCH_AT = 12; // the egg hatches after 12 pet minutes
const uint16_t KID_AT = 360; // a kid at six pet hours
const uint16_t ADULT_AT = 1440; // an adult at one pet day
const uint8_t SAVE_EVERY = 15; // pet minutes between saves
// ------------------------------------------------------------------ strings
//
// A string literal on an AVR is copied into RAM at start-up. FS() keeps it in
// flash instead, the way avr-libc's PSTR does.
#define FS(s) (__extension__({ static const char text_[] PROGMEM = (s); &text_[0]; }))
// ------------------------------------------------------------------ sprites
// ---- sprites (generated) ----
// 16x18
const uint8_t EGG[] PROGMEM = {0x03, 0xc0, 0x0c, 0x30, 0x10, 0x08, 0x26, 0x04, 0x2f, 0x04, 0x46, 0x02, 0x40, 0x02, 0x40, 0xc2, 0x81, 0xe1, 0x80, 0xc1, 0x80, 0x01, 0x8c, 0x01, 0x9e, 0x01, 0x4c, 0x02, 0x40, 0x02, 0x20, 0x04, 0x18, 0x18, 0x07, 0xe0};
// 16x16
const uint8_t BABY_A[] PROGMEM = {0x01, 0x80, 0x02, 0x40, 0x01, 0x80, 0x07, 0xe0, 0x1f, 0xf8, 0x3f, 0xfc, 0x7f, 0xfe, 0x7f, 0xfe, 0x7f, 0xfe, 0x7f, 0xfe, 0x7f, 0xfe, 0x7f, 0xfe, 0x3f, 0xfc, 0x1f, 0xf8, 0x0c, 0x30, 0x00, 0x00};
// 16x16
const uint8_t BABY_B[] PROGMEM = {0x00, 0x00, 0x01, 0x80, 0x02, 0x40, 0x01, 0x80, 0x0f, 0xf0, 0x3f, 0xfc, 0x7f, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xfe, 0x3f, 0xfc, 0x1c, 0x38, 0x00, 0x00, 0x00, 0x00};
// 24x23
const uint8_t KID_A[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x18, 0x3c, 0x00, 0x3c, 0x3c, 0xff, 0x3c, 0x3f, 0xff, 0xfc, 0x3f, 0xff, 0xfc, 0x3f, 0xff, 0xfc, 0x7f, 0xff, 0xfe, 0x7f, 0xff, 0xfe, 0x7f, 0xff, 0xfe, 0x7f, 0xff, 0xfe, 0x7f, 0xff, 0xfe, 0x7f, 0xff, 0xfe, 0x7f, 0xff, 0xfe, 0x7f, 0xff, 0xfe, 0x3f, 0xff, 0xfc, 0x3f, 0xff, 0xfc, 0x1f, 0xff, 0xf8, 0x07, 0xff, 0xe0, 0x07, 0x00, 0xe0, 0x0f, 0x00, 0xf0, 0x00, 0x00, 0x00};
// 24x23
const uint8_t KID_B[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x18, 0x3c, 0x00, 0x3c, 0x3c, 0xff, 0x3c, 0x3f, 0xff, 0xfc, 0x7f, 0xff, 0xfe, 0x7f, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xff, 0xfe, 0x7f, 0xff, 0xfe, 0x3f, 0xff, 0xfc, 0x1f, 0xff, 0xf8, 0x0f, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
// 32x28
const uint8_t ADULT_A[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x0c, 0x38, 0x00, 0x00, 0x1c, 0x3c, 0x00, 0x00, 0x3c, 0x3e, 0xff, 0xff, 0x7c, 0x3f, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xfc, 0x7f, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xfc, 0x1f, 0xff, 0xff, 0xf8, 0x0f, 0xff, 0xff, 0xf0, 0x07, 0xff, 0xff, 0xe0, 0x03, 0xff, 0xff, 0xc0, 0x03, 0xc0, 0x03, 0xc0, 0x07, 0xc0, 0x03, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
// 32x28
const uint8_t ADULT_B[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x0c, 0x38, 0x00, 0x00, 0x1c, 0x3c, 0x00, 0x00, 0x3c, 0x3e, 0xff, 0xff, 0x7c, 0x3f, 0xff, 0xff, 0xfc, 0x7f, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0xff, 0xfc, 0x1f, 0xff, 0xff, 0xf8, 0x0f, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xc0, 0x07, 0xc0, 0x03, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
// 8x8
const uint8_t ICON_FEED[] PROGMEM = {0x18, 0x24, 0x42, 0x81, 0xbd, 0xff, 0x7e, 0x3c};
// 8x8
const uint8_t ICON_PLAY[] PROGMEM = {0x3c, 0x5a, 0x99, 0xff, 0x99, 0x99, 0x5a, 0x3c};
// 8x8
const uint8_t ICON_CLEAN[] PROGMEM = {0x03, 0x06, 0x0c, 0x18, 0x78, 0xfc, 0xfe, 0x54};
// 8x8
const uint8_t ICON_LIGHT[] PROGMEM = {0x3c, 0x42, 0x81, 0x81, 0x42, 0x3c, 0x3c, 0x18};
// 8x8
const uint8_t ICON_MEDS[] PROGMEM = {0x18, 0x18, 0x7e, 0x99, 0xbd, 0x99, 0x81, 0x7e};
// 8x8
const uint8_t ICON_STATS[] PROGMEM = {0x03, 0x03, 0x1b, 0x1b, 0xdb, 0xdb, 0xdb, 0xff};
// 12x12
const uint8_t MEAL[] PROGMEM = {0x06, 0x00, 0x0f, 0x00, 0x1f, 0x80, 0x3f, 0xc0, 0x3f, 0xc0, 0x7f, 0xe0, 0x7f, 0xe0, 0xc0, 0x30, 0xc0, 0x30, 0xc0, 0x30, 0xe0, 0x70, 0x7f, 0xe0};
// 12x12
const uint8_t SNACK[] PROGMEM = {0x09, 0x00, 0x09, 0x00, 0x1f, 0x80, 0x3f, 0xc0, 0x2a, 0xc0, 0x7f, 0xe0, 0x49, 0x20, 0x7f, 0xe0, 0xff, 0xf0, 0xaa, 0xb0, 0xff, 0xf0, 0x7f, 0xe0};
// 10x10
const uint8_t POOP[] PROGMEM = {0x08, 0x00, 0x18, 0x00, 0x1c, 0x00, 0x3e, 0x00, 0x3f, 0x00, 0x7f, 0x80, 0x6b, 0x80, 0xff, 0xc0, 0xff, 0xc0, 0x7f, 0x80};
// 7x7
const uint8_t HEART[] PROGMEM = {0x6c, 0xfe, 0xfe, 0xfe, 0x7c, 0x38, 0x10};
// 7x7
const uint8_t SKULL[] PROGMEM = {0x7c, 0xfe, 0x92, 0x92, 0xee, 0x7c, 0x54};
// 8x8
const uint8_t MOON[] PROGMEM = {0x1e, 0x70, 0x60, 0xc0, 0xc0, 0x60, 0x70, 0x1e};
// 7x7
const uint8_t ROCK[] PROGMEM = {0x38, 0x7c, 0xfe, 0xde, 0xfe, 0x7c, 0x38};
// 12x12
const uint8_t SYRINGE[] PROGMEM = {0x00, 0x40, 0x00, 0xa0, 0x01, 0x10, 0x03, 0xc0, 0x07, 0xc0, 0x0f, 0x80, 0x1f, 0x00, 0x3e, 0x00, 0x7c, 0x00, 0xb8, 0x00, 0x40, 0x00, 0x80, 0x00};
// 5x9
const uint8_t ARROW_L[] PROGMEM = {0x08, 0x18, 0x38, 0x78, 0xf8, 0x78, 0x38, 0x18, 0x08};
// 5x9
const uint8_t ARROW_R[] PROGMEM = {0x80, 0xc0, 0xe0, 0xf0, 0xf8, 0xf0, 0xe0, 0xc0, 0x80};
// 9x9
const uint8_t WING_L[] PROGMEM = {0x80, 0x00, 0xc0, 0x00, 0xe1, 0x80, 0x73, 0x80, 0x7f, 0x80, 0x3f, 0x80, 0x3f, 0x00, 0x1e, 0x00, 0x0c, 0x00};
// 9x9
const uint8_t WING_R[] PROGMEM = {0x00, 0x80, 0x01, 0x80, 0xc3, 0x80, 0xe7, 0x00, 0xff, 0x00, 0xfe, 0x00, 0x7e, 0x00, 0x3c, 0x00, 0x18, 0x00};
// ---- end sprites ----
// ------------------------------------------------------------------ the pet
enum Stage : uint8_t { EGG_STAGE, BABY, KID, ADULT, GONE };
struct Pet {
uint16_t magic;
uint8_t stage;
uint8_t hunger; // 100 is full
uint8_t joy; // 100 is delighted
uint8_t energy; // 100 is wide awake
uint8_t health; // 100 is perfectly well
uint8_t poop; // piles on the floor, up to 3
uint8_t sugar; // snacks lately: too many and Pip gets a tummy ache
uint8_t doses; // medicine taken while sick
bool asleep;
bool sick;
bool lightsOn;
uint32_t age; // pet minutes since the egg was laid
uint16_t poopIn; // pet minutes until the next poop, 0 for none due
uint16_t dirty; // pet minutes with poop on the floor
uint16_t starving; // pet minutes with an empty tummy
uint8_t check;
};
const uint16_t MAGIC = 0x5049; // "PI"
Pet pet;
uint8_t checksum() {
const uint8_t *b = (const uint8_t *)&pet;
uint8_t sum = 0x5a;
for (uint8_t i = 0; i < sizeof(Pet) - 1; i++) sum = (uint8_t)((sum << 1 | sum >> 7) ^ b[i]);
return sum;
}
void save() {
pet.check = checksum();
EEPROM.put(0, pet);
}
bool load() {
EEPROM.get(0, pet);
return pet.magic == MAGIC && pet.check == checksum() && pet.stage <= GONE;
}
void newEgg() {
memset(&pet, 0, sizeof pet);
pet.magic = MAGIC;
pet.stage = EGG_STAGE;
pet.hunger = 80;
pet.joy = 80;
pet.energy = 90;
pet.health = 100;
pet.lightsOn = true;
save();
}
uint8_t up(uint8_t v, uint8_t by) { return v + by > 100 ? 100 : v + by; }
uint8_t down(uint8_t v, uint8_t by) { return v < by ? 0 : v - by; }
// ------------------------------------------------------------------ drawing
//
// One small function per drawing call, so each picture below costs a call
// rather than a fresh copy of the library's code: on an Uno, flash is the
// budget that runs out first.
const uint8_t ON = SSD1306_WHITE;
const uint8_t OFF = SSD1306_BLACK;
__attribute__((noinline)) void px(int16_t x, int16_t y, uint8_t c) { display.drawPixel(x, y, c); }
__attribute__((noinline)) void hline(int16_t x, int16_t y, int16_t w, uint8_t c) { display.drawFastHLine(x, y, w, c); }
__attribute__((noinline)) void box(int16_t x, int16_t y, int16_t w, int16_t h, uint8_t c) {
display.fillRect(x, y, w, h, c);
}
// A picture from flash, one row of bytes after another, the leftmost pixel in
// the top bit: the layout drawBitmap() takes, and every pixel art tool exports.
__attribute__((noinline)) void sprite(int16_t x, int16_t y, const uint8_t *b, uint8_t w, uint8_t h, uint8_t c) {
uint8_t stride = (w + 7) / 8;
for (uint8_t r = 0; r < h; r++) {
for (uint8_t col = 0; col < w; col++) {
uint8_t bits = pgm_read_byte(b + r * stride + (col >> 3));
if (bits & (0x80 >> (col & 7))) display.drawPixel(x + col, y + r, c);
}
}
}
__attribute__((noinline)) void vline(int16_t x, int16_t y, int16_t h, uint8_t c) { display.drawFastVLine(x, y, h, c); }
// A box with its corners clipped off, filled or as an outline: the rounded
// rectangle of a screen this size, at a fraction of the library's cost.
__attribute__((noinline)) void rbox(int16_t x, int16_t y, int16_t w, int16_t h, uint8_t c, bool fill) {
if (fill) {
box(x, y + 1, w, h - 2, c);
} else {
vline(x, y + 1, h - 2, c);
vline(x + w - 1, y + 1, h - 2, c);
}
hline(x + 1, y, w - 2, c);
hline(x + 1, y + h - 1, w - 2, c);
}
__attribute__((noinline)) void at(int16_t x, int16_t y) { display.setCursor(x, y); }
__attribute__((noinline)) void put(char c) { display.write(c); }
__attribute__((noinline)) void num(unsigned int v) { display.print(v); }
// ------------------------------------------------------------------ helpers
void say(const char *p) { // a flash string to the serial monitor
for (char c; (c = (char)pgm_read_byte(p)) != 0; p++) Serial.write(c);
}
void words(const char *p) { // a flash string to the screen, at the cursor
for (char c; (c = (char)pgm_read_byte(p)) != 0; p++) put(c);
}
void text(int16_t x, int16_t y, const char *p) {
at(x, y);
words(p);
}
uint8_t textWidth(const char *p) {
uint8_t n = 0;
while (pgm_read_byte(p + n)) n++;
return n * 6;
}
void centered(int16_t y, const char *p) { text((128 - textWidth(p)) / 2, y, p); }
void two(uint8_t v) { // two digits, for the clock
put('0' + v / 10);
put('0' + v % 10);
}
// ------------------------------------------------------------------ sound
//
// Tunes are lists of (frequency, milliseconds) in flash, played a note at a
// time from loop() so the animation never stops for the music.
const uint16_t TUNE_CHIRP[] PROGMEM = {1568, 60, 2093, 80, 0, 0};
const uint16_t TUNE_HAPPY[] PROGMEM = {1047, 90, 1319, 90, 1568, 90, 2093, 180, 0, 0};
const uint16_t TUNE_EAT[] PROGMEM = {784, 70, 0, 60, 784, 70, 0, 60, 988, 120, 0, 0};
const uint16_t TUNE_SAD[] PROGMEM = {659, 160, 587, 160, 523, 300, 0, 0};
const uint16_t TUNE_CALL[] PROGMEM = {2349, 70, 0, 70, 2349, 70, 0, 70, 2349, 70, 0, 0};
const uint16_t TUNE_HATCH[] PROGMEM = {523, 100, 659, 100, 784, 100, 1047, 100, 784, 100, 1047, 300, 0, 0};
const uint16_t TUNE_NIGHT[] PROGMEM = {784, 200, 659, 200, 523, 400, 0, 0};
const uint16_t TUNE_SWEEP[] PROGMEM = {1200, 40, 1400, 40, 1600, 40, 1800, 40, 2000, 60, 0, 0};
const uint16_t TUNE_PLOP[] PROGMEM = {300, 80, 200, 120, 0, 0};
const uint16_t *tune = nullptr;
unsigned long noteEnds = 0;
void play(const uint16_t *t) {
tune = t;
noteEnds = 0;
}
void click() {
if (!tune) tone(BUZZER, 2400, 6);
}
void music(unsigned long now) {
if (!tune || now < noteEnds) return;
uint16_t f = pgm_read_word(tune);
uint16_t ms = pgm_read_word(tune + 1);
if (ms == 0) {
tune = nullptr;
noTone(BUZZER);
return;
}
if (f) tone(BUZZER, f, ms);
else noTone(BUZZER);
noteEnds = now + ms;
tune += 2;
}
// ------------------------------------------------------------------ buttons
//
// Sending a busy frame down two bit-banged wires can take a few hundred
// milliseconds, longer than a quick tap. So the presses are caught by the
// pin-change interrupt for port D, which pins 2, 3 and 4 all belong to, and
// loop() picks them up when it gets there.
//
// The handler also does the debouncing. A press counts when a pin goes LOW
// after it has been HIGH for 15 ms; the flicker of a contact settling comes
// faster than that, so it never counts twice.
struct Button {
uint8_t mask; // the button's bit in PIND
bool down; // held, as loop() last saw it
bool pressed; // went down since the last loop
};
Button buttons[3] = {{1 << BTN_SELECT, false, false},
{1 << BTN_CONFIRM, false, false},
{1 << BTN_CANCEL, false, false}};
Button &bSelect = buttons[0];
Button &bConfirm = buttons[1];
Button &bCancel = buttons[2];
unsigned long lastPress = 0;
bool seeded = false;
volatile uint8_t latched = 0; // buttons that went down since loop() looked
volatile uint8_t wasLow = 0; // the buttons' levels at the last interrupt
volatile unsigned long upAt[3]; // when each one last went HIGH
ISR(PCINT2_vect) {
uint8_t low = ~PIND;
unsigned long now = millis();
for (uint8_t i = 0; i < 3; i++) {
uint8_t m = buttons[i].mask;
if ((low & m) && !(wasLow & m) && now - upAt[i] >= 15) latched |= m;
if (!(low & m) && (wasLow & m)) upAt[i] = now;
}
wasLow = low;
}
void readButtons(unsigned long now) {
noInterrupts();
uint8_t hit = latched;
latched = 0;
interrupts();
uint8_t pins = PIND;
for (Button &b : buttons) {
b.pressed = (hit & b.mask) != 0;
b.down = b.pressed || !(pins & b.mask);
if (b.pressed) {
lastPress = now;
if (!seeded) {
randomSeed(micros());
seeded = true;
}
}
}
}
// ------------------------------------------------------------------ screens
enum Mode : uint8_t { MAIN, FEED, EAT, PLAYMENU, GUESS, CATCH, CLEAN, MEDS, STATS, HATCH, EVOLVE, MESSAGE, GONE_MODE };
Mode mode = MAIN;
unsigned long modeAt = 0; // when the mode began
int8_t menu = -1; // the lit icon, -1 for none
uint8_t choice = 0; // the item in a sub-menu
const char *message = nullptr;
uint16_t messageMs = 0;
uint8_t frame = 0; // counts every drawn frame
void go(Mode m) {
mode = m;
modeAt = millis();
}
void tell(const char *p, uint16_t ms) {
message = p;
messageMs = ms;
go(MESSAGE);
}
// The pet walks about the room.
int16_t petX = 64;
int16_t targetX = 64;
int8_t look = 0;
uint8_t loved = 0; // frames of a big smile after a treat or a win
// ------------------------------------------------------------------ drawing Pip
struct Look {
const uint8_t *a;
const uint8_t *b;
uint8_t w, h;
uint8_t eyeL, eyeR, eyeY, eyeH;
uint8_t mouthX, mouthY;
uint8_t dropB; // how far frame B's face sits below frame A's
};
const Look LOOKS[3] PROGMEM = {
{BABY_A, BABY_B, 16, 16, 5, 9, 8, 2, 8, 11, 1},
{KID_A, KID_B, 24, 23, 7, 15, 10, 2, 12, 14, 2},
{ADULT_A, ADULT_B, 32, 28, 9, 21, 12, 3, 16, 17, 2},
};
enum Face : uint8_t { F_NORMAL, F_BLINK, F_HAPPY, F_SAD, F_SLEEPY, F_SLEEP, F_SICK, F_EAT, F_OPEN };
Look lookOf(uint8_t stage) {
Look l;
memcpy_P(&l, &LOOKS[stage - BABY], sizeof l);
return l;
}
void eyes(int16_t x, int16_t y, uint8_t h, Face f, uint8_t c) {
switch (f) {
case F_BLINK:
case F_SLEEP:
hline(x - 1, y + h - 1, 4, c);
break;
case F_SLEEPY:
hline(x - 1, y + h - 1, 4, c);
hline(x, y + h - 2, 2, c);
break;
case F_HAPPY:
case F_EAT:
px(x - 1, y + 1, c);
hline(x, y, 2, c);
px(x + 2, y + 1, c);
break;
case F_SICK:
for (int8_t i = 0; i < 4; i++) {
px(x - 1 + i, y - 1 + i, c);
px(x - 1 + i, y + 2 - i, c);
}
break;
case F_SAD:
box(x, y + 1, 2, h, c);
break;
default:
box(x, y, 2, h, c);
break;
}
}
void mouth(int16_t x, int16_t y, Face f, uint8_t c) {
switch (f) {
case F_HAPPY:
hline(x - 2, y, 4, c);
hline(x - 1, y + 1, 2, c);
break;
case F_SAD:
hline(x - 1, y, 2, c);
px(x - 2, y + 1, c);
px(x + 1, y + 1, c);
break;
case F_SICK:
px(x - 2, y + 1, c);
px(x - 1, y, c);
px(x, y + 1, c);
px(x + 1, y, c);
break;
case F_EAT:
case F_OPEN:
box(x - 1, y, 3, 2, c);
break;
case F_SLEEP:
case F_SLEEPY:
hline(x - 1, y, 2, c);
break;
default:
px(x - 2, y, c);
hline(x - 1, y + 1, 2, c);
px(x + 1, y, c);
break;
}
}
// Pip, standing on `base` with the middle of its body at `cx`.
void drawPip(int16_t cx, int16_t base, Face f, int8_t lookX, bool bob) {
if (pet.stage == EGG_STAGE || pet.stage == GONE) return;
Look l = lookOf(pet.stage);
int16_t x = cx - l.w / 2;
int16_t y = base - l.h;
sprite(x, y, bob ? l.b : l.a, l.w, l.h, ON);
int16_t fy = y + (bob ? l.dropB : 0);
eyes(x + l.eyeL + lookX, fy + l.eyeY, l.eyeH, f, OFF);
eyes(x + l.eyeR + lookX, fy + l.eyeY, l.eyeH, f, OFF);
mouth(x + l.mouthX + lookX, fy + l.mouthY, f, OFF);
if ((f == F_HAPPY || f == F_EAT) && pet.stage != BABY) { // rosy cheeks, two dots each
int16_t cy = fy + l.eyeY + l.eyeH;
hline(x + l.eyeL + lookX - 4, cy, 2, OFF);
hline(x + l.eyeR + lookX + 4, cy, 2, OFF);
}
if (pet.stage == ADULT) { // whiskers
int16_t wy = fy + l.mouthY - 2;
hline(x + 2, wy, 4, OFF);
hline(x + 2, wy + 2, 4, OFF);
hline(x + l.w - 6, wy, 4, OFF);
hline(x + l.w - 6, wy + 2, 4, OFF);
}
}
int16_t pipTop(int16_t base) {
if (pet.stage == EGG_STAGE || pet.stage == GONE) return base - 18;
return base - lookOf(pet.stage).h;
}
void sparkle(int16_t x, int16_t y, uint8_t size) {
hline(x - size, y, size * 2 + 1, ON);
vline(x, y - size, size * 2 + 1, ON);
}
void bubble(int16_t x, int16_t y, const uint8_t *icon, uint8_t w, uint8_t h) {
rbox(x, y, w + 6, h + 6, ON, true);
sprite(x + 3, y + 3, icon, w, h, OFF);
box(x + 1, y + h + 7, 2, 2, ON);
px(x - 1, y + h + 10, ON);
}
// ------------------------------------------------------------------ moods
bool needsYou() {
if (pet.stage == EGG_STAGE || pet.stage == GONE) return false;
if (pet.asleep) return pet.lightsOn;
return pet.hunger < 25 || pet.joy < 25 || pet.sick || pet.poop > 0 || pet.energy < 20 || !pet.lightsOn;
}
Face mood() {
if (pet.asleep) return F_SLEEP;
if (pet.sick) return F_SICK;
if (loved) return F_HAPPY;
if ((frame & 31) == 0 || (frame & 63) == 3) return F_BLINK;
if (pet.energy < 20) return (frame & 127) < 12 ? F_OPEN : F_SLEEPY;
if (pet.hunger < 25 || pet.joy < 30) return F_SAD;
if (pet.joy >= 75) return F_HAPPY;
return F_NORMAL;
}
const char *stageName() {
switch (pet.stage) {
case EGG_STAGE: return FS("Egg");
case BABY: return FS("Baby");
case KID: return FS("Kid");
case ADULT: return FS("Adult");
default: return FS("Star");
}
}
// ------------------------------------------------------------------ the room
const uint8_t *const ICONS[6] PROGMEM = {ICON_FEED, ICON_PLAY, ICON_CLEAN, ICON_LIGHT, ICON_MEDS, ICON_STATS};
void drawMenu() {
for (uint8_t i = 0; i < 6; i++) {
int16_t x = i * 21 + 2;
const uint8_t *icon = (const uint8_t *)pgm_read_ptr(&ICONS[i]);
if (menu == (int8_t)i) {
rbox(x, 55, 16, 9, ON, true);
sprite(x + 4, 56, icon, 8, 8, OFF);
} else {
sprite(x + 4, 56, icon, 8, 8, ON);
}
}
}
void drawTop() {
uint16_t clock = (pet.age + 480) % DAY; // Pip's day starts at 8 in the morning
display.setTextColor(ON);
at(0, 0);
words(FS("Day "));
num(pet.age / DAY);
at(48, 0);
two(clock / 60);
put(':');
two(clock % 60);
if (pet.sick) sprite(108, 0, SKULL, 7, 7, ON);
if (needsYou() && (frame & 4)) {
rbox(119, 0, 9, 9, ON, true);
box(123, 1, 1, 5, OFF);
px(123, 7, OFF);
}
}
// The floor and the menu along the bottom hardly ever change, so they are
// drawn only when they do; everything above them is drawn fresh each frame.
int8_t shownMenu = -2;
bool shownLights = true;
void drawRoom(bool fresh) {
const int16_t floorY = 53;
if (fresh || menu != shownMenu || pet.lightsOn != shownLights) {
box(0, 54, 128, 10, OFF);
if (pet.lightsOn)
for (int16_t x = 0; x < 128; x += 3) px(x, floorY + 1, ON);
drawMenu();
shownMenu = menu;
shownLights = pet.lightsOn;
}
box(0, 0, 128, 54, OFF);
drawTop();
if (!pet.lightsOn) {
sprite(4, 12, MOON, 8, 8, ON);
px(30, 14, ON);
px(100, 18, ON);
px(76, 12, ON);
if (pet.asleep) {
uint8_t t = frame % 24;
at(petX + 8 + t / 4, 30 - t / 2);
put('z');
at(petX + 14, 20 - (t / 6));
put('Z');
} else if (pet.stage != EGG_STAGE && (frame & 31) != 0) { // eyes in the dark
Look l = lookOf(pet.stage);
int16_t x = petX - l.w / 2, y = floorY - l.h + l.eyeY;
box(x + l.eyeL, y, 2, l.eyeH, ON);
box(x + l.eyeR, y, 2, l.eyeH, ON);
}
return;
}
if (pet.stage == EGG_STAGE) {
int8_t wobble = 0;
if (pet.age + 4 >= HATCH_AT) wobble = (frame & 1) ? 2 : -2;
else if ((frame & 15) < 2) wobble = (frame & 1) ? 1 : -1;
sprite(56 + wobble, floorY - 18, EGG, 16, 18, ON);
} else {
bool bob = (frame / 4) & 1;
if (pet.asleep) bob = (frame / 10) & 1;
drawPip(petX, floorY, mood(), look, bob);
int16_t top = pipTop(floorY);
if (pet.asleep) {
uint8_t t = frame % 24;
at(petX + 10 + t / 4, top - 2 - t / 3);
put('z');
} else if ((frame & 63) < 28) {
if (pet.sick) bubble(petX + 10, top - 16, SKULL, 7, 7);
else if (pet.hunger < 25) bubble(petX + 10, top - 18, MEAL, 12, 12);
else if (loved) bubble(petX + 10, top - 16, HEART, 7, 7);
}
}
for (uint8_t i = 0; i < pet.poop; i++) {
int16_t x = 116 - i * 13;
sprite(x, floorY - 10, POOP, 10, 10, ON);
uint8_t w = (frame / 3 + i) & 1; // stink lines
px(x + 2 + w, floorY - 13, ON);
px(x + 3 - w, floorY - 15, ON);
px(x + 7 - w, floorY - 13, ON);
px(x + 6 + w, floorY - 15, ON);
}
}
void walk() {
if (pet.stage == EGG_STAGE || pet.asleep || pet.sick) {
look = 0;
return;
}
if (petX == targetX) {
look = 0;
if (random(40) == 0) {
int16_t right = 92 - pet.poop * 13;
targetX = random(22, right > 30 ? right : 30);
}
return;
}
if (frame & 1) return; // a step every other frame
look = targetX > petX ? 1 : -1;
petX += look;
}
// ------------------------------------------------------------------ the pet's minute
bool called = false;
uint16_t minute; // the pet's age in minutes, modulo 360
__attribute__((noinline)) bool every(uint8_t n) { return minute % n == 0; }
void evolveCheck() {
if ((pet.stage == BABY && pet.age >= KID_AT) || (pet.stage == KID && pet.age >= ADULT_AT)) {
go(EVOLVE);
play(TUNE_HATCH);
}
}
void petMinute() {
if (pet.stage == GONE) return;
pet.age++;
// Every rate below divides 360, so one 32-bit division a minute is enough
// and the rest are on a 16-bit number, which is far cheaper on an AVR.
minute = pet.age % 360;
if (pet.stage == EGG_STAGE) {
if (pet.age >= HATCH_AT && mode != HATCH) {
go(HATCH);
play(TUNE_HATCH);
}
return;
}
if (pet.asleep) {
if (every((pet.lightsOn ? 8 : 4))) pet.energy = up(pet.energy, 1);
if (every(18)) pet.hunger = down(pet.hunger, 1);
if (pet.lightsOn && every(6)) pet.joy = down(pet.joy, 1);
if (pet.energy >= 100) {
pet.asleep = false;
say(FS("Pip woke up\n"));
play(TUNE_CHIRP);
}
} else {
if (every(6)) pet.hunger = down(pet.hunger, 1);
if (every(9)) pet.energy = down(pet.energy, 1);
if (every(8)) pet.joy = down(pet.joy, 1);
if (pet.poop && every(4)) pet.joy = down(pet.joy, 1);
if (pet.hunger < 20 && every(5)) pet.joy = down(pet.joy, 1);
if (!pet.lightsOn && every(5)) pet.joy = down(pet.joy, 1);
if (pet.energy < 12) {
pet.asleep = true;
say(FS("Pip fell asleep\n"));
}
}
// Health, and what makes Pip ill.
if (pet.sick && every(5)) pet.health = down(pet.health, 1);
if (pet.hunger == 0) {
pet.starving++;
if (every(5)) pet.health = down(pet.health, 1);
} else {
pet.starving = 0;
if (!pet.sick && pet.hunger > 40 && every(30)) pet.health = up(pet.health, 1);
}
if (pet.poop) pet.dirty++;
else pet.dirty = 0;
if (every(30) && pet.sugar) pet.sugar--;
if (!pet.sick && (pet.dirty > 90 || pet.starving > 60 || pet.sugar >= 4)) {
pet.sick = true;
pet.doses = 0;
pet.dirty = 0;
say(FS("Pip is sick\n"));
}
// What goes in comes out.
if (pet.poopIn && --pet.poopIn == 0 && pet.poop < 3) {
pet.poop++;
play(TUNE_PLOP);
say(FS("Pip pooped\n"));
}
if (pet.health == 0) {
pet.stage = GONE;
go(GONE_MODE);
play(TUNE_SAD);
say(FS("Pip went home to the stars\n"));
save();
return;
}
if (mode == MAIN) evolveCheck();
bool need = needsYou();
if (need && !called) play(TUNE_CALL);
called = need;
if (every(60)) {
say(FS("Day "));
Serial.print((unsigned int)(pet.age / DAY));
say(FS(" food "));
Serial.print(pet.hunger);
say(FS(" joy "));
Serial.print(pet.joy);
say(FS(" rest "));
Serial.print(pet.energy);
say(FS(" health "));
Serial.println(pet.health);
}
if (every(SAVE_EVERY)) save();
}
// ------------------------------------------------------------------ the menu
void act(uint8_t which) {
bool baby = pet.stage == EGG_STAGE;
switch (which) {
case 0: // feed
if (baby) return tell(FS("Still an egg!"), 1200);
if (pet.asleep) return tell(FS("Shh, asleep"), 1200);
choice = 0;
go(FEED);
break;
case 1: // play
if (baby) return tell(FS("Still an egg!"), 1200);
if (pet.asleep) return tell(FS("Shh, asleep"), 1200);
if (pet.sick) return tell(FS("Too poorly"), 1200);
if (pet.energy < 15) return tell(FS("Too tired..."), 1200);
choice = 0;
go(PLAYMENU);
break;
case 2: // clean
if (!pet.poop) return tell(FS("All clean!"), 1000);
go(CLEAN);
play(TUNE_SWEEP);
break;
case 3: // light
pet.lightsOn = !pet.lightsOn;
if (!pet.lightsOn && pet.asleep) play(TUNE_NIGHT);
say(pet.lightsOn ? FS("Lights on\n") : FS("Lights off\n"));
save();
break;
case 4: // medicine
if (!pet.sick) return tell(FS("I feel fine!"), 1200);
go(MEDS);
break;
case 5: // stats
go(STATS);
break;
}
}
void doMain() {
if (bSelect.pressed) {
menu = (int8_t)((menu + 1) % 6);
click();
}
if (bCancel.pressed) {
menu = -1;
click();
}
if (bConfirm.pressed && menu >= 0) {
click();
act((uint8_t)menu);
} else if (bConfirm.pressed && pet.stage >= BABY && pet.stage <= ADULT && !pet.asleep) {
loved = 20; // a pat on the head
pet.joy = up(pet.joy, 1);
play(TUNE_CHIRP);
}
if (menu >= 0 && millis() - lastPress > 15000) menu = -1;
}
// ------------------------------------------------------------------ feeding
void drawFeed() {
centered(0, FS("What's for dinner?"));
const char *names[2] = {FS("Meal"), FS("Snack")};
for (uint8_t i = 0; i < 2; i++) {
int16_t x = 18 + i * 56;
if (choice == i) rbox(x - 4, 13, 44, 42, ON, false);
sprite(x + 12, 20, i ? SNACK : MEAL, 12, 12, ON);
text(x + 18 - textWidth(names[i]) / 2, 40, names[i]);
}
text(0, 57, FS("SEL next OK eat"));
}
void doFeed() {
if (bSelect.pressed) {
choice ^= 1;
click();
}
if (bCancel.pressed) {
click();
go(MAIN);
}
if (bConfirm.pressed) {
if (choice == 0 && pet.hunger >= 95) {
play(TUNE_SAD);
return tell(FS("I'm full!"), 1200);
}
if (choice == 0) {
pet.hunger = up(pet.hunger, 30);
say(FS("Pip ate a meal\n"));
} else {
pet.hunger = up(pet.hunger, 8);
pet.joy = up(pet.joy, 15);
pet.sugar++;
say(FS("Pip ate a snack\n"));
}
if (!pet.poopIn) pet.poopIn = 40 + random(30);
play(TUNE_EAT);
go(EAT);
}
}
void drawEat(unsigned long t) {
uint8_t step = t / 300;
drawTop();
bool open = step % 2 == 0 && step < 6;
drawPip(52, 53, open ? F_EAT : F_HAPPY, 1, false);
const uint8_t *food = choice ? SNACK : MEAL;
if (step < 7) {
sprite(78, 41, food, 12, 12, ON);
uint8_t bites = step < 6 ? (step + 1) / 2 : 3;
for (uint8_t i = 0; i < bites; i++) display.fillCircle(77, 44 + i * 4, 3 + i, OFF);
if (step >= 6) box(76, 40, 16, 14, OFF);
}
if (step >= 6) {
sprite(48, 18 - (t - 1800) / 60, HEART, 7, 7, ON);
}
}
// ------------------------------------------------------------------ playing
void drawPlayMenu() {
centered(0, FS("Let's play!"));
const char *names[2] = {FS("Left or right?"), FS("Treat catch")};
for (uint8_t i = 0; i < 2; i++) {
int16_t y = 16 + i * 16;
if (choice == i) {
rbox(8, y - 3, 112, 13, ON, true);
display.setTextColor(OFF);
}
centered(y, names[i]);
display.setTextColor(ON);
}
text(0, 57, FS("SEL next OK play"));
}
// Left or right: guess which way Pip will look.
uint8_t round_ = 0, wins = 0;
int8_t guess = 0, answer = 0;
unsigned long revealAt = 0;
// Treat catch: hearts are good, rocks are not.
struct Drop {
int16_t x, y;
uint8_t speed;
bool rock;
bool live;
};
Drop drops[3];
uint8_t caught = 0;
int16_t catchX = 64;
unsigned long nextDrop = 0;
const unsigned long CATCH_MS = 20000;
void doPlayMenu() {
if (bSelect.pressed) {
choice ^= 1;
click();
}
if (bCancel.pressed) {
click();
go(MAIN);
}
if (bConfirm.pressed) {
click();
if (choice == 0) {
round_ = 0;
wins = 0;
guess = 0;
go(GUESS);
} else {
caught = 0;
catchX = 64;
nextDrop = millis() + 600;
for (Drop &d : drops) d.live = false;
go(CATCH);
}
}
}
void finishGame(uint8_t score, uint8_t joy, bool won) {
pet.joy = up(pet.joy, joy);
pet.energy = down(pet.energy, 6);
say(won ? FS("Pip won, score ") : FS("Pip played, score "));
Serial.println(score);
loved = won ? 40 : 0;
play(won ? TUNE_HAPPY : TUNE_SAD);
save();
}
void doGuess(unsigned long now) {
if (bCancel.pressed) {
click();
return go(MAIN);
}
if (guess == 0) {
if (bSelect.pressed || bConfirm.pressed) {
guess = bSelect.pressed ? -1 : 1;
answer = random(2) ? 1 : -1;
revealAt = now;
round_++;
if (guess == answer) {
wins++;
play(TUNE_CHIRP);
} else {
tone(BUZZER, 220, 150);
}
}
} else if (now - revealAt > 1100) {
guess = 0;
if (round_ == 5) {
bool won = wins >= 3;
finishGame(wins, wins * 6, won);
tell(won ? FS("You win!") : FS("Next time!"), 1600);
}
}
}
void drawGuess(unsigned long now) {
at(0, 0);
words(FS("Round "));
num(round_ + (guess ? 0 : 1));
words(FS("/5"));
for (uint8_t i = 0; i < wins; i++) sprite(120 - i * 9, 0, HEART, 7, 7, ON);
if (guess == 0) {
drawPip(64, 50, (frame & 15) == 0 ? F_BLINK : F_NORMAL, 0, (frame / 4) & 1);
centered(12, FS("Which way?"));
text(0, 56, FS("<SEL"));
text(104, 56, FS("OK>"));
} else {
bool right = guess == answer;
drawPip(64, 50, right ? F_HAPPY : F_SAD, answer * 2, false);
int16_t ax = answer < 0 ? 30 : 92;
sprite(ax, 26, answer < 0 ? ARROW_L : ARROW_R, 5, 9, ON);
if (right) sprite(60, 10 - (now - revealAt) / 100, HEART, 7, 7, ON);
else centered(12, FS("Oops!"));
}
}
void doCatch(unsigned long now) {
unsigned long t = now - modeAt;
if (bCancel.pressed) {
click();
return go(MAIN);
}
if (t >= CATCH_MS) {
bool won = caught >= 8;
finishGame(caught, caught > 15 ? 30 : caught * 2, won);
return tell(won ? FS("Yum! Great job") : FS("Good try!"), 1600);
}
}
void stepCatch(unsigned long now) {
unsigned long t = now - modeAt;
if (bSelect.down && catchX > 12) catchX -= 3;
if (bConfirm.down && catchX < 116) catchX += 3;
if (now >= nextDrop) {
for (Drop &d : drops) {
if (d.live) continue;
d.live = true;
d.x = random(4, 118);
d.y = 8;
d.rock = random(5) == 0;
d.speed = 1 + t / 7000 + random(2);
break;
}
nextDrop = now + 700 - t / 50;
}
Look l = lookOf(pet.stage);
int16_t top = 53 - l.h;
for (Drop &d : drops) {
if (!d.live) continue;
d.y += d.speed;
if (d.y + 7 >= top + 2 && d.y < top + 8 && d.x + 6 >= catchX - l.w / 2 && d.x <= catchX + l.w / 2) {
d.live = false;
if (d.rock) {
if (caught) caught--;
tone(BUZZER, 150, 120);
} else {
caught++;
tone(BUZZER, 1760 + caught * 40, 40);
}
} else if (d.y > 54) {
d.live = false;
}
}
}
void drawCatch(unsigned long now) {
unsigned long t = now - modeAt;
sprite(0, 0, HEART, 7, 7, ON);
at(10, 0);
num(caught);
unsigned long left = t >= CATCH_MS ? 0 : (CATCH_MS - t + 999) / 1000;
at(104, 0);
num(left);
put('s');
for (Drop &d : drops) {
if (d.live) sprite(d.x, d.y, d.rock ? ROCK : HEART, 7, 7, ON);
}
int8_t lx = bSelect.down ? -1 : (bConfirm.down ? 1 : 0);
drawPip(catchX, 53, F_HAPPY, lx, (frame / 2) & 1);
}
// ------------------------------------------------------------------ cleaning and medicine
void drawClean(unsigned long t) {
int16_t wave = 128 - t / 8; // the mop sweeps from right to left
drawTop();
drawPip(petX, 53, F_NORMAL, 1, false);
for (uint8_t i = 0; i < pet.poop; i++) {
int16_t x = 116 - i * 13;
if (x < wave) sprite(x, 43, POOP, 10, 10, ON);
}
for (int16_t y = 12; y < 54; y += 2) {
int16_t x = wave + ((y / 2 + t / 40) % 3);
px(x, y, ON);
px(x + 1, y, ON);
}
if (wave < 110) sparkle(wave + 16, 30 + (t / 30) % 8, 2);
if (wave < 60) sparkle(wave + 34, 46 - (t / 40) % 6, 1);
}
void drawMeds(unsigned long t) {
drawTop();
int8_t shiver = t > 700 && t < 1100 ? ((t / 50) & 1) * 2 - 1 : 0;
drawPip(64 + shiver, 53, t > 1100 ? F_HAPPY : F_SICK, 0, false);
if (t < 1100) {
int16_t x = 100 - (t < 700 ? t / 25 : 28);
sprite(x, 20, SYRINGE, 12, 12, ON);
} else {
sparkle(48, 24, 2);
sparkle(82, 20, 3);
sparkle(76, 36, 1);
}
}
// ------------------------------------------------------------------ stats
void bar(int16_t y, const char *label, uint8_t value) {
text(0, y, label);
rbox(46, y, 80, 8, ON, false);
box(48, y + 2, (uint16_t)value * 76 / 100, 4, ON);
}
void drawStats() {
bar(0, FS("Food"), pet.hunger);
bar(11, FS("Joy"), pet.joy);
bar(22, FS("Rest"), pet.energy);
bar(33, FS("Health"), pet.health);
hline(0, 45, 128, ON);
at(0, 50);
words(stageName());
words(FS(", day "));
num(pet.age / DAY);
if (pet.sick) sprite(120, 49, SKULL, 7, 7, ON);
else if (pet.asleep) text(116, 50, FS("zz"));
}
// ------------------------------------------------------------------ growing up
void drawHatch(unsigned long t) {
drawTop();
if (t < 1800) {
int8_t shake = ((t / 60) & 1) ? 2 : -2;
sprite(56 + shake, 35, EGG, 16, 18, ON);
uint8_t cracks = t / 450; // a zigzag that grows across the shell
for (uint8_t i = 0; i < cracks && i < 4; i++) {
for (int8_t j = 0; j < 4; j++) px(57 + shake + i * 4 + j, 44 + ((i & 1) ? 3 - j : j), ON);
}
} else if (t < 2100) {
box(0, 10, 128, 44, ON); // a flash
} else {
drawPip(64, 53, F_HAPPY, 0, false);
sparkle(40, 26, 3);
sparkle(88, 22, 2);
sparkle(84, 42, 1);
centered(12, FS("Hello, Pip!"));
}
}
void drawEvolve(unsigned long t) {
uint8_t was = pet.stage;
// Flicker between the old shape and the new one, faster and faster.
uint16_t period = t < 800 ? 200 : (t < 1600 ? 100 : 50);
if (t < 2400 && (t / period) & 1) pet.stage = was + 1;
if (t >= 2400) pet.stage = was + 1;
drawPip(64, 53, t >= 2400 ? F_HAPPY : F_NORMAL, 0, false);
pet.stage = was;
if (t >= 2400) {
sparkle(36, 24, 3);
sparkle(94, 30, 2);
centered(12, FS("Pip grew up!"));
}
}
void drawGone(unsigned long t) {
int16_t rise = t / 120 < 20 ? t / 120 : 20;
int16_t y = 50 - rise;
// A little star with wings, drifting up.
display.fillCircle(64, y - 8, 6, ON);
rbox(59, y - 20, 11, 3, ON, false); // a halo
sprite(48, y - 16 + (frame & 2), WING_L, 9, 9, ON);
sprite(72, y - 16 + (frame & 2), WING_R, 9, 9, ON);
hline(61, y - 9, 2, OFF);
hline(66, y - 9, 2, OFF);
px(64, y - 5, OFF);
for (uint8_t i = 0; i < 6; i++) {
if ((frame + i * 5) % 16 < 10) px((i * 41 + 7) % 128, 10 + (i * 23) % 30, ON);
}
centered(46, FS("Pip went home"));
centered(56, FS("to the stars. OK"));
}
// ------------------------------------------------------------------ setup and loop
unsigned long lastTick = 0;
unsigned long lastFrame = 0;
void setup() {
Serial.begin(115200);
pinMode(BTN_SELECT, INPUT_PULLUP);
pinMode(BTN_CONFIRM, INPUT_PULLUP);
pinMode(BTN_CANCEL, INPUT_PULLUP);
pinMode(BUZZER, OUTPUT);
PCMSK2 = (1 << BTN_SELECT) | (1 << BTN_CONFIRM) | (1 << BTN_CANCEL);
PCICR |= 1 << PCIE2;
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
say(FS("No OLED at 0x3C: check SDA on A4 and SCL on A5\n"));
for (;;) {
}
}
Wire.setClock(400000);
display.setTextColor(ON);
delay(20);
bool fresh = digitalRead(BTN_CANCEL) == LOW; // CANCEL held at power-up
if (!fresh && load()) {
say(FS("Welcome back! Pip is a "));
say(stageName());
say(FS(", day "));
Serial.println((unsigned int)(pet.age / DAY));
if (pet.stage == GONE) {
go(GONE_MODE);
} else {
loved = 30;
tell(FS("Welcome back!"), 1800);
play(TUNE_HAPPY);
}
} else {
newEgg();
say(FS("Pocket pet: a new egg! Keep it warm.\n"));
play(TUNE_CHIRP);
}
lastTick = millis();
}
void loop() {
unsigned long now = millis();
readButtons(now);
music(now);
if (now - lastTick >= TICK_MS) {
lastTick += TICK_MS;
if (mode != GONE_MODE) petMinute();
}
unsigned long t = now - modeAt;
switch (mode) {
case MAIN: doMain(); break;
case FEED: doFeed(); break;
case PLAYMENU: doPlayMenu(); break;
case GUESS: doGuess(now); break;
case CATCH: doCatch(now); break;
case STATS:
if (bSelect.pressed || bConfirm.pressed || bCancel.pressed) {
click();
go(MAIN);
}
break;
case EAT:
if (t > 2500) {
loved = 25;
go(MAIN);
}
break;
case CLEAN:
if (t > 1300) {
pet.poop = 0;
pet.dirty = 0;
pet.joy = up(pet.joy, 5);
say(FS("All clean\n"));
save();
go(MAIN);
}
break;
case MEDS:
if (t > 1800) {
pet.doses++;
if (pet.health >= 50 || pet.doses >= 2) {
pet.sick = false;
pet.sugar = 0;
say(FS("Pip feels better\n"));
play(TUNE_HAPPY);
tell(FS("All better!"), 1200);
} else {
say(FS("One more dose\n"));
tell(FS("One more dose"), 1200);
}
save();
}
break;
case HATCH:
if (t > 3600) {
pet.stage = BABY;
say(FS("Pip hatched!\n"));
save();
go(MAIN);
}
break;
case EVOLVE:
if (t > 3600) {
pet.stage++;
say(FS("Pip grew up: "));
say(stageName());
Serial.println();
loved = 30;
save();
go(MAIN);
}
break;
case MESSAGE:
if (t > messageMs || ((bConfirm.pressed || bCancel.pressed) && t > 300)) go(MAIN);
break;
case GONE_MODE:
if (bConfirm.pressed && t > 2500) {
newEgg();
say(FS("A new egg!\n"));
play(TUNE_CHIRP);
go(MAIN);
}
break;
}
if (now - lastFrame < 80) return;
lastFrame = now;
frame++;
if (loved) loved--;
if (mode == CATCH) stepCatch(now);
if (mode == MAIN) walk();
// The library sends only the parts of the screen that changed, so a frame
// is cheapest when as little as possible is rubbed out and drawn again. A
// new screen starts from a clean slate; after that, a menu is redrawn only
// when its choice moves, and an animation only rubs out its own area.
static Mode drawnMode = GONE_MODE;
static uint8_t drawnChoice = 255;
bool fresh = mode != drawnMode;
if (fresh) {
display.clearDisplay();
drawnMode = mode;
}
bool menuChanged = fresh || choice != drawnChoice;
drawnChoice = choice;
t = now - modeAt;
switch (mode) {
case MAIN: drawRoom(fresh); break;
case FEED:
if (menuChanged) box(0, 0, 128, 64, OFF), drawFeed();
break;
case PLAYMENU:
if (menuChanged) box(0, 0, 128, 64, OFF), drawPlayMenu();
break;
case STATS:
if (fresh || (frame & 15) == 0) box(0, 0, 128, 64, OFF), drawStats();
break;
case CATCH:
if (fresh)
for (int16_t x = 0; x < 128; x += 3) px(x, 54, ON);
box(0, 0, 128, 54, OFF);
drawCatch(now);
break;
case GUESS: box(0, 0, 128, 64, OFF), drawGuess(now); break;
case EAT: box(0, 0, 128, 64, OFF), drawEat(t); break;
case CLEAN: box(0, 0, 128, 64, OFF), drawClean(t); break;
case MEDS: box(0, 0, 128, 64, OFF), drawMeds(t); break;
case HATCH: box(0, 0, 128, 64, OFF), drawHatch(t); break;
case EVOLVE: box(0, 0, 128, 64, OFF), drawEvolve(t); break;
case GONE_MODE: box(0, 0, 128, 64, OFF), drawGone(t); break;
case MESSAGE: {
drawRoom(fresh);
uint8_t w = textWidth(message) + 12;
int16_t x = (128 - w) / 2;
rbox(x, 12, w, 15, OFF, true);
rbox(x, 12, w, 15, ON, false);
text(x + 6, 16, message);
break;
}
}
display.display();
}
Code walkthrough
The key functions of the sketch, and what each one does.
setup sketch.ino
Sets the three button pins to INPUT_PULLUP and turns on the pin-change interrupt for them. It starts the OLED at address 0x3C and stops with a message if none answers. Then it checks whether CANCEL is held. If not, it tries load. If that fails, or CANCEL was held, it creates a new egg.
void setup() {
Serial.begin(115200);
pinMode(BTN_SELECT, INPUT_PULLUP);
pinMode(BTN_CONFIRM, INPUT_PULLUP);
pinMode(BTN_CANCEL, INPUT_PULLUP);
pinMode(BUZZER, OUTPUT);
PCMSK2 = (1 << BTN_SELECT) | (1 << BTN_CONFIRM) | (1 << BTN_CANCEL);
PCICR |= 1 << PCIE2;
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
say(FS("No OLED at 0x3C: check SDA on A4 and SCL on A5\n"));
for (;;) {
}
}
Wire.setClock(400000);
display.setTextColor(ON);
delay(20);
bool fresh = digitalRead(BTN_CANCEL) == LOW; // CANCEL held at power-up
if (!fresh && load()) {
say(FS("Welcome back! Pip is a "));
say(stageName());
say(FS(", day "));
Serial.println((unsigned int)(pet.age / DAY));
if (pet.stage == GONE) {
go(GONE_MODE);
} else {
loved = 30;
// ... 10 more lines in the editorreadButtons sketch.ino
Picks up button presses that the interrupt handler latched while a slow screen update was running. A press only counts if the pin had been HIGH for at least 15 ms, which filters out contact flicker. Each button gets a pressed flag for a new press and a down flag for held.
void readButtons(unsigned long now) {
noInterrupts();
uint8_t hit = latched;
latched = 0;
interrupts();
uint8_t pins = PIND;
for (Button &b : buttons) {
b.pressed = (hit & b.mask) != 0;
b.down = b.pressed || !(pins & b.mask);
if (b.pressed) {
lastPress = now;
if (!seeded) {
randomSeed(micros());
seeded = true;
}
}
}
}petMinute sketch.ino
Runs once per TICK_MS. It adds one to the age, then lowers hunger, joy and energy at set intervals, handles sleep, sickness and poop, and calls save every SAVE_EVERY pet minutes. If health reaches 0, Pip is gone.
void petMinute() {
if (pet.stage == GONE) return;
pet.age++;
// Every rate below divides 360, so one 32-bit division a minute is enough
// and the rest are on a 16-bit number, which is far cheaper on an AVR.
minute = pet.age % 360;
if (pet.stage == EGG_STAGE) {
if (pet.age >= HATCH_AT && mode != HATCH) {
go(HATCH);
play(TUNE_HATCH);
}
return;
}
if (pet.asleep) {
if (every((pet.lightsOn ? 8 : 4))) pet.energy = up(pet.energy, 1);
if (every(18)) pet.hunger = down(pet.hunger, 1);
if (pet.lightsOn && every(6)) pet.joy = down(pet.joy, 1);
if (pet.energy >= 100) {
pet.asleep = false;
say(FS("Pip woke up\n"));
play(TUNE_CHIRP);
}
} else {
if (every(6)) pet.hunger = down(pet.hunger, 1);
if (every(9)) pet.energy = down(pet.energy, 1);
if (every(8)) pet.joy = down(pet.joy, 1);
if (pet.poop && every(4)) pet.joy = down(pet.joy, 1);
// ... 62 more lines in the editorload sketch.ino
Reads the saved struct from EEPROM and accepts it only if the magic number and the checksum both match and the stage is valid. That protects against an empty or corrupted EEPROM, in which case the sketch starts a fresh egg instead.
bool load() {
EEPROM.get(0, pet);
return pet.magic == MAGIC && pet.check == checksum() && pet.stage <= GONE;
}Troubleshooting
The usual mistakes with this circuit, and the fix for each.
- The screen stays blank and Serial prints a message about no OLED This project
Check SDA on A4 and SCL on A5, plus 5 V and GND on the display. The sketch looks for address 0x3C. - Pip starts over as an egg after every reset This project
You may be holding CANCEL (pin 4) during power-up, which begins again on purpose. Check that button is wired to pin 4 and ground and is not stuck. - A button does nothing This project
Each button goes between its pin (2, 3 or 4) and ground. The pins use the internal pull-up, so no resistor is needed. - The OLED stays blank. OLED display
Check SDA and SCL are not swapped, and power it with 3.3 V or 5 V as its board allows. Most use address 0x3C, some 0x3D. Call display.begin() with the matching address. - Text never shows up even though the code runs. OLED display
Drawing only writes to a buffer. Call display.display() after drawing to push it out. Also set a text color with setTextColor(WHITE) and clear the buffer between frames. - The input reads random HIGH and LOW when the button is not pressed. Pushbutton
A floating pin picks up noise. Add a 10 k pull-down or pull-up resistor, or use INPUT_PULLUP and wire the button between the pin and GND. Then pressed reads LOW. - The button seems always pressed, or never changes. Pushbutton
A 4-leg tactile switch has legs joined in pairs. Place it across the breadboard center gap so the pairs sit on different rows. Wired along one side, the legs are already connected. - A passive piezo stays silent when you just set the pin HIGH. Piezo speaker
A passive piezo needs a changing signal. Use tone(pin, frequency) or a PWM wave. Around 2 to 4 kHz is loudest. A buzzer with a built in oscillator makes sound from plain DC instead.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.