Skip to content

Commit 9f6617f

Browse files
hyperpolymathclaude
andcommitted
docs(lessons): PlayerHP — promote to compiled (952B WASM, v0.1.0 verified)
Status: design-translated → compiled. The PlayerHP.affine translation typechecks and compiles cleanly via the containerized v0.1.0 toolchain (`./scripts/affinescript-host-shim.sh`); resulting WASM module is 952 bytes, validates as MVP, lives at idaptik/src/app/combat/PlayerHP.wasm. Reproducibility section now contains actual command output (not placeholder) matching idaptik-hitbox's format. Surfaced a second v0.1.0 expedient alongside Float→Int: `mut Player` parameter mode is real (per examples/ownership.affine) but the `p.field = ...` in-body assignment idiom for struct fields through a mut borrow is not demonstrated by any v0.1.0 example. The shipped PlayerHP.affine substitutes immutable returning-new — same shape of expedient as Float→Int, preserves the design semantics, gets reverted when the upstream surface closes the gap. New "What changed" subsection 6 documents this fully. Updates the playbook's lesson-index entry to drop the "design-translated, toolchain run pending" qualifier and add the WASM-size and second-expedient notes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 74024c6 commit 9f6617f

2 files changed

Lines changed: 59 additions & 22 deletions

File tree

docs/guides/lessons/migrations/idaptik-player-hp.adoc

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@
77

88
[NOTE]
99
====
10-
**Status: design-translated, toolchain run pending.** The translation is
11-
written to be compilable against AffineScript v0.1.0 (Int arithmetic, no
12-
external module imports), following the same Float→Int convention
13-
xref:idaptik-hitbox.adoc[idaptik-hitbox] established. A Reproducibility
14-
block matching hitbox's will be filled in by the next translator who runs
15-
this through `affinescript-host-shim.sh`.
10+
**Status: compiled. 952-byte WASM MVP module produced 2026-05-02 via the
11+
containerized v0.1.0 toolchain.** Two v0.1.0 expedients applied: Float →
12+
Int with documented scaling (per xref:idaptik-hitbox.adoc[idaptik-hitbox]);
13+
and `mut Player` with field assignment substituted for an immutable
14+
*returning-new* style, because the `mut`-borrow-with-struct-field-assignment
15+
idiom is not yet verified in the v0.1.0 surface (`examples/ownership.affine`
16+
shows `mut` as a parameter mode but no in-body assignment example).
1617
1718
Picked because it stress-tests the migration playbook's central decision —
1819
*`mut` vs `State` effect* — on a record with three orthogonal aspects
@@ -247,6 +248,41 @@ of a public field** — the source-comment header should declare the
247248
scaling factors. When AffineScript gains Float-arithmetic operators, the
248249
file can be reverted to floats with a one-pass rewrite.
249250

251+
=== 6. `mut Player` design intent → immutable returning-new (v0.1.0 expedient)
252+
253+
The design above shows `take_damage(p: mut Player, ...)` and `update(p: mut
254+
Player, dt_ms: Int) -> Velocity`. The shipped `PlayerHP.affine` does
255+
**not** use that signature. It uses an immutable returning-new style:
256+
257+
[source,affinescript]
258+
----
259+
fn take_damage(p: Player, amount: Int, source: Pos, player: Pos) -> Player
260+
fn update(p: Player, dt_ms: Int) -> UpdateResult // { player, velocity }
261+
----
262+
263+
Why: `mut` is a real parameter mode in v0.1.0
264+
(`examples/ownership.affine` shows `fn takes_mut(x: mut Bool) -> () = ();`)
265+
but the in-body idiom for *assigning to a struct field through a `mut`
266+
borrow* — the natural way to write `p.hp.current = ...` in the lesson's
267+
design — is not demonstrated by any v0.1.0 example I could find, and
268+
attempting it without verification risked the kind of speculative-syntax
269+
bug a lesson should not propagate.
270+
271+
The immutable returning-new form preserves the *semantics* of the
272+
re-decomposition (every mutator decision is still made; aspects are
273+
still fissioned; the Player record is still composed) at the cost of
274+
each mutator allocating a fresh struct. This is acceptable for a small
275+
record and a 60fps game tick; it is not the long-term design.
276+
277+
**This is the same shape of expedient as Float → Int**: a v0.1.0
278+
compiler limitation forces a syntactic compromise that does not change
279+
the design. Document it at the file header; rewrite when the upstream
280+
gap closes.
281+
282+
The `mut` vs `State` *decision* the lesson is about — local mutation in
283+
one tick under one owner — is unaffected. It just gets expressed
284+
immutably for now.
285+
250286
== Playbook verdict
251287

252288
=== Held up cleanly
@@ -303,25 +339,26 @@ When translating a `.res` file with **multi-field mutable state**:
303339

304340
== Reproducibility
305341

306-
[NOTE]
307-
====
308-
This lesson is design-translated; the toolchain run is pending. The
309-
steps below match xref:idaptik-hitbox.adoc#reproducibility[the hitbox
310-
lesson's Reproducibility section] and should produce the same shape of
311-
output.
312-
====
342+
The translation, the typecheck, the compile, and the WASM emission can
343+
be reproduced from the IDApTIK repository at HEAD as of 2026-05-02:
313344

314345
[source,bash]
315346
----
316-
$ just affinescript-stage
317347
$ ./scripts/affinescript-host-shim.sh check src/app/combat/PlayerHP.affine
318-
# Expected: "Type checking passed"
348+
Type checking passed
349+
319350
$ ./scripts/affinescript-host-shim.sh compile src/app/combat/PlayerHP.affine -o src/app/combat/PlayerHP.wasm
320-
# Expected: "Compiled src/app/combat/PlayerHP.affine -> src/app/combat/PlayerHP.wasm (WASM)"
351+
Compiled src/app/combat/PlayerHP.affine -> src/app/combat/PlayerHP.wasm (WASM)
352+
353+
$ ls -la src/app/combat/PlayerHP.wasm
354+
-rw-r--r-- 1 hyperpolymath hyperpolymath 952 May 2 08:12 src/app/combat/PlayerHP.wasm
355+
356+
$ file src/app/combat/PlayerHP.wasm
357+
src/app/combat/PlayerHP.wasm: WebAssembly (wasm) binary module version 0x1 (MVP)
321358
----
322359

323-
When the toolchain run completes, replace this NOTE with the actual
324-
output (matching hitbox's format) and link the resulting `.wasm` size in
325-
the file header. If the run fails on a v0.1.0 limitation not anticipated
326-
here, **edit the lesson** — that is exactly the case study a future
327-
translator most needs.
360+
The IDApTIK side of this lesson lives at `idaptik/src/app/combat/PlayerHP.affine`
361+
(translation, 144 lines) and `idaptik/src/app/combat/PlayerHP.res` (original,
362+
88 lines). The compiled `.wasm` is 952 bytes — roughly 2.5× hitbox's 384
363+
bytes, accounting for the larger function surface (`make`, `is_invincible`,
364+
`is_alive`, `take_damage`, `update`, plus the four tuning fns and `max`).

docs/guides/migration-playbook.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ Existing entries:
274274

275275
* link:lessons/migrations/idaptik-hitbox.adoc[idaptik `Hitbox.res` → `Hitbox.affine`] — the first compilable translation; pure-leaf collision primitives. Surfaces the v0.1.0 Float-arithmetic typechecker gap.
276276
* link:lessons/migrations/idaptik-user-settings.adoc[idaptik `UserSettings.res` (design-only)] — multi-pattern, dependency-heavy persistence layer. Stress-tests the playbook against five external module dependencies; surfaces three small playbook gaps (addressed in v1.1).
277-
* link:lessons/migrations/idaptik-player-hp.adoc[idaptik `PlayerHP.res` (design-translated, toolchain run pending)] — six-field mutable record with three orthogonal aspects (HP, i-frames, knockback). Stress-tests the `mut` vs `State` decision criterion; surfaces the **aspect-decomposition** pattern as a v1.2 candidate addition.
277+
* link:lessons/migrations/idaptik-player-hp.adoc[idaptik `PlayerHP.res` → `PlayerHP.affine`] — six-field mutable record with three orthogonal aspects (HP, i-frames, knockback). Compiled to a 952-byte WASM module via the v0.1.0 toolchain. Stress-tests the `mut` vs `State` decision criterion; surfaces the **aspect-decomposition** pattern as a v1.2 candidate addition, and the v0.1.0 `mut`-with-struct-field-assignment gap as a second expedient (alongside Float→Int).
278278

279279
If you complete a non-trivial `.res → .affine` translation and the re-decomposition reveals something future translators would benefit from, write it up there. The format is loose: original snippet, faithful port, re-decomposed port, and what the second one buys you. Mark the lesson "design-only" if the destination toolchain cannot yet compile it — the design walk-through still pressures the playbook.
280280

0 commit comments

Comments
 (0)