Connect native Nexus reactions to classic independent cards

This commit is contained in:
Quintine committed 2026-10-18 04:51:33 +11:00
1 parent c466107452
commit 366c967f8a
15 files changed
+483 -29

No files matched your search

+27 -4
View File
@@ -9648,7 +9648,7 @@ export function initializeNexusGameForAudit(state: Game): Game {
}
/** Fresh audit admission only; ordinary games and public starts never opt in. */
export function initializeHarkonnenNexusBetrayalGameForAudit(state: Game): Game {
requireRule(harkonnenNexusBetrayalModeSupported(state) &&
requireRule(!state.ecazTreachery && !state.leaderSkills && harkonnenNexusBetrayalModeSupported(state) &&
state.nexusCards?.cards === null && state.nexusCards.phase === null &&
state.nexusHarkonnenBetrayalPreview === undefined &&
state.pendingNexusHarkonnenBetrayal === undefined &&
@@ -9668,6 +9668,7 @@ export function initializeHarkonnenNexusBetrayalGameForAudit(state: Game): Game
return g;
}
function harkonnenNexusBetrayalModeSupported(g: Game): boolean {
if (classicIndependentNexusProfile(g)) return !!byFaction(g, 'harkonnen');
return typeof g.advanced === 'boolean' && !!g.nexusCards && g.expansions.length === 0 &&
g.players.length >= 2 && g.players.length <= 6 && !!byFaction(g,'harkonnen') &&
new Set(g.players.map(p => p.faction)).size === g.players.length &&
@@ -9684,7 +9685,7 @@ function harkonnenNexusBetrayalContext(g: Game) {
leader:b.plans[id].leader,...(b.plans[id].kwisatz === undefined ? {} : {kwisatz:b.plans[id].kwisatz}),
}]));
const battle = {event:b.event!,attacker:b.attacker,defender:b.defender,plans,heroLeaderIds:[]};
return {status:g.status,phase:g.phase,turn:g.turn,advanced:g.advanced,
return {status:g.status,phase:g.phase,turn:g.turn,advanced:g.advanced,ecazTreachery:g.ecazTreachery === true,
sequence:g.nexusHarkonnenBetrayalCursor!.sequence,players:g.players,
cards:g.nexusCards!.cards!,physicalCards:physicalTreacheryCards(g),
traitors:nexusTraitorSnapshot(g),universe:nexusTraitorUniverse(g),battle};
@@ -10012,7 +10013,7 @@ function decideIxianNexusBetrayal(g: Game,id: string,action: Action): void {
}
/** Fresh classic, Tleilaxu or Richese profile; no public start or retrofit. */
export function initializeIxianNexusReplacementGameForAudit(state: Game): Game {
requireRule(!state.nexusIxianReplacementPreview && !state.pendingNexusIxianReplacement &&
requireRule(!state.ecazTreachery && !state.leaderSkills && !state.nexusIxianReplacementPreview && !state.pendingNexusIxianReplacement &&
!state.pendingNexusIxianSpecialPurchase &&
state.nexusIxianReplacementHistory === undefined && state.nexusIxianReplacementCursor === undefined &&
ixianNexusReplacementModeSupported(state),
@@ -10027,6 +10028,7 @@ export function initializeIxianNexusReplacementGameForAudit(state: Game): Game {
return g;
}
function ixianNexusReplacementModeSupported(g: Game): boolean {
if (classicIndependentNexusProfile(g)) return true;
return !!g.nexusCards && g.players.length >= 2 && g.players.length <= 6 &&
new Set(g.players.map(p => p.faction)).size === g.players.length &&
((!g.expansions.length && g.players.every(p => CLASSIC_FACTIONS[p.faction] === true)) ||
@@ -10057,7 +10059,7 @@ function ixianNexusReplacementContext(g: Game) {
requireRule(g.pendingNexusIxianSpecialPurchase || g.auction || g.richeseAuction,
'The Nexus purchased-card choice lost its original purchase.');
return {status:g.status,phase:g.phase,turn:g.turn,sequence:g.nexusIxianReplacementCursor!.sequence,
advanced:g.advanced,expansions:g.expansions,richese:!!byFaction(g,'richese'),
advanced:g.advanced,expansions:g.expansions,richese:!!byFaction(g,'richese'),ecazTreachery:g.ecazTreachery === true,
auction:g.auction,richeseAuction:g.richeseAuction ?? null,sale:g.currentAuctionSale ?? null,
specialPurchase:g.pendingNexusIxianSpecialPurchase ?? null,
players:g.players.map(p => ({id:p.id,hand:p.hand,handLimit:handLimit(p)})),
@@ -10894,6 +10896,27 @@ function initializeSetupGameForAudit(state: Game, homeworlds: boolean, nexus = f
g.spiceBankerIncomePreview = true;
g.spiceBankerIncome = bankerIncomeRule(() => createBankerIncomeState(bankerIncomeContext(g)));
}
if (classicIndependentNexusProfile(g)) {
requireRule(g.nexusIxianReplacementPreview === undefined &&
g.nexusIxianReplacementHistory === undefined && g.nexusIxianReplacementCursor === undefined &&
!g.pendingNexusIxianReplacement && !g.pendingNexusIxianSpecialPurchase &&
g.nexusHarkonnenBetrayalPreview === undefined && g.nexusHarkonnenBetrayalHistory === undefined &&
g.nexusHarkonnenBetrayalCursor === undefined && !g.pendingNexusHarkonnenBetrayal &&
!g.pendingNexusHarkonnenReplacement && g.nexusHarkonnenReplacementHistory === undefined,
'Native Nexus reactions initialize only with fresh independent-card setup.');
g.nexusIxianReplacementPreview = true;
g.pendingNexusIxianReplacement = null;
g.nexusIxianReplacementHistory = [];
g.nexusIxianReplacementCursor = initialIxianReplacementCursor();
if (byFaction(g, 'harkonnen')) {
g.nexusHarkonnenBetrayalPreview = true;
g.pendingNexusHarkonnenBetrayal = null;
g.nexusHarkonnenBetrayalHistory = [];
g.nexusHarkonnenBetrayalCursor = initialHarkonnenBetrayalCursor();
g.pendingNexusHarkonnenReplacement = null;
g.nexusHarkonnenReplacementHistory = [];
}
}
initializeSetup(g, strongholdFactions || !!g.strongholdCards);
if (nativeTech && byFaction(g, 'ecaz')) g.ecazOccupyPreview = true;
return normalizeAutomaticGame(g);
+3 -3
View File
@@ -27,7 +27,7 @@ export type FactionModuleProfile = {
};
/** Independent36 + original Nexus12 in classic tables, optionally all14 Skills.
* Initializers own undealt component freshness; other overlays stay separate. */
* Fresh native Harkonnen/Ixian effect stamps are not additional selected modules. */
export function classicIndependentNexusProfile(
game: Partial<FactionModuleProfile> & Pick<FactionModuleProfile, 'expansions'> & {
sandtrout?: unknown; moritaniAssassinatePreview?: unknown;
@@ -39,8 +39,8 @@ export function classicIndependentNexusProfile(
game.homeworlds || game.techTokens || game.strongholdCards || game.sandtrout ||
game.discoveryEnabled || game.discoveries || game.discoveryStash || game.greatMaker ||
game.semutaPreview || game.moritaniAssassinatePreview || game.advancedPreview || game.kullPreview || game.nexusKullPreview ||
game.guildBetrayalPreview || game.richeseBetrayalPreview || game.nexusIxianReplacementPreview ||
game.nexusIxianBetrayalPreview || game.nexusHarkonnenBetrayalPreview) return false;
game.guildBetrayalPreview || game.richeseBetrayalPreview ||
game.nexusIxianBetrayalPreview) return false;
for (let i = 0; i < players.length; i++) {
for (let j = 0; j < i; j++) if (players[j].faction === players[i].faction) return false;
let classic = false;
+20 -12
View File
@@ -1,4 +1,5 @@
import { baseDeck, leaders, type Card } from './cards';
import { ecazTreacheryCards } from './ecaz-cards';
import {
nexusCardMode, validateNexusCards, validateNexusPlayers,
type NexusPlayer, type NexusState,
@@ -24,6 +25,8 @@ export type HarkonnenBetrayalContext = {
turn: number;
advanced: boolean;
sequence: number;
/** Public selection; absence retains the original base33 census. */
ecazTreachery?: boolean;
players: readonly NexusPlayer[];
cards: NexusState;
physicalCards: readonly Card[];
@@ -80,6 +83,7 @@ const classic: Readonly<Record<string, true>> = {
atreides: true, harkonnen: true, emperor: true, fremen: true, guild: true, beneGesserit: true,
};
const canonicalCards = new Map(baseDeck().map(card => [card.id, card]));
const independentCards = new Map<string, Card>(ecazTreacheryCards().map(card => [card.id, card]));
const declarationKeys = ['event', 'voter', 'beneficiary', 'target', 'leader', 'identity', 'signature'] as const;
const plain = (value: unknown): value is Record<string, unknown> =>
value !== null && typeof value === 'object' && !Array.isArray(value) &&
@@ -117,15 +121,16 @@ function validateUniverse(universe: readonly string[], players: readonly NexusPl
universe.every(identity => expected.includes(identity)),
'Harkonnen Betrayal needs the original complete canonical classic Traitor universe.');
}
function validatePhysical(cards: readonly Card[]): void {
requireBetrayal(Array.isArray(cards) && cards.length === canonicalCards.size &&
new Set(cards.map(card => card?.id)).size === canonicalCards.size && cards.every(card => {
function validatePhysical(cards: readonly Card[], independent = false): void {
const count = canonicalCards.size + (independent ? independentCards.size : 0);
requireBetrayal(Array.isArray(cards) && cards.length === count &&
new Set(cards.map(card => card?.id)).size === count && cards.every(card => {
if (!plain(card) || !text(card.id)) return false;
const expected = canonicalCards.get(card.id);
const expected = canonicalCards.get(card.id) ?? (independent ? independentCards.get(card.id) : undefined);
return !!expected && exact(card, ['id', 'name', 'kind',
...(expected.effect === undefined ? [] : ['effect'])]) && card.name === expected.name &&
card.kind === expected.kind && card.effect === expected.effect;
}), 'Harkonnen Betrayal requires complete unique canonical base33 Treachery custody.');
}), 'Harkonnen Betrayal requires complete unique canonical selected Treachery custody.');
}
function validateBattle(battle: Battle): void {
requireBetrayal(exact(battle, ['event', 'attacker', 'defender', 'plans', 'heroLeaderIds']) &&
@@ -196,11 +201,12 @@ export function harkonnenBetrayalEvent(
}
function validateContext(ctx: HarkonnenBetrayalContext): void {
requireBetrayal(plain(ctx) && ctx.status === 'playing' && ctx.phase === 6 &&
whole(ctx.turn) && ctx.turn > 0 && whole(ctx.sequence) && typeof ctx.advanced === 'boolean',
whole(ctx.turn) && ctx.turn > 0 && whole(ctx.sequence) && typeof ctx.advanced === 'boolean' &&
(ctx.ecazTreachery === undefined || typeof ctx.ecazTreachery === 'boolean'),
'Harkonnen Betrayal requires its current Battle phase and independent cursor.');
validateRoster(ctx.players);
validateNexusCards(ctx.cards, ctx.players);
validatePhysical(ctx.physicalCards);
validatePhysical(ctx.physicalCards, ctx.ecazTreachery);
validateUniverse(ctx.universe, ctx.players);
validateNexusTraitorSnapshot(ctx.traitors, ctx.universe);
requireBetrayal(ctx.traitors.players.length === ctx.players.length &&
@@ -214,7 +220,9 @@ function sourceAuthority(source: HarkonnenBetrayalSource): HarkonnenBetrayalAuth
nativeContext: source.nativeContext, nativeRequired: source.nativeRequired, nativePassed: source.nativePassed };
}
function inventory(ctx: HarkonnenBetrayalContext): string {
return JSON.stringify([ctx.cards, ctx.traitors, ctx.physicalCards]);
const census: unknown[] = [ctx.cards, ctx.traitors, ctx.physicalCards];
if (ctx.ecazTreachery) census.push(true);
return JSON.stringify(census);
}
function sourceSignature(source: HarkonnenBetrayalSource): string {
return JSON.stringify(['harkonnenBetrayalSource', source.version, source.event, source.turn,
@@ -247,13 +255,13 @@ function validateSavedSource(value: unknown): asserts value is HarkonnenBetrayal
} catch {
throw new HarkonnenBetrayalError('Harkonnen Betrayal lost its original physical census.');
}
requireBetrayal(Array.isArray(stored) && stored.length === 3,
requireBetrayal(Array.isArray(stored) && (stored.length === 3 || (stored.length === 4 && stored[3] === true)),
'Harkonnen Betrayal lost its original physical census.');
type SavedInventory = [NexusState, NexusTraitorSnapshot, readonly Card[]];
const [cards, traitors, physicalCards] = stored as SavedInventory;
type SavedInventory = [NexusState, NexusTraitorSnapshot, readonly Card[], true?];
const [cards, traitors, physicalCards, independent] = stored as SavedInventory;
validateNexusCards(cards, source.players);
validateNexusTraitorSnapshot(traitors, source.universe);
validatePhysical(physicalCards);
validatePhysical(physicalCards, independent);
requireBetrayal(traitors.players.length === source.players.length &&
source.players.every(player => traitors.players.some(seat =>
seat.id === player.id && seat.faction === player.faction)) &&
+6 -1
View File
@@ -1,4 +1,5 @@
import { baseDeck, choamDeck, ixDeck, type Card } from './cards';
import { ecazTreacheryCards } from './ecaz-cards';
import { richeseCards } from './richese-cards';
import type { RicheseAuction } from './richese-auction';
@@ -19,6 +20,8 @@ export type IxianReplacementContext = {
turn: number;
sequence: number;
expansions: readonly string[];
/** Original independent three-card selection, never inferred from a hand. */
ecazTreachery?: boolean;
richese: boolean;
/** Native Black Market bidding and Richese special Karama require Advanced play. */
advanced: boolean;
@@ -77,7 +80,8 @@ const base = catalogue(baseDeck());
const ix = catalogue(ixDeck());
const choam = catalogue(choamDeck());
const cache = catalogue(richeseCards());
const canonical: Readonly<Record<string, Card>> = { ...base, ...ix, ...choam, ...cache };
const independent = catalogue(ecazTreacheryCards());
const canonical: Readonly<Record<string, Card>> = { ...base, ...ix, ...choam, ...cache, ...independent };
const genesis = 'ixian-replacement:0';
const plain = (value: unknown): value is Record<string, unknown> =>
value !== null && typeof value === 'object' && !Array.isArray(value) &&
@@ -104,6 +108,7 @@ function canonicalCard(value: unknown): value is Card {
function selectedCard(value: unknown, ctx: IxianReplacementContext): value is Card {
return canonicalCard(value) && (
Object.hasOwn(base, value.id) ||
(ctx.ecazTreachery === true && Object.hasOwn(independent, value.id)) ||
(Object.hasOwn(ix, value.id) && ctx.expansions.includes('ix')) ||
(Object.hasOwn(choam, value.id) && ctx.expansions.includes('choam')) ||
(Object.hasOwn(cache, value.id) && ctx.expansions.includes('choam') && ctx.richese)
+9 -8
View File
@@ -4574,7 +4574,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: [
'The fresh local profile admits two-to-six unique seats in Basic/Advanced: classics/base33, Tleilaxu plus classics/Ix47, or Richese plus classics/CHOAM35 and original cache, with Ixians absent and Nexus alone. Normal purchases qualify, as do genuine positive Richese cache sales in Basic/Advanced and Black Market sales in Advanced. Sandtrout is omitted only for fresh native Ix setup. Other natives/modules and public starts or played-save conversion remain excluded.',
'Original standalone entries admit classic/base33, Tleilaxu plus classics/Ix47, or Richese plus classics/CHOAM35 and cache, with Ixians absent and Nexus alone. Fresh normal classic2–6 Basic/Advanced independent36/Nexus12 now also initializes this same effect, optionally all14 Skills. Native selected families, Worlds, phase/Discovery/Semuta and other reaction envelopes retain their separate 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.',
'Normal paid/Karama purchases, genuine native Richese sold lots and actual Advanced special-Karama acquisition during Bidding supply their own purchase authority. A direct purchase has already spent its Karama, three spice and once-use; its target/price3 are not an unrelated row or sale. Use/Keep resumes original income, active player and suspended controls. Only unpaid bids remain reserved; already-delivered sale commitments are not held twice. Other phases keep native acquisition without a Bidding-only Nexus choice. Unbid keeps, gifts, bonus draws, Technology swaps and old hand cards supply no purchase.',
@@ -4586,7 +4586,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Harkonnen uses a visibly provisional order matching the native Ixian ally sale pipeline: this replacement choice, original income response, original bonus-card response, then the next lot. It is product composition, not a printed priority ruling. Use draws the replacement before any allowed bonus; Keep leaves that top card for the bonus. Replacement grants no second bonus or slot at the eight-card limit. Canceling the later native bonus never undoes the replacement or original payment.',
'Keep the original payment, funded contribution and free/paid source. There is no refund or second debit. Original income and the native auction continuation finish once after the choice, preserving existing response and decision priority. Other lot, card, transfer and payment actions cannot bypass the pending source lock; independent own-seat autopilot changes remain available.',
'Refresh restores the same pending event or closed outcome without another payment, draw, shuffle or Nexus spend. A later genuine purchase may create a new event; closed, stale, foreign or malformed events cannot reopen an old one. Later transfers and reshuffles do not invalidate history merely because the old physical card has moved again. Receipt links are nonrecursive so saved history stays linear across retained purchases.',
'Easy, Medium, Hard and Brutal use their own projected legal Use/Pass choices. These are minimal legal policies, not calibrated Nexus strategy. Frozen native, private controls, actual local CLI/phone and authenticated recovery evidence verifies only this bounded path. Full faction/module games, wider compositions, public starts and deployed acceptance remain separate gates.',
'Easy, Medium, Hard and Brutal use their own projected legal Use/Pass choices, not calibrated strategy. Current classic independent-card controls preserve original paid settlement and native Harkonnen bonus through this same protocol. Earlier native, private and saved evidence remains dated. Full faction/module games, wider compositions and deployed acceptance remain separate gates.',
],
example: 'You buy a card for four spice and inspect it privately. Pass keeps it and the Nexus. Use keeps the original four-spice purchase settled, spends Ixian Nexus, discards that exact card and draws the next deck card. If the draw deck is empty, its new discard joins the actual recycled pile and can be drawn again.',
related: ['nexus-cards', 'bidding', 'ix-technology', 'card-karama', 'alliance-funding', 'privacy', 'implementation-checklist'],
@@ -4595,7 +4595,7 @@ export const RULE_TOPICS: RuleTopic[] = [
{ area: 'Player controls', status: 'Partial', detail: 'Actual390px native special purchase controls are enabled at three remaining spice during older paid income. Direct Use preserves the older card and both credits; reserved-bid Keep reaches a distinct genuine normal offer, then one row advance. Guidance states original cost/once-use committed and no new auction. Provisional cache/Harkonnen scope remains visible. No overflow/browser/application error; broader acceptance remains open.', evidence: ['components/nexus-ixian-replacement.tsx', 'components/richese-special-karama.tsx', 'components/game-table.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'Four minimal legal policies use their own projected offer while original settlement waits. Sixteen original-state actions cover quiet Emperor/bank, reserved bid and nested paid income direct purchases; previous source-qualified paths remain in the contract. No strategy or difficulty calibration is claimed.', evidence: ['game/bots.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'Actual auction/direct authority, native price3 and cost commitment, once-use, active/control restoration, paid-versus-unpaid reservations, source-v1 preservation and provisional cache/Harkonnen interpretations are documented. Outside Bidding the native special purchase adds no Nexus offer. Other modules and universal/publisher priority remain separate.', evidence: ['docs/NEXUS_IXIAN_REPLACEMENT_RULES.md', 'docs/NEXUS_CARD_RULES.md', 'docs/RULE_DECISIONS.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Ten new direct scenarios and affected eleven-file107/107 pass, including existing funding consumers, with types/lint/build. Ten original runtime paths, sixteen legal policy actions and two390px actual native-action journeys prove original cost/target, Emperor/bank, quiet controls, reserved-bid automatic payment, distinct following normal offer, nested income and real Revival exclusion. A paid-bid double reservation was fixed; an obsolete serialized-identifier substring pin was deleted while semantic rival privacy checks remain. No broad, HTTP, saved/deployed assurance; all five facets remain Partial.', evidence: ['tests/nexus-ixian-replacement.test.ts', 'tests/nexus-ixian-replacement-engine.test.ts', 'tests/fixtures/nexus-ixian-native-depleted.json', 'tests/fixture-nexus-ixian-replacement.ts', 'tests/fixture-native-ixian-replacement.ts', 'tests/native-ixian-replacement.test.ts', 'tests/bg-ixian-replacement.test.ts', 'tests/fixture-bg-ixian-replacement.ts', 'tests/harkonnen-ixian-replacement.test.ts', 'tests/fixture-harkonnen-ixian-replacement.ts', 'tests/richese-ixian-replacement.test.ts', 'tests/fixture-richese-ixian-replacement.ts', 'tests/richese-ixian-special-replacement.test.ts', 'tests/fixture-richese-ixian-special-replacement.ts', 'tests/richese-ixian-direct-replacement.test.ts', 'tests/fixture-richese-ixian-direct-replacement.ts'] },
{area:'Verification',status:'Partial',detail:"Classic independent36/Nexus12 optional14 now initializes the original effect at fresh setup. Actual paid2 delivers selected Reinforcements; Use discards only that card for the real deck top with no second invoice, while Harkonnen Pass keeps both cards and allows exactly one original bonus. Six combined consumer regressions, three390px journeys and four legal policies pass. Older native/direct and saved evidence remains dated; broader modules and deployed assurance stay open.",evidence:['tests/classic-independent-nexus-reactions.test.ts','tests/fixture-classic-independent-nexus-reactions.ts','tests/nexus-ixian-replacement.test.ts', 'tests/nexus-ixian-replacement-engine.test.ts', 'tests/fixtures/nexus-ixian-native-depleted.json', 'tests/fixture-nexus-ixian-replacement.ts', 'tests/fixture-native-ixian-replacement.ts', 'tests/native-ixian-replacement.test.ts', 'tests/bg-ixian-replacement.test.ts', 'tests/fixture-bg-ixian-replacement.ts', 'tests/harkonnen-ixian-replacement.test.ts', 'tests/fixture-harkonnen-ixian-replacement.ts', 'tests/richese-ixian-replacement.test.ts', 'tests/fixture-richese-ixian-replacement.ts', 'tests/richese-ixian-special-replacement.test.ts', 'tests/fixture-richese-ixian-special-replacement.ts', 'tests/richese-ixian-direct-replacement.test.ts', 'tests/fixture-richese-ixian-direct-replacement.ts']},
],
},
{
@@ -4640,15 +4640,16 @@ export const RULE_TOPICS: RuleTopic[] = [
category: 'Advanced & expansions',
coverage: 'Partial',
developmentStage: 'Verified',
summary: 'The fresh local harkonnen-betrayal preview cancels one original native Harkonnen traitor call, returns that physical card immediately and gives Harkonnen a private replacement during Mentat Pause.',
summary: 'Fresh supported classic starts cancel one original native Harkonnen traitor call, return that physical card immediately and give Harkonnen a private replacement during Mentat Pause.',
searchText: 'Harkonnen Nexus Betrayal revealed declared traitor ally caller beneficiary shuffle replacement Mentat neutral acknowledgement',
steps: [
'The bounded development profile requires native Harkonnen and two to six unique classic factions, Basic or Advanced, the base Treachery Deck and Nexus alone. No public start, existing-game retrofit, expansion deck, Homeworld, Leader Skill, Discovery or other overlay is enabled.',
'The original standalone preview requires native Harkonnen, classic2–6 Basic/Advanced, base33 and Nexus alone. Fresh normal classic independent36/Nexus12 now initializes the same effect, optionally all14 Skills, alongside native Ixian replacement. Original setup and public selection bind the physical census. No played game is reflagged; native selected families, Worlds, phase/Discovery/Semuta and other reaction envelopes stay separate.',
'Nexus has no initial deal. An actual alliance-qualified closing Nexus must supply the unallied holder. An effect scenario needs at least three seats; two seats can continue ordinary play without inventing a card. Two simultaneous receivers need four actual seats.',
'The photographed publisher face cancels a revealed Harkonnen traitor, shuffles that exact card back immediately, and gives Harkonnen one replacement during Mentat Pause. It is not a draw-now exchange, whole-battle ban or refund of an already resolved outcome. Native Cunning and absent-Harkonnen Secret Ally remain separate effects.',
'Only a genuine legally matching native personal or allied Harkonnen call is a source. Preserve the actual caller, combatant beneficiary, target, original revealed plans and physical identity. A false or invalid call, protected leader, another faction or finished battle cannot offer this effect.',
'Resolve the original allied Harkonnen Karama counter first, including legal Advanced Bene Gesserit conversion. Native cancellation opens no Nexus gate or cost. A personal Harkonnen call gains no invented native counter. After native allowance, declare the actual matched traitor before its outcome and await the neutral acknowledgements.',
'Every publicly possible unallied non-Harkonnen held-Nexus owner sees the same declared-traitor acknowledgement, regardless of secret face. Only an actual privately eligible Harkonnen-card holder sees Use and their own unavailable reason. Native-counter-first and this uniform local preview timing are bounded inferences, not an answer to the outstanding universal reaction-policy question.',
'The classic independent-card and optional Skills entry inherits that same visibly provisional acknowledgement convention. Original allied printed Karama may prevent the call before any Nexus opportunity; allowing it preserves the actual caller and combatant beneficiary. No second counter window or new universal policy is invented.',
'Pass retains both cards and cannot be withdrawn for that event; all required passes retain the original call and resume it once. Use spends the physical singleton Nexus, returns only that declared Harkonnen traitor to the actual shuffled reserve, prevents only that call and resumes the surviving original battle once.',
'The replacement is automatic at the actual current Mentat opening, before optional actions and victory acceptance in this bounded profile. Draw the actual reserve top privately once; it may be the returned card after its shuffle. No extra confirmation, selected replacement, Treachery slot, activation spice, force or leader cost is added.',
'The public declaration exposes only its actual matched traitor and roles, not other held Harkonnen cards, rival Nexus faces, reserve order or private source. Inspect that declared Traitor Card through its public inspector, and inspect only your own actually held Nexus. A replacement identity remains private to Harkonnen; prior public history must not claim continued ownership of the returned card.',
@@ -4662,7 +4663,7 @@ export const RULE_TOPICS: RuleTopic[] = [
{ area: 'Player controls', status: 'Partial', detail: 'Public declared-Traitor inspector, own eligible event-only Use/Pass/Nexus inspector, sole/multiple public ownership and every-seat independent autopilot are connected. Actual phone Use, wrong-face Pass, partial refresh and raw administrator owner agree.', evidence: ['components/nexus-harkonnen-betrayal.tsx', 'components/game-table.tsx', 'game/table-turn.ts', 'db/admin-directory.ts'] },
{ area: 'AI', status: 'Partial', detail: 'All four minimal policies consume canonical own-view choices and native pending priority with legal paced continuation; full strategy and calibration remain separate.', evidence: ['game/bot-nexus-harkonnen-betrayal.ts', 'game/bots.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'Printed immediate/delayed operations, personal/allied roles, inferred local counter/privacy convention, fresh natural card rights and unchanged outside-profile/module/public/deployment questions are explicit.', evidence: ['docs/NEXUS_HARKONNEN_RULES.md', 'docs/NEXUS_CARD_RUNTIME.md', 'docs/RULE_DECISIONS.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Genuine native personal/allied Use/all-pass and actual Mentat top, printed/BG preemption, canonical stock, own/public entitlement, four bots and authenticated restart/races/rejection pass. Four privately backed-up original-setup CLI/phone rooms prove Use, wrong-face Pass, partial refresh, administrator owner and saved stock through real HTTP Mentat once. Independent native/privacy reviews have no finding; wider and deployed acceptance remain open.', evidence: ['tests/fixture-nexus-harkonnen-betrayal.ts', 'tests/nexus-harkonnen-betrayal.test.ts', 'tests/nexus-harkonnen-betrayal-runtime.test.ts', 'tests/nexus-harkonnen-betrayal-controls.test.tsx', 'tests/bot-nexus-harkonnen-betrayal.test.ts', 'tests/nexus-harkonnen-betrayal-recovery.test.ts', 'tests/prototype-room.test.ts', 'tests/admin-directory.test.ts'] },
{area:'Verification',status:'Partial',detail:"Classic independent36/Nexus12 optional14 now initializes the existing effect only at fresh setup. Genuine personal Use, all-pass and allied native counter or allowance preserve actual Harass, the declared physical Traitor and one private real Mentat draw. Six combined consumer regressions, three390px native journeys and four legal policies pass. Earlier native/saved reviews remain dated; universal timing, wider modules, assurance and deployed acceptance remain open.",evidence:['tests/classic-independent-nexus-reactions.test.ts','tests/fixture-classic-independent-nexus-reactions.ts','tests/fixture-nexus-harkonnen-betrayal.ts', 'tests/nexus-harkonnen-betrayal.test.ts', 'tests/nexus-harkonnen-betrayal-runtime.test.ts', 'tests/nexus-harkonnen-betrayal-controls.test.tsx', 'tests/bot-nexus-harkonnen-betrayal.test.ts', 'tests/nexus-harkonnen-betrayal-recovery.test.ts', 'tests/prototype-room.test.ts', 'tests/admin-directory.test.ts']},
],
},
{
@@ -4697,10 +4698,10 @@ export const RULE_TOPICS: RuleTopic[] = [
'With Harkonnen absent, Secret Ally draws two Traitor Cards during Mentat Pause, then you choose exactly two distinct held identities to return. Newly drawn cards may be returned. Tleilaxu receives Face Dancers and can return newly drawn or existing unrevealed dancers; previously revealed dancers stay protected.',
'The Harkonnen exchange spends the Nexus Card when you draw and shuffles the returned cards into the Traitor Deck after your selection. Your original hand size is restored. Other players see that the exchange is pending, without learning your drawn or returned identities.',
'Moritani victim cards permanently set aside are outside reserve, ordinary hands and Face Dancers, so Nexus draws and returns cannot recycle them. A card merely drawn as an earlier Moritani replacement is different: it may be legally returned and later drawn again, with the older receipt unchanged. Actual Hasimir6 return, resumed Mori placement and a later Esmar3 retirement/redraw are connected; two historical references still represent one physical card.',
'Harkonnen Betrayal has a separate fresh local harkonnen-betrayal profile for an actual personal or allied native traitor call. After any original allied counter allows it, uniform public possible held-Nexus owners acknowledge the actually declared traitor before its outcome. Use returns that exact physical traitor immediately and schedules one private actual Mentat replacement; all-pass keeps the original call. This provisional bounded convention does not answer the outstanding universal reaction policy; see its dedicated guide for bounded verified native/private/recovery/CLI/phone evidence and unchanged module/public/deployment gates.',
'Harkonnen Betrayal follows an actual personal or allied native traitor call in its original standalone preview or fresh classic independent36/Nexus12 entry, optionally all14 Skills. After original allied counters allow it, public possible held-Nexus owners acknowledge the actually declared traitor before its outcome. Use returns that physical card immediately and schedules one private actual Mentat replacement; all-pass preserves the call. This inherited provisional convention does not settle universal reaction timing outside these bounds.',
'Tleilaxu Cunning sets aside all your revealed Face Dancers, draws replacements secretly, then shuffles that temporary group into the Traitor Deck. Unrevealed dancers stay held; permanent Moritani victim markers never enter that draw/return group. Original native Mentat and all-three-revealed usage/order remain distinct. Recorded source zones and old absent-zone history remain unchanged by later live markers.',
'Ixian Cunning is used in your battle before submitting your Battle Plan. Your Suboids then count as strength one without spice support in every battle for the rest of the turn, in Basic and Advanced. Cyborg strength and support are unchanged. Earlier inspected or promised plan elements remain binding; you cannot activate the card if the new strength would make them impossible.',
'Ixian Secret Ally has a fresh local ixian-replacement profile for normal paid or printed Karama-paid auction purchases in classic base-deck/Nexus Basic or Advanced games. A supported unallied buyer may spend its actual Nexus to discard exactly the just-purchased card and draw the real deck top privately; Pass keeps both cards. Full-hand replacement and empty-deck same-card redraw are allowed, and original payment/income/auction continuation occur once. Harkonnen buyers, cache/Black Market/special origins and combined modules remain guarded. The neutral buyer choice is an inferred privacy convention, not publisher timing. See the dedicated guide for bounded verified native, private, recovery and phone evidence; this is not full-module certification.',
'Ixian Secret Ally uses the exact just-purchased card in supported original standalone entries and fresh classic independent36/Nexus12 starts, optionally all14 Skills. A supported unallied buyer spends its actual Nexus, discards only that purchased card and draws the real deck top privately; Pass keeps both. Full-hand replacement and same-card recycling remain legal, with original payment and continuation once. Native Harkonnen bonus and supported Richese sale/direct origins retain their dedicated source contracts and provisional order; already-owned paid retention creates no new acquisition. Neutral buyer acknowledgement is an inferred convention, not universal publisher timing. Other modules, complete modes and deployed assurance remain separate.',
'Ixian Betrayal has a separate fresh native-Ixian ixian-betrayal profile for both original Bidding inspection/draw and Advanced Technology attempts. Native counters resolve first; then every publicly possible unallied rival held-Nexus seat acknowledges uniformly before the effect. A legal Use spends one physical Ixian Nexus to deny only that attempt; all-pass preserves the native effect once. No completed-card undo or whole-phase ban is added. This bounded ordering/privacy is an inference, not universal publisher priority; see its bounded verified native/private/recovery/phone evidence and unchanged Richese/module/public/deployment gates.',
'With Tleilaxu absent, its actual unallied Nexus holder may use a quiet Revival control to return one mandatory own dead leader free and up to five exact physical counters for one bank spice each. The provisional policy uses the ordinary leader slot and expands the shared ordinary ceiling to at least five, counting prior returns and retaining larger native/Recruits allowance. Only the unopened roster trigger is waived: repeated deaths, actual stars/Cyborgs and special custody retain canonical rules, and ordinary cycle history is not fabricated. Zero forces is legal; force-only use is not. Spend the physical card once; native Tleilaxu income and cancellation are not imported.',
'In Advanced play, Bene Gesserit Cunning converts selected whole advisor territories during your own Shipment and Movement action. All your counters in each selected territory become fighters together across sectors. It spends the Nexus card once, with no spice, shipment or movement cost.',