Compose original mixed E3 and three-family Nexus
This commit is contained in:
1 parent
6935b8650d
commit
48b2755088
11 files changed
+255
-63
No files matched your search
+28
-6
@@ -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
|
||||
|
||||
@@ -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') ||
|
||||
|
||||
@@ -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
@@ -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) ||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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' &&
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user