Offer Semuta after clean Ix-deck Amal discard
This commit is contained in:
1 parent
f0eafc175e
commit
dffbe7b9f7
9 files changed
+249
-26
No files matched your search
+77
-2
@@ -1160,6 +1160,14 @@ export type Game = {
|
||||
resume: NonNullable<Game['pendingNullentropy']>['resume'];
|
||||
stateSignature: string;
|
||||
}
|
||||
| {
|
||||
kind: 'amalDiscard';
|
||||
player: string;
|
||||
card: string;
|
||||
before: { player: string; spice: number }[];
|
||||
resume: NonNullable<Game['pendingNullentropy']>['resume'];
|
||||
stateSignature: string;
|
||||
}
|
||||
| {
|
||||
kind: 'truthtranceDiscard';
|
||||
consumed: TruthQueueEntry;
|
||||
@@ -4750,6 +4758,24 @@ function thumperDiscardSignature(g: Game, c: ThumperDiscardContinuation) {
|
||||
})),
|
||||
});
|
||||
}
|
||||
type AmalDiscardContinuation = Extract<
|
||||
NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
|
||||
{ kind: 'amalDiscard' }
|
||||
>;
|
||||
function amalDiscardSignature(g: Game, c: AmalDiscardContinuation) {
|
||||
return JSON.stringify({
|
||||
player: c.player, card: c.card, before: c.before,
|
||||
parent: nullentropyParentSignature(g, c.resume, `amal:${c.card}`),
|
||||
turn: g.turn, phase: g.phase, advanced: g.advanced, expansions: g.expansions,
|
||||
active: g.active, order: g.order, ready: g.ready, storm: g.storm,
|
||||
deck: g.deck, richeseCache: g.richeseCache, auction: g.auction,
|
||||
spice: g.spice, spiceDeck: g.spiceDeck,
|
||||
players: g.players.map(p => ({
|
||||
id: p.id, faction: p.faction, spice: p.spice, bribes: p.bribes,
|
||||
forces: p.forces, reserves: p.reserves, tanks: p.tanks,
|
||||
})),
|
||||
});
|
||||
}
|
||||
type TruthDiscardContinuation = Extract<
|
||||
NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
|
||||
{ kind: 'truthtranceDiscard' }
|
||||
@@ -5207,6 +5233,29 @@ function treacheryDiscardIntegrity(g: Game) {
|
||||
c.stateSignature === thumperDiscardSignature(g, c),
|
||||
'The used Thumper no longer matches its undrawn Spice Blow.',
|
||||
);
|
||||
} else if (continuation?.kind === 'amalDiscard') {
|
||||
const c = continuation, entry = batch.entries[0];
|
||||
requireRule(
|
||||
g.expansions.includes('ix') &&
|
||||
g.players.some(p => p.id === c.player) &&
|
||||
batch.cause === 'amal' && batch.entries.length === 1 &&
|
||||
entry.publicFace && entry.discardedBy === c.player &&
|
||||
entry.card.id === c.card && entry.card.name === 'Amal' &&
|
||||
entry.card.kind === 'special' && entry.card.effect === 'amal' &&
|
||||
c.resume && !c.resume.response && !c.resume.decision &&
|
||||
!c.resume.pendingKarama &&
|
||||
c.resume.phaseOpening &&
|
||||
Array.isArray(c.resume.phaseOpening.passed) &&
|
||||
c.resume.phaseOpening.passed.length === 0 &&
|
||||
typeof c.resume.phaseOpening.initialize === 'boolean' &&
|
||||
Array.isArray(c.before) && c.before.length === g.players.length &&
|
||||
c.before.every((row, index) => row.player === g.players[index].id &&
|
||||
Number.isSafeInteger(row.spice) && row.spice >= 0 &&
|
||||
g.players[index].spice === Math.floor(row.spice / 2)) &&
|
||||
c.stateSignature === amalDiscardSignature(g, c),
|
||||
'The used Amal no longer matches its halved spice and saved phase opening.',
|
||||
);
|
||||
suspendedControlsIntegrity(g, c.resume);
|
||||
} else if (continuation?.kind === 'truthtranceDiscard') {
|
||||
const c = continuation,
|
||||
entry = batch.entries[0],
|
||||
@@ -5823,6 +5872,7 @@ function semutaOfferSupported(
|
||||
continuation.kind !== 'distransDiscard' &&
|
||||
continuation.kind !== 'saphoMovementDiscard' &&
|
||||
continuation.kind !== 'thumperDiscard' &&
|
||||
continuation.kind !== 'amalDiscard' &&
|
||||
!(continuation.kind === 'battleCleanup' && continuation.source === 'winner') &&
|
||||
continuation.kind !== 'winnerMandatoryDiscard' &&
|
||||
continuation.kind !== 'battleResolved' &&
|
||||
@@ -5865,7 +5915,10 @@ function semutaOfferSupported(
|
||||
continuation.kind === 'winnerMandatoryDiscard' ||
|
||||
continuation.kind === 'battleResolved' ||
|
||||
(!continuation.resume.response && !continuation.resume.decision &&
|
||||
!continuation.resume.pendingKarama && !continuation.resume.phaseOpening)) &&
|
||||
!continuation.resume.pendingKarama &&
|
||||
(continuation.kind === 'amalDiscard'
|
||||
? continuation.resume.phaseOpening?.passed.length === 0
|
||||
: !continuation.resume.phaseOpening))) &&
|
||||
!g.karamaShipping && !g.auction && !g.richeseAuction && !g.battle &&
|
||||
!g.pendingExchange && !g.pendingIxAlly && !g.pendingNullentropy &&
|
||||
!g.pendingAmbassador && !g.pendingTerrorEntry && !g.pendingShipment &&
|
||||
@@ -6034,6 +6087,11 @@ function finishTreacheryDiscard(g: Game, claim?: { player: string; card: string
|
||||
g.pendingKarama = next.resume.pendingKarama;
|
||||
g.phaseOpening = next.resume.phaseOpening;
|
||||
if (g.active !== g.movementRemaining?.[0]) movementTurn(g);
|
||||
} else if (next.kind === 'amalDiscard') {
|
||||
g.response = next.resume.response;
|
||||
g.decision = next.resume.decision;
|
||||
g.pendingKarama = next.resume.pendingKarama;
|
||||
g.phaseOpening = next.resume.phaseOpening;
|
||||
} else if (next.kind === 'thumperDiscard') {
|
||||
g.response = next.resume.response;
|
||||
g.decision = next.resume.decision;
|
||||
@@ -24030,7 +24088,10 @@ function applyActionInner(
|
||||
t === 'card' && card && !action.mode,
|
||||
'Play Amal or pass the phase opening before taking other actions.',
|
||||
);
|
||||
discard(g, p, card.id);
|
||||
const before = g.semutaPreview
|
||||
? g.players.map(player => ({ player: player.id, spice: player.spice }))
|
||||
: null;
|
||||
const used = discard(g, p, card.id);
|
||||
for (const player of g.players)
|
||||
player.spice = Math.floor(player.spice / 2);
|
||||
g.phaseOpening.passed = [];
|
||||
@@ -24038,6 +24099,20 @@ function applyActionInner(
|
||||
g,
|
||||
`${p.name} played Amal. Each faction returned half its available spice to the bank, rounded up.`,
|
||||
);
|
||||
if (before) {
|
||||
const continuation: AmalDiscardContinuation = {
|
||||
kind: 'amalDiscard', player: p.id, card: used.id, before,
|
||||
resume: { response: g.response, decision: g.decision,
|
||||
pendingKarama: g.pendingKarama, phaseOpening: g.phaseOpening },
|
||||
stateSignature: '',
|
||||
};
|
||||
const entries = [{ card: used, discardedBy: p.id, publicFace: true }];
|
||||
if (semutaOfferSupported(g, continuation, entries)) {
|
||||
g.phaseOpening = null;
|
||||
continuation.stateSignature = amalDiscardSignature(g, continuation);
|
||||
stageTreacheryDiscard(g, 'amal', entries, continuation);
|
||||
}
|
||||
}
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
+5
-4
@@ -1156,7 +1156,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
: definition.card.effect === 'portableSnooper'
|
||||
? 'Choose Portable Snooper as your ordinary poison defense, or use the late-defense panel after reveal before your own traitor decision. It uses ordinary winner retention and cannot stop Poison Tooth.'
|
||||
: definition.card.effect === 'semutaDrug'
|
||||
? 'In the explicit Richese development preview, every seat sees a neutral response to supported clean public discards. A CHOAM & Richese game may also include the physical Ix deck: using Thumper then pauses before its injected worm and Spice Blow draw. Other connected sources include ordinary cards, paid Box, retired Ornithopter, private Distrans, final Truthtrance, movement-order Sapho and three separate battle batches. Commit held Semuta with a free hand slot; for several eligible cards, inspect and select exactly one only after commitment. The original effect resumes once. Nested responses, unreconciled battle rewards, other sources, full-hand sequencing and public starts remain unfinished.'
|
||||
? 'In the explicit Richese development preview, every seat sees a neutral response to supported clean public discards. A CHOAM & Richese game may include the Ix deck: Thumper pauses before its injected worm and spice draw; Amal pauses after halving everyone’s spice but before the opening resumes. Other sources include ordinary cards, paid Box, Ornithopter, Distrans, final Truthtrance, movement-order Sapho and three battle batches. Commit held Semuta with a free hand slot; for several eligible cards, inspect and select exactly one after commitment. Saved effects resume once. Nested responses, pending rewards, other sources, full-hand sequencing and public starts remain unfinished.'
|
||||
: 'Reference component only: this Richese card’s game actions are not enabled.',
|
||||
],
|
||||
...(definition.card.effect === 'semutaDrug'
|
||||
@@ -1166,7 +1166,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
area: 'Implementation' as const,
|
||||
status: 'Partial' as const,
|
||||
detail:
|
||||
'The explicit Richese profile interrupts clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, movement-order Sapho, Ix-deck Thumper and three battle discard stages. Thumper pauses before any injected worm or spice-card draw; claim or decline resumes one normal Spice Blow. A neutral offer precedes each eligible batch. Only a committed holder sees multi-card candidates from other players; Semuta transfers one card, is discarded once and resumes the saved effect once. Competing responses, pending rewards, other producers and full-hand exchange remain unfinished.',
|
||||
'The explicit Richese profile interrupts clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, movement-order Sapho, Ix-deck Thumper and Amal, and three battle discard stages. Thumper waits before injecting its worm or drawing spice. Amal has already halved each personal spice amount and reset prior opening passes; claim or decline restores the same phase opening once. A neutral offer precedes every eligible batch, owner-only committed candidates remain private, and Semuta transfers one physical card. Competing responses, pending rewards, other producers and full-hand exchange remain unfinished.',
|
||||
},
|
||||
{
|
||||
area: 'Player controls' as const,
|
||||
@@ -1184,13 +1184,13 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
area: 'Documentation' as const,
|
||||
status: 'Partial' as const,
|
||||
detail:
|
||||
'The guide distinguishes clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, movement-order Sapho, Ix-deck Thumper and initial/mandatory-winner/optional-winner battle batches from privacy-neutral acknowledgements, provisional free-slot policy and remaining discard sources.',
|
||||
'The guide distinguishes clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, movement-order Sapho, Ix-deck Thumper/Amal and three battle batches from privacy-neutral acknowledgements, provisional free-slot policy and remaining discard sources.',
|
||||
},
|
||||
{
|
||||
area: 'Verification' as const,
|
||||
status: 'Partial' as const,
|
||||
detail:
|
||||
'Genuine setup, custody, hidden-hand parity, saved JSON, authenticated SQLite CAS and legal AI participation cover clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, movement-order Sapho, Ix-deck Thumper and three battle discard stages. Pending rewards, full games and live deployment remain unverified.',
|
||||
'Genuine setup, custody, hidden-hand parity, saved JSON, authenticated SQLite CAS and legal AI participation cover clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, movement-order Sapho, Ix-deck Thumper/Amal and three battle discard stages. Pending rewards, full games and live deployment remain unverified.',
|
||||
evidence: [
|
||||
'tests/semuta-engine.test.ts',
|
||||
'tests/semuta-recovery.test.ts',
|
||||
@@ -1831,6 +1831,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Ix tables open a public confirmation window before each phase. Play Amal or pass. Everyone receives this window regardless of who holds a card. Once everyone passes, the phase begins.',
|
||||
'A pass commits until another opening card is played. Playing Amal resets the confirmations, so everyone can respond to the changed state before continuing.',
|
||||
'Automatic auctions, collection and Mentat payments wait for confirmation. Unpaid bribes are not available spice until paid at Mentat. Income from the previous phase and unused ally credit are settled before the next opening.',
|
||||
'In the explicitly opted-in Semuta preview with a physical Ix deck, a clean Amal discard pauses after the spice reduction and opening-pass reset. Another holder may claim only the used Amal card; all-seat decline leaves it discarded. Either path restores the same phase-opening confirmation window before bidding, draws or other opening effects begin.',
|
||||
'The server, phase controls and AI support this timing. Full expansion starts remain gated. Printed card timing, conflicts among simultaneous beginning-of-phase powers and later expansion economy interactions still need the complete audit.',
|
||||
],
|
||||
related: ['bidding', 'collection', 'mentat', 'card-thumper', 'ix-modules'],
|
||||
|
||||
Reference in new issue
Block a user