feat: pogoda, poprawki w wyświetlaniu. Clean Architecture

This commit is contained in:
2026-06-05 16:36:43 +02:00
parent 7ba9c55e27
commit 856990f232
17 changed files with 1517 additions and 634 deletions
+5
View File
@@ -92,6 +92,11 @@ tasks:
&& echo "[config-download] Zapisano do data/config.json (tylko gesty, bez WiFi)" \
|| { echo "[config-download] BLAD: nie mozna polaczyc z $IP"; exit 1; }
test:
desc: Uruchom testy jednostkowe (native, na Mac)
cmds:
- pio test -e native
default:
desc: Pokaz liste dostepnych zadan
cmds:
+130
View File
@@ -0,0 +1,130 @@
#include "BuddyLogic.h"
#include <algorithm>
// Replaces Arduino constrain()
template<typename T>
static T clamp(T v, T lo, T hi) { return std::max(lo, std::min(hi, v)); }
void initBuddy(BuddyState &b, uint32_t now)
{
b = {};
b.blinkState = BLINK_OPEN;
b.blinkRy = EYE_RY;
b.lastEvent = now;
b.nextBlink = now + 3000;
b.nextLook = now + 2000;
b.nextZzz = now + 3000;
b.nextMicroTremor = now + 500;
}
void setBuddyMood(BuddyState &b, Mood m, uint32_t now, uint32_t durationMs)
{
b.mood = m;
b.revertAt = (durationMs > 0) ? now + durationMs : 0;
b.lastEvent = now;
switch (m) {
case MOOD_HAPPY:
b.pupilTargetDx = 0; b.pupilTargetDy = -2; break;
case MOOD_SLEEPY:
b.pupilTargetDx = 0; b.pupilTargetDy = 4; break;
case MOOD_SURPRISED:
b.pupilTargetDx = 0; b.pupilTargetDy = 0; break;
case MOOD_SAD:
b.pupilTargetDx = 0; b.pupilTargetDy = 4; break;
case MOOD_ANGRY:
b.pupilTargetDx = 2; b.pupilTargetDy = 2; break;
default: break;
}
}
void updateBuddyAnim(BuddyState &b, uint32_t now, RngFn rng)
{
// Timed mood revert
if (b.revertAt > 0 && now >= b.revertAt) {
b.mood = MOOD_NORMAL;
b.revertAt = 0;
}
// Auto-sleep after 5 min idle
if (b.mood == MOOD_NORMAL && now - b.lastEvent > 300000UL)
setBuddyMood(b, MOOD_SLEEPY, now, 0);
// Blink state machine (disabled for SURPRISED, EXCITED, WINKs)
if (b.mood != MOOD_SURPRISED && b.mood != MOOD_EXCITED &&
b.mood != MOOD_WINK_L && b.mood != MOOD_WINK_R)
{
switch (b.blinkState) {
case BLINK_OPEN:
if (now >= b.nextBlink)
b.blinkState = BLINK_CLOSING;
break;
case BLINK_CLOSING:
if (b.blinkRy > 3)
b.blinkRy -= 4;
else {
b.blinkRy = 1;
b.blinkState = BLINK_CLOSED;
b.closedTicks = 0;
}
break;
case BLINK_CLOSED:
if (b.closedTicks++ >= 2)
b.blinkState = BLINK_OPENING;
break;
case BLINK_OPENING:
b.blinkRy += 4;
if (b.blinkRy >= EYE_RY) {
b.blinkRy = EYE_RY;
b.blinkState = BLINK_OPEN;
b.nextBlink = now + (int32_t)((b.mood == MOOD_SLEEPY)
? rng(800, 2000) : rng(2500, 6000));
}
break;
}
} else {
b.blinkRy = EYE_RY;
}
// Saccadic pupil movement — fast (3px/tick) when far (|delta|>=4), slow (1px) when close
{
int8_t ddx = b.pupilTargetDx - b.pupilDx;
int8_t ddy = b.pupilTargetDy - b.pupilDy;
int8_t absDdx = ddx < 0 ? (int8_t)-ddx : ddx;
int8_t absDdy = ddy < 0 ? (int8_t)-ddy : ddy;
if (ddx) b.pupilDx = (int8_t)(b.pupilDx + (ddx > 0 ? 1 : -1) * (absDdx >= 4 ? 3 : 1));
if (ddy) b.pupilDy = (int8_t)(b.pupilDy + (ddy > 0 ? 1 : -1) * (absDdy >= 4 ? 3 : 1));
}
// Gaze — random saccades + micro-tremor
{
bool fixated = (b.pupilDx == b.pupilTargetDx && b.pupilDy == b.pupilTargetDy);
if (b.mood == MOOD_SLEEPY) {
if (fixated && now >= b.nextLook) {
b.pupilTargetDx = (int8_t)rng(-3, 4);
b.pupilTargetDy = (int8_t)rng(2, 6);
b.nextLook = now + (uint32_t)rng(4000, 9000);
}
} else if (b.mood != MOOD_WINK_L && b.mood != MOOD_WINK_R) {
if (fixated && now >= b.nextLook) {
b.pupilTargetDx = (int8_t)rng(-7, 8);
b.pupilTargetDy = (int8_t)rng(-5, 6);
b.nextLook = now + (uint32_t)rng(1500, 4500);
}
if ((b.mood == MOOD_NORMAL || b.mood == MOOD_HAPPY) &&
fixated && now >= b.nextMicroTremor)
{
b.pupilTargetDx = (int8_t)clamp(
(int32_t)b.pupilTargetDx + rng(-1, 2), (int32_t)-7, (int32_t)7);
b.pupilTargetDy = (int8_t)clamp(
(int32_t)b.pupilTargetDy + rng(-1, 2), (int32_t)-5, (int32_t)5);
b.nextMicroTremor = now + (uint32_t)rng(300, 700);
}
}
}
// ZZZ bubbles when sleepy
if (b.mood == MOOD_SLEEPY && now >= b.nextZzz) {
b.zzzPhase = (b.zzzPhase % 3) + 1;
b.nextZzz = now + 700;
}
}
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include "BuddyTypes.h"
// Eye radius — used in blink state machine and main.cpp drawing
static const uint8_t EYE_RY = 15;
// Inject RNG so tests can supply a deterministic function
using RngFn = int32_t(*)(int32_t lo, int32_t hi);
void initBuddy(BuddyState &b, uint32_t now);
void setBuddyMood(BuddyState &b, Mood m, uint32_t now, uint32_t durationMs = 0);
void updateBuddyAnim(BuddyState &b, uint32_t now, RngFn rng);
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include <stdint.h>
enum Mood : uint8_t {
MOOD_NORMAL = 0,
MOOD_HAPPY,
MOOD_SLEEPY,
MOOD_SURPRISED,
MOOD_ANGRY,
MOOD_SAD,
MOOD_EXCITED,
MOOD_WINK_L,
MOOD_WINK_R,
MOOD_HUNGRY,
MOOD_PLAYFUL,
MOOD_DIRTY
};
enum BlinkState : uint8_t {
BLINK_OPEN,
BLINK_CLOSING,
BLINK_CLOSED,
BLINK_OPENING
};
struct BuddyState {
Mood mood;
uint32_t revertAt;
uint32_t lastEvent;
BlinkState blinkState;
uint8_t blinkRy;
uint8_t closedTicks;
uint32_t nextBlink;
int8_t pupilDx, pupilDy;
int8_t pupilTargetDx, pupilTargetDy;
uint32_t nextLook;
uint8_t zzzPhase;
uint32_t nextZzz;
uint32_t nextMicroTremor;
};
+1
View File
@@ -0,0 +1 @@
{ "name": "BuddyDomain", "version": "1.0.0", "frameworks": "*", "platforms": "*" }
+31
View File
@@ -0,0 +1,31 @@
#include "GestureConfig.h"
const char *GNAME[NUM_GESTURES] = {
"up", "down", "left", "right", "forward", "backward", "clockwise", "anticlockwise", "wave"};
const char *GKEY[NUM_GESTURES] = {
"u", "d", "l", "r", "f", "b", "cw", "ccw", "w"};
const char *MOOD_LABELS[] = {
"-- bez zmiany --", "happy ^_^", "sleepy zZz", "surprised o_O",
"angry >_<", "sad T_T", "excited *_*", "wink L ;)", "wink R (;",
"hungry :(", "playful :D", "dirty ..."};
const char *ACTION_LABELS[] = {
"-- brak --", "Data i godzina", "Status WiFi",
"Nakarm", "Pobaw sie", "Umyj", "Status tamagotchi"};
const Mood DEFAULT_MOOD[NUM_GESTURES] = {
MOOD_HAPPY, // up
MOOD_SAD, // down
MOOD_SURPRISED, // left
MOOD_SURPRISED, // right
MOOD_SLEEPY, // forward
MOOD_ANGRY, // backward
MOOD_EXCITED, // clockwise
MOOD_NORMAL, // anticlockwise
MOOD_EXCITED, // wave
};
const uint32_t DEFAULT_MOOD_DUR[NUM_GESTURES] = {
2000, 2000, 1500, 1500, 0, 3000, 2000, 0, 2000};
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <stdint.h>
#include "BuddyTypes.h"
static const uint8_t NUM_GESTURES = 9;
static const uint8_t NUM_ACTIONS = 7;
enum Action : uint8_t {
ACTION_NONE = 0,
ACTION_DATETIME = 1,
ACTION_WIFI = 2,
ACTION_FEED = 3,
ACTION_PLAY = 4,
ACTION_CLEAN = 5,
ACTION_STATUS = 6
};
struct GestureConfig {
char url[128];
uint8_t mood; // 0 = no change, 1-11 matches Mood enum
uint8_t action; // Action enum
bool enabled;
};
extern const char *GNAME[NUM_GESTURES];
extern const char *GKEY[NUM_GESTURES];
extern const char *MOOD_LABELS[];
extern const char *ACTION_LABELS[];
extern const Mood DEFAULT_MOOD[NUM_GESTURES];
extern const uint32_t DEFAULT_MOOD_DUR[NUM_GESTURES];
+1
View File
@@ -0,0 +1 @@
{ "name": "GestureConfig", "version": "1.0.0", "frameworks": "*", "platforms": "*" }
+57
View File
@@ -0,0 +1,57 @@
#include "TamaLogic.h"
void initTama(TamaState &t, uint32_t now)
{
t.hunger = 10;
t.happiness = 80;
t.hygiene = 90;
t.nextHungerTick = now + 120000UL;
t.nextHappyTick = now + 180000UL;
t.nextHygieneTick = now + 240000UL;
}
Mood updateTama(TamaState &t, const BuddyState &b, uint32_t now)
{
// Tick needs over time
if (now >= t.nextHungerTick) {
t.nextHungerTick = now + 120000UL;
if (t.hunger < 100) t.hunger++;
}
if (now >= t.nextHappyTick) {
t.nextHappyTick = now + 180000UL;
if (t.happiness > 0) t.happiness--;
}
if (now >= t.nextHygieneTick) {
t.nextHygieneTick = now + 240000UL;
if (t.hygiene > 0) t.hygiene--;
}
// Return mood override based on critical needs.
// Guard: skip when buddy has a timed mood or is in non-overridable state.
if (b.revertAt == 0 && b.mood != MOOD_SLEEPY &&
b.mood != MOOD_WINK_L && b.mood != MOOD_WINK_R)
{
if (t.hunger >= 80) return MOOD_HUNGRY;
if (t.hygiene <= 20) return MOOD_DIRTY;
if (t.happiness <= 20) return MOOD_PLAYFUL;
// Needs satisfied — signal "reset to normal" (caller handles if buddy is in need-mood)
}
return MOOD_NORMAL;
}
void tamaFeed(TamaState &t)
{
t.hunger = (t.hunger >= 30) ? t.hunger - 30 : 0;
}
void tamaPlay(TamaState &t)
{
uint16_t v = (uint16_t)t.happiness + 25;
t.happiness = (v > 100) ? 100 : (uint8_t)v;
}
void tamaClean(TamaState &t)
{
uint16_t v = (uint16_t)t.hygiene + 40;
t.hygiene = (v > 100) ? 100 : (uint8_t)v;
}
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <stdint.h>
#include "BuddyTypes.h"
struct TamaState {
uint8_t hunger; // 0=full → 100=starving
uint8_t happiness; // 100=happy → 0=bored
uint8_t hygiene; // 100=clean → 0=dirty
uint32_t nextHungerTick;
uint32_t nextHappyTick;
uint32_t nextHygieneTick;
};
void initTama(TamaState &t, uint32_t now);
// Updates ticks and returns the mood override for buddy.
// Returns MOOD_HUNGRY / MOOD_DIRTY / MOOD_PLAYFUL when a need is critical.
// Returns MOOD_NORMAL when needs are satisfied or guard blocks (revertAt != 0, sleepy, wink).
Mood updateTama(TamaState &t, const BuddyState &b, uint32_t now);
void tamaFeed(TamaState &t); // hunger -= 30 (floor 0)
void tamaPlay(TamaState &t); // happy += 25 (cap 100)
void tamaClean(TamaState &t); // hygiene += 40 (cap 100)
+1
View File
@@ -0,0 +1 @@
{ "name": "TamaLogic", "version": "1.0.0", "frameworks": "*", "platforms": "*" }
+87
View File
@@ -0,0 +1,87 @@
# ESP32-C6 Desk Buddy — Project Memory
## Hardware
- Board: Seeed XIAO ESP32-C6
- Display: SSD1306 128x64 OLED
- Gesture sensor: CJMCU-7620 (PAJ7620U2, I2C addr 0x73)
## I2C — CRITICAL
- ESP32-C6 has two I2C buses:
- I2C0 (HP, `Wire`): configurable to any GPIO — USE THIS
- I2C1 (LP, `Wire1`): hardware-locked to SDA=GPIO6, SCL=GPIO7 — NOT usable on XIAO (pins not exposed)
- **Both SSD1306 and PAJ7620 share one bus**: `Wire.begin(22, 23)` — SDA=GPIO22(D4), SCL=GPIO23(D5)
- SSD1306=0x3C, PAJ7620=0x73 — different addresses, coexist fine
- SW I2C (U8g2 bit-bang) blocks CPU ~40ms/frame — kills WiFi on single-core ESP32-C6. Always use HW I2C.
- U8g2 HW I2C constructor: `U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, 23, 22)`
## PAJ7620 API
- Library: `acrandal/RevEng PAJ7620`
- Gesture type: `Gesture` (NOT `Gesture_t`)
- begin() takes pointer: `sensor.begin(&Wire)` (NOT reference)
- `Wire1` pre-defined in framework — do not redeclare `TwoWire Wire1(1)`
- Gesture index: `(int)g - 1` maps GES_UP(1)..GES_WAVE(9) to 0..8
## HTTP Server — CRITICAL
- `WebServer.h` does NOT work reliably on ESP32-C6 with IDF 5.x (pioarduino platform)
- Use `AsyncWebServer` from `mathieucarbou/ESPAsyncWebServer@^3.3.12`
- No `handleClient()` needed — callback-based, works on interrupts
- JSON POST body handler: use the 3-arg `on()` with body callback (4th param)
## NVS / Preferences
- Always open with `prefs.begin("namespace", false)``true` (read-only) fails with NOT_FOUND if namespace doesn't exist yet
- NVS key max 15 chars — use short prefixes: u/d/l/r/f/b/cw/ccw/w for gestures
## platformio.ini
```ini
platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip
board = seeed_xiao_esp32c6
framework = arduino
lib_deps =
olikraus/U8g2
acrandal/RevEng PAJ7620
bblanchon/ArduinoJson@^7.2.1
mathieucarbou/ESPAsyncWebServer@^3.3.12
build_flags =
-std=gnu++17
board_build.partitions = default # no Zigbee partitions needed
```
## Desk Buddy — Architecture
- Moods (0-11): NORMAL, HAPPY, SLEEPY, SURPRISED, ANGRY, SAD, EXCITED, WINK_L, WINK_R, HUNGRY, PLAYFUL, DIRTY
- Actions (0-6): NONE, DATETIME, WIFI, FEED, PLAY, CLEAN, STATUS
- Eyes: `drawFilledEllipse` + mood-specific shapes; wink = `drawCircle` UPPER arc
- Wink: WINK_L closes screen-RIGHT eye (!isLeft = buddy's left), WINK_R closes screen-LEFT
- Blink: state machine OPEN→CLOSING→CLOSED→OPENING, 4px/tick
- Pupil: smooth drift toward random target every 1.5-4s (NORMAL only)
- Sleepy: triggered after 5 min idle (`lastEvent` tracking), ZZZ bubbles
- Auto-revert: `revertAt` timestamp, 0 = permanent mood
- Display refresh: 50ms (~20fps) — OK with HW I2C
## Webhook System
- Per-gesture config: URL (128 chars) + mood (0=none, 1-6) + enabled
- Fired async via FreeRTOS `xTaskCreate` — non-blocking
- POST `{"gesture":"wave"}` to configured URL, 3s timeout
- Config persisted in NVS, served/edited via HTTP at GET/POST /
- JSON API: GET /api/config, POST /api/config
## Key Files
- `src/main.cpp` — hardware + drawing + HTTP server + WiFi + FreeRTOS (~400 lines)
- `lib/BuddyDomain/` — BuddyTypes.h, BuddyLogic.h/.cpp (mood, blink, pupil — no Arduino)
- `lib/TamaLogic/` — TamaLogic.h/.cpp (hunger/happiness/hygiene ticks — no Arduino)
- `lib/GestureConfig/` — GestureConfig.h/.cpp (struct, enums, string tables)
- `platformio.ini` — [env:esp32-c6] + [env:native]
- `test/native/test_buddy/` + `test/native/test_tama/` — Unity tests (40 total)
- Reference project: `../handsensor/` — webhook/config patterns
## Testing
- `pio test -e native` or `task test` — runs 40 unit tests on Mac (no device needed)
- `pio run -e esp32-c6` — firmware build
- Domain libs have zero Arduino deps: `<algorithm>` only; RngFn injection for determinism
## Domain API
- `initBuddy(BuddyState &b, uint32_t now)` — pass millis()
- `setBuddyMood(BuddyState &b, Mood m, uint32_t now, uint32_t durationMs=0)`
- `updateBuddyAnim(BuddyState &b, uint32_t now, RngFn rng)` — RngFn = int32_t(*)(lo,hi)
- `initTama(TamaState &t, uint32_t now)`
- `updateTama(TamaState &t, const BuddyState &b, uint32_t now)` → returns Mood override
- `tamaFeed/Play/Clean(TamaState &t)` — direct state mutations
+5
View File
@@ -0,0 +1,5 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x1C0000,
spiffs, data, spiffs, 0x1D0000, 0x230000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x1C0000
5 spiffs data spiffs 0x1D0000 0x230000
+12
View File
@@ -14,4 +14,16 @@ lib_deps =
build_flags =
-std=gnu++17
-D FORMAT_LITTLEFS_IF_FAILED
board_build.partitions = partitions.csv
board_build.filesystem = littlefs
test_ignore = native/*
[env:native]
platform = native
build_flags =
-std=c++17
-I lib/BuddyDomain
-I lib/TamaLogic
-I lib/GestureConfig
test_build_src = no
test_filter = native/*
+659 -634
View File
File diff suppressed because it is too large Load Diff
+220
View File
@@ -0,0 +1,220 @@
#include <unity.h>
#include "BuddyLogic.h"
static int32_t fixed_rng(int32_t lo, int32_t hi) { (void)hi; return lo; }
void setUp() {}
void tearDown() {}
// ── initBuddy ──────────────────────────────────────────────────────────────
void test_initBuddy_sets_open_blink() {
BuddyState b{};
initBuddy(b, 0);
TEST_ASSERT_EQUAL(BLINK_OPEN, b.blinkState);
TEST_ASSERT_EQUAL(EYE_RY, b.blinkRy);
}
void test_initBuddy_sets_normal_mood() {
BuddyState b{};
initBuddy(b, 0);
TEST_ASSERT_EQUAL(MOOD_NORMAL, b.mood);
}
void test_initBuddy_timestamps_relative_to_now() {
BuddyState b{};
initBuddy(b, 1000);
TEST_ASSERT_EQUAL(1000u, b.lastEvent);
TEST_ASSERT_EQUAL(1000u + 3000, b.nextBlink);
TEST_ASSERT_EQUAL(1000u + 2000, b.nextLook);
}
// ── setBuddyMood ───────────────────────────────────────────────────────────
void test_setBuddyMood_no_duration_sets_revertAt_zero() {
BuddyState b{};
initBuddy(b, 0);
setBuddyMood(b, MOOD_HAPPY, 1000, 0);
TEST_ASSERT_EQUAL(0u, b.revertAt);
}
void test_setBuddyMood_with_duration_sets_revertAt() {
BuddyState b{};
initBuddy(b, 0);
setBuddyMood(b, MOOD_HAPPY, 1000, 4000);
TEST_ASSERT_EQUAL(5000u, b.revertAt);
}
void test_setBuddyMood_updates_lastEvent() {
BuddyState b{};
initBuddy(b, 0);
setBuddyMood(b, MOOD_HAPPY, 5000);
TEST_ASSERT_EQUAL(5000u, b.lastEvent);
}
void test_setBuddyMood_happy_sets_pupil_target_up() {
BuddyState b{};
initBuddy(b, 0);
setBuddyMood(b, MOOD_HAPPY, 0);
TEST_ASSERT_EQUAL( 0, b.pupilTargetDx);
TEST_ASSERT_EQUAL(-2, b.pupilTargetDy);
}
void test_setBuddyMood_sad_sets_pupil_target_down() {
BuddyState b{};
initBuddy(b, 0);
setBuddyMood(b, MOOD_SAD, 0);
TEST_ASSERT_EQUAL(0, b.pupilTargetDx);
TEST_ASSERT_EQUAL(4, b.pupilTargetDy);
}
// ── updateBuddyAnim — mood revert ─────────────────────────────────────────
void test_updateBuddyAnim_reverts_mood_when_revertAt_passed() {
BuddyState b{};
initBuddy(b, 0);
setBuddyMood(b, MOOD_HAPPY, 0, 4000); // revertAt = 4000
updateBuddyAnim(b, 5000, fixed_rng); // now > revertAt
TEST_ASSERT_EQUAL(MOOD_NORMAL, b.mood);
TEST_ASSERT_EQUAL(0u, b.revertAt);
}
void test_updateBuddyAnim_does_not_revert_before_revertAt() {
BuddyState b{};
initBuddy(b, 0);
setBuddyMood(b, MOOD_HAPPY, 0, 4000);
updateBuddyAnim(b, 3999, fixed_rng);
TEST_ASSERT_EQUAL(MOOD_HAPPY, b.mood);
}
void test_updateBuddyAnim_goes_sleepy_after_idle() {
BuddyState b{};
initBuddy(b, 0); // lastEvent = 0, mood = NORMAL
updateBuddyAnim(b, 300001, fixed_rng);
TEST_ASSERT_EQUAL(MOOD_SLEEPY, b.mood);
}
void test_updateBuddyAnim_no_sleepy_before_idle() {
BuddyState b{};
initBuddy(b, 0);
updateBuddyAnim(b, 299999, fixed_rng);
TEST_ASSERT_EQUAL(MOOD_NORMAL, b.mood);
}
// ── Blink state machine ────────────────────────────────────────────────────
void test_blink_open_to_closing_when_nextBlink_passed() {
BuddyState b{};
initBuddy(b, 0); // nextBlink = 3000
updateBuddyAnim(b, 3001, fixed_rng);
TEST_ASSERT_EQUAL(BLINK_CLOSING, b.blinkState);
}
void test_blink_closing_decrements_blinkRy() {
BuddyState b{};
initBuddy(b, 0);
b.blinkState = BLINK_CLOSING;
b.blinkRy = 15;
updateBuddyAnim(b, 0, fixed_rng);
TEST_ASSERT_EQUAL(11, b.blinkRy);
TEST_ASSERT_EQUAL(BLINK_CLOSING, b.blinkState);
}
void test_blink_closing_reaches_closed_when_small() {
BuddyState b{};
initBuddy(b, 0);
b.blinkState = BLINK_CLOSING;
b.blinkRy = 3;
updateBuddyAnim(b, 0, fixed_rng);
TEST_ASSERT_EQUAL(BLINK_CLOSED, b.blinkState);
TEST_ASSERT_EQUAL(1, b.blinkRy);
}
void test_blink_closed_ticks_to_opening() {
BuddyState b{};
initBuddy(b, 0);
b.blinkState = BLINK_CLOSED;
b.closedTicks = 2;
updateBuddyAnim(b, 0, fixed_rng);
TEST_ASSERT_EQUAL(BLINK_OPENING, b.blinkState);
}
void test_blink_opening_increments_blinkRy() {
BuddyState b{};
initBuddy(b, 0);
b.blinkState = BLINK_OPENING;
b.blinkRy = 7; // 7+4=11 < EYE_RY(15), stays OPENING
updateBuddyAnim(b, 0, fixed_rng);
TEST_ASSERT_EQUAL(11, b.blinkRy);
TEST_ASSERT_EQUAL(BLINK_OPENING, b.blinkState);
}
void test_blink_opening_returns_to_open_at_eye_ry() {
BuddyState b{};
initBuddy(b, 0);
b.blinkState = BLINK_OPENING;
b.blinkRy = 13; // 13+4=17 >= EYE_RY(15) → OPEN
updateBuddyAnim(b, 0, fixed_rng);
TEST_ASSERT_EQUAL(BLINK_OPEN, b.blinkState);
TEST_ASSERT_EQUAL(EYE_RY, b.blinkRy);
}
void test_surprised_does_not_blink() {
BuddyState b{};
initBuddy(b, 0);
setBuddyMood(b, MOOD_SURPRISED, 0);
b.nextBlink = 0; // past-due
updateBuddyAnim(b, 1000, fixed_rng);
TEST_ASSERT_EQUAL(BLINK_OPEN, b.blinkState); // no transition
TEST_ASSERT_EQUAL(EYE_RY, b.blinkRy); // stays full
}
// ── Pupil saccade ──────────────────────────────────────────────────────────
void test_pupil_moves_fast_when_far() {
BuddyState b{};
initBuddy(b, 0);
b.pupilDx = 0;
b.pupilTargetDx = 8; // |delta| = 8 >= 4 → step 3
b.pupilDy = b.pupilTargetDy = 0;
updateBuddyAnim(b, 0, fixed_rng);
TEST_ASSERT_EQUAL(3, b.pupilDx);
}
void test_pupil_moves_slow_when_close() {
BuddyState b{};
initBuddy(b, 0);
b.pupilDx = 0;
b.pupilTargetDx = 2; // |delta| = 2 < 4 → step 1
b.pupilDy = b.pupilTargetDy = 0;
updateBuddyAnim(b, 0, fixed_rng);
TEST_ASSERT_EQUAL(1, b.pupilDx);
}
// ── main ──────────────────────────────────────────────────────────────────
int main() {
UNITY_BEGIN();
RUN_TEST(test_initBuddy_sets_open_blink);
RUN_TEST(test_initBuddy_sets_normal_mood);
RUN_TEST(test_initBuddy_timestamps_relative_to_now);
RUN_TEST(test_setBuddyMood_no_duration_sets_revertAt_zero);
RUN_TEST(test_setBuddyMood_with_duration_sets_revertAt);
RUN_TEST(test_setBuddyMood_updates_lastEvent);
RUN_TEST(test_setBuddyMood_happy_sets_pupil_target_up);
RUN_TEST(test_setBuddyMood_sad_sets_pupil_target_down);
RUN_TEST(test_updateBuddyAnim_reverts_mood_when_revertAt_passed);
RUN_TEST(test_updateBuddyAnim_does_not_revert_before_revertAt);
RUN_TEST(test_updateBuddyAnim_goes_sleepy_after_idle);
RUN_TEST(test_updateBuddyAnim_no_sleepy_before_idle);
RUN_TEST(test_blink_open_to_closing_when_nextBlink_passed);
RUN_TEST(test_blink_closing_decrements_blinkRy);
RUN_TEST(test_blink_closing_reaches_closed_when_small);
RUN_TEST(test_blink_closed_ticks_to_opening);
RUN_TEST(test_blink_opening_increments_blinkRy);
RUN_TEST(test_blink_opening_returns_to_open_at_eye_ry);
RUN_TEST(test_surprised_does_not_blink);
RUN_TEST(test_pupil_moves_fast_when_far);
RUN_TEST(test_pupil_moves_slow_when_close);
return UNITY_END();
}
+203
View File
@@ -0,0 +1,203 @@
#include <unity.h>
#include "TamaLogic.h"
void setUp() {}
void tearDown() {}
// Helper: build a minimal BuddyState for updateTama calls
static BuddyState makeBuddy(uint32_t revertAt = 0, Mood m = MOOD_NORMAL) {
BuddyState b{};
b.revertAt = revertAt;
b.mood = m;
return b;
}
// ── initTama ──────────────────────────────────────────────────────────────
void test_initTama_sets_initial_values() {
TamaState t{};
initTama(t, 0);
TEST_ASSERT_EQUAL(10, t.hunger);
TEST_ASSERT_EQUAL(80, t.happiness);
TEST_ASSERT_EQUAL(90, t.hygiene);
}
void test_initTama_sets_tick_timestamps() {
TamaState t{};
initTama(t, 1000);
TEST_ASSERT_EQUAL(1000u + 120000u, t.nextHungerTick);
TEST_ASSERT_EQUAL(1000u + 180000u, t.nextHappyTick);
TEST_ASSERT_EQUAL(1000u + 240000u, t.nextHygieneTick);
}
// ── updateTama ticks ──────────────────────────────────────────────────────
void test_updateTama_increments_hunger_on_tick() {
TamaState t{};
initTama(t, 0); // nextHungerTick = 120000
BuddyState b = makeBuddy();
updateTama(t, b, 120000);
TEST_ASSERT_EQUAL(11, t.hunger);
}
void test_updateTama_does_not_increment_hunger_before_tick() {
TamaState t{};
initTama(t, 0);
BuddyState b = makeBuddy();
updateTama(t, b, 119999);
TEST_ASSERT_EQUAL(10, t.hunger);
}
void test_updateTama_caps_hunger_at_100() {
TamaState t{};
initTama(t, 0);
t.hunger = 100;
BuddyState b = makeBuddy();
updateTama(t, b, 120000);
TEST_ASSERT_EQUAL(100, t.hunger);
}
void test_updateTama_decrements_happiness_on_tick() {
TamaState t{};
initTama(t, 0); // nextHappyTick = 180000
BuddyState b = makeBuddy();
updateTama(t, b, 180000);
TEST_ASSERT_EQUAL(79, t.happiness);
}
void test_updateTama_caps_happiness_at_zero() {
TamaState t{};
initTama(t, 0);
t.happiness = 0;
BuddyState b = makeBuddy();
updateTama(t, b, 180000);
TEST_ASSERT_EQUAL(0, t.happiness);
}
void test_updateTama_decrements_hygiene_on_tick() {
TamaState t{};
initTama(t, 0); // nextHygieneTick = 240000
BuddyState b = makeBuddy();
updateTama(t, b, 240000);
TEST_ASSERT_EQUAL(89, t.hygiene);
}
// ── updateTama mood returns ───────────────────────────────────────────────
void test_updateTama_returns_hungry_when_hunger_high() {
TamaState t{};
initTama(t, 0);
t.hunger = 80;
t.nextHungerTick = 999999; // prevent tick
BuddyState b = makeBuddy();
TEST_ASSERT_EQUAL(MOOD_HUNGRY, updateTama(t, b, 0));
}
void test_updateTama_returns_dirty_when_hygiene_low() {
TamaState t{};
initTama(t, 0);
t.hygiene = 20;
t.nextHygieneTick = 999999;
BuddyState b = makeBuddy();
TEST_ASSERT_EQUAL(MOOD_DIRTY, updateTama(t, b, 0));
}
void test_updateTama_returns_playful_when_happiness_low() {
TamaState t{};
initTama(t, 0);
t.happiness = 20;
t.nextHappyTick = 999999;
BuddyState b = makeBuddy();
TEST_ASSERT_EQUAL(MOOD_PLAYFUL, updateTama(t, b, 0));
}
void test_updateTama_no_override_when_revertAt_set() {
TamaState t{};
initTama(t, 0);
t.hunger = 90;
t.nextHungerTick = 999999;
BuddyState b = makeBuddy(5000); // revertAt != 0 → guard blocks
TEST_ASSERT_EQUAL(MOOD_NORMAL, updateTama(t, b, 0));
}
void test_updateTama_returns_normal_when_needs_ok() {
TamaState t{};
initTama(t, 0); // hunger=10, happy=80, hygiene=90 — all fine
t.nextHungerTick = t.nextHappyTick = t.nextHygieneTick = 999999;
BuddyState b = makeBuddy();
TEST_ASSERT_EQUAL(MOOD_NORMAL, updateTama(t, b, 0));
}
// ── tamaFeed ──────────────────────────────────────────────────────────────
void test_tamaFeed_reduces_hunger_by_30() {
TamaState t{};
t.hunger = 50;
tamaFeed(t);
TEST_ASSERT_EQUAL(20, t.hunger);
}
void test_tamaFeed_floors_hunger_at_zero() {
TamaState t{};
t.hunger = 10;
tamaFeed(t);
TEST_ASSERT_EQUAL(0, t.hunger);
}
// ── tamaPlay ──────────────────────────────────────────────────────────────
void test_tamaPlay_increases_happiness_by_25() {
TamaState t{};
t.happiness = 50;
tamaPlay(t);
TEST_ASSERT_EQUAL(75, t.happiness);
}
void test_tamaPlay_caps_happiness_at_100() {
TamaState t{};
t.happiness = 90;
tamaPlay(t);
TEST_ASSERT_EQUAL(100, t.happiness);
}
// ── tamaClean ─────────────────────────────────────────────────────────────
void test_tamaClean_increases_hygiene_by_40() {
TamaState t{};
t.hygiene = 50;
tamaClean(t);
TEST_ASSERT_EQUAL(90, t.hygiene);
}
void test_tamaClean_caps_hygiene_at_100() {
TamaState t{};
t.hygiene = 80;
tamaClean(t);
TEST_ASSERT_EQUAL(100, t.hygiene);
}
// ── main ──────────────────────────────────────────────────────────────────
int main() {
UNITY_BEGIN();
RUN_TEST(test_initTama_sets_initial_values);
RUN_TEST(test_initTama_sets_tick_timestamps);
RUN_TEST(test_updateTama_increments_hunger_on_tick);
RUN_TEST(test_updateTama_does_not_increment_hunger_before_tick);
RUN_TEST(test_updateTama_caps_hunger_at_100);
RUN_TEST(test_updateTama_decrements_happiness_on_tick);
RUN_TEST(test_updateTama_caps_happiness_at_zero);
RUN_TEST(test_updateTama_decrements_hygiene_on_tick);
RUN_TEST(test_updateTama_returns_hungry_when_hunger_high);
RUN_TEST(test_updateTama_returns_dirty_when_hygiene_low);
RUN_TEST(test_updateTama_returns_playful_when_happiness_low);
RUN_TEST(test_updateTama_no_override_when_revertAt_set);
RUN_TEST(test_updateTama_returns_normal_when_needs_ok);
RUN_TEST(test_tamaFeed_reduces_hunger_by_30);
RUN_TEST(test_tamaFeed_floors_hunger_at_zero);
RUN_TEST(test_tamaPlay_increases_happiness_by_25);
RUN_TEST(test_tamaPlay_caps_happiness_at_100);
RUN_TEST(test_tamaClean_increases_hygiene_by_40);
RUN_TEST(test_tamaClean_caps_hygiene_at_100);
return UNITY_END();
}