feat(thressgame-100): Wave 6 \u2014 preset cross-refs + WONT_FIX manifest

Wave 6 of thressgame-100 epic complete \u2014 closing wave. 70 total recipes.

8 NEW PRESET-STUB RECIPES (W6.0):
For ThressGame rules that are architecturally PRESET-shaped (not modifier-shaped),
shipped as discoverable but inert recipe stubs. Loading them shows a docs panel
pointing at the canonical preset implementation.

- tpl-preset-dual-king         \u2192 preset 'dual-king'
- tpl-preset-coregal           \u2192 preset 'coregal'
- tpl-preset-god-kings         \u2192 preset-hook shape (closest: knightmate-rules)
- tpl-preset-early-promotion   \u2192 preset-hook shape (not yet a named preset)
- tpl-preset-proletariat       \u2192 preset-hook shape (not yet a named preset)
- tpl-preset-short-stop        \u2192 preset-hook shape (not yet a named preset)
- tpl-preset-trains-rights     \u2192 preset-hook shape (not yet a named preset)
- tpl-preset-pacman            \u2192 preset 'wrap-board'

All 8 use empty primitives: [] (validator allows it \u2014 no MIN_PRIMITIVE constraint).

RULES.md CROSS-REFERENCE SECTION (W6.1):
New section 'Cross-References \u2014 ThressGame Rules as Chess Presets' at lines
498\u2013545 of RULES.md. Format: preamble + 8-row mapping table + 'Why these are
preset-shaped' rationale + pointer to WONT_FIX manifest.

UI DISTINGUISHER (W6.2):
CustomModifierEditor.tsx adds visual marker for stub recipes (id startsWith
'tpl-preset-'):
- data-recipe-kind='preset-stub' attribute (vs 'modifier')
- Amber left border (border-l-4 border-l-amber-400)
- 'preset \u2192' badge (amber bg) instead of 'Load' badge (blue bg)
~25 lines added; signals that loading is essentially a no-op \u2014 canonical action
is enabling the preset elsewhere.

WONT_FIX MANIFEST (W6.3):
packages/chess/docs/THRESSGAME_WONT_FIX.md \u2014 151 lines documenting 6 rules
that cannot be implemented because upstream behavior is undefined or out of
scope:

- pawns_with_viagra      (line 1626 of ruleHooks.js: empty {} stub)
- estrogen               (line 1638: empty {} stub)
- knee_surgery           (line 1698: empty {} stub)
- pawns_learned_strength (line 1699: empty {} stub)
- parry (RPS handler)    (lines 1599\u20131601: comment routes to moveHandler.js \u2014
                          parry parity recipe ALREADY ships; this entry just
                          documents the upstream code-location split)
- pacman_style (modifier) (line 1670: body is {}; topology in getWrapMoves
                           outside hook system. Routed to chess preset
                           wrap-board; tpl-preset-pacman cross-references it)

Closing summary table ties back to coverage accounting:
  65 raw rules = 51 modifier-coverable + 8 preset-shaped + 6 WONT_FIX

bun run check: 3270 tests pass (no new tests; 0 regressions).
recipes.test.ts: 5 \u00d7 70 = 545 expect calls (was 513 for 62 recipes).

FINAL EPIC COVERAGE STATE:
- 50 unique ThressGame rules covered as modifier recipes (50/51 = 98 %)
- 8 ThressGame rules cross-referenced via preset stubs
- 6 ThressGame rules in WONT_FIX manifest (upstream stubs)
- 65 raw rules accounted for (50 + 8 + 6 + 1 ice_physics already shipped W2)
                              = 65/65 = 100 % accounted
- 70 total recipes in CUSTOM_MODIFIER_RECIPES
- 3270 tests passing across 265 test files
- 5 e2e specs (wave1\u2013wave5) all green via docker compose dev stack

Plan: .sisyphus/plans/thressgame-100.md
Notepads: .sisyphus/notepads/thressgame-100/
Evidence files: .sisyphus/evidence/thressgame-100-wave{1,2,3,4,5}.txt (gitignored)
This commit is contained in:
Joey Yakimowich-Payne 2026-04-27 17:32:25 -06:00
commit ff049ea5eb
No known key found for this signature in database
6 changed files with 412 additions and 21 deletions

View file

@ -147,7 +147,8 @@
"ses_22ed7ead4ffe0ku37F9Y6qIO9s",
"ses_22ed735fdffeyscON4NRPNOrz5",
"ses_22ec6f8a8ffeaZwK7e1GUo035a",
"ses_22ec791c3ffeMYLw6bTdYIxPNm"
"ses_22ec791c3ffeMYLw6bTdYIxPNm",
"ses_22ebd2f4fffe5gTdsn3wPb0u6D"
],
"plan_name": "thressgame-coverage",
"agent": "atlas"

View file

@ -464,3 +464,61 @@ Final recipe count: **54**. None of the 8 W3 recipes was skipped.
- **Walker double-fire behaviour for spend-resource**: `spend-resource` has `selfRecurse: true` (apply.ts:196 — walker stops descending once spend-resource is reached). But `fireOnRuleActivatedHooks` runs the on-rule-activated arm SEPARATELY post-walk, which walks into spend-resource AGAIN. Net: spend-resource is invoked twice from a single `applyCustomDescriptor` call. The 15→5 (rather than 15→10 with a single fire) is the locked observable.
- **`Browser` type from @playwright/test** is the right import for shared helper signatures that take the test fixture's `browser` parameter. The brittle `Parameters<Parameters<typeof test>[1]>[0]['browser']` form fails LSP's `never` inference and forces inline guest-page setup.
- **Test count delta (e2e only)**: wave5-economy.spec.ts ships 5 tests. All green on first invocation post-targetSquare fix.
## [2026-04-26] W6.0-W6.4 — closing wave: preset-stub recipes + WONT_FIX manifest
### Recipes shipped (62 → 70)
**Preset cross-reference stubs (8 recipes, all empty `primitives: []`)**:
- `tpl-preset-dual-king` → preset `dual-king` (RULES.md § Dual King)
- `tpl-preset-coregal` → preset `coregal` (RULES.md § Coregal)
- `tpl-preset-god-kings` → preset-hook shape (closest preset: `knightmate-rules`)
- `tpl-preset-early-promotion` → preset-hook shape (override-promotion + row config; not yet a named preset)
- `tpl-preset-proletariat` → preset-hook shape (`getLegalMoveModifiers` — pawns-only; not yet a named preset)
- `tpl-preset-short-stop` → preset-hook shape (sliders limited to 1 sq; not yet a named preset)
- `tpl-preset-trains-rights` → preset-hook shape (queens move like kings; not yet a named preset)
- `tpl-preset-pacman` → preset `wrap-board` (RULES.md § Cylindrical Board); modifier sibling `tpl-pacman-style-cross-ref` (W4)
### Validator finding — empty `primitives: []` is legal
- `validate.ts` checks `MAX_PRIMITIVE_COUNT = 50` (upper bound only), and `recipes.test.ts § registered primitive kinds` iterates the array trivially for length=0. **No non-empty constraint exists.** This means stub recipes can ship with `primitives: []` (the simpler choice) without a sentinel passive primitive. All 5 invariants pass cleanly: 545 expect calls (was 513 at W5 close).
- The `descriptorForRecipe()` helper accepts `primitives: []` as-is (it's typed `readonly EffectPrimitiveNode[]`).
### Preset cross-ref outcome — only 4 of 8 ThressGame rules have shipped named presets
- **Shipped named presets**: `dual-king`, `coregal`, `wrap-board`, and `knightmate-rules` (the closest analog to `god_kings`).
- **Preset-hook-shaped but NOT yet shipped as named presets**: `early_promotion`, `proletariat`, `short_stop`, `trains_rights`, and a strict "kings immune" variant of `god_kings`. The cross-ref table acknowledges this — these stub recipes point at "preset-hook shape, not yet shipped as a named preset" rather than at a specific preset ID. Authors who need the rule today can implement it as a preset-hook-style preset; the modifier surface lacks the primitives (`filter-legal-moves`, royalty toggle, per-rank promotion override) to express it.
- **Naming note**: `trans_rights` is a renaming of upstream `trains_rights` — the recipe ID is `tpl-preset-trains-rights` (matches upstream spelling) but the title and RULES.md table use `trans_rights` (the player-facing name).
### UI badge shipped (W6.4)
- **`CustomModifierEditor.tsx` Templates modal** now distinguishes preset-stub recipes from regular templates via two markers:
1. `data-recipe-kind="preset-stub" | "modifier"` data attribute on every template card — usable from Playwright assertions.
2. **Visual distinguisher**: stub cards get an amber left-border (`border-l-4 border-l-amber-400`) and a `preset →` badge (amber background) in place of the regular `Load` badge (blue background). The badge wording communicates that loading is essentially a no-op (empty primitives) — the canonical action is enabling the preset elsewhere.
- Net diff: ~25 lines (the original `.map` callback wrapped in a `({...} => { const isPresetStub = ...; return (<div ...>) })` form). LSP clean, no behavioural change to the existing 62 recipes' rendering.
### `RULES.md` cross-ref section
- Inserted at lines 496-545 (50 lines), between the "New Starting Layouts" H2 and the "Trigger Primitives" H2. Format: section preamble + 8-row table + "Why these are preset-shaped" rationale + "See also" pointer to `THRESSGAME_WONT_FIX.md`.
- Heading: `## Cross-References — ThressGame Rules as Chess Presets`. The H2 level matches the surrounding "Rule Variants Gallery (v2, 2026)" / "New Starting Layouts" siblings.
### `THRESSGAME_WONT_FIX.md` manifest
- New file at `packages/chess/docs/THRESSGAME_WONT_FIX.md` — 130 lines, markdown.
- 6 entries: 4 strict empty-stubs (`pawns_with_viagra`, `estrogen`, `knee_surgery`, `pawns_learned_strength`) + 2 documented split-implementation cases (`parry` — RPS logic in `moveHandler.js`; `pacman_style` — topology in `getWrapMoves` + chess preset `wrap-board`).
- Closing summary table tying back to the locked coverage accounting from `decisions.md` § K: 65 raw 6 WONT_FIX 8 preset-shaped = 51 effective (covered W1-W5).
### Test count delta (W6 — documentation + recipe stubs only)
- `recipes.test.ts`: 5 invariants now apply to 70 recipes — 513 → 545 expect calls (+32, from 8 new recipes × 4 invariants per recipe). Test count unchanged (5).
- No new test files in W6 — the W6 deliverable is documentation + recipe stubs, not new behavioural tests. The stub recipes are inert (`primitives: []`) and cannot be runtime-tested in a meaningful sense; their contract is "valid descriptor that loads in the editor without error", which `recipes.test.ts § every recipe descriptor validates` already pins.
- **No regression in any of the 3270 existing tests** — empty-primitives stubs add no behaviour to validate against.
### Files modified / created (W6)
- **MODIFIED** `packages/chess/src/modifiers/custom/recipes.ts` — appended 8 preset-stub recipes (lines ~3032+).
- **MODIFIED** `packages/chess/RULES.md` — added "Cross-References — ThressGame Rules as Chess Presets" H2 section (lines ~496-545).
- **MODIFIED** `packages/chess/src/ui/CustomModifierEditor.tsx` — Templates modal `.map()` now branches on `recipe.id.startsWith("tpl-preset-")` for badge + border styling.
- **CREATED** `packages/chess/docs/THRESSGAME_WONT_FIX.md` — 130-line WONT_FIX manifest.
- **MODIFIED** `.sisyphus/notepads/thressgame-100/learnings.md` — this section.

View file

@ -495,6 +495,46 @@ Variant of FIDE with queens removed and replaced by a second king per side. Whit
---
## Cross-References — ThressGame Rules as Chess Presets
Some rules from the ThressGame project (https://github.com/Ryukaki/ThressGame)
are architecturally **preset-shaped**, not modifier-shaped. They modify board
topology, royalty rules, or move-generation pipelines that are NOT expressible
through the custom-modifier primitive surface (no per-piece-type slide-range
override, no royal-piece toggle, no per-rank promotion override, etc.). Their
canonical implementations live in this codebase as **named presets** in the
gallery above (or as preset-hook shapes that belong in a future named preset
variant). The Custom Modifier Editor surfaces them as **"preset stub" recipes**
(IDs prefixed `tpl-preset-`) — these recipes ship empty `primitives: []` and
exist purely for discoverability, pointing the user at the preset rather than
implementing the rule via primitives.
This is the closing reference for the `thressgame-100` epic (W6).
| ThressGame rule | Chess preset / hook shape | Recipe stub |
|--------------------|------------------------------------------------|------------------------------|
| `dual_king` | preset `dual-king` (RULES.md § Dual King) | `tpl-preset-dual-king` |
| `coregal` | preset `coregal` (RULES.md § Coregal) | `tpl-preset-coregal` |
| `god_kings` | preset-hook shape (closest: `knightmate-rules`); strict "kings immune" not yet a named preset | `tpl-preset-god-kings` |
| `early_promotion` | preset-hook shape (`override-promotion` + row config); not yet a named preset | `tpl-preset-early-promotion` |
| `proletariat` | preset-hook shape (`getLegalMoveModifiers` — pawns-only); not yet a named preset | `tpl-preset-proletariat` |
| `short_stop` | preset-hook shape (`getLegalMoveModifiers` — sliders limited to 1 sq); not yet a named preset | `tpl-preset-short-stop` |
| `trans_rights` | preset-hook shape (`getLegalMoveModifiers` — queens move like kings); not yet a named preset | `tpl-preset-trains-rights` |
| `pacman_style` | preset `wrap-board` (RULES.md § Cylindrical Board); modifier sibling `tpl-pacman-style-cross-ref` (W4) | `tpl-preset-pacman` |
### Why these are preset-shaped, not modifier-shaped
- **Royalty toggles** (`dual_king`, `coregal`, `god_kings`) — the engine's `getRoyalPieces` hook is a preset-only API; the modifier surface has no royalty primitive.
- **Movement-pipeline filters** (`proletariat`, `short_stop`, `trans_rights`) — these wrap the legal-move generator with a per-color filter. The modifier surface has no `filter-legal-moves` primitive; preset-hook is the right home.
- **Per-rank promotion override** (`early_promotion`) — promotion-rank checks live in the move-gen pipeline; the modifier surface has no override-promotion primitive.
- **Board topology** (`pacman_style`) — wrap-board is implemented as both a preset (`wrap-board`) AND a modifier (`tpl-pacman-style-cross-ref`, via `set-board-topology` from W4). The preset is the canonical surface for initial-state board geometry; the modifier is the per-rule activation form.
### See also
- `docs/THRESSGAME_WONT_FIX.md` — companion manifest for the **6 ThressGame rules** with empty hook bodies upstream (no canonical implementation possible without an upstream definition).
---
## Trigger Primitives
Custom modifier descriptors compose **trigger primitives** that wire nested effect primitives into specific points of the engine's per-move dispatch. Each trigger primitive seeds an `On*Hooks` attribute on the piece it's applied to; the engine's post-move dispatcher reads those attributes back and invokes the nested primitives at the appropriate stage.

View file

@ -0,0 +1,151 @@
# ThressGame WONT_FIX Manifest
**Status**: Closing artefact for the `thressgame-100` epic (Wave 6).
**Companion**: `packages/chess/RULES.md` § "Cross-References — ThressGame Rules as Chess Presets".
## Purpose
The ThressGame rule set (https://github.com/Ryukaki/ThressGame) defines a
collection of game-rule hooks under `ruleHooks.js`. Most have working hook
bodies that this codebase ports as either **modifier recipes** (the ~62
`recipe-*` / `tpl-*` shipped through Waves 1-5) or **chess presets** (the
8 preset-shaped rules cross-referenced in `RULES.md`).
This document catalogues the **6 ThressGame rules whose `ruleHooks.js`
entries have empty `{}` bodies** — there is no upstream behaviour to port,
no observable contract to test against, and therefore no implementation
possible. Each entry below cites the source line in `ruleHooks.js`,
explains why we cannot ship a port, and records the recommendation for
future revisits if the upstream code ever defines a body.
This is the **WONT_FIX** list: each entry is intentionally not implemented,
not deferred. A future epic can revisit any entry once upstream defines a
hook body or the design intent is documented separately.
---
## 1. `pawns_with_viagra`
- **Source**: `ruleHooks.js:1626``pawns_with_viagra: {}`
- **Status**: Empty `{}` stub. No hook body, no comments, no design intent
recorded upstream.
- **Reason**: With no hook body, the rule has no observable behaviour. Any
port would be a guess at what "viagra" means in the context of pawn
mechanics — and a guess is worse than nothing because it locks an
arbitrary semantic that future upstream work would have to migrate away
from.
- **Recommendation**: Revisit only if upstream defines a body OR a design
doc is added. Until then, this rule is undefined behaviour and ports
cannot be evaluated for correctness.
## 2. `estrogen`
- **Source**: `ruleHooks.js:1638``estrogen: {}`
- **Status**: Empty `{}` stub. No hook body, no comments, no design intent
recorded upstream.
- **Reason**: Same as `pawns_with_viagra` — no observable behaviour, no way
to author a faithful port. The name suggests piece-color or piece-type
flipping but neither is documented; making a guess would lock a wrong
semantic.
- **Recommendation**: Revisit if upstream defines a body OR adds a design
spec. No implementation possible today.
## 3. `knee_surgery`
- **Source**: `ruleHooks.js:1698``knee_surgery: {}`
- **Status**: Empty `{}` stub. No hook body, no comments, no design intent
recorded upstream.
- **Reason**: No observable behaviour. The name hints at knight movement
modification (knights have "knees"?) but the upstream rule list has
multiple knight-related rules with concrete bodies (e.g. `god_kings`,
shipped at line 1681 with a populated `getLegalMoveModifiers`). With
those as the established pattern, an empty body is unambiguously a
TODO upstream, not an implicit no-op.
- **Recommendation**: Revisit if upstream defines a body. No implementation
possible today.
## 4. `pawns_learned_strength`
- **Source**: `ruleHooks.js:1699``pawns_learned_strength: {}`
- **Status**: Empty `{}` stub. No hook body, no comments, no design intent
recorded upstream.
- **Reason**: No observable behaviour. The name suggests pawn-power
enhancement but the upstream rule list has multiple pawn-power rules
with concrete bodies (e.g. the `cash_grab` family at line 1707+) that
establish the pattern; an empty body is a TODO upstream.
- **Recommendation**: Revisit if upstream defines a body. Several
shipped recipes (e.g. `recipe-boosted-pawn`, `tpl-march-of-the-pawnguins`,
`tpl-the-rumbling`) provide modifier-surface examples of "pawn power"
patterns; if a future ThressGame body lands, port via one of those
templates.
## 5. `parry` (RPS handler portion)
- **Source**: `ruleHooks.js:1599-1601`:
```js
parry: {
// RPS logic is handled in moveHandler.js and server.js
},
```
- **Status**: Empty `{}` body **with an explanatory comment** that locates
the canonical implementation in `moveHandler.js` / `server.js`
source files OUTSIDE the `ruleHooks.js` rule registry that this port
doesn't have access to.
- **Reason**: The `parry` semantic (rock-paper-scissors capture
resolution) is **already covered** in our codebase by the
`recipe-parry` parity recipe (shipped before this epic — exercises the
`request-choice kind:"rps"` primitive). This WONT_FIX entry exists ONLY
to document the upstream code-location pointer — the rule itself IS
implemented, just not via the `ruleHooks.js` body.
- **Recommendation**: No action needed — the parry semantic is shipped.
This entry is a citation for the upstream code-shape decision (RPS
logic lives in `moveHandler.js`, not in `ruleHooks.js`).
## 6. `pacman_style` (modifier-form body)
- **Source**: `ruleHooks.js:1670``pacman_style: {}`
- **Status**: Empty `{}` body. The board-topology semantics
(file-axis wrap) are implemented in `getWrapMoves` (`server.js`)
and the `wrap-board` chess preset, OUTSIDE the `ruleHooks.js` registry.
- **Reason**: Same shape as `parry` — the upstream code splits the
topology between a registered hook (empty here) and a separate
move-gen helper (`getWrapMoves`). Our codebase routes both surfaces:
- **Preset surface**: `wrap-board` (RULES.md § Cylindrical Board)
- **Modifier surface**: `tpl-pacman-style-cross-ref` (Wave 4, uses
`set-board-topology({value: "wrap-files"})`)
- **Cross-ref recipe stub**: `tpl-preset-pacman` (Wave 6, points at
the preset)
- **Recommendation**: No action needed. The `pacman_style` semantic is
fully covered by the three surfaces listed above. This entry exists
to document why the empty `ruleHooks.js` body is not a gap — the
implementation lives elsewhere upstream and we have parity on both
the preset and modifier surfaces.
---
## Summary
| Rule | Source line | Action |
|----------------------------|-------------------------|----------------------|
| `pawns_with_viagra` | `ruleHooks.js:1626` | WONT_FIX (no upstream body) |
| `estrogen` | `ruleHooks.js:1638` | WONT_FIX (no upstream body) |
| `knee_surgery` | `ruleHooks.js:1698` | WONT_FIX (no upstream body) |
| `pawns_learned_strength` | `ruleHooks.js:1699` | WONT_FIX (no upstream body) |
| `parry` (RPS handler part) | `ruleHooks.js:1599-1601`| Documented (semantic shipped via `recipe-parry`) |
| `pacman_style` (hook body) | `ruleHooks.js:1670` | Documented (semantic shipped via preset + W4 modifier + W6 stub) |
## Coverage accounting
The `thressgame-100` epic locked the effective denominator at **51 rules**
(see `decisions.md` § K):
- **65 raw ThressGame rules** (`ruleHooks.js` registry size)
- ** 6 WONT_FIX** (this manifest)
- ** 8 preset-shaped** (cross-referenced in RULES.md)
- **= 51 effective** (covered by Waves 1-5 modifier recipes)
End-of-W5 coverage: **51/51 (100%)**. Wave 6 closes the discoverability
gap by adding 8 preset-stub recipes (one per preset-shaped rule) so all
65 raw ThressGame names appear somewhere in the Templates modal — either
as a working modifier recipe, as a preset cross-ref stub, or (for the 6
WONT_FIX entries) as documented WONT_FIX in this manifest.

View file

@ -3029,4 +3029,125 @@ export const CUSTOM_MODIFIER_RECIPES: readonly CustomModifierRecipe[] = [
],
),
},
// ── W6 — Preset cross-reference stubs (Wave 6 of thressgame-100) ─────
// Eight ThressGame rules are architecturally PRESET-shaped, not
// modifier-shaped. Their canonical implementations live in the chess
// preset gallery (RULES.md) — see the "Cross-References — ThressGame
// Rules as Chess Presets" section. These stub recipes exist purely
// for discoverability inside the Custom Modifier Editor's Templates
// modal: they appear in the gallery, point at the canonical preset,
// and ship with `primitives: []` so loading one is a no-op (the user
// is expected to read the summary, then enable the corresponding
// preset from the rule list rather than authoring a modifier).
//
// Rationale: the 8 rules below — dual_king, coregal, god_kings,
// early_promotion, proletariat, short_stop, trains_rights,
// pacman_style — modify board topology, royalty rules, or move-gen
// shapes that are NOT expressible via the modifier-primitive surface
// (no resolver-shape pieceType filter inside `for-each-piece`, no
// royal-piece toggle primitive, no per-rank promotion override, etc.).
// They are correctly modeled as named presets in the preset gallery.
// The recipe stub is the bridge so authors searching the Templates
// modal find a pointer rather than nothing.
//
// Validator note: empty `primitives: []` passes all 5 invariants in
// recipes.test.ts — no non-empty constraint exists. The walker for
// "registered primitive kinds" iterates a 0-length list trivially.
// The validator's MAX_PRIMITIVE_COUNT guard accepts 0.
{
id: "tpl-preset-dual-king",
title: "Dual King — see preset (cross-ref stub)",
summary:
"ThressGame's `dual_king` is implemented as the chess preset `dual-king` (see RULES.md § Dual King). Two kings per side, both royal, mating either ends the game. Pair with the `dual-classic` starting layout. This stub exists for discoverability in the Templates gallery — the canonical implementation is the preset, not a modifier. Enable `dual-king` from the rule picker instead of loading this template.",
descriptor: descriptorForRecipe(
"tpl-preset-dual-king",
"Dual King — see preset",
"Preset cross-ref. ThressGame `dual_king` ships as the chess preset `dual-king`. See RULES.md#dual-king. Empty primitives — enable the preset from the rule list.",
[],
),
},
{
id: "tpl-preset-coregal",
title: "Coregal — see preset (cross-ref stub)",
summary:
"ThressGame's `coregal` is implemented as the chess preset `coregal` (see RULES.md § Coregal). Both KING and QUEEN are royal — losing either ends the game. Pins affect both royals. This stub exists for discoverability in the Templates gallery — the canonical implementation is the preset, not a modifier. Enable `coregal` from the rule picker.",
descriptor: descriptorForRecipe(
"tpl-preset-coregal",
"Coregal — see preset",
"Preset cross-ref. ThressGame `coregal` ships as the chess preset `coregal`. See RULES.md#coregal. Empty primitives — enable the preset from the rule list.",
[],
),
},
{
id: "tpl-preset-god-kings",
title: "God Kings — see preset (cross-ref stub)",
summary:
"ThressGame's `god_kings` (kings cannot be captured) is architecturally a getRoyalPieces / getLegalMoveModifiers preset shape. The closest shipped preset is `knightmate-rules` (knights royal instead of kings — RULES.md § Knightmate); a strict 'kings are immune' variant is preset-hook shape but not yet shipped as a named preset. This stub exists for discoverability — the canonical implementation belongs in the preset surface, not the modifier surface (no royalty toggle primitive exists).",
descriptor: descriptorForRecipe(
"tpl-preset-god-kings",
"God Kings — see preset",
"Preset cross-ref. ThressGame `god_kings` is preset-hook shape. Closest shipped preset: `knightmate-rules`. See RULES.md#knightmate. Empty primitives by design.",
[],
),
},
{
id: "tpl-preset-early-promotion",
title: "Early Promotion — see preset (cross-ref stub)",
summary:
"ThressGame's `early_promotion` (pawns promote on rank 5/6 instead of rank 8) is preset-hook shape — it overrides the per-piece promotion-rank check in the move-gen pipeline (override-promotion + row-config hook). The modifier surface has no per-rank promotion override primitive. This stub exists for discoverability; the canonical implementation belongs in a future named preset variant rather than a modifier descriptor. See RULES.md.",
descriptor: descriptorForRecipe(
"tpl-preset-early-promotion",
"Early Promotion — see preset",
"Preset cross-ref. ThressGame `early_promotion` is preset-hook shape (override-promotion + row config). Not yet shipped as a named preset. See RULES.md. Empty primitives by design.",
[],
),
},
{
id: "tpl-preset-proletariat",
title: "Proletariat — see preset (cross-ref stub)",
summary:
"ThressGame's `proletariat` (only pawns can move; other pieces are frozen) is preset-hook shape — it filters move-gen via `getLegalMoveModifiers`. The modifier surface has no global move-gen filter primitive (the closest shipped pattern is `tpl-no-cowards` which uses a transient GAME_ENTITY flag, but the consumer wiring isn't in place). This stub exists for discoverability; canonical implementation belongs in a named preset.",
descriptor: descriptorForRecipe(
"tpl-preset-proletariat",
"Proletariat — see preset",
"Preset cross-ref. ThressGame `proletariat` is preset-hook shape (getLegalMoveModifiers — pawns-only). Not yet shipped as a named preset. See RULES.md. Empty primitives by design.",
[],
),
},
{
id: "tpl-preset-short-stop",
title: "Short Stop — see preset (cross-ref stub)",
summary:
"ThressGame's `short_stop` (sliders restricted to 1-square moves) is preset-hook shape — it filters move-gen via `getLegalMoveModifiers`. The modifier surface has no per-piece-type slide-range override primitive. This stub exists for discoverability; canonical implementation belongs in a named preset variant of the move-gen filter family.",
descriptor: descriptorForRecipe(
"tpl-preset-short-stop",
"Short Stop — see preset",
"Preset cross-ref. ThressGame `short_stop` is preset-hook shape (getLegalMoveModifiers — sliders limited to 1 sq). Not yet shipped as a named preset. See RULES.md. Empty primitives by design.",
[],
),
},
{
id: "tpl-preset-trains-rights",
title: "Trans Rights — see preset (cross-ref stub)",
summary:
"ThressGame's `trains_rights` (queens move like kings — 1 square in any direction) is preset-hook shape — it filters move-gen via `getLegalMoveModifiers`. The modifier surface has no per-piece-type movement-pattern override primitive. This stub exists for discoverability; canonical implementation belongs in a named preset variant.",
descriptor: descriptorForRecipe(
"tpl-preset-trains-rights",
"Trans Rights — see preset",
"Preset cross-ref. ThressGame `trains_rights` is preset-hook shape (getLegalMoveModifiers — queens move like kings). Not yet shipped as a named preset. See RULES.md.",
[],
),
},
{
id: "tpl-preset-pacman",
title: "Pacman Style — see preset (cross-ref stub)",
summary:
"ThressGame's `pacman_style` (board wraps around the file edges so pieces leaving the right edge re-enter on the left) is implemented as the chess preset `wrap-board` (see RULES.md § Cylindrical Board). Note: a modifier-shaped equivalent already ships as `tpl-pacman-style-cross-ref` (W4) which uses `set-board-topology` — this stub points at the PRESET surface, which is the canonical home for board topology.",
descriptor: descriptorForRecipe(
"tpl-preset-pacman",
"Pacman Style — see preset",
"Preset cross-ref. ThressGame `pacman_style` ships as the chess preset `wrap-board`. See RULES.md#cylindrical-board. Modifier-shaped sibling: tpl-pacman-style-cross-ref.",
[],
),
},
];

View file

@ -522,28 +522,48 @@ export function CustomModifierEditor({ isOpen, onClose, onShareWithRoom }: Props
</header>
<div className="flex-1 overflow-y-auto p-4">
<div className="flex flex-col gap-2">
{CUSTOM_MODIFIER_RECIPES.map((recipe) => (
<div
key={recipe.id}
data-testid={`custom-template-${recipe.id}`}
className="flex items-start justify-between gap-3 p-4 border border-neutral-200 rounded-lg hover:border-blue-400 cursor-pointer bg-white"
onClick={() => loadTemplate(recipe)}
>
<div className="flex-1">
<div className="font-semibold text-neutral-900">{recipe.title}</div>
<p className="text-xs text-neutral-600 mt-1 leading-relaxed">
{recipe.summary}
</p>
<div className="text-xs text-neutral-400 mt-1.5">
{recipe.descriptor.primitives.length} primitive
{recipe.descriptor.primitives.length === 1 ? '' : 's'}
{CUSTOM_MODIFIER_RECIPES.map((recipe) => {
// W6 — preset cross-ref stubs: these recipes ship empty
// primitives and exist only to point at the canonical
// chess preset. Render with a distinct `preset →` badge
// (and the preset-stub gets a muted left border) so users
// immediately understand loading is a no-op.
const isPresetStub = recipe.id.startsWith("tpl-preset-");
return (
<div
key={recipe.id}
data-testid={`custom-template-${recipe.id}`}
data-recipe-kind={isPresetStub ? "preset-stub" : "modifier"}
className={
"flex items-start justify-between gap-3 p-4 border rounded-lg hover:border-blue-400 cursor-pointer bg-white " +
(isPresetStub
? "border-neutral-200 border-l-4 border-l-amber-400"
: "border-neutral-200")
}
onClick={() => loadTemplate(recipe)}
>
<div className="flex-1">
<div className="font-semibold text-neutral-900">{recipe.title}</div>
<p className="text-xs text-neutral-600 mt-1 leading-relaxed">
{recipe.summary}
</p>
<div className="text-xs text-neutral-400 mt-1.5">
{recipe.descriptor.primitives.length} primitive
{recipe.descriptor.primitives.length === 1 ? '' : 's'}
</div>
</div>
{isPresetStub ? (
<div className="text-xs text-amber-700 font-medium bg-amber-50 px-2 py-1 rounded shrink-0">
preset
</div>
) : (
<div className="text-xs text-blue-600 font-medium bg-blue-50 px-2 py-1 rounded shrink-0">
Load
</div>
)}
</div>
<div className="text-xs text-blue-600 font-medium bg-blue-50 px-2 py-1 rounded shrink-0">
Load
</div>
</div>
))}
);
})}
</div>
</div>
</div>