From 4224bb3e2a957c102af758a9517e61d2d874d36c Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Thu, 29 Oct 2026 17:52:25 +1100 Subject: [PATCH] Preserve Ghola promises during Box selection --- docs/CURRENT_STATUS.md | 8 ++++++++ docs/TRUTHTRANCE_LEADER_REVIVAL_PROMISES.md | 10 ++++++++++ game/engine.ts | 7 +++++++ tests/multi-ghola-leader-runtime.test.ts | 11 +++++++++-- 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 5df98643..566cc1e6 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -17,6 +17,14 @@ open mode gates. ## Current checkpoint and work +**Ghola promise during Box selection — 29 October. Prototyped / Partial.** +A paid Nullentropy Box selection must preserve an outstanding named Ghola Yes; +an incompatible card is rejected before shuffle while the paid search remains +open. A controlled conserved-card fixture exercises rejection and the real +Ghola replay. Eleven related files pass 51 cases; broader interactions and +deployed verification remain open. +[Contract](TRUTHTRANCE_LEADER_REVIVAL_PROMISES.md#paid-box-selection-preserves-a-ghola-answer--29-october-2026). + **Multiple named Ghola returns — 29 October. Prototyped / Partial.** Multiple distinct Yes answers can share a bounded native completion when currently held Ghola and known Box recovery permit separate physical returns. diff --git a/docs/TRUTHTRANCE_LEADER_REVIVAL_PROMISES.md b/docs/TRUTHTRANCE_LEADER_REVIVAL_PROMISES.md index fb2ee362..88c117f3 100644 --- a/docs/TRUTHTRANCE_LEADER_REVIVAL_PROMISES.md +++ b/docs/TRUTHTRANCE_LEADER_REVIVAL_PROMISES.md @@ -4,6 +4,16 @@ The [grouped follow-up](TRUTHTRANCE_REVIVAL_EXPRESSIONS.md) combines these named returns and ordinary force minima with whole AND/OR, preserving dynamic choices and the shared native planner. Scalar questions keep their existing meaning. +## Paid Box selection preserves a Ghola answer — 29 October 2026 + +**Prototyped / Partial.** Once the owner has paid for a Nullentropy Box search, +a selected recovery must preserve the shared completion of any outstanding +named Ghola Yes. An incompatible choice is rejected before the discard pile is +shuffled; the paid search remains open for a legal selection. A controlled +multi-leader fixture uses the original physical Ghola and Box and checks that +an alternative card is rejected before the actual Ghola replay. Broader +interactions, saved-game assurance and deployed verification remain open. + ## Multiple named Ghola answers — 29 October 2026 **Prototyped / Partial.** Multiple diff --git a/game/engine.ts b/game/engine.ts index 629baacd..b49b1e9b 100644 --- a/game/engine.ts +++ b/game/engine.ts @@ -5229,6 +5229,13 @@ function finishNullentropy( requireRule(!hasMovementPromises(g, player, true) || groundMovementCompletion(movementPreview, player) !== null, 'Choose a card that preserves your accepted ground-movement promises. The paid search remains available.'); + // The owner has paid for this search but has not yet committed a selection. + // A different recovered card must not consume the Box and erase the last + // owned route to an accepted named Ghola return. Quote the selected outcome + // before shuffling, with the original suspended controls restored. + requireRule(!liveGholaLeaderPromises(g.gholaLeaderPromises ?? [], player, g.turn).some(promise => promise.answer) || + !!turnActionCompletion(movementPreview, player), + 'Choose a card that preserves your named Ghola Truthtrance answers. The paid search remains available.'); // All rejection paths precede this single server shuffle. The paid record and // final shuffled order are each persisted by the room's normal CAS fence. const order = shuffle(remaining).map((c) => c.id); diff --git a/tests/multi-ghola-leader-runtime.test.ts b/tests/multi-ghola-leader-runtime.test.ts index 60ff5f1b..b2dffbef 100644 --- a/tests/multi-ghola-leader-runtime.test.ts +++ b/tests/multi-ghola-leader-runtime.test.ts @@ -98,8 +98,15 @@ void test('two named Ghola Yes answers reuse one physical card through the paid assert.ok(recovery, 'The owner receives the next Box preparation'); assert.equal(recovery.action.type, 'card'); assert.equal(recovery.action.card, f.box); game = act(game, f.target, recovery.action); - if (game.decision?.kind === 'nullentropy') - game = act(game, f.target, {type:'decision', event:viewGame(game, f.target).nullentropy!.search!.event, card:f.ghola}); + assert.equal(game.decision?.kind, 'nullentropy'); + const search = viewGame(game, f.target).nullentropy!.search!; + assert.ok(search); + const alternative = search.cards.find(card => card.id !== f.ghola); + assert.ok(alternative, 'The paid search must also contain an independently eligible non-Ghola card'); + reject(game, f.target, {type:'decision', event:search.event, card:alternative.id}); + assert.equal(viewGame(game, f.target).nullentropy!.search!.event, search.event, + 'Rejecting an evasion keeps the already-paid search available'); + game = act(game, f.target, {type:'decision', event:search.event, card:f.ghola}); game = advance(game, clean); assert.ok(player(game, f.target).hand.some(card => card.id === f.ghola)); const secondGuide = viewGame(game, f.target).gholaLeaderPromiseGuide;