Compose original mixed E3 and three-family Nexus

This commit is contained in:
Quintine committed 2026-10-07 15:16:37 +11:00
1 parent 6935b8650d
commit 48b2755088
11 files changed
+255 -63

No files matched your search

+28 -6
View File
@@ -17,12 +17,34 @@ open mode gates.
## Current checkpoint and work
**In progress — mixed E3 and three-family Nexus.** Read-only mapping of
original Ecaz/Moritani plus selected E1/E2 and all-three-family native
Nexus admission and consumers, with Skills or no Skills and optional
original Homeworld/Discovery/Tech/Advanced Strongholds. Original selected
decks, physical setup and pending-ruling boundaries remain; no new
admission/runtime checkpoint or public activation is claimed.
**In progress — mixed E3 and three-family Nexus.** Original selected Ecaz
plus Ix and/or CHOAM now requires an actual native from every selected
family, with optional classics: Basic/Advanced2–6/all12 Nexus, optionally
all14 Skills/Homeworlds/Discovery7/8/Tech3+/Advanced Strongholds2+.
Original1536 admissions across36 roster families reach actual Traitors,
Skills or Ixian pre-training offers versus eight legacy combined admissions
before, on an unchanged source tree. The original generic Nexus/Skills and
the combined five/six-native entry now also mark fresh native Ecaz Occupy
and Advanced Moritani assassination, and the ordinary coalition, Bureaucrat,
Duke, Richese Cunning, serialized overlap and printed native-opponent
consumers are connected. Two real captured consumer omissions are repaired:
the CLI's fixed physical stock check now counts Ecaz's separately held
Loyalty card, and the engine's assassination inventory check now counts
Tleilaxu Face Dancers. Selected decks/cache, shared-Duke/capture,
force/payment and pending-effect guards remain.
Dedicated original programme evidence for this envelope is **not** yet
green: two independently authored programme pairs reached11/21 cases and
were withdrawn from this checkpoint rather than shipped failing. Recorded
engine constraints for resuming: the loser must dial for the winner to have
rescuable casualties; Ixian Suboids are half-strength so dials are
half-integers; support is capped by `maxCombatSupport`/`battleSupportBudget`;
a Worthless card is a defense, not a weapon; a normal Traitor call needs the
held card to match the opponent's plan leader; and the mixed-E3 Moritani
Terror/Cunning windows need their own real shipment. No new frozen
test/build/game/human checkpoint, public start or played-save conversion
is claimed.
**Mixed E1/E2 and paired E3 Nexus — bounded Development Verified.**
Original both-family Ix+CHOAM/union47 or both Ecaz+Moritani/ecaz33/classic
+2 -2
View File
@@ -1,4 +1,4 @@
import { homeworldLeaderSkillsProfile, classicNexusLeaderSkillsProfile, classicTechLeaderSkillsProfile, mixedE1E2NexusLeaderSkillsProfile, nativeDiscoveryLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, noOtherLeaderSkillModules, pairedE3NexusLeaderSkillsProfile, standaloneE3NexusLeaderSkillsProfile, strongholdLeaderSkillsProfile, type LeaderSkillProfile } from './leader-skill-profile';
import { homeworldLeaderSkillsProfile, classicNexusLeaderSkillsProfile, classicTechLeaderSkillsProfile, mixedE1E2NexusLeaderSkillsProfile, mixedE3NexusLeaderSkillsProfile, nativeDiscoveryLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, noOtherLeaderSkillModules, pairedE3NexusLeaderSkillsProfile, standaloneE3NexusLeaderSkillsProfile, strongholdLeaderSkillsProfile, type LeaderSkillProfile } from './leader-skill-profile';
import { classicDiscoveryLeaderSkillsProfile } from './discovery-module-profile';
export type BureaucratPaymentKind = 'auction' | 'shipment' | 'bribe' | 'revival';
export type BureaucratPaymentSource = {
@@ -36,7 +36,7 @@ export function bureaucratPaymentSignature(value: object): string {
return JSON.stringify({ ...value, signature: undefined });
}
export function bureaucratPaymentModeSupported(game: LeaderSkillProfile): boolean {
return mixedE1E2NexusLeaderSkillsProfile(game) || pairedE3NexusLeaderSkillsProfile(game) ||
return mixedE3NexusLeaderSkillsProfile(game) || mixedE1E2NexusLeaderSkillsProfile(game) || pairedE3NexusLeaderSkillsProfile(game) ||
(noOtherLeaderSkillModules(game) &&
(!game.expansions.length ||
(game.expansions.length === 1 && game.expansions[0] === 'choam') ||
+14 -2
View File
@@ -1,5 +1,5 @@
import { FACTIONS } from './catalog';
import type { FactionModuleProfile } from './faction-module-profile';
import { mixedE3NexusRosterProfile, type FactionModuleProfile } from './faction-module-profile';
import type { LeaderSkillProfile } from './leader-skill-profile';
/** Public unused-lobby admission only. The original initializer owns physical
@@ -17,7 +17,7 @@ export function homeworldDiscoveryProfile(game: FactionModuleProfile): boolean {
/** The same roster/module contract during play; live Discovery frames are expected. */
export function discoveryModeSupported(game: FactionModuleProfile): boolean {
if (game.nexusCards) return standaloneE3DiscoveryNexusProfile(game) || singleE1E2DiscoveryNexusProfile(game) ||
mixedE1E2DiscoveryNexusProfile(game) || pairedE3DiscoveryNexusProfile(game);
mixedE3DiscoveryNexusProfile(game) || mixedE1E2DiscoveryNexusProfile(game) || pairedE3DiscoveryNexusProfile(game);
if (typeof game.advanced !== 'boolean' || game.discoveryEnabled !== true ||
game.players.length < 2 || game.players.length > 6 ||
(game.techTokens && game.players.length < 3) || game.expansions.length > 2 ||
@@ -236,3 +236,15 @@ export function pairedE3DiscoveryNexusProfile(game: FactionModuleProfile): boole
}
return ecaz && moritani;
}
/** Original mixed E3 or all-three-family Nexus/Discovery without Skills. */
export function mixedE3DiscoveryNexusProfile(game: FactionModuleProfile): boolean {
return game.discoveryEnabled === true && !!game.nexusCards && !game.leaderSkills &&
(!game.techTokens || game.players.length >= 3) &&
(!game.strongholdCards || game.advanced) &&
!game.ecazTreachery && !game.semutaPreview && !game.advancedPreview &&
!game.kullPreview && !game.nexusKullPreview && !game.guildBetrayalPreview &&
!game.richeseBetrayalPreview && !game.nexusIxianReplacementPreview &&
!game.nexusIxianBetrayalPreview && !game.nexusHarkonnenBetrayalPreview &&
mixedE3NexusRosterProfile(game);
}
+34 -22
View File
@@ -1,7 +1,7 @@
import { classicNexusModulesProfile, mixedE1E2NexusProfile, pairedE3NexusProfile, pairedNexusModulesProfile, singleE1E2NexusProfile, standaloneE3NexusProfile } from './nexus-module-profile';
import { classicNexusModulesProfile, mixedE1E2NexusProfile, mixedE3NexusProfile, pairedE3NexusProfile, pairedNexusModulesProfile, singleE1E2NexusProfile, standaloneE3NexusProfile } from './nexus-module-profile';
import { nativeFactionTechProfile } from './faction-module-profile';
import { classicDiscoveryLeaderSkillsProfile, classicDiscoveryNexusProfile, homeworldDiscoveryProfile, discoveryModeSupported, discoveryModuleProfile, mixedE1E2DiscoveryNexusProfile, pairedDiscoveryNexusProfile, pairedE3DiscoveryNexusProfile, singleE1E2DiscoveryNexusProfile, standaloneE3DiscoveryNexusProfile } from './discovery-module-profile';
import { advancedMoritaniLeaderSkillsProfile, advancedNativeLeaderSkillsProfile, homeworldLeaderSkillsProfile, classicNexusLeaderSkillsProfile, classicTechLeaderSkillsProfile, mixedE1E2NexusLeaderSkillsProfile, nativeDiscoveryLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, ordinaryLeaderSkillModeSupported, pairedE3NexusLeaderSkillsProfile, pairedNexusLeaderSkillsProfile, singleE1E2NexusLeaderSkillsProfile, standaloneE3NexusLeaderSkillsProfile, strongholdLeaderSkillsProfile } from './leader-skill-profile';
import { classicDiscoveryLeaderSkillsProfile, classicDiscoveryNexusProfile, homeworldDiscoveryProfile, discoveryModeSupported, discoveryModuleProfile, mixedE1E2DiscoveryNexusProfile, mixedE3DiscoveryNexusProfile, pairedDiscoveryNexusProfile, pairedE3DiscoveryNexusProfile, singleE1E2DiscoveryNexusProfile, standaloneE3DiscoveryNexusProfile } from './discovery-module-profile';
import { advancedMoritaniLeaderSkillsProfile, advancedNativeLeaderSkillsProfile, homeworldLeaderSkillsProfile, classicNexusLeaderSkillsProfile, classicTechLeaderSkillsProfile, mixedE1E2NexusLeaderSkillsProfile, mixedE3NexusLeaderSkillsProfile, nativeDiscoveryLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, ordinaryLeaderSkillModeSupported, pairedE3NexusLeaderSkillsProfile, pairedNexusLeaderSkillsProfile, singleE1E2NexusLeaderSkillsProfile, standaloneE3NexusLeaderSkillsProfile, strongholdLeaderSkillsProfile } from './leader-skill-profile';
import { bribeTimingBlock, maximumBribe, type BribeOptions } from './bribe-options';
import { quoteSpicePlacement, stormExposesTerritory, stormSectorAfter, wormConsumesForces } from './disaster-rules';
import { isStormCardDistance, type StormCardComponent } from './storm-cards';
@@ -7612,7 +7612,8 @@ function e3StrongholdFactionProfile(g: Game): boolean {
return native !== null && (native !== 'moritani' || !byFaction(g, 'harkonnen'));
}
function moritaniAssassinateModeSupported(g: Game) {
if (standaloneE3NexusProfile(g) || pairedE3NexusProfile(g) || pairedE3NexusLeaderSkillsProfile(g))
if (mixedE3NexusProfile(g) || mixedE3NexusLeaderSkillsProfile(g) ||
standaloneE3NexusProfile(g) || pairedE3NexusProfile(g) || pairedE3NexusLeaderSkillsProfile(g))
return g.advanced && g.players.some(player => player.faction === 'moritani');
if (g.discoveryEnabled || g.discoveries)
return g.advanced && !!g.discoveries &&
@@ -7695,7 +7696,8 @@ function moritaniAssassinateIntegrity(g: Game) {
} else requireRule(!resume && g.decision?.kind !== 'moritaniAssassinate', 'A settled assassination cannot retain its decision.');
if (g.status === 'playing' || g.status === 'finished') {
const retired = state.opportunities.filter(r => r.stage === 'replaced').map(r => r.card!);
const physical = [...(g.traitorReserve ?? []),...g.players.flatMap(p => p.traitors),...retired].sort();
const physical = [...(g.traitorReserve ?? []), ...g.players.flatMap(p =>
[...p.traitors, ...(p.faceDancers ?? []).map(dancer => dancer.leader)]), ...retired].sort();
requireRule(JSON.stringify(physical) === JSON.stringify([...nexusTraitorUniverse(g)].sort()),
'Assassination must preserve every physical Traitor Card in hand, deck or face-up set-aside custody.');
for (const r of state.opportunities.filter(r => r.stage === 'revealed'))
@@ -8574,9 +8576,11 @@ export function initializeNexusGameForAudit(state: Game): Game {
'Enable Nexus cards in a fresh audit lobby first.');
const standalone = standaloneE3NexusProfile(state);
const pairedE3 = pairedE3NexusProfile(state);
const native = standalone || pairedE3 || singleE1E2NexusProfile(state) || mixedE1E2NexusProfile(state);
const mixedE3 = mixedE3NexusProfile(state);
const native = mixedE3 || standalone || pairedE3 || singleE1E2NexusProfile(state) || mixedE1E2NexusProfile(state);
const discovery = classicDiscoveryNexusProfile(state) || standaloneE3DiscoveryNexusProfile(state) ||
singleE1E2DiscoveryNexusProfile(state) || mixedE1E2DiscoveryNexusProfile(state) || pairedE3DiscoveryNexusProfile(state);
mixedE3DiscoveryNexusProfile(state) || singleE1E2DiscoveryNexusProfile(state) ||
mixedE1E2DiscoveryNexusProfile(state) || pairedE3DiscoveryNexusProfile(state);
if (state.discoveryEnabled) {
requireRule(discovery,
'Discovery Nexus requires a fresh supported original classic or native roster and module envelope.');
@@ -8584,8 +8588,8 @@ export function initializeNexusGameForAudit(state: Game): Game {
requireFreshFactionInventory(state);
}
const g = initializeSetupGameForAudit(state, !!state.homeworlds, true, false, discovery, false, false, native);
if ((standalone || pairedE3) && byFaction(g, 'ecaz')) g.ecazOccupyPreview = true;
if ((standalone || pairedE3) && g.advanced && byFaction(g, 'moritani')) initializeMoritaniAssassinateState(g);
if ((mixedE3 || standalone || pairedE3) && byFaction(g, 'ecaz')) g.ecazOccupyPreview = true;
if ((mixedE3 || standalone || pairedE3) && g.advanced && byFaction(g, 'moritani')) initializeMoritaniAssassinateState(g);
return g;
}
/** Fresh audit admission only; ordinary games and public starts never opt in. */
@@ -9378,10 +9382,13 @@ export function initializeCombinedNexusGameForAudit(state: Game, homeworlds = fa
.every(faction => state.players.some(player => player.faction === faction)),
'The combined Nexus sample requires five or six expansion factions and their three decks.');
requireRule(!!state.nexusCards && state.nexusCards.cards === null && state.nexusCards.phase === null &&
!!state.homeworlds === homeworlds && !state.leaderSkills && !state.discoveryEnabled &&
!state.ecazTreachery && !state.techTokens && !state.strongholdCards,
'Enable only the selected Nexus and Homeworld modules in a fresh combined expansion lobby.');
return initializeSetupGameForAudit(state, homeworlds, true, false, false, false, false, true);
!!state.homeworlds === homeworlds && mixedE3NexusProfile(state),
'Enable original Nexus, optional Homeworlds/Discovery/Tech/Advanced Strongholds in a fresh combined expansion lobby.');
const g = initializeSetupGameForAudit(state, homeworlds, true, false,
mixedE3DiscoveryNexusProfile(state), false, false, true);
if (byFaction(g, 'ecaz')) g.ecazOccupyPreview = true;
if (g.advanced && byFaction(g, 'moritani')) initializeMoritaniAssassinateState(g);
return g;
}
/** Offline prototype entry through real Ix setup, including both expansion factions.
@@ -9409,9 +9416,9 @@ export function initializeFactionExpansionsGameForAudit(state: Game): Game {
return initialized;
}
function ecazOccupyCompositionSupported(g: Game): boolean {
if (g.ecazOccupyPreview === true && (standaloneE3NexusProfile(g) || pairedE3NexusProfile(g)) &&
if (g.ecazOccupyPreview === true && (mixedE3NexusProfile(g) || standaloneE3NexusProfile(g) || pairedE3NexusProfile(g)) &&
g.players.some(player => player.faction === 'ecaz')) return true;
if (g.leaderSkills && (standaloneE3NexusLeaderSkillsProfile(g) || pairedE3NexusLeaderSkillsProfile(g)) &&
if (g.leaderSkills && (mixedE3NexusLeaderSkillsProfile(g) || standaloneE3NexusLeaderSkillsProfile(g) || pairedE3NexusLeaderSkillsProfile(g)) &&
g.players.some(p => p.faction === 'ecaz')) return true;
if (g.ecazOccupyPreview === true && g.discoveries && discoveryModeSupported(g) &&
g.players.some(p => p.faction === 'ecaz')) return true;
@@ -9574,13 +9581,15 @@ export function initializeLeaderSkillsGameForAudit(state: Game): Game {
g.leaderSkills = createLeaderSkills(random);
const classicNexus = classicNexusLeaderSkillsProfile(g);
const pairedE3 = pairedE3NexusLeaderSkillsProfile(g);
const mixedE3 = mixedE3NexusLeaderSkillsProfile(g);
const initialized = initializeSetupGameForAudit(g, homeworlds, classicNexus || pairedNexusLeaderSkillsProfile(g) ||
singleE1E2NexusLeaderSkillsProfile(g) || mixedE1E2NexusLeaderSkillsProfile(g) ||
standaloneE3NexusLeaderSkillsProfile(g) || pairedE3, false,
mixedE3 || standaloneE3NexusLeaderSkillsProfile(g) || pairedE3, false,
classicDiscoveryLeaderSkillsProfile(g) || nativeDiscoveryLeaderSkillsProfile(g) || (g.discoveryEnabled === true && classicNexus), true,
g.expansions.length === 1 && g.expansions[0] === 'choam', nativeExpansionLeaderSkillsProfile(g));
if (advancedMoritaniLeaderSkillsProfile(initialized) || (pairedE3 && initialized.advanced)) initializeMoritaniAssassinateState(initialized);
if ((homeworlds || nativeDiscoveryLeaderSkillsProfile(initialized) || pairedE3) && byFaction(initialized, 'ecaz')) initialized.ecazOccupyPreview = true;
if (advancedMoritaniLeaderSkillsProfile(initialized) || ((mixedE3 || pairedE3) && initialized.advanced &&
byFaction(initialized, 'moritani'))) initializeMoritaniAssassinateState(initialized);
if ((mixedE3 || homeworlds || nativeDiscoveryLeaderSkillsProfile(initialized) || pairedE3) && byFaction(initialized, 'ecaz')) initialized.ecazOccupyPreview = true;
return initialized;
}
/** Fresh all-fourteen-card classic or supported native skills; no public income toggle. */
@@ -9620,12 +9629,12 @@ function initializeSetupGameForAudit(state: Game, homeworlds: boolean, nexus = f
const nativeTech = factions && !leaderSkills && nativeFactionTechProfile(g);
const discoveryComposition = discovery && (discoveryModuleProfile(g) ||
classicDiscoveryNexusProfile(g) || pairedDiscoveryNexusProfile(g) ||
mixedE1E2DiscoveryNexusProfile(g) || pairedE3DiscoveryNexusProfile(g) ||
mixedE3DiscoveryNexusProfile(g) || mixedE1E2DiscoveryNexusProfile(g) || pairedE3DiscoveryNexusProfile(g) ||
(leaderSkills && (classicDiscoveryLeaderSkillsProfile(g) || nativeDiscoveryLeaderSkillsProfile(g) || classicNexusLeaderSkillsProfile(g))));
const nexusModules = nexus && (classicNexusModulesProfile(g) || pairedNexusModulesProfile(g) ||
singleE1E2NexusProfile(g) || mixedE1E2NexusProfile(g) || pairedE3NexusProfile(g) || standaloneE3NexusProfile(g) ||
mixedE3NexusProfile(g) || singleE1E2NexusProfile(g) || mixedE1E2NexusProfile(g) || pairedE3NexusProfile(g) || standaloneE3NexusProfile(g) ||
(leaderSkills && (classicNexusLeaderSkillsProfile(g) || pairedNexusLeaderSkillsProfile(g) ||
singleE1E2NexusLeaderSkillsProfile(g) || mixedE1E2NexusLeaderSkillsProfile(g) ||
mixedE3NexusLeaderSkillsProfile(g) || singleE1E2NexusLeaderSkillsProfile(g) || mixedE1E2NexusLeaderSkillsProfile(g) ||
pairedE3NexusLeaderSkillsProfile(g) || standaloneE3NexusLeaderSkillsProfile(g))));
requireRule(!g.techTokens || ((skillTech || nativeTech || nexusModules || discoveryComposition) &&
JSON.stringify(g.techTokens) === JSON.stringify(createTechTokens())),
@@ -14710,6 +14719,8 @@ function ambassadorEffectBlock(
return null;
}
function arrivalOverlapModeSupported(g: Game): boolean {
if ((mixedE3NexusLeaderSkillsProfile(g) || mixedE3NexusProfile(g)) &&
byFaction(g, 'ecaz') && byFaction(g, 'moritani')) return true;
return pairedE3NexusLeaderSkillsProfile(g) || pairedE3NexusProfile(g) ||
(!!byFaction(g, 'ecaz') && !!byFaction(g, 'moritani') &&
g.players.every(player => CLASSIC_FACTIONS[player.faction] === true ||
@@ -25771,7 +25782,8 @@ const RICHESE_CUNNING_ROSTER: Partial<Record<FactionId, true>> = {
beneGesserit: true, guild: true, richese: true, choam: true,
};
function richeseCunningModeSupported(g: Game) {
const mixed = mixedE1E2NexusProfile(g) || (!!g.leaderSkills && mixedE1E2NexusLeaderSkillsProfile(g));
const mixed = mixedE3NexusProfile(g) || mixedE1E2NexusProfile(g) ||
(!!g.leaderSkills && (mixedE3NexusLeaderSkillsProfile(g) || mixedE1E2NexusLeaderSkillsProfile(g)));
if (mixed) return !g.sandtrout;
const skills = !!g.leaderSkills && (pairedNexusLeaderSkillsProfile(g) || singleE1E2NexusLeaderSkillsProfile(g));
return pairedDiscoveryNexusProfile(g) || singleE1E2DiscoveryNexusProfile(g) ||
+35
View File
@@ -66,3 +66,38 @@ export function nativeFactionTechProfile(game: FactionModuleProfile): boolean {
return native > 0 && (!e3 || native === 1) &&
!(game.advanced && moritani && harkonnen);
}
export type MixedE3NativeRoster = {
advanced?: boolean;
expansions: readonly string[];
players?: readonly { faction: string }[];
};
/** Selected E3 plus E1 and/or E2, with an actual native from every selected family. */
export function mixedE3NexusRosterProfile(game: MixedE3NativeRoster, skills = false): boolean {
const players = game.players;
if (typeof game.advanced !== 'boolean' || !players || players.length < 2 || players.length > 6 ||
game.expansions.length < 2 || game.expansions.length > 3 || !game.expansions.includes('ecaz'))
return false;
for (let i = 0; i < game.expansions.length; i++) {
const expansion = game.expansions[i];
if (expansion !== 'ecaz' && expansion !== 'ix' && expansion !== 'choam') return false;
for (let j = 0; j < i; j++) if (expansion === game.expansions[j]) return false;
}
let ix = false, choam = false, e3 = false, moritani = false, harkonnen = false;
for (let i = 0; i < players.length; i++) {
const player = players[i];
for (let j = 0; j < i; j++) if (player.faction === players[j].faction) return false;
let family: string | undefined;
for (const faction of FACTIONS)
if (faction.id === player.faction) { family = faction.expansion; break; }
if (family === undefined || (family !== 'base' && !game.expansions.includes(family))) return false;
if (family === 'ix') ix = true;
if (family === 'choam') choam = true;
if (family === 'ecaz') e3 = true;
if (player.faction === 'moritani') moritani = true;
if (player.faction === 'harkonnen') harkonnen = true;
}
return e3 && ix === game.expansions.includes('ix') && choam === game.expansions.includes('choam') &&
!(game.advanced && harkonnen && (skills || moritani));
}
+9 -2
View File
@@ -1,4 +1,5 @@
import { FACTIONS } from './catalog';
import { mixedE3NexusRosterProfile } from './faction-module-profile';
import { classicDiscoveryLeaderSkillsProfile } from './discovery-module-profile';
/** Public configuration only; never gate a skill on a hidden card or random deal. */
@@ -167,6 +168,12 @@ export function pairedE3NexusLeaderSkillsProfile(game: LeaderSkillProfile): bool
return ecaz && moritani;
}
/** Original two/three selected families with native E3, Nexus and optional original modules. */
export function mixedE3NexusLeaderSkillsProfile(game: LeaderSkillProfile): boolean {
return nexusLeaderSkillModulesSupported(game, game.advanced === true, true) &&
mixedE3NexusRosterProfile(game, true);
}
/** Discovery keeps original native roster predicates; only this module envelope
* differs. Live token/entry/Great Maker frames are original consumers. */
function discoveryLeaderSkillModulesSupported(game: LeaderSkillProfile): boolean {
@@ -425,7 +432,7 @@ export function advancedNativeLeaderSkillsProfile(game: LeaderSkillProfile): boo
}
export function nativeExpansionLeaderSkillsProfile(game: LeaderSkillProfile): boolean {
return mixedE1E2NexusLeaderSkillsProfile(game) || pairedE3NexusLeaderSkillsProfile(game) ||
return mixedE3NexusLeaderSkillsProfile(game) || mixedE1E2NexusLeaderSkillsProfile(game) || pairedE3NexusLeaderSkillsProfile(game) ||
basicExpansionLeaderSkillsProfile(game) || advancedNativeLeaderSkillsProfile(game) ||
advancedMoritaniLeaderSkillsProfile(game) || nativeEcazLeaderSkillsProfile(game);
}
@@ -485,7 +492,7 @@ export function ordinaryLeaderSkillModeSupported(game: LeaderSkillProfile): bool
classicTechLeaderSkillsProfile(game) || nativeTechLeaderSkillsProfile(game) ||
strongholdLeaderSkillsProfile(game) || classicNexusLeaderSkillsProfile(game) ||
singleE1E2NexusLeaderSkillsProfile(game) ||
mixedE1E2NexusLeaderSkillsProfile(game) || pairedE3NexusLeaderSkillsProfile(game) ||
mixedE3NexusLeaderSkillsProfile(game) || mixedE1E2NexusLeaderSkillsProfile(game) || pairedE3NexusLeaderSkillsProfile(game) ||
pairedNexusLeaderSkillsProfile(game) || standaloneE3NexusLeaderSkillsProfile(game) ||
classicDiscoveryLeaderSkillsProfile(game) || nativeDiscoveryLeaderSkillsProfile(game) ||
homeworldLeaderSkillsProfile(game);
+4
View File
@@ -65,6 +65,10 @@ const opponents: readonly FactionId[] = [
'emperor',
'fremen',
'ecaz',
'ixians',
'tleilaxu',
'choam',
'richese',
];
const identifier = (value: unknown): value is string =>
typeof value === 'string' &&
+4 -4
View File
@@ -3,8 +3,8 @@ import type { Game } from './engine';
import { acquireDuke, DUKE_VIDAL_ID, type DukeSource } from './duke-vidal';
import { nexusCardMode } from './nexus-cards';
import { nexusCleanPlayBlocked } from './nexus-play-boundary';
import { pairedE3NexusLeaderSkillsProfile, standaloneE3NexusLeaderSkillsProfile } from './leader-skill-profile';
import { pairedE3NexusProfile, standaloneE3NexusProfile } from './nexus-module-profile';
import { mixedE3NexusLeaderSkillsProfile, pairedE3NexusLeaderSkillsProfile, standaloneE3NexusLeaderSkillsProfile } from './leader-skill-profile';
import { mixedE3NexusProfile, pairedE3NexusProfile, standaloneE3NexusProfile } from './nexus-module-profile';
const SUPPORTED_FACTIONS: Partial<Record<FactionId, true>> = {
ecaz: true, moritani: true, atreides: true, harkonnen: true,
@@ -88,11 +88,11 @@ export function quoteNexusEcazDuke(g: Game, owner: string, automaticPending = fa
}
export function nexusEcazDukeModeSupported(g: Game): boolean {
if ((standaloneE3NexusProfile(g) || pairedE3NexusProfile(g)) &&
if ((mixedE3NexusProfile(g) || standaloneE3NexusProfile(g) || pairedE3NexusProfile(g)) &&
g.players.some(player => player.faction === 'ecaz') &&
!g.sandtrout && !(g.advanced && g.players.some(player => player.faction === 'harkonnen')))
return !!g.nexusCards?.cards && g.players.length >= 3;
if (g.leaderSkills && (standaloneE3NexusLeaderSkillsProfile(g) || pairedE3NexusLeaderSkillsProfile(g)) &&
if (g.leaderSkills && (mixedE3NexusLeaderSkillsProfile(g) || standaloneE3NexusLeaderSkillsProfile(g) || pairedE3NexusLeaderSkillsProfile(g)) &&
g.players.some(player => player.faction === 'ecaz') && !g.sandtrout)
return !!g.nexusCards?.cards && g.players.length >= 3;
return !!g.nexusCards?.cards && g.players.length >= 3 && g.players.length <= 6 &&
+16 -2
View File
@@ -1,6 +1,6 @@
import { FACTIONS } from './catalog';
import type { FactionModuleProfile } from './faction-module-profile';
import { classicDiscoveryNexusProfile, mixedE1E2DiscoveryNexusProfile, pairedDiscoveryNexusProfile, pairedE3DiscoveryNexusProfile, singleE1E2DiscoveryNexusProfile, standaloneE3DiscoveryNexusProfile } from './discovery-module-profile';
import { mixedE3NexusRosterProfile, type FactionModuleProfile } from './faction-module-profile';
import { classicDiscoveryNexusProfile, mixedE1E2DiscoveryNexusProfile, mixedE3DiscoveryNexusProfile, pairedDiscoveryNexusProfile, pairedE3DiscoveryNexusProfile, singleE1E2DiscoveryNexusProfile, standaloneE3DiscoveryNexusProfile } from './discovery-module-profile';
/** Original classic Nexus modules, optionally Homeworlds; initializer checks undealt components. */
export function classicNexusModulesProfile(game: FactionModuleProfile): boolean {
@@ -172,3 +172,17 @@ export function pairedE3NexusProfile(game: FactionModuleProfile): boolean {
}
return ecaz && moritani;
}
/** Original mixed E3 or all-three-family Nexus without Skills, optionally original modules. */
export function mixedE3NexusProfile(game: FactionModuleProfile): boolean {
if (game.discoveryEnabled || game.discoveries) return mixedE3DiscoveryNexusProfile(game);
return !!game.nexusCards && !game.leaderSkills &&
(!game.techTokens || game.players.length >= 3) &&
(!game.strongholdCards || game.advanced) &&
!game.discoveryEnabled && !game.discoveries && !game.discoveryStash && !game.greatMaker &&
!game.ecazTreachery && !game.semutaPreview && !game.advancedPreview &&
!game.kullPreview && !game.nexusKullPreview && !game.guildBetrayalPreview &&
!game.richeseBetrayalPreview && !game.nexusIxianReplacementPreview &&
!game.nexusIxianBetrayalPreview && !game.nexusHarkonnenBetrayalPreview &&
mixedE3NexusRosterProfile(game);
}
+103 -18
View File
@@ -1,10 +1,10 @@
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { readFileSync, statSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { basename, dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseArgs } from 'node:util';
import { classicNexusModulesProfile, mixedE1E2NexusProfile, pairedE3NexusProfile, pairedNexusModulesProfile, singleE1E2NexusProfile, standaloneE3NexusProfile } from '../game/nexus-module-profile';
import { classicNexusModulesProfile, mixedE1E2NexusProfile, mixedE3NexusProfile, pairedE3NexusProfile, pairedNexusModulesProfile, singleE1E2NexusProfile, standaloneE3NexusProfile } from '../game/nexus-module-profile';
import { nativeFactionTechProfile } from '../game/faction-module-profile';
import { classicDiscoveryLeaderSkillsProfile } from '../game/discovery-module-profile';
import { botActions } from '../game/bots';
@@ -115,6 +115,24 @@ const MIXED_PAIRED_NEXUS_MODULE_PROFILES = [
'paired-e3-homeworld-nexus-discovery', 'paired-e3-homeworld-nexus-discovery-tech', 'paired-e3-homeworld-nexus-discovery-stronghold', 'paired-e3-homeworld-nexus-discovery-stronghold-tech',
'paired-e3-homeworld-nexus-discovery-skills', 'paired-e3-homeworld-nexus-discovery-skills-tech', 'paired-e3-homeworld-nexus-discovery-skills-stronghold', 'paired-e3-homeworld-nexus-discovery-skills-stronghold-tech',
] as const;
const MIXED_E3_NEXUS_MODULE_PROFILES = [
'mixed-e3-nexus', 'mixed-e3-nexus-tech', 'mixed-e3-nexus-stronghold', 'mixed-e3-nexus-stronghold-tech',
'mixed-e3-nexus-skills', 'mixed-e3-nexus-skills-tech', 'mixed-e3-nexus-skills-stronghold', 'mixed-e3-nexus-skills-stronghold-tech',
'mixed-e3-discovery-nexus', 'mixed-e3-discovery-nexus-tech', 'mixed-e3-discovery-nexus-stronghold', 'mixed-e3-discovery-nexus-stronghold-tech',
'mixed-e3-discovery-nexus-skills', 'mixed-e3-discovery-nexus-skills-tech', 'mixed-e3-discovery-nexus-skills-stronghold', 'mixed-e3-discovery-nexus-skills-stronghold-tech',
'mixed-e3-homeworld-nexus', 'mixed-e3-homeworld-nexus-tech', 'mixed-e3-homeworld-nexus-stronghold', 'mixed-e3-homeworld-nexus-stronghold-tech',
'mixed-e3-homeworld-nexus-skills', 'mixed-e3-homeworld-nexus-skills-tech', 'mixed-e3-homeworld-nexus-skills-stronghold', 'mixed-e3-homeworld-nexus-skills-stronghold-tech',
'mixed-e3-homeworld-nexus-discovery', 'mixed-e3-homeworld-nexus-discovery-tech', 'mixed-e3-homeworld-nexus-discovery-stronghold', 'mixed-e3-homeworld-nexus-discovery-stronghold-tech',
'mixed-e3-homeworld-nexus-discovery-skills', 'mixed-e3-homeworld-nexus-discovery-skills-tech', 'mixed-e3-homeworld-nexus-discovery-skills-stronghold', 'mixed-e3-homeworld-nexus-discovery-skills-stronghold-tech',
'three-family-nexus', 'three-family-nexus-tech', 'three-family-nexus-stronghold', 'three-family-nexus-stronghold-tech',
'three-family-nexus-skills', 'three-family-nexus-skills-tech', 'three-family-nexus-skills-stronghold', 'three-family-nexus-skills-stronghold-tech',
'three-family-discovery-nexus', 'three-family-discovery-nexus-tech', 'three-family-discovery-nexus-stronghold', 'three-family-discovery-nexus-stronghold-tech',
'three-family-discovery-nexus-skills', 'three-family-discovery-nexus-skills-tech', 'three-family-discovery-nexus-skills-stronghold', 'three-family-discovery-nexus-skills-stronghold-tech',
'three-family-homeworld-nexus', 'three-family-homeworld-nexus-tech', 'three-family-homeworld-nexus-stronghold', 'three-family-homeworld-nexus-stronghold-tech',
'three-family-homeworld-nexus-skills', 'three-family-homeworld-nexus-skills-tech', 'three-family-homeworld-nexus-skills-stronghold', 'three-family-homeworld-nexus-skills-stronghold-tech',
'three-family-homeworld-nexus-discovery', 'three-family-homeworld-nexus-discovery-tech', 'three-family-homeworld-nexus-discovery-stronghold', 'three-family-homeworld-nexus-discovery-stronghold-tech',
'three-family-homeworld-nexus-discovery-skills', 'three-family-homeworld-nexus-discovery-skills-tech', 'three-family-homeworld-nexus-discovery-skills-stronghold', 'three-family-homeworld-nexus-discovery-skills-stronghold-tech',
] as const;
type Profile = 'base' | 'choam' | 'ecaz' | 'ecaz-treachery' | 'ecaz-occupy' | 'moritani-assassinate' | 'stronghold-factions' | 'combined' | 'combined-nexus' | 'combined-homeworld-nexus' | 'homeworld' | 'nexus' | 'homeworld-nexus' | 'choam-roster' | 'ecaz-roster' | 'ix-roster' | 'choam-nexus' | 'ecaz-nexus' | 'ix-nexus' | 'moritani-skills' | 'tleilaxu-skills' | 'ix-skills' | 'choam-skills' | 'richese-skills' | 'ecaz-skills' | 'skills-tech' | 'skills-stronghold' | 'skills-stronghold-tech'
| 'factions-tech' | 'stronghold-factions-tech' | 'nexus-tech' | 'nexus-stronghold' | 'nexus-stronghold-tech'
| 'paired-nexus-tech' | 'paired-nexus-stronghold' | 'paired-nexus-stronghold-tech' | 'nexus-skills' | 'paired-nexus-skills'
@@ -134,7 +152,8 @@ type Profile = 'base' | 'choam' | 'ecaz' | 'ecaz-treachery' | 'ecaz-occupy' | 'm
| (typeof HOMEWORLD_NATIVE_NEXUS_MODULE_PROFILES)[number]
| (typeof E3_NEXUS_MODULE_PROFILES)[number]
| (typeof SINGLE_NATIVE_NEXUS_MODULE_PROFILES)[number]
| (typeof MIXED_PAIRED_NEXUS_MODULE_PROFILES)[number];
| (typeof MIXED_PAIRED_NEXUS_MODULE_PROFILES)[number]
| (typeof MIXED_E3_NEXUS_MODULE_PROFILES)[number];
const discoveryStrongholdProfile = (profile: string) => profile === 'discovery-stronghold' ||
profile === 'discovery-stronghold-tech' || profile === 'discovery-skills-stronghold' ||
profile === 'discovery-skills-stronghold-tech' || profile === 'native-discovery-skills-stronghold' ||
@@ -157,6 +176,13 @@ const nativeDiscoveryNexusSkillsProfile = (profile: string) =>
profile === 'paired-discovery-nexus-skills-stronghold' || profile === 'paired-discovery-nexus-skills-stronghold-tech' ||
profile === 'e3-discovery-nexus-skills' || profile === 'e3-discovery-nexus-skills-tech' ||
profile === 'e3-discovery-nexus-skills-stronghold' || profile === 'e3-discovery-nexus-skills-stronghold-tech';
const mixedE3NexusSampleProfile = (profile: string) =>
(MIXED_E3_NEXUS_MODULE_PROFILES as readonly string[]).includes(profile);
const threeFamilyNexusSampleProfile = (profile: string) =>
mixedE3NexusSampleProfile(profile) && profile.startsWith('three-family-');
// These original entry names predate native-power marks; resume their saved opt-ins, never retrofit them.
const originalNativeNexusSampleProfile = (profile: string) =>
profile === 'combined-nexus' || profile === 'combined-homeworld-nexus' || profile === 'ecaz-nexus';
const mixedPairedNexusSampleProfile = (profile: string) =>
(MIXED_PAIRED_NEXUS_MODULE_PROFILES as readonly string[]).includes(profile);
const pairedE3NexusSampleProfile = (profile: string) =>
@@ -168,7 +194,7 @@ const e3NexusSampleProfile = (profile: string) =>
const nativeHomeworldNexusSampleProfile = (profile: string) =>
(HOMEWORLD_NATIVE_NEXUS_MODULE_PROFILES as readonly string[]).includes(profile);
const homeworldNexusSampleProfile = (profile: string) =>
((e3NexusSampleProfile(profile) || singleNativeNexusSampleProfile(profile) || mixedPairedNexusSampleProfile(profile)) && profile.includes('-homeworld-')) ||
((e3NexusSampleProfile(profile) || singleNativeNexusSampleProfile(profile) || mixedPairedNexusSampleProfile(profile) || mixedE3NexusSampleProfile(profile)) && profile.includes('-homeworld-')) ||
(HOMEWORLD_NEXUS_MODULE_PROFILES as readonly string[]).includes(profile) || nativeHomeworldNexusSampleProfile(profile);
const homeworldModuleSampleProfile = (profile: string) =>
(HOMEWORLD_MODULE_PROFILES as readonly string[]).includes(profile) || homeworldNexusSampleProfile(profile);
@@ -316,16 +342,16 @@ const CLASSIC_NEXUS_MODULE_SCENARIOS: readonly Scenario[] =
(profile === 'nexus-stronghold' || scenario.roster.length >= 3))
.map((scenario, index): Scenario => ({ ...scenario, profile, ordinal: 800 + band * 100 + index })));
const discoveryNexusProfile = (profile: string) =>
((e3NexusSampleProfile(profile) || singleNativeNexusSampleProfile(profile) || mixedPairedNexusSampleProfile(profile)) && profile.includes('-discovery')) || profile === 'discovery-nexus' ||
((e3NexusSampleProfile(profile) || singleNativeNexusSampleProfile(profile) || mixedPairedNexusSampleProfile(profile) || mixedE3NexusSampleProfile(profile)) && profile.includes('-discovery')) || profile === 'discovery-nexus' ||
profile === 'discovery-nexus-tech' || profile === 'discovery-nexus-stronghold' ||
profile === 'discovery-nexus-stronghold-tech' || discoveryNexusSkillsProfile(profile) ||
pairedDiscoveryNexusSampleProfile(profile) || nativeDiscoveryNexusSkillsProfile(profile);
const nexusModuleProfile = (profile: string) =>
mixedPairedNexusSampleProfile(profile) || singleNativeNexusSampleProfile(profile) || e3NexusSampleProfile(profile) || homeworldNexusSampleProfile(profile) ||
mixedE3NexusSampleProfile(profile) || mixedPairedNexusSampleProfile(profile) || singleNativeNexusSampleProfile(profile) || e3NexusSampleProfile(profile) || homeworldNexusSampleProfile(profile) ||
profile === 'nexus-tech' || profile === 'nexus-stronghold' || profile === 'nexus-stronghold-tech' ||
discoveryNexusProfile(profile) || pairedNexusModuleProfile(profile) || nexusSkillsModuleProfile(profile) || pairedNexusSkillsModuleProfile(profile) || e3NexusSkillsProfile(profile);
const nexusTechProfile = (profile: string) =>
((e3NexusSampleProfile(profile) || singleNativeNexusSampleProfile(profile) || mixedPairedNexusSampleProfile(profile)) && profile.endsWith('-tech')) ||
((e3NexusSampleProfile(profile) || singleNativeNexusSampleProfile(profile) || mixedPairedNexusSampleProfile(profile) || mixedE3NexusSampleProfile(profile)) && profile.endsWith('-tech')) ||
((discoveryNexusSkillsProfile(profile) || pairedDiscoveryNexusSampleProfile(profile) || nativeDiscoveryNexusSkillsProfile(profile)) && profile.endsWith('tech')) ||
profile === 'discovery-nexus-tech' || profile === 'discovery-nexus-stronghold-tech' || profile === 'nexus-tech' || profile === 'nexus-stronghold-tech' ||
profile === 'paired-nexus-tech' || profile === 'paired-nexus-stronghold-tech' ||
@@ -333,7 +359,7 @@ const nexusTechProfile = (profile: string) =>
profile === 'paired-nexus-skills-tech' || profile === 'paired-nexus-skills-stronghold-tech' ||
profile === 'e3-nexus-skills-tech' || profile === 'e3-nexus-skills-stronghold-tech';
const nexusStrongholdProfile = (profile: string) =>
((e3NexusSampleProfile(profile) || singleNativeNexusSampleProfile(profile) || mixedPairedNexusSampleProfile(profile)) && profile.includes('stronghold')) ||
((e3NexusSampleProfile(profile) || singleNativeNexusSampleProfile(profile) || mixedPairedNexusSampleProfile(profile) || mixedE3NexusSampleProfile(profile)) && profile.includes('stronghold')) ||
((discoveryNexusSkillsProfile(profile) || pairedDiscoveryNexusSampleProfile(profile) || nativeDiscoveryNexusSkillsProfile(profile)) && profile.includes('stronghold')) ||
profile === 'discovery-nexus-stronghold' || profile === 'discovery-nexus-stronghold-tech' || profile === 'nexus-stronghold' || profile === 'nexus-stronghold-tech' ||
profile === 'paired-nexus-stronghold' || profile === 'paired-nexus-stronghold-tech' ||
@@ -651,6 +677,56 @@ const MIXED_PAIRED_NEXUS_MODULE_SCENARIOS: readonly Scenario[] =
.filter(scenario => (!nexusStrongholdProfile(profile) || scenario.rules === 'advanced') &&
(!nexusTechProfile(profile) || scenario.roster.length >= 3))
.map((scenario, index): Scenario => ({ ...scenario, profile, ordinal: 17400 + band * 100 + index })));
const MIXED_E3_NEXUS_BASE_SCENARIOS: readonly Scenario[] =
(['ecaz', 'moritani'] as const).flatMap(e3 =>
(['ixians', 'tleilaxu', 'choam', 'richese'] as const).flatMap(native =>
[2, 3, 4, 5, 6].flatMap(players => (['basic', 'advanced'] as const).map((rules): Scenario => ({
profile: 'mixed-e3-nexus', ordinal: 0, rules,
expansions: ['ecaz', native === 'ixians' || native === 'tleilaxu' ? 'ix' : 'choam'],
roster: ([e3, native, 'emperor', 'guild', 'fremen', 'beneGesserit'] as FactionId[]).slice(0, players),
}))))).concat(
(['ixians', 'tleilaxu', 'choam', 'richese'] as const).flatMap(native =>
[3, 4, 5, 6].flatMap(players => (['basic', 'advanced'] as const).map((rules): Scenario => ({
profile: 'mixed-e3-nexus', ordinal: 0, rules,
expansions: ['ecaz', native === 'ixians' || native === 'tleilaxu' ? 'ix' : 'choam'],
roster: (['ecaz', 'moritani', native, 'emperor', 'guild', 'fremen'] as FactionId[]).slice(0, players),
})))),
([['ixians', 'tleilaxu'], ['choam', 'richese']] as const).flatMap(natives =>
[4, 5, 6].flatMap(players => (['basic', 'advanced'] as const).map((rules): Scenario => ({
profile: 'mixed-e3-nexus', ordinal: 0, rules,
expansions: ['ecaz', natives[0] === 'ixians' ? 'ix' : 'choam'],
roster: (['ecaz', 'moritani', ...natives, 'guild', 'fremen'] as FactionId[]).slice(0, players),
})))),
);
const THREE_FAMILY_NEXUS_BASE_SCENARIOS: readonly Scenario[] =
(['ecaz', 'moritani'] as const).flatMap(e3 =>
(['ixians', 'tleilaxu'] as const).flatMap(e1 =>
(['choam', 'richese'] as const).flatMap(e2 =>
[3, 4, 5, 6].flatMap(players => (['basic', 'advanced'] as const).map((rules): Scenario => ({
profile: 'three-family-nexus', ordinal: 0, rules, expansions: ['ix', 'choam', 'ecaz'],
roster: ([e3, e1, e2, 'emperor', 'guild', 'fremen'] as FactionId[]).slice(0, players),
})))))).concat(
(['ixians', 'tleilaxu'] as const).flatMap(e1 =>
(['choam', 'richese'] as const).flatMap(e2 =>
[4, 5, 6].flatMap(players => (['basic', 'advanced'] as const).map((rules): Scenario => ({
profile: 'three-family-nexus', ordinal: 0, rules, expansions: ['ix', 'choam', 'ecaz'],
roster: (['ecaz', 'moritani', e1, e2, 'guild', 'fremen'] as FactionId[]).slice(0, players),
}))))),
[5, 6].flatMap(players => (['basic', 'advanced'] as const).map((rules): Scenario => ({
profile: 'three-family-nexus', ordinal: 0, rules, expansions: ['ix', 'choam', 'ecaz'],
roster: (['ecaz', 'moritani', 'ixians', 'tleilaxu', 'choam', 'guild'] as FactionId[]).slice(0, players),
}))),
(['basic', 'advanced'] as const).map((rules): Scenario => ({
profile: 'three-family-nexus', ordinal: 0, rules, expansions: ['ix', 'choam', 'ecaz'],
roster: ['ecaz', 'moritani', 'ixians', 'tleilaxu', 'choam', 'richese'],
})),
);
const MIXED_E3_NEXUS_MODULE_SCENARIOS: readonly Scenario[] =
MIXED_E3_NEXUS_MODULE_PROFILES.flatMap((profile, band) =>
(threeFamilyNexusSampleProfile(profile) ? THREE_FAMILY_NEXUS_BASE_SCENARIOS : MIXED_E3_NEXUS_BASE_SCENARIOS)
.filter(scenario => (!nexusStrongholdProfile(profile) || scenario.rules === 'advanced') &&
(!nexusTechProfile(profile) || scenario.roster.length >= 3))
.map((scenario, index): Scenario => ({ ...scenario, profile, ordinal: 23800 + band * 200 + index })));
const STRONGHOLD_FACTIONS_ROSTER: readonly FactionId[] = [
'ixians', 'choam', 'emperor', 'fremen', 'harkonnen', 'beneGesserit',
];
@@ -729,14 +805,14 @@ const SKILLS_STRONGHOLD_SCENARIOS: readonly Scenario[] =
const strongholdSkillProfile = (profile: string) => profile === 'skills-stronghold' || profile === 'skills-stronghold-tech';
const discoverySkillsProfile = (profile: string) => profile === 'discovery-skills' || profile === 'discovery-skills-tech' ||
profile === 'discovery-skills-stronghold' || profile === 'discovery-skills-stronghold-tech';
const skillProfile = (profile: string) => ((singleNativeNexusSampleProfile(profile) || mixedPairedNexusSampleProfile(profile)) && profile.includes('-skills')) ||
const skillProfile = (profile: string) => ((singleNativeNexusSampleProfile(profile) || mixedPairedNexusSampleProfile(profile) || mixedE3NexusSampleProfile(profile)) && profile.includes('-skills')) ||
homeworldSkillsSampleProfile(profile) || nativeDiscoveryNexusSkillsProfile(profile) || discoveryNexusSkillsProfile(profile) || nativeDiscoverySkillsProfile(profile) || discoverySkillsProfile(profile) || e3NexusSkillsProfile(profile) || pairedNexusSkillsModuleProfile(profile) || nexusSkillsModuleProfile(profile) || profile === 'nexus-skills' || profile === 'paired-nexus-skills' || profile === 'moritani-skills' || profile === 'tleilaxu-skills' || profile === 'ix-skills' || profile === 'choam-skills' || profile === 'richese-skills' || profile === 'ecaz-skills' || profile === 'skills-tech' || strongholdSkillProfile(profile);
const discoveryProfile = (profile: string) => profile === 'discovery' || profile === 'discovery-tech' ||
profile === 'discovery-stronghold' || profile === 'discovery-stronghold-tech' ||
(homeworldDiscoverySampleProfile(profile) && !homeworldSkillsSampleProfile(profile));
const assassinationScenario = (scenario: Scenario) => scenario.rules === 'advanced' &&
scenario.roster.includes('moritani') &&
(pairedE3NexusSampleProfile(scenario.profile) || e3NexusSampleProfile(scenario.profile) || scenario.profile === 'moritani-assassinate' || discoveryProfile(scenario.profile) || nativeStrongholdProfile(scenario.profile) || nativeTechProfile(scenario.profile) || skillProfile(scenario.profile));
(mixedE3NexusSampleProfile(scenario.profile) || pairedE3NexusSampleProfile(scenario.profile) || e3NexusSampleProfile(scenario.profile) || scenario.profile === 'moritani-assassinate' || discoveryProfile(scenario.profile) || nativeStrongholdProfile(scenario.profile) || nativeTechProfile(scenario.profile) || skillProfile(scenario.profile));
type TraceEntry = {
attempt: number;
@@ -775,9 +851,10 @@ function usage() {
return (
'Usage: node --import tsx tools/faction-games.ts --out NEW_PRIVATE_DIR ' +
'[--seed UINT32] [--profile all|base|discovery|discovery-tech|discovery-skills|discovery-skills-tech|discovery-nexus|discovery-nexus-tech|discovery-nexus-stronghold|discovery-nexus-stronghold-tech|choam|ecaz|ecaz-treachery|ecaz-occupy|moritani-assassinate|stronghold-factions|factions-tech|stronghold-factions-tech|combined|combined-nexus|combined-homeworld-nexus|homeworld|homeworld-occupation|nexus|nexus-skills|paired-nexus-skills|nexus-skills-tech|nexus-skills-stronghold|nexus-skills-stronghold-tech|paired-nexus-skills-tech|paired-nexus-skills-stronghold|paired-nexus-skills-stronghold-tech|e3-nexus-skills|e3-nexus-skills-tech|e3-nexus-skills-stronghold|e3-nexus-skills-stronghold-tech|nexus-tech|nexus-stronghold|nexus-stronghold-tech|paired-nexus-tech|paired-nexus-stronghold|paired-nexus-stronghold-tech|homeworld-nexus|choam-roster|ecaz-roster|ix-roster|choam-nexus|ecaz-nexus|ix-nexus|moritani-skills|tleilaxu-skills|ix-skills|choam-skills|richese-skills|ecaz-skills|skills-tech|skills-stronghold|skills-stronghold-tech|' +
MIXED_PAIRED_NEXUS_MODULE_PROFILES.join('|') + '|' + SINGLE_NATIVE_NEXUS_MODULE_PROFILES.join('|') + '|' + E3_NEXUS_MODULE_PROFILES.join('|') + '] ' +
MIXED_E3_NEXUS_MODULE_PROFILES.join('|') + '|' + MIXED_PAIRED_NEXUS_MODULE_PROFILES.join('|') + '|' + SINGLE_NATIVE_NEXUS_MODULE_PROFILES.join('|') + '|' + E3_NEXUS_MODULE_PROFILES.join('|') + '] ' +
'[--rules both|basic|advanced] [--players all|2|3|4|5|6] [--max-actions POSITIVE] ' +
'[--resume FAILED_GAME.json]\n' +
'Mixed E3 and three-family Nexus: mixed-e3-nexus/mixed-e3-discovery-nexus/mixed-e3-homeworld-nexus/mixed-e3-homeworld-nexus-discovery, or three-family versions, each optional -skills then -tech/-stronghold/-stronghold-tech. Selected Ecaz plus Ix and/or CHOAM with at least one actual native from EVERY selected family, classics allowed; original47/35 decks and separate native cache, all12 Nexus/all14 only with Skills. Basic/Advanced2–6, Tech3+, Strongholds Advanced, original Ecaz Occupy/Advanced Moritani assassination and ordinary native setup. Shared Duke never trains; existing capture, rounding, payment and pending-effect guards remain. New23800–36400 bands use200-wide source slots; old names/seeds unchanged. No public start or played-game conversion.\n' +
'Mixed E1/E2 and paired E3 Nexus: mixed-nexus/mixed-discovery-nexus/mixed-homeworld-nexus/mixed-homeworld-nexus-discovery, or paired-e3-nexus/paired-e3-discovery-nexus/paired-e3-homeworld-nexus/paired-e3-homeworld-nexus-discovery, each optional -skills then -tech/-stronghold/-stronghold-tech. Both original E1/E2 native families with ix+choam union47, or BOTH Ecaz+Moritani/ecaz33 with classics; all12 Nexus and all14 only when Skills selected, Basic/Advanced2–6, Tech3+, Strongholds Advanced. Original native setup, Duke/assassination/capture/rounding/income/card guards remain; no public starts or save conversion. New17400–23700 bands leave older names/seeds unchanged.\n' +
'Single-native E1/E2: single-nexus and single-discovery-nexus, single-homeworld-nexus and single-homeworld-nexus-discovery, each with optional -skills and -tech/-stronghold/-stronghold-tech. Exactly one original Ixians OR Tleilaxu OR CHOAM OR Richese with classics, one family deck/all12 and all14 only when Skills selected. Basic/Advanced2–6, Tech3+, Strongholds Advanced; original setup/effects/ruling guards remain. Stable new14200–17300 bands; older names/seeds unchanged.\n' +
'Standalone E3 without Skills: e3-nexus, e3-discovery-nexus, e3-homeworld-nexus and e3-homeworld-nexus-discovery, each optionally -tech, -stronghold or -stronghold-tech. Original Ecaz OR Moritani/classic ecaz33/all12 setup, Basic/Advanced2–6, Tech3+, Strongholds Advanced, no training/public starts/save conversion. Native Duke/Occupy and Moritani traitor/Terror/assassination retain their original source and ruling boundaries. Stable new12600–14100 bands leave older names/seeds unchanged.\n' +
@@ -839,7 +916,7 @@ function positive(value: string | undefined) {
}
function scenarioName(scenario: Scenario) {
if (mixedPairedNexusSampleProfile(scenario.profile) || singleNativeNexusSampleProfile(scenario.profile) || e3NexusSampleProfile(scenario.profile) || homeworldModuleSampleProfile(scenario.profile))
if (mixedE3NexusSampleProfile(scenario.profile) || mixedPairedNexusSampleProfile(scenario.profile) || singleNativeNexusSampleProfile(scenario.profile) || e3NexusSampleProfile(scenario.profile) || homeworldModuleSampleProfile(scenario.profile))
return `${scenario.profile}-${scenario.expansions.join('-') || 'classic'}-${scenario.roster.join('-')}-${scenario.roster.length}-${scenario.rules}`;
if (nativeDiscoverySkillsProfile(scenario.profile) || discoveryNexusSkillsProfile(scenario.profile) ||
pairedDiscoveryNexusSampleProfile(scenario.profile) || nativeDiscoveryNexusSkillsProfile(scenario.profile))
@@ -1011,11 +1088,13 @@ function resumedGame(path: string) {
if (
(game.leaderSkills && !nativeExpansionLeaderSkillsProfile(game) && !composedSkills) ||
((game.discoveryEnabled || game.discoveries || game.discoveryStash || game.greatMaker) && !discovery) ||
(game.techTokens && !discovery && !composedSkills && !nativeFactionTechProfile(game) && !classicNexusModulesProfile(game) && !pairedNexusModulesProfile(game) && !mixedE1E2NexusProfile(game) && !pairedE3NexusProfile(game) && !singleE1E2NexusProfile(game) && !standaloneE3NexusProfile(game)) ||
(game.techTokens && !discovery && !composedSkills && !nativeFactionTechProfile(game) && !classicNexusModulesProfile(game) && !pairedNexusModulesProfile(game) && !mixedE3NexusProfile(game) && !mixedE1E2NexusProfile(game) && !pairedE3NexusProfile(game) && !singleE1E2NexusProfile(game) && !standaloneE3NexusProfile(game)) ||
game.mentatQuestionPreview
)
throw new Error('--resume sample scenarios exclude unsupported optional modules.');
const scenario = [...SCENARIOS, ...MIXED_PAIRED_NEXUS_MODULE_SCENARIOS, ...SINGLE_NATIVE_NEXUS_MODULE_SCENARIOS, ...E3_NEXUS_MODULE_SCENARIOS, ...HOMEWORLD_NATIVE_NEXUS_MODULE_SCENARIOS, ...HOMEWORLD_CLASSIC_NEXUS_MODULE_SCENARIOS, ...COMBINED_NEXUS_SCENARIOS, ...COMBINED_HOMEWORLD_NEXUS_SCENARIOS, ...BASE_SCENARIOS, ...MODULE_SCENARIOS, ...EXPANSION_ROSTER_SCENARIOS, ...ECAZ_TREACHERY_SCENARIOS, ...ECAZ_OCCUPY_SCENARIOS, ...PAIRED_NEXUS_SCENARIOS, ...MORITANI_ASSASSINATE_SCENARIOS, ...STRONGHOLD_FACTIONS_SCENARIOS, ...MORITANI_SKILLS_SCENARIOS, ...TLEILAXU_SKILLS_SCENARIOS, ...IX_SKILLS_SCENARIOS, ...CHOAM_SKILLS_SCENARIOS, ...RICHESE_SKILLS_SCENARIOS, ...ECAZ_SKILLS_SCENARIOS, ...SKILLS_TECH_SCENARIOS, ...SKILLS_STRONGHOLD_SCENARIOS, ...NATIVE_TECH_SCENARIOS, ...CLASSIC_NEXUS_MODULE_SCENARIOS, ...PAIRED_NEXUS_MODULE_SCENARIOS, ...NEXUS_SKILLS_SCENARIOS, ...PAIRED_NEXUS_SKILLS_SCENARIOS, ...NEXUS_SKILLS_MODULE_SCENARIOS, ...PAIRED_NEXUS_SKILLS_MODULE_SCENARIOS, ...E3_NEXUS_SKILLS_SCENARIOS, ...HOMEWORLD_OCCUPATION_SCENARIOS, ...DISCOVERY_SCENARIOS, ...DISCOVERY_SKILLS_SCENARIOS, ...DISCOVERY_NEXUS_SCENARIOS, ...DISCOVERY_STRONGHOLD_SCENARIOS, ...NATIVE_DISCOVERY_SKILLS_SCENARIOS, ...DISCOVERY_NEXUS_SKILLS_SCENARIOS, ...PAIRED_DISCOVERY_NEXUS_SCENARIOS, ...NATIVE_DISCOVERY_NEXUS_SKILLS_SCENARIOS, ...HOMEWORLD_CLASSIC_MODULE_SCENARIOS, ...HOMEWORLD_NATIVE_MODULE_SCENARIOS].find((candidate) =>
const sourceName = basename(path), namedSource = sourceName.startsWith('failed-');
const scenario = [...SCENARIOS, ...MIXED_E3_NEXUS_MODULE_SCENARIOS, ...MIXED_PAIRED_NEXUS_MODULE_SCENARIOS, ...SINGLE_NATIVE_NEXUS_MODULE_SCENARIOS, ...E3_NEXUS_MODULE_SCENARIOS, ...HOMEWORLD_NATIVE_NEXUS_MODULE_SCENARIOS, ...HOMEWORLD_CLASSIC_NEXUS_MODULE_SCENARIOS, ...COMBINED_NEXUS_SCENARIOS, ...COMBINED_HOMEWORLD_NEXUS_SCENARIOS, ...BASE_SCENARIOS, ...MODULE_SCENARIOS, ...EXPANSION_ROSTER_SCENARIOS, ...ECAZ_TREACHERY_SCENARIOS, ...ECAZ_OCCUPY_SCENARIOS, ...PAIRED_NEXUS_SCENARIOS, ...MORITANI_ASSASSINATE_SCENARIOS, ...STRONGHOLD_FACTIONS_SCENARIOS, ...MORITANI_SKILLS_SCENARIOS, ...TLEILAXU_SKILLS_SCENARIOS, ...IX_SKILLS_SCENARIOS, ...CHOAM_SKILLS_SCENARIOS, ...RICHESE_SKILLS_SCENARIOS, ...ECAZ_SKILLS_SCENARIOS, ...SKILLS_TECH_SCENARIOS, ...SKILLS_STRONGHOLD_SCENARIOS, ...NATIVE_TECH_SCENARIOS, ...CLASSIC_NEXUS_MODULE_SCENARIOS, ...PAIRED_NEXUS_MODULE_SCENARIOS, ...NEXUS_SKILLS_SCENARIOS, ...PAIRED_NEXUS_SKILLS_SCENARIOS, ...NEXUS_SKILLS_MODULE_SCENARIOS, ...PAIRED_NEXUS_SKILLS_MODULE_SCENARIOS, ...E3_NEXUS_SKILLS_SCENARIOS, ...HOMEWORLD_OCCUPATION_SCENARIOS, ...DISCOVERY_SCENARIOS, ...DISCOVERY_SKILLS_SCENARIOS, ...DISCOVERY_NEXUS_SCENARIOS, ...DISCOVERY_STRONGHOLD_SCENARIOS, ...NATIVE_DISCOVERY_SKILLS_SCENARIOS, ...DISCOVERY_NEXUS_SKILLS_SCENARIOS, ...PAIRED_DISCOVERY_NEXUS_SCENARIOS, ...NATIVE_DISCOVERY_NEXUS_SKILLS_SCENARIOS, ...HOMEWORLD_CLASSIC_MODULE_SCENARIOS, ...HOMEWORLD_NATIVE_MODULE_SCENARIOS].find((candidate) =>
(!namedSource || sourceName === `failed-${scenarioName(candidate)}.json`) &&
(homeworldDiscoverySampleProfile(candidate.profile) || nativeDiscoverySkillsProfile(candidate.profile) || discoveryProfile(candidate.profile) || discoverySkillsProfile(candidate.profile) || discoveryNexusProfile(candidate.profile)) === discovery &&
(homeworldModuleSampleProfile(candidate.profile) || candidate.profile === 'homeworld' || candidate.profile === 'homeworld-occupation' || candidate.profile === 'homeworld-nexus' || candidate.profile === 'combined-homeworld-nexus') === !!game.homeworlds &&
(candidate.profile === 'paired-nexus-skills' || candidate.profile === 'nexus-skills' || candidate.profile === 'nexus' || nexusModuleProfile(candidate.profile) || candidate.profile === 'homeworld-nexus' || candidate.profile === 'choam-nexus' || candidate.profile === 'ecaz-nexus' || candidate.profile === 'ix-nexus' || candidate.profile === 'combined-nexus' || candidate.profile === 'combined-homeworld-nexus') === !!game.nexusCards &&
@@ -1024,9 +1103,12 @@ function resumedGame(path: string) {
(homeworldTechSampleProfile(candidate.profile) || discoveryTechProfile(candidate.profile) || candidate.profile === 'skills-tech' || candidate.profile === 'skills-stronghold-tech' || nativeTechProfile(candidate.profile) || nexusTechProfile(candidate.profile)) === !!game.techTokens &&
(homeworldStrongholdSampleProfile(candidate.profile) || discoveryStrongholdProfile(candidate.profile) || nativeStrongholdProfile(candidate.profile) || strongholdSkillProfile(candidate.profile) || nexusStrongholdProfile(candidate.profile)) === !!game.strongholdCards &&
(candidate.profile === 'ecaz-treachery' || candidate.ecazTreachery === true) === !!game.ecazTreachery &&
assassinationScenario(candidate) === !!game.moritaniAssassinatePreview &&
(candidate.profile === 'ecaz-occupy' || ((pairedE3NexusSampleProfile(candidate.profile) || e3NexusSampleProfile(candidate.profile) || homeworldModuleSampleProfile(candidate.profile) || nativeDiscoveryNexusSkillsProfile(candidate.profile) || nativeDiscoverySkillsProfile(candidate.profile) || nativeTechProfile(candidate.profile) || discoveryProfile(candidate.profile)) && candidate.roster.includes('ecaz'))) === !!game.ecazOccupyPreview &&
assassinationScenario(candidate) === !!game.moritaniAssassinate &&
(originalNativeNexusSampleProfile(candidate.profile)
? !!game.moritaniAssassinatePreview === !!game.moritaniAssassinate
: assassinationScenario(candidate) === !!game.moritaniAssassinatePreview &&
assassinationScenario(candidate) === !!game.moritaniAssassinate) &&
(originalNativeNexusSampleProfile(candidate.profile) ||
(candidate.profile === 'ecaz-occupy' || ((mixedE3NexusSampleProfile(candidate.profile) || pairedE3NexusSampleProfile(candidate.profile) || e3NexusSampleProfile(candidate.profile) || homeworldModuleSampleProfile(candidate.profile) || nativeDiscoveryNexusSkillsProfile(candidate.profile) || nativeDiscoverySkillsProfile(candidate.profile) || nativeTechProfile(candidate.profile) || discoveryProfile(candidate.profile)) && candidate.roster.includes('ecaz'))) === !!game.ecazOccupyPreview) &&
candidate.rules === (game.advanced ? 'advanced' : 'basic') &&
JSON.stringify(candidate.expansions) ===
JSON.stringify(game.expansions) &&
@@ -1229,6 +1311,8 @@ async function main() {
throw new Error('--players requires a roster or optional-module profile.');
if ((profile === 'combined-nexus' || profile === 'combined-homeworld-nexus') && players !== 'all' && ![5, 6].includes(players))
throw new Error(`--profile ${profile} supports only five or six players.`);
if (threeFamilyNexusSampleProfile(profile) && players === 2)
throw new Error('Three-family Nexus samples require three through six players.');
if (profile === 'moritani-assassinate' && rules === 'basic')
throw new Error('Moritani assassination samples require Advanced rules.');
if ((homeworldStrongholdSampleProfile(profile) || profile === 'stronghold-factions' || strongholdSkillProfile(profile) || nexusStrongholdProfile(profile)) && rules === 'basic')
@@ -1238,7 +1322,8 @@ async function main() {
if ((discoveryTechProfile(profile) || (discoveryNexusProfile(profile) && nexusTechProfile(profile))) && players === 2)
throw new Error('Discovery with Tech Tokens requires three through six players.');
const resume = values.resume ? resumedGame(values.resume) : null;
const requestedSamples = mixedPairedNexusSampleProfile(profile) ? MIXED_PAIRED_NEXUS_MODULE_SCENARIOS :
const requestedSamples = mixedE3NexusSampleProfile(profile) ? MIXED_E3_NEXUS_MODULE_SCENARIOS :
mixedPairedNexusSampleProfile(profile) ? MIXED_PAIRED_NEXUS_MODULE_SCENARIOS :
singleNativeNexusSampleProfile(profile) ? SINGLE_NATIVE_NEXUS_MODULE_SCENARIOS :
e3NexusSampleProfile(profile) ? E3_NEXUS_MODULE_SCENARIOS : homeworldModuleSampleProfile(profile)
? (nativeHomeworldNexusSampleProfile(profile) ? HOMEWORLD_NATIVE_NEXUS_MODULE_SCENARIOS
+6 -5
View File
@@ -35,9 +35,10 @@ async function main() {
'\nNexus preserves selected unused original Tech Tokens in classic/base33/all12 three-through-six-seat Basic/Advanced lobbies, and/or unused Stronghold Cards in Advanced two-through-six-seat lobbies; both requires3+. Existing Homeworlds may be retained. Genuine first-Storm/end-Mentat custody and closing Nexus deals remain. Borrowed tariff/force identity, original free-return ledgers, phase-end industry, held support and retention/winner rewards reuse existing handlers. Skills, expansion families outside their documented entries, unrelated previews, unresolved effects and public starts remain separate.',
'\nNexus additionally routes a supported paired native E1 (Ixians+Tleilaxu) OR E2 (CHOAM+Richese) lobby with classics and one required deck through original paired setup when Tech and/or Advanced Strongholds is selected. Tech requires3–6 Basic/Advanced, Strongholds2–6 Advanced, both3–6 Advanced. All12 Nexus, original47/35 decks/cache, native setup and actual module custody remain. Mixed families, E3, Skills, other overlays, public starts and pending rulings stay separate.',
'\nNexus additionally admits standalone Ecaz OR Moritani with classics and the original ecaz33/all12 deck without Skills, Basic/Advanced2–6, optionally original Homeworlds, Discovery through --discoveries, Tech3+ and Advanced Strongholds2+. Original Traitors/native setup and qualifying end-Spice closing draw remain; no starting Nexus deal or training is added. Ecaz Arrakis Occupy and quiet living-Duke Cunning retain their own boundaries; an otherwise legal Advanced Ecaz/Harkonnen roster does not lift the separate Cunning exclusion. Advanced Moritani excludes Harkonnen and uses original post-loss assassination/normal-call forfeiture, private replacement and supply-only Terror Cunning. E3 pairs/mixed families, native free-return/relocation rulings, unrelated previews, public starts and save conversion stay separate.',
'\nNexus additionally admits exactly one native Ixians OR Tleilaxu with the Ix47 deck, or CHOAM OR Richese with CHOAM35 and the original Richese cache when seated, plus classic opponents, Basic/Advanced2–6. Original Homeworlds may be retained; --discoveries preserves Discovery7/8, Tech requires3+ and Strongholds require Advanced2+. Without Skills it reuses original faction/Traitor/native setup; leader-skills --nexus-cards additionally retains all14 offered training. Original Ix offer/HMS, Tleilaxu Face Dancers, CHOAM Auditor and signed Richese markers remain physical. The paired entry still requires both native factions. Mixed-family Nexus, E3pairs, public starts, played-save conversion and pending effect rulings remain separate.',
'\nNexus additionally admits exactly one native Ixians OR Tleilaxu/Ix47, or CHOAM OR Richese/CHOAM35 with its original cache, plus classic opponents, Basic/Advanced2–6. Original Homeworlds/Discovery7/8/Tech3+/Advanced Strongholds2+ may remain; leader-skills --nexus-cards retains all14 original offers. Ix offer/HMS, Tleilaxu Face Dancers, CHOAM Auditor and signed Richese markers remain physical. Original paired entries require both natives; additional exact mixed/paired E3 compositions are below. Public starts, played-save conversion and pending effect rulings remain separate.',
'\nThe original Nexus and Skills/Nexus entries additionally admit natives from BOTH selected Ix+CHOAM families with classics and union47 Treachery Cards, or BOTH Ecaz+Moritani with classics/ecaz33, Basic/Advanced2–6. Optional Homeworlds/Discovery7+8/Tech3+/Advanced Strongholds2+ retain original setup and consumers. Richese cache stays separate; paired E3 preserves ordinary native training, untrained shared Duke, Arrakis Occupy and Advanced assassination, excluding Advanced Harkonnen. Existing mixed-family free-return/companion, own concealed-plan, Sandtrout, low-income/cache-count, shared/captured training and Terror relocation questions remain guarded. No public starts or played-save conversion.',
'\nLeader-skills --nexus-cards explicitly adds all12 Nexus to fresh classic/base33, original single or paired E1/E2 with one family deck, both selected Ix+CHOAM native families with union47, or standalone/paired Ecaz/Moritani with ecaz33 and classics, all14 skills, two-through-six seats Basic/Advanced. Existing Homeworlds may be retained. Original starting hands/offers, skill/Traitor/native order and genuine closing deals remain. Each native roster/capture guard, Banker/Mentat previews, borrowed Smuggler/pair-companion arithmetic, public starts and unresolved interactions retain their boundaries.',
'\nThe same original Nexus/Skills-Nexus entries now compose selected Ecaz plus Ix and/or CHOAM, with at least one actual native from EVERY selected family and optional classic opponents: two-family mixed E3 or all-three-family tables, Basic/Advanced2–6, all12 Nexus/all14 only with Skills, optional original Homeworlds/Discovery7+8/Tech3+/Advanced Strongholds2+. Ordinary Ix47 if selected, otherwise CHOAM35, and the seated native Richese cache stay physical. Fresh Ecaz gets original Arrakis Occupy; Advanced native Moritani gets original assassination against supported ordinary printed native opponents. The shared Duke never trains; Advanced Skills/capture and Moritani/Harkonnen, Sandtrout, own mixed markers, unusual Gholas and pending force/payment effects retain their guards. Original hands/offers, native setup, actual closing draw and winner/cleanup/Tech timing are reused. No new initializer, return adapter, public start or played-save conversion.',
'\nLeader-skills --nexus-cards adds all12 Nexus to original classic/base33, single or paired one-family E1/E2, both selected Ix+CHOAM/union47, standalone or paired E3/ecaz33, or mixed E3/two-or-three selected families with an actual native from each and classics. All14 original offers and Basic/Advanced2–6 remain; Homeworlds may be retained, Tech3+/StrongholdsAdvanced. Actual Ix pre-training hand, ordinary discs and native components remain. Each capture/exceptional shared-Duke, Banker/Mentat preview, borrowed Smuggler/companion/native-return and pending-effect boundary remains; no public start or played conversion.',
'\nThese classic, single-native or paired one-family skill-Nexus entries preserve selected unused original Tech Tokens at3–6 Basic/Advanced and/or Stronghold Cards at2–6 Advanced; both require3–6 Advanced. Real first-Storm/end-Mentat custody, phase-end industry, held subsidy, typed skill rescue/equal substitution, signed one-invoice marker pairs and mandatory original-winner Tech before Face Dance use original handlers. No mixed-family/native free-return accounting or pending rulings are opened.',
'\nThe same fresh skill/Nexus entry admits standalone Ecaz OR Moritani with classics, exact ecaz33/all14/all12, Basic/Advanced2–6. Optional unused Tech requires3+; Strongholds requires Advanced; both3+ Advanced. Native Ecaz five-disc/Duke assignment and quiet living-Duke Cunning, plus original Advanced Moritani skill-first assassination/normal-traitor forfeiture, remain. Advanced Harkonnen, shared-Duke assignment, enhanced Terror relocation/HMS/Grumman and other pending effects stay guarded.',
'\nIn the standalone Ecaz skill/Nexus profile, mandatory Advanced Occupy composes the chosen faction’s skill with separately labelled armies. Ecaz-led Suk rescues its own fixed casualties after the ally’s variable losses; Diplomat uses the chosen faction’s undialed own pool. Shared training, Basic odd rounding and other pending effects remain separate.',
@@ -45,9 +46,9 @@ async function main() {
'\nWhen native CHOAM is seated, this fresh entry additionally marks original occupied Tupile hand slots and owner-selected normal-limit cleanup. The occupier/reciprocal ally gain one validated slot through existing capacity consumers; Advanced departure and ally removal queue the original lost-holder cleanup. Private selected-ID discards preserve current phase/opening and remaining movement, with no new card or fee. Known occupation suppresses new native low-intelligence queries; proved Advanced expiry restores eligibility without resetting historical answers/usage. Basic unknown cases retain source guards. Unmarked played profiles are not upgraded.',
'\nWhen native Ecaz is seated, this original fresh entry separately marks its printed high Homeworld alliance victory: current native seven-plus, one actual joint stronghold, two distinct other native factions with proved current occupied holdings. It adds no ordinary stronghold/Tech points, pays no reward and changes no Duke authority. Public progress remains prospective until original Mentat; qualifying members join original winners before BG prediction/final fallback. Unmarked played/public profiles remain unchanged.',
'\nDiscovery opts fresh classic, selected E1/E2 native families or standalone Ecaz OR Moritani with classics into original Discovery setup in Basic/Advanced2–6, optionally retaining original Homeworlds, unused Tech3–6 and Advanced Strongholds2–6. Original decks/cache, HMS/Face Dancers, typed entry, revealed-only nested No-Fields, stash limits, end-Mentat claims and native outcomes remain. Advanced Moritani excludes Harkonnen. Skills/Nexus, paired/mixed E3, contested/shared-payer rulings, public starts and played-game conversion stay separate.',
'\nLeader-skills --discoveries composes fresh supported classic or native original decks/all14 Skills with Discovery7/8 at2–6 seats Basic/Advanced under each roster predicate. Original Homeworlds may be retained; adding --nexus-cards supports original single/paired one-family E1/E2, natives from both selected Ix+CHOAM families, and standalone/paired Ecaz/Moritani with classics. Tech requires3–6 and Strongholds Advanced2–6. Original private offers/training/hands, Maker/votes/rides, residual Cyborg exchange, native aftermath and cleanup/Tech/Face Dance retain their own timing. Three-family mixtures, independent variants, Banker/Mentat previews, shared/captured Duke training, borrowed Smuggler/pair companion/native returns and pending rulings remain separate.',
'\nNexus --discoveries explicitly composes fresh classic/base33, original single/paired E1/E2 one-family decks, natives from both selected Ix+CHOAM families/union47, or standalone/paired Ecaz/Moritani/ecaz33 with classics, all12 Nexus and Discovery7/8 at2–6 seats Basic/Advanced without Skills, optionally Homeworlds. Unused Tech requires3+; Strongholds requires Advanced; both3+. Actual Maker losses/votes/typed reserve rides, both Advanced piles, settled-alliance end-Spice deals and held claims remain. Native signed two-marker shipments retain one invoice, revealed destination and original reserve materialization. Mixed E3 families, mixed-module Fremen Betrayal, pending effects, public activation and save conversion stay guarded.',
'\nFresh supported classic or native lobbies with original Homeworlds retain leader-skills, discovery or both under their original contracts. The Nexus entries additionally retain source-clear single/paired E1/E2, mixed selected Ix+CHOAM natives and standalone/paired Ecaz/Moritani, Basic/Advanced2–6, optional all14 Skills/Discovery7+8/Tech3+/Advanced Strongholds2+. Original native decks/inventories/capture exclusions remain; shared Duke never becomes a training disc. Actual typed native/visitor/nested pools, real closing draws, original revival deposits, industry, held effects and winner rewards use the existing consumers. Three-family/other E3 mixtures, unrelated previews, pending effects, public starts and played-save conversion remain separate.',
'\nLeader-skills --discoveries composes original classic/supported native decks/all14 and Discovery7/8 at2–6 seats Basic/Advanced. Adding --nexus-cards admits single/paired one-family E1/E2, both Ix+CHOAM, standalone/paired E3 or selected mixed E3/all-three-family natives with classics and an actual native from every family. Original Homeworlds may be retained; Tech3+, StrongholdsAdvanced. Hands/offers, actual Maker/votes/rides, winning typed rescue/residual Cyborg exchange, native aftermath and cleanup/Tech/Face Dance remain. Independent variants, preview/exceptional training, borrowed Smuggler/companions/native returns and pending rulings stay separate.',
'\nNexus --discoveries preserves NO Skills, all12 Nexus/Discovery7/8 and original classic, single/paired E1/E2, mixed Ix+CHOAM or standalone/paired/mixed E3/all-three-family envelopes with an actual native from EVERY selected family. Original Homeworlds/Tech3+/Advanced Strongholds remain optional. Actual Maker losses/votes/typed reserve ride, both Advanced piles and a settled alliance precede the closing draw. Signed native two-marker shipment retains one original invoice, revealed site and reserve-capped materialization. Unfinished card/force/payment effects, mixed-module Fremen Betrayal, public activation and played-save conversion stay guarded.',
'\nFresh Homeworlds retain ordinary Leader Skills/Discovery under their existing non-Nexus contracts. Original Nexus entries additionally compose single/paired E1/E2, mixed Ix+CHOAM, standalone/paired E3 and selected mixed E3/all-three-family natives with classics, original native decks/all12 and optional all14/Discovery7+8/Tech3+/Advanced Strongholds2+. Native source/roster/capture exclusions and separate untrained Duke remain. Typed native/visitor/nested pools, real closing deals, original revival deposits, industry, held effects and original winner rewards use existing consumers. Independent variants, unrelated previews, exceptional/pending effects, public starts and played-save conversion remain separate.',
);
return;
}