Skip to main content

race-example

A walkthrough of the Race project (the 'race' template): the start countdown, the start line and when the boat reaches it. Which file does what in engine/, kindle/src/ and watch/src/, how an event travels from a button to the engine and the view back to both screens, and what to copy when starting a new bundle from it.

来源信息

仓库
esensible/extremers
最近来源活动
2026年10月13日 04:29
检测到的 SKILL.md 语言
英语
星标
0
分支
0

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
race-example
description
A walkthrough of the Race project (the 'race' template): the start countdown, the start line and when the boat reaches it. Which file does what in engine/, kindle/src/ and watch/src/, how an event travels from a button to the engine and the view back to both screens, and what to copy when starting a new bundle from it.
# race-example — the Race project, file by file Create it with `await api.project.create('race', { template: 'race' })`. The files come from the studio's vendored copy (`vendor/examples/race/`) of the monorepo's `engine/race/`, `extreme-race/client-js/src/` and `extreme-race/watch-js/src/`, unchanged. The bundle is named `race` whatever the project is called (`studio.json`: `{"name": "race", "version": 1}`), so its id is `packageId("race")` (FNV-1a) and its wire module is `race-wire`. ## The engine (`engine/`) | File | What | |---|---| | `engine.expr` | one expression, evaluated once per event: dispatches on `event.gps`, `event.wake`, `line_stbd`, `line_port`, `bump_seq`, `race_finish`, else keeps the state; then builds the view from the new state and sets `wake` to the gun while counting down. Section 10 of the `expr-engine` skill explains it line by line. | | `state.schema.json` | `phase` (variants `idle` / `countdown{gun}` / `racing{gun}`), `line` (`stbd`, `port`: the fixes where they were set; `crossing`: predicted epoch ms), `velocity` (`speed` kn, `heading` °) | | `view.schema.json` | what the clients get: `phase`, `line{stbd: bool, port: bool}`, `crossing?`, `speed?`, `heading?` (only while racing) | | `event.schema.json` | `line_stbd {}`, `line_port {}`, `bump_seq {seconds: int32, tapped_at: int64 ≥ 0}`, `race_finish {}` | | `samples.json` | 15 chained runs: the smoke runs, a fix, both line ends, fixes with velocity (a crossing prediction), a 5-minute sequence started, rounded down (`seconds: 0`), bumped 30 s, the gun's wake (→ racing), a fix while racing, `race_finish`, and a stale wake that changes nothing | What the engine shows about the language: the initial state written once (`state ?? {phase: {idle: {}}, line: {}}`); a boolean `let predict` that narrows four values at once; `pointing` checked with `bearing`/`angle_diff` before `course_crosses_line` (the crossings are whole great circles); time to the line as distance over speed from `fix.t`; `wake: next.phase.countdown?.gun` (a timer exactly while counting down); the wake handler ignoring a stale wake. ## The Kindle page (`kindle/src/`) | File | What | |---|---| | `index.jsx` | `<Switch>` on the phase: `Active` (idle), `Sequence` (countdown), `Race` (racing), "Loading…" before the first view; the exit button (two taps → back to Choose); "Connection lost" while the socket is down; imports `touch.js` and `style.css` | | `api.js` | the websocket, Envelope decoding, the `race` store (`reconcile`), the device-clock offset, staleness → reload `/`, `postEvent`, `exit` (the `kindle-ui` skill describes the pattern) | | `common.jsx` | readers of the store (`phase()`, `speed()`, `heading()`, `startTime()`, `lineTag()`, `crossTime()`), the wall clock, `LineButtons` (Port/Stbd; with both ends set, the time to the line) | | `idle.jsx` | speed, clock, line buttons, the sequence buttons 10/5/4/1 min → `bump_seq {seconds, tapped_at}` behind a confirm | | `sequence.jsx` | the countdown (a tick loop on the device clock), the line buttons, the bump buttons 5, 1, Sync, 1, 5 → `bump_seq {seconds: -300, -60, 0, 60, 300}` (> 0 moves the gun earlier, < 0 later, 0 = Sync: round the time left down to a whole minute), each behind a confirm | | `race.jsx` | race time since the gun, speed, heading; the race-time button finishes (`race_finish`, two taps) | | `confirm.jsx` | the second-tap confirm widget (`client-common/confirm.js`) | | `touch.js`, `style.css`, `assets/pissing.svg` | Kindle touch guards; the styles; an image | ## The watch app (`watch/src/`) The same screens as the Kindle in solid-watch nodes: `index.jsx` (`createApp(App, { onState })`), `api.js` (`onState(bytes)` from the shell, `postEvent` via `send`, `selectDefault`), `common.jsx`, `idle.jsx`, `sequence.jsx`, `race.jsx`, `confirm.jsx`, `style.jsx` (the Kindle's CSS classes as props: `Big`, `Row`, `Pad`) and `haptics.jsx` (buzzes each whole minute, at 30 s, 10 s, each of the last five seconds and at the gun). ## One event, end to end 1. A person taps **Stbd** on the Kindle: `postEvent({ line_stbd: {} })`. 2. `api.js` encodes it with the `EVENT` descriptor and sends a HostEvent `Engine { package: race, bytes }` over the websocket (the watch: `send` → BLE). 3. The C6's host checks the bytes against `event.schema.json`, then evaluates `engine.expr` with `event = {line_stbd: {}}`, the state, `timestamp` and the track as `gps`. 4. The new state stores `line.stbd = last(gps)`; the view becomes `line: {stbd: true, port: false}`. The host publishes the view only if its encoded bytes changed, in an Envelope to every client. 5. Both screens decode it and the Stbd button shows as set. In the studio the same path runs without hardware: `api.sim.start()`, `api.sim.gps({...})`, `api.sim.event({ line_stbd: {} })`, `api.sim.state()`; and with the previews, `api.preview.press('kindle', { text: 'Stbd' })`. ## Starting a new bundle from Race Keep the shape: `api.js` per client (rename `race-wire` to `<name>-wire` and `packageId("race")` to your name, and set it as the bundle's name: `api.project.settings({ name })`), one page per variant of your view's phase, the store fed with `reconcile`, every time on the device clock. Replace the engine and schemas first, check them (`api.engine.check()`), then change the pages to read your view.
在 GitHub 查看