feat(face): integrate Idle Task Protocol v1.0 end-to-end
Watchface now syncs with the Nine Hazes idle task system via its own
pkjs client — the three-artifact architecture is operational.
Wire side:
- pkjs fetches GET /v1/queue on launch, applies clock offset, converts
epoch-ms to seconds for AppMessage, resolves task name from the
cached dictionary (refetch on version drift)
- GameSnapshot persisted at key 0x30 (protocol-specified); task name
at 0x32 (face-local nicety)
- AppMessage inbox widened to 256 bytes to carry the full snapshot
dictionary in a single logical message
Display side:
- Task strip renders two lines: countdown-to-completion + used/36 +
task count, then task name with "(+)" pending-rewards cue
- Status states render distinctly: FROZEN / ABORTED(code) / LOCKED /
No hero yet (EMPTY)
- serverDown sentinel key: oxblood "!" icon on failed check; absence
on successful sync clears it; volatile — never persisted
- Countdown math is server-clock corrected (clock_off), computed
locally from bound_sec — zero radio to stay fresh
Also:
- ES5-normalized pkjs for SDK webpack 1.15/acorn parser
- Replaced face_packet.{h,c} with snapshot.{h,c} (PACKET_KEY 101
retired; old blob orphaned harmlessly)
- Mock Task API server seeded with 3-task queue (in-memory)
Verified: seeded queue renders "0:xx 16/36 seg 3 tasks / Gather (+)";
server kill → cached strip + offline icon, no error surface.
This commit is contained in:
208
docs/DESIGN.md
Normal file
208
docs/DESIGN.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Project Record: "Nine Hazes" (working title) — Celtic Idle RPG for Pebble Time 2
|
||||
|
||||
> v2.0 — post-architecture-revamp. The design was rebuilt on verified PebbleOS
|
||||
> platform constraints (see §4.4). Game mechanics (§§6–9) carry over unchanged.
|
||||
|
||||
## 1. Concept
|
||||
|
||||
A Celtic-themed idle RPG ecosystem for Pebble Time 2 (emery), built as:
|
||||
1. **A fully customizable watchface** — the flagship and first artifact. Ten
|
||||
toggleable, movable, stylable widgets incl. a Current Task strip. Complete
|
||||
and useful standalone, before any game exists.
|
||||
2. **A companion game watchapp** — the idle RPG itself. Canonical save lives
|
||||
on the watch. Entry via Quick Launch.
|
||||
3. **A self-hostable bridge server** — lightweight mirror of game state so the
|
||||
watchface can display the hero's current task. The developer's instance is
|
||||
default; players may self-host. Later doubles as multiplayer backend.
|
||||
|
||||
The game is played by queuing 15-minute-chunk tasks and returning to collect
|
||||
rewards. Battery impact of the watchface must be indistinguishable from a
|
||||
normal watchface.
|
||||
|
||||
## 2. Reference Projects (inspiration only — clean-room rules)
|
||||
|
||||
| Reference | What we take | Boundary |
|
||||
|---|---|---|
|
||||
| IdleFantasy (tristinbaker) | Task/expedition loop, chunk queue | No code; repo is Android/Kotlin |
|
||||
| Doors of Doom (msx80) | Combat structure, Range pattern, weighted loot, Entity/Run serialization, instance-vs-definition | Clean-room |
|
||||
| Pixel8 watchface | AppMessage patterns, persist-key discipline | No code reuse; license unknown |
|
||||
| My Vampire System | Leveling *system shape* only | No names/lore (IP risk) |
|
||||
| **Muninn (C-D-Lewis)** | Mechanic of on-watch battery-days estimation: slow sampling, moving-average drain rate, charge-event recalibration, 3-sample cold-start ramp. Also: Wakeup-not-Worker, rolling-single-wakeup scheduling, wakeup-collision retry | Study only; no code copied (repo has no LICENSE file) |
|
||||
| **freakified/pebble-calculator** | Existence proof that TouchService works on emery in the current SDK | API reference only |
|
||||
|
||||
## 3. Platform & Environment
|
||||
|
||||
- **Target: emery only** (Pebble Time 2, 200×228 color). `"targetPlatforms": ["emery"]`
|
||||
- **Toolchain: Core Devices PebbleOS SDK (Apache-2.0)**, local builds: `pebble build --emery`
|
||||
- **Docs live at developer.repebble.com** — the project verified against these
|
||||
- Custom fonts: bundled OFL TTFs at compile time (system fonts proprietary)
|
||||
- **License for our code: Apache-2.0**
|
||||
- **Server: TypeScript, zero dependencies**, pure `node:http` + stdlib. TLS
|
||||
terminates at a reverse proxy (documented deployment pattern). State is
|
||||
inspectable JSON files, atomic write-temp-rename. No database.
|
||||
- Repo topology, donations: unchanged from v1 (Gitea master, GitHub mirror at
|
||||
v0.1.0; Liberapay/Ko-fi + FUNDING.yml)
|
||||
|
||||
## 4. Architecture — three artifacts, one protocol
|
||||
|
||||
### 4.1 Topology
|
||||
|
||||
```
|
||||
┌──────────────┐ PUT state ┌─────────────┐ GET packet ┌──────────────┐
|
||||
│ GAME WATCHAPP │ ───────────▶ │ SERVER │ ◀──────────── │ WATCHFACE │
|
||||
│ (truth) │ (JS, when │ (mirror, │ (JS, throttled│ (display only)│
|
||||
│ │ running) │ never │ poll) │ │
|
||||
└──────────────┘ │ authority) │ └──────────────┘
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
- **Game watchapp** owns ALL game truth in its per-app persist storage.
|
||||
Full buttons + TouchService. Entered via Quick Launch (can be a single
|
||||
tap on current hardware). Presence via App Glance line and timeline pins.
|
||||
- **Watchface** is pure display: no game logic, no input handling for game
|
||||
purposes, fed by a cached server packet. End-timestamps are fixed at
|
||||
scheduling, so the countdown is computed locally each minute from the
|
||||
cached packet — zero radio needed to stay fresh.
|
||||
- **Server** mirrors the small "face packet" (task name, end timestamp,
|
||||
queue fill, sync_version). Game watch = source of truth, so last-write-
|
||||
wins is correct, not a bug. Token in auth header, never query string.
|
||||
|
||||
### 4.2 Server (self-hostable by design)
|
||||
|
||||
- Endpoints: `GET /v1/state/:player_id`, `PUT /v1/state/:player_id`.
|
||||
Token auth, body-size cap (~2 KB), 401 on bad token, silent throttle.
|
||||
- `sync_version` in every packet, forward/backward tolerant — strangers run
|
||||
servers, coordinated upgrades are impossible.
|
||||
- Contract lives in-repo as `/docs/PROTOCOL.md` (public spec for self-hosters).
|
||||
- Server-down semantics: watchface renders cached data or hides the strip;
|
||||
game is fully playable. **Fail-dead-silent, always.**
|
||||
- Self-hosting = the privacy pitch for this audience. Developer instance is
|
||||
default; donor-funded upkeep.
|
||||
|
||||
### 4.3 Wake cycle (game app only)
|
||||
|
||||
- **No worker, no background process — ever.** Game logic is dormant until
|
||||
invited (user opens app) or a user-enabled semantic wakeup fires.
|
||||
- Wakeups launch the app to the FOREGROUND (visible blip ~1–2 s), it computes,
|
||||
updates App Glance + pushes packet/pins via its JS, and exits cleanly
|
||||
(AppExitReason). No invisible execution exists on PebbleOS.
|
||||
- **Semantic events only** — whole-task completion, milestones worth pinning;
|
||||
typically a handful per day. Never per-chunk. Never per-minute. The
|
||||
API *permits* waking every minute via rebooking; the design forbids it
|
||||
(violates Laws 1 and 3).
|
||||
- **Rolling single wakeup**: exactly one pending event (next semantic event),
|
||||
rebooked each cycle. Handle `E_OUT_OF_RESOURCES` and collision retry
|
||||
gracefully (Muninn pattern). Max 8 pending / 1-minute spacing per docs.
|
||||
- Wakeups are an *optimization over* lazy check-in, never a dependency.
|
||||
Must tolerate "no wakeups ever fired."
|
||||
- Default OFF or "pins only"; user opt-in. Missed-sample tolerance required.
|
||||
|
||||
### 4.4 Platform constraint ledger (verified — do not re-litigate)
|
||||
|
||||
1. Watchfaces receive NO button or touch input. UP/DOWN/BACK/SELECT are
|
||||
OS-owned on the face (timeline/health/menu); watchfaces cannot push
|
||||
interactive secondary screens into usability. Double-tap-accel entry was
|
||||
considered and rejected as a user nightmare.
|
||||
2. Persist storage is per-app sandboxed. No app↔app sharing on watch, in the
|
||||
Pebble phone app, or anywhere else. The only sanctioned cross-binary
|
||||
bridges: timeline pins (with openWatchApp launch args), AppMessage
|
||||
(phone↔own binary only).
|
||||
3. Timeline is write-only/push-only to apps; watchfaces cannot read pins.
|
||||
4. Quick Launch assigns apps (and OS functions) only — not watchfaces.
|
||||
5. PebbleKit JS lifecycle is tied to its watch binary: spawns at launch,
|
||||
killed at exit. No persistent phone-side service via the Pebble app.
|
||||
6. Wakeup ≠ Worker. (Muninn demonstrates both coexisting; we use neither
|
||||
worker nor any background process.)
|
||||
7. AppExitReason (SDK 4.0+) lets a wakeup-launched app exit back to the
|
||||
watchface cleanly.
|
||||
|
||||
### 4.5 Input model (final)
|
||||
|
||||
- **Watchface**: no input. All interaction is customization via the phone
|
||||
settings editor (drag-drop on a virtual 200×228 screen, inspector,
|
||||
restore-defaults). Touch-to-wake backlight is a user system setting we
|
||||
never fight.
|
||||
- **Game app**: standard buttons; TouchService edge zones (upper-right/mid/
|
||||
lower-right = UP/SELECT/DOWN analogues) plus the system gesture bridge on
|
||||
MenuLayer/ScrollLayer where suitable; swipe-dismiss = BACK. Central input
|
||||
dispatch; every screen declares its actions once.
|
||||
- **Launch**: Quick Launch binds a button to the game app (single tap on
|
||||
current hardware). `launch_reason()` detects QUICK_LAUNCH; timeline pin
|
||||
taps deep-link via `launch_get_args()`.
|
||||
|
||||
## 5. Watchface Widgets (all toggleable, movable, font/color stylable)
|
||||
|
||||
Ten widgets: Time, Date, Watch Battery, Phone Battery, Steps, Distance,
|
||||
Weather composite (temp + hi/lo, sub-elements hideable), Weather Advisory
|
||||
(omens), Sleep Time, Current Task strip (game).
|
||||
|
||||
- Update frequencies user-configurable (health 1/5/15/30/60 min; weather
|
||||
15/30/60 min) — battery lever. Clock ticks per-minute only. Redraw on
|
||||
change only (e-paper).
|
||||
- Metric/imperial toggle; conversion at render time only.
|
||||
- **Current Task strip**: renders from cached server packet; countdown
|
||||
computed locally from end timestamp per-minute; staleness is detectable
|
||||
and shown honestly ("Return for your reward"). With no packet: hidden or
|
||||
"No hero yet".
|
||||
- **Watch Battery**: modes `percent | days | both` (default `both`).
|
||||
Days-remaining computed ON-WATCH (Muninn-inspired mechanics, own code):
|
||||
battery samples piggyback on wake-ups the face already performs;
|
||||
moving-average drain rate; charge events recalibrate; honest 3-sample
|
||||
cold start (percentage-only until trend exists). Gradient moss→brass→
|
||||
oxblood applies to both display modes. **Muninn web ingestion rejected**
|
||||
(unstable third-party surface; more code + radio than the math).
|
||||
- Weather/phone-battery arrive via face's own pkjs; JS lifecycle = face
|
||||
foreground (poll on JS wake, not fixed timers). Cache last-good in persist.
|
||||
|
||||
## 6.–9. Game Mechanics, Settlement, Theme
|
||||
|
||||
**Carried over verbatim from v1 — FINAL, locked.** Idle pillar (15-min
|
||||
chunks, 36-chunk budget, lazy resolution, deterministic seeds, items-as-XP,
|
||||
skills-only), combat pillar (barrows, gear-defined builds, agility-as-armor,
|
||||
blackout/permadeath), settlement chain, Celtic theme. Unchanged.
|
||||
|
||||
## 10. Multiplayer & Server Evolution (deferred)
|
||||
|
||||
Solo-first. The bridge server's `player_id`+token becomes the account seed.
|
||||
Phased: trade codes → shared-storage rivals → async competition /
|
||||
leaderboards — all served by the same self-hostable TS server, which is
|
||||
also the donation-sustainability story. No P2P on Pebble (established).
|
||||
|
||||
## 11. Design Laws (north stars)
|
||||
|
||||
1. **Casual glance game — benefit, not chore.** Nothing demands a session;
|
||||
no interruptions; the watchface never buzzes for game reasons.
|
||||
2. **Game logic is dormant until invited.** Math only at check-in / enqueue /
|
||||
cancel / collect / opt-in semantic wakeups. **No background process.**
|
||||
3. Fully freeform face; "restore default" escape hatch.
|
||||
4. **Never load-bearing externals.** Server down, Muninn gone, phone away —
|
||||
the game plays, the face degrades gracefully and silently.
|
||||
5. Verified-platform-only features: if the docs don't permit it, we don't
|
||||
ship it (see §4.4 ledger).
|
||||
|
||||
## 12. Status & Next Steps
|
||||
|
||||
**Done:** full design spec; architecture verified against current SDK docs;
|
||||
server language (zero-dep TS) and self-hosting model locked; battery-days
|
||||
decision (self-compute) locked.
|
||||
|
||||
**Build order:**
|
||||
1. **Watchface skeleton** — `main.c`: widget table (10 types), renderer,
|
||||
layout persist (own key, own version byte), placeholder task strip,
|
||||
clock/date/battery(percent) live, stubs for the rest
|
||||
2. **`/docs/PROTOCOL.md`** — packet/API contract (face-packet schema,
|
||||
sync_version semantics, failure ladder)
|
||||
3. **`server.ts`** — zero-dep TS mirror; curl-testable before any client
|
||||
4. **`widgets.c` + font table** — health/weather wiring; battery days-
|
||||
remaining estimator
|
||||
5. **Settings editor** (phase 2, drag-drop virtual face)
|
||||
6. **Game watchapp** — `game.c` in full (chunk resolver, enqueue validator,
|
||||
XP routing, prorating, persist version byte); Play Mode menus; wake
|
||||
cycle; App Glance + pins
|
||||
7. **Content tables** — TASKS/RECIPES/ITEMS (xp_value), founding chain,
|
||||
MONSTERS[]
|
||||
8. PDC sprites, theme skinning, polish
|
||||
|
||||
**First build verification checklist:** `pebble build --emery` + emulator;
|
||||
TouchService delivery + subscription lifecycle; AppMessage plumbing against
|
||||
stub server endpoint.
|
||||
89
docs/PROTOCOL.md
Normal file
89
docs/PROTOCOL.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Nine Hazes — Watch Client for OSRIC MUD Idle System
|
||||
## Consolidated record v3.0 (post-pivot, 2026-09-12)
|
||||
|
||||
### 1. Concept
|
||||
Pebble Time 2 (emery) watchface + companion watchapp acting as remote monitor
|
||||
and manager for the Nine Hazes OSRIC MUD idle task system. NOT a standalone
|
||||
game — all game logic lives on the MUD server. The watch displays server
|
||||
snapshots and submits intents (add/cancel/claim); the server validates and
|
||||
executes. Watch behaves like a normal watchface except during active sync.
|
||||
|
||||
### 2. Critical architecture decision — "face gets its own sync"
|
||||
Pebble persist storage is per-app sandboxed with NO app↔app communication
|
||||
(verified against platform docs). Therefore the watchface and watchapp each
|
||||
own an independent pkjs sync to the same Task API. No watch-side data sharing
|
||||
exists or is attempted. Shared truth = server.
|
||||
|
||||
### 3. Protocol
|
||||
Wire contract: "Nine Hazes — Idle Task Protocol v1.0" (separate document,
|
||||
authored server-side; authoritative). Summary:
|
||||
- HTTPS + Bearer token (scoped queue:read queue:write claim; token on phone
|
||||
localStorage only, NEVER on watch)
|
||||
- GET /v1/dictionary (versioned, 304-style caching via ?v=)
|
||||
- GET /v1/queue — full sync in one call (tasks[], used/budget, pend, bound,
|
||||
status, tok.exp)
|
||||
- POST /v1/queue/tasks (reqId idempotency), DELETE /v1/queue/tasks/:tid,
|
||||
POST /v1/claim, POST /v1/token/refresh
|
||||
- Device protocol: GameSnapshot packed struct (~28 bytes) at persist key 0x30;
|
||||
dictionary blob at 0x31 (watchapp use)
|
||||
- Clock correction: all countdowns from bound + clock_off, never raw watch clock
|
||||
- Failure ladder: unreachable/malformed → silent, render cached snapshot
|
||||
|
||||
### 4. Artifacts & ownership
|
||||
| Artifact | Owner | Status |
|
||||
|---|---|---|
|
||||
| Watchface (C, canvas renderer, widget table) | watch team | RENDERING BASELINE COMPLETE: custom-canvas data-driven renderer, 10 widget types + 2 spare slots, per-widget layout persist (key 100), parchment/ink palette, task strip rendering, server-down icon, dev.sh tooling. Committed to Gitea. |
|
||||
| Watchapp (queue manager UI) | watch team | deferred (build order item) |
|
||||
| pkjs ×2 (face sync, watchapp sync) | watch team | face pkjs fetches /v1/queue → AppMessage snapshot |
|
||||
| Bridge/state server | OBSOLETE — deleted. Superseded by MUD gateway + placeholder mock (below) |
|
||||
| Placeholder mock server (idle protocol §10) | watch team | zero-dependency node:http implementation (NOT Express — locked project law: TypeScript, node stdlib only) |
|
||||
|
||||
### 5. Widget set (unchanged)
|
||||
Time, date, watch battery (+days estimator, key 102), phone battery, steps,
|
||||
distance, weather composite (NWS default / Open-Meteo fallback — geographic
|
||||
auto-switch), advisory omens, sleep, Current Task Strip (2-line: line 1 =
|
||||
countdown to task completion + used/36 + task count, line 2 = current task
|
||||
name (+n) pending indicator; offline "!" icon on failed check). All
|
||||
toggleable/movable/font+color stylable. Spare slots reserved (incl. game
|
||||
preview / companion preview, deferred).
|
||||
|
||||
### 6. Design laws (north stars)
|
||||
1. Casual glance — no game-triggered interrupts, ever.
|
||||
2. Game logic dormant until invited; watch computes only countdown strings.
|
||||
3. Fully free face — no protected widgets; "restore default" escape hatch.
|
||||
4. Face shows minimal: countdown + queue line only.
|
||||
5. Battery-first: zero network in ambient use; one sync per interaction.
|
||||
6. Fail-dead-silent: server down → cached data or hidden strip; no errors.
|
||||
7. Zero-dependency TypeScript server code; no workers/background radio ever;
|
||||
wakeups = semantic events only, rolling single wakeup (Muninn pattern).
|
||||
|
||||
### 7. Platform/environment
|
||||
emery only ("targetPlatforms": ["emery"]) · Core Devices PebbleOS SDK ·
|
||||
system fonts first (carved from Apache release), OFL TTFs bundled later ·
|
||||
Apache-2.0 · Gitea master (auth-gated), GitHub public mirror at v0.1.0 ·
|
||||
Liberapay/Ko-fi FUNDING.yml.
|
||||
|
||||
### 8. Reference projects (ideas only, clean-room; unlicensed repos = read-for-protocol
|
||||
never copy code; Remix-on-CloudPebble ≠ redistribution grant)
|
||||
IdleFantasy (queue model), Doors of Doom (AppMessage), Pixel8 (chunked
|
||||
transfer), Muninn (wakeup/battery sampling patterns), Tamagotchi-API/TamaTime
|
||||
(companion-preview idea, DEFERRED — feature not focus).
|
||||
|
||||
### 9. Build order (current)
|
||||
1. ✔ Watchface rendering baseline (committed)
|
||||
2. ✔ pkjs fetch + AppMessage + cached-packet strip + offline icon (v1 protocol)
|
||||
3. → NOW: idle-protocol adaptation — snapshot struct, mock server, clock-offset
|
||||
countdown (this revision)
|
||||
4. Widget wiring: health (steps/distance/sleep), weather (NWS/Open-Meteo)
|
||||
5. Watchapp: queue UI (add via presets/segment picker, cancel, claim),
|
||||
wakeup at segment boundary, launch_args deep links, App Glance/timeline pins
|
||||
6. Settings phase 2: hosted config page (drag-drop layout, presets, server URL/
|
||||
token entry)
|
||||
7. Polish: PDC sprites/icons (pickaxe, fist, coin, moon), theme, vibration
|
||||
|
||||
### 10. Open items to relay to server team
|
||||
- bound/t/sync timestamps in epoch-ms exceed AppMessage uint32 → watchface
|
||||
uses epoch-SECONDS for wire transfer (recommend protocol clarify units)
|
||||
- Face receives resolved task NAME from pkjs (v1); dictionary blob 0x31 is
|
||||
watchapp-scoped, chunked transfer deferred
|
||||
- Player identity: X-Pebble-Id informational; real auth = MUD token
|
||||
Reference in New Issue
Block a user