Skip to content

Feature: difficulty director and normal flying formations, live - #34

Merged
StarshipSuperjam merged 5 commits into
mainfrom
claude/xevious-difficulty-formations
Aug 18, 2026
Merged

Feature: difficulty director and normal flying formations, live#34
StarshipSuperjam merged 5 commits into
mainfrom
claude/xevious-difficulty-formations

Conversation

@StarshipSuperjam

@StarshipSuperjam StarshipSuperjam commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Closes #56

A green mechanical check below shows this change conforms to the engine's rules — not that it is correct. What covers correctness is the behavioural steps in Review and your own read of the change; a green check is never a substitute for that. Your merge is the binding gate.

About those checks: only the one that runs when the change is proposed for merge can stop a risky merge — a check that ran while the change was still being written is early advice. Each check is itself proven against a deliberately broken example it must catch, so a passing check can't be one that quietly did nothing — but that proves the check works, not that this change is right. And a check that could not run leaves its area unverified.

Purpose

Make enemy pressure real: the adaptive difficulty and the normal flying formations now run live in the game, the last foundation before enemies can be scheduled to attack.

  • Slice 6 built the area scheduler but left the per-record dispatch an empty seam. This fills the difficulty/formation half of that seam: the AI level accumulates and folds back, formations are selected, and the eight per-family fire masks are set — all as live, watchable Stage state.
  • Completes the model + live-state obligations of DIF-01, DIF-02, DIF-03, FORM-01 (records 019–022). The enemy-facing consumption (waves spawning, families firing) is a separate slice-8 leaf and stays open.
  • Includes a correction to the locked spec: its formation-index rule ("offset + AI level") did not match the reference it cites; corrected to the reference (set-formation indexes by the offset alone; the raise indexes by the folded AI level).

Impact: enemy behaviour in slices 8+ now has a live difficulty number and a selected formation to read; nothing stochastic is added (difficulty/formations are deterministic, so the shared RNG stays dormant one more slice — first consumer is enemy behaviour).

Scope

A live wiring of difficulty + formations into slice 6's dispatch seam, plus a faithful correction to the locked difficulty spec — no data re-extraction.

  • _consume_schedule gains handler-keyed branches: raise (AI level += DIP increment [2,0,6,16], fold back at 0x80, re-select formation from the folded level), adjust (score re-tune), set/reset formation, the 8 fire_mask_<family> setters, and ground-stop-firing. A 4th parallel schedule column schedule arg carries the one runtime-readable scalar each record needs (Scratch can't parse the JSON payload).
  • New Stage state: ai level, formation count, formation type offset, the 8 fire masks, ground stop firing row (all reset on a world reset via a dedicated difficulty_reset receiver); plus machinery registers formation index, ai adjust. The formation table (formations.json, index −32..127) is baked as two parallel logical lists.
  • The first commit corrects docs/spec/difficulty-and-formations.md (a status: locked document) — prose only, no data change.

Change profile — the shape of this pull request at a glance:

  • Size: 13 files changed, +7201 / −2511 lines.
  • Kinds of thing touched: 13 other files (not in the engine's map).
  • Where: docs, src, tests, harness, tools.
  • Shape: 5 commits on this branch — a standalone change unless a Part of #N line below says it is one slice of a larger effort.

This is a description, not a gate — it never blocks a merge.

Part of #18.

Impact: the +7201 lines are almost entirely the regenerated src/xevious/project.json + runtime_identifiers.json (the baked tables and dispatch); the executable change is the dispatch, _select_formation, and the schedule loaders in tools/game_director.py.

Behaviors

Difficulty and formation state are computed live from the real schedules, and the formation index is proven to stay in its table domain.

  • The AI level climbs as raise records fire and folds back below 0x80DifficultyAndFormations.test_ai_level_fold_back; live in the difficulty-and-formations scratch-vm scenario.
  • Formation selection reproduces every committed table entry across −32..127 (set-formation by offset, raise by folded AI level) — test_formation_lookup_reproduces_committed_table.
  • The formation index never leaves −32..127 under the slice's full dynamics including DIF-02's worst-case adjust — test_formation_index_in_domain_over_committed_schedules (so the two-sided guard is proven-dead, not asserted).
  • The score re-tune adds min(16, floor(floor(score/1000)/craft)) for craft>0test_score_retune_rule.
  • All eight fire masks + the ground-stop row are set live from the schedule — the fire-permission-masks scenario (exercises Andor Genesis in area 4 and the ground-stop row); the schedule arg column round-trips — test_all_area_schedules_round_trip_from_json.
  • The build stays byte-deterministic — test_full_repository_verification (build hash dfd51ede…).

Out of scope

Everything that needs enemies on screen.

  • Spawning the selected formation and per-family firing gated by the masks — the slice-8 enemy vertical slices read this state; the spawn/boss dispatch handlers remain the empty seam.
  • The RNG's first consumer (enemy movement/firing) — difficulty/formations are deterministic, so no consumer is added here; the stream stays dormant (record 004).
  • The spec's operator acceptance rows that need enemies ("waves grow", "playing better = harder", "families fire only when scheduled") — deferred to slice 8, per the spec's own table.

Impact: these are dependencies not yet built, recorded as such; nothing here is a silent stub.

Risk

Low behaviourally — the one real hazard (an out-of-domain formation index) is proven unreachable and fail-safe; the notable governance item is the locked-spec edit.

  • The formation lookup guards both bounds (Scratch item N of list returns "" out of range); an out-of-domain index leaves the prior formation, and the range fixture proves it is never reached under raises + sets + DIF-02's worst-case adjust. Residual: exact in-game index fidelity (that the emitted blocks compute the right slot) rests on the deferred operator playtest, per the spec's engine/operator split.
  • DIF-02 divides by the live craft count with no subtraction (matching the reference's solvalou_number); whether that is off-by-one against a strict "in reserve" reading is recorded as residual uncertainty in record 021.
  • Guardrail (settled artifact): this edits a status: locked spec. That is a change to settled ground and needs your guardrail-ack label on this PR — a deliberate confirmation separate from the merge click. No automated gate enforces it (the weakening guard's floor does not cover docs/spec/), so it rests on your applying the label; the edit itself is a faithful, prose-only correction verified against the cited reference.

Impact: a wrong formation slice fails the range fixture or shows in playtest; it cannot ship silently. The locked-spec change is disclosed for your guardrail-ack.

Validation

All mechanical checks green; the build is byte-deterministic; the reviewed artifact is byte-identical to the submitted one.

  • Full project suite green (181 tests via unittest discover -s tests), including 24 new roadmap-closure tests carried in from main.
  • Headless scratch-vm harness 25/25 (node --test in harness/), including the two new live scenarios and their biting negatives.
  • Two builds byte-identical + import round-trip (tools/scratch_project.py verify, sha256 dfd51ede…); generator at its fixpoint (game_director.py check).
  • Engine self-test suite green (selftest.py, exit 0) — this change touches no engine surface, so the engine's own index files are unchanged.

Impact: an approver can rely on the difficulty/formation state being faithfully computed and the build reproducible; the live gameplay effect is the operator's playtest.

Review

Standard-depth review: four cold plan-review passes before building and four cold pre-submission passes after — no blocking findings; the serious findings were test-coverage/accuracy gaps, all fixed in line.

  • Plan-review (before building): four cold passes (product need, architecture, feasibility, risk). They reshaped the plan — the reset moved to its own receiver, the formation guard made two-sided, the harness scenarios re-cast as live-step pacing, and the DIP-index placeholder disclosed as consumed-live. All folded in before code.
  • Mid-build escalation: building surfaced that the locked spec's formation-index rule contradicted its cited reference. Rather than absorb a divergence from a settled artifact, this was escalated to the operator, who chose to correct the spec to the reference (first commit) — verified directly against the reference assembly at the pin.
  • Pre-submission (after building): four cold passes — spec-conformance and an adversarial divergence-hunt (coupled, against the locked spec), technical-integrity, and security-governance. To judge it, passes ran the project's own suites in this worktree (a throwaway copy relative to main; nothing committed changed) and independently simulated the difficulty dynamics over the committed data. Spec-conformance independently re-read the reference assembly and confirmed the correction is faithful. Technical-integrity mechanically confirmed zero reporter block-id reuse anywhere in the project and the determinism. No blocking findings.
  • Findings & dispositions: the serious findings were all coverage/accuracy gaps, not behaviour bugs, and were fixed in line: the domain-safety fixture now models DIF-02's worst-case adjust (flagged by three passes); the harness now exercises the two previously-unexercised DIF-03 targets (Andor Genesis, ground-stop row); stale "later commit" comments corrected; the DIF-03 playtest watcher added; the catalog aligned to "partial (foundation)". Accepted/disclosed: the 13 handler reads per dispatched record (a warp-mode nit), and the DIF-02 craft off-by-one uncertainty. Escalated to you: the locked-spec guardrail-ack (Risk), and an optional engine hardening (add docs/spec/ to the guarded-paths floor) I can log as a tracked issue.
  • Post-review touch-up: the fixes were test-coverage, a harness assertion, doc/comment accuracy, and the regeneration of the derived settled-criteria matrix (.engine/product-spec-matrix.json) that the spec correction required — no block-graph change, so the shipped artifact is byte-identical (build hash dfd51ede… unchanged). Between reviewed e29f461 and submitted e8d284f, 51 lines were added and 28 deleted or modified, a net change of +23 lines. Given the tiny size and byte-identical output, no re-audit was warranted; the full suite, harness, and determinism were re-checked green.

Things you can confirm yourself

  • Wave sizes stay within the table's recorded range and grow as the game progresses at a fixed setting: Play several areas at one setting; waves grow denser and never exceed six enemies
  • Playing better produces visibly harder waves: Play one area twice — once scoring heavily, once minimally — and compare wave pressure (paired with the seeded re-tune fixture above, since the two runs also differ in what was destroyed)
  • Enemy families fire only when their area's schedule has permitted them: Play area 1's Lograms (its scheduled firing family): they begin firing at their scheduled point, not from the start

Things I checked for you
(these ran on the engine's side — listed so you know what was checked; nothing for you to do)

  • The committed formation table (including negative indices) and difficulty tables match a re-derivation from the pinned commit: python3 tools/reference_extract.py --verify --checkout <clone> with a fresh clone at the pin (clone recipe in the index); the run passes or names the failing table
  • The four difficulty-setting increments are 2, 0, 6, 16 and the build's data matches the committed file: Data-table comparison in the deterministic build fixtures
  • 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): Python fixture over the committed tables; operator play for the in-game half
  • A model fixture over representative score/lives pairs computes the re-tune rule (score per reserve craft, capped at 16); the build's in-game re-tune is confirmed in play (a runtime-harness candidate once the lives slice ships): Python fixture implementing the documented rule; operator play for the in-game half

Impact: this is the engine's own account of the review; your merge is the binding gate.

Note — the enemy-dependent acceptance rows above need slice 8. The three "confirm yourself" rows and the "in play" halves of the two engine rows can't be run this slice (no enemies yet); they are the spec's own deferred rows. What you can run now is in Demonstration.

Demonstration

Run the model fixtures, and watch the difficulty/formation state live on the variable monitors.

  • python3 -m unittest tests.test_spec_docs.DifficultyAndFormations -v — the model fixtures over the committed data (baked tables, formation lookup, fold-back, score re-tune, and the in-domain proof over the full dynamics). They fail on a wrong table, a wrong index rule, or an out-of-domain index; they pass here.
  • Operator playtest (PLAYTEST_CHECKLIST step 4, extended this slice): open the ai level, formation count, formation type offset, a fire mask *, and ground stop firing row watchers and fly — ai level climbs and stays below 128, the formation values change to valid waves (count 1–6), and the masks take their scheduled bytes. Read the growth rate as placeholder-driven (the DIP is a project default), not fidelity — only the mechanism is confirmed; no enemies spawn from it yet.

Impact: you can watch the difficulty director and formation selector run live against the real schedules, and run one command to see the models proven against the committed data.

Files of interest

The dispatch, the formation selector, and the four records.

  • tools/game_director.py_consume_schedule (the handler dispatch), _select_formation (the two-sided guard), _schedule_arg / _load_formation_tables, and the difficulty_reset receiver.
  • tests/test_spec_docs.py — the DifficultyAndFormations model fixtures (incl. the full-dynamics range proof) and the roadmap-evidence markers.
  • harness/lib/catalog.js — the difficulty-and-formations and fire-permission-masks live scenarios.
  • docs/spec/difficulty-and-formations.md — the locked-spec formation-index correction.
  • docs/mechanics/019022 — the DIF-01/02/03 + FORM-01 records.

Impact: the dispatch and the range fixture most determine whether the difficulty/formation state is faithful.

AI involvement

Claude Code (Opus 4.8) authored the change end to end under the engine's Build workflow; the operator chose the review depth, approved the plan and the live-state scope, and directed the locked-spec correction.

  • The AI planned the slice, wrote the dispatch/tests/records/spec-correction, and ran the plan-review and pre-submission cold passes as subagents. The operator selected Standard depth, chose "live state now" over dormant fixtures, and — when the AI escalated the locked-spec/reference contradiction mid-build — directed the correction. The operator's playtest, guardrail-ack, and merge are the binding gates.
  • Load-bearing AI judgment: the reference-faithful index model (verified against the reference assembly), the two-sided guard + full-dynamics range proof, and the honest framing of the placeholder DIP and the dormant RNG — recorded in the mechanics records and this Review section.

Impact: the mechanical checks and the operator's playtest / guardrail-ack / merge, not the AI's account, are what gate the change.

StarshipSuperjam and others added 4 commits August 15, 2026 13:49
The locked difficulty-and-formations spec described the set-formation
table index as "that offset plus the current AI level, doubled". The
reference it cites does not add the AI level on that path:

- sub_2_fn_2__set_flying_enemies indexes by the record's signed offset
  alone (sign-extended, doubled to address the 2-byte entries).
- sub_2_fn_3 (the raise record) indexes the SAME table by the raised,
  folded AI level instead — with no record offset.

The spec fused the two paths into one wrong "offset + level" formula.
Under that reading the real committed schedules drive the index to 233,
far past the table's -32..127 domain; under the reference reading, set
offsets (-26..126) and the folded AI level (0..127) both land in range.
The committed formations.json (domain -32..127, sized to the offset
range) independently corroborates the reference, so only the prose was
wrong — no data changes.

Correcting a status:locked spec — carries guardrail-ack at merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…1, FORM-01)

Wire the first behaviour into slice 6's empty schedule-dispatch seam, to the
reference-faithful model the spec correction (prior commit) restored:

- DIF-01: an `ai level` accumulates live from `raise_ai_level_and_set_formation`
  records (cabinet increment [2,0,6,16] at a project-defined DIP index 0 = +2),
  folding back below 0x80. Its own `difficulty_reset` receiver zeroes it on a
  world reset; it persists across a life.
- FORM-01: the incoming wave's `formation count` + `formation type offset` are
  selected from the normal formation table baked as two parallel 160-lists
  (index -32..127). set-formation indexes by the record offset; raise by the
  folded AI level; reset zeroes. A two-sided guard skips an out-of-domain index
  (Scratch item-of-list returns "" out of range); a range fixture proves the
  guard is a dead defensive branch under this slice's raise/set dynamics.
- A 4th parallel schedule column `schedule arg` carries the one runtime-readable
  scalar each dispatched record needs (Scratch can't parse the JSON payload).

Verification: model fixtures over the committed data (baked-table golden, full
lookup reproduction, the in-domain range proof, the fold-back math) in
test_spec_docs; the extended round-trip golden covers the new column; the
`area-scheduler` structural checks track the now-wired dispatch; a live
scratch-vm harness scenario (difficulty-and-formations) with a biting negative;
build_hash re-pinned; two builds byte-identical + round-trip. Records 019/020;
catalog DIF-01/FORM-01 -> present; playtest step 4 gains the difficulty watchers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the adjust_ai_level_from_score handler into the schedule dispatch: when an
adjust record fires it adds floor(floor(score/1000) / craft), capped at 16, to
the AI level, guarded on craft > 0. Reserve is the live `craft` count (the
reference divides by solvalou_number with no subtraction). Unlike the raise, the
reference does not fold this add back.

- `ai adjust` transient working register (machinery).
- Model fixture (test_score_retune_rule) reproduces the rule: sub-1000 floor,
  the cap at 16, the craft-zero guard.
- Fixed the PLY-02 death-decision negative fixture to target the death `craft >
  threshold` specifically (a control_if_else condition), not DIF-02's new `craft
  > 0` re-tune guard.

Record 021; catalog DIF-02 -> present; playtest step 4 notes the score-adjust
jump. build_hash re-pinned; two builds byte-identical + round-trip; harness green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the eight fire_mask_<family> handlers and ground_stop_firing_row into the
schedule dispatch: each sets its Stage variable from the schedule arg column as
its record fires. The eight branches + variables are generated from one family
table (FIRE_MASK_FAMILIES) so a family cannot be half-wired; all reset to 0 on a
world reset via difficulty_reset. Held as eight named variables (not one indexed
list) because the slice-8 consumers are per-family sprites reading one mask each.

The per-family FIRING that consumes these masks is the enemy slices' (8+).

Verification: the mask bytes round-trip via the schedule arg golden; a live
scratch-vm scenario (fire-permission-masks) confirms the masks are set to their
scheduled values, with a biting negative on the logram branch. Record 022;
catalog DIF-03 -> present.

Also adds the roadmap closure-evidence markers for leaf #56 (DIF-01/02/03/FORM-01
model obligations) to the difficulty model-fixture class, per the dependency-aware
roadmap contract merged (#36) while this slice was in flight.

build_hash re-pinned; two builds byte-identical + round-trip; full suite + harness green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@StarshipSuperjam
StarshipSuperjam force-pushed the claude/xevious-difficulty-formations branch from cd68e27 to e29f461 Compare August 15, 2026 20:55
Post-review fixes (no block-graph change — build hash unchanged, dfd51ede):
- Prove, don't assert, the formation-domain guard: the range fixture now models
  DIF-02's un-folded score adjust at its worst-case +16 over many loop cycles, so
  the "guard is dead" claim holds under the slice's FULL live dynamics, not just
  raises/sets (flagged by three review passes). Record 021 tightened to match.
- Cover all nine DIF-03 targets live: the harness fire-permission-masks scenario
  now reaches area 4 and asserts fire_mask_andor_genesis and ground_stop_firing_row
  are set (previously unexercised).
- Correct stale "later commit" comments in game_director.py and record 020 —
  DIF-02/DIF-03 ship in this same PR, not later.
- Add the DIF-03 fire-mask / ground-stop watcher to the playtest checklist.
- Align MECHANICS_CATALOG DIF-01/02/03/FORM-01 to "partial" (foundation).
- Regenerate .engine/product-spec-matrix.json (the derived settled-acceptance
  record) after the locked spec's formation acceptance-criterion was corrected —
  the engine's settled-criteria check requires it to match the spec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@StarshipSuperjam
StarshipSuperjam force-pushed the claude/xevious-difficulty-formations branch from fa1d42d to e8d284f Compare August 15, 2026 21:21
@github-actions

Copy link
Copy Markdown

These engine-internal files are regenerated by the update.

The engine rebuilds them from your project's own current state on each update. A change here that reflects a real change in your project — you edited what the file is generated from and regenerated it — is reproduced, not lost. But content typed directly into one of these files, with no matching change to the source it comes from, will not survive: it is rebuilt away on the next update. So if you meant to change what one of these records, edit the source it is generated from (for the settled-criteria record, your docs/spec/), not the file itself:

  • .engine/product-spec-matrix.json

This is a heads-up only — it does not block your merge, and your merge is the decision.

@StarshipSuperjam
StarshipSuperjam marked this pull request as ready for review August 17, 2026 23:29
@StarshipSuperjam

Copy link
Copy Markdown
Owner Author

Operator playtest approved on head commit e8d284f6729ece17e590813688b4b8802e3d069c.

Verified on the Scratch variable watchers (no enemies this slice): the AI level accumulates and folds back below 0x80, formations select a valid wave (count 1–6) with a type-offset, and the per-family fire masks + ground-stop row take their scheduled values as the area scrolls.

@StarshipSuperjam StarshipSuperjam added guardrail-ack You add this to approve a change the engine flagged as weakening a built-in safety protection. playtest-approved Operator approved the playable build at the recorded commit labels Aug 17, 2026
@StarshipSuperjam
StarshipSuperjam merged commit 5df1b60 into main Aug 18, 2026
18 of 24 checks passed
@StarshipSuperjam
StarshipSuperjam deleted the claude/xevious-difficulty-formations branch August 18, 2026 13:14
@github-project-automation github-project-automation Bot moved this from In progress to Done in Xevious Project Board Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

guardrail-ack You add this to approve a change the engine flagged as weakening a built-in safety protection. playtest-approved Operator approved the playable build at the recorded commit

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Slice 7: difficulty and formation models plus observable live state

1 participant