Order simultaneous Truthtrances during battle
Verify and publish checkpoint container / container (push) Successful in 11m19s
Verify and publish checkpoint container / container (push) Successful in 11m19s
This commit is contained in:
1 parent
50703664cf
commit
cdc47ed2e1
7 files changed
+201
-9
No files matched your search
@@ -17,6 +17,16 @@ open mode gates.
|
||||
|
||||
## Current checkpoint and work
|
||||
|
||||
**Sapho ordering of battle Truthtrances — 29 October. Prototyped / Partial.**
|
||||
A quiet unrevealed battle can retain its event and plans while simultaneous
|
||||
Truthtrance holders use Sapho to reorder their questions. In the supported
|
||||
Basic Semuta profile, claim or pass resumes the same signed battle and queue.
|
||||
Eleven related files pass 62 cases, including real Basic battle continuations;
|
||||
the fixture uses documented conserved Truth and Semuta card swaps. Typecheck,
|
||||
lint and build pass. Advanced,
|
||||
sealed-plan journeys, optional-module combinations and revealed battles remain
|
||||
unverified. [Scope](JUICE_OF_SAPHO_RUNTIME.md#simultaneous-truthtrance-in-battle--29-october-2026).
|
||||
|
||||
**Advanced and Basic Ecaz future-battle Truthtrance — 29 October. Prototyped / Partial.**
|
||||
The conditional native-leader question now supports Advanced games without
|
||||
Ecaz or optional modules, and Basic Ecaz games. A Basic combined battle binds
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
# Juice of Sapho: finite ordering implementation
|
||||
|
||||
## Simultaneous Truthtrance in battle — 29 October 2026
|
||||
|
||||
**Prototyped / Partial.** When simultaneous Truthtrances are declared in a
|
||||
quiet, unrevealed live battle with a recorded event, the existing public Sapho
|
||||
ordering window can reorder their groups before the first question. The actual
|
||||
battle and its plans stay in place; cards reserved by a plan or an inspected
|
||||
commitment cannot be spent. Pending preparations and battle aftermath remain
|
||||
outside this window. In the supported bare Basic Semuta preview, the Sapho
|
||||
discard binds the exact battle and reordered queue through the physical
|
||||
claim/pass reaction before questions continue. Existing pre-battle continuation
|
||||
signatures remain unchanged. The printed card and FAQ support their individual
|
||||
effects; the ordering between them remains a provisional interpretation.
|
||||
|
||||
Focused tests exercise Basic first/last and Semuta claim/pass followed by real
|
||||
questions and battle completion. They use original setup, a paid Sapho purchase,
|
||||
actual shipments and battle choice, and explicitly conserved Truth and Semuta
|
||||
card swaps. A controlled probe checks a reserved Sapho and a changed battle
|
||||
event. Advanced, sealed-plan journeys, optional modules, revealed battles and
|
||||
broader combinations are not certified by this evidence.
|
||||
|
||||
## Recovered Sapho after suspending an already-started Movement turn — 28 October 2026
|
||||
|
||||
**Prototyped / Partial.** A holder who used Last to suspend its own already
|
||||
@@ -149,8 +169,8 @@ The physical Sapho cost must retain an earlier whole-turn answer. Detached
|
||||
feasibility quotes reserve the actually queued Truth cards; they cannot be
|
||||
borrowed as generic Distrans payloads after the live window is temporarily
|
||||
removed from a forecast. This is a conservative guarantee, not a new publisher
|
||||
ruling. Other special auctions, battle, nested response windows and broader modules
|
||||
remain outside this slice. The FAQ and Sapho face establish the individual
|
||||
ruling. Other special auctions, battle at this checkpoint, nested response windows and broader modules
|
||||
remained outside this slice. The later battle section above extends that scope. The FAQ and Sapho face establish the individual
|
||||
rules; their relative order remains a provisional implementation choice.
|
||||
|
||||
Original Basic/Advanced public setup earns Sapho and reaches an unbid or an
|
||||
@@ -187,9 +207,10 @@ neutral Semuta reaction and suspends the Truth window; claim/pass restores the
|
||||
same queued event or proceeds to the first question when the last holder used
|
||||
Sapho. The full parent signature, original queue and physical card custody are
|
||||
checked without replaying the effect. The original scope excluded live auctions
|
||||
and battle; the ordinary and cache-auction extensions above now apply. Battle,
|
||||
other special auctions, active pending responses/preparations and broader Semuta/module
|
||||
profiles remain open rather than treating the restriction as printed rules. Wider repeated-card combinations, browser, deployed and complete-mode
|
||||
and battle; the ordinary and cache-auction extensions above now apply. At this
|
||||
checkpoint, battle, other special auctions, active pending responses/preparations
|
||||
and broader Semuta/module profiles remained open rather than treating the
|
||||
restriction as printed rules. See the later battle extension above. Wider repeated-card combinations, browser, deployed and complete-mode
|
||||
acceptance remain open.
|
||||
|
||||
Runtime tests use a genuine native Basic/Advanced setup and an earned Sapho,
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Rule decision index
|
||||
|
||||
## Sapho and simultaneous battle Truthtrances — 29 October 2026
|
||||
|
||||
In the provisional online ordering, a clean unrevealed battle retains its
|
||||
original event and plans while Sapho reorders simultaneous Truthtrance groups.
|
||||
The FAQ's storm order and Sapho's printed choice do not explicitly settle their
|
||||
relative precedence. [Scope](JUICE_OF_SAPHO_RUNTIME.md#simultaneous-truthtrance-in-battle--29-october-2026).
|
||||
|
||||
## Basic Ecaz conditional battle answer — 29 October 2026
|
||||
|
||||
For the provisional first-later-battle Truthtrance question, a Basic combined
|
||||
|
||||
+17
-3
@@ -1712,6 +1712,8 @@ export type Game = {
|
||||
truth: TruthWindow;
|
||||
/** Present only on new supported Richese cache continuations. */
|
||||
richeseLotEvent?: string;
|
||||
/** New battle continuations bind their original battle event and state. */
|
||||
battleEvent?: string;
|
||||
resume: NonNullable<Game['pendingNullentropy']>['resume'];
|
||||
stateSignature: string;
|
||||
}
|
||||
@@ -6351,6 +6353,7 @@ function saphoTruthOrderDiscardSignature(g: Game, c: SaphoTruthOrderDiscardConti
|
||||
// Older clean-parent receipts omit these fields; only an actual ordinary
|
||||
// auction adds a binding to its original lot and pledged funding.
|
||||
...(g.auction ? {auction:g.auction,aid:g.aid} : {}),
|
||||
...(c.battleEvent ? {battleEvent:c.battleEvent,battle:g.battle} : {}),
|
||||
...(c.richeseLotEvent ? {richeseLotEvent:c.richeseLotEvent,richeseAuction:g.richeseAuction,
|
||||
richeseBidding:g.richeseBidding,richeseFunding:g.richeseFunding,richeseCache:g.richeseCache,aid:g.aid} : {})});
|
||||
}
|
||||
@@ -8182,6 +8185,8 @@ function treacheryDiscardIntegrity(g: Game) {
|
||||
richeseCardDefinition(entry.card)?.card.effect === 'juiceOfSapho' &&
|
||||
!c.resume.response && !c.resume.decision && !c.resume.pendingKarama && !c.resume.phaseOpening &&
|
||||
(!c.richeseLotEvent || supportedRicheseTruthLot(g) && g.richeseAuction?.event === c.richeseLotEvent) &&
|
||||
(!c.battleEvent || g.phase === 6 && g.battle?.event === c.battleEvent &&
|
||||
!g.battle.revealed && !g.battle.preparation) &&
|
||||
truth && truth.question === null && (truth.stage === 'order' || truth.stage === 'ask') &&
|
||||
truth.queue.length > 0 && new Set(truth.queue.map(row => row.card)).size === truth.queue.length &&
|
||||
truth.queue.every(row => queuedTruthCardMatches(g.players.find(player => player.id === row.player)?.hand ?? [],row)) &&
|
||||
@@ -9848,6 +9853,9 @@ function semutaOfferSupported(
|
||||
(continuation.kind === 'karamaIncomeDiscard' &&
|
||||
continuation.use.response.kind === 'emperorIncome') || richeseTruthDiscard || richeseBoxDiscard || !g.richeseAuction) &&
|
||||
(richeseGiftDiscard || combatDiscard || alliedTraitorDiscard || occupyDiscard || continuation.kind === 'saphoAggressorDiscard' ||
|
||||
(continuation.kind === 'saphoTruthOrderDiscard' && !!continuation.battleEvent &&
|
||||
g.phase === 6 && g.battle?.event === continuation.battleEvent && !g.battle.revealed && !g.battle.preparation &&
|
||||
bareBasicSemutaProfile(g)) ||
|
||||
bgDiscard ||
|
||||
continuation.kind === 'specialKaramaDiscard' ||
|
||||
continuation.kind === 'residualPoisonDiscard' ||
|
||||
@@ -28284,6 +28292,11 @@ function saphoExtortionDecision(g: Game) {
|
||||
decision?.kind === 'moritaniExtortion' && decision.player === state.queue[state.cursor] &&
|
||||
decision.event === extortionEvent(state);
|
||||
}
|
||||
function saphoBattleCardReserved(g: Game, player: string, card: string): boolean {
|
||||
const battle = g.battle, plan = battle?.plans[player];
|
||||
return !!battle && (!!plan && [plan.weapon, plan.defense, plan.leader].includes(card) ||
|
||||
committedPlanElements(battle, player).some(element => element.value === card));
|
||||
}
|
||||
/** Quote the exact public parent/discard continuation on a detached copy.
|
||||
* This does not inspect whether anyone holds Semuta; it prevents offering a
|
||||
* Sapho action whose mandatory supported reaction cannot be staged. */
|
||||
@@ -28302,6 +28315,7 @@ function saphoTruthOrderSemutaSupported(g: Game,p: Player,card: string,mode: 'fi
|
||||
const c: SaphoTruthOrderDiscardContinuation = {kind:'saphoTruthOrderDiscard',player:p.id,card:used.id,
|
||||
event,revision,mode,truth:structuredClone(w),
|
||||
...(supportedRicheseTruthLot(trial) ? {richeseLotEvent:trial.richeseAuction!.event} : {}),
|
||||
...(trial.battle?.event ? {battleEvent:trial.battle.event} : {}),
|
||||
resume:{response:trial.response,decision:trial.decision,
|
||||
pendingKarama:trial.pendingKarama,phaseOpening:trial.phaseOpening},stateSignature:''};
|
||||
trial.truthtrance = null; trial.response = null; trial.decision = null;
|
||||
@@ -28315,7 +28329,7 @@ function saphoTruthOrderChoices(g: Game,p: Player): {card:string;mode:'first'|'l
|
||||
printedRolePlayBlock(g,p.id,'juiceOfSapho')) return [];
|
||||
const card = p.hand.find(row => row.id === 'richese-juice-of-sapho' && row.effect === 'juiceOfSapho');
|
||||
if (!card || !richeseCardDefinition(card) || p.hand.filter(row => row.id === card.id).length !== 1 ||
|
||||
transferCardBlock(g,p,card) || giftReserved(g,p.id,card.id)) return [];
|
||||
transferCardBlock(g,p,card) || giftReserved(g,p.id,card.id) || saphoBattleCardReserved(g,p.id,card.id)) return [];
|
||||
try { return quoteSaphoTruthOrder(w.order,w.queue,p.id).modes
|
||||
.filter(mode => saphoTruthOrderSemutaSupported(g,p,card.id,mode)).map(mode => ({card:card.id,mode})); }
|
||||
catch (error) { if (error instanceof SaphoTruthOrderError) return []; throw error; }
|
||||
@@ -28341,6 +28355,7 @@ function playSaphoTruthOrder(g: Game,p: Player,action: Action) {
|
||||
event:action.event as string,revision:action.revision as number,mode:action.mode as 'first'|'last',
|
||||
truth:structuredClone(w),
|
||||
...(supportedRicheseTruthLot(g) ? {richeseLotEvent:g.richeseAuction!.event} : {}),
|
||||
...(g.battle?.event ? {battleEvent:g.battle.event} : {}),
|
||||
resume:{response:g.response,decision:g.decision,pendingKarama:g.pendingKarama,phaseOpening:g.phaseOpening},
|
||||
stateSignature:''};
|
||||
g.truthtrance = null; g.response = null; g.decision = null; g.pendingKarama = null; g.phaseOpening = null;
|
||||
@@ -28448,8 +28463,7 @@ function saphoOptions(g: Game, p: Player): SaphoOption[] {
|
||||
p.hand.filter((c) => c.id === card.id).length !== 1 ||
|
||||
transferCardBlock(g, p, card) ||
|
||||
giftReserved(g, p.id, card.id) ||
|
||||
(g.battle?.plans[p.id] && [g.battle.plans[p.id].weapon, g.battle.plans[p.id].defense, g.battle.plans[p.id].leader].includes(card.id)) ||
|
||||
(g.battle && committedPlanElements(g.battle, p.id).some(element => element.value === card.id))
|
||||
saphoBattleCardReserved(g,p.id,card.id)
|
||||
)
|
||||
return [];
|
||||
const options: SaphoOption[] = [];
|
||||
|
||||
@@ -512,6 +512,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'During a clean native Extortion payer decision, use Sapho to take your optional choice first/last among unanswered non-Moritani players. Earlier declines, collected bank5 and the one-payment3 limit remain. The next payer event changes even when that player stays next. You may decline instead of paying.',
|
||||
'During ordered Advanced Charity, take your remaining turn first or last, then discard Sapho. An accepted current claim provisionally protects that actor while another holder reorders only later unstarted turns. At a clean boundary, the holder may pause its own claimed turn and resume last without repeating the claim or payment. Completed turns stay closed and physical First Player order is unchanged. Basic and older concurrent phases have no ordered scope.',
|
||||
'During ordered Advanced Revival, take your unfinished opportunity first or last, then discard Sapho. If another player has started, provisional queued timing lets that player finish unchanged. At a clean boundary, you may instead pause your own started turn and resume last, retaining used allowances. Pending requests must finish first. Completed turns and already-held positions cannot be repeated. Basic and older concurrent Revival phases have no ordered scope.',
|
||||
'When two or more players declare simultaneous Truthtrances in a quiet unrevealed battle, their public question-order window can use Sapho to move the holder’s group first or last. The current battle and committed plans remain in place. In the supported Basic Semuta preview, the discard and any exchange finish before the reordered questions resume.',
|
||||
'Finish pending shipment, movement and card preparations and decisions before changing order. The exact Extortion payer window is a supported exception for its own order only. Your hand panel lists available choices. Reserved cards and positions you already hold cannot be selected.',
|
||||
'Completed bids and turns stay completed. Storm order, committed funding and movement counters do not reset. Refreshing preserves the same remaining opportunities and any declared last position.',
|
||||
'In the explicitly opted-in Semuta preview, clean Once Around, movement and ordered Charity/Revival discards pause after their queues reorder; a clean between-battles chooser discard pauses after changing battle priority but before the next chooser acts; an aggressor discard pauses after changing ordinary tie priority. Charity/Revival retain their current participant, claims or requests and allowances through Semuta claim/pass. Fresh late continuations also bind revealed choices and the public final revision; old continuation bytes remain unchanged. Every seat receives the same neutral offer. Claim or decline moves only the used card or leaves it discarded; each committed queue or unresolved battle continues once. Direct combined late claim/capture proof remains open.',
|
||||
|
||||
+4
-1
@@ -1389,7 +1389,10 @@ export function resolveTruthAction(
|
||||
!g.richeseAuction.outcome && !g.currentAuctionSale && !g.auction && !g.ixAuction;
|
||||
const clean = g.status === 'playing' && g.expansions.includes('choam') && !g.response && !g.decision && !g.phaseOpening &&
|
||||
!g.pendingKarama && !g.pendingTreacheryDiscard && !g.pendingNullentropy && (!g.auction || ordinaryLot) &&
|
||||
(!g.richeseAuction || richeseCacheLot) && !g.battle && !g.pendingRevival && !g.pendingShipment &&
|
||||
(!g.richeseAuction || richeseCacheLot) &&
|
||||
(!g.battle || g.phase === 6 && !!g.battle.event && !g.battle.revealed && !g.battle.preparation &&
|
||||
!g.pendingWinnerDiscards && !g.pendingCapture && !g.pendingTech && !g.pendingFaceDance && !g.pendingIxSubstitution) &&
|
||||
!g.pendingRevival && !g.pendingShipment &&
|
||||
!g.pendingAmbassador && !g.pendingTerrorEntry && !g.pendingChoamMove &&
|
||||
!g.pendingIxMove && !g.pendingFremenMove && !g.pendingExchange && !g.pendingRicheseGift &&
|
||||
!g.karamaShipping && !g.ornithopter && !g.summonedWorm && g.wormRides.length === 0 &&
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame, type Game } from '../game/engine';
|
||||
import { TERRITORIES } from '../game/board';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { nextOccupyRichese, finishOccupyRichese } from './fixture-ecaz-occupy-richese';
|
||||
import { act, inventory, player, createSaphoTruthOrderFixture,
|
||||
truthOrderFrame as frame, playSaphoTruthOrder as play, finishTruthOrder as finish,
|
||||
finishOriginalTruthQuestions as answer, passFreshDiscard } from './fixture-sapho-truth-order';
|
||||
|
||||
const clean = (game: Game) => !game.response && !game.phaseOpening && !game.decision &&
|
||||
!game.pendingTreacheryDiscard && !game.truthtrance;
|
||||
function advance(game: Game, until: (game: Game) => boolean): Game {
|
||||
for (let i = 0; i < 200 && !until(game); i++) game = nextOccupyRichese(game);
|
||||
assert.ok(until(game), 'The native battle must reach the requested control.');
|
||||
return game;
|
||||
}
|
||||
|
||||
/** Genuine public Basic setup and paid cache purchase, then two native paid
|
||||
* shipments and the actual battle choice. The inherited fixture swaps one
|
||||
* original Truthtrance with another held card so its real Sapho holder can
|
||||
* ask; the claim supplement swaps the original cached Semuta into Atreides'
|
||||
* hand. No phase, battle, plan, answer or spice balance is installed. */
|
||||
function battleSource(mode: 'first' | 'last', semutaClaim = false) {
|
||||
const f = createSaphoTruthOrderFixture({ mode });
|
||||
let game = structuredClone(f.before);
|
||||
if (semutaClaim) {
|
||||
const claimant = player(game, 'atreides'), cache = game.richeseCache;
|
||||
assert.ok(cache && claimant.hand.length);
|
||||
const at = cache.findIndex(card => card.effect === 'semutaDrug');
|
||||
assert.ok(at >= 0);
|
||||
const [semuta] = cache.splice(at, 1), [exchange] = claimant.hand.splice(0, 1);
|
||||
claimant.hand.push(semuta); cache.push(exchange);
|
||||
}
|
||||
assert.deepEqual(inventory(game), f.originalCards);
|
||||
const site = TERRITORIES.find(t => t.id === 'the_great_flat')!;
|
||||
const sector = site.sectors.find(value => value !== game.storm);
|
||||
assert.notEqual(sector, undefined);
|
||||
while (game.phase === 5) {
|
||||
if (!clean(game)) { game = nextOccupyRichese(game); continue; }
|
||||
const actor = game.active!;
|
||||
if (actor === 'atreides' || actor === 'emperor') {
|
||||
game = act(game, actor, { type: 'ship', territory: site.id, sector, amount: 1 });
|
||||
game = advance(game, clean);
|
||||
}
|
||||
game = act(game, actor, { type: 'endMovement' });
|
||||
}
|
||||
game = advance(game, state => state.phase === 6 && clean(state) && !!state.active && !state.battle);
|
||||
const choice = viewGame(game, game.active!).battleChoices.find(row => row.territory === site.id);
|
||||
assert.ok(choice);
|
||||
game = act(game, choice.chooser, { type: 'chooseBattle', territory: site.id,
|
||||
target: choice.attacker === choice.chooser ? choice.defender : choice.attacker });
|
||||
game = advance(game, state => !!state.battle && clean(state) && !state.battle.preparation &&
|
||||
(!state.battle.preLeader || state.battle.preLeader.closed));
|
||||
const battle = structuredClone(game.battle);
|
||||
for (const actor of [f.owner, f.other]) {
|
||||
const truth = player(game, actor).hand.find(card => card.effect === 'truthtrance');
|
||||
assert.ok(truth);
|
||||
game = act(game, actor, { type: 'card', card: truth.id });
|
||||
}
|
||||
while (game.truthtrance?.stage === 'priority') {
|
||||
const seat = game.players.find(row => !game.truthtrance!.passed.includes(row.id));
|
||||
assert.ok(seat);
|
||||
game = act(game, seat.id, { type: 'truthPass' });
|
||||
}
|
||||
const current = frame(game);
|
||||
assert.deepEqual([...current.holders].sort(), [f.owner, f.other].sort());
|
||||
assert.equal(mode === 'first' ? game.truthtrance!.queue.at(-1)!.player : game.truthtrance!.queue[0].player, f.owner);
|
||||
assert.deepEqual(game.battle, battle);
|
||||
return { ...f, game, battle, semutaClaim };
|
||||
}
|
||||
|
||||
for (const mode of ['first', 'last'] as const) for (const claim of [false, true])
|
||||
void test(`Basic battle Truthtrance Sapho ${mode} survives Semuta ${claim ? 'claim' : 'pass'} and the native battle`, () => {
|
||||
const f = battleSource(mode, claim);
|
||||
const before = structuredClone(f.game.battle), order = [...f.game.order];
|
||||
const option = viewGame(f.game, f.owner).saphoTruthOrderChoices.find(row => row.card === f.card && row.mode === mode);
|
||||
assert.ok(option);
|
||||
let game = play(f.game, f.owner, f.card, mode);
|
||||
assert.ok(game.pendingTreacheryDiscard?.reaction);
|
||||
if (claim) {
|
||||
const event = game.pendingTreacheryDiscard.batch.event;
|
||||
assert.equal(viewGame(game, 'atreides').semutaReaction?.canCommit, true);
|
||||
game = act(game, 'atreides', { type: 'semutaCommit', event });
|
||||
assert.ok(player(game, 'atreides').hand.some(card => card.id === f.card));
|
||||
} else game = passFreshDiscard(game);
|
||||
assert.equal(game.pendingTreacheryDiscard, null);
|
||||
assert.deepEqual(game.battle, before, 'No battle plans, preparations or original event are replayed or changed.');
|
||||
assert.deepEqual(game.order, order);
|
||||
game = finish(game);
|
||||
assert.equal(mode === 'first' ? game.truthtrance!.queue[0].player : game.truthtrance!.queue.at(-1)!.player, f.owner);
|
||||
game = answer(game);
|
||||
assert.equal(game.truthtrance, null);
|
||||
assert.deepEqual(game.battle, before);
|
||||
const battle = game.battle!;
|
||||
for (const actor of [battle.attacker, battle.defender]) {
|
||||
const leader = player(game, actor).leaders.find(row => !row.dead && !row.usedAt);
|
||||
assert.ok(leader);
|
||||
game = act(game, actor, { type: 'battlePlan', dial: 0, support: 0, leader: leader.id });
|
||||
}
|
||||
assert.ok(game.battle?.revealed);
|
||||
game = finishOccupyRichese(game);
|
||||
assert.equal(game.battle, null);
|
||||
assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
|
||||
void test('All minimal bot profiles can use the Basic battle Truth-order window legally', () => {
|
||||
const f = battleSource('first');
|
||||
for (const difficulty of DIFFICULTIES) for (const actor of [f.owner, f.other]) {
|
||||
const view = viewGame(f.game, actor);
|
||||
view.players.find(row => row.id === actor)!.bot = difficulty;
|
||||
const action = botActions(view)[0];
|
||||
assert.ok(action && (action.type === 'truthOrderPass' || action.type === 'saphoTruthOrder'));
|
||||
const game = act(f.game, actor, action);
|
||||
assert.deepEqual(game.battle, f.battle);
|
||||
}
|
||||
});
|
||||
|
||||
void test('Battle Sapho continuation rejects a changed battle and keeps committed cards reserved', () => {
|
||||
const f = battleSource('first');
|
||||
// Controlled reservation probe: put the actual Sapho into the owner's
|
||||
// sealed weapon slot to exercise the shared reservation guard directly.
|
||||
const reserved = structuredClone(f.game);
|
||||
reserved.battle!.plans[f.owner] = { leader: null, dial: 0, support: 0,
|
||||
weapon: f.card, defense: null };
|
||||
assert.deepEqual(viewGame(reserved, f.owner).saphoTruthOrderChoices, []);
|
||||
|
||||
const pending = play(f.game, f.owner, f.card, 'first');
|
||||
assert.ok(pending.pendingTreacheryDiscard?.reaction);
|
||||
const changed = structuredClone(pending);
|
||||
changed.battle!.event = 'different-battle';
|
||||
const snapshot = structuredClone(changed);
|
||||
assert.throws(() => passFreshDiscard(changed));
|
||||
assert.deepEqual(changed, snapshot);
|
||||
});
|
||||
Reference in new issue
Block a user