PanBadge/tama.c

539 lines
15 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"
#include <stdio.h>
#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);
}
}