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;
+3 -3
View File
@@ -103,13 +103,13 @@ function basicNativeNexusCore(game: NexusCoreConfiguration): boolean {
game.homeworlds || game.leaderSkills || game.techTokens || game.strongholdCards ||
game.ecazTreachery || game.discoveryEnabled || game.discoveries || game.discoveryStash || game.greatMaker ||
game.advancedPreview || game.richeseBetrayalPreview) return false;
if (game.semutaPreview && (game.semutaPreview !== true || game.kullPreview || game.nexusKullPreview ||
if (game.semutaPreview && (game.semutaPreview !== true ||
!game.expansions.includes('choam') || !players.some(player => player.faction === 'richese'))) return false;
if (game.kullPreview || game.nexusKullPreview) {
if (game.kullPreview !== true || game.nexusKullPreview !== true || game.semutaPreview ||
if (game.kullPreview !== true || game.nexusKullPreview !== true ||
game.expansions.length !== 2 || !game.expansions.includes('ix') || !game.expansions.includes('choam') ||
!players.some(player => player.faction === 'choam') ||
players.some(player => player.faction !== 'choam' && player.faction !== 'ixians' && player.faction !== 'tleilaxu' &&
players.some(player => player.faction !== 'choam' && player.faction !== 'richese' && player.faction !== 'ixians' && player.faction !== 'tleilaxu' &&
!FACTIONS.some(faction => faction.id === player.faction && faction.expansion === 'base'))) return false;
}
return true;
+2 -2
View File
@@ -61,12 +61,12 @@ export function kullDefaultProfile(game: KullDefaultProfile): KullDefaultEligibi
const options = game.lobbyOptions, projected = game.lobbyModules;
if (game.nexusCards || projected?.nexusCards) {
if (game.expansions.length !== 2 ||
!game.players.every(player => player.faction === 'choam' || classic.has(player.faction) || ix.has(player.faction)) ||
!game.players.every(player => choam.has(player.faction) || classic.has(player.faction) || ix.has(player.faction)) ||
options?.leaderSkills || options?.ecazTreachery || projected?.leaderSkills ||
projected?.ecazTreachery || projected?.discoveries || game.leaderSkills || game.ecazTreachery ||
game.discoveryEnabled || game.discoveries || game.discoveryStash || game.greatMaker ||
game.homeworlds || game.techTokens || game.strongholdCards || game.sandtrout)
return blocked('Kull with Nexus currently requires Basic CHOAM, Ixian/Tleilaxu or classic factions, the Ix and CHOAM decks, and no other optional modules.');
return blocked('Kull with Nexus currently requires Basic CHOAM, Richese, Ixian/Tleilaxu or classic factions, the Ix and CHOAM decks, and no other optional modules.');
return { eligible: true, reason: null };
}
if (options?.leaderSkills || projected?.leaderSkills ||
+2 -1
View File
@@ -5993,7 +5993,8 @@ export const RULE_TOPICS: RuleTopic[] = [
developmentStage: 'Verified',
summary: 'In the explicit Nexus Kull preview, choose one actual Treachery fuel card at a Karama attempt; the CHOAM Nexus is spent when the declaration is accepted, even if Kull is prevented.',
steps: [
'Fresh Basic CHOAM games with classic or Ixian/Tleilaxu factions, the CHOAM and Ix decks and Nexus alone enable this preview by default; the host may turn it off. Original Ix Sandtrout stays in the Spice deck. The separate local Advanced audit remains available. Other optional modules, Richese, Semuta and Richese Betrayal stay outside this profile; existing games are not converted.',
'Fresh Basic CHOAM games with Richese, classic or Ixian/Tleilaxu factions, the CHOAM and Ix decks and Nexus alone enable this preview by default; the host may turn it off. Original Ix Sandtrout stays in the Spice deck. Semuta remains active with Richese even when Kull is turned off. The separate local Advanced audit remains available; other optional modules and Richese Betrayal remain outside this profile.',
'In this Basic Richese profile, a successful printed Kull or distinct Karama counter offers its public discard to Semuta. Successful Nexus-fueled Kull offers the actual private fuel only after the physical Nexus has been spent and the effect committed; claim or pass retains the original Karama continuation.',
'The native Ixian Bidding counter remains bound while Kull resolves, then its original Ixian Nexus opportunity and auction proceed. A waiting Sandtrout does not disable an earned Emperor Nexus ordinary bank-auction benefit; other Sandtrout purchase sources retain their separate limits.',
'The CHOAM Nexus face says: Discard any Treachery Card to obtain a Worthless Card special effect of your choice. Basic uses the CHOAM phase-wide scope; new Advanced attempts use the supplied specific-effect scope with provisional phase expiry. Cunning changes the fuel identity, not the effect, target or timing. Neither text supplies a nested counter priority: distinct counters and BG-before-conversion custody are the recorded user-selected policy, not a publisher clarification.',
'Wait for another player’s validated Karama attempt. There is no proactive Kull choice in the generic Nexus panel. The public event, attempted player and purpose are the same whether CHOAM holds CHOAM Nexus, another Nexus, no eligible fuel or no Nexus at all. Only CHOAM receives its private source-aware plays and blocking reasons; opponents do not see the cost list or reserved original card.',