Connect Semuta with Nexus Kull in Basic games
Verify and publish checkpoint container / container (push) Successful in 10m47s

This commit is contained in:
Quintine committed 2026-10-30 18:40:01 +11:00
1 parent d4cd03afff
commit e697d7f558
13 files changed
+558 -24

No files matched your search

+53 -15
View File
@@ -7131,7 +7131,8 @@ function kullSemutaParentSupported(g: Game, frame?: NonNullable<Game['pendingKul
const battle = g.battle;
const use = frame?.intent.kind === 'ordinary' ? frame.intent.use : undefined;
const nativeResponse = !!frame && !g.advanced &&
(bareBasicSemutaProfile(g) || g.semutaRecruitsPreview === true && basicIndependentRecruitsSemutaProfile(g)) &&
(bareBasicSemutaProfile(g) || basicNexusKullSemutaProfile(g) ||
g.semutaRecruitsPreview === true && basicIndependentRecruitsSemutaProfile(g)) &&
use?.kind === 'cancel' && JSON.stringify(use.response) === JSON.stringify(frame.resume.response) &&
!frame.resume.decision && !frame.resume.pendingKarama && !frame.resume.phaseOpening;
let revivalSupported = false;
@@ -7200,10 +7201,29 @@ function kullSemutaDiscardSignature(g: Game, c: KullSemutaDiscardContinuation):
...(c.frame.auctionReceipt ? {auctionReceipt:c.frame.auctionReceipt, sale:g.currentAuctionSale,
round:g.richeseBidding, cache:g.richeseCache?.map(card => card.id), funding:g.richeseFunding, aid:g.aid} : {}),
restrictions: g.kullRestrictions, discard: g.discard.map(card => card.id),
...(c.frame.selection?.source === 'nexus' ? {nexusChoamHistory:g.nexusChoamHistory, nexusChoamLast:g.nexusChoamLast} : {}),
...(c.battleBinding ? {battle:g.battle} : {}),
players: g.players.map(player => ({id:player.id, hand:player.hand.map(card => card.id),
spice:player.spice, reserves:player.reserves, forces:player.forces, tanks:player.tanks})) });
}
/** A Nexus-fueled success is bound to its original physical Cunning receipt. */
function kullSemutaNexusReceiptMatches(g: Game, c: KullSemutaDiscardContinuation): boolean {
const selection = c.frame.selection;
if (c.kind !== 'kullSuccessDiscard' || selection?.source !== 'nexus' || !selection.nexusEvent) return false;
const record = g.nexusChoamHistory?.find(row => row.receipt.event === selection.nexusEvent);
if (!record || record.stage !== 'complete' || record.signature !== nexusChoamSignature(record) ||
record.receipt.owner !== c.player || record.receipt.card !== c.card || record.receipt.effect !== 'kull' ||
record.receipt.turn !== g.turn || record.receipt.phase !== g.phase ||
!g.nexusCards?.cards?.discard.includes('choam')) return false;
try {
nexusRule(() => validateNexusChoam(g, record.receipt));
const declared = JSON.parse(record.frame) as {owner?:string;card?:string;effect?:string;nexusEvent?:string};
const parent = JSON.parse(record.parent) as {kull?: {event?:string;signature?:string;selection?:unknown}};
return declared.owner === c.player && declared.card === c.card && declared.effect === 'kull' &&
declared.nexusEvent === selection.nexusEvent && parent.kull?.event === c.frame.event &&
parent.kull.signature === c.frame.signature && JSON.stringify(parent.kull.selection) === JSON.stringify(selection);
} catch { return false; }
}
function karamaResponseDiscardSignature(g: Game, c: KaramaResponseDiscardContinuation) {
const battle = c.kind === 'karamaBattleDiscard';
const worthless = c.kind === 'karamaChoamWorthlessDiscard';
@@ -8072,6 +8092,10 @@ function semutaNexusProfile(g: Game): boolean {
mixedE1E2NexusLeaderSkillsProfile(g)
: pairedNexusModulesProfile(g) || singleE1E2NexusProfile(g) || mixedE1E2NexusProfile(g)));
}
function basicNexusKullSemutaProfile(g: Game): boolean {
return g.advanced === false && g.semutaPreview === true && g.kullPreview === true &&
g.nexusKullPreview === true && nexusCoreProfile(g);
}
/** Original classic occupation or a focused Advanced Tleilax SEND profile.
* Other optional envelopes remain separate; no recorded game is redealt. */
function semutaHomeworldRoster(g: Game): boolean {
@@ -8256,11 +8280,12 @@ function treacheryDiscardIntegrity(g: Game) {
frame.event === JSON.stringify(['kull', g.turn, g.phase, g.kullSequence, frame.player, frame.owner]) &&
frame.signature === kullSignature(g, frame) &&
getPlayer(g, frame.owner).hand.some(card => card.id === frame.card) &&
batch.entries.length === 1 && entry.publicFace && entry.discardedBy === c.player &&
batch.entries.length === 1 && entry.publicFace === !(c.kind === 'kullSuccessDiscard' && frame.selection?.source === 'nexus') && entry.discardedBy === c.player &&
entry.card.id === c.card && canonicalKullCard(entry.card) &&
(c.kind === 'kullSuccessDiscard'
? c.player === frame.player && frame.selection?.source === 'printed' &&
frame.selection.card === c.card && !c.declaration && !c.use &&
? c.player === frame.player && (frame.selection?.source === 'printed' ||
basicNexusKullSemutaProfile(g) && kullSemutaNexusReceiptMatches(g, c)) &&
frame.selection?.card === c.card && !c.declaration && !c.use &&
!!g.kullRestrictions?.some(row => row.player === frame.owner && row.turn === g.turn && row.phase === g.phase)
: c.card !== frame.card && entry.card.effect === 'karama' && !!c.declaration &&
c.declaration.effect === 'kull' && c.declaration.owner === frame.player &&
@@ -9739,12 +9764,13 @@ function semutaOfferSupported(
!continuation.resume.response && !continuation.resume.decision &&
!continuation.resume.pendingKarama && !continuation.resume.phaseOpening;
if (continuation.kind === 'kullSuccessDiscard' || continuation.kind === 'kullCounterDiscard')
return (g.semutaPreview === true && bareBasicSemutaProfile(g) ||
return (g.semutaPreview === true && (bareBasicSemutaProfile(g) || basicNexusKullSemutaProfile(g)) ||
g.semutaRecruitsPreview === true && basicIndependentRecruitsSemutaProfile(g) &&
(!g.pendingTreacheryDiscard || !!g.pendingTreacheryDiscard.reaction)) && g.advanced === false &&
!!byFaction(g, 'richese') && kullSemutaParentSupported(kullSource(g, continuation.frame), continuation.frame) &&
(!!g.battle === !!continuation.battleBinding) && entries.length === 1 &&
entries[0].publicFace && entries[0].discardedBy === continuation.player &&
entries[0].publicFace === !(continuation.kind === 'kullSuccessDiscard' &&
continuation.frame.selection?.source === 'nexus') && entries[0].discardedBy === continuation.player &&
entries[0].card.id === continuation.card;
// These completed public costs retain the original ordinary lot through the
// existing signed continuation. Semuta itself has no during-bid exception;
@@ -11196,12 +11222,14 @@ 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-deck Nexus starts retain the existing native Semuta path. */
/** Fresh Basic CHOAM/optional Ix 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.length === 1 && g.expansions[0] === 'choam' && !!byFaction(g, 'richese') &&
g.players.every(p => p.faction === 'richese' || p.faction === 'choam' || CLASSIC_FACTIONS[p.faction] === true) &&
g.expansions.includes('choam') && g.expansions.length <= 2 &&
g.expansions.every(id => id === 'choam' || id === 'ix') && !!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.nexusCards && !g.homeworlds && !g.discoveryEnabled && !g.ecazTreachery && !g.leaderSkills &&
!g.techTokens && !g.strongholdCards && !g.twoPlayerControllers && !g.shortAllianceVictory &&
!g.coalitionPreview && !g.kullPreview && !g.nexusKullPreview;
@@ -12826,6 +12854,7 @@ function ixianNexusBetrayalIntegrity(g: Game): void {
const contexts = [g, g.pendingExchange, g.pendingNullentropy?.resume,
g.pendingRicheseGift?.resume, g.pendingRichesePurchaseIncome?.resume,
g.summonedWorm?.resume, g.pendingKull?.resume,
continuation?.kind === 'kullCounterDiscard' ? continuation.frame.resume : null,
continuation && 'resume' in continuation ? continuation.resume : null];
const provider = byFaction(g, 'ixians')!.id;
const ownsWindow = (kind: 'ixAuction' | 'ixTechnology') => contexts.some(context => {
@@ -13726,12 +13755,13 @@ export function initializeNexusKullGameForAudit(state: Game): Game {
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 === 'ixians' || p.faction === 'tleilaxu')),
'Nexus Kull requires a fresh CHOAM lobby with classic or Basic Ixian/Tleilaxu factions, CHOAM and Ix decks, and only Nexus Cards.');
!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.');
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;
return g;
}
/** Prototype-only Discovery setup. Public starts stay gated while remaining effects are connected. */
@@ -20193,7 +20223,8 @@ function kullCostPromisesAllow(g: Game, p: Player, card: Card, fuelSource: 'prin
}
function kullCostCards(g: Game, p: Player, source: 'printed' | 'nexus' = 'printed'): Card[] {
const original = g.pendingKull ? kullSource(g, g.pendingKull) : g;
if (semutaCardProducerEnabled(g) && (!kullSemutaParentSupported(original, g.pendingKull ?? undefined) || source !== 'printed')) return [];
if (semutaCardProducerEnabled(g) && (!kullSemutaParentSupported(original, g.pendingKull ?? undefined) ||
source !== 'printed' && !basicNexusKullSemutaProfile(g))) return [];
if (source === 'nexus' && (!g.nexusKullPreview || p.faction !== 'choam' || p.ally ||
(g.pendingKull?.selection?.source !== 'nexus' && g.nexusCards?.cards?.hands[p.id] !== 'choam')))
return [];
@@ -20273,10 +20304,11 @@ function kullIntegrity(g: Game): void {
g.expansions.length === 2 && 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 === 'ixians' || p.faction === 'tleilaxu')) &&
!g.advanced && (p.faction === 'richese' || p.faction === 'ixians' || p.faction === 'tleilaxu')) &&
!g.homeworlds && !g.leaderSkills && !g.discoveryEnabled && !g.discoveries &&
!g.discoveryStash && !g.greatMaker &&
!g.techTokens && !g.strongholdCards && !g.ecazTreachery && !g.semutaPreview &&
!g.techTokens && !g.strongholdCards && !g.ecazTreachery &&
(!g.semutaPreview || !g.advanced && !!byFaction(g, 'richese')) &&
!g.richeseBetrayalPreview && !g.richeseBetrayal && !g.pendingRicheseBetrayal,
'The Nexus Kull profile has lost its supported native inventory and modules.');
if (g.kullRestrictions !== undefined) validateKullPhaseRestrictions(g, g.kullRestrictions);
@@ -38824,7 +38856,7 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
};
g.response = null; g.decision = null; g.pendingKarama = null; g.phaseOpening = null;
continuation.stateSignature = kullSemutaDiscardSignature(g, continuation);
stageTreacheryDiscard(g, 'choam:kull', [{card:used, discardedBy:choam.id, publicFace:true}], continuation);
stageTreacheryDiscard(g, 'choam:kull', [{card:used, discardedBy:choam.id, publicFace:pending.nexusEvent === undefined}], continuation);
}
return;
}
@@ -42008,10 +42040,16 @@ function nexusChoamIntegrity(g: Game) {
const karama = context && 'pendingKarama' in context ? context.pendingKarama as Game['pendingKarama'] : null;
return [context?.response, karama?.use.kind === 'cancel' ? karama.use.response : null];
}).filter((response): response is ResponseWindow => response?.kind === 'choamWorthless');
const parkedCounter = continuation?.kind === 'kullCounterDiscard' && continuation.declaration?.nexusEvent
? {...g, pendingKull:continuation.frame, pendingChoamWorthless:continuation.declaration,
response:continuation.resume.response, decision:continuation.resume.decision,
pendingKarama:continuation.resume.pendingKarama, phaseOpening:continuation.resume.phaseOpening} : null;
const transactions = [
{ state: g, pending: g.pendingChoamWorthless, responses: responseList(controls) },
...(g.pendingKull ? [{ state: kullSource(g, g.pendingKull), pending: g.pendingKull.worthless,
responses: responseList([g.pendingKull.resume]) }] : []),
...(parkedCounter && continuation?.kind === 'kullCounterDiscard' ? [{ state: parkedCounter,
pending: continuation.declaration, responses: responseList([continuation.resume]) }] : []),
];
const declared = transactions.filter(transaction => transaction.pending?.nexusEvent);
const history = g.nexusChoamHistory;