Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .engine/product-spec-matrix.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@
"who": "engine"
},
{
"criterion": "A model fixture over the committed data reproduces formation lookups (AI level + offset, fold-back at 0x80); the build's in-game selection is confirmed in play (a runtime-harness candidate once the enemy/formation slice ships)",
"digest": "sha256:14db719434b37cf2a448988587ad3c46e62454fed6ec6598e493f08d42ae59a4",
"criterion": "A model fixture over the committed data reproduces formation lookups (set-formation indexed by the record offset; raise indexed by the folded AI level; fold-back at 0x80); the build's in-game selection is confirmed in play (a runtime-harness candidate once the enemy/formation slice ships)",
"digest": "sha256:2dcfd47438dea90e3bb21b17beb5d5618a8327d46adcd30d79bd8b2ab511b3dc",
"doc": "docs/spec/difficulty-and-formations.md",
"how_verified": "Python fixture over the committed tables; operator play for the in-game half",
"position": 2,
Expand Down
8 changes: 4 additions & 4 deletions docs/MECHANICS_CATALOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ document differ, the spec document wins.
| AREA-02 | Area object scheduler | partial | AREA-01, SYS-02 | sub: `sub_fn_2__handle_objects`, `obj_fn_tbl`, `area_object_tbl_tbl_normal` | Normal events spawn in recorded order and position. The ordered schedule consume, a lossless slice-6-ready representation of one fixture area (area 1) with a materialized sentinel, and the `schedule fired` observable are built ([016](mechanics/016-area-scheduler.md)); foundation-only — the per-record dispatch is an empty seam (handlers arrive slices 8+) and all 16 tables + the full trace are slice 6. |
| AREA-03 | All 16 normal area tables | present | AREA-02 | sub: `area_1_obj_tbl_normal` through `area_16_obj_tbl_normal` | An accelerated trace consumes every normal table without unknown or Super objects. All 16 normal schedules are ingested into the flattened columns with per-area offsets and the accelerated 1–16→7 data trace passes ([017](mechanics/017-all-area-schedules.md)); the trace/round-trip prove the data, the built per-area consume rests on the offset golden + operator playtest. |
| AREA-04 | Transitions and 16-to-7 loop | present | AREA-03 | main: `main_gameplay_loop`; sub: `sub_fn_3__handle_next_area` | Areas advance 1–16 then continue at 7, with no win screen. The advance/wrap shipped in AREA-01; with all 16 schedules ingested the loop is data-complete and the accelerated trace confirms 1–16→7 with no win screen ([018](mechanics/018-area-transitions-and-loop.md)) — a data trace, runtime traversal on the operator playtest. |
| DIF-01 | Difficulty setting | missing | AREA-02 | sub: `difficulty_tbl`, `sub_2_fn_3__inc_enemy_AI_and_flying_enemies` | Each normal setting starts at its recorded pressure. |
| DIF-02 | Score-per-life adaptive AI | missing | DIF-01, ECO-01 | sub: `sub_2_fn_23__adjust_AI_level_based_on_score`, `avg_score_per_solvalou` | Fixtures show ordered AI changes from score per craft. |
| DIF-03 | Per-family fire frequency | missing | DIF-01, SYS-04 | sub: `sub_2_fn_8__fire_freq_mask_derota` through `sub_2_fn_22__fire_freq_mask_andor_genesis` | Family fire fixtures follow recorded masks. |
| FORM-01 | Normal flying formations | missing | AREA-02, DIF-01 | sub: `flying_enemy_type_offset_tbl_normal`, `sub_2_fn_2__set_flying_enemies` | Fixtures preserve normal type, count, offsets, and order. |
| DIF-01 | Difficulty setting | partial | AREA-02 | sub: `difficulty_tbl`, `sub_2_fn_3__inc_enemy_AI_and_flying_enemies` | Each normal setting starts at its recorded pressure. The adaptive AI level accumulates live from the schedule's raise records and folds back below 0x80 ([019](mechanics/019-difficulty-ai-level.md)); foundation — the AI level is live/observable, its consumption as enemy pressure is slices 8+, and the cabinet DIP is a project-defined placeholder (increment +2). |
| DIF-02 | Score-per-life adaptive AI | partial | DIF-01, ECO-01 | sub: `sub_2_fn_23__adjust_AI_level_based_on_score`, `avg_score_per_solvalou` | Fixtures show ordered AI changes from score per craft. The adjust records re-tune the AI level by floor(floor(score/1000)/craft) capped at 16, reading the live score and craft ([021](mechanics/021-score-adaptive-ai-level.md)); foundation — the rule is live and fixture-proven, the visible "playing better = harder" difference needs enemies (slice 8+). |
| DIF-03 | Per-family fire frequency | partial | DIF-01, SYS-04 | sub: `sub_2_fn_8__fire_freq_mask_derota` through `sub_2_fn_22__fire_freq_mask_andor_genesis` | Family fire fixtures follow recorded masks. The area schedule sets the eight per-family fire-permission masks + the ground-stop-firing row live from the record bytes ([022](mechanics/022-fire-permission-masks.md)); foundation — the masks are set/observable, the firing that consumes them is slices 8+. |
| FORM-01 | Normal flying formations | partial | AREA-02, DIF-01 | sub: `flying_enemy_type_offset_tbl_normal`, `sub_2_fn_2__set_flying_enemies` | Fixtures preserve normal type, count, offsets, and order. The wave size + type-table offset are selected live — set-formation by the record offset, raise by the folded AI level, reset zeroes ([020](mechanics/020-normal-flying-formations.md)); foundation — selection is live/observable, spawning the wave is slice 8. |

## Flying enemies and projectiles

Expand Down
16 changes: 15 additions & 1 deletion docs/PLAYTEST_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,21 @@ so area position is read from the `area progress`/`area number` variable watcher
area runs its own schedule. (Some areas legitimately share a record count, so two areas showing the
*same* peak is **not** itself a bug; the exact per-area correctness is guaranteed by the build-time
round-trip test, not the eye.) The visual terrain is not yet driven by the clock, so these are
variable-watcher checks, not on-screen ones.
variable-watcher checks, not on-screen ones. **Also tick `ai level`, `formation count`, and
`formation type offset`** (DIF-01 / FORM-01): as you fly, `ai level` climbs a little each time a raise
record fires and stays below **128** (a raise folds it back — you should never see it reach 128); if you
have been scoring (press **S** to raise the score), it can also jump when a score-adjust record fires
(DIF-02) — the *amount* is score/craft-dependent and its visible effect on enemies is deferred to slice 8; and
`formation count` / `formation type offset` change to a new wave (count in **1–6**) when a raise or a
set-formation record fires, and drop to 0 on a reset-formation. **Read the `ai level` growth *rate* as a
placeholder, not fidelity:** the cabinet difficulty is a project-chosen default (increment +2), so how
*fast* the level climbs is not meaningful yet — only that it climbs, folds, and drives a valid formation
is. No enemies spawn from the formation this slice, so this is a watcher check, not an on-screen one; the
exact table correctness is the build-time model fixture's, not the eye's. **You can also tick a
`fire mask *` watcher** (e.g. `fire mask logram`) and **`ground stop firing row`** (DIF-03): each takes
its scheduled byte value as the area scrolls (logram, for instance, is set near the top of area 1) and
resets to 0 on a new game. As with the masks above, no family *fires* from these yet — that is slice 8 —
so this only confirms the schedule sets them.
5. **Repeated deaths and the near-end checkpoint.** Die several times in a row (today: press D; once
killers exist, die to a bullet, an enemy, and a Bacura): the full death presentation and sound
complete uncut, the craft respawns immediately vulnerable, and nothing from the previous life
Expand Down
15 changes: 15 additions & 0 deletions docs/mechanics/019-difficulty-ai-level.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Difficulty AI level — the accumulating pressure the schedule raises

- Mechanic: DIF-01 (partial, foundation) — one adaptive "AI level" accumulates live during play. The schedule's `raise_ai_level_and_set_formation` records add the cabinet difficulty increment to it and fold it back below 0x80, and re-select the incoming formation from the new level ([FORM-01](020-normal-flying-formations.md)). The AI level is durable per-game state: it persists across death and respawn and resets for a new game.
- Derived behavior: While an area scrolls, each raise record (one of the schedule's two most common kinds) adds the cabinet DIP-selected increment to the AI level; if that would take the level to 0x80 (128) or above, 0x40 (64) is subtracted first, so the level *saturates by folding back, not by clamping*. The four DIP settings add 2, 0, 6, or 16 respectively. The raised level then indexes the flying-formation table (the raise's re-select path). The AI level is the number every later enemy behaviour reads to scale its pressure; the accumulation and fold-back are this mechanic.
- Reference provenance: `jotd666/xevious@71473685a8c7856c8401c8519276cd97a38d4183`; `src/xevious_sub.68k`: `sub_2_fn_3__inc_enemy_AI_and_flying_enemies` (317–329) — the increment add, the `cmp #0x80` / `sub #0x40` fold-back, and the branch that re-selects the formation with the raised level as the index. The four increments are the committed, hash-pinned `difficulty_tbl` in [docs/spec/data/difficulty.json](../spec/data/difficulty.json) (`[2, 0, 6, 16]`, source lines 338–342); the behaviour is the settled [difficulty and formations](../spec/difficulty-and-formations.md) document.
- Transfer class: General behavior and numeric constant (instruction-derived rule and a derived numeric table; no source text or media copied).
- Scratch interpretation: The Stage owns `ai level`. The raise branch of the schedule consume (`_consume_schedule`) reads the increment from the baked `difficulty increment` list at the project's DIP index, adds it to `ai level` (through the centralized `op_*` helpers — never inline operator blocks), then, in a single `if`, subtracts 64 when the level exceeds 127 (the fold-back), and re-selects the formation using the new `ai level` as the table index (FORM-01). The cabinet DIP is a project-defined constant `DIFFICULTY_DIP_INDEX = 0` (increment +2) — a four-marker placeholder, since the spec records no arcade power-on default (like the RNG cold-start seed); index 0 is the lowest increment that still *progresses* (index 1 = +0 would make raises inert), and it is an independent switch from the starting-lives / bonus DIPs. A separate Stage `director reset` receiver (`difficulty_reset`) zeroes `ai level` on a world reset (cold-start / new-game) only, so it persists across a new life.
- Scratch evidence: the DIF-01 constants, the raise branch and `difficulty_reset` receiver, the `ai level` Stage variable, and the `difficulty increment` Stage list, all in `tools/game_director.py`. Model fixtures in `tests/test_spec_docs.py` (`DifficultyAndFormations`): `test_baked_tables_match_committed_data` (the baked increments equal `difficulty.json`) and `test_ai_level_fold_back` (the raise math + fold-back below 0x80). Dynamic check in the scratch-vm harness (`harness/lib/catalog.js` `difficulty-and-formations`): real play drives `ai level` above zero and confirms it never reaches 0x80, with a negative that breaks the raise dispatch so it never rises. The `difficulty_state` variable category and the write-forbid set in `tests/test_scratch_project.py` guard the new state.
- Acceptance criteria: The four difficulty-setting increments are `[2, 0, 6, 16]` and the build's baked list matches the committed file (engine, data-table fixture). The raise adds the increment and folds back once at 0x80 (engine, model fixture). In real play the AI level climbs as raise records fire and never reaches 0x80 (engine, scratch-vm harness). Operator: with the `ai level` watcher open, it climbs as the area scrolls and resets on a new game — an inspectable confirmation; the growth *rate* reflects the placeholder DIP and is not itself fidelity evidence.
- Fidelity status: DIF-01 is **partial** (foundation) — the AI-level accumulation and fold-back are built, live, and observable, and the rule is reference-faithful. What is deferred: the *consumption* of the AI level as enemy pressure (formation size feeds spawning, and per-family firing) arrives with the enemy slices (8+), and the score-adaptive re-tune (DIF-02, a later commit of this slice) is not yet wired — so this commit moves the AI level only by raises.
- License status: The pinned reference states no reusable license; only derived behavior and the committed, hash-pinned `difficulty.json` increments are used, cited to the settled spec and the data file, and no reference source text or media was reproduced.
- Known deviations or uncertainty: The DIP index (`0`, increment +2) is a **project-defined placeholder** with no arcade basis. Unlike the dormant RNG cold-start seed, it is **consumed live this slice** and directly scales the observable AI-level growth, so the growth *rate* an operator sees is placeholder-driven and is **not** a fidelity claim — only the growth *mechanism* is confirmed. The AI level moves only via raises this commit (the score re-tune lands with DIF-02); with raises alone the fold-back keeps it in `[0, 127]`, so the fold-back is exercised by the model fixture rather than the short live playtest.
- [x] No assembly or other source code was copied into the Scratch project.
- [x] No arcade ROM files were acquired, opened, extracted, or distributed.
- [x] Any transferred graphics or audio are recorded in `src/xevious/assets/provenance.json`.
15 changes: 15 additions & 0 deletions docs/mechanics/020-normal-flying-formations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Normal flying formations — the wave size and type the schedule selects

- Mechanic: FORM-01 (partial, foundation) — the incoming flying wave's size and its flying-enemy type-table offset are selected live from the normal formation table. A set-formation record indexes the table by its own signed offset; a raise record ([DIF-01](019-difficulty-ai-level.md)) re-selects using the folded AI level as the index; a reset-formation record zeroes the wave between formations.
- Derived behavior: The formation table yields two values per entry — the number of flying enemies in the incoming wave (observed range 1–6) and an offset into the flying-enemy type table that determines *which* types compose it. Two schedule record kinds pick the entry, by two different indices: a `set_flying_formation` record uses **its signed offset as the index** (the AI level is *not* added on this path), and a `raise_ai_level_and_set_formation` record uses **the raised, folded AI level as the index** (with no record offset). A `reset_flying_formation` record sets both values to zero, ending the pressure between waves.
- Reference provenance: `jotd666/xevious@71473685a8c7856c8401c8519276cd97a38d4183`; `src/xevious_sub.68k`: `sub_2_fn_2__set_flying_enemies` (300–311) — the signed record offset, sign-extended and doubled to address the two-byte entries, indexes the table with **no AI level added**; `sub_2_fn_3` (317–329) enters the same lookup with the AI level as the index; `sub_2_fn_5__reset_flying_enemies` (331–335) zeroes both. The table, decoded completely (including its 32 negative-index entries, index −32..127), is the committed, hash-pinned `flying_enemy_type_offset_tbl_normal` in [docs/spec/data/formations.json](../spec/data/formations.json); the behaviour is the settled [difficulty and formations](../spec/difficulty-and-formations.md) document, whose formation-index wording was corrected to match this reference in the same pull request.
- Transfer class: General behavior and structured table.
- Scratch interpretation: The formation table is baked into two parallel Stage lists — `formation count table` and `formation type offset table` — of 160 logical entries in index order −32..127 (the reference's byte "doubling" is already absorbed by decoding to logical entries, so no doubling is needed at runtime). The Stage owns `formation count` and `formation type offset` (the selected wave, which the slice-8 spawner will read) and the transient `formation index` working register. The shared `_select_formation` helper writes `formation index`, then — because Scratch `item N of list` returns the empty string for an out-of-range N, which would silently poison arithmetic — GUARDS both bounds: only when the index is in −32..127 does it set the two values from the table at slot `index − (−32) + 1`; an out-of-domain index leaves the prior formation unchanged (no faithful ROM-adjacent value exists to fabricate). The set / raise / reset branches of `_consume_schedule` call it with the record offset, the folded AI level, and a zeroing, respectively.
- Scratch evidence: `_select_formation` and the set/raise/reset branches of `_consume_schedule`, the `formation count table` / `formation type offset table` Stage lists, the `formation count` / `formation type offset` / `formation index` variables, and the new `schedule arg` schedule column (the runtime-readable set-formation offset), all in `tools/game_director.py`. Model fixtures in `tests/test_spec_docs.py` (`DifficultyAndFormations`): `test_baked_tables_match_committed_data`, `test_formation_lookup_reproduces_committed_table` (every entry across the domain), and `test_formation_index_in_domain_over_committed_schedules` (walking the real 1–16→7 schedules with the AI-level dynamics, proving every selection index stays in −32..127 so the guard is a proven-dead defensive branch this slice, and both selection paths are exercised). The extended round-trip golden (`test_all_area_schedules_round_trip_from_json`) re-derives the `schedule arg` column from source. Dynamic check in the scratch-vm harness (`difficulty-and-formations`): real play selects a live formation whose wave size stays in 1–6, with a negative that bites.
- Acceptance criteria: The baked count/type-offset lists equal a re-derivation from `formations.json` (engine, golden). The selection reproduces every committed entry across the −32..127 domain (engine, model fixture). Over the real committed schedules the selection index never leaves the table's domain (engine, range fixture). In real play a formation is selected whose recorded wave size is 1–6 (engine, scratch-vm harness). Operator: with the `formation count` / `formation type offset` watchers open, they change as set-formation and raise records fire and zero on a reset — an inspectable confirmation; no enemies spawn from the selection yet.
- Fidelity status: FORM-01 is **partial** (foundation) — the formation *selection* (both index paths, the reset, and the guarded table read) is built, live, and observable, and matches the reference. What is deferred: *spawning* the selected wave — the slice-8 flying-enemy vertical slice reads `formation count` / `formation type offset` (and the type table in [aerial enemies](../spec/aerial-enemies.md)) to place enemies — so no enemies appear from the formation this slice.
- License status: The pinned reference states no reusable license; only derived behavior and the committed, hash-pinned `formations.json` table are used, cited to the settled spec and the data file, and no reference source text or media was reproduced.
- Known deviations or uncertainty: The locked spec previously described the set-formation index as "that offset plus the current AI level"; the reference adds no AI level on that path (it is the offset alone), and the AI level is the index only for the raise path — the spec prose was corrected to match its own cited reference in this pull request, with no data change. The two-sided formation guard leaves the prior formation on an out-of-domain index rather than fabricating a value; the range fixture proves it is never reached under this slice's FULL dynamics — raises, set-formation, and [DIF-02](021-score-adaptive-ai-level.md)'s un-folded score adjust at its worst-case cap — so the guard is a defensive dead branch (DIF-02 is built in this same pull request; its interaction is recorded in 021).
- [x] No assembly or other source code was copied into the Scratch project.
- [x] No arcade ROM files were acquired, opened, extracted, or distributed.
- [x] Any transferred graphics or audio are recorded in `src/xevious/assets/provenance.json`.
Loading
Loading