/* * PANAGOTCHI -- a Tamagotchi-style virtual pet for the CH32V003F4P6 badge. * * Hardware (see pinout.md): * PA1 indicator LED (active-high) * PC0 OK button (active-low, internal pull-up) * PD5 Right button (active-low, internal pull-up) * PD6 Left button (active-low, internal pull-up) * PC1/PC2 SSD1306 128x64 OLED over I2C1 (driven by ssd1306_i2c.h) * PD2 passive piezo buzzer (square-wave bit-bang tone) * * Controls: Left / Right move the menu cursor, OK activates the selected * action. On the stats / dead screens any button returns / restarts. */ #include "ch32fun.h" // The bundled I2C driver calls printf() only to log a timeout we can't act on // (by then the display is dead). Stub it to a no-op so the ~1 KB printf // formatter is never referenced and the linker discards it. We format our own // numbers with the tiny s_cat/u_cat helpers below. #define printf(...) ((void)0) #define SSD1306_128X64 // 128x64 panel (see pinout.md / OLED size) #include "ssd1306_i2c.h" #include "ssd1306.h" #include "sprites.h" // ----------------------------------------------------------------- pins #define LED_PIN PA1 #define BTN_OK PC0 #define BTN_RIGHT PD5 #define BTN_LEFT PD6 #define BUZZER PD2 // ----------------------------------------------------------------- tuning #define FRAME_MS 60 // main loop period #define TICK_FRAMES 250 // stat update every ~15 s (250 * 60 ms) #define ANIM_FRAMES 12 // how long an action reaction is shown #define TICK_MS (TICK_FRAMES * FRAME_MS) // 15000 ms: also the screen-off check period // With the values below an untended pet survives roughly half an hour: // a need empties in ~20 min, after which health bleeds down over ~10 more. // ----------------------------------------------------------------- state enum { SCR_SELECT, SCR_MAIN, SCR_STATS, SCR_DEAD, SCR_SETTINGS }; // notification thresholds: gentle chirp at <50%, urgent at <20% enum { N_FOOD, N_FUN, N_ENGY, N_WASH, N_COUNT }; typedef struct { int8_t satiety; // 0..100 (fullness) int8_t happy; // 0..100 int8_t energy; // 0..100 int8_t hygiene; // 0..100 int8_t health; // 0..100 uint16_t age_min; // age in game-minutes (one per tick) uint16_t xp; // experience earned by good care uint8_t species; // SP_* starter type chosen on the welcome screen uint8_t level; // 1..MAX_LEVEL uint8_t form; // FORM_* (display + life stage), derived from level uint8_t ascend; // 0 undecided, 1 Claude, 2 glitch (latched at max lvl) uint8_t poop; // 0..3 piles on the floor uint8_t sleeping; // 1 while napping uint8_t alive; } Pet; // Hacker privilege-escalation life stages: proc -> user -> root, then the pet // either ascends to "Claude" (well-tended) or rots into a glitch. enum { FORM_PROC, FORM_USER, FORM_ROOT, FORM_CLAUDE, FORM_GLITCH, FORM_COUNT }; static const char *const form_names[FORM_COUNT] = { "proc", "user", "root", "Claude", "glitch", }; #define MAX_LEVEL 4 // cumulative XP required to reach level (index = level-1) static const uint16_t level_xp[MAX_LEVEL] = { 0, 45, 120, 240 }; static Pet pet; static uint8_t screen; static uint8_t cursor; // selected menu icon 0..ICON_COUNT-1 static uint8_t anim_exp; // expression forced during a reaction static uint8_t anim_timer; // frames remaining for the reaction static uint32_t frame; // free-running frame counter static uint16_t tick_acc; // frames since last stat tick static uint8_t warn_flag[N_COUNT]; // already chirped a <50% warning static uint8_t crit_flag[N_COUNT]; // already chirped a <20% warning static uint8_t evo_anim; // frames remaining for an evolution banner // settings static uint8_t sound_on = 1; static uint8_t timeout_idx = 1; // index into screen_timeout_opts (default 10 s) static uint8_t set_sel = 0; // highlighted row on the settings screen static uint8_t sel_species = 0; // highlighted species on the welcome screen // screen-off-on-inactivity options, in frames (0 = never sleep the display). // Shorter timeouts blank the OLED sooner, which is the single biggest power // win on a CR2032 — the panel draws far more than the dozing MCU. static const uint16_t screen_timeout_opts[] = { 83, 167, 250, 500, 1000, 0 }; static const char *const screen_timeout_names[] = { "5s", "10s", "15s", "30s", "60s", "Off" }; #define TIMEOUT_OPT_COUNT 6 #define SETTINGS_ROWS 3 // screen-off, sound, back // display power state static uint8_t disp_on = 1; static uint32_t idle_frames = 0; // frames since the last button activity // menu icon order must match sprites.h menu_icons[] / menu_labels[] enum { ACT_FEED, ACT_PLAY, ACT_SLEEP, ACT_CLEAN, ACT_HEAL, ACT_STATS, ACT_SETTINGS }; // ----------------------------------------------------------------- helpers static int8_t clamp100(int v) { return v < 0 ? 0 : (v > 100 ? 100 : v); } // Tiny string builders used instead of snprintf: we only ever paste short // strings and small unsigned ints, and avoiding keeps the whole // printf machinery (~1 KB) out of the image. Each returns the new end pointer // so calls chain; remember to NUL-terminate the final buffer yourself. static char *s_cat(char *p, const char *s) { while (*s) *p++ = *s++; return p; } static char *u_cat(char *p, unsigned v) { char tmp[5]; int n = 0; do { tmp[n++] = '0' + v % 10; v /= 10; } while (v); while (n) *p++ = tmp[--n]; return p; } // short square-wave beep on the passive piezo (blocking, ms-scale only) static void beep(uint16_t freq, uint16_t ms) { if (!sound_on) return; if (freq == 0) { Delay_Ms(ms); return; } uint32_t half = 500000u / freq; // half-period in us uint32_t cycles = ((uint32_t)ms * 1000u) / (half * 2u); for (uint32_t i = 0; i < cycles; i++) { funDigitalWrite(BUZZER, FUN_HIGH); Delay_Us(half); funDigitalWrite(BUZZER, FUN_LOW); Delay_Us(half); } } static void led_blink(void) { funDigitalWrite(LED_PIN, FUN_HIGH); Delay_Ms(30); funDigitalWrite(LED_PIN, FUN_LOW); } static void pet_reset(void) { pet.satiety = 80; pet.happy = 80; pet.energy = 90; pet.hygiene = 100; pet.health = 100; pet.age_min = 0; pet.xp = 0; pet.species = sel_species; pet.level = 1; pet.form = FORM_PROC; pet.ascend = 0; pet.poop = 0; pet.sleeping = 0; pet.alive = 1; screen = SCR_MAIN; cursor = 0; evo_anim = 0; for (int i = 0; i < N_COUNT; i++) { warn_flag[i] = 0; crit_flag[i] = 0; } } // Map the current level (and, at the top, care quality) to a display form. static void compute_form(void) { switch (pet.level) { case 1: pet.form = FORM_PROC; break; case 2: pet.form = FORM_USER; break; case 3: pet.form = FORM_ROOT; break; default: // max level: branch once, latched if (!pet.ascend) pet.ascend = (pet.health >= 60) ? 1 : 2; pet.form = (pet.ascend == 1) ? FORM_CLAUDE : FORM_GLITCH; break; } } // Award XP; promote (and trigger the evolution banner) when a tier is reached. static void add_xp(uint8_t n) { if (!pet.alive) return; if (pet.xp < 60000) pet.xp += n; uint8_t nl = 1; for (uint8_t i = 1; i < MAX_LEVEL; i++) if (pet.xp >= level_xp[i]) nl = i + 1; if (nl > pet.level) { pet.level = nl; compute_form(); evo_anim = 30; // ~1.8 s celebratory banner beep(784, 60); beep(988, 60); beep(1175, 60); beep(1568, 140); led_blink(); } } // Chirp once when a need first drops below 50% (gentle) or 20% (urgent), // re-arming only after it recovers. Audible even while the screen is off, // so it works as a "tend to me" reminder. static void notify(uint8_t idx, int8_t val) { if (val < 20) { if (!crit_flag[idx]) { // urgent: descending triple beep(1568, 60); beep(1175, 60); beep(880, 90); crit_flag[idx] = 1; warn_flag[idx] = 1; } } else if (val < 50) { if (!warn_flag[idx]) { // gentle: single chirp beep(1568, 50); warn_flag[idx] = 1; } crit_flag[idx] = 0; } else { warn_flag[idx] = 0; crit_flag[idx] = 0; } } static void check_notifications(void) { notify(N_FOOD, pet.satiety); notify(N_FUN, pet.happy); notify(N_ENGY, pet.energy); notify(N_WASH, pet.hygiene); } // pick the expression frame to draw this instant static uint8_t current_expression(void) { if (!pet.alive) return EXP_DEAD; if (anim_timer) return anim_exp; if (pet.sleeping) return EXP_SLEEP; if (pet.health < 30 || pet.happy < 25 || pet.satiety < 20) return EXP_SAD; // idle, with an occasional blink if ((frame % 50) < 4) return EXP_BLINK; return EXP_IDLE; } // ----------------------------------------------------------------- actions static void react(uint8_t exp) { anim_exp = exp; anim_timer = ANIM_FRAMES; } static void do_action(uint8_t act) { switch (act) { case ACT_FEED: pet.satiety = clamp100(pet.satiety + 30); pet.hygiene = clamp100(pet.hygiene - 5); react(EXP_EAT); beep(880, 60); beep(1175, 80); led_blink(); add_xp(4); break; case ACT_PLAY: if (pet.energy < 15) { beep(220, 120); break; } // too tired pet.happy = clamp100(pet.happy + 25); pet.energy = clamp100(pet.energy - 12); react(EXP_HAPPY); beep(988, 60); beep(1319, 60); beep(1568, 90); led_blink(); add_xp(6); break; case ACT_SLEEP: pet.sleeping = !pet.sleeping; beep(pet.sleeping ? 440 : 660, 120); break; case ACT_CLEAN: pet.poop = 0; pet.hygiene = 100; pet.happy = clamp100(pet.happy + 5); beep(1319, 50); beep(1047, 50); add_xp(3); break; case ACT_HEAL: pet.health = clamp100(pet.health + 35); beep(784, 60); beep(1047, 60); beep(1319, 90); led_blink(); add_xp(4); break; case ACT_STATS: screen = SCR_STATS; beep(659, 40); break; case ACT_SETTINGS: screen = SCR_SETTINGS; set_sel = 0; beep(659, 40); break; } } // ----------------------------------------------------------------- game tick static void game_tick(void) { pet.age_min++; if (pet.sleeping) { pet.energy = clamp100(pet.energy + 6); // needs still drift down slowly even while asleep pet.satiety = clamp100(pet.satiety - 1); if (pet.energy >= 100) pet.sleeping = 0; // wakes when rested } else { pet.satiety = clamp100(pet.satiety - 1); pet.energy = clamp100(pet.energy - 1); pet.happy = clamp100(pet.happy - 1); } // hygiene drifts every other tick if (pet.age_min & 1) pet.hygiene = clamp100(pet.hygiene - 1); // occasional bowel movements (derived from counters, no RNG needed) if (!pet.sleeping && pet.poop < 3 && ((pet.age_min * 7u + frame) & 0x1F) == 0) pet.poop++; // being dirty / full of poop slowly saps happiness if (pet.poop && (pet.age_min & 1)) pet.happy = clamp100(pet.happy - 1); if (pet.hygiene < 20 && (pet.age_min & 1)) pet.happy = clamp100(pet.happy - 1); // health responds to overall wellbeing int8_t lo = pet.satiety; if (pet.happy < lo) lo = pet.happy; if (pet.energy < lo) lo = pet.energy; if (pet.hygiene < lo) lo = pet.hygiene; if (lo < 15) pet.health = clamp100(pet.health - 3); else if (lo > 50) pet.health = clamp100(pet.health + 2); // a well-kept pet matures: passive XP while every need stays healthy if (lo > 50 && !pet.sleeping) add_xp(3); check_notifications(); if (pet.health <= 0) { pet.alive = 0; screen = SCR_DEAD; beep(330, 200); beep(247, 200); beep(165, 400); } } // ----------------------------------------------------------------- drawing static void draw_center_str(int y, const char *s) { int len = 0; while (s[len]) len++; int x = (SSD1306_W - len * 8) / 2; if (x < 0) x = 0; ssd1306_drawstr(x, y, s, 1); } static void draw_menu_bar(void) { // 7 icons across the bottom (18 px slots), selected one boxed for (uint8_t i = 0; i < ICON_COUNT; i++) { int x = i * 18 + 1; ssd1306_drawImage(x, 48, menu_icons[i], ICON_W, ICON_H, 0); if (i == cursor) ssd1306_drawRect(x - 1, 47, ICON_W + 1, ICON_H + 1, 1); } } // Per-species eye geometry, so the shades land on the right spot. static const uint8_t eye_cy[SPECIES_COUNT] = { 11, 17, 12 }; // SP_SHELL,PANKO,TMUX static const uint8_t eye_lx[SPECIES_COUNT] = { 11, 11, 9 }; static const uint8_t eye_rx[SPECIES_COUNT] = { 20, 21, 23 }; // Hacker garb earned as the pet levels up, drawn dark over the lit body so it // reads as black gear. static void draw_accessories(int x, int y) { uint8_t sp = pet.species; uint8_t cy = eye_cy[sp], lx = eye_lx[sp], rx = eye_rx[sp]; if (pet.form >= FORM_USER) { // shades ssd1306_fillRect(x + lx - 3, y + cy - 2, 7, 4, 0); ssd1306_fillRect(x + rx - 3, y + cy - 2, 7, 4, 0); if (rx - lx > 7) ssd1306_drawFastHLine(x + lx + 4, y + cy - 1, rx - lx - 7, 0); } if (pet.form >= FORM_ROOT) { // gaming headset + mic boom ssd1306_drawLine(x + 3, y + 10, x + 9, y + 2, 0); ssd1306_drawLine(x + 9, y + 2, x + 23, y + 2, 0); ssd1306_drawLine(x + 23, y + 2, x + 29, y + 10, 0); ssd1306_fillRect(x + 1, y + 12, 4, 7, 0); ssd1306_drawLine(x + 3, y + 18, x + 10, y + 24, 0); } } static void draw_creature(int x, int y) { const unsigned char *spr; if (pet.form == FORM_CLAUDE) spr = spr_claude; else if (pet.form == FORM_GLITCH) spr = spr_glitch; else spr = species_frames[pet.species][current_expression()]; ssd1306_drawImage(x, y, spr, PANA_W, PANA_H, 0); // growing forms wear gear; the ascended/glitched forms speak for themselves if (spr != spr_claude && spr != spr_glitch && !pet.sleeping) draw_accessories(x, y); // Claude radiates: a few twinkling pixels that shift each frame if (pet.form == FORM_CLAUDE) { ssd1306_drawPixel(x - 2 + (frame & 3), y + 4, 1); ssd1306_drawPixel(x + PANA_W + (frame & 1), y + 10, 1); } } static void draw_evo_banner(void) { ssd1306_fillRect(6, 18, SSD1306_W - 12, 24, 0); ssd1306_drawRect(6, 18, SSD1306_W - 12, 24, 1); draw_center_str(21, ">> evolving"); char b[17], *p = b; p = s_cat(p, "[ "); p = s_cat(p, form_names[pet.form]); p = s_cat(p, " ]"); *p = 0; draw_center_str(31, b); } static void draw_main(void) { ssd1306_setbuf(0); // top bar: action label + level badge, with a tiny low-health warning draw_center_str(0, menu_labels[cursor]); if (pet.alive && pet.health < 30 && (frame & 8)) ssd1306_drawstr(0, 0, "!", 1); char badge[6], *bp = badge; *bp++ = 'L'; bp = u_cat(bp, pet.level); *bp = 0; ssd1306_drawstr(SSD1306_W - 2 * 8, 0, badge, 1); ssd1306_drawFastHLine(0, 10, SSD1306_W, 1); // pet: gentle horizontal stroll + vertical bob (still while asleep) int px, py; if (pet.sleeping) { px = 48; py = 16; ssd1306_drawstr(px + PANA_W - 2, 14, "z", 1); ssd1306_drawstr(px + PANA_W + 4, 12, "Z", 1); } else { uint32_t t = frame % 80; px = (t < 40) ? 30 + t : 30 + (80 - t); // 30..70..30 py = 14 + ((frame % 16) < 8 ? 0 : 1); } draw_creature(px, py); // floor + poop piles ssd1306_drawFastHLine(0, 46, SSD1306_W, 1); for (uint8_t i = 0; i < pet.poop; i++) ssd1306_drawImage(8 + i * 12, 38, spr_poop, 8, 8, 0); draw_menu_bar(); if (evo_anim) draw_evo_banner(); // celebratory overlay on top ssd1306_refresh(); } static void draw_bar(int y, const char *label, int8_t val) { ssd1306_drawstr(0, y, label, 1); int bx = 48, bw = 76; ssd1306_drawRect(bx, y, bw, 7, 1); int fill = (val * (bw - 2)) / 100; if (fill > 0) ssd1306_fillRect(bx + 1, y + 1, fill, 5, 1); } static void draw_stats(void) { ssd1306_setbuf(0); draw_center_str(0, "~/pet $ stat"); draw_bar(10, "FOOD", pet.satiety); draw_bar(19, "HAPPY", pet.happy); draw_bar(28, "ENERGY", pet.energy); draw_bar(37, "CLEAN", pet.hygiene); draw_bar(46, "HEALTH", pet.health); // no form name here: keep room for the XP-to-next-level readout char line[17], *p = line; *p++ = 'L'; p = u_cat(p, pet.level); p = s_cat(p, " xp "); if (pet.level < MAX_LEVEL) { p = u_cat(p, pet.xp); *p++ = '/'; p = u_cat(p, level_xp[pet.level]); } else { p = s_cat(p, "MAX"); } *p = 0; ssd1306_drawstr(0, 56, line, 1); ssd1306_refresh(); } static void draw_dead(void) { ssd1306_setbuf(0); ssd1306_drawImage(48, 2, spr_grave, 32, 32, 0); draw_center_str(36, pet.ascend == 2 ? "** CORRUPTED **" : "SEGFAULT (11)"); char line[17], *p = line; p = s_cat(p, "core dumped L"); p = u_cat(p, pet.level); *p = 0; draw_center_str(46, line); draw_center_str(56, "OK: respawn"); ssd1306_refresh(); } // welcome screen: choose a starter species before booting the pet static void draw_select(void) { ssd1306_setbuf(0); draw_center_str(0, "~ $ adopt --pet"); ssd1306_drawFastHLine(0, 10, SSD1306_W, 1); // big idle preview of the highlighted species (no gear at level 1) ssd1306_drawImage(48, 13, species_frames[sel_species][EXP_IDLE], PANA_W, PANA_H, 0); char line[17], *p = line; p = s_cat(p, "< "); p = s_cat(p, species_names[sel_species]); p = s_cat(p, " >"); *p = 0; draw_center_str(47, line); draw_center_str(56, "L/R pick OK go"); ssd1306_refresh(); } static void draw_settings(void) { ssd1306_setbuf(0); draw_center_str(0, "- SETTINGS -"); ssd1306_drawFastHLine(0, 10, SSD1306_W, 1); char rows[SETTINGS_ROWS][17]; *s_cat(s_cat(rows[0], "Screen off:"), screen_timeout_names[timeout_idx]) = 0; *s_cat(s_cat(rows[1], "Sound: "), sound_on ? "On" : "Off") = 0; *s_cat(rows[2], "Back") = 0; for (uint8_t r = 0; r < SETTINGS_ROWS; r++) { int y = 18 + r * 12; if (r == set_sel) ssd1306_fillRect(0, y - 1, SSD1306_W, 10, 1); // selected row drawn inverted (color 0 on the lit bar) ssd1306_drawstr(4, y, rows[r], r == set_sel ? 0 : 1); } draw_center_str(56, "L/R move OK use"); ssd1306_refresh(); } // ----------------------------------------------------------------- input typedef struct { uint8_t ok, left, right; } Buttons; static Buttons read_buttons(void) { // active-low: pressed reads 0 Buttons b; b.ok = !funDigitalRead(BTN_OK); b.left = !funDigitalRead(BTN_LEFT); b.right = !funDigitalRead(BTN_RIGHT); return b; } // ----------------------------------------------------------------- low power // Two wake sources let the core idle in WFI light-sleep while the OLED is off: // * a button press (EXTI falling edge) -> light the screen back up // * a periodic TIM2 overflow (~once per game tick) -> run a quick check // WFI only gates the CPU clock, so SRAM/registers (and the whole Pet) survive // untouched; full standby would wipe them and force a reboot/splash on wake. static volatile uint8_t exti_wake; // a button interrupt fired static volatile uint8_t tick_wake; // the periodic check timer fired // OK=PC0->EXTI0, Right=PD5->EXTI5, Left=PD6->EXTI6 all share this vector. void EXTI7_0_IRQHandler(void) INTERRUPT_DECORATOR; void EXTI7_0_IRQHandler(void) { EXTI->INTFR = EXTI_Line0 | EXTI_Line5 | EXTI_Line6; // clear pending exti_wake = 1; } void TIM2_IRQHandler(void) INTERRUPT_DECORATOR; void TIM2_IRQHandler(void) { TIM2->INTFR = (uint16_t)~TIM_FLAG_Update; // clear update flag tick_wake = 1; } static void lowpower_setup(void) { // --- button wake: falling-edge EXTI on the three button pins --- // (AFIO + GPIO clocks were already enabled by funGpioInitAll.) AFIO->EXTICR |= AFIO_EXTICR_EXTI0_PC | // OK on PC0 AFIO_EXTICR_EXTI5_PD | // Right on PD5 AFIO_EXTICR_EXTI6_PD; // Left on PD6 EXTI->INTENR |= EXTI_Line0 | EXTI_Line5 | EXTI_Line6; EXTI->FTENR |= EXTI_Line0 | EXTI_Line5 | EXTI_Line6; // press == falling edge NVIC_EnableIRQ(EXTI7_0_IRQn); // --- periodic wake: TIM2 update IRQ, one overflow per game tick --- RCC->APB1PCENR |= RCC_APB1Periph_TIM2; TIM2->PSC = 23999; // 24 MHz / 24000 = 1 kHz (1 ms/count) TIM2->ATRLR = TICK_MS - 1; // overflow once per game tick TIM2->SWEVGR = TIM_PSCReloadMode_Immediate; // latch PSC/ARR now TIM2->INTFR = (uint16_t)~TIM_FLAG_Update; TIM2->DMAINTENR |= TIM_UIE; // enable update interrupt NVIC_EnableIRQ(TIM2_IRQn); // the counter itself stays disabled until we actually enter low-power sleep } // Doze in WFI until a button or the periodic timer wakes us. Returns only once // the display has been switched back on (button press, or the pet dying while // we slept); otherwise it keeps running background ticks and sleeping again. static void enter_low_power(void) { for (;;) { exti_wake = 0; tick_wake = 0; TIM2->CNT = 0; TIM2->INTFR = (uint16_t)~TIM_FLAG_Update; TIM2->CTLR1 |= TIM_CEN; // arm the periodic wake __WFI(); // core clock gated until an IRQ TIM2->CTLR1 &= ~TIM_CEN; // halt it while we're awake if (exti_wake) { // a button: bring the screen back ssd1306_cmd(SSD1306_DISPLAYON); disp_on = 1; idle_frames = 0; tick_acc = 0; return; } // otherwise it was the periodic check: advance the pet a tick. This // still chirps low-need reminders (and the death tune) over the buzzer. if (pet.alive) { game_tick(); if (!pet.alive) { // it died in its sleep: show the grave ssd1306_cmd(SSD1306_DISPLAYON); disp_on = 1; idle_frames = 0; return; } } } } // ----------------------------------------------------------------- setup static void gpio_setup(void) { funGpioInitAll(); funPinMode(LED_PIN, GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); funDigitalWrite(LED_PIN, FUN_LOW); funPinMode(BUZZER, GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); funDigitalWrite(BUZZER, FUN_LOW); // buttons: input with internal pull-up (ODR high selects pull-up) funPinMode(BTN_OK, GPIO_CNF_IN_PUPD); funPinMode(BTN_LEFT, GPIO_CNF_IN_PUPD); funPinMode(BTN_RIGHT, GPIO_CNF_IN_PUPD); funDigitalWrite(BTN_OK, FUN_HIGH); funDigitalWrite(BTN_LEFT, FUN_HIGH); funDigitalWrite(BTN_RIGHT, FUN_HIGH); } static void splash(void) { // fake terminal boot sequence, revealed line by line static const char *const boot[] = { "panagotchi v2", "booting kernel..", "mount /pet ok", "spawn shell ok", "> ./tamagotchi_", }; ssd1306_setbuf(0); for (uint8_t i = 0; i < 5; i++) { ssd1306_drawstr(0, i * 10, boot[i], 1); ssd1306_refresh(); beep(1200 + i * 120, 25); Delay_Ms(220); } draw_center_str(54, "press any key"); ssd1306_refresh(); beep(988, 120); // wait until a button is pressed (or ~4 s timeout) for (int i = 0; i < 200; i++) { Buttons b = read_buttons(); if (b.ok || b.left || b.right) break; Delay_Ms(20); } } // ----------------------------------------------------------------- main int main(void) { SystemInit(); gpio_setup(); lowpower_setup(); // button-wake EXTI + periodic-check timer Delay_Ms(120); // let the OLED rail settle if (ssd1306_i2c_init()) { // non-zero == failure // no display: fall back to a heartbeat blink so the board still shows life while (1) { led_blink(); Delay_Ms(700); } } ssd1306_init(); splash(); screen = SCR_SELECT; // pick a species before a pet is born Buttons prev = {0, 0, 0}; while (1) { if (!disp_on) { // Display is off: hand the core to WFI light-sleep, waking only // for button presses or the periodic background check. It returns // once the screen is lit again; swallow whatever press woke us so // it just wakes the display rather than also firing an action. enter_low_power(); prev = read_buttons(); continue; } Buttons b = read_buttons(); uint8_t ok_edge = b.ok && !prev.ok; uint8_t left_edge = b.left && !prev.left; uint8_t right_edge = b.right && !prev.right; uint8_t any_edge = ok_edge | left_edge | right_edge; prev = b; if (any_edge) idle_frames = 0; { switch (screen) { case SCR_SELECT: if (left_edge) { sel_species = (sel_species + SPECIES_COUNT - 1) % SPECIES_COUNT; beep(1047, 25); } if (right_edge) { sel_species = (sel_species + 1) % SPECIES_COUNT; beep(1047, 25); } if (ok_edge) { // born! boot the chosen species pet_reset(); // sets screen = SCR_MAIN, species = sel_species beep(659, 80); beep(988, 120); } draw_select(); break; case SCR_MAIN: if (left_edge) { cursor = (cursor + ICON_COUNT - 1) % ICON_COUNT; beep(1047, 25); } if (right_edge) { cursor = (cursor + 1) % ICON_COUNT; beep(1047, 25); } if (ok_edge) do_action(cursor); draw_main(); break; case SCR_STATS: if (any_edge) { screen = SCR_MAIN; beep(880, 30); } draw_stats(); break; case SCR_SETTINGS: if (left_edge) { set_sel = (set_sel + SETTINGS_ROWS - 1) % SETTINGS_ROWS; beep(1047, 25); } if (right_edge) { set_sel = (set_sel + 1) % SETTINGS_ROWS; beep(1047, 25); } if (ok_edge) { if (set_sel == 0) { // cycle screen-off timeout timeout_idx = (timeout_idx + 1) % TIMEOUT_OPT_COUNT; beep(880, 40); } else if (set_sel == 1) { // toggle sound sound_on = !sound_on; beep(988, 60); // only audible if just enabled } else { // Back screen = SCR_MAIN; beep(880, 30); } } draw_settings(); break; case SCR_DEAD: if (ok_edge) { // back to the welcome screen to re-pick screen = SCR_SELECT; beep(659, 80); beep(988, 120); } draw_dead(); break; } // sleep the display after the configured idle period uint16_t timeout = screen_timeout_opts[timeout_idx]; if (timeout && idle_frames >= timeout) { ssd1306_cmd(SSD1306_DISPLAYOFF); disp_on = 0; } } if (anim_timer) anim_timer--; if (evo_anim) evo_anim--; // advance game time independent of screen / display state if (pet.alive && ++tick_acc >= TICK_FRAMES) { tick_acc = 0; game_tick(); } frame++; idle_frames++; Delay_Ms(FRAME_MS); } }