Connect three-deck Nexus Kull and Ixian Semuta
Verify and publish checkpoint container / container (push) Successful in 10m42s

This commit is contained in:
Quintine committed 2026-10-30 18:50:24 +11:00
1 parent e697d7f558
commit a1b09c5b02
14 files changed
+643 -25

No files matched your search

+81 -11
View File
@@ -2074,6 +2074,11 @@ export type Game = {
sale: NonNullable<Game['currentAuctionSale']>;
auctionIndex: number | null;
auctionEvent: string | null;
}
| {
kind: 'nexusIxianReplacementDiscard';
source: IxianReplacementSource;
stateSignature: string;
};
} | null;
ornithopter?: {
@@ -8294,6 +8299,19 @@ function treacheryDiscardIntegrity(g: Game) {
c.use.response.owner === frame.player && c.use.response.recipient === frame.owner &&
JSON.stringify(c.resume.response) === JSON.stringify(c.use.response)),
'The saved Kull discard has lost its original cost or suspended response.');
} else if (continuation?.kind === 'nexusIxianReplacementDiscard') {
const c = continuation, source = c.source, sale = g.currentAuctionSale, entry = batch.entries[0];
requireRule(g.semutaPreview === true && !g.advanced && g.phase === 3 &&
!g.pendingNexusIxianReplacement && !g.pendingNexusIxianSpecialPurchase &&
!!g.nexusIxianReplacementCursor && !!g.nexusCards?.cards && g.nexusCards.cards.discard.includes('ixians') &&
sale && sale.winner === source.buyer && sale.amount === source.amount && sale.free === source.free &&
source.turn === g.turn && source.sequence === g.nexusIxianReplacementCursor.sequence &&
batch.cause === 'nexusIxianReplacement' && batch.entries.length === 1 &&
!entry.publicFace && entry.discardedBy === source.buyer &&
JSON.stringify(entry.card) === JSON.stringify(source.card) &&
c.stateSignature === nexusIxianReplacementDiscardSignature(g, c),
'The Ixian Nexus discard lost its committed purchase or replacement.');
nexusRule(() => closeIxianReplacementSource(source, 'use', source.card, g.nexusIxianReplacementCursor!));
} else if (continuation?.kind === 'nullentropyDiscard') {
const c = continuation;
const owner = g.players.find((p) => p.id === c.player);
@@ -9745,6 +9763,15 @@ function semutaOfferSupported(
entries.length === 1 && entries[0].publicFace && entries[0].discardedBy === continuation.player &&
entries[0].card.id === continuation.card && entries[0].card.effect === 'juiceOfSapho';
}
if (continuation.kind === 'nexusIxianReplacementDiscard') {
const source = continuation.source, sale = g.currentAuctionSale;
return g.semutaPreview === true && !g.advanced && !!g.nexusCards && g.phase === 3 &&
!!sale && sale.winner === source.buyer && sale.amount === source.amount && sale.free === source.free &&
!g.pendingNexusIxianReplacement && !g.pendingNexusIxianSpecialPurchase &&
(!g.pendingTreacheryDiscard || !!g.pendingTreacheryDiscard.reaction) &&
entries.length === 1 && !entries[0].publicFace && entries[0].discardedBy === source.buyer &&
JSON.stringify(entries[0].card) === JSON.stringify(source.card);
}
if (continuation.kind === 'saphoArrivalOrderDiscard') {
const order = g.pendingArrivalOverlap?.saphoOrder;
return g.semutaPreview === true && bareBasicSemutaProfile(g) && g.phase === 5 &&
@@ -10650,6 +10677,17 @@ function finishTreacheryDiscard(g: Game, claim?: { player: string; card: string
g.phaseOpening = next.resume.phaseOpening;
if (next.source === 'move') finishMovedGroup(g, next.movement!);
else if (next.source === 'end') finishMovementTurn(g, next.flight.player);
} else if (next.kind === 'nexusIxianReplacementDiscard') {
const buyer = getPlayer(g, next.source.buyer), card = draw(g);
requireRule(card && g.nexusIxianReplacementCursor && g.nexusIxianReplacementHistory,
'The committed Ixian Nexus discard requires its original replacement draw.');
buyer.hand.push(card);
const closed = nexusRule(() => closeIxianReplacementSource(next.source,'use',card,g.nexusIxianReplacementCursor!));
g.nexusIxianReplacementHistory.push(closed.receipt);
g.nexusIxianReplacementCursor = closed.cursor;
log(g,`${buyer.name} spent Ixian Nexus Secret Ally to discard the purchased Treachery Card and draw a private replacement.`,
{faction:buyer.faction,name:'Nexus purchased-card replacement'});
continueAuctionSale(g,next.source.free);
} else if (next.kind === 'ixAllyCard') {
const buyer = getPlayer(g, next.player);
const card = draw(g);
@@ -11222,14 +11260,15 @@ function publicSemutaDiscoveryProfile(g: Game): boolean {
!g.homeworlds && !g.nexusCards && g.discoveryEnabled === true && !g.ecazTreachery &&
!g.leaderSkills && !g.kullPreview && !g.nexusKullPreview && discoveryModuleProfile(g);
}
/** Fresh Basic CHOAM/optional Ix Nexus starts retain the native Semuta path. */
/** Fresh Basic selected-deck Nexus starts retain the native Semuta path. */
function publicSemutaNexusProfile(g: Game): boolean {
return g.status === 'lobby' && g.advanced === false &&
g.players.length >= 2 && g.players.length <= 6 &&
g.expansions.includes('choam') && g.expansions.length <= 2 &&
g.expansions.every(id => id === 'choam' || id === 'ix') && !!byFaction(g, 'richese') &&
g.expansions.includes('choam') && g.expansions.length <= 3 &&
g.expansions.every(id => id === 'choam' || id === 'ix' || id === 'ecaz') && !!byFaction(g, 'richese') &&
g.players.every(p => p.faction === 'richese' || p.faction === 'choam' ||
g.expansions.includes('ix') && (p.faction === 'ixians' || p.faction === 'tleilaxu') || CLASSIC_FACTIONS[p.faction] === true) &&
g.expansions.includes('ix') && (p.faction === 'ixians' || p.faction === 'tleilaxu') ||
g.expansions.includes('ecaz') && (p.faction === 'ecaz' || p.faction === 'moritani') || CLASSIC_FACTIONS[p.faction] === true) &&
!!g.nexusCards && !g.homeworlds && !g.discoveryEnabled && !g.ecazTreachery && !g.leaderSkills &&
!g.techTokens && !g.strongholdCards && !g.twoPlayerControllers && !g.shortAllianceVictory &&
!g.coalitionPreview && !g.kullPreview && !g.nexusKullPreview;
@@ -12974,6 +13013,17 @@ function ixianNexusReplacementParent(g: Game): string {
purchaseIncome:g.pendingRichesePurchaseIncome } : {}),
decision:g.decision,response:g.response,phaseOpening:g.phaseOpening});
}
type NexusIxianReplacementDiscardContinuation = Extract<
NonNullable<Game['pendingTreacheryDiscard']>['continuation'], {kind:'nexusIxianReplacementDiscard'}
>;
function nexusIxianReplacementDiscardSignature(g: Game, c: NexusIxianReplacementDiscardContinuation): string {
return JSON.stringify({source:c.source, turn:g.turn, phase:g.phase, active:g.active,
auction:g.auction, sale:g.currentAuctionSale, richeseAuction:g.richeseAuction,
richeseBidding:g.richeseBidding, richeseCache:g.richeseCache, richeseRemoved:g.richeseRemoved,
deck:g.deck, discard:g.discard, nexus:g.nexusCards, cursor:g.nexusIxianReplacementCursor,
history:g.nexusIxianReplacementHistory, aid:g.aid,
players:g.players.map(p => ({id:p.id,faction:p.faction,ally:p.ally,spice:p.spice,hand:p.hand}))});
}
function ixianNexusReplacementContext(g: Game) {
requireRule(g.pendingNexusIxianSpecialPurchase || g.auction || g.richeseAuction,
'The Nexus purchased-card choice lost its original purchase.');
@@ -13076,7 +13126,20 @@ function decideIxianNexusReplacement(g: Game,id: string,action: Action): void {
// All original-source/custody checks precede either the singleton cost or RNG.
g.nexusCards.cards = nexusRule(() => discardNexusCard(g.nexusCards!.cards!,id,g.players));
const buyer = getPlayer(g,id);
discard(g,buyer,source.card.id);
const used = discard(g,buyer,source.card.id);
if (g.semutaPreview === true && !g.advanced && !special) {
g.pendingNexusIxianReplacement = null;
g.decision = null;
const continuation: NexusIxianReplacementDiscardContinuation = {
kind:'nexusIxianReplacementDiscard', source:structuredClone(source), stateSignature:'',
};
const entries = [{card:used, discardedBy:id, publicFace:false}];
if (semutaOfferSupported(g, continuation, entries)) {
continuation.stateSignature = nexusIxianReplacementDiscardSignature(g, continuation);
stageTreacheryDiscard(g, 'nexusIxianReplacement', entries, continuation);
return;
}
}
replacement = draw(g) ?? null;
requireRule(replacement, 'The purchased-card discard must supply a replacement draw.');
buyer.hand.push(replacement);
@@ -13703,10 +13766,13 @@ export function initializeSemutaGameForAudit(state: Game, skills = false): Game
const advancedEcaz = bareAdvancedEcazSemutaProfile(state);
const selectedSkillsDiscovery = skills && state.discoveryEnabled === true && !state.homeworlds &&
selectedDiscoveryWithoutCardsProfile(state);
const basicSelectedNexus = !state.advanced && !skills && !!state.nexusCards && nexusCoreProfile(state) &&
!state.homeworlds && !state.discoveryEnabled && !state.ecazTreachery && !state.leaderSkills &&
!state.techTokens && !state.strongholdCards;
requireRule(!advancedEcaz || !skills, 'Advanced Ecaz Semuta excludes Leader Skills and other optional modules.');
requireRule(!state.semutaPreview &&
state.expansions.includes('choam') &&
(state.expansions.every(id => id === 'choam' || id === 'ix') || basicNative || advancedEcaz || selectedSkillsDiscovery) &&
(state.expansions.every(id => id === 'choam' || id === 'ix') || basicNative || advancedEcaz || selectedSkillsDiscovery || basicSelectedNexus) &&
state.players.some(p => p.faction === 'richese'),
'Semuta needs a fresh Richese faction game: bare Basic selected native families, bare Advanced Ecaz/Richese, or CHOAM with optional Ix and supported original modules/skills; separate classic or Advanced Tleilax Homeworld occupation has no other modules. Other overlays remain excluded.');
if (state.discoveryEnabled) requireRule(selectedSkillsDiscovery || (!basicNative && !skills && !state.homeworlds &&
@@ -13751,17 +13817,19 @@ export function initializeNexusKullGameForAudit(state: Game): Game {
!state.discoveryEnabled && !state.discoveries && !state.discoveryStash && !state.greatMaker &&
!state.techTokens && !state.strongholdCards && !state.ecazTreachery &&
state.nexusCards?.cards === null && state.nexusCards.phase === null &&
typeof state.advanced === 'boolean' && state.expansions.length === 2 &&
typeof state.advanced === 'boolean' && (state.expansions.length === 2 || !state.advanced && state.expansions.length === 3 && state.expansions.includes('ecaz')) &&
state.expansions.includes('choam') && state.expansions.includes('ix') &&
state.players.some(p => p.faction === 'choam') &&
state.players.every(p => p.faction === 'choam' || faction(p.faction).expansion === 'base' ||
!state.advanced && (p.faction === 'richese' || p.faction === 'ixians' || p.faction === 'tleilaxu')),
'Nexus Kull requires a fresh CHOAM lobby with classic or Basic Richese/Ixian/Tleilaxu factions, CHOAM and Ix decks, and only Nexus Cards.');
!state.advanced && (p.faction === 'richese' || p.faction === 'ixians' || p.faction === 'tleilaxu' ||
state.expansions.includes('ecaz') && (p.faction === 'ecaz' || p.faction === 'moritani'))),
'Nexus Kull requires a fresh supported CHOAM lobby with selected faction decks and only Nexus Cards.');
requireFreshBaseRuntime(state);
const g = initializeSetupGameForAudit(state, false, true, false, false, false, false, true);
g.kullPreview = true;
g.nexusKullPreview = true;
if (!g.advanced && byFaction(g, 'richese')) g.semutaPreview = true;
if (!g.advanced && byFaction(g, 'ecaz')) g.ecazOccupyPreview = true;
return g;
}
/** Prototype-only Discovery setup. Public starts stay gated while remaining effects are connected. */
@@ -20301,10 +20369,12 @@ function kullIntegrity(g: Game): void {
try {
requireRule(g.nexusKullPreview === undefined || g.nexusKullPreview === true &&
g.kullPreview === true && !!g.nexusCards?.cards &&
g.expansions.length === 2 && g.expansions.includes('choam') && g.expansions.includes('ix') &&
(g.expansions.length === 2 || !g.advanced && g.expansions.length === 3 && g.expansions.includes('ecaz')) &&
g.expansions.includes('choam') && g.expansions.includes('ix') &&
g.players.some(p => p.faction === 'choam') &&
g.players.every(p => p.faction === 'choam' || faction(p.faction).expansion === 'base' ||
!g.advanced && (p.faction === 'richese' || p.faction === 'ixians' || p.faction === 'tleilaxu')) &&
!g.advanced && (p.faction === 'richese' || p.faction === 'ixians' || p.faction === 'tleilaxu' ||
g.expansions.includes('ecaz') && (p.faction === 'ecaz' || p.faction === 'moritani'))) &&
!g.homeworlds && !g.leaderSkills && !g.discoveryEnabled && !g.discoveries &&
!g.discoveryStash && !g.greatMaker &&
!g.techTokens && !g.strongholdCards && !g.ecazTreachery &&