Compose native faction Tech and Advanced Strongholds without Skills
This commit is contained in:
1 parent
7cdb9b5a62
commit
2da3c9273b
16 files changed
+1358
-45
No files matched your search
+18
-12
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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.',
|
||||
|
||||
Reference in new issue
Block a user