414 lines
11 KiB
C
414 lines
11 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 50 // stat update every ~3 s (50 * 60 ms)
|
|
#define ANIM_FRAMES 12 // how long an action reaction is shown
|
|
|
|
// ----------------------------------------------------------------- state
|
|
enum { SCR_MAIN, SCR_STATS, SCR_DEAD };
|
|
|
|
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
|
|
|
|
// menu icon order must match sprites.h menu_icons[] / menu_labels[]
|
|
enum { ACT_FEED, ACT_PLAY, ACT_SLEEP, ACT_CLEAN, ACT_HEAL, ACT_STATS };
|
|
|
|
// ----------------------------------------------------------------- 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 (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;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------- game tick
|
|
static void game_tick(void)
|
|
{
|
|
pet.age_min++;
|
|
|
|
if (pet.sleeping) {
|
|
pet.energy = clamp100(pet.energy + 10);
|
|
// 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 - 3);
|
|
pet.energy = clamp100(pet.energy - 2);
|
|
pet.happy = clamp100(pet.happy - 2);
|
|
}
|
|
|
|
pet.hygiene = clamp100(pet.hygiene - 1);
|
|
|
|
// random-ish bowel movements (LFSR-free: derive from counters)
|
|
if (!pet.sleeping && pet.poop < 3 &&
|
|
((pet.age_min * 7u + frame) & 0x0F) == 0)
|
|
pet.poop++;
|
|
|
|
// being dirty / full of poop saps happiness
|
|
if (pet.poop) pet.happy = clamp100(pet.happy - pet.poop);
|
|
if (pet.hygiene < 20) 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 - 5);
|
|
else if (lo > 50) pet.health = clamp100(pet.health + 2);
|
|
|
|
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)
|
|
{
|
|
// 6 icons across the bottom, selected one boxed
|
|
for (uint8_t i = 0; i < ICON_COUNT; i++) {
|
|
int x = i * 21 + 3;
|
|
ssd1306_drawImage(x, 48, menu_icons[i], ICON_W, ICON_H, 0);
|
|
if (i == cursor)
|
|
ssd1306_drawRect(x - 2, 46, ICON_W + 3, ICON_H + 2, 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();
|
|
}
|
|
|
|
// ----------------------------------------------------------------- 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;
|
|
prev = b;
|
|
|
|
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 (ok_edge || left_edge || right_edge) {
|
|
screen = SCR_MAIN;
|
|
beep(880, 30);
|
|
}
|
|
draw_stats();
|
|
break;
|
|
|
|
case SCR_DEAD:
|
|
if (ok_edge) { // start a new pet
|
|
pet_reset();
|
|
beep(659, 80); beep(988, 120);
|
|
}
|
|
draw_dead();
|
|
break;
|
|
}
|
|
|
|
if (anim_timer) anim_timer--;
|
|
|
|
// advance game time independent of which screen is showing
|
|
if (pet.alive && ++tick_acc >= TICK_FRAMES) {
|
|
tick_acc = 0;
|
|
game_tick();
|
|
}
|
|
|
|
frame++;
|
|
Delay_Ms(FRAME_MS);
|
|
}
|
|
}
|