Compose native Homeworld skill and Discovery modules

This commit is contained in:
Quintine committed 2026-10-07 05:16:26 +11:00
1 parent a8758bc6b5
commit 3fb047c93d
23 files changed
+1732 -107

No files matched your search

+1 -1
View File
@@ -1762,7 +1762,7 @@ function policyActions(g: GameView): Action[] {
ambassadors.nextCost <= (me.spice ?? 0)
) {
const token = ambassadors.tokens.find((t) => t.zone === 'supply');
const target = gameTerritories(g).find(
const target = TERRITORIES.find(
(t) =>
t.type === 'stronghold' &&
t.id !== MOBILE_STRONGHOLD &&
+2 -2
View File
@@ -1,4 +1,4 @@
import { classicHomeworldLeaderSkillsProfile, classicNexusLeaderSkillsProfile, classicTechLeaderSkillsProfile, nativeDiscoveryLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, noOtherLeaderSkillModules, standaloneE3NexusLeaderSkillsProfile, strongholdLeaderSkillsProfile, type LeaderSkillProfile } from './leader-skill-profile';
import { homeworldLeaderSkillsProfile, classicNexusLeaderSkillsProfile, classicTechLeaderSkillsProfile, nativeDiscoveryLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, noOtherLeaderSkillModules, standaloneE3NexusLeaderSkillsProfile, strongholdLeaderSkillsProfile, type LeaderSkillProfile } from './leader-skill-profile';
import { classicDiscoveryLeaderSkillsProfile } from './discovery-module-profile';
export type BureaucratPaymentKind = 'auction' | 'shipment' | 'bribe' | 'revival';
export type BureaucratPaymentSource = {
@@ -39,7 +39,7 @@ export function bureaucratPaymentModeSupported(game: LeaderSkillProfile): boolea
return (noOtherLeaderSkillModules(game) &&
(!game.expansions.length ||
(game.expansions.length === 1 && game.expansions[0] === 'choam') ||
nativeExpansionLeaderSkillsProfile(game))) || classicHomeworldLeaderSkillsProfile(game) || classicTechLeaderSkillsProfile(game) || nativeTechLeaderSkillsProfile(game) || strongholdLeaderSkillsProfile(game) || classicNexusLeaderSkillsProfile(game) || standaloneE3NexusLeaderSkillsProfile(game) || classicDiscoveryLeaderSkillsProfile(game) || nativeDiscoveryLeaderSkillsProfile(game);
nativeExpansionLeaderSkillsProfile(game))) || homeworldLeaderSkillsProfile(game) || classicTechLeaderSkillsProfile(game) || nativeTechLeaderSkillsProfile(game) || strongholdLeaderSkillsProfile(game) || classicNexusLeaderSkillsProfile(game) || standaloneE3NexusLeaderSkillsProfile(game) || classicDiscoveryLeaderSkillsProfile(game) || nativeDiscoveryLeaderSkillsProfile(game);
}
export function bureaucratUsed(
used: readonly BureaucratPaymentUse[],
+4 -21
View File
@@ -9,34 +9,17 @@ export function discoveryModuleProfile(game: FactionModuleProfile): boolean {
discoveryModeSupported(game);
}
/** Original classic Homeworld/Discovery, optionally Tech/Advanced Strongholds. */
export function classicHomeworldDiscoveryProfile(game: FactionModuleProfile): boolean {
if (!game.homeworlds || typeof game.advanced !== 'boolean' || game.discoveryEnabled !== true ||
game.expansions.length || game.players.length < 2 || game.players.length > 6 ||
game.leaderSkills || game.nexusCards || (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) return false;
for (let i = 0; i < game.players.length; i++) {
const player = game.players[i];
for (let j = 0; j < i; j++) if (game.players[j].faction === player.faction) return false;
let classic = false;
for (const faction of FACTIONS)
if (faction.id === player.faction) { classic = faction.expansion === 'base'; break; }
if (!classic) return false;
}
return true;
/** Original Homeworld/Discovery under the same classic/native roster contract. */
export function homeworldDiscoveryProfile(game: FactionModuleProfile): boolean {
return !!game.homeworlds && discoveryModeSupported(game);
}
/** The same roster/module contract during play; live Discovery frames are expected. */
export function discoveryModeSupported(game: FactionModuleProfile): boolean {
if (classicHomeworldDiscoveryProfile(game)) return true;
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 ||
(game.strongholdCards && !game.advanced) || game.leaderSkills || game.homeworlds || game.nexusCards ||
(game.strongholdCards && !game.advanced) || game.leaderSkills || game.nexusCards ||
game.ecazTreachery ||
game.semutaPreview || game.advancedPreview || game.kullPreview || game.nexusKullPreview ||
game.guildBetrayalPreview || game.richeseBetrayalPreview ||
+16 -13
View File
@@ -1,7 +1,7 @@
import { classicNexusModulesProfile, pairedNexusModulesProfile } from './nexus-module-profile';
import { nativeFactionTechProfile } from './faction-module-profile';
import { classicDiscoveryLeaderSkillsProfile, classicDiscoveryNexusProfile, classicHomeworldDiscoveryProfile, discoveryModeSupported, discoveryModuleProfile, pairedDiscoveryNexusProfile } from './discovery-module-profile';
import { advancedMoritaniLeaderSkillsProfile, advancedNativeLeaderSkillsProfile, classicHomeworldLeaderSkillsProfile, classicNexusLeaderSkillsProfile, classicTechLeaderSkillsProfile, nativeDiscoveryLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, ordinaryLeaderSkillModeSupported, pairedNexusLeaderSkillsProfile, standaloneE3NexusLeaderSkillsProfile, strongholdLeaderSkillsProfile } from './leader-skill-profile';
import { classicDiscoveryLeaderSkillsProfile, classicDiscoveryNexusProfile, homeworldDiscoveryProfile, discoveryModeSupported, discoveryModuleProfile, pairedDiscoveryNexusProfile } from './discovery-module-profile';
import { advancedMoritaniLeaderSkillsProfile, advancedNativeLeaderSkillsProfile, homeworldLeaderSkillsProfile, classicNexusLeaderSkillsProfile, classicTechLeaderSkillsProfile, nativeDiscoveryLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, ordinaryLeaderSkillModeSupported, pairedNexusLeaderSkillsProfile, 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';
@@ -7622,7 +7622,8 @@ function moritaniAssassinateModeSupported(g: Game) {
(!g.nexusCards || (!!g.leaderSkills && standaloneE3NexusLeaderSkillsProfile(g))) &&
(!g.leaderSkills || advancedMoritaniLeaderSkillsProfile(g)) &&
(!g.strongholdCards || e3StrongholdFactionProfile(g) ||
(!!g.leaderSkills && strongholdLeaderSkillsProfile(g))) && !g.homeworlds &&
(!!g.leaderSkills && strongholdLeaderSkillsProfile(g))) &&
(!g.homeworlds || (!!g.leaderSkills && homeworldLeaderSkillsProfile(g))) &&
!g.discoveryEnabled && !g.discoveries &&
(!g.techTokens || nativeFactionTechProfile(g) || (!!g.leaderSkills && nativeTechLeaderSkillsProfile(g)));
}
@@ -7652,7 +7653,8 @@ function moritaniAssassinateIntegrity(g: Game) {
const opponent = getPlayer(g,receipt.opponent);
requireRule(receipt.turn <= g.turn && opponent.faction === receipt.faction &&
receipt.owner === state.owner && (TERRITORIES.some(t => t.id === receipt.territory) ||
validGameLocation(g, receipt.territory, 0)),
validGameLocation(g, receipt.territory, 0) ||
(receipt.territory.startsWith('homeworld:') && !!homeworldBattleLocation(g, receipt.territory))),
'The assassination history no longer matches its original battle participants.');
}
const pending = state.opportunities.filter(r => r.stage === 'skills' || r.stage === 'choice');
@@ -9524,7 +9526,7 @@ export function initializeDiscoveryGameForAudit(state: Game): Game {
requireRule(discoveryModuleProfile(state), 'Discovery requires a fresh supported classic, E1/E2 or standalone E3 lobby without unrelated overlays.');
requireFreshBaseRuntime(state);
requireFreshFactionInventory(state);
const homeworlds = classicHomeworldDiscoveryProfile(state);
const homeworlds = homeworldDiscoveryProfile(state);
if (homeworlds) requireRule(state.homeworlds?.custody === null,
'Start Homeworld Discovery with original undealt native custody.');
const g = initializeSetupGameForAudit(state, homeworlds, false, false, true, false, false, state.expansions.length > 0);
@@ -9535,10 +9537,10 @@ export function initializeDiscoveryGameForAudit(state: Game): Game {
/** Gated development setup; never dispatched by a player action or room route. */
export function initializeLeaderSkillsGameForAudit(state: Game): Game {
requireRule(!state.leaderSkills, 'Leader Skills cannot redeal existing skill cards.');
const homeworlds = classicHomeworldLeaderSkillsProfile(state);
const homeworlds = homeworldLeaderSkillsProfile(state);
if (state.homeworlds) {
requireRule(homeworlds && state.homeworlds.custody === null,
'Homeworld Leader Skills require a fresh classic lobby with only supported original Discovery, Tech or Advanced Stronghold modules.');
'Homeworld Leader Skills require a fresh supported classic or native lobby with only original Discovery, Tech or Advanced Stronghold modules.');
requireFreshBaseRuntime(state);
requireFreshFactionInventory(state);
}
@@ -9553,7 +9555,7 @@ export function initializeLeaderSkillsGameForAudit(state: Game): Game {
const initialized = initializeSetupGameForAudit(g, homeworlds, classicNexus || pairedNexusLeaderSkillsProfile(g) || standaloneE3NexusLeaderSkillsProfile(g), false, classicDiscoveryLeaderSkillsProfile(g) || nativeDiscoveryLeaderSkillsProfile(g) || (g.discoveryEnabled === true && classicNexus), true,
g.expansions.length === 1 && g.expansions[0] === 'choam', nativeExpansionLeaderSkillsProfile(g));
if (advancedMoritaniLeaderSkillsProfile(initialized)) initializeMoritaniAssassinateState(initialized);
if (nativeDiscoveryLeaderSkillsProfile(initialized) && byFaction(initialized, 'ecaz')) initialized.ecazOccupyPreview = true;
if ((homeworlds || nativeDiscoveryLeaderSkillsProfile(initialized)) && byFaction(initialized, 'ecaz')) initialized.ecazOccupyPreview = true;
return initialized;
}
/** Fresh all-fourteen-card classic or supported native skills; no public income toggle. */
@@ -9587,7 +9589,7 @@ function initializeSetupGameForAudit(state: Game, homeworlds: boolean, nexus = f
homeworldShipmentIntegrity(state);
const g = structuredClone(state);
requireFreshSetup(g, homeworlds || nexus || ix || factions);
const homeworldSkills = leaderSkills && classicHomeworldLeaderSkillsProfile(g);
const homeworldSkills = leaderSkills && homeworldLeaderSkillsProfile(g);
const skillStronghold = leaderSkills && strongholdLeaderSkillsProfile(g);
const skillTech = leaderSkills && (classicTechLeaderSkillsProfile(g) || nativeTechLeaderSkillsProfile(g) || skillStronghold || homeworldSkills || classicDiscoveryLeaderSkillsProfile(g));
const nativeTech = factions && !leaderSkills && nativeFactionTechProfile(g);
@@ -21902,7 +21904,7 @@ function sukPhysicalSignature(p: Player, g?: Game) {
...(g?.homeworlds?.custody ? { homeworlds: g.homeworlds.custody } : {}) });
}
function sukReserveHomes(g: Game, player: Player, territory: string, options: readonly SukRescueOption[]): Extract<Decision, { kind: 'sukRescue' }>['reserveHomes'] {
if (!g.homeworlds?.custody || !classicHomeworldLeaderSkillsProfile(g) ||
if (!g.homeworlds?.custody || !homeworldLeaderSkillsProfile(g) ||
!options.some(option => option.normal + option.elite > (option.kept ? 1 : 0))) return;
const homes = homeworldRule(() => homeworldForceGroups(homeworldContext(g), g.homeworlds!.custody!));
if (homes.some(home => home.id === territory && home.native === player.id)) return;
@@ -21972,11 +21974,11 @@ function settleSukRescue(g: Game, option: SukRescueOption, automatic: boolean, d
sardaukar.casualties.options.some(choice => JSON.stringify(choice) === JSON.stringify(pending.losses)),
'The Suk rescue lost its original Nexus Sardaukar casualty allocation.');
let physical: SukPhysicalRescueQuote | undefined;
if (classicHomeworldLeaderSkillsProfile(g)) {
if (homeworldLeaderSkillsProfile(g)) {
try {
physical = homeworldRule(() => quoteSukPhysicalRescue(homeworldLossContext(g), g.homeworlds!.custody!, {
player: player.id, territory: pending.territory, skill: pending.skill,
pool: pending.pool, losses: pending.losses!, option, destinations,
pool: pending.pool, losses: pending.losses!, option, destinations, eliteOrigins: pending.eliteOrigins,
}));
} catch (error) {
if (error instanceof SukPhysicalRescueError) throw new RuleError(error.message);
@@ -22020,7 +22022,8 @@ function settleSukRescue(g: Game, option: SukRescueOption, automatic: boolean, d
: 'none remained in the battle territory';
log(g, `${player.name}'s Suk Graduate saved ${option.normal} ordinary and ${option.elite} elite forces: ${kept}, ${returned} returned to reserves, and ${tanks.normal + tanks.elite} casualties went to the Tanks.${automatic ? ' The only legal rescue was applied automatically.' : ''}`, { faction: player.faction, name: 'Suk Graduate rescue' });
observeOccupation(g);
if (pending.eliteOrigins) stageIxSubstitution(g, player, pending.territory, pending.cards, quote!.eliteTanks!);
if (pending.eliteOrigins) stageIxSubstitution(g, player, pending.territory, pending.cards,
physical ? physical.eliteTanks! : quote!.eliteTanks!);
finishWinner(g, player, pending.territory, pending.cards);
}
function winnerDiscardSignature(pending: NonNullable<Game['pendingWinnerDiscards']>) {
+3 -1
View File
@@ -38,7 +38,9 @@ export function homeworldGameIntegrity(g: Game): void {
const beforeForces =
g.status === 'lobby' ||
(g.status === 'setup' &&
(g.setupStage === 'prediction' || (g.setupStage === 'leaderSkills' && !!g.leaderSkills) || g.setupStage === 'traitors'));
(g.setupStage === 'prediction' ||
((g.setupStage === 'skillTreachery' || g.setupStage === 'leaderSkills') && !!g.leaderSkills) ||
g.setupStage === 'traitors'));
if (beforeForces) {
if (state.custody !== null)
throw new HomeworldCustodyError(
+20 -5
View File
@@ -113,6 +113,8 @@ export function standaloneE3NexusLeaderSkillsProfile(game: LeaderSkillProfile):
/** 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 {
if (game.homeworlds)
return game.discoveryEnabled === true && homeworldLeaderSkillModulesSupported(game);
const players = game.players;
return typeof game.advanced === 'boolean' && game.discoveryEnabled === true &&
!!players && players.length >= 2 && players.length <= 6 &&
@@ -129,6 +131,7 @@ function discoveryLeaderSkillModulesSupported(game: LeaderSkillProfile): boolean
/** E1/E2 native skills may add Tech and Advanced Strongholds; original roster/decks remain. */
function nativeLeaderSkillModulesSupported(game: LeaderSkillProfile): boolean {
if (game.homeworlds) return homeworldLeaderSkillModulesSupported(game);
if (noOtherLeaderSkillModules(game) || pairedNexusLeaderSkillsProfile(game)) return true;
if (game.discoveryEnabled || game.discoveries) return discoveryLeaderSkillModulesSupported(game);
const players = game.players;
@@ -151,6 +154,7 @@ function nativeLeaderSkillModulesSupported(game: LeaderSkillProfile): boolean {
/** Standalone E3 native predicates retain their own roster, Duke and assassination limits. */
function e3LeaderSkillModulesSupported(game: LeaderSkillProfile): boolean {
if (game.homeworlds) return homeworldLeaderSkillModulesSupported(game);
if (noOtherLeaderSkillModules(game) || standaloneE3NexusLeaderSkillsProfile(game)) return true;
if (game.discoveryEnabled || game.discoveries) return discoveryLeaderSkillModulesSupported(game);
const players = game.players;
@@ -376,20 +380,31 @@ export function nativeTechLeaderSkillsProfile(game: LeaderSkillProfile): boolean
return !!game.techTokens && nativeExpansionLeaderSkillsProfile(game);
}
/** Original classic Homeworld/Skills, optionally Discovery/Tech/Advanced Strongholds. */
export function classicHomeworldLeaderSkillsProfile(game: LeaderSkillProfile): boolean {
/** Original Homeworld module envelope; native roster/deck predicates stay authoritative. */
function homeworldLeaderSkillModulesSupported(game: LeaderSkillProfile): boolean {
const players = game.players;
if (!game.homeworlds || typeof game.advanced !== 'boolean' || game.expansions.length ||
if (!game.homeworlds || typeof game.advanced !== 'boolean' ||
!players || players.length < 2 || players.length > 6 ||
game.nexusCards ||
(game.discoveryEnabled !== true && (game.discoveryEnabled || game.discoveries || game.discoveryStash || game.greatMaker)) ||
(game.techTokens && players.length < 3) || (game.strongholdCards && !game.advanced) ||
game.ecazTreachery ||
game.semutaPreview || game.moritaniAssassinatePreview || game.advancedPreview ||
game.semutaPreview || game.advancedPreview ||
(game.moritaniAssassinatePreview && (!game.advanced || game.expansions.length !== 1 ||
game.expansions[0] !== 'ecaz' || !players.some(player => player.faction === 'moritani'))) ||
game.kullPreview || game.nexusKullPreview || game.guildBetrayalPreview ||
game.richeseBetrayalPreview || game.nexusIxianReplacementPreview ||
game.nexusIxianBetrayalPreview || game.nexusHarkonnenBetrayalPreview ||
game.mentatQuestionPreview || game.spiceBankerIncomePreview) return false;
return true;
}
/** Original classic or existing native Homeworld/Skills, optionally Discovery/Tech/Strongholds. */
export function homeworldLeaderSkillsProfile(game: LeaderSkillProfile): boolean {
if (!game.homeworlds) return false;
if (game.expansions.length) return nativeExpansionLeaderSkillsProfile(game);
if (!homeworldLeaderSkillModulesSupported(game)) return false;
const players = game.players!;
for (let i = 0; i < players.length; i++) {
for (let j = 0; j < i; j++) if (players[j].faction === players[i].faction) return false;
let classic = false;
@@ -408,5 +423,5 @@ export function ordinaryLeaderSkillModeSupported(game: LeaderSkillProfile): bool
strongholdLeaderSkillsProfile(game) || classicNexusLeaderSkillsProfile(game) ||
pairedNexusLeaderSkillsProfile(game) || standaloneE3NexusLeaderSkillsProfile(game) ||
classicDiscoveryLeaderSkillsProfile(game) || nativeDiscoveryLeaderSkillsProfile(game) ||
classicHomeworldLeaderSkillsProfile(game);
homeworldLeaderSkillsProfile(game);
}
+10 -8
View File
@@ -3820,7 +3820,7 @@ export const RULE_TOPICS: RuleTopic[] = [
).join(' '),
steps: [
'Homeworlds are an optional module from Ecaz & Moritani, available under the official rules with Basic or Advanced play. They are separate locations outside Arrakis; they do not receive ordinary storm movement or count as ordinary strongholds.',
'Fresh private classic Homeworld prototypes compose all fourteen Leader Skills, Discovery seven cards/eight tokens or both, Basic/Advanced two through six unique base factions. Optional original Tech requires three or more seats; Stronghold Cards require Advanced. Original setup/native placement, phase-end industry, source-local held cards and cleanup before mandatory winner Tech remain. Typed native Suk saves stay at the same home; external Emperor returns explicitly allocate normal/elite counters to actual native homes, an implementation interpretation rather than ordinary revival placement. Nested battles receive no Homeworld bonus; later Cistern income is separate from support/rescue. Nexus/native rosters, unrelated overlays, public starts and pending interactions stay separate.',
'Fresh private Homeworld prototypes compose all fourteen Leader Skills, Discovery seven cards/eight tokens or both, Basic/Advanced two through six classic or supported native E1/E2 seats, or standalone Ecaz OR Moritani with classic opponents. Original family decks and Duke/assassination exclusions remain. Optional original Tech requires three or more seats; Stronghold Cards require Advanced. Original setup/native placement, phase-end industry, source-local held cards and cleanup before mandatory winner Tech remain. Typed native Suk saves stay at the same home; external Emperor returns explicitly allocate normal/elite counters to actual native homes, an implementation interpretation rather than ordinary revival placement. Nested battles receive no Homeworld bonus; later Cistern income is separate from support/rescue. Nexus, unsupported E3 pairs/mixed families, unrelated overlays, public starts and pending interactions stay separate.',
'Each faction uses its own card and matching world token. In Basic play the Emperor uses Kaitain alone. Advanced play adds Salusa Secundus for its Sardaukar; movement can transfer both force types between the two Emperor worlds.',
'Native reserves determine high or low population. Salusa counts Sardaukar, while the other cards count the faction’s reserves there. Reaching the high face’s minimum restores high population. Occupation also applies the low penalty.',
'Low population normally grants one additional free force revival and one additional bank spice when collecting CHOAM Charity. Salusa grants neither bonus; CHOAM’s Tupile reverses the usual pattern by granting an advantage while low and a penalty while high.',
@@ -3984,12 +3984,14 @@ export const RULE_TOPICS: RuleTopic[] = [
area: 'Verification',
status: 'Partial',
detail:
'Classic Homeworld Skills/Discovery or both optionally retain Tech/Advanced Strongholds:145 affected cases, typed native/visitor/nested rescue, phase-end industries, source-local held effects and cleanup before winner Tech. Fourteen completed original samples9008/9008 actions/237 JSON span two source batches after deleting an incidental undefined-key copy assertion; its original capture and actual continuation remain. Human390px nested Suk and native-world winner Tech choices retain actual armies. Earlier setup, typed transfers, native payments, occupied percentages/Bidding/private-lot binding and BG accompaniment evidence remains. The earlier bounded occupation profile completes44/50 samples; four Basic occupation/two CHOAM ruling captures remain, alongside five resumed and five fresh Advanced advisor-repair games. Full rules, all combinations, final assurance and deployed acceptance remain unverified.',
'Native Homeworld optional modules add35 semantic cases; affected180/180 and final repair union46/46, types/lint/build pass. Original six-seat games14/19 complete; twelve native families initially134/144 Advanced three-seat completions. Two preserved Ambassador deadlocks finish after the base-board bot repair; ruling captures remain, not an uninterrupted144/final-tree certificate. Human390px Ixian original card/training and typed invasion, plus native Moritani Suk rescue, retain physical custody. Dated classic14-game9008/9008/237 and occupation44/50 evidence remains in the source-bound runtime log. Full rules, combinations, final assurance and deployed acceptance remain open.',
evidence: [
'tests/homeworld-classic-skills-runtime.test.ts',
'tests/homeworld-classic-discovery-runtime.test.ts',
'tests/homeworld-skills-discovery-runtime.test.ts',
'tests/homeworld-optional-modules-runtime.test.ts',
'tests/homeworld-native-e1e2-modules-runtime.test.ts',
'tests/homeworld-native-e3-modules-runtime.test.ts',
'tests/homeworld-custody.test.ts',
'tests/homeworld-cards.test.ts',
'tests/homeworld-payment-income.test.ts',
@@ -4085,8 +4087,8 @@ export const RULE_TOPICS: RuleTopic[] = [
searchText: 'Hiereg Smuggler Jacurutu Sietch Cistern Ecological Testing Station Shrine Orgiz Processing Station Treachery Card Stash Spice Stash Ornithopter',
steps: [
'Fresh Discovery development setup supports classic, selected Ixian/Tleilaxu/CHOAM/Richese families, or standalone Ecaz or Moritani with classic opponents in Basic or Advanced, with seven extra Spice Cards and eight tokens. Original unused Tech Tokens may be selected with three through six seats; original Stronghold Cards require Advanced. Other modules, paired or mixed Ecaz/Moritani families and normal public starts stay gated.',
'Fresh classic Discovery retains Homeworlds in Basic/Advanced two through six unique base factions, original seven cards/eight tokens and native reserve placement. Original Tech requires three or more seats; Stronghold Cards require Advanced. The separate original leader-skills --discoveries entry also composes all fourteen Skills. Maker, later parent entry, nested shipment/rescue, phase-end industry and cleanup before mandatory winner Tech retain actual sources; held benefits never spill into a Homeworld or unrelated nested site. Nexus/native Homeworld rosters and unrelated overlays remain separate in this bounded profile, not outside the full goal.',
'The fresh Leader Skills entry may explicitly add Discoveries with original classic or supported native decks, all fourteen skills, original Tech at three or more seats and Advanced Stronghold Cards. Classic profiles may retain Homeworlds without Nexus. --nexus-cards additionally retains all12 with the actual settled-alliance end-Spice closing deal after Great Maker and both Advanced piles. Existing native roster and Duke/assassination/Harkonnen restrictions remain. Actual hands/offers, physical entry, nested tariffs, stash capacity, typed rescue, native aftermath and printed invoices retain their sources. Homeworlds with Nexus or native rosters, independent variants and separate Banker/Mentat previews remain outside these bounded compositions.',
'Fresh Discovery retains Homeworlds in Basic/Advanced two through six classic or supported native E1/E2 seats, or standalone Ecaz OR Moritani with classic opponents, original seven cards/eight tokens and native reserve placement. Original Tech requires three or more seats; Stronghold Cards require Advanced. The separate original leader-skills --discoveries entry also composes all fourteen Skills. Maker, later parent entry, nested shipment/rescue, phase-end industry and cleanup before mandatory winner Tech retain actual sources; held benefits never spill into a Homeworld or unrelated nested site. Nexus with Homeworlds, unsupported E3 pairs/mixed families and unrelated overlays remain separate in this bounded profile, not outside the full goal.',
'The fresh Leader Skills entry may explicitly add Discoveries with original classic or supported native decks, all fourteen skills, original Tech at three or more seats and Advanced Stronghold Cards. Supported profiles may retain Homeworlds without Nexus. --nexus-cards additionally retains all12 with the actual settled-alliance end-Spice closing deal after Great Maker and both Advanced piles. Existing native roster and Duke/assassination/Harkonnen restrictions remain. Actual hands/offers, physical entry, nested tariffs, stash capacity, typed rescue, native aftermath and printed invoices retain their sources. Homeworlds with Nexus, unsupported E3 pairs/mixed families, independent variants and separate Banker/Mentat previews remain outside these bounded compositions.',
'The same fresh entry also adds --nexus-cards --discoveries to paired Ixians+Tleilaxu, paired CHOAM+Richese or standalone Ecaz OR Moritani with classics: original all14 offered training, all12 Nexus and Discovery7/8, Basic/Advanced two through six, optional Tech at three or more seats and Advanced Strongholds. Original setup, first hands, offers and native components stay authoritative. Real Maker losses/votes/typed rides, both Advanced piles and settled alliance precede the end-Spice closing draw. E1 Suboid Cunning settles typed Suk rescue before one equal Cyborg Tank substitution; original cleanup and mandatory winner Tech precede a matching Face Dance. E2 keeps the signed two-marker frame and one invoice. Ecaz keeps quiet living/unclaimed Duke separate from training and a real owner-labelled mandatory Occupy; Moritani keeps skill-first assassination and one private Mentat replacement. Mixed/E3 pairs, Homeworlds, shared Duke, borrowed Smuggler/pair companions, native free returns, captured/foreign training, Basic odd Ecaz and Advanced Harkonnen stay separate.',
'A separate fresh Nexus entry may explicitly add Discoveries to classic factions or both native Ixians/Tleilaxu or CHOAM/Richese with classics, one original family deck and no Skills. Original Tech requires three or more seats; Stronghold Cards require Advanced. End-Mentat ownership and next-turn physical entry remain. Great Maker finishes its losses, votes and typed reserve ride before ordinary Nexus settlement; cards are offered only at the end of the entire Spice Blow after both Advanced piles and settled alliance, not immediately or at Mentat. Source-clear return ledgers, Guild-priced nested shipment and signed native one-invoice marker pairs retain physical pieces and original capacities. E3/mixed families, Homeworlds, unrelated previews and unresolved mixed-module Fremen Betrayal stay separate from this no-Skills entry.',
'Original Stronghold Cards are claimed only at end-Mentat. A held benefit applies in its named territory or the mobile stronghold through its actual declared copy; revealed Discovery locations do not inherit another territory’s defense or bank support. Jacurutu counts toward stronghold victory but creates no seventh Stronghold Card. Physical free entry, nested tariffs, trained or native force casualties, card cleanup and optional Tech reward retain their original sources.',
@@ -4128,8 +4130,8 @@ export const RULE_TOPICS: RuleTopic[] = [
},
{
area: 'Verification', status: 'Partial',
detail: 'Classic Homeworld Discovery with optional all14 Skills/Tech/Advanced Strongholds adds original Maker, parent entry, typed nested rescue, phase-end industry, source-local held cards and cleanup before native-world winner Tech. Affected145 cases and fourteen completed samples9008/9008 actions/237 JSON span two source batches after an incidental undefined-key copy assertion was deleted; original capture/continuation are preserved. Human390px nested Suk and mandatory Tech choices retain real armies. Earlier native/skill/Nexus phone programmes prove entry, tariffs/No-Field, stash, HMS support, CHOAM invoices and native transitions;31 native skill samples complete after three hidden-Smuggler repairs across two trees. Earlier proofs and the exhausted-cache ruling capture remain separate. Not every-effect/full combinations, assurance or deployed acceptance.',
evidence: ['tests/homeworld-classic-discovery-runtime.test.ts', 'tests/homeworld-skills-discovery-runtime.test.ts', 'tests/homeworld-optional-modules-runtime.test.ts', 'tests/discovery-native-e1-skills-runtime.test.ts', 'tests/discovery-native-e2-skills-runtime.test.ts', 'tests/discovery-native-e3-skills-runtime.test.ts', 'tests/discovery-skills-strongholds-runtime.test.ts', 'tests/discovery-native-strongholds-runtime.test.ts', 'tests/ecaz-stronghold-runtime.test.ts', 'tests/e3-native-tech-runtime.test.ts', 'tests/discovery-classic-nexus-lifecycle.test.ts', 'tests/discovery-classic-nexus-effects.test.ts', 'tests/discovery-orgiz-advanced-runtime.test.ts', 'tests/discovery-classic-skills-runtime.test.ts', 'tests/discovery-module-profile.test.ts', 'tests/discovery-native-typed-runtime.test.ts', 'tests/discovery-native-marker-runtime.test.ts', 'tests/discovery-native-e3-runtime.test.ts', 'tests/discoveries.test.ts', 'tests/discovery-runtime.test.ts', 'tests/great-maker.test.ts', 'tests/discovery-board.test.ts', 'tests/discovery-admission.test.ts', 'tests/discovery-controls.test.ts', 'tests/discovery-entry.test.ts', 'tests/discovery-entry-runtime.test.ts', 'tests/discovery-entry-controls.test.ts', 'tests/discovery-entry-review.test.ts', 'tests/discovery-collection.test.ts', 'tests/discovery-flight.test.ts', 'tests/discovery-flight-engine.test.ts', 'tests/discovery-flight-recovery.test.ts', 'tests/discovery-battle.test.ts', 'tests/discovery-battle-recovery.test.ts', 'tests/discovery-storm.test.ts', 'tests/discovery-storm-recovery.test.ts', 'tests/discovery-shrine-engine.test.ts', 'tests/discovery-shrine-recovery.test.ts'],
detail: 'Native Homeworld Discovery with optional all14 Skills/Tech/Advanced Strongholds adds original native setup, source-qualified parent/nested entry, capped No-Fields and physical aftermath. New35 cases; affected180/180, final repair46/46, types/lint/build pass. Twelve native families initially134/144 Advanced three-seat completions; two original Ambassador placement deadlocks finish after the bot respects existing base-board targets. CHOAM source captures and prior six-seat ruling captures remain. Human390px Ixian typed invasion and native Moritani Suk retain original armies. Dated classic/native/Nexus proofs remain separate in canonical docs; not every-effect/full combinations, assurance or deployed acceptance.',
evidence: ['tests/homeworld-native-e1e2-modules-runtime.test.ts', 'tests/homeworld-native-e3-modules-runtime.test.ts', 'tests/homeworld-classic-discovery-runtime.test.ts', 'tests/homeworld-skills-discovery-runtime.test.ts', 'tests/homeworld-optional-modules-runtime.test.ts', 'tests/discovery-native-e1-skills-runtime.test.ts', 'tests/discovery-native-e2-skills-runtime.test.ts', 'tests/discovery-native-e3-skills-runtime.test.ts', 'tests/discovery-skills-strongholds-runtime.test.ts', 'tests/discovery-native-strongholds-runtime.test.ts', 'tests/ecaz-stronghold-runtime.test.ts', 'tests/e3-native-tech-runtime.test.ts', 'tests/discovery-classic-nexus-lifecycle.test.ts', 'tests/discovery-classic-nexus-effects.test.ts', 'tests/discovery-orgiz-advanced-runtime.test.ts', 'tests/discovery-classic-skills-runtime.test.ts', 'tests/discovery-module-profile.test.ts', 'tests/discovery-native-typed-runtime.test.ts', 'tests/discovery-native-marker-runtime.test.ts', 'tests/discovery-native-e3-runtime.test.ts', 'tests/discoveries.test.ts', 'tests/discovery-runtime.test.ts', 'tests/great-maker.test.ts', 'tests/discovery-board.test.ts', 'tests/discovery-admission.test.ts', 'tests/discovery-controls.test.ts', 'tests/discovery-entry.test.ts', 'tests/discovery-entry-runtime.test.ts', 'tests/discovery-entry-controls.test.ts', 'tests/discovery-entry-review.test.ts', 'tests/discovery-collection.test.ts', 'tests/discovery-flight.test.ts', 'tests/discovery-flight-engine.test.ts', 'tests/discovery-flight-recovery.test.ts', 'tests/discovery-battle.test.ts', 'tests/discovery-battle-recovery.test.ts', 'tests/discovery-storm.test.ts', 'tests/discovery-storm-recovery.test.ts', 'tests/discovery-shrine-engine.test.ts', 'tests/discovery-shrine-recovery.test.ts', 'docs/HOMEWORLD_RUNTIME_IMPLEMENTATION.md'],
},
],
},
@@ -4648,7 +4650,7 @@ export const RULE_TOPICS: RuleTopic[] = [
developmentStage: 'Prototyped',
summary: 'Fourteen physical skills, private assignment choices, five battle disciplines, Planetologist movement/battle effects and Suk Graduate rescues, Mentat/Bureaucrat scoring and Bureaucrat payment redirection, Sandmaster victory spice, Rihani exchanges, optional Smuggler shipping and unmodified battle collection, Sandmaster route and worm-ride collection, sealed Spice Banker battle spending and optional Diplomat base-defense copying in development tables. Other effects and combined-module acceptance remain unfinished.',
steps: [
'Fresh classic Leader Skills retains Homeworlds in Basic/Advanced two through six unique base factions and all fourteen original offers, optionally original Discovery7/8, Tech at three or more seats and Advanced Stronghold Cards. Native placement follows training. Normal/trained Suk uses actual native, visitor or nested physical pools; native saved counters remain at that home. External Emperor returns explicitly allocate ordinary/elite counters to actual native homes, an implementation interpretation rather than ordinary revival placement. Support precedes rescue; later Cistern income, source-local held effects and mandatory winner Tech after original cleanup stay separate. Bureaucrat invoices and minimal legal bots are connected. Nexus/native Homeworld rosters, unrelated overlays and pending interactions stay separate.',
'Fresh Leader Skills retains Homeworlds in Basic/Advanced two through six classic or supported native E1/E2 seats, or standalone Ecaz OR Moritani with classic opponents, and all fourteen original offers, optionally original Discovery7/8, Tech at three or more seats and Advanced Stronghold Cards. Native placement follows training, including the original Ixian pre-training card choice. Normal/trained Suk uses actual native, visitor or nested physical pools; native saved counters remain at that home. External Emperor returns explicitly allocate ordinary/elite counters to actual native homes, an implementation interpretation rather than ordinary revival placement. Actual residual Cyborg Tanks feed optional equal Suboid substitution after rescue. Support precedes rescue; later Cistern income, source-local held effects and mandatory winner Tech after original cleanup stay separate. Bureaucrat invoices and minimal legal bots are connected. Nexus with Homeworlds, unsupported E3 pairs/mixed families, unrelated overlays and pending interactions stay separate.',
'Basic Moritani retains base opponents including Harkonnen. Its separate bounded Advanced classic/non-Harkonnen roster now composes the original assassination preview and all fourteen skills: winner Suk rescue and Rihani choice finish first, then an eligible different trained disc may be assassinated with its exact card return and printed bounty. No casualties repeat; original set-aside and one private Mentat replacement remain. See Moritani with Leader Skills for the exact scope.',
'Fresh Advanced profiles compose native Ixians, Tleilaxu, CHOAM and/or Richese with classic opponents and distinct required family decks. All fourteen skills and native starts remain. Auditor stays unskilled; cyborg casualties, original CHOAM support and Richese capped marker revelation remain physical. Foreign revival creates no new skill. Native Ixian Technology on Richese lots, own mixed No-Field, captured replacement, Advanced Atreides Suk/KH and unrelated overlays retain their guards.',
'Fresh three-through-six-seat Basic/Advanced development tables may compose all fourteen Skills with original Tech Tokens: classic factions, supported E1/E2 native families, or standalone Ecaz/Moritani with the exact ecaz deck and original roster exclusions. Starting cards/cache, skill/Traitor choices and printed/first-Storm assignment stay genuine. Existing leader-skills/banker-income entry preserves unused selected tokens; mixed E3 families, other overlays, public activation and saved-game conversion remain separate.',
@@ -4686,7 +4688,7 @@ export const RULE_TOPICS: RuleTopic[] = [
{ area:'Player controls',status:'Partial',detail:'Private card/leader selection, readable inspectors, public battle posture and revival draw/decline, Planetologist range/gather controls, Special weapon selection and Suk Graduate physical rescue choices are connected. Rihani has private history and separate keep-new/reveal-old controls; automatic battle effects need no confirmation. Smuggler has explicit ordinary shipping opt-out and No-Field companion opt-in with physical-versus-priced force counts, plus automatic surviving-leader battle collection with reveal-time guidance. Banker exposes separate sealed spending and authorized inspection. Sandmaster exposes explicit legal routes, per-territory collection choices and an optional worm-destination checkbox. Diplomat offers a named committed Worthless choice or decline after public reveal, and a separate surviving-loser destination/normal/elite retreat or decline. Bureaucrat offers redirect or full payment with explicit recipient amounts and deferred bribe custody. Remaining effect controls are missing.',evidence:['components/leader-skills.tsx','components/planetologist-movement.tsx','components/suk-graduate.tsx','components/rihani-decipherer.tsx','components/mentat-question.tsx','components/bureaucrat-payment.tsx','components/leader-skill-battle-guide.tsx','components/smuggler-shipment.tsx','components/spice-banker.tsx','components/sandmaster-movement.tsx','components/sandmaster-worm.tsx','components/diplomat-defense.tsx','components/diplomat-retreat.tsx','components/game-table.tsx'] },
{ area:'AI',status:'Partial',detail:'All four profiles use private offers, legal visibility/revival actions and Planetologist movement/battle candidates and legal Suk Graduate rescues, Rihani exchanges, shared battle scoring, Smuggler reserve/owned No-Field shipments and unmodified collection plans, Sandmaster route and worm collection, funded Banker commitments and optional Diplomat copying from public revealed defenses or choosing legal losing-battle retreat. Bureaucrat uses the public payee alliance to redirect enemy income or preserve allied income. These are legal prototype paths; complete skill strategy and difficulty calibration remain unfinished.',evidence:['game/bots.ts'] },
{ area:'Documentation',status:'Partial',detail:'All fourteen faces and common rules are recorded. Skilled capture follows public physical-card custody; replacement entitlement and other material timing interpretations remain explicit.',evidence:['docs/LEADER_SKILLS_RULES.md','docs/LEADER_SKILLS_CAPTURE.md','docs/LEADER_SKILLS_RUNTIME.md','docs/PLANETOLOGIST_RULES.md','docs/SUK_GRADUATE_RULES.md','docs/LEADER_BATTLE_EFFECTS.md','docs/MENTAT_QUESTION.md','docs/BUREAUCRAT_PAYMENTS.md','docs/SMUGGLER_SHIPMENT.md','docs/SPICE_BANKER_RUNTIME.md','docs/SANDMASTER_WORM.md','docs/DIPLOMAT_DEFENSE.md'] },
{ area:'Verification',status:'Partial',detail:'Classic Homeworld Skills optionally composes Discovery/Tech/Advanced Strongholds:145 affected cases, original nested/native/visitor rescue and physical industry/held-card/reward ordering. Fourteen completed samples9008/9008 actions/237 JSON span two source batches after an incidental undefined-key copy assertion was deleted; original capture/continuation remain. Human390px typed nested Suk and Homeworld winner Tech choice retain actual armies. Dated classic/native Skills-Tech and E1/E2 Stronghold evidence remains. Standalone E3 adds15 meaningful cases,152 affected, types/lint/build, nine actual programs and eight games1531 actions/38 continuations; expanded47-game batch13936/358. Human normal Warmaster1/Duke6/Carthag Shield wins7–6/releases Duke; Guild Suk precedes unused Master3 death/bounty, winner Tech and one private Caid. Full combinations, assurance, public/deployed acceptance stay open.',evidence:['tests/homeworld-classic-skills-runtime.test.ts','tests/homeworld-skills-discovery-runtime.test.ts','tests/homeworld-optional-modules-runtime.test.ts','tests/leader-skills-engine.test.ts','tests/leader-skills-capture.test.ts','tests/leader-battle-effects.test.ts','tests/suk-graduate.test.ts','tests/mentat-question.test.ts','tests/bureaucrat-payment.test.ts','tests/smuggler-battle.test.ts','tests/spice-banker.test.ts','tests/advanced-native-skills.test.ts','tests/fixture-advanced-native-skills.ts','tests/skills-tech-payments-runtime.test.ts','tests/skills-tech-battle-runtime.test.ts','tests/ix-choam-skills-tech-runtime.test.ts','tests/tleilaxu-skills-tech-runtime.test.ts','tests/richese-skills-tech-runtime.test.ts','tests/classic-skills-stronghold-runtime.test.ts','tests/native-skills-stronghold-runtime.test.ts','tests/ecaz-skills-modules-runtime.test.ts','tests/moritani-skills-modules-runtime.test.ts','docs/LEADER_SKILLS_RUNTIME.md','docs/HOMEWORLD_RUNTIME_IMPLEMENTATION.md'] },
{ area:'Verification',status:'Partial',detail:'Native Homeworld Skills optionally retains Discovery/Tech/Advanced Strongholds:35 new cases, affected180/180 and final repair46/46, types/lint/build pass. Original six-seat games14/19 complete; twelve native families initially134/144 Advanced three-seat completions, with two preserved Ambassador deadlocks completing after the canonical-target bot repair. Ruling captures remain; no uninterrupted144/final-tree certificate. Human390px actual Ixian pre-training card/training and typed native invasion, plus native Moritani Suk rescue, retain original custody. Dated classic/native skill, module and assassination proofs remain in canonical source logs. Full combinations, assurance, public/deployed acceptance stay open.',evidence:['tests/homeworld-native-e1e2-modules-runtime.test.ts','tests/homeworld-native-e3-modules-runtime.test.ts','tests/homeworld-classic-skills-runtime.test.ts','tests/homeworld-skills-discovery-runtime.test.ts','tests/homeworld-optional-modules-runtime.test.ts','tests/leader-skills-engine.test.ts','tests/leader-skills-capture.test.ts','tests/leader-battle-effects.test.ts','tests/suk-graduate.test.ts','tests/mentat-question.test.ts','tests/bureaucrat-payment.test.ts','tests/smuggler-battle.test.ts','tests/spice-banker.test.ts','tests/advanced-native-skills.test.ts','tests/fixture-advanced-native-skills.ts','tests/skills-tech-payments-runtime.test.ts','tests/skills-tech-battle-runtime.test.ts','tests/ix-choam-skills-tech-runtime.test.ts','tests/tleilaxu-skills-tech-runtime.test.ts','tests/richese-skills-tech-runtime.test.ts','tests/classic-skills-stronghold-runtime.test.ts','tests/native-skills-stronghold-runtime.test.ts','tests/ecaz-skills-modules-runtime.test.ts','tests/moritani-skills-modules-runtime.test.ts','docs/LEADER_SKILLS_RUNTIME.md','docs/HOMEWORLD_RUNTIME_IMPLEMENTATION.md'] },
],
},
{
+6 -3
View File
@@ -43,6 +43,8 @@ export type SukPhysicalRescueRequest = {
losses: Casualties;
option: SukRescueOption;
destinations?: SukReserveDestinations;
/** Preserve actual per-source Cyborg Tank losses for subsequent substitution. */
eliteOrigins?: true;
};
export type SukPhysicalRescueReceipt = {
player: string;
@@ -58,6 +60,7 @@ export type SukPhysicalRescueQuote = {
players: HomeworldCombatLossPlayer[];
removed: SukForceGroup[];
receipt: SukPhysicalRescueReceipt;
eliteTanks?: Record<string, number>;
};
export function sukReceiptSignature(receipt: SukRescueReceipt): string {
@@ -171,7 +174,7 @@ export class SukPhysicalRescueError extends Error {
}
}
/** Detached classic Homeworld settlement, including Arrakis rescue returns.
/** Detached Homeworld settlement, including Arrakis rescue returns.
* Printed Suk saves only real casualties. Native Homeworld forces already ARE
* reserves, so every saved native counter remains in its original pool.
*
@@ -190,7 +193,7 @@ export function quoteSukPhysicalRescue(
quoteHomeworldCombatLoss(context, custody, {
location: nativeHomes[0].id, player: request.player, losses: { normal: 0, elite: 0 },
});
const rescue = quoteSukRescue(request.skill, request.pool, request.losses, request.option);
const rescue = quoteSukRescue(request.skill, request.pool, request.losses, request.option, request.eliteOrigins);
const home = homes.find(candidate => candidate.id === request.territory);
if (request.territory.startsWith('homeworld:') && !home)
throw new SukPhysicalRescueError('Suk Graduate needs its original canonical battle Homeworld.');
@@ -254,7 +257,7 @@ export function quoteSukPhysicalRescue(
location: nativeHomes[0].id, player: request.player, losses: { normal: 0, elite: 0 },
});
return {
custody: transaction.state, players, removed,
custody: transaction.state, players, removed, eliteTanks: rescue.eliteTanks,
receipt: {
player: request.player, territory: request.territory,
source: home ? native ? 'native-homeworld' : 'visitor-homeworld' : 'arrakis',