Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f31f8050bf | ||
|
|
a3b261b100 | ||
|
|
8d891d037b | ||
|
|
b19717141e | ||
|
|
7f48cde7d9 | ||
|
|
aaa292ac49 |
@@ -0,0 +1,75 @@
|
||||
# AGENTS.md — vela-snake
|
||||
|
||||
Guidance for AI agents working on this repo. Humans can read it too.
|
||||
|
||||
## What this is
|
||||
|
||||
Textured Snake for the Xiaomi Watch S4 (round 466px screen, Vela quickapp).
|
||||
Package `com.ilias.snake`. Source of truth for releases:
|
||||
https://gitea.oimcloud.myaddr.tools/oiwmiodev/vela-snake (Big Intelligence group).
|
||||
Upstream codex-bot/vela-snake is read-only for this account (no write permission).
|
||||
|
||||
## Hard rules
|
||||
|
||||
- Never commit `sign/*.pem` (gitignored on purpose). Builds currently use the
|
||||
toolkit's default debug keys; if real keys are added, keep them out of git.
|
||||
- Never edit files under `node_modules/`. The toolkit is locked at aiot-toolkit
|
||||
2.0.5; do not upgrade it casually (release uses plain JS, not JSC/protobuf).
|
||||
- Tests must pass before any commit: `npm test` (pure Node, no watch needed).
|
||||
- Bump `versionName`/`versionCode` in `src/manifest.json` for any release.
|
||||
Hotfixes: `0.9-Hotfix.1` style, versionCode still increments (integer only).
|
||||
- No em dashes in docs, commits or comments.
|
||||
|
||||
## Crown control (the fragile part)
|
||||
|
||||
The Vela firmware does NOT expose crown events to quickapps (verified by
|
||||
firmware string analysis, see docs/CROWN.md). Crown rotation arrives only as
|
||||
scroll deltas on the focused scroll container. The page is a full-screen
|
||||
vertical `scroll` with the dial centered; `onscroll` converts accumulated
|
||||
deltas into queued 90-degree turns (`CROWN_STEP_PX`, 40px per turn) and snaps
|
||||
the scroller back to center (`scrollTo({top: 233})`).
|
||||
|
||||
Known failure mode (issue oiwmiodev/vela-snake#1): the game can still scroll
|
||||
away, likely because momentum scrolling outlives the snap-back or onShow's
|
||||
scrollTo runs before layout. Any fix must keep: swipe steering working, the
|
||||
dial visually pinned, reversals impossible. If scroll-delta extraction proves
|
||||
unreliable on hardware, the fallback is accepting codex-bot issue #2's
|
||||
conclusion (swipe-only input) and removing the scroll wrapper.
|
||||
|
||||
Crown work happens on the `crown-experimental` branch; `main` tracks the
|
||||
released state.
|
||||
|
||||
## Layout invariants
|
||||
|
||||
- Dial/board: 466px screen, board at 49px offset, 368px circle, 32px cells,
|
||||
11x11 grid, 89 legal cells (circular mask, see src/common/snake.js ARENA).
|
||||
- Scroll content height = 932px (233px padding each side), center offset 233.
|
||||
- Overlays fit the watch chord; no legal cell is ever covered by HUD/footer.
|
||||
- No CSS transforms anywhere (tests assert this); tiles are positioned with
|
||||
left/top only.
|
||||
|
||||
## Testing
|
||||
|
||||
`npm test` runs snake logic, piece classification, page wiring (including the
|
||||
crown scroll path), upgrade regressions, numeric layout, randomized property
|
||||
tests and art checks. When fixing a bug, add a failing test first, then fix.
|
||||
Node tests are not device tests: behavior on the actual watch is the only
|
||||
final proof, and whoever has the watch does that testing.
|
||||
|
||||
## Release checklist
|
||||
|
||||
1. All tests green.
|
||||
2. `src/manifest.json` version bumped.
|
||||
3. `npm run release`, verify dist/com.ilias.snake.release.<ver>.rpk exists.
|
||||
4. Commit + push to origin (oiwmiodev/vela-snake).
|
||||
5. Create the release with `tea releases create --repo oiwmiodev/vela-snake
|
||||
--tag v<ver>` and attach the rpk with `tea releases assets create`.
|
||||
Download the asset back and byte-compare before claiming success.
|
||||
6. Release notes: real changelog, never "see commit history".
|
||||
|
||||
## Git
|
||||
|
||||
Git identity is not globally configured; commits must set the repo persona
|
||||
(the "big pc republic" identity used across this Gitea, see prior commits)
|
||||
via `git -c user.name=... -c user.email=...` or local config. Pushes go to
|
||||
origin (the org repo). Auth uses the `oimcloud` tea login.
|
||||
@@ -5,9 +5,21 @@ screen. **32px tiles** (60% larger than 0.7) occupy 89 legal cells of an 11×11
|
||||
bounding grid; every tile corner stays inside the actual circle. The square
|
||||
board frame is gone. Directional sprite artwork and user icon are preserved. Package identity: `com.ilias.snake`.
|
||||
|
||||
**Version 0.8 (versionCode 8): locally tested and release-built; the new features
|
||||
still need an on-watch check.** Earlier versions rendered and played on the
|
||||
user's Watch S4 41mm. Node tests and numeric layout checks are not device tests.
|
||||
**Version 0.9 (versionCode 9): crown control experiment, unit-tested, needs an
|
||||
on-watch check.** See docs/CROWN.md for the firmware reasoning.
|
||||
|
||||
## Crown control (new in 0.9)
|
||||
|
||||
- The watch firmware exposes **no crown event to quickapps**; it routes crown
|
||||
rotation into the scrollable container as scroll deltas. The page is now a
|
||||
full-screen vertical scroll with the dial centered; `onscroll` fires for both
|
||||
crown rotation and finger scroll.
|
||||
- Scroll deltas accumulate; every **40 px of scroll = one 90° turn**,
|
||||
clockwise = clockwise turn. Reversals are structurally impossible because
|
||||
turns are relative 90° steps (`rotateDir`-style), never absolute directions.
|
||||
- The scroller is snapped back to center after every event, so the board never
|
||||
visibly moves, even though the scroll travel exists.
|
||||
- Swipes still work exactly as before; both inputs share the two-slot queue.
|
||||
|
||||
## Play
|
||||
|
||||
@@ -28,6 +40,10 @@ user's Watch S4 41mm. Node tests and numeric layout checks are not device tests.
|
||||
- Leaving the page pauses it and stops the timer. Returning requires an explicit
|
||||
resume; game-over never resumes automatically. The system back action remains
|
||||
untouched. No crown, rotary, side-button or back interception APIs are used.
|
||||
- **Crown to steer** (0.9): rotate the crown; every 40px of scroll is one
|
||||
90° turn, clockwise = clockwise. Crown and swipe feed the same queue; the
|
||||
board stays visually fixed while you turn. If your firmware gives the crown
|
||||
coarser steps than 40px, adjust `CROWN_STEP_PX` at the top of `index.ux`.
|
||||
|
||||
## Score, speed and persistence
|
||||
|
||||
@@ -74,9 +90,11 @@ npm run release
|
||||
regressions, the numeric layout suite, and deterministic randomized property tests. Upgrade coverage includes rapid
|
||||
swipes, lifecycle/timer races, storage failures and async ordering, mode changes,
|
||||
all four Wrap edges, seam textures, full-board victory and bounded food spawning.
|
||||
The page suite also drives the crown scroll path: one notch = one queued turn,
|
||||
scroller re-centered, queue capped at two, no reversal, turn consumed on tick.
|
||||
New behaviors were exercised through failing-then-passing test cycles.
|
||||
|
||||
Release output: `dist/com.ilias.snake.release.0.8.rpk`. The locked
|
||||
Release output: `dist/com.ilias.snake.release.0.9.rpk`. The locked
|
||||
`aiot-toolkit` 2.0.5 uses the existing `sign/private.pem` and `sign/certificate.pem`;
|
||||
do not regenerate or commit those keys. Release uses plain JS (not JSC/protobuf),
|
||||
matching the previously working toolchain. The dependency lock is unchanged;
|
||||
@@ -85,15 +103,15 @@ matching the previously working toolchain. The dependency lock is unchanged;
|
||||
|
||||
## Files
|
||||
|
||||
- `src/pages/index/index.ux`: round-screen UI, swipe queue, game/lifecycle state.
|
||||
- `src/pages/index/index.ux`: round-screen UI, swipe/crown queue, game/lifecycle state.
|
||||
- `src/common/snake.js`: movement, collision, Wrap piece classification, food.
|
||||
- `src/common/best.js`: asynchronous storage coordinator.
|
||||
- `src/common/tex/`: native 32px sprite artwork and circular board.
|
||||
- `docs/CROWN.md`: firmware-derived rationale for the crown scroll path.
|
||||
- `tests/{snake,pieces,page,upgrade,layout,properties,round,art,round-history}.cjs`: Node regression suites, including history-based circular seam orientation checks.
|
||||
|
||||
Install the signed RPK through AstroBox. Source/releases live at
|
||||
https://gitea.oimcloud.myaddr.tools/codex-bot/vela-snake — publication is a
|
||||
separate step after review; a local build is not evidence of a published release.
|
||||
https://gitea.oimcloud.myaddr.tools/oiwmiodev/vela-snake (Big Intelligence group).
|
||||
|
||||
## Earlier versions
|
||||
|
||||
@@ -101,3 +119,4 @@ separate step after review; a local build is not evidence of a published release
|
||||
- 0.6: directional textured tiles and preserved user-supplied icon.
|
||||
- 0.7: queued swipes, live/persisted records, capped acceleration, pause/resume,
|
||||
lifecycle safety, retry/win panels and selectable Classic/Wrap.
|
||||
- 0.8: circular arena (no square frame), 32px tiles, 89 legal cells.
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Crown control experiment (v0.9)
|
||||
|
||||
Firmware study of the O63 OTA (`upd_miwear.watch.o63.zip`) found how the crown
|
||||
reaches quickapps: there is **no crown/rotary JS event** (no `onCrown`,
|
||||
`onRotary`, wheel keyevents in `vela_quickapp.bin`), but the native layer
|
||||
(`vela_ap.bin`, `inputmanager/lv_touchpad_wheel_interface.c` + the
|
||||
`crown_scroll` input tag in `lvx_inputmanager.c`) routes crown rotation into
|
||||
**scroll deltas of the focused scrollable container**.
|
||||
|
||||
So Snake listens to the crown indirectly:
|
||||
|
||||
- the page is wrapped in a full-screen vertical `scroll` component with the
|
||||
dial centered inside it,
|
||||
- `onscroll` fires for both crown rotation and finger scroll,
|
||||
- the page converts each scroll delta into a **queued 90° turn**
|
||||
(`snake.rotateDir`, already in the codebase): positive delta =
|
||||
clockwise, negative = counterclockwise, `CROWN_STEP_PX` px of scroll per
|
||||
turn,
|
||||
- because turns are 90° relative steps, the crown always turns the snake
|
||||
left/right regardless of where the head is pointing, and reversals are
|
||||
structurally impossible (rotateDir never emits a 180° flip),
|
||||
- the scroll position is actively re-centered (`scrollTo({ top: center })`)
|
||||
after each event so the board never visually moves; only the idle-swipe
|
||||
fallback path (when no scroll events arrive, e.g. some firmwares coalesce
|
||||
crown scroll with the swipe handler) uses `onswipe` as before,
|
||||
- swipes still work exactly as in 0.8; both input paths feed the same
|
||||
two-slot turn queue.
|
||||
|
||||
Open questions that need the physical watch:
|
||||
|
||||
- does the Crown S4 emit scroll deltas with sign, and at what px granularity?
|
||||
- does `onscroll` fire for crown motion at all on this firmware, or only for
|
||||
touch scroll? If not, the `CROWN_STEP_PX` accumulator still catches the
|
||||
swipe-path fallback but true crown control would need native access.
|
||||
+2
-2
@@ -2,8 +2,8 @@
|
||||
"package": "com.ilias.snake",
|
||||
"name": "SNAKE",
|
||||
"icon": "/common/icon.png",
|
||||
"versionName": "0.8",
|
||||
"versionCode": 8,
|
||||
"versionName": "0.9",
|
||||
"versionCode": 9,
|
||||
"minAPILevel": 1,
|
||||
"deviceTypeList": [
|
||||
"watch"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="page" onswipe="onSwipe">
|
||||
<stack class="dial">
|
||||
<div class="page">
|
||||
<scroll id="scroller" class="page" scroll-y="true" direction="vertical" onscroll="onCrownScroll">
|
||||
<stack class="dial">
|
||||
<image class="board" src="/common/tex/board.png" onclick="onTap"></image>
|
||||
<image class="food" src="/common/tex/apple.png" show="{{ hasFood }}" onclick="onTap" style="left: {{ food.left }}; top: {{ food.top }};"></image>
|
||||
<image class="seg" src="{{ seg.src }}" for="{{ seg in snake }}" onclick="onTap" style="left: {{ seg.left }}; top: {{ seg.top }};"></image>
|
||||
@@ -32,7 +33,8 @@
|
||||
<text class="modeButton" onclick="selectMode('wrap')">WRAP</text>
|
||||
</div>
|
||||
</div>
|
||||
</stack>
|
||||
</stack>
|
||||
</scroll>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@@ -43,6 +45,17 @@ import best from '../../common/best.js'
|
||||
const CELL = snake.ARENA.cell
|
||||
const GRID = snake.ARENA
|
||||
const OFFSET = snake.ARENA.offset
|
||||
// Crown rotation arrives as scroll deltas on the focused scroll container
|
||||
// (see docs/CROWN.md). This many px of accumulated scroll = one 90° turn.
|
||||
const CROWN_STEP_PX = 40
|
||||
const CW_ORDER = [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 0, y: -1 }]
|
||||
function rotate90(d, clockwise) {
|
||||
let i = 0
|
||||
for (let k = 0; k < 4; k++) {
|
||||
if (CW_ORDER[k].x === d.x && CW_ORDER[k].y === d.y) { i = k; break }
|
||||
}
|
||||
return clockwise ? CW_ORDER[(i + 1) % 4] : CW_ORDER[(i + 3) % 4]
|
||||
}
|
||||
|
||||
export default {
|
||||
private: {
|
||||
@@ -68,6 +81,8 @@ export default {
|
||||
onShow() {
|
||||
if (this.destroyed) return
|
||||
this.visible = true
|
||||
this.crownAnchor = 233
|
||||
try { this.$element('scroller').scrollTo({ top: 233 }) } catch (err) {}
|
||||
this.records.sync()
|
||||
this.startTimer()
|
||||
},
|
||||
@@ -157,6 +172,31 @@ export default {
|
||||
if (snake.turn(last, v) === last) return
|
||||
this.queue.push(v)
|
||||
},
|
||||
// Crown rotation: firmware routes it into this container's scroll offset
|
||||
// (docs/CROWN.md). Accumulate deltas, one 90° turn per CROWN_STEP_PX, then
|
||||
// re-center so the board never visibly moves.
|
||||
onCrownScroll(e) {
|
||||
if (this.destroyed) return
|
||||
const top = e && typeof e.scrollTop === 'number' ? e.scrollTop : 0
|
||||
const center = 233
|
||||
const d = top - (this.crownAnchor == null ? center : this.crownAnchor)
|
||||
this.crownAnchor = top
|
||||
if (d === 0) return
|
||||
// keep the dial pinned: snap back to center immediately
|
||||
try { this.$element('scroller').scrollTo({ top: center }) } catch (err) {}
|
||||
this.crownAnchor = center
|
||||
if (this.over || this.paused || !this.visible) return
|
||||
this.crownAcc = (this.crownAcc || 0) + d
|
||||
while (Math.abs(this.crownAcc) >= CROWN_STEP_PX) {
|
||||
const clockwise = this.crownAcc > 0
|
||||
this.crownAcc += clockwise ? -CROWN_STEP_PX : CROWN_STEP_PX
|
||||
const base = this.queue.length ? this.queue[this.queue.length - 1] : this.dir
|
||||
if (this.queue.length >= 2) return
|
||||
const v = rotate90(base, clockwise)
|
||||
if (v.x === base.x && v.y === base.y) return
|
||||
this.queue.push(v)
|
||||
}
|
||||
},
|
||||
onTap() {
|
||||
if (this.destroyed || !this.visible || this.over) return
|
||||
this.paused = !this.paused
|
||||
@@ -190,6 +230,10 @@ export default {
|
||||
background-color: #000000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* scrollable travel: dial centered in 932px content, 233px of crown travel
|
||||
each way; onReady + onscroll snap back to the center offset (233) */
|
||||
padding-top: 233px;
|
||||
padding-bottom: 233px;
|
||||
}
|
||||
.dial {
|
||||
width: 466px; height: 466px;
|
||||
|
||||
+20
-2
@@ -64,8 +64,26 @@ assert.match(ux, /<image class="board" src="\/common\/tex\/board\.png"[^>]*>/);
|
||||
assert.match(ux, /<image class="seg" src="\{\{ seg\.src \}\}"/);
|
||||
assert.match(ux, /for="\{\{ seg in snake \}\}"/);
|
||||
assert.match(ux, /show="\{\{ over \}\}"/);
|
||||
assert.match(ux, /onswipe="onSwipe"/);
|
||||
assert.match(ux, /onscroll="onCrownScroll"/, 'scroll container wired for crown deltas');
|
||||
assert.match(ux, /<scroll id="scroller"/, 'scroller element present for re-centering');
|
||||
assert.doesNotMatch(ux, /crownrotationchanged/i);
|
||||
assert.doesNotMatch(ux, /transform\s*:/);
|
||||
|
||||
console.log('PAGE WIRING PASS (textures, movement, turn, reversal, game over, restart, directives)');
|
||||
// crown path: scroll deltas become queued 90° turns, board stays centered
|
||||
c = mk();
|
||||
c.visible = true;
|
||||
c.crownAnchor = 233;
|
||||
c.onCrownScroll({ scrollTop: 233 + 40 }); // one notch "down"
|
||||
c.onCrownScroll({ scrollTop: 233 });
|
||||
assert.equal(c.queue.length, 1, 'one turn queued after one crown notch');
|
||||
assert.equal(c.crownAnchor, 233, 'scroller re-centered');
|
||||
assert.equal(JSON.stringify(c.queue[0]), JSON.stringify({ x: 0, y: 1 }), 'right-facing snake turned clockwise = down');
|
||||
c.onCrownScroll({ scrollTop: 233 - 120 }); // three notches back
|
||||
assert.equal(c.queue.length, 2, 'queue capped at 2');
|
||||
assert.equal(c.dir.x, 1, 'no reversal emitted, x unchanged');
|
||||
assert.equal(c.dir.y, 0, 'no reversal emitted, y unchanged');
|
||||
c.onCrownScroll({ scrollTop: 233 });
|
||||
c.tick();
|
||||
assert.equal(JSON.stringify(c.dir), JSON.stringify({ x: 0, y: 1 }), 'first queued turn consumed');
|
||||
|
||||
console.log('PAGE WIRING PASS (textures, movement, turn, reversal, game over, restart, directives, crown scroll)');
|
||||
|
||||
Reference in New Issue
Block a user