PanBadge/tama.c

838 lines
25 KiB
C

/*
* 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 <stdio.h> 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);
}
}