feat: preserve native Face Dance through Semuta battle discards
This commit is contained in:
1 parent
12e4bcbf45
commit
0d545deb63
9 files changed
+191
-10
No files matched your search
@@ -17,6 +17,12 @@ casualties, cleanup and capture suffix. The capture offer samples no captive
|
||||
until its original response finishes. Other pending reward exclusions are
|
||||
unchanged; this does not certify complete Semuta or expansion combinations.
|
||||
|
||||
The same preview now admits native Tleilaxu Face Dance pending across mandatory
|
||||
and winner-card batches. Original casualties and cleanup precede the same
|
||||
private opportunity; native replacement runs once and does not undo a Semuta
|
||||
claim. Face Dancer cards remain their separate physical deck, never Treachery
|
||||
discard targets. Other pending reward exclusions are unchanged.
|
||||
|
||||
Sequence retirement precedes the remaining work. Ordinary public actions still drain these saved steps automatically. Test-only observation of unchanged private production dispatch captures real intermediate frames; this is not a public Semuta reaction window.
|
||||
|
||||
## Integrity, privacy and multiplayer
|
||||
|
||||
@@ -29,6 +29,21 @@ optional skill bands, combinations and public expansion starts remain open.
|
||||
Rules implementation continues; comprehensive assurance and administration
|
||||
remain deferred until all rules are implemented.
|
||||
|
||||
**Semuta / native Face Dance — 11 October.** Mandatory and winner battle
|
||||
discards now offer Semuta while native Tleilaxu replacement remains pending.
|
||||
Original casualties and card cleanup still precede the same private opportunity;
|
||||
one native replacement preserves the claimed Treachery card. Other reward
|
||||
exclusions are unchanged. The regression fails before and passes after.
|
||||
Types/lint and 117 affected unit cases across eight files pass.
|
||||
Twenty-four runtime scenarios cover both producers/modes, claim/pass and four
|
||||
Semuta AI profiles; eight more prove all four native Face Dance AI continuations
|
||||
in both modes. Real GameTable at 390px claims Hunter Seeker, restores the original
|
||||
reserves/sector controls and replaces three in Arrakeen10. Old winner
|
||||
reserves18/Tanks2, Tleilaxu board3/reserves12/Tanks5, claimed card still held.
|
||||
Phase7's native Ix opening remains; no premature Collection claim or overflow.
|
||||
Temporary QA scaffolds are removed; no saved store or deployed game was targeted.
|
||||
See [the source/runtime boundary](SEMUTA_SOURCE_UPDATE_20260907.md).
|
||||
|
||||
**Semuta / CHOAM final Mentat — 11 October.** Gamont disposal in the native
|
||||
final opportunity now offers Semuta, then restores the same CHOAM-owned window.
|
||||
Claim/pass returns the selected force but never checks victory or advances
|
||||
|
||||
@@ -1,5 +1,39 @@
|
||||
# Dune implementation status
|
||||
|
||||
## 11 October 2026 — native Face Dance after Semuta battle discards
|
||||
|
||||
The battle receipt already validates pending native Face Dance against the
|
||||
original winner, territory and Tleilaxu owner. Removing only its battle-reward
|
||||
exclusion admits mandatory and winner-card batches without changing native
|
||||
replacement, force sources or aftermath ordering. Other reward exclusions
|
||||
remain intact; Face Dancer cards are not Treachery targets.
|
||||
|
||||
The genuine mixed CHOAM/Ix fixture stages a matching physical Face Dancer by a
|
||||
conserved identity swap with the actual reserve/held traitor zones. The
|
||||
regression fails before and passes after, covering both discard producers,
|
||||
claim/pass, original casualties/cleanup, one replacement and winning leader
|
||||
death, card preservation after replacement and retired reaction/Face Dance
|
||||
replay rejection.
|
||||
|
||||
Twenty-four isolated runtime scenarios cover both producers, both modes,
|
||||
claim/pass and all four legal Semuta AI profiles, followed by native replacement.
|
||||
Eight further native Face Dance AI cases cover each profile in Basic/Advanced;
|
||||
all legally reveal their own matching card and replace three survivors. The old
|
||||
winner ends with reserves18/Tanks2 and no board forces; Tleilaxu holds
|
||||
board3/reserves12/Tanks5, with one winning-leader death.
|
||||
|
||||
Real production GameTable at 390px claims the winner's Hunter Seeker, restores
|
||||
the original private reserve/sector controls, then reveals the matching Face
|
||||
Dancer and replaces three in Arrakeen10. Hunter Seeker stays held by Richese.
|
||||
Phase7's original Ix opening remains; this is not premature Collection proof.
|
||||
There is no horizontal overflow. Temporary scripts, isolated server and browser
|
||||
tab are removed/stopped; no saved store or deployed game was targeted.
|
||||
Types/lint and 117 affected unit cases across eight files pass.
|
||||
|
||||
Contracts: [Semuta](SEMUTA_SOURCE_UPDATE_20260907.md),
|
||||
[battle ordering](BATTLE_DISCARD_CONTINUATIONS.md). Full Semuta/Tleilaxu,
|
||||
combinations and deployed acceptance remain open.
|
||||
|
||||
## 11 October 2026 — original final Mentat after Semuta/Gamont
|
||||
|
||||
The Gamont receipt now carries an optional final-Mentat marker and signs the
|
||||
|
||||
@@ -39,6 +39,12 @@ Harkonnen, loser and territory; winner casualties and optional card cleanup
|
||||
still precede the capture offer. Claim or decline neither samples a captive
|
||||
early nor repeats the capture. Other pending battle rewards remain gated.
|
||||
|
||||
Mandatory and winner battle discards may also pause before native Tleilaxu
|
||||
Face Dance. The same saved winner/territory/owner opportunity survives claim or
|
||||
decline; original casualties and winner cleanup still precede it. Native
|
||||
replacement runs once, and the claimed Treachery card remains held afterward.
|
||||
Face Dancer cards are not Treachery cards and never enter the fresh batch.
|
||||
|
||||
Completed ordinary Ghola disposal may now offer Semuta during battle as well.
|
||||
Revival has already committed; the receipt binds the returned leader/forces and
|
||||
original battle, including a sealed opposing plan. Claim or decline restores
|
||||
@@ -179,8 +185,8 @@ winning-card cleanup uses a separate `winnerMandatoryDiscard` batch,
|
||||
and an optional winner selection uses a third `battleCleanup` batch.
|
||||
Every supported stage has a neutral all-seat offer; committed holders
|
||||
select one fresh card, leave the others in the pile and resume exactly
|
||||
once. Native Advanced Harkonnen capture now remains pending across the offer;
|
||||
Auditor, tech, Face Dance and CHOAM rewards remain automatic without Semuta.
|
||||
once. Native Advanced Harkonnen capture and native Face Dance remain pending
|
||||
across the offer; Auditor, tech and CHOAM rewards remain automatic without Semuta.
|
||||
|
||||
The explicit Richese preview also accepts a fresh physical Ix deck.
|
||||
Clean Thumper disposal stages `thumperDiscard` before `blowSpice`
|
||||
|
||||
@@ -237,8 +237,8 @@ policy**, not a newly discovered publisher timing rule. Full-hand
|
||||
Semuta uses the explicit user-approved atomic exchange; its completed
|
||||
hand count is unchanged. Pending gifts, competing responses,
|
||||
completed Ornithopter arrivals with Ecaz Ambassador or Moritani Terror,
|
||||
pending battle rewards other than native Harkonnen capture and other typed
|
||||
producers retain their automatic continuation; Semuta cannot claim those events
|
||||
pending battle rewards other than native Harkonnen capture/Face Dance and other
|
||||
typed producers retain automatic continuation; Semuta cannot claim those events
|
||||
yet.
|
||||
The completed Box's selected
|
||||
search card stays private and its paid shuffle is not repeated
|
||||
@@ -248,6 +248,36 @@ separate [reaction privacy audit](SEMUTA_PRIVACY_REVIEW.md) explains why
|
||||
auto-closing only when nobody secretly holds the card would leak
|
||||
possession. The main game and public expansion gates remain closed.
|
||||
|
||||
**11 October 2026 — native Face Dance suffix.** Mandatory and winner
|
||||
battle discard batches now offer Semuta with native Tleilaxu Face Dance pending.
|
||||
Only that reward exclusion changed. The existing battle receipt already binds
|
||||
the pending native owner, original winner and territory. Claim/pass preserves
|
||||
casualties and winner-card cleanup before the same private opportunity; it
|
||||
cannot replace an army early, repeat replacement or move the claimed card back.
|
||||
Auditor, tech, CHOAM income and other reward exclusions remain separate.
|
||||
|
||||
The permanent regression fails before and passes after, covering both discard
|
||||
producers, claim/pass, one replacement, original leader death, force and card
|
||||
custody, and retired reaction/Face Dance replay rejection. Genuine mixed
|
||||
CHOAM/Ix setup supplies the physical cards; a conserved traitor/Face Dancer
|
||||
identity swap stages the matching winner without inventing a second card.
|
||||
|
||||
Twenty-four isolated runtime scenarios cover both producers, Basic/Advanced,
|
||||
claim/pass and four legal Semuta AI profiles, then one native replacement.
|
||||
Eight further native Face Dance AI cases cover all four profiles in both modes:
|
||||
each uses its own matching card and legally replaces three survivors. The old
|
||||
winner ends with reserves18/Tanks2 and no board group; Tleilaxu has board3,
|
||||
reserves12/Tanks5, and the winning leader has one death.
|
||||
|
||||
Real GameTable at 390px claims the winner's Hunter Seeker before restoring
|
||||
Tleilaxu's actual reserves/sector selector. Reveal Face Dancer replaces three
|
||||
in Arrakeen10 and keeps Hunter Seeker in Richese's hand. Phase7's original Ix
|
||||
opening remains; no premature collection settlement is claimed. No horizontal
|
||||
overflow. Temporary scripts/server/browser are removed or stopped; no saved
|
||||
store or deployed game was targeted. This is bounded continuation coverage,
|
||||
not complete Semuta/Tleilaxu or deployed acceptance.
|
||||
Types/lint and 117 affected unit cases across eight files pass.
|
||||
|
||||
**11 October 2026 — original final-Mentat continuation.** Gamont disposal
|
||||
inside CHOAM's final opportunity now offers Semuta too. The receipt signs an
|
||||
optional `mentat` marker and the still-pending native opportunity. Claim or
|
||||
|
||||
@@ -40,7 +40,7 @@ export function richeseCardActionBlock(card: VisibleCard): string | null {
|
||||
if (definition?.card.effect === 'distrans')
|
||||
return 'Choose a recipient and card in the Distrans transfer panel.';
|
||||
if (definition?.card.effect === 'semutaDrug')
|
||||
return 'Use the neutral fresh-discard response in the Semuta development preview. Clean ordinary-card, paid Box, retired Ornithopter (before native Intrusion), Distrans, Truthtrance, four Sapho scopes, unready pre-leader Residual Poison, printed Karama canceling CHOAM Charity, Inflation or Bene Gesserit Charity, battle-card and Ix-deck Thumper/Amal discards are connected, as are private normal-auction and Black Market Ixian allied replacements. Battle discards may pause before native Advanced Harkonnen capture; capture resumes after cleanup. A Basic Richese/Moritani CHOAM/Ecaz profile also reacts to private Sabotage and Robbery overflow after clean shipment/movement arrivals. Clean reactive Baliset and La La La pause after cost without completing the stopped move or free revival. Proactive Trip to Gamont preserves the exact ordinary/elite return or native No-Field reveal and capped recall. The approved full-hand exchange spends Semuta without increasing the completed hand. Other pending rewards, producers and nested responses remain unfinished.';
|
||||
return 'Use the neutral fresh-discard response in the Semuta development preview. Clean ordinary-card, paid Box, retired Ornithopter (before native Intrusion), Distrans, Truthtrance, four Sapho scopes, unready pre-leader Residual Poison, printed Karama canceling CHOAM Charity, Inflation or Bene Gesserit Charity, battle-card and Ix-deck Thumper/Amal discards are connected, as are private normal-auction and Black Market Ixian allied replacements. Battle discards may pause before native Advanced Harkonnen capture or Tleilaxu Face Dance; original cleanup and the same opportunity resume once. A Basic Richese/Moritani CHOAM/Ecaz profile reacts to private Sabotage and Robbery overflow after clean shipment/movement arrivals. Clean reactive Baliset and La La La pause after cost without completing the stopped move or free revival. Proactive/final-Mentat Trip to Gamont preserves the exact ordinary/elite return or native No-Field reveal and capped recall, restoring its final owner window when applicable. The approved full-hand exchange spends Semuta without increasing the completed hand. Other pending rewards, producers and nested responses remain unfinished.';
|
||||
return definition && definition.card.effect !== 'karama'
|
||||
? 'This Richese card’s effect is not implemented yet. You can inspect its rules.'
|
||||
: null;
|
||||
@@ -92,7 +92,7 @@ export function cardPresentation(card: VisibleCard): CardPresentation {
|
||||
richese.card.effect === 'juiceOfSapho'
|
||||
? 'Richese effect integration and verification are incomplete. Supported controls cover Once Around first or last while your bid remains open, bounded movement and battle-choice ordering, and aggressor during the shared pre-plan opportunity. Earlier bids stay committed. Later aggressor intervention and other phase or auction modes remain unfinished; expansion starts remain disabled.'
|
||||
: richese.card.effect === 'semutaDrug'
|
||||
? 'The explicit Semuta development preview offers neutral responses after clean public ordinary-card discards, paid Box disposal, retired Ornithopter movement (including a following Bene Gesserit Intrusion), Distrans, ordinary and queued definite Truthtrance answers, four Sapho scopes, unready pre-leader Residual Poison, printed Karama cancelling CHOAM Charity/Inflation/Bene Gesserit Charity or settling a normal-auction purchase or winning bid, a completed CHOAM sale, clean Kulon, proactive or reactive La La La/Baliset, proactive Trip to Gamont with an ordinary/elite force or concealed No-Field, accepted Jubba Cloak before storm protection, Ix-deck Thumper/Amal, three battle discard stages (including before native Advanced Harkonnen capture) and private normal-auction or Black Market Ixian ally replacements. The Basic Richese/Moritani CHOAM/Ecaz entry adds private Sabotage and Robbery overflow after clean shipment/movement arrivals, preserving the original victim/draw and arrival suffix. Commitment is owner-only; the approved full-hand exchange spends Semuta without increasing your completed hand. Other pending rewards, producers, nested responses and normal expansion starts remain unfinished.'
|
||||
? 'The explicit Semuta development preview offers neutral responses after clean public ordinary-card discards, paid Box disposal, retired Ornithopter movement (including a following Bene Gesserit Intrusion), Distrans, ordinary and queued definite Truthtrance answers, four Sapho scopes, unready pre-leader Residual Poison, printed Karama cancelling CHOAM Charity/Inflation/Bene Gesserit Charity or settling a normal-auction purchase or winning bid, a completed CHOAM sale, clean Kulon, proactive or reactive La La La/Baliset, proactive/final-Mentat Trip to Gamont with an ordinary/elite force or concealed No-Field, accepted Jubba Cloak before storm protection, Ix-deck Thumper/Amal, three battle discard stages (including before native Advanced Harkonnen capture or Tleilaxu Face Dance) and private normal-auction or Black Market Ixian ally replacements. The Basic Richese/Moritani CHOAM/Ecaz entry adds private Sabotage and Robbery overflow after clean shipment/movement arrivals, preserving the original victim/draw and arrival suffix. Commitment is owner-only; the approved full-hand exchange spends Semuta without increasing your completed hand. Other pending rewards, producers, nested responses and normal expansion starts remain unfinished.'
|
||||
: richese.card.effect === 'mirrorWeapon'
|
||||
? 'Mirror Weapon works in bounded CHOAM/Richese-deck classic-faction battles; copied Tooth and Stone choices resolve separately, and a winning physical Mirror may be kept even after copying Tooth or Artillery. This winner-retention treatment is a user-selected interpretation, not an official clarification. Combined modes and full Richese starts remain gated.'
|
||||
: richese.card.effect === 'stoneBurner'
|
||||
|
||||
+2
-2
@@ -6654,7 +6654,7 @@ function treacheryDiscardIntegrity(g: Game) {
|
||||
} else throw new RuleError('Unknown saved discard continuation.');
|
||||
}
|
||||
/** Bounded public single-card reaction after completed effects. Battle cleanup
|
||||
* may preserve native Harkonnen capture; other pending rewards remain gated.
|
||||
* may preserve native Harkonnen capture and Face Dance; other rewards remain gated.
|
||||
* Final flight arrival may resume native Intrusion, but other entry responses remain gated. */
|
||||
function semutaOfferSupported(
|
||||
g: Game,
|
||||
@@ -6703,7 +6703,7 @@ function semutaOfferSupported(
|
||||
continuation.kind === 'winnerMandatoryDiscard' ||
|
||||
continuation.kind === 'battleResolved') &&
|
||||
(g.pendingAuditor || g.pendingTech ||
|
||||
g.pendingFaceDance || g.pendingChoamBattleIncome ||
|
||||
g.pendingChoamBattleIncome ||
|
||||
g.pendingWinnerDiscards || g.moritaniRetention ||
|
||||
g.pendingIxSubstitution)) return false;
|
||||
if (continuation.kind === 'terrorDiscard' &&
|
||||
|
||||
@@ -1173,6 +1173,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
: 'Reference component only: this Richese card’s game actions are not enabled.',
|
||||
...(definition.card.effect === 'semutaDrug' ? [
|
||||
'Mandatory and winner battle discards also pause before native Advanced Harkonnen capture. Claim or Continue completes the original casualties and card cleanup before the capture offer; it does not sample a captive early or repeat capture. Other pending rewards remain outside this bounded path.',
|
||||
'Mandatory and winner battle discards also pause before native Tleilaxu Face Dance. Claim or Continue preserves the original winner’s casualties and card cleanup, then restores the same private Face Dancer opportunity. Replacement, leader death and force sources remain native; the claimed Treachery card stays with its claimant, and no Face Dancer card becomes a Semuta target.',
|
||||
'An ordinary Ghola played during battle also offers its consumed card after revival. Claim or Continue preserves the revived leader or forces and the opposing sealed plan, then normal planning resumes. It cannot revive again by resuming the receipt.',
|
||||
'A completed paid Nullentropy Box search during battle offers only the used Box. Claim or Continue preserves the two-spice payment, private recovered card, already-shuffled pile and sealed opposing plan. The recovered card may be used in normal legal planning; the search is not repeated.',
|
||||
'A clean Distrans transfer during battle also offers only the used Distrans. The private card has already reached its recipient; claim or Continue cannot repeat the gift or change a sealed plan. A recipient holding Semuta may claim the used Distrans and keep the already-transferred card.',
|
||||
|
||||
@@ -25,11 +25,11 @@ function inventory(g: Game) {
|
||||
assert.equal(new Set(ids).size, ids.length);
|
||||
return ids;
|
||||
}
|
||||
function fixture(ix = false, ixianAlly = false, advanced = false, opponent: 'emperor' | 'beneGesserit' | 'harkonnen' | 'moritani' | 'choam' = 'emperor', actor: 'atreides' | 'emperor' = 'atreides') {
|
||||
function fixture(ix = false, ixianAlly = false, advanced = false, opponent: 'emperor' | 'beneGesserit' | 'harkonnen' | 'moritani' | 'choam' | 'tleilaxu' = 'emperor', actor: 'atreides' | 'emperor' = 'atreides') {
|
||||
let g = createGame('SEMUTAQ2', newPlayer('r', 'Richese', 'richese'), advanced,
|
||||
opponent === 'moritani' ? ['choam', 'ecaz'] : ix ? ['choam', 'ix'] : ['choam']);
|
||||
joinGame(g, newPlayer('a', actor === 'emperor' ? 'Emperor' : 'Atreides', actor));
|
||||
joinGame(g, newPlayer('e', ixianAlly ? 'Ixians' : opponent === 'beneGesserit' ? 'Bene Gesserit' : opponent === 'harkonnen' ? 'Harkonnen' : opponent === 'moritani' ? 'Moritani' : opponent === 'choam' ? 'CHOAM' : 'Emperor',
|
||||
joinGame(g, newPlayer('e', ixianAlly ? 'Ixians' : opponent === 'beneGesserit' ? 'Bene Gesserit' : opponent === 'harkonnen' ? 'Harkonnen' : opponent === 'moritani' ? 'Moritani' : opponent === 'choam' ? 'CHOAM' : opponent === 'tleilaxu' ? 'Tleilaxu' : 'Emperor',
|
||||
ixianAlly ? 'ixians' : opponent));
|
||||
for (const p of g.players) { p.bot = 'Medium'; p.ready = true; }
|
||||
g = initializeSemutaGameForAudit(g);
|
||||
@@ -53,6 +53,95 @@ function fixture(ix = false, ixianAlly = false, advanced = false, opponent: 'emp
|
||||
return { g, semuta, hajr };
|
||||
}
|
||||
|
||||
void test('Semuta settles mandatory and winner discards before one native Face Dance replacement', () => {
|
||||
const { g, semuta, hajr } = fixture(true, false, true, 'tleilaxu');
|
||||
player(g, 'a').hand = [];
|
||||
g.deck.push(hajr);
|
||||
const weapon = take(g, g.deck.find(c => c.kind === 'projectile')!.id);
|
||||
const defense = take(g, g.deck.find(c => c.kind === 'snooper')!.id);
|
||||
player(g, 'a').hand.push(weapon);
|
||||
player(g, 'e').hand.push(defense);
|
||||
for (const p of g.players) Object.assign(p, { forces: {}, reserves: 20, tanks: 0 });
|
||||
for (const id of ['a', 'e']) Object.assign(player(g, id), { forces: { 'arrakeen:10': 5 }, reserves: 15 });
|
||||
const leader = player(g, 'a').leaders[0].id;
|
||||
const tleilaxu = player(g, 'e');
|
||||
if (!tleilaxu.faceDancers!.some(card => card.leader === leader && !card.revealed)) {
|
||||
const dancer = tleilaxu.faceDancers![0];
|
||||
const old = dancer.leader;
|
||||
const reserve = g.traitorReserve!.indexOf(leader);
|
||||
if (reserve >= 0) g.traitorReserve![reserve] = old;
|
||||
else {
|
||||
const holder = g.players.find(p => p.traitors.includes(leader))!;
|
||||
assert.ok(holder, 'The actual traitor identity must exist in a physical zone.');
|
||||
holder.traitors[holder.traitors.indexOf(leader)] = old;
|
||||
}
|
||||
dancer.leader = leader;
|
||||
}
|
||||
Object.assign(g, { phase: 6, active: 'a', order: ['a', 'e', 'r'],
|
||||
response: null, decision: null, phaseOpening: null, movementRemaining: [], ready: [] });
|
||||
const original = inventory(g);
|
||||
let state = applyAction(g, 'a', { type: 'chooseBattle', territory: 'arrakeen', target: 'e' });
|
||||
for (let step = 0; step < 40; step++) {
|
||||
if (state.response) {
|
||||
const actor = state.players.find(p => !state.response!.passed.includes(p.id))!;
|
||||
state = applyAction(state, actor.id, { type: 'passResponse' });
|
||||
} else if (state.battle?.preparation)
|
||||
state = applyAction(state, state.battle.preparation.owner, { type: 'declineBattlePower' });
|
||||
else if (state.decision?.kind === 'fullPlanOffer')
|
||||
state = applyAction(state, state.decision.player, { type: 'decision', decline: true });
|
||||
else break;
|
||||
}
|
||||
for (const id of ['a', 'e'])
|
||||
state = applyAction(state, id, { type: 'battlePreparationReady', event: state.battle!.preLeader!.event });
|
||||
state = applyAction(state, 'a', { type: 'battlePlan', dial: 2, support: 2, leader, weapon: weapon.id });
|
||||
state = applyAction(state, 'e', { type: 'battlePlan', dial: 0, support: 0,
|
||||
leader: player(state, 'e').leaders[0].id, defense: defense.id });
|
||||
for (const id of ['a', 'e']) state = applyAction(state, id, { type: 'traitorCall', call: false });
|
||||
assert.equal(state.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
||||
assert.equal(state.pendingFaceDance?.winner, 'a');
|
||||
const pass = (game: Game) => {
|
||||
const event = game.pendingTreacheryDiscard!.batch.event;
|
||||
for (const id of ['a', 'e', 'r']) game = applyAction(game, id, { type: 'semutaPass', event });
|
||||
return game;
|
||||
};
|
||||
const cleaned = pass(reload(state));
|
||||
assert.equal(cleaned.decision?.kind, 'battleCards');
|
||||
const winner = applyAction(reload(cleaned), 'a', { type: 'decision', discard: [weapon.id] });
|
||||
assert.equal(winner.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
||||
for (const pending of [state, winner]) {
|
||||
const event = pending.pendingTreacheryDiscard!.batch.event;
|
||||
const fresh = pending.pendingTreacheryDiscard!.batch.entries[0].card.id;
|
||||
const declined = pass(reload(pending));
|
||||
const claimed = applyAction(reload(pending), 'r', { type: 'semutaCommit', event });
|
||||
for (let resumed of [declined, claimed]) {
|
||||
const took = resumed === claimed;
|
||||
assert.deepEqual(resumed.pendingFaceDance, state.pendingFaceDance);
|
||||
if (resumed.decision?.kind === 'battleCards')
|
||||
resumed = applyAction(resumed, 'a', { type: 'decision', discard: [] });
|
||||
assert.equal(resumed.decision?.kind, 'faceDance');
|
||||
assert.equal(resumed.decision?.player, 'e');
|
||||
assert.equal(player(resumed, 'a').forces['arrakeen:10'], 3);
|
||||
resumed = applyAction(resumed, 'e', { type: 'decision', reveal: true, sources: { reserves: 3 }, sector: 10 });
|
||||
assert.equal(resumed.pendingFaceDance, null);
|
||||
assert.deepEqual(player(resumed, 'a').forces, {});
|
||||
assert.equal(player(resumed, 'a').reserves, 18);
|
||||
assert.equal(player(resumed, 'a').tanks, 2);
|
||||
assert.deepEqual(player(resumed, 'e').forces, { 'arrakeen:10': 3 });
|
||||
assert.equal(player(resumed, 'e').reserves, 12);
|
||||
assert.equal(player(resumed, 'e').tanks, 5);
|
||||
assert.equal(player(resumed, 'a').leaders.find(l => l.id === leader)!.deaths, 1);
|
||||
assert.equal(player(resumed, 'e').faceDancers!.find(c => c.leader === leader)!.revealed, true);
|
||||
assert.deepEqual(inventory(resumed), original);
|
||||
assert.equal(player(resumed, 'r').hand.some(c => c.id === fresh), took);
|
||||
assert.equal(resumed.discard.some(c => c.id === semuta.id), took);
|
||||
assert.throws(() => applyAction(resumed, 'r', { type: 'semutaCommit', event }));
|
||||
assert.throws(() => applyAction(resumed, 'e', { type: 'decision', reveal: true, sources: { reserves: 3 }, sector: 10 }));
|
||||
}
|
||||
assert.equal(player(claimed, 'r').hand.some(c => c.id === fresh), true);
|
||||
assert.equal(claimed.discard.some(c => c.id === semuta.id), true);
|
||||
}
|
||||
});
|
||||
|
||||
void test('Semuta restores closing CHOAM Mentat after Gamont before the native victory check', () => {
|
||||
const { g, semuta } = fixture(false, false, false, 'choam');
|
||||
const card = take(g, g.deck.find(c => c.name === 'Trip to Gamont')!.id);
|
||||
|
||||
Reference in new issue
Block a user