diff --git a/README.md b/README.md index 0575701..bbe5c8c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # SNAKE — Xiaomi Watch S4 -Textured Snake on a 16×16 grid: 20px tiles, 320px board, centered on a round -466px screen. Existing PNG board, directional snake pieces, apple and icon are -preserved. Package identity: `com.ilias.snake`. +Textured Snake on a **368px circular arena**, centered on the round 466px +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.7 (versionCode 7): locally tested and release-built; the new features +**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. @@ -15,14 +16,15 @@ user's Watch S4 41mm. Node tests and numeric layout checks are not device tests. queued direction. Excess inputs are ignored, not allowed to replace a turn. - **Tap the board, snake or apple** to pause. Tap **RESUME** (or an exposed part of the board) to resume. Pausing clears queued turns. -- In the pause or game-over panel, select **CLASSIC** (walls end the run) or - **WRAP** (cross an edge to appear on the opposite edge). Changing mode starts +- In the pause or game-over panel, select **CLASSIC** (the circular boundary ends the run) or + **WRAP** (cross the circular boundary to appear at the opposite legal end + of the **same row or column**). Changing mode starts a fresh, paused game; tap RESUME to begin. Selecting the current mode does not reset the run. The current mode is shown below the board. - Both modes end on self-collision. Moving into the vacating tail is legal. - **PLAY AGAIN** is a large 216×56px retry target. Retry keeps the selected mode, resets score/speed/queued turns, and starts playing. Filling the board displays - **YOU WIN** and hides the apple safely. + **YOU WIN** at 89 occupied cells (86 points) and hides the apple safely. - 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. @@ -41,18 +43,24 @@ paused, finished or destroyed. Old timer callbacks are invalidated. Storage reads merge with the in-memory maximum; writes wait for the initial read and are serialized/coalesced so slow callbacks cannot overwrite a newer record. The coordinator survives page recreation within the module lifetime. -Invalid saved values are ignored. Errors/exceptions do not interrupt gameplay: +Old 0.7 records through 253 remain valid and are never reduced to the new +86-point maximum. Invalid saved values are ignored. Errors/exceptions do not interrupt gameplay: failed reads/writes retry on later apples or page-show activity, without a retry loop. A failed/uncompleted write or immediate app termination may lose the unsaved record. Storage persistence still needs verification on the watch. ## Lightweight rendering -Directional PNG tiles use only `left`/`top` positioning, no transforms or new -art assets. Wrapped neighbors are normalized before classifying seam textures. -Food placement takes at most 256 cell checks, including a nearly full board. -The HUD, footer, board and overlay fit the circular chord numerically with -at least an 8px total horizontal safety allowance. +Directional PNG tiles use only `left`/`top` positioning, no transforms. Existing +20px sprite art is nearest-neighbor enlarged to native 32px assets (crisp pixel +art, not new high-resolution detail). The board is a newly generated circular +PNG with transparent corners and a subtle legal-cell checker pattern. Wrapped +texture connections handle each varying row/column length. All rows/columns +have at least three cells, avoiding ambiguous one-/two-cell seams. +Food scans only 89 legal cells, including a nearly full board. HUD/footer +rectangles never overlap legal play tiles; text/overlay fit the watch chord. +`tests/preview-0.8-static.png` is a static composition of actual geometry/art, +with a legal-cell overlay—not a runtime screenshot. ## Build and test @@ -62,13 +70,13 @@ npm test npm run release ``` -`npm test` runs the original core, piece and page tests, 13 incremental upgrade +`npm test` runs the original core, piece and page tests, 14 upgrade 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. New behaviors were exercised through failing-then-passing test cycles. -Release output: `dist/com.ilias.snake.release.0.7.rpk`. The locked +Release output: `dist/com.ilias.snake.release.0.8.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; @@ -80,8 +88,8 @@ matching the previously working toolchain. The dependency lock is unchanged; - `src/pages/index/index.ux`: round-screen UI, swipe queue, game/lifecycle state. - `src/common/snake.js`: movement, collision, Wrap piece classification, food. - `src/common/best.js`: asynchronous storage coordinator. -- `src/common/tex/`: preserved textured artwork. -- `tests/{snake,pieces,page,upgrade,layout,properties}.cjs`: Node regression suites. +- `src/common/tex/`: native 32px sprite artwork and circular board. +- `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 diff --git a/package.json b/package.json index 2badb6b..f598d6d 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "description": "Textured Snake for the round Xiaomi Watch S4 screen", "scripts": { - "test": "node tests/snake.cjs && node tests/pieces.cjs && node tests/page.cjs && node --test tests/upgrade.cjs && node tests/layout.cjs && node tests/properties.cjs", + "test": "node tests/snake.cjs && node tests/pieces.cjs && node tests/page.cjs && node --test tests/upgrade.cjs && node tests/layout.cjs && node tests/properties.cjs && node tests/round.cjs && node tests/art.cjs && node tests/round-history.cjs", "build": "aiot build", "release": "aiot release", "build:source-map": "aiot build --devtool=source-map", diff --git a/src/common/snake.js b/src/common/snake.js index e1d8144..06476c4 100644 --- a/src/common/snake.js +++ b/src/common/snake.js @@ -1,4 +1,18 @@ -// Snake core — pure functions, no Vela APIs. Exports: initialSnake, step, turn, spawnFood. +// Shared geometry: 32px tiles wholly inside the 368px circular arena. +const ARENA = { cell: 32, size: 11, offset: 57, radius: 184, cells: [], rows: [], columns: [] } +for (let y = 0; y < ARENA.size; y++) { + ARENA.rows[y] = [] + ARENA.columns[y] = [] +} +for (let y = 0; y < ARENA.size; y++) for (let x = 0; x < ARENA.size; x++) { + const dx = Math.abs((x + 0.5) * ARENA.cell - 176) + ARENA.cell / 2 + const dy = Math.abs((y + 0.5) * ARENA.cell - 176) + ARENA.cell / 2 + if (dx * dx + dy * dy <= ARENA.radius * ARENA.radius) { + ARENA.cells.push({ x, y }) + ARENA.rows[y].push(x) + ARENA.columns[x].push(y) + } +} function initialSnake() { return [ { x: 6, y: 7 }, @@ -12,13 +26,22 @@ function step(snake, dir, food, N, mode) { const head = snake[0] let nx = head.x + dir.x let ny = head.y + dir.y - if (mode === 'wrap') { + if (typeof N === 'object') { + const row = N.rows[ny] + if (!row || row.indexOf(nx) === -1) { + if (mode !== 'wrap') return { snake, ate: false, alive: false } + const span = dir.x ? N.rows[head.y] : N.columns[head.x] + const opposite = dir.x + dir.y > 0 ? span[0] : span[span.length - 1] + if (dir.x) nx = opposite + else ny = opposite + } + } else if (mode === 'wrap') { nx = (nx + N) % N ny = (ny + N) % N } else if (nx < 0 || ny < 0 || nx >= N || ny >= N) { return { snake, ate: false, alive: false } } - const ate = (nx === food.x && ny === food.y) + const ate = !!food && (nx === food.x && ny === food.y) // tail cell vacates unless we grew, so exclude it from self-collision check const body = ate ? snake : snake.slice(0, snake.length - 1) for (let i = 0; i < body.length; i++) { @@ -44,13 +67,22 @@ function renderPieces(snakeCells, dir, N) { const D = { '1,0': 'e', '-1,0': 'w', '0,-1': 'n', '0,1': 's' } const opp = { e: 'w', w: 'e', n: 's', s: 'n' } const delta = d => N && d === N - 1 ? -1 : N && d === 1 - N ? 1 : d + // Circular seams join the endpoints of this row/column, not the bounding grid. + const side = (s, neighbor) => { + let dx = neighbor.x - s.x, dy = neighbor.y - s.y + if (N && typeof N === 'object') { + if (Math.abs(dx) > 1) dx = dx > 0 ? -1 : 1 + if (Math.abs(dy) > 1) dy = dy > 0 ? -1 : 1 + } else { dx = delta(dx); dy = delta(dy) } + return D[dx + ',' + dy] + } return snakeCells.map((s, i) => { if (i === 0) return 'head_' + D[dir.x + ',' + dir.y] const prev = snakeCells[i - 1] const next = snakeCells[i + 1] - const from = D[delta(prev.x - s.x) + ',' + delta(prev.y - s.y)] // side toward head + const from = side(s, prev) // side toward head if (!next) return 'tail_' + opp[from] // tip points away from body - const to = D[delta(next.x - s.x) + ',' + delta(next.y - s.y)] // side toward tail tip + const to = side(s, next) // side toward tail tip if ((from === 'e' || from === 'w') && (to === 'e' || to === 'w')) return 'body_h' if ((from === 'n' || from === 's') && (to === 'n' || to === 's')) return 'body_v' // corner: name = the two sides it connects; pairs are alphabetically sorted @@ -75,6 +107,10 @@ function rotateDir(dir, clockwise) { // Random empty cell, avoiding the snake. function spawnFood(snake, N) { const occupied = new Set(snake.map(s => s.x + ',' + s.y)) + if (typeof N === 'object') { + const empty = N.cells.filter(c => !occupied.has(c.x + ',' + c.y)) + return empty.length ? empty[Math.floor(Math.random() * empty.length)] : null + } if (occupied.size >= N * N) return null // Select an empty-cell rank uniformly; at most N*N checks, even almost full. let rank = Math.floor(Math.random() * (N * N - occupied.size)) @@ -89,4 +125,4 @@ function spawnFood(snake, N) { return null } -module.exports = { initialSnake, step, turn, renderPieces, rotateDir, spawnFood } +module.exports = { ARENA, initialSnake, step, turn, renderPieces, rotateDir, spawnFood } diff --git a/src/common/tex/apple.png b/src/common/tex/apple.png index 556ee0e..3d5413f 100644 Binary files a/src/common/tex/apple.png and b/src/common/tex/apple.png differ diff --git a/src/common/tex/board.png b/src/common/tex/board.png index 3fc917a..6c161fc 100644 Binary files a/src/common/tex/board.png and b/src/common/tex/board.png differ diff --git a/src/common/tex/body.png b/src/common/tex/body.png index 186c627..0729f22 100644 Binary files a/src/common/tex/body.png and b/src/common/tex/body.png differ diff --git a/src/common/tex/body_h.png b/src/common/tex/body_h.png index 2e50ed1..73ece64 100644 Binary files a/src/common/tex/body_h.png and b/src/common/tex/body_h.png differ diff --git a/src/common/tex/body_v.png b/src/common/tex/body_v.png index f43235b..88c9379 100644 Binary files a/src/common/tex/body_v.png and b/src/common/tex/body_v.png differ diff --git a/src/common/tex/corner_en.png b/src/common/tex/corner_en.png index 3bb5503..9a03cd2 100644 Binary files a/src/common/tex/corner_en.png and b/src/common/tex/corner_en.png differ diff --git a/src/common/tex/corner_es.png b/src/common/tex/corner_es.png index bdccc5f..b80b8d4 100644 Binary files a/src/common/tex/corner_es.png and b/src/common/tex/corner_es.png differ diff --git a/src/common/tex/corner_nw.png b/src/common/tex/corner_nw.png index 9382c87..1e0422d 100644 Binary files a/src/common/tex/corner_nw.png and b/src/common/tex/corner_nw.png differ diff --git a/src/common/tex/corner_sw.png b/src/common/tex/corner_sw.png index 06020aa..17ab34c 100644 Binary files a/src/common/tex/corner_sw.png and b/src/common/tex/corner_sw.png differ diff --git a/src/common/tex/head_e.png b/src/common/tex/head_e.png index 29d2ad0..ea09c4a 100644 Binary files a/src/common/tex/head_e.png and b/src/common/tex/head_e.png differ diff --git a/src/common/tex/head_n.png b/src/common/tex/head_n.png index 89772e8..a2f3349 100644 Binary files a/src/common/tex/head_n.png and b/src/common/tex/head_n.png differ diff --git a/src/common/tex/head_s.png b/src/common/tex/head_s.png index 6b97ccc..33ac95e 100644 Binary files a/src/common/tex/head_s.png and b/src/common/tex/head_s.png differ diff --git a/src/common/tex/head_w.png b/src/common/tex/head_w.png index 2878d29..896661f 100644 Binary files a/src/common/tex/head_w.png and b/src/common/tex/head_w.png differ diff --git a/src/common/tex/tail_e.png b/src/common/tex/tail_e.png index 1cbeb52..d9e49d5 100644 Binary files a/src/common/tex/tail_e.png and b/src/common/tex/tail_e.png differ diff --git a/src/common/tex/tail_n.png b/src/common/tex/tail_n.png index 5891509..d5c4551 100644 Binary files a/src/common/tex/tail_n.png and b/src/common/tex/tail_n.png differ diff --git a/src/common/tex/tail_s.png b/src/common/tex/tail_s.png index 9168b17..b34fb24 100644 Binary files a/src/common/tex/tail_s.png and b/src/common/tex/tail_s.png differ diff --git a/src/common/tex/tail_w.png b/src/common/tex/tail_w.png index bee4e41..3a24de0 100644 Binary files a/src/common/tex/tail_w.png and b/src/common/tex/tail_w.png differ diff --git a/src/manifest.json b/src/manifest.json index d8ee02e..5ef6c13 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -2,8 +2,8 @@ "package": "com.ilias.snake", "name": "SNAKE", "icon": "/common/icon.png", - "versionName": "0.7", - "versionCode": 7, + "versionName": "0.8", + "versionCode": 8, "minAPILevel": 1, "deviceTypeList": [ "watch" diff --git a/src/pages/index/index.ux b/src/pages/index/index.ux index 7eb0c5d..57abf79 100644 --- a/src/pages/index/index.ux +++ b/src/pages/index/index.ux @@ -10,7 +10,7 @@