Skip to content

Commit ff12392

Browse files
demonstrate history states in platformer
1 parent 1d1f35f commit ff12392

15 files changed

Lines changed: 1065 additions & 562 deletions

File tree

.github/workflows/ci.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,25 @@ jobs:
4242
- run: pnpm --dir ../.. build
4343
- run: pnpm install --frozen-lockfile
4444
- run: pnpm check
45+
46+
platformer-example:
47+
runs-on: ubuntu-latest
48+
defaults:
49+
run:
50+
working-directory: examples/platformer
51+
steps:
52+
- uses: actions/checkout@v7
53+
- uses: pnpm/action-setup@v6
54+
with:
55+
package_json_file: examples/platformer/package.json
56+
- uses: actions/setup-node@v7
57+
with:
58+
node-version: 24
59+
cache: pnpm
60+
cache-dependency-path: |
61+
pnpm-lock.yaml
62+
examples/platformer/pnpm-lock.yaml
63+
- run: pnpm --dir ../.. install --frozen-lockfile
64+
- run: pnpm --dir ../.. build
65+
- run: pnpm install --frozen-lockfile
66+
- run: pnpm check

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,8 @@ need an `initial` handler to construct those new child values:
261261

262262
```ts
263263
payment: {
264-
initial: ({ state }) => new CardEntry({ attempt: state.attempt, cardNumber: "" })
264+
initial: ;
265+
;(({ state }) => new CardEntry({ attempt: state.attempt, cardNumber: "" }))
265266
}
266267
```
267268

@@ -499,8 +500,9 @@ TypeScript consumer with `skipLibCheck: false`.
499500
The [platformer statechart example](./examples/platformer) is a playable SVG
500501
demo centered on a schema-first character machine. It demonstrates nested
501502
compound locomotion, parallel airborne motion and air-jump regions, independent
502-
facing and wall-contact regions, typed protocol events, state-scoped timers,
503-
and state-driven SVG transforms.
503+
facing and wall-contact regions, a pause/resume flow backed by typed deep
504+
history, typed protocol events, state-scoped timers, and state-driven SVG
505+
transforms.
504506

505507
The [Pokémon statechart example](./examples/pokemon) is a standalone React and
506508
Vite project demonstrating compound and parallel states, state-scoped invokes,

examples/platformer/README.md

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,44 @@ pnpm check
2121
- **W**, **up**, or **Space** — jump; press again once in the air for a double jump
2222
- Touch either wall and jump — turn and kick away; repeat after returning to a wall
2323
- **S** or **down** — duck while grounded; dive while airborne
24+
- **P** — pause and resume the exact playable configuration through deep history
2425
- **R** — reset
2526

2627
## Statechart
2728

2829
`Character` is parallel: `locomotion`, `facing`, and `contact` update
29-
independently. The locomotion region is compound and makes `Grounded` and
30-
`Airborne` mutually exclusive. Each branch is compound again:
30+
independently. The locomotion region switches between `Playing` and `Paused`.
31+
Inside `Playing`, `Grounded` and `Airborne` are mutually exclusive. Each branch
32+
is compound again:
3133

3234
```text
3335
Character (parallel)
3436
├─ locomotion
35-
│ ├─ Grounded: Standing | Running | Ducking | Landing
36-
│ └─ Airborne (parallel)
37-
│ ├─ motion: Jumping | Falling | Diving
38-
│ └─ airJump: GroundLock | WallLock | Ready | Spent
37+
│ ├─ Playing
38+
│ │ ├─ Grounded: Standing | Running | Ducking | Landing
39+
│ │ ├─ Airborne (parallel)
40+
│ │ │ ├─ motion: Jumping | Falling | Diving
41+
│ │ │ └─ airJump: GroundLock | WallLock | Ready | Spent
42+
│ │ └─ resume (deep history)
43+
│ └─ Paused
3944
├─ facing: Left | Right
4045
└─ contact: NoWall | LeftWall | RightWall
4146
```
4247

48+
`Pause` exits `Playing`, which records its current deep configuration. Physics
49+
stops while `Paused`. `Resume` targets `Playing.resume`, restoring both the
50+
active descendants and their typed values: for example, an airborne wall jump
51+
returns with its `originY`, `startedAt`, `push`, jump kind, and air-jump lock.
52+
This is one saved configuration, not an undo stack; pausing again replaces the
53+
previous history. The history implementation also supplies a typed default
54+
`Playing` snapshot for the case where the history node is targeted before the
55+
region has ever been exited.
56+
57+
State-scoped invocations follow normal statechart entry/exit semantics. Pausing
58+
cancels an active landing or air-jump timer, and restoring that state starts its
59+
invocation again. History restores state configuration and values, not elapsed
60+
wall-clock time or the adapter's past events.
61+
4362
State payloads live only where they are valid: `Landing` owns impact and resume
4463
direction, while `Airborne` owns only the jump origin. Air-jump availability is
4564
modeled entirely as state: lock states own cancellable readiness timers,

examples/platformer/index.html

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,17 @@ <h1>Orbit Courier</h1>
5353
<path class="scarf" d="M2 20h27v5H17l-7 5 2-5H2z" />
5454
</g>
5555
</g>
56+
<g class="pause-overlay" aria-hidden="true">
57+
<rect width="640" height="360" />
58+
<text class="pause-title" x="320" y="170">PAUSED</text>
59+
<text class="pause-copy" x="320" y="197">P resumes from deep history</text>
60+
</g>
5661
</svg>
5762
<div class="controls" aria-label="Controls">
5863
<span><kbd>A</kbd><kbd>D</kbd> move</span>
5964
<span><kbd>W</kbd>/<kbd>Space</kbd> jump ×2</span>
6065
<span><kbd>S</kbd> duck / dive</span>
66+
<span><kbd>P</kbd> pause / resume</span>
6167
<span><kbd>R</kbd> reset</span>
6268
</div>
6369
</div>
@@ -72,33 +78,40 @@ <h1>Orbit Courier</h1>
7278
<div class="regions">
7379
<section>
7480
<h2>locomotion</h2>
75-
<div class="branch">
76-
<h3 data-node="Grounded">Grounded</h3>
77-
<div class="state-row">
78-
<span data-node="Standing">Standing</span>
79-
<span data-node="Running">Running</span>
80-
<span data-node="Ducking">Ducking</span>
81-
<span data-node="Landing">Landing</span>
82-
</div>
81+
<div class="state-row play-state-row">
82+
<span data-node="Playing">Playing</span>
83+
<span data-node="Paused">Paused</span>
84+
<span class="history-node">resume · H*</span>
8385
</div>
84-
<div class="branch">
85-
<h3 data-node="Airborne">Airborne</h3>
86-
<div class="parallel-label">parallel regions</div>
87-
<div class="nested-region">
88-
<small>motion</small>
86+
<div class="playing-region">
87+
<div class="branch">
88+
<h3 data-node="Grounded">Grounded</h3>
8989
<div class="state-row">
90-
<span data-node="Jumping">Jumping</span>
91-
<span data-node="Falling">Falling</span>
92-
<span data-node="Diving">Diving</span>
90+
<span data-node="Standing">Standing</span>
91+
<span data-node="Running">Running</span>
92+
<span data-node="Ducking">Ducking</span>
93+
<span data-node="Landing">Landing</span>
9394
</div>
9495
</div>
95-
<div class="nested-region">
96-
<small>air jump</small>
97-
<div class="state-row">
98-
<span data-node="AirJumpGroundLock">ground lock</span>
99-
<span data-node="AirJumpWallLock">wall lock</span>
100-
<span data-node="AirJumpReady">ready</span>
101-
<span data-node="AirJumpSpent">spent</span>
96+
<div class="branch">
97+
<h3 data-node="Airborne">Airborne</h3>
98+
<div class="parallel-label">parallel regions</div>
99+
<div class="nested-region">
100+
<small>motion</small>
101+
<div class="state-row">
102+
<span data-node="Jumping">Jumping</span>
103+
<span data-node="Falling">Falling</span>
104+
<span data-node="Diving">Diving</span>
105+
</div>
106+
</div>
107+
<div class="nested-region">
108+
<small>air jump</small>
109+
<div class="state-row">
110+
<span data-node="AirJumpGroundLock">ground lock</span>
111+
<span data-node="AirJumpWallLock">wall lock</span>
112+
<span data-node="AirJumpReady">ready</span>
113+
<span data-node="AirJumpSpent">spent</span>
114+
</div>
102115
</div>
103116
</div>
104117
</div>
@@ -140,7 +153,8 @@ <h2>wall contact</h2>
140153

141154
<p class="note">
142155
A small adapter owns coordinates and gravity. The machine owns legal behavior, and the box simply transforms to
143-
show its active state. Wall contact is tracked explicitly, so floor-corner jumps stay ordinary.
156+
show its active state. Pause exits <code>Playing</code>; resume targets its deep-history node and restores the
157+
exact grounded or airborne configuration, including state-local values.
144158
</p>
145159
</main>
146160
<script type="module" src="/src/main.ts"></script>

examples/platformer/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,20 @@
66
"type": "module",
77
"scripts": {
88
"dev": "vite",
9+
"test": "vitest run",
910
"typecheck": "tsc --noEmit",
1011
"build": "pnpm typecheck && vite build",
1112
"preview": "vite preview",
12-
"check": "pnpm build"
13+
"check": "pnpm test && pnpm build"
1314
},
1415
"dependencies": {
1516
"@typeonce/effect-machine": "file:../..",
1617
"effect": "4.0.0-beta.102"
1718
},
1819
"devDependencies": {
1920
"typescript": "6.0.3",
20-
"vite": "8.1.5"
21+
"vite": "8.1.5",
22+
"vitest": "4.1.10"
2123
},
2224
"packageManager": "pnpm@10.17.1",
2325
"engines": {

0 commit comments

Comments
 (0)