Compose native faction Tech and Advanced Strongholds without Skills

This commit is contained in:
Quintine committed 2026-10-04 14:59:12 +11:00
1 parent 7cdb9b5a62
commit 2da3c9273b
16 files changed
+1358 -45

No files matched your search

+18 -12
View File
@@ -1,3 +1,4 @@
import { nativeFactionTechProfile } from './faction-module-profile';
import { advancedMoritaniLeaderSkillsProfile, advancedNativeLeaderSkillsProfile, classicTechLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, ordinaryLeaderSkillModeSupported, strongholdLeaderSkillsProfile } from './leader-skill-profile';
import { bribeTimingBlock, maximumBribe, type BribeOptions } from './bribe-options';
import { quoteSpicePlacement, stormExposesTerritory, stormSectorAfter, wormConsumesForces } from './disaster-rules';
@@ -7514,9 +7515,9 @@ function leaderSkillAssignmentUnavailable(
function e3StrongholdFactionProfile(g: Game): boolean {
if (!g.advanced || g.expansions.length !== 1 || g.expansions[0] !== 'ecaz' ||
g.players.length < 2 || g.players.length > 6 ||
g.homeworlds || g.nexusCards || g.leaderSkills || g.techTokens ||
g.homeworlds || g.nexusCards || g.leaderSkills ||
g.discoveryEnabled || g.discoveries || g.discoveryStash || g.greatMaker ||
g.ecazTreachery) return false;
g.ecazTreachery || (g.techTokens && !nativeFactionTechProfile(g))) return false;
let native: 'ecaz' | 'moritani' | null = null;
for (const player of g.players) {
if (player.faction === 'ecaz' || player.faction === 'moritani') {
@@ -7534,7 +7535,7 @@ function moritaniAssassinateModeSupported(g: Game) {
(!g.strongholdCards || e3StrongholdFactionProfile(g) ||
(!!g.leaderSkills && strongholdLeaderSkillsProfile(g))) && !g.homeworlds &&
!g.discoveryEnabled && !g.discoveries &&
(!g.techTokens || (!!g.leaderSkills && nativeTechLeaderSkillsProfile(g)));
(!g.techTokens || nativeFactionTechProfile(g) || (!!g.leaderSkills && nativeTechLeaderSkillsProfile(g)));
}
function moritaniAssassinateContext(g: Game, receipt: MoritaniAssassinateReceipt) {
const p = getPlayer(g,receipt.owner), opponent = getPlayer(g,receipt.opponent);
@@ -9244,10 +9245,13 @@ export function initializeFactionExpansionsGameForAudit(state: Game): Game {
'The faction prototype needs a nonempty selection of distinct known expansions.');
requireRule(!state.homeworlds && !state.nexusCards && !state.leaderSkills &&
!state.discoveryEnabled && !state.discoveries && !state.discoveryStash && !state.greatMaker &&
!state.techTokens && !state.strongholdCards,
'The faction prototype excludes optional modules, including Leader Skills and Discoveries.');
(!state.techTokens || nativeFactionTechProfile(state)) && !state.strongholdCards,
'The faction prototype excludes other optional modules; Tech Tokens require a supported native three-through-six-seat lobby.');
requireFreshFactionInventory(state);
return initializeSetupGameForAudit(state, false, false, false, false, false, false, true);
const initialized = initializeSetupGameForAudit(state, false, false, false, false, false, false, true);
if (initialized.advanced && nativeFactionTechProfile(initialized) && byFaction(initialized, 'moritani'))
initializeMoritaniAssassinateState(initialized);
return initialized;
}
function ecazOccupyCompositionSupported(g: Game): boolean {
return typeof g.advanced === 'boolean' && Array.isArray(g.expansions) &&
@@ -9264,7 +9268,7 @@ function ecazOccupyCompositionSupported(g: Game): boolean {
g.expansions.includes(faction(p.faction).expansion))) &&
!g.homeworlds && !g.nexusCards && !g.leaderSkills &&
(!g.strongholdCards || e3StrongholdFactionProfile(g)) &&
!g.techTokens && !g.discoveryEnabled && !g.discoveries &&
(!g.techTokens || nativeFactionTechProfile(g)) && !g.discoveryEnabled && !g.discoveries &&
!g.discoveryStash && !g.greatMaker &&
(g.ecazTreachery === undefined || g.ecazTreachery === true);
}
@@ -9290,9 +9294,9 @@ export function initializeStrongholdFactionsGameForAudit(state: Game): Game {
'Stronghold factions require supported Advanced native factions, classic opponents and their selected distinct decks.');
requireRule(!state.homeworlds && !state.nexusCards && !state.leaderSkills &&
!state.discoveryEnabled && !state.discoveries && !state.discoveryStash && !state.greatMaker &&
!state.techTokens && !state.ecazTreachery &&
(!state.techTokens || nativeFactionTechProfile(state)) && !state.ecazTreachery &&
(!state.strongholdCards || JSON.stringify(state.strongholdCards) === JSON.stringify(createStrongholdCards())),
'Stronghold factions admit only unused Stronghold Cards, without other optional modules.');
'Stronghold factions admit unused Stronghold Cards and supported native Tech Tokens, without other optional modules.');
requireFreshFactionInventory(state);
requireFreshBaseRuntime(state);
const initialized = initializeSetupGameForAudit(state, false, false, false, false, false, false, true, false, true);
@@ -9424,9 +9428,10 @@ function initializeSetupGameForAudit(state: Game, homeworlds: boolean, nexus = f
requireFreshSetup(g, homeworlds || nexus || ix || factions);
const skillStronghold = leaderSkills && strongholdLeaderSkillsProfile(g);
const skillTech = leaderSkills && (classicTechLeaderSkillsProfile(g) || nativeTechLeaderSkillsProfile(g) || skillStronghold);
requireRule(!g.techTokens || (skillTech &&
const nativeTech = factions && !leaderSkills && nativeFactionTechProfile(g);
requireRule(!g.techTokens || ((skillTech || nativeTech) &&
JSON.stringify(g.techTokens) === JSON.stringify(createTechTokens())),
'Leader Skills with Tech Tokens requires a fresh supported classic/native three-through-six-seat lobby with unused tokens.');
'Tech Tokens require a fresh supported three-through-six-seat lobby with unused tokens.');
requireRule(!g.strongholdCards || strongholdFactions || (skillStronghold &&
JSON.stringify(g.strongholdCards) === JSON.stringify(createStrongholdCards())),
'Leader Skills with Stronghold Cards requires a fresh supported Advanced lobby with unused cards.');
@@ -9438,7 +9443,7 @@ function initializeSetupGameForAudit(state: Game, homeworlds: boolean, nexus = f
(discovery || !g.discoveryEnabled) &&
(nexus || !g.nexusCards) &&
(ecazTreachery || !g.ecazTreachery) &&
(!g.techTokens || skillTech) &&
(!g.techTokens || skillTech || nativeTech) &&
(strongholdFactions || skillStronghold || !g.strongholdCards) &&
(homeworlds || !g.homeworlds) &&
g.players.every((p) =>
@@ -9461,6 +9466,7 @@ function initializeSetupGameForAudit(state: Game, homeworlds: boolean, nexus = f
: 'The audit initializer supports base factions without expansions or optional modules.',
);
initializeSetup(g, strongholdFactions || !!g.strongholdCards);
if (nativeTech && byFaction(g, 'ecaz')) g.ecazOccupyPreview = true;
return normalizeAutomaticGame(g);
}
function ecazStartingForcesComplete(p: Player): boolean {
+68
View File
@@ -0,0 +1,68 @@
import { FACTIONS } from './catalog';
/** Public lobby configuration only; physical freshness is checked by the initializer. */
export type FactionModuleProfile = {
advanced: boolean;
expansions: readonly string[];
players: readonly { faction: string }[];
techTokens?: unknown;
strongholdCards?: unknown;
leaderSkills?: unknown;
homeworlds?: unknown;
nexusCards?: unknown;
discoveryEnabled?: unknown;
discoveries?: unknown;
discoveryStash?: unknown;
greatMaker?: unknown;
ecazTreachery?: unknown;
semutaPreview?: unknown;
advancedPreview?: unknown;
kullPreview?: unknown;
nexusKullPreview?: unknown;
guildBetrayalPreview?: unknown;
richeseBetrayalPreview?: unknown;
nexusIxianReplacementPreview?: unknown;
nexusIxianBetrayalPreview?: unknown;
nexusHarkonnenBetrayalPreview?: unknown;
};
/** Original E1/E2 families or standalone Ecaz/Moritani; no unrelated overlays. */
export function nativeFactionTechProfile(game: FactionModuleProfile): boolean {
if (typeof game.advanced !== 'boolean' || !game.techTokens ||
game.players.length < 3 || game.players.length > 6 ||
game.expansions.length < 1 || game.expansions.length > 2 ||
(game.strongholdCards && !game.advanced) || game.leaderSkills ||
game.homeworlds || game.nexusCards || 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) return false;
const e3 = game.expansions.length === 1 && game.expansions[0] === 'ecaz';
if (!e3) {
for (let i = 0; i < game.expansions.length; i++) {
if (game.expansions[i] !== 'ix' && game.expansions[i] !== 'choam') return false;
for (let j = 0; j < i; j++) if (game.expansions[i] === game.expansions[j]) return false;
}
}
let native = 0;
let moritani = false;
let harkonnen = false;
for (let i = 0; i < game.players.length; i++) {
const player = game.players[i];
for (let j = 0; j < i; j++) if (player.faction === game.players[j].faction) return false;
let expansion: string | undefined;
for (const faction of FACTIONS)
if (faction.id === player.faction) { expansion = faction.expansion; break; }
if (expansion === undefined) return false;
if (expansion !== 'base') {
if (e3 ? player.faction !== 'ecaz' && player.faction !== 'moritani'
: !game.expansions.includes(expansion)) return false;
native++;
}
if (player.faction === 'moritani') moritani = true;
if (player.faction === 'harkonnen') harkonnen = true;
}
return native > 0 && (!e3 || native === 1) &&
!(game.advanced && moritani && harkonnen);
}
+4 -3
View File
@@ -3254,7 +3254,7 @@ export const RULE_TOPICS: RuleTopic[] = [
developmentStage: 'Verified',
summary: 'Fresh Advanced Stronghold play connects native E1/E2 families and standalone Ecaz or Moritani through original holder-only rule handlers.',
steps: [
'This private local profile admits selected native Ixian/Tleilaxu and CHOAM/Richese families with classic opponents in unique two-to-six-seat Advanced rosters. Standalone Ecaz OR Moritani with classic opponents additionally uses the exact ecaz deck. Moritani excludes Harkonnen under its original assassination/Duke boundary. An E3 pair, E1/E2/E3 mixtures, Basic and other overlays remain separate; only Stronghold Cards are enabled.',
'This private local profile admits selected native Ixian/Tleilaxu and CHOAM/Richese families with classic opponents in unique two-to-six-seat Advanced rosters. Standalone Ecaz OR Moritani with classic opponents additionally uses the exact ecaz deck. Moritani excludes Harkonnen under its original assassination/Duke boundary. Original unused Tech Tokens may additionally be selected with three through six seats, without Leader Skills; an E3 pair, E1/E2/E3 mixtures, Basic Strongholds and unrelated overlays remain separate.',
'CHOAM-only ordinary Treachery has 35 physical cards; Ix or both decks have47, not49, because CHOAM replaces the shared Ix cards. Native Richese’s ten-card cache remains separate. Genuine setup retains original faction offers/leaders/forces and any native Auditor/HMS, with all six Stronghold Cards initially unowned.',
'Card ownership begins only at the real first end of Mentat and is retained through the following turn. It is not assigned by a battle fixture or reset on refresh. Hidden Mobile Stronghold interior forces and its outside pointing territory stay distinct.',
'The mobile card copies an advantage of another currently controlled stronghold, not merely a retained card. A sole choice is automatic; multiple choices use the existing public pre-plan selection. Accepted choices stay bound to the same original battle through private plans and saved continuation.',
@@ -3266,7 +3266,7 @@ export const RULE_TOPICS: RuleTopic[] = [
],
related: ['stronghold-cards', 'mobile-stronghold', 'choam-combat', 'setup', 'implementation-checklist'],
checklist: [
{ area: 'Implementation', status: 'Implemented', detail: 'Fresh selected E1/E2 setup and standalone Ecaz/Moritani exact ecaz deck, actual end-Mentat ownership, holder-only Ecaz coalition funding and original Moritani loss/assassination compose existing effects without public-start or old-game changes.', evidence: ['game/engine.ts', 'game/stronghold-cards.ts', 'game/battle-resolution-quote.ts'] },
{ area: 'Implementation', status: 'Implemented', detail: 'Fresh selected E1/E2 setup and standalone Ecaz/Moritani exact ecaz deck, actual end-Mentat ownership, holder-only Ecaz coalition funding and original Moritani loss/assassination compose existing effects. Optional original Tech requires three through six seats and awards the actual selected lead before Face Dance, without public-start or old-game changes.', evidence: ['game/engine.ts', 'game/faction-module-profile.ts', 'game/stronghold-cards.ts', 'game/battle-resolution-quote.ts'] },
{ area: 'Player controls', status: 'Implemented', detail: 'Original local390px controls cover HMS copy/support, real cache Stone reveal mode and partial Face Dance replacement. Standalone E3 human controls select the Ecaz holder lead, show bank2/personal1 for actual support3, seal native plans and keep the physical Shield; Moritani reveals printed Master Bewt bounty through its original inspector and choice. Original Collection and Mentat continue after refresh.', evidence: ['components/stronghold-cards.tsx', 'components/game-table.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'Four minimal legal policies consume actual marker/Stone/FaceDance/copy choices; eighteen genuine paired/mixed Advanced2..6 games complete. Strategy/calibration and other combinations remain deferred.', evidence: ['game/bots.ts', 'tools/faction-games.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'Advanced family/deck/cache admission, existing six effects, original winner versus replacement and explicit guarded combinations are recorded without completing every card or faction.', evidence: ['docs/STRONGHOLD_CARDS.md', 'docs/RULE_DECISIONS.md'] },
@@ -3287,7 +3287,8 @@ export const RULE_TOPICS: RuleTopic[] = [
'Fremen reserve arrivals and Guild cross-planet or return shipments do not trigger Heighliners. A stopped shipment never arrives and cannot trigger it. Bene Gesserit accompanying forces do arrive from off planet and can trigger the token even after a Guild shipment.',
'A battle winner takes one tech token from the defeated faction after casualties and played-card decisions. With several available tokens, the winner chooses. A winner still takes the token if no winning forces survive. Mutual destruction has no winner and transfers no token.',
'Owning all three tokens counts as one additional stronghold at victory checks. An alliance cannot combine separate token holdings to qualify: one member must own the complete set. Multiple sides reaching the victory target can share the win.',
'Token ownership and pending income are public and persist with the room. Fresh supported classic or E1/E2 native Leader Skills tables retain original three-through-six-seat Basic/Advanced setup and effects; Advanced may also retain Stronghold Cards. Skill rescue or board-spice awards finish before the mandatory original winner token reward and later Face Dance. Banker remains a separate source-to-Mentat opt-in. CHOAM’s separate native opening payout is not ordinary poverty Charity. Two-player tokens, E3/other combinations and full combined acceptance remain unfinished.',
'Token ownership and pending income are public and persist with the room. Fresh supported classic, E1/E2 native or standalone Ecaz/Moritani Leader Skills tables retain original three-through-six-seat Basic/Advanced setup and effects; Advanced may also retain Stronghold Cards. Skill rescue or board-spice awards finish before the mandatory original winner token reward and later Face Dance. Banker remains a separate source-to-Mentat opt-in. CHOAM’s separate native opening payout is not ordinary poverty Charity. Two-player tokens, mixed E3/other combinations and full combined acceptance remain unfinished.',
'Without Skills, fresh native E1/E2 families or standalone Ecaz OR Moritani with classic opponents may preserve unused original Tech Tokens through the local factions entry, Basic/Advanced three through six players; Advanced stronghold-factions adds the original six Stronghold Cards. Native payments, typed casualties, selected Ecaz lead rewards and Moritani original assassination remain separate. Split allied holdings never qualify as a complete set. Basic odd Occupy, Richese cache/No-Field rulings, unrelated overlays, public starts and saved-game conversion remain guarded.',
],
example:
'You own Heighliners and Axlotl Tanks. When Emperor ships from reserves, place two spice on Heighliners. A later shipment adds nothing. Collect the two spice when Shipping and Movement ends.',