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 &&
|
||||
|
||||
@@ -94,12 +94,12 @@ export function selectedDiscoverySkillsSemutaProfile(game: NexusCoreConfiguratio
|
||||
selectedDiscoveryWithoutCardsProfile({...game, semutaPreview: undefined});
|
||||
}
|
||||
|
||||
/** Fresh Basic E1/E2 Nexus without additional modules. Native reaction stamps
|
||||
/** Fresh Basic selected-deck Nexus without additional modules. Native reaction stamps
|
||||
* are part of this core; Semuta and Nexus Kull keep their physical prerequisites. */
|
||||
function basicNativeNexusCore(game: NexusCoreConfiguration): boolean {
|
||||
const players = game.players;
|
||||
if (game.advanced !== false || !game.nexusCards || !players || !game.expansions.length ||
|
||||
!game.expansions.every(id => id === 'ix' || id === 'choam') ||
|
||||
!game.expansions.every(id => id === 'ix' || id === 'choam' || id === 'ecaz') ||
|
||||
game.homeworlds || game.leaderSkills || game.techTokens || game.strongholdCards ||
|
||||
game.ecazTreachery || game.discoveryEnabled || game.discoveries || game.discoveryStash || game.greatMaker ||
|
||||
game.advancedPreview || game.richeseBetrayalPreview) return false;
|
||||
@@ -107,9 +107,10 @@ function basicNativeNexusCore(game: NexusCoreConfiguration): boolean {
|
||||
!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.expansions.length !== 2 || !game.expansions.includes('ix') || !game.expansions.includes('choam') ||
|
||||
(game.expansions.length !== 2 && game.expansions.length !== 3) || !game.expansions.includes('ix') || !game.expansions.includes('choam') ||
|
||||
!players.some(player => player.faction === 'choam') ||
|
||||
players.some(player => player.faction !== 'choam' && player.faction !== 'richese' && player.faction !== 'ixians' && player.faction !== 'tleilaxu' &&
|
||||
!(game.expansions.includes('ecaz') && (player.faction === 'ecaz' || player.faction === 'moritani')) &&
|
||||
!FACTIONS.some(faction => faction.id === player.faction && faction.expansion === 'base'))) return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -60,13 +60,11 @@ export function kullDefaultProfile(game: KullDefaultProfile): KullDefaultEligibi
|
||||
return blocked('Automatic Kull needs one to six distinct factions, including CHOAM, with their selected decks.');
|
||||
const options = game.lobbyOptions, projected = game.lobbyModules;
|
||||
if (game.nexusCards || projected?.nexusCards) {
|
||||
if (game.expansions.length !== 2 ||
|
||||
!game.players.every(player => choam.has(player.faction) || classic.has(player.faction) || ix.has(player.faction)) ||
|
||||
options?.leaderSkills || options?.ecazTreachery || projected?.leaderSkills ||
|
||||
if (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, Richese, Ixian/Tleilaxu or classic factions, the Ix and CHOAM decks, and no other optional modules.');
|
||||
return blocked('Kull with Nexus currently requires Basic rules, the Ix and CHOAM decks and no other optional modules; the Ecaz deck may also be selected.');
|
||||
return { eligible: true, reason: null };
|
||||
}
|
||||
if (options?.leaderSkills || projected?.leaderSkills ||
|
||||
|
||||
+2
-1
@@ -5993,7 +5993,7 @@ 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 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.',
|
||||
'Fresh Basic CHOAM games with selected Ix and CHOAM decks and Nexus alone enable this preview by default, including selected Ecaz and Moritani factions with their deck; 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.',
|
||||
@@ -6167,6 +6167,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
summary: 'An unallied holder with Ixians absent may spend Ixian Nexus to replace the exact card just bought in a normal auction or supported native Richese sale, preserving original settlement and drawing the actual deck top privately.',
|
||||
searchText: 'Ixian Nexus Secret Ally purchased card replacement normal auction cache Black Market Karama full hand empty deck same card',
|
||||
steps: [
|
||||
'In supported Basic Richese/Semuta Nexus games, using Ixian Secret Ally offers the private purchased-card discard to Semuta before the replacement draw. Claim or pass preserves the paid sale and spent Nexus, then the buyer draws once; an empty deck uses the normal discard recycle.',
|
||||
'Original standalone entries admit classic/base33, Tleilaxu plus classics/Ix47, or Richese plus classics/CHOAM35 and cache, with Ixians absent. Fresh classic2–6 Basic/Advanced Nexus core initializes the same effect with base33 or selected36, optional14, original Tech3+ and Advanced Strongholds2+; independent cards are not required. Other native/module envelopes keep their bounds; no played game gains a new stamp.',
|
||||
'Nexus has no starting deal. A draw requires a qualifying closing Nexus with an alliance. A two-seat profile can continue its base game but cannot naturally create an unallied Secret Ally holder; actual replacement needs at least three seats, or four for two simultaneous unallied recipients.',
|
||||
'The photographed publisher face grants an independent Secret Ally operation when Ixians are absent. Remain unallied and hold the actual singleton Ixian Nexus. Native Ixian allied replacement is a different cancelable advantage and is not removed or used as a fallback. The adopted unofficial Advanced rulebook supplies relevant core rules, not this absent Nexus face or a new Nexus ruling.',
|
||||
|
||||
Reference in new issue
Block a user