/* * 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" #include #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 // 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_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) uint8_t poop; // 0..3 piles on the floor uint8_t sleeping; // 1 while napping uint8_t alive; } Pet; 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 // settings static uint8_t sound_on = 1; static uint8_t timeout_idx = 1; // index into screen_timeout_opts (default 30 s) static uint8_t set_sel = 0; // highlighted row on the settings screen // screen-off-on-inactivity options, in frames (0 = never sleep the display) static const uint16_t screen_timeout_opts[] = { 250, 500, 1000, 0 }; static const char *const screen_timeout_names[] = { "15s", "30s", "60s", "Off" }; #define TIMEOUT_OPT_COUNT 4 #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); } // 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.poop = 0; pet.sleeping = 0; pet.alive = 1; screen = SCR_MAIN; cursor = 0; for (int i = 0; i < N_COUNT; i++) { warn_flag[i] = 0; crit_flag[i] = 0; } } // 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(); 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(); 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); break; case ACT_HEAL: pet.health = clamp100(pet.health + 35); beep(784, 60); beep(1047, 60); beep(1319, 90); led_blink(); 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); 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); } } static void draw_main(void) { ssd1306_setbuf(0); // top bar: action label, 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); 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); } ssd1306_drawImage(px, py, pana_frames[current_expression()], PANA_W, PANA_H, 0); // 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(); 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, "-- STATUS --"); draw_bar(10, "FOOD", pet.satiety); draw_bar(19, "FUN ", pet.happy); draw_bar(28, "ENGY", pet.energy); draw_bar(37, "WASH", pet.hygiene); draw_bar(46, "HLTH", pet.health); char line[17]; snprintf(line, sizeof(line), "Age %u %s", pet.age_min, pet.sleeping ? "Zzz" : ""); ssd1306_drawstr(0, 56, line, 1); ssd1306_refresh(); } static void draw_dead(void) { ssd1306_setbuf(0); ssd1306_drawImage(48, 8, spr_grave, 32, 32, 0); draw_center_str(44, "R.I.P."); char line[17]; snprintf(line, sizeof(line), "lived %u min", pet.age_min); draw_center_str(52, line); 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]; snprintf(rows[0], 17, "Screen off:%s", screen_timeout_names[timeout_idx]); snprintf(rows[1], 17, "Sound: %s", sound_on ? "On" : "Off"); snprintf(rows[2], 17, "Back"); 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; } // ----------------------------------------------------------------- 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) { ssd1306_setbuf(0); draw_center_str(4, "PANAGOTCHI"); ssd1306_drawImage(48, 16, pana_idle, PANA_W, PANA_H, 0); draw_center_str(52, "press any key"); ssd1306_refresh(); beep(659, 80); beep(784, 80); 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(); 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(); pet_reset(); splash(); Buttons prev = {0, 0, 0}; while (1) { 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; if (!disp_on) { // screen is asleep: the first press only wakes it (not an action) if (any_edge) { ssd1306_cmd(SSD1306_DISPLAYON); disp_on = 1; } } else { switch (screen) { 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) { // start a new pet pet_reset(); 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--; // 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); } }