Integrate sourced Treachery cards with Advanced Occupy

This commit is contained in:
Quintine committed 2026-10-02 17:53:24 +10:00
1 parent 85caee6c83
commit cd532222a5
21 files changed
+1226 -99

No files matched your search

+18 -7
View File
@@ -45,6 +45,7 @@ type Scenario = {
rules: Rules;
expansions: string[];
roster: FactionId[];
ecazTreachery?: boolean;
};
const SCENARIOS: readonly Scenario[] = [
@@ -157,8 +158,8 @@ const ECAZ_TREACHERY_SCENARIOS: readonly Scenario[] = EXPANSION_ROSTER_SCENARIOS
.map(scenario => ({
...scenario, profile: 'ecaz-treachery', ordinal: scenario.ordinal + 58,
}));
/** Ordinary Advanced Ecaz Occupy, without the independent three-card variant. */
const ECAZ_OCCUPY_SCENARIOS: readonly Scenario[] = [
/** Advanced Occupy samples retain explicit independent card-variant selection. */
const ECAZ_OCCUPY_BASE_SCENARIOS: readonly Scenario[] = [
...[2, 3, 4, 5, 6].map((players): Scenario => ({
ordinal: 169 + players - 2, profile: 'ecaz-occupy', rules: 'advanced',
expansions: ['ecaz'],
@@ -176,6 +177,12 @@ const ECAZ_OCCUPY_SCENARIOS: readonly Scenario[] = [
roster: (['ecaz', 'ixians', 'tleilaxu', 'choam', 'guild', 'fremen'] as FactionId[]).slice(0, players),
})),
];
const ECAZ_OCCUPY_SCENARIOS: readonly Scenario[] = [
...ECAZ_OCCUPY_BASE_SCENARIOS,
...ECAZ_OCCUPY_BASE_SCENARIOS.map((scenario, index): Scenario => ({
...scenario, ordinal: 192 + index, ecazTreachery: true,
})),
];
const PAIRED_NEXUS_SCENARIOS: readonly Scenario[] = EXPANSION_ROSTER_SCENARIOS
.map(scenario => ({
...scenario,
@@ -314,7 +321,7 @@ function usage() {
'[--rules both|basic|advanced] [--players all|2|3|4|5|6] [--max-actions POSITIVE] ' +
'[--resume FAILED_GAME.json]\n' +
'Runs genuine setup and gameplay offline. Default/all keeps the six expansion samples; combined-nexus and combined-homeworld-nexus add five/six-seat Basic/Advanced all-expansion samples. Base, Homeworld, Nexus, Homeworld-Nexus, expansion roster, paired expansion Nexus, Ecaz card variant, Advanced Moritani assassination and skill profiles select their documented rosters. Stronghold-factions is Advanced only: Ixians + CHOAM, native Ixians + Tleilaxu, or native CHOAM + Richese with classic opponents, two through six seats; mixed four-native rosters also run at four through six seats. Selected Ix/CHOAM decks determine the canonical 47/35 Treachery Cards; Richese has a separate ten-card cache and all samples have six separate Stronghold Cards without other modules. Scenario names identify their native roster. --players requires a supported profile. Output must be a new private directory outside the checkout.' +
'\nEcaz-occupy is Advanced only: native Ecaz plus classic/Ixian/Tleilaxu/CHOAM paired or mixed rosters, two through six seats; selected E3/E1/E2 sources determine ordinary33/47/35 cards. No Richese mixed planning, independent three-card variant or optional overlays.'
'\nEcaz-occupy is Advanced only: native Ecaz plus classic/Ixian/Tleilaxu/CHOAM paired or mixed rosters, two through six seats; selected E3/E1/E2 sources determine ordinary33/47/35 cards. Explicit independent three-card samples additionally use36/50/38. No Richese mixed planning or other optional overlays.'
);
}
@@ -352,7 +359,8 @@ function scenarioName(scenario: Scenario) {
}
if (scenario.profile === 'ecaz-occupy') {
const native = scenario.roster.filter(faction => ['ixians', 'tleilaxu', 'choam'].includes(faction));
if (native.length) return `${scenario.profile}-${native.join('-')}-${scenario.roster.length}-${scenario.rules}`;
if (native.length || scenario.ecazTreachery)
return `${scenario.profile}${scenario.ecazTreachery ? '-treachery' : ''}${native.length ? '-' + native.join('-') : ''}-${scenario.roster.length}-${scenario.rules}`;
}
return scenario.profile === 'base' || scenario.profile === 'homeworld' || scenario.profile === 'nexus' || scenario.profile === 'homeworld-nexus' || scenario.profile === 'combined-nexus' || scenario.profile === 'combined-homeworld-nexus' ||
scenario.profile === 'choam-roster' || scenario.profile === 'ecaz-roster' || scenario.profile === 'ecaz-treachery' || scenario.profile === 'ecaz-occupy' || scenario.profile === 'moritani-assassinate' || scenario.profile === 'ix-roster' || scenario.profile === 'choam-nexus' || scenario.profile === 'ecaz-nexus' || scenario.profile === 'ix-nexus' || skillProfile(scenario.profile)
@@ -407,6 +415,7 @@ function freshGame(scenario: Scenario) {
player.bot = DIFFICULTIES[index % DIFFICULTIES.length];
player.ready = true;
}
if (scenario.ecazTreachery) game.ecazTreachery = true;
if (scenario.profile === 'homeworld' || scenario.profile === 'homeworld-nexus' || scenario.profile === 'combined-homeworld-nexus')
game.homeworlds = { custody: null };
if (scenario.profile === 'nexus' || scenario.profile === 'homeworld-nexus' || scenario.profile === 'choam-nexus' || scenario.profile === 'ecaz-nexus' || scenario.profile === 'ix-nexus' || scenario.profile === 'combined-nexus' || scenario.profile === 'combined-homeworld-nexus')
@@ -481,7 +490,7 @@ function resumedGame(path: string) {
(candidate.profile === 'nexus' || 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 &&
skillProfile(candidate.profile) === !!game.leaderSkills &&
(candidate.profile === 'stronghold-factions') === !!game.strongholdCards &&
(candidate.profile === 'ecaz-treachery') === !!game.ecazTreachery &&
(candidate.profile === 'ecaz-treachery' || candidate.ecazTreachery === true) === !!game.ecazTreachery &&
(candidate.profile === 'moritani-assassinate') === !!game.moritaniAssassinatePreview &&
(candidate.profile === 'ecaz-occupy') === !!game.ecazOccupyPreview &&
(candidate.profile === 'moritani-assassinate') === !!game.moritaniAssassinate &&
@@ -829,9 +838,11 @@ async function main() {
families: selected.filter(scenario => scenario.profile === 'ecaz-occupy').map(scenario => ({
roster: scenario.roster,
expansions: scenario.expansions,
treacheryCards: scenario.expansions.includes('ix') ? 47 : scenario.expansions.includes('choam') ? 35 : 33,
treacheryCards: (scenario.expansions.includes('ix') ? 47 : scenario.expansions.includes('choam') ? 35 : 33) +
(scenario.ecazTreachery ? 3 : 0),
ecazTreachery: !!scenario.ecazTreachery,
})),
scope: 'Advanced native Ecaz/classic/Ixian/Tleilaxu/CHOAM paired or mixed families, two through six seats. Selected E3/E1/E2 ordinary decks only; no Richese mixed planning, independent Ecaz three-card variant or optional overlays. Authorized revised Advanced ceil contribution/floor survivors; not a designer erratum or Basic ruling.',
scope: 'Advanced native Ecaz/classic/Ixian/Tleilaxu/CHOAM paired or mixed families, two through six seats. Selected E3/E1/E2 decks and explicit independent three-card variant samples; no Richese mixed planning or other optional overlays. Authorized revised Advanced ceil contribution/floor survivors; not a designer erratum or Basic ruling.',
},
} : {}),
options: {
+8
View File
@@ -44,6 +44,10 @@ export const PROTOTYPE_PROFILES = [
'harkonnen-betrayal',
] as const;
export type PrototypeProfile = (typeof PROTOTYPE_PROFILES)[number];
export type PrototypeOptions = {
/** Explicit independent card variant for the fresh combined-army profile. */
ecazTreachery?: boolean;
};
export function isPrototypeProfile(value: string): value is PrototypeProfile {
return PROTOTYPE_PROFILES.some((profile) => profile === value);
}
@@ -62,6 +66,7 @@ export function startPrototypeRoom(
code: string,
expectedVersion: number,
profile: PrototypeProfile,
options: PrototypeOptions = {},
) {
if (
!/^[A-Z0-9]{8}$/.test(code) ||
@@ -81,6 +86,9 @@ export function startPrototypeRoom(
throw new Error('The room identity is inconsistent.');
if (!isPrototypeProfile(profile))
throw new Error('Unknown prototype profile.');
if (options.ecazTreachery && profile !== 'ecaz-occupy')
throw new Error('--ecaz-treachery requires the ecaz-occupy profile.');
if (options.ecazTreachery) initial.ecazTreachery = true;
const game =
profile === 'ix'
? initializeIxGameForAudit(initial)
+7 -2
View File
@@ -15,16 +15,19 @@ async function main() {
room: { type: 'string' },
version: { type: 'string' },
out: { type: 'string' },
'ecaz-treachery': { type: 'boolean' },
help: { type: 'boolean' },
},
});
if (values.help) {
console.log(
'Usage: node --import tsx tools/start-prototype.ts --profile ix|discovery|leader-skills|banker-income|factions|stronghold-factions|nexus|moritani-assassinate|ecaz-treachery|ecaz-occupy|semuta|kull|richese-betrayal|nexus-kull|guild-betrayal|ixian-replacement|ixian-betrayal|harkonnen-betrayal --db PATH --room CODE --version NUMBER --out /private/new-directory\nStarts only a fresh ready lobby for the selected development prototype. Stronghold-factions opts a fresh ready 2–6-seat Advanced roster with at least one selected native Ixians, Tleilaxu, CHOAM or Richese faction and otherwise classic opponents into Stronghold Cards alone. Select one or both distinct Ix/CHOAM decks; each native faction requires its family deck. The selected decks supply 47 Ix or combined, or 35 CHOAM canonical physical Treachery Cards, with a separate ten-card Richese cache when Richese plays. Native setup choices, HMS placement, Face Dancers, Richese cache and CHOAM Auditor remain genuine; custody starts unowned and is claimed by actual END Mentat. It does not redeal, reset or retrofit a saved game. Banker-income opts a fresh 2–6-seat classic Basic/Advanced roster with base 33-card Treachery and all fourteen Leader Skills alone into automatic deferred normal-band income and native Mentat collection; already-earned spice stays with its original faction under this explicit local policy. Nexus enables the existing private Nexus setup. Moritani-assassinate explicitly opts into the bounded Advanced assassination preview. Semuta opts a fresh Richese roster into bounded clean-discard reactions; an included Ix deck supplies physical Thumper without opening public starts. Kull opts a fresh eligible CHOAM roster into the user-selected Karama interception preview. Richese-betrayal opts a fresh paired CHOAM/Richese Nexus roster into cache-purchase veto and sale-bank diversion. Nexus-kull opts a fresh CHOAM-plus-classic roster with CHOAM/Ix decks and Nexus into source-aware printed or any-Treachery-fueled Cunning Kull. Guild-betrayal opts a fresh classic roster with native Guild into full funded shipment-payment replacement; it preserves an explicitly enabled Homeworld module for Junction routes. Ixian-replacement opts a fresh classic Basic/Advanced roster with base Treachery and Nexus only into the optional exact normal-auction purchased-card replacement; native Ixians, Harkonnen buyers and special purchase sources remain unsupported. Ixian-betrayal opts a fresh 2–6-seat Basic/Advanced native Ixian plus classic/Tleilaxu roster with Ix 47-card Treachery and Nexus only into prevention of one actual native Bidding draw or Advanced Technology exchange, after native Karama counters. Harkonnen-betrayal opts a fresh 2–6-seat classic Basic/Advanced roster with native Harkonnen, base 33-card Treachery and Nexus alone into cancellation of an actual declared personal or allied Harkonnen Traitor Card after native counters, physical retirement and shuffle, and one private actual Mentat replacement. Modules, overlays, public starts and old-game retrofits remain excluded. Nexus is not dealt initially: a qualifying alliance requires at least three seats; two unallied receivers require four. Backs up all rooms first, preserves sessions and existing games, and rejects stale versions. Normal game-start and publication gates remain closed.',
'\nEcaz-occupy (--profile ecaz-occupy) opts only a fresh ready 2–6-seat Advanced lobby with native Ecaz and classic/Ixian/Tleilaxu/CHOAM opponents (optional native Moritani). Distinct selected decks include ecaz and each native ix/choam family; ordinary33/47/35 cards retain real original E3/E1/E2 offers/HMS/Auditor/Face Dancers. No Richese mixed planning, independent Ecaz three-card variant, modules or overlays.',
'\nEcaz-occupy (--profile ecaz-occupy) opts only a fresh ready 2–6-seat Advanced lobby with native Ecaz and classic/Ixian/Tleilaxu/CHOAM opponents (optional native Moritani). Distinct selected decks include ecaz and each native ix/choam family; ordinary33/47/35 cards retain real original E3/E1/E2 offers/HMS/Auditor/Face Dancers. Explicit --ecaz-treachery independently adds the three physical variant cards, making36/50/38. No Richese mixed planning, modules or overlays.',
);
return;
}
if (values['ecaz-treachery'] && values.profile !== 'ecaz-occupy')
throw new Error('--ecaz-treachery requires --profile ecaz-occupy.');
if (
!isPrototypeProfile(values.profile) ||
!values.db ||
@@ -47,7 +50,8 @@ async function main() {
throw new Error('The requested lobby version is absent from the backup.');
const db = new DatabaseSync(values.db);
try {
const result = startPrototypeRoom(db, values.room, version, values.profile);
const result = startPrototypeRoom(db, values.room, version, values.profile,
{ ecazTreachery: values['ecaz-treachery'] });
writeFileSync(
resolve(values.out, 'prototype.json'),
JSON.stringify(
@@ -55,6 +59,7 @@ async function main() {
format: 1,
startedAt: new Date().toISOString(),
profile: values.profile,
ecazTreachery: !!values['ecaz-treachery'],
source,
beforeVersion: version,
...result,