Connect three-deck Nexus Kull and Ixian Semuta
Verify and publish checkpoint container / container (push) Successful in 10m42s
Verify and publish checkpoint container / container (push) Successful in 10m42s
This commit is contained in:
1 parent
e697d7f558
commit
a1b09c5b02
14 files changed
+643
-25
No files matched your search
+81
-11
@@ -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 &&
|
||||
|
||||
Reference in new issue
Block a user