Compose Advanced native Ixian and CHOAM Leader Skills

This commit is contained in:
Quintine committed 2026-10-03 13:06:44 +10:00
1 parent 64c8dfc770
commit 7ee064df14
20 files changed
+723 -71

No files matched your search

+9 -3
View File
@@ -5,6 +5,13 @@ existed. This follow-up connects the ordinary skill action paths for Basic CHOAM
with base opponents and checks native card sales and revival interruptions. with base opponents and checks native card sales and revival interruptions.
It does not certify the full expansion or open public starts/publication. It does not certify the full expansion or open public starts/publication.
**4 October Advanced continuation:** native CHOAM and/or Ixians with classic
opponents now compose under the
[bounded native contract](LEADER_SKILLS_RUNTIME.md#advanced-native-ixian-and-choam-composition--4-october-2026).
The Auditor remains ineligible at setup and paid revival. Native support income,
bank-held ally funding and Smuggler collection retain their original order.
Other Richese/module compositions below retain their separate guards.
## Configuration and components ## Configuration and components
The shared ordinary-skill profile requires Basic rules, exactly CHOAM & Richese, The shared ordinary-skill profile requires Basic rules, exactly CHOAM & Richese,
@@ -93,9 +100,8 @@ remains open.
Normal Banker income, ordinary Mentat questioning, modified Smuggler collection, Normal Banker income, ordinary Mentat questioning, modified Smuggler collection,
captured replacement entitlement and other recorded skill questions remain captured replacement entitlement and other recorded skill questions remain
unfinished. The [bounded Diplomat retreat](LEADER_SKILLS_RUNTIME.md) is connected unfinished. The [bounded Diplomat retreat](LEADER_SKILLS_RUNTIME.md) is connected
for supported battles, not a ruling for modified or simultaneous effects. Advanced for supported battles, not a ruling for modified or simultaneous effects. Mixed
Auditor rules and mixed Richese/other faction or module combinations remain Richese/other faction or optional-module combinations remain separate work. See the
separate work. See the
[runtime boundaries](LEADER_SKILLS_RUNTIME.md) and [decision register](RULE_DECISIONS.md). [runtime boundaries](LEADER_SKILLS_RUNTIME.md) and [decision register](RULE_DECISIONS.md).
Full AI implementation and difficulty calibration wait until every non-AI Full AI implementation and difficulty calibration wait until every non-AI
feature is complete. feature is complete.
+22 -1
View File
@@ -1,6 +1,6 @@
# Current development status # Current development status
Updated 3 October 2026. **The full twelve-faction goal is unfinished. Basic Updated 4 October 2026. **The full twelve-faction goal is unfinished. Basic
is playable and the six classic factions have a user-authorized Advanced preview. is playable and the six classic factions have a user-authorized Advanced preview.
Expansion starts and full rules/publication acceptance remain gated.** Expansion starts and full rules/publication acceptance remain gated.**
@@ -16,6 +16,27 @@ open mode gates.
## Current checkpoint and work ## Current checkpoint and work
**Advanced native Ixian/CHOAM Leader Skills — bounded Development Verified:**
fresh two-to-six-seat native Ixian and/or CHOAM plus classic tables retain
all fourteen skills, distinct required family decks and genuine native setup.
Typed support, mixed Suk rescue and native Suboid substitution compose; CHOAM
keeps Auditor exclusion, original support payer/income and Smuggler Collection
order. Explicit private Mentat and separate normal Banker opt-ins also retain
their original boundaries. Basic/public starts and unresolved rulings are unchanged.
Affected122/122, types/lint/build, ten controlled actual native programs and
three genuine CLI games pass:511 accepted actions/no rejection/twelve JSON
continuations. The natural games contain no battles, so they do not certify
battle effects. Fresh CLI **KFW75TCB v6** and human390px **ARRHUS7Z v20**
preserve original seats; actual typed-loss/rescue/substitution controls reach
Collection after refresh with wallet9, three board counters including one
cyborg and four Tanks counters. Controlled battle positions are staged after
genuine setup, not represented as natural histories. See the
[Advanced native skill contract](LEADER_SKILLS_RUNTIME.md#advanced-native-ixian-and-choam-composition--4-october-2026).
Full assurance/deployment remains open; live **ab5c782** remains last observed.
Final CLI2/2 and native/guide18/18 pass after the contract update. The latest
isolated build preserves human v20/Collection/wallet9; its native-scope guide
is visually readable at390px (351px topic width).
**Native Richese Occupy — bounded Development Verified:** **Native Richese Occupy — bounded Development Verified:**
Richese-present Advanced tables now resolve their mandatory Ecaz coalitions Richese-present Advanced tables now resolve their mandatory Ecaz coalitions
instead of rejecting unrelated native factions. Ordinary or marker-only Richese instead of rejecting unrelated native factions. Ordinary or marker-only Richese
+41
View File
@@ -1,5 +1,46 @@
# Dune implementation status # Dune implementation status
## 4 October 2026 — Advanced native Ixian and CHOAM Leader Skills
Two independent owners extended the bounded native profile/helper contracts and
genuine full-deck rule fixtures. One integration owner migrated engine admission,
native aftermath and Banker eligibility, preserved existing Basic/legacy entries,
and exercised original human controls. The new Advanced subset requires unique
two-to-six native Ixian/CHOAM plus classic seats, distinct required family decks
and no unrelated overlays. Foreign-ghola, captured replacement and Advanced
Atreides Suk/Kwisatz Haderach rulings remain guarded.
Affected122/122, types/lint/build and ten actual controlled programs pass:
five Ixian mixed-rescue/manual/four-policy continuations, one CHOAM Smuggler
collection, two private Mentat disclosures and two real bank-payment-to-Mentat
Banker grants. Three genuine new CLI games accept511 actions with no rejection
and twelve JSON continuations; none has a battle, so no battle-effect claim is
attributed to those histories. Natural scenario ordinals remain stable; new
Advanced Ixian/CHOAM/mixed samples use220–234.
Fresh original CLI **KFW75TCB v6** preserves genuine skill setup. Human390px
**ARRHUS7Z v20** begins with real admitted Ixian/Atreides/Emperor seats, then an
explicitly staged conserved Wind Pass position. Actual Dominic Vernius dial5/
support1, ordinary opposing leader/dial0, native Traitor declines and4suboid/
2cyborg loss controls open skilled Suk. Actual rescue saves one ordinary and one
cyborg, keeping the ordinary counter; actual native substitution sacrifices one
ordinary survivor for the remaining lost cyborg. Original phase opening finishes
through real ready actions; refreshed Collection has wallet9, three board forces
including one cyborg, four Tanks and zero cyborg Tanks. This is not a played
phase history, exhaustive persistence audit or deployed evidence.
Canonical runtime/guide/native follow-ups record current scope. Ordinary Mentat
activation, empty hands, normal Banker earned custody and other pending questions
are not settled by this composition. No game reset, public activation, AI tuning
or comprehensive assurance campaign.
Final CLI2/2 and native/guide18/18 pass; the obsolete Advanced CLI rejection
contracts now exercise original full-deck saved artifacts instead. Types/lint
and build pass. The rebuilt isolated worker retains human v20/Collection/
wallet9, and fresh visual guide evidence fits390px (351px topic width).
Five new canonical scope links resolve. Throwaway drivers and the owned tab
are removed/released; QA rooms and the original isolated store remain.
## 3 October 2026 — native Richese, No-Field and Stone Occupy composition ## 3 October 2026 — native Richese, No-Field and Stone Occupy composition
Two bounded owners implemented physical Stone normalization and native Richese Two bounded owners implemented physical Stone normalization and native Richese
+9 -2
View File
@@ -5,6 +5,13 @@ now supports the full fourteen-card Leader Skills deck, optionally alongside
Tleilaxu and base factions. Public starts and completion/publication gates remain Tleilaxu and base factions. Public starts and completion/publication gates remain
closed. Existing missing skill bands are not certified by this admission. closed. Existing missing skill bands are not certified by this admission.
**4 October Advanced continuation:** native Ixians and/or CHOAM with classic
opponents, distinct required family decks and all fourteen skills now compose
under the [bounded native contract](LEADER_SKILLS_RUNTIME.md#advanced-native-ixian-and-choam-composition--4-october-2026).
Advanced Ixian support, skilled Suk rescue and subsequent native substitution
use original physical counters. Basic Tleilaxu admission below is unchanged;
this Advanced subset does not admit foreign gholas or optional overlays.
## Setup and configuration ## Setup and configuration
Use Basic rules, exactly Ixians & Tleilaxu, two through six players including Use Basic rules, exactly Ixians & Tleilaxu, two through six players including
@@ -77,8 +84,8 @@ starting-card choice, subsequent private setup and unchanged seats. Final
source-bound check/build, samples, browser and preservation reports remain private. source-bound check/build, samples, browser and preservation reports remain private.
These are playability checks, not AI strength studies. These are playability checks, not AI strength studies.
Advanced Ixians, additional faction/module combinations, unresolved Sandmaster Additional faction/module combinations, unresolved Sandmaster eligibility during
eligibility during native HMS relocation and other missing Leader Skill effects remain native HMS relocation and other missing Leader Skill effects remain
separate work. Existing Banker, Mentat, Diplomat and modified-Smuggler questions separate work. Existing Banker, Mentat, Diplomat and modified-Smuggler questions
remain in the [decision register](RULE_DECISIONS.md). Full AI implementation and remain in the [decision register](RULE_DECISIONS.md). Full AI implementation and
difficulty tuning wait until all non-AI game features are complete. difficulty tuning wait until all non-AI game features are complete.
+38 -2
View File
@@ -28,6 +28,42 @@ activation still awaits the uniform-response UX decision; empty hands and
Advanced native/combined-module skills remain guarded. See Advanced native/combined-module skills remain guarded. See
[native preview scope](MENTAT_QUESTION.md#native-skill-integration--2-october-2026). [native preview scope](MENTAT_QUESTION.md#native-skill-integration--2-october-2026).
## Advanced native Ixian and CHOAM composition — 4 October 2026
The fresh local `leader-skills` profile now admits two through six unique
Advanced seats with native Ixians and/or CHOAM and otherwise classic factions.
Select distinct `ix` and/or `choam` decks, including each native faction's
required family. All fourteen physical skills and original native setup choices
remain: Ixian starting-card selection and HMS, genuine traitor offers, CHOAM's
unskilled Auditor, posture before faction powers and optional own revival.
Basic entries and public activation are unchanged. Foreign gholas, captured
replacement, Advanced Atreides Suk/Kwisatz Haderach, Richese mixed planning and
unrelated optional overlays retain their explicit guards.
Advanced cyborg support and casualties feed the existing skilled Suk rescue,
then native Suboid substitution without paying twice or creating counters.
CHOAM keeps its original support payer/income distinction and unmodified
Smuggler collection before ordinary Collection. Existing Planetologist,
Sandmaster, Mentat, Bureaucrat and Banker helpers share this admitted native
profile rather than independently accepting a different roster.
Explicit `--mentat-question` remains a private timing preview; ordinary
activation and empty-hand rulings are not resolved. The separate `banker-income`
profile also admits this native Advanced subset; normal front-shield income
remains unavailable until actual Mentat and keeps its provisional earned custody.
Bounded Development Verified: affected122/122, types/lint/build, ten actual
controlled native programs (five Ixian rescue/policy cases, one CHOAM collection,
two private Mentat and two original-payment-to-Mentat Banker cases), and three
genuine unstaged CLI games pass. Those games accept511 actions without rejection
and twelve JSON continuations, but contain no battle: battle evidence comes
from the controlled programs and human controls, not those natural histories.
Fresh CLI **KFW75TCB v6** preserves original setup. Human390px **ARRHUS7Z v20**
uses original admitted seats and an explicitly staged conserved Wind Pass
position, then real plans, typed losses, mixed Suk rescue, native substitution
and phase opening. Actual Collection after refresh retains wallet9, three board
forces including one cyborg, four Tanks counters and zero cyborg Tanks.
No full assurance campaign, public release or deployed claim.
## Connected boundary ## Connected boundary
@@ -94,8 +130,8 @@ response step awaits the user's UX decision; ordinary activation remains disable
auction, shipment, Richese sale and paid-bribe families within its recorded auction, shipment, Richese sale and paid-bribe families within its recorded
boundary. Other Bureaucrat payments and split funding remain incomplete. boundary. Other Bureaucrat payments and split funding remain incomplete.
[Banker normal income](SPICE_BANKER_RUNTIME.md#normal-income-local-contract--2-october-2026) [Banker normal income](SPICE_BANKER_RUNTIME.md#normal-income-local-contract--2-october-2026)
is connected only in its separate fresh classic or supported Basic native is connected only in its separate fresh classic, supported Basic native or
`banker-income` profile; ordinary/default and combined starts gain no activation. the bounded Advanced Ixian/CHOAM `banker-income` profile; ordinary/default starts gain no activation.
[Banker battle spending](SPICE_BANKER_RUNTIME.md) retains its separate [Banker battle spending](SPICE_BANKER_RUNTIME.md) retains its separate
sealed funds and survivor strength. [Smuggler shipping](SMUGGLER_SHIPMENT.md) sealed funds and survivor strength. [Smuggler shipping](SMUGGLER_SHIPMENT.md)
connects its optional normal reserve bonus and the [owned No-Field companion](SMUGGLER_NO_FIELD.md). connects its optional normal reserve bonus and the [owned No-Field companion](SMUGGLER_NO_FIELD.md).
+7
View File
@@ -7,6 +7,13 @@ Ordinary Leader Skills starts do not activate it. Leader Skills and public mode
acceptance remain partial. [Common skill rules](LEADER_SKILLS_RULES.md) govern assignment, acceptance remain partial. [Common skill rules](LEADER_SKILLS_RULES.md) govern assignment,
capture, death and the right to conceal a skilled leader. capture, death and the right to conceal a skilled leader.
**4 October composition:** the explicit preview also admits the bounded
Advanced native Ixian and/or CHOAM plus classic profile with distinct required
family decks. Actual private naming/disclosure returns to native posture and
keeps the shown card held; unrelated players receive no learned card.
See the [current native contract](LEADER_SKILLS_RUNTIME.md#advanced-native-ixian-and-choam-composition--4-october-2026).
Ordinary activation, empty hands and other pending rulings remain unchanged.
## Native skill integration — 2 October 2026 ## Native skill integration — 2 October 2026
The existing private preview now composes with the already-supported **Basic The existing private preview now composes with the already-supported **Basic
+7
View File
@@ -6,6 +6,13 @@ positive bank payments to automatic current Mentat collection.
Bounded coverage is **Partial / Development Verified**. No public start, Bounded coverage is **Partial / Development Verified**. No public start,
old-game retrofit or other module is enabled; this is not deployed acceptance. old-game retrofit or other module is enabled; this is not deployed acceptance.
**4 October composition:** the separate `banker-income` entry additionally
admits bounded Advanced native Ixians and/or CHOAM plus classic seats and their
distinct required family decks. Actual native support payment grants one
unavailable front-shield spice and original Mentat collects it once.
See the [current native contract](LEADER_SKILLS_RUNTIME.md#advanced-native-ixian-and-choam-composition--4-october-2026).
Ordinary/default activation and provisional earned custody are unchanged.
## Normal income local contract — 2 October 2026 ## Normal income local contract — 2 October 2026
The original photographed card says gain one spice when another player makes The original photographed card says gain one spice when another player makes
+2 -2
View File
@@ -1,4 +1,4 @@
import { basicExpansionLeaderSkillsProfile, noOtherLeaderSkillModules, type LeaderSkillProfile } from './leader-skill-profile'; import { nativeExpansionLeaderSkillsProfile, noOtherLeaderSkillModules, type LeaderSkillProfile } from './leader-skill-profile';
export type BureaucratPaymentKind = 'auction' | 'shipment' | 'bribe' | 'revival'; export type BureaucratPaymentKind = 'auction' | 'shipment' | 'bribe' | 'revival';
export type BureaucratPaymentSource = { export type BureaucratPaymentSource = {
event: string; event: string;
@@ -38,7 +38,7 @@ export function bureaucratPaymentModeSupported(game: LeaderSkillProfile): boolea
return noOtherLeaderSkillModules(game) && return noOtherLeaderSkillModules(game) &&
(!game.expansions.length || (!game.expansions.length ||
(game.expansions.length === 1 && game.expansions[0] === 'choam') || (game.expansions.length === 1 && game.expansions[0] === 'choam') ||
basicExpansionLeaderSkillsProfile(game)); nativeExpansionLeaderSkillsProfile(game));
} }
export function bureaucratUsed( export function bureaucratUsed(
used: readonly BureaucratPaymentUse[], used: readonly BureaucratPaymentUse[],
+7 -7
View File
@@ -1,4 +1,4 @@
import { basicExpansionLeaderSkillsProfile, ordinaryLeaderSkillModeSupported } from './leader-skill-profile'; import { nativeExpansionLeaderSkillsProfile, ordinaryLeaderSkillModeSupported } from './leader-skill-profile';
import { bribeTimingBlock, maximumBribe, type BribeOptions } from './bribe-options'; import { bribeTimingBlock, maximumBribe, type BribeOptions } from './bribe-options';
import { quoteSpicePlacement, stormExposesTerritory, stormSectorAfter, wormConsumesForces } from './disaster-rules'; import { quoteSpicePlacement, stormExposesTerritory, stormSectorAfter, wormConsumesForces } from './disaster-rules';
import { isStormCardDistance, type StormCardComponent } from './storm-cards'; import { isStormCardDistance, type StormCardComponent } from './storm-cards';
@@ -7778,7 +7778,7 @@ function bankerIncomeProfile(g: Game): boolean {
return g.players.length >= 2 && g.players.length <= 6 && return g.players.length >= 2 && g.players.length <= 6 &&
new Set(g.players.map(p => p.faction)).size === g.players.length && new Set(g.players.map(p => p.faction)).size === g.players.length &&
((!g.expansions.length && g.players.every(p => FACTIONS.some(f => f.id === p.faction && f.expansion === 'base'))) || ((!g.expansions.length && g.players.every(p => FACTIONS.some(f => f.id === p.faction && f.expansion === 'base'))) ||
basicExpansionLeaderSkillsProfile(g)) && nativeExpansionLeaderSkillsProfile(g)) &&
!g.nexusCards && !g.homeworlds && !g.techTokens && !g.nexusCards && !g.homeworlds && !g.techTokens &&
!g.strongholdCards && !g.discoveryEnabled && !g.discoveries && !g.discoveryStash && !g.strongholdCards && !g.discoveryEnabled && !g.discoveries && !g.discoveryStash &&
!g.greatMaker && !g.ecazTreachery && !g.semutaPreview && !g.mentatQuestionPreview && !g.greatMaker && !g.ecazTreachery && !g.semutaPreview && !g.mentatQuestionPreview &&
@@ -9302,13 +9302,13 @@ export function initializeLeaderSkillsGameForAudit(state: Game): Game {
const g = structuredClone(state); const g = structuredClone(state);
g.leaderSkills = createLeaderSkills(random); g.leaderSkills = createLeaderSkills(random);
return initializeSetupGameForAudit(g, false, false, false, false, true, return initializeSetupGameForAudit(g, false, false, false, false, true,
g.expansions.length === 1 && g.expansions[0] === 'choam', basicExpansionLeaderSkillsProfile(g)); g.expansions.length === 1 && g.expansions[0] === 'choam', nativeExpansionLeaderSkillsProfile(g));
} }
/** Fresh all-fourteen-card classic or Basic native skills; no public income toggle. */ /** Fresh all-fourteen-card classic or supported native skills; no public income toggle. */
export function initializeSpiceBankerIncomeGameForAudit(state: Game): Game { export function initializeSpiceBankerIncomeGameForAudit(state: Game): Game {
requireRule(bankerIncomeProfile(state) && !state.leaderSkills && requireRule(bankerIncomeProfile(state) && !state.leaderSkills &&
!state.spiceBankerIncomePreview && state.spiceBankerIncome === undefined, !state.spiceBankerIncomePreview && state.spiceBankerIncome === undefined,
'Spice Banker income requires a fresh classic or supported Basic native lobby with Leader Skills alone.'); 'Spice Banker income requires a fresh classic or supported native lobby with Leader Skills alone.');
requireFreshSetup(state, state.expansions.length > 0); requireFreshSetup(state, state.expansions.length > 0);
const g = structuredClone(state); const g = structuredClone(state);
g.leaderSkills = createLeaderSkills(random); g.leaderSkills = createLeaderSkills(random);
@@ -20277,11 +20277,11 @@ function currentBattleResolutionQuote(g: Game, canceledVoter?: string) {
'Suk Graduate rescue for Advanced Atreides awaits the Kwisatz Haderach loss-count ruling.'); 'Suk Graduate rescue for Advanced Atreides awaits the Kwisatz Haderach loss-count ruling.');
if (quote.sukGraduate) requireRule( if (quote.sukGraduate) requireRule(
ordinaryLeaderSkillModeSupported(g) && ordinaryLeaderSkillModeSupported(g) &&
(basicExpansionLeaderSkillsProfile(g) || g.players.every((p) => faction(p.faction).expansion === 'base')), (nativeExpansionLeaderSkillsProfile(g) || g.players.every((p) => faction(p.faction).expansion === 'base')),
'Suk Graduate rescue with expansion factions or other optional modules is still being implemented.'); 'Suk Graduate rescue with expansion factions or other optional modules is still being implemented.');
if (quote.rihani || quote.sandmaster) requireRule( if (quote.rihani || quote.sandmaster) requireRule(
ordinaryLeaderSkillModeSupported(g) && ordinaryLeaderSkillModeSupported(g) &&
(basicExpansionLeaderSkillsProfile(g) || g.players.every((p) => faction(p.faction).expansion === 'base')), (nativeExpansionLeaderSkillsProfile(g) || g.players.every((p) => faction(p.faction).expansion === 'base')),
'These Leader Skill victory effects with expansion factions or other optional modules are still being integrated.'); 'These Leader Skill victory effects with expansion factions or other optional modules are still being integrated.');
const smuggler = b.smugglerCollection ? nexusRule(() => settleSmugglerBattle(b.smugglerCollection!, const smuggler = b.smugglerCollection ? nexusRule(() => settleSmugglerBattle(b.smugglerCollection!,
!(b.smugglerCollection!.player === b.attacker ? quote.leaderDeaths.attacker : quote.leaderDeaths.defender), g.spice)) : null; !(b.smugglerCollection!.player === b.attacker ? quote.leaderDeaths.attacker : quote.leaderDeaths.defender), g.spice)) : null;
+47 -1
View File
@@ -12,6 +12,16 @@ export type LeaderSkillProfile = {
strongholdCards?: unknown; strongholdCards?: unknown;
techTokens?: unknown; techTokens?: unknown;
ecazTreachery?: unknown; ecazTreachery?: unknown;
semutaPreview?: unknown;
moritaniAssassinatePreview?: unknown;
advancedPreview?: unknown;
kullPreview?: unknown;
nexusKullPreview?: unknown;
guildBetrayalPreview?: unknown;
richeseBetrayalPreview?: unknown;
nexusIxianReplacementPreview?: unknown;
nexusIxianBetrayalPreview?: unknown;
nexusHarkonnenBetrayalPreview?: unknown;
}; };
export function noOtherLeaderSkillModules(game: LeaderSkillProfile): boolean { export function noOtherLeaderSkillModules(game: LeaderSkillProfile): boolean {
@@ -60,8 +70,44 @@ export function basicExpansionLeaderSkillsProfile(game: LeaderSkillProfile): boo
basicIxLeaderSkillsProfile(game) || basicChoamLeaderSkillsProfile(game); basicIxLeaderSkillsProfile(game) || basicChoamLeaderSkillsProfile(game);
} }
/** Source-clear Advanced native factions; optional decks never imply a seated faction. */
export function advancedNativeLeaderSkillsProfile(game: LeaderSkillProfile): boolean {
const players = game.players;
if (game.advanced !== true || !players || players.length < 2 || players.length > 6 ||
game.expansions.length < 1 || game.expansions.length > 2 ||
(game.expansions.length === 2 && game.expansions[0] === game.expansions[1]) ||
!noOtherLeaderSkillModules(game) ||
game.semutaPreview || game.moritaniAssassinatePreview || game.advancedPreview ||
game.kullPreview || game.nexusKullPreview ||
game.guildBetrayalPreview || game.richeseBetrayalPreview ||
game.nexusIxianReplacementPreview || game.nexusIxianBetrayalPreview ||
game.nexusHarkonnenBetrayalPreview) return false;
for (const expansion of game.expansions)
if (expansion !== 'ix' && expansion !== 'choam') return false;
let native = false;
for (let i = 0; i < players.length; i++) {
const player = players[i];
// Six seats at most: check prior seats without allocating per-rule quote sets.
for (let j = 0; j < i; j++) if (players[j].faction === player.faction) return false;
if (player.faction === 'ixians' || player.faction === 'choam') {
if (!game.expansions.includes(player.faction === 'ixians' ? 'ix' : 'choam')) return false;
native = true;
} else {
let classic = false;
for (const faction of FACTIONS)
if (faction.id === player.faction && faction.expansion === 'base') { classic = true; break; }
if (!classic) return false;
}
}
return native;
}
export function nativeExpansionLeaderSkillsProfile(game: LeaderSkillProfile): boolean {
return basicExpansionLeaderSkillsProfile(game) || advancedNativeLeaderSkillsProfile(game);
}
/** Shared by rule quotes, controls and minimal legal bot participation. */ /** Shared by rule quotes, controls and minimal legal bot participation. */
export function ordinaryLeaderSkillModeSupported(game: LeaderSkillProfile): boolean { export function ordinaryLeaderSkillModeSupported(game: LeaderSkillProfile): boolean {
return noOtherLeaderSkillModules(game) && return noOtherLeaderSkillModules(game) &&
(!game.expansions.length || basicExpansionLeaderSkillsProfile(game)); (!game.expansions.length || nativeExpansionLeaderSkillsProfile(game));
} }
+2 -2
View File
@@ -1,7 +1,7 @@
import { treacheryDeck, type Card } from './cards'; import { treacheryDeck, type Card } from './cards';
import { richeseCards } from './richese-cards'; import { richeseCards } from './richese-cards';
import { quoteTupileIntelligenceAnswer } from './tupile-intelligence-answer'; import { quoteTupileIntelligenceAnswer } from './tupile-intelligence-answer';
import {basicExpansionLeaderSkillsProfile,noOtherLeaderSkillModules,type LeaderSkillProfile} from './leader-skill-profile'; import {nativeExpansionLeaderSkillsProfile,noOtherLeaderSkillModules,type LeaderSkillProfile} from './leader-skill-profile';
export class MentatQuestionError extends Error {} export class MentatQuestionError extends Error {}
export const MENTAT_EMPTY_HAND = export const MENTAT_EMPTY_HAND =
@@ -43,7 +43,7 @@ export function mentatQuestionModeSupported(game: LeaderSkillProfile): boolean {
return noOtherLeaderSkillModules(game) && return noOtherLeaderSkillModules(game) &&
(!game.expansions.length || (!game.expansions.length ||
(game.expansions.length === 1 && game.expansions[0] === 'choam') || (game.expansions.length === 1 && game.expansions[0] === 'choam') ||
basicExpansionLeaderSkillsProfile(game)); nativeExpansionLeaderSkillsProfile(game));
} }
function inventory(expansions: readonly string[]): Card[] { function inventory(expansions: readonly string[]): Card[] {
return [ return [
+3 -3
View File
@@ -1,5 +1,5 @@
import { FACTIONS } from './catalog'; import { FACTIONS } from './catalog';
import { basicExpansionLeaderSkillsProfile } from './leader-skill-profile'; import { nativeExpansionLeaderSkillsProfile } from './leader-skill-profile';
import type { Game, GameView } from './engine'; import type { Game, GameView } from './engine';
import { splitLocation } from './board'; import { splitLocation } from './board';
@@ -66,8 +66,8 @@ export function groundMovementRange(
return planetologistRange(base + (options.choamBonus ?? 0), mode); return planetologistRange(base + (options.choamBonus ?? 0), mode);
} }
/** Keep the existing base-roster boundary and add the connected Basic Moritani profile. */ /** Keep the existing classic-roster boundary and add supported native factions. */
export function planetologistMovementModeSupported(game: Game | GameView): boolean { export function planetologistMovementModeSupported(game: Game | GameView): boolean {
return game.players.every(player => FACTIONS.some(f => f.id === player.faction && f.expansion === 'base')) || return game.players.every(player => FACTIONS.some(f => f.id === player.faction && f.expansion === 'base')) ||
basicExpansionLeaderSkillsProfile(game); nativeExpansionLeaderSkillsProfile(game);
} }
+11 -10
View File
@@ -4412,13 +4412,13 @@ export const RULE_TOPICS: RuleTopic[] = [
category: 'Advanced & expansions', category: 'Advanced & expansions',
coverage: 'Partial', coverage: 'Partial',
developmentStage: 'Prototyped', developmentStage: 'Prototyped',
summary: 'Basic CHOAM connects ordinary skill actions and private revival choices during a suspended sale.', summary: 'Native CHOAM connects full-deck skill actions, Basic suspended-sale revival and bounded Advanced support and aftermath.',
steps: [ steps: [
'This development combination uses Basic CHOAM and base opponents, the full fourteen-skill deck and no other optional modules. Starting cards precede skills and Traitors. Its 35-card Treachery deck includes Poison Tooth and Artillery; neither is a green Special. Auditor belongs to Advanced and cannot receive a skill.', 'This development combination uses Basic CHOAM and base opponents, the full fourteen-skill deck and no other optional modules. Starting cards precede skills and Traitors. Its 35-card Treachery deck includes Poison Tooth and Artillery; neither is a green Special. Auditor belongs to Advanced and cannot receive a skill.',
'Connected movement, shipping, payment and battle skills share their normal controls and legal choices. CHOAM cannot spend a card already committed to a sale or sell a Special already discarded after Planetologist battle use.', 'Connected movement, shipping, payment and battle skills share their normal controls and legal choices. CHOAM cannot spend a card already committed to a sale or sell a Special already discarded after Planetologist battle use.',
'Ghola may revive an eligible own leader while a declared sale waits. Finish the spent Ghola, optional private replacement draw or decline, and any required assignment before the original sale resumes. The replacement belongs to the actual revived leader.', 'Ghola may revive an eligible own leader while a declared sale waits. Finish the spent Ghola, optional private replacement draw or decline, and any required assignment before the original sale resumes. The replacement belongs to the actual revived leader.',
'The sale resumes with its original card, price and response passes. Canceling that sale preserves the completed free revival and does not return the spent Ghola.', 'The sale resumes with its original card, price and response passes. Canceling that sale preserves the completed free revival and does not return the spent Ghola.',
'Advanced Auditor, Richese and further combinations retain separate implementation boundaries. Remaining skill effects are unfinished; this prototype does not certify complete expansion rules or AI strength.', 'Fresh Advanced CHOAM and/or Ixians plus classic tables use distinct required family decks and all fourteen skills. Auditor is excluded both at assignment and paid revival; original support income and bank-held allied funding remain distinct. Richese and further optional combinations retain separate boundaries. Remaining skill effects are unfinished; this prototype does not certify complete expansion rules or AI strength.',
], ],
related: ['leader-skills', 'choam-modules', 'implementation-checklist'], related: ['leader-skills', 'choam-modules', 'implementation-checklist'],
checklist: [ checklist: [
@@ -4435,9 +4435,9 @@ export const RULE_TOPICS: RuleTopic[] = [
category: 'Advanced & expansions', category: 'Advanced & expansions',
coverage: 'Partial', coverage: 'Partial',
developmentStage: 'Prototyped', developmentStage: 'Prototyped',
summary: 'Basic Ixians connect the full skill deck to cyborg movement, the HMS and battle aftermath.', summary: 'Native Ixians connect the full skill deck to cyborg movement, the HMS and Basic or bounded Advanced battle aftermath.',
steps: [ steps: [
'The private starting-card choice precedes the full skill deal, Traitors and real first-Storm HMS placement. Basic Ixians can share this development combination with Tleilaxu and base factions; other optional modules and Advanced remain unavailable.', 'The private starting-card choice precedes the full skill deal, Traitors and real first-Storm HMS placement. Basic Ixians retain Tleilaxu and base combinations. Fresh Advanced Ixians and/or CHOAM plus classic tables use distinct required family decks; unrelated optional overlays and foreign gholas remain guarded.',
'Planetologist calculates each gathered origin separately using its selected cyborgs. Canceling native cyborg movement leaves the independent skill range available. Sandmaster routes can enter or leave the HMS through its current pointer.', 'Planetologist calculates each gathered origin separately using its selected cyborgs. Canceling native cyborg movement leaves the independent skill range available. Sandmaster routes can enter or leave the HMS through its current pointer.',
'Suk Graduate rescues finish before substitution. Select the original sectors of rescued cyborgs where needed; only cyborgs actually sent to the Tanks can return through substitution. Rihani also finishes before native substitution.', 'Suk Graduate rescues finish before substitution. Select the original sectors of rescued cyborgs where needed; only cyborgs actually sent to the Tanks can return through substitution. Rihani also finishes before native substitution.',
'Planetologist may substitute the green Thumper, Harvester or Amal in its weapon role. Their normal effects do not execute, and mandatory disposal still follows allowed, declined or canceled substitution.', 'Planetologist may substitute the green Thumper, Harvester or Amal in its weapon role. Their normal effects do not execute, and mandatory disposal still follows allowed, declined or canceled substitution.',
@@ -4505,7 +4505,7 @@ export const RULE_TOPICS: RuleTopic[] = [
summary: 'The fresh banker-income preview gains one deferred spice once per phase when another player makes one qualifying payment to the Spice Bank, then collects automatically at native Mentat.', summary: 'The fresh banker-income preview gains one deferred spice once per phase when another player makes one qualifying payment to the Spice Bank, then collects automatically at native Mentat.',
searchText: 'Spice Banker normal income bank payment four spice phase deferred front shield original owner death capture Mentat', searchText: 'Spice Banker normal income bank payment four spice phase deferred front shield original owner death capture Mentat',
steps: [ steps: [
'This separate fresh local profile admits unique two-to-six classic factions in Basic or Advanced with the base deck, or supported Basic Ixian/Tleilaxu/CHOAM/Moritani skill rosters with their single required deck. All fourteen skills and native setup remain. Advanced native rosters, mixed families, Nexus, Homeworld, Discovery, Tech Tokens and other overlays stay guarded; no public start or old-game retrofit.', 'This separate fresh local profile admits unique two-to-six classic factions in Basic or Advanced with the base deck, supported Basic Ixian/Tleilaxu/CHOAM/Moritani skill rosters with their single required deck, or bounded Advanced native Ixians and/or CHOAM plus classic seats with distinct required family decks. All fourteen skills and native setup remain. Foreign gholas, unrelated native families, Nexus, Homeworld, Discovery, Tech Tokens and other overlays stay guarded; no public start or old-game retrofit.',
'The printed normal band is mandatory once per phase: when another player makes a payment of at least four spice to the Spice Bank, gain one spice and place it in front of the shield. It is an additional bank grant, not a reduction of the payer’s payment or another player’s income.', 'The printed normal band is mandatory once per phase: when another player makes a payment of at least four spice to the Spice Bank, gain one spice and place it in front of the shield. It is an additional bank grant, not a reduction of the payer’s payment or another player’s income.',
'Only an actual completed positive bank payment qualifies. A payment to a player, zero/free/waived cost, self-payment, invalid declaration or unfinished possible payment supplies no income. Several smaller payments do not combine to reach four.', 'Only an actual completed positive bank payment qualifies. A payment to a player, zero/free/waived cost, self-payment, invalid declaration or unfinished possible payment supplies no income. Several smaller payments do not combine to reach four.',
'Use the original actual bank portion after recipient and counter settlement, not a quoted fee or guessed wallet change. Paid auctions, funded shipments, paid revival and eligible resolved battle support keep their native continuation once. Canceled Emperor, Guild or Tleilaxu revival income can leave a real bank payment. Tleilaxu self-revival pays the Bank; other-player revival normally pays Tleilaxu. Its separate free-revival bank award is not payer debit: three paid plus that one reward never becomes four paid to the Bank. A stopped shipment supplies no payment.', 'Use the original actual bank portion after recipient and counter settlement, not a quoted fee or guessed wallet change. Paid auctions, funded shipments, paid revival and eligible resolved battle support keep their native continuation once. Canceled Emperor, Guild or Tleilaxu revival income can leave a real bank payment. Tleilaxu self-revival pays the Bank; other-player revival normally pays Tleilaxu. Its separate free-revival bank award is not payer debit: three paid plus that one reward never becomes four paid to the Bank. A stopped shipment supplies no payment.',
@@ -4520,7 +4520,7 @@ export const RULE_TOPICS: RuleTopic[] = [
], ],
related: ['leader-skills', 'battle', 'bidding', 'revival', 'implementation-checklist'], related: ['leader-skills', 'battle', 'bidding', 'revival', 'implementation-checklist'],
checklist: [ checklist: [
{ area: 'Implementation', status: 'Implemented', detail: 'Actual final bank payer legs, native skill eligibility, one physical-card phase grant and original-owner automatic Mentat collection connect through fresh classic and supported Basic native skill profiles. Tleilaxu self-revival and canceled revival income preserve actual payer cost separately from the free award.', evidence: ['game/engine.ts', 'game/spice-banker-income.ts'] }, { area: 'Implementation', status: 'Implemented', detail: 'Actual final bank payer legs, native skill eligibility, one physical Banker phase grant and original-owner automatic Mentat collection connect through fresh classic, supported Basic native and bounded Advanced Ixian/CHOAM skill profiles. Tleilaxu self-revival and canceled revival income preserve actual payer cost separately from the free award.', evidence: ['game/engine.ts', 'game/spice-banker-income.ts'] },
{ area: 'Player controls', status: 'Implemented', detail: 'A full-width read-only public front-shield notice explains unavailable spice and automatic collection; genuine original payment and final Collection controls work at 390px through refresh. No new income action or confirmation.', evidence: ['components/spice-banker-income.tsx', 'components/game-table.tsx'] }, { area: 'Player controls', status: 'Implemented', detail: 'A full-width read-only public front-shield notice explains unavailable spice and automatic collection; genuine original payment and final Collection controls work at 390px through refresh. No new income action or confirmation.', evidence: ['components/spice-banker-income.tsx', 'components/game-table.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'All four minimal legal policies preserve identical available-budget actions with deferred income present or absent. Original wallets exclude front-shield currency; strategy/calibration remain deferred.', evidence: ['game/bots.ts', 'game/spice-banker.ts'] }, { area: 'AI', status: 'Partial', detail: 'All four minimal legal policies preserve identical available-budget actions with deferred income present or absent. Original wallets exclude front-shield currency; strategy/calibration remain deferred.', evidence: ['game/bots.ts', 'game/spice-banker.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'Printed gain/threshold/one-payment/phase/collection, original payment separation, provisional earned custody and per-payer split composition are explicit without universal rulings.', evidence: ['docs/LEADER_SKILLS_RULES.md', 'docs/SPICE_BANKER_RUNTIME.md', 'docs/RULE_DECISIONS.md'] }, { area: 'Documentation', status: 'Partial', detail: 'Printed gain/threshold/one-payment/phase/collection, original payment separation, provisional earned custody and per-payer split composition are explicit without universal rulings.', evidence: ['docs/LEADER_SKILLS_RULES.md', 'docs/SPICE_BANKER_RUNTIME.md', 'docs/RULE_DECISIONS.md'] },
@@ -4536,6 +4536,7 @@ export const RULE_TOPICS: RuleTopic[] = [
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.', 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: [ steps: [
'A development profile also connects Basic Moritani with base opponents and no other optional modules. It keeps the full deck and existing unfinished-effect boundaries; see Moritani with Leader Skills.', 'A development profile also connects Basic Moritani with base opponents and no other optional modules. It keeps the full deck and existing unfinished-effect boundaries; see Moritani with Leader Skills.',
'Fresh Advanced development profiles now compose native Ixians and/or CHOAM with classic opponents, two through six unique seats and their distinct required Ix/CHOAM decks. All fourteen skills and original native starting choices remain. Auditor never receives a skill. Typed cyborg support flows through Suk rescue before native substitution; CHOAM keeps original payer/income and Smuggler-before-Collection order. Existing Basic entries are unchanged; foreign gholas, captured replacement, Advanced Atreides Suk/KH and unrelated overlays remain guarded.',
'With this optional module, starting Treachery Cards are dealt after prediction and before skill assignment. Every faction privately receives two skills, keeps one, returns the other to the shuffled deck, and assigns its chosen card to an eligible leader. Skill and leader become public before traitor selection.', 'With this optional module, starting Treachery Cards are dealt after prediction and before skill assignment. Every faction privately receives two skills, keeps one, returns the other to the shuffled deck, and assigns its chosen card to an eligible leader. Skill and leader become public before traitor selection.',
'Before Battle Plans, leave the skilled leader face up to keep its normal effect, or move it behind the shield for possible use. A face-up leader cannot be selected for the plan. Concealing it while choosing another leader gives neither effect. A leader that must be used because no alternative remains moves behind the shield automatically.', 'Before Battle Plans, leave the skilled leader face up to keep its normal effect, or move it behind the shield for possible use. A face-up leader cannot be selected for the plan. Concealing it while choosing another leader gives neither effect. A leader that must be used because no alternative remains moves behind the shield automatically.',
'Warmaster, Master of Assassins, Swordmaster of Ginaz, Killer Medic and Prana-Bindu Adept are connected. They give another leader +1 for the matching physical card role, or the surviving assigned leader +3. These bonuses do not stack to +4. They change battle strength, not the printed disc value or leader bounty.', 'Warmaster, Master of Assassins, Swordmaster of Ginaz, Killer Medic and Prana-Bindu Adept are connected. They give another leader +1 for the matching physical card role, or the surviving assigned leader +3. These bonuses do not stack to +4. They change battle strength, not the printed disc value or leader bounty.',
@@ -4548,12 +4549,12 @@ export const RULE_TOPICS: RuleTopic[] = [
'A surviving skilled Sandmaster victory automatically adds three board spice to an existing pile in the battle territory, even with no surviving forces. Multiple piles await a placement ruling.', 'A surviving skilled Sandmaster victory automatically adds three board spice to an existing pile in the battle territory, even with no surviving forces. Multiple piles await a placement ruling.',
'Rihani normally inspects two random Traitor Deck cards privately, then reshuffles them. A surviving selected native Rihani also offers a separate optional draw of two. Decide before seeing them: once drawn, keep one new card and publicly reveal and return one unused old card. The other new card returns secretly. A captured Rihani grants only the optional exchange. Your private history records both samples; a card just called to win is already used.', 'Rihani normally inspects two random Traitor Deck cards privately, then reshuffles them. A surviving selected native Rihani also offers a separate optional draw of two. Decide before seeing them: once drawn, keep one new card and publicly reveal and return one unused old card. The other new card returns secretly. A captured Rihani grants only the optional exchange. Your private history records both samples; a card just called to win is already used.',
'Smuggler can include one free accompanying force in an off-planet shipment into an empty territory. Select the entire physical total; three forces cost the ordinary price of two. The checkbox can decline this optional benefit. Guild rounding follows the free force. Richese may instead ship one separate free reserve force beside its owned No-Field into an empty territory; the marker retains its ordinary price and hidden value. Existing forces, including advisors, prevent the bonus; ordinary Fremen on-planet reinforcement has no bonus.', 'Smuggler can include one free accompanying force in an off-planet shipment into an empty territory. Select the entire physical total; three forces cost the ordinary price of two. The checkbox can decline this optional benefit. Guild rounding follows the free force. Richese may instead ship one separate free reserve force beside its owned No-Field into an empty territory; the marker retains its ordinary price and hidden value. Existing forces, including advisors, prevent the bonus; ordinary Fremen on-planet reinforcement has no bonus.',
'Spice Banker can commit one through three own spice with its Battle Plan, separate from force support. Only the surviving selected skilled disc gains that strength. The amount stays private until authorized plan inspection or public reveal. It is paid on win or loss, including leader death, except a sole traitor-call winner spends nothing. Normal deferred income connects only in the separate fresh classic or supported Basic native banker-income profile; ordinary/default and combined starts are unchanged.', 'Spice Banker can commit one through three own spice with its Battle Plan, separate from force support. Only the surviving selected skilled disc gains that strength. The amount stays private until authorized plan inspection or public reveal. It is paid on win or loss, including leader death, except a sole traitor-call winner spends nothing. Normal deferred income connects only in the separate fresh classic, supported Basic native or bounded Advanced Ixian/CHOAM banker-income profile; ordinary/default starts are unchanged.',
'Diplomat may use one committed Worthless card as the opponent’s base Shield or Snooper if you played no defense. After both plans reveal, choose the card or decline before traitor decisions. A native Diplomat may protect its own selected trained leader this way; a captive grants no upper effect. The chosen Worthless must be discarded afterward, even if you win. A surviving trained leader that loses a supported battle may retreat selected undialed physical counters up to its printed/copied disc strength into one empty adjacent non-stronghold territory before ordinary loser losses; modified defenses remain unfinished.', 'Diplomat may use one committed Worthless card as the opponent’s base Shield or Snooper if you played no defense. After both plans reveal, choose the card or decline before traitor decisions. A native Diplomat may protect its own selected trained leader this way; a captive grants no upper effect. The chosen Worthless must be discarded afterward, even if you win. A surviving trained leader that loses a supported battle may retreat selected undialed physical counters up to its printed/copied disc strength into one empty adjacent non-stronghold territory before ordinary loser losses; modified defenses remain unfinished.',
'Mentat has an explicit private developer preview for naming a specific weapon before posture and faction powers, including supported Basic Ixian/Tleilaxu/CHOAM/Moritani skill starts. The target privately shows the named held card or chooses another held card. Weirding Way is a nameable default weapon; Chemistry is not. The card stays held and need not enter a Battle Plan. Every answer uses the same private response step; ordinary activation awaits the user’s timing-design decision. Empty hands skip without an answer. Private history is an observation, not a later hand guarantee.', 'Mentat has an explicit private developer preview for naming a specific weapon before posture and faction powers, including supported Basic Ixian/Tleilaxu/CHOAM/Moritani and bounded Advanced Ixian/CHOAM skill starts. The target privately shows the named held card or chooses another held card. Weirding Way is a nameable default weapon; Chemistry is not. The card stays held and need not enter a Battle Plan. Every answer uses the same private response step; ordinary activation awaits the user’s timing-design decision. Empty hands skip without an answer. Private history is an observation, not a later hand guarantee.',
'Bureaucrat may redirect two spice from a payment of at least five between two other players, once per phase. The payer still pays in full; the recipient receives two less. Declining keeps the use available for a later payment. Paid bribes qualify when transferred, with the remaining share still unavailable until Mentat Pause. Ordinary auctions, Guild income, Richese sales and actual paid Tleilaxu revival income in the Basic Tleilaxu skill profile have connected payment paths. A separate free-revival award is not part of the paid price and stays with Tleilaxu. Split funding, gifts, other payment families and combined modules remain unfinished.', 'Bureaucrat may redirect two spice from a payment of at least five between two other players, once per phase. The payer still pays in full; the recipient receives two less. Declining keeps the use available for a later payment. Paid bribes qualify when transferred, with the remaining share still unavailable until Mentat Pause. Ordinary auctions, Guild income, Richese sales and actual paid Tleilaxu revival income in the Basic Tleilaxu skill profile have connected payment paths. A separate free-revival award is not part of the paid price and stays with Tleilaxu. Split funding, gifts, other payment families and combined modules remain unfinished.',
'Smuggler records the existing battle-territory spice when plans reveal, capped by the selected leader’s unmodified strength. If that leader survives, it collects automatically even when losing or no forces remain. A captive pays its controller. Pending spice cannot fund the plan. Collection precedes Sandmaster’s addition; a drained pile receives none. Modified strength, multiple positive piles and combined modules remain guarded. Older saved battles keep their original behavior.', 'Smuggler records the existing battle-territory spice when plans reveal, capped by the selected leader’s unmodified strength. If that leader survives, it collects automatically even when losing or no forces remain. A captive pays its controller. Pending spice cannot fund the plan. Collection precedes Sandmaster’s addition; a drained pile receives none. Modified strength, multiple positive piles and combined modules remain guarded. Older saved battles keep their original behavior.',
'Other special Sandmaster relocation remains missing. Normal Banker income has a separate fresh classic or supported Basic native source-to-Mentat preview with provisional original-earned-faction custody, not general activation. Diplomat retreat uses a documented conservative outcome-order interpretation in standalone Leader Skills play. Foreign gholas, Face Dancers and combined modules remain unfinished. This development prototype does not enable public module starts.', 'Other special Sandmaster relocation remains missing. Normal Banker income has a separate fresh classic, supported Basic native or bounded Advanced Ixian/CHOAM source-to-Mentat preview with provisional original-earned-faction custody, not general activation. Diplomat retreat uses a documented conservative outcome-order interpretation in standalone Leader Skills play. Foreign gholas, Face Dancers and combined modules remain unfinished. This development prototype does not enable public module starts.',
...LEADER_SKILL_CARDS.map(card => `${card.name}. Normal: ${card.normal.join(' ')} Skilled battle: ${card.battle.join(' ')}`), ...LEADER_SKILL_CARDS.map(card => `${card.name}. Normal: ${card.normal.join(' ')} Skilled battle: ${card.battle.join(' ')}`),
], ],
related: ['spice-banker-income', 'choam-leader-skills', 'ixian-leader-skills', 'tleilaxu-leader-skills', 'moritani-leader-skills', 'setup','choam-modules','implementation-checklist'], related: ['spice-banker-income', 'choam-leader-skills', 'ixian-leader-skills', 'tleilaxu-leader-skills', 'moritani-leader-skills', 'setup','choam-modules','implementation-checklist'],
@@ -4562,7 +4563,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:'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:'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:'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:'Focused inventory, scoring, Voice, capture, saved private setup/revival Planetologist movement/cancellation and Suk Graduate casualty/SQLite checks, plus Rihani privacy/concurrent recovery battle-effect regressions and Smuggler shipment and battle collection, losing/captured survival, Sandmaster order, explosion, legacy and concurrent recovery, and Banker funding, death, traitor, privacy and concurrent settlement checks cover this boundary. Diplomat checks cover self-protection, mandatory discard, public-choice privacy, concurrent saved decisions, legacy continuation, losing-battle ordinary/elite retreat and concurrent saved force custody. Bureaucrat checks cover once-per-phase use, private source receipts and resumed payment custody. Sandmaster worm checks cover typed forces, decline, saved arrival reactions, concurrent writes, repeated distinct rides and special-Karama origin. Sample games and browser continuation are recorded separately; full module acceptance remains open.',evidence:['tests/leader-skills-engine.test.ts','tests/leader-skills-capture.test.ts','tests/leader-skills-recovery.test.ts','tests/leader-skills-controls.test.ts','tests/suk-graduate.test.ts','tests/suk-graduate-recovery.test.ts','tests/leader-battle-effects.test.ts','tests/leader-battle-effects-recovery.test.ts','tests/leader-battle-effects-controls.test.tsx','tests/mentat-question.test.ts','tests/mentat-question-bots.test.ts','tests/mentat-question-controls.test.tsx','tests/mentat-question-recovery.test.ts','tests/bureaucrat-payment.test.ts','tests/bureaucrat-payment-controls.test.tsx','tests/bureaucrat-payment-bots.test.ts','tests/bureaucrat-payment-recovery.test.ts','tests/smuggler-battle.test.ts','tests/smuggler-battle-recovery.test.ts','tests/smuggler-shipment.test.ts','tests/smuggler-shipment-controls.test.tsx','tests/smuggler-shipment-recovery.test.ts','tests/smuggler-no-field.test.ts','tests/smuggler-no-field-controls.test.tsx','tests/smuggler-no-field-recovery.test.ts','tests/spice-banker.test.ts','tests/spice-banker-recovery.test.ts','tests/spice-banker-controls.test.tsx','tests/sandmaster-movement.test.ts','tests/sandmaster-movement-recovery.test.ts','tests/sandmaster-movement-controls.test.tsx','tests/sandmaster-worm.test.ts','tests/sandmaster-worm-controls.test.tsx','tests/sandmaster-worm-recovery.test.ts','tests/diplomat-defense.test.ts','tests/diplomat-defense-controls.test.tsx','tests/diplomat-defense-recovery.test.ts','tests/diplomat-retreat-engine.test.ts','tests/diplomat-retreat-controls.test.tsx','tests/diplomat-retreat-recovery.test.ts'] }, { area:'Verification',status:'Partial',detail:'Existing role, custody, privacy, saved-continuation and physical-effect regressions cover the bounded standalone paths, not full module acceptance. Advanced Ixian/CHOAM composition adds affected122/122, types/lint/build, ten controlled actual native programs and three genuine CLI games (511 accepted actions, no rejection, twelve JSON continuations). Native games contained no battles; controlled programs and390px ARRHUS7Z v20 prove typed support, mixed Suk rescue and original substitution into Collection. Original KFW75TCB v6 retains genuine CLI setup. Comprehensive assurance follows all rules; no full-module or deployed claim.',evidence:['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','docs/LEADER_SKILLS_RUNTIME.md'] },
], ],
}, },
{ {
+2 -2
View File
@@ -1,7 +1,7 @@
import type { Game, GameView } from './engine'; import type { Game, GameView } from './engine';
import { splitLocation, validGameLocation } from './board'; import { splitLocation, validGameLocation } from './board';
import { faction } from './catalog'; import { faction } from './catalog';
import { basicExpansionLeaderSkillsProfile } from './leader-skill-profile'; import { nativeExpansionLeaderSkillsProfile } from './leader-skill-profile';
import { sandmasterLeader, sandmasterModeSupported } from './sandmaster-movement'; import { sandmasterLeader, sandmasterModeSupported } from './sandmaster-movement';
export type SandmasterWormCollection = { export type SandmasterWormCollection = {
@@ -26,7 +26,7 @@ export function sandmasterWormCollection(
const blocked = (reason: string): SandmasterWormCollection => const blocked = (reason: string): SandmasterWormCollection =>
({ leader, key: null, before: 0, blocked: reason }); ({ leader, key: null, before: 0, blocked: reason });
if (!sandmasterModeSupported(game) || game.ecazTreachery || if (!sandmasterModeSupported(game) || game.ecazTreachery ||
!(basicExpansionLeaderSkillsProfile(game) || !(nativeExpansionLeaderSkillsProfile(game) ||
game.players.every(p => faction(p.faction).expansion === 'base'))) game.players.every(p => faction(p.faction).expansion === 'base')))
return blocked('Sandmaster worm collection with other optional modules is still being integrated.'); return blocked('Sandmaster worm collection with other optional modules is still being integrated.');
if (destination === decision.territory || if (destination === decision.territory ||
+2 -2
View File
@@ -1,5 +1,5 @@
import { FACTIONS } from './catalog'; import { FACTIONS } from './catalog';
import { basicExpansionLeaderSkillsProfile, ordinaryLeaderSkillModeSupported, type LeaderSkillProfile } from './leader-skill-profile'; import { nativeExpansionLeaderSkillsProfile, ordinaryLeaderSkillModeSupported, type LeaderSkillProfile } from './leader-skill-profile';
import type { Card, Leader } from './cards'; import type { Card, Leader } from './cards';
import { splitLocation, validGameLocation } from './board'; import { splitLocation, validGameLocation } from './board';
import { leaderSkillBattleBonus, usesSurvivingSkilledLeader, type BattleLeaderSkill } from './leader-skill-combat'; import { leaderSkillBattleBonus, usesSurvivingSkilledLeader, type BattleLeaderSkill } from './leader-skill-combat';
@@ -11,7 +11,7 @@ export type SmugglerBattleReceipt = {
}; };
export function smugglerBattleModeSupported(g: LeaderSkillProfile & { players: readonly { faction: string }[] }) { export function smugglerBattleModeSupported(g: LeaderSkillProfile & { players: readonly { faction: string }[] }) {
return ordinaryLeaderSkillModeSupported(g) && return ordinaryLeaderSkillModeSupported(g) &&
(basicExpansionLeaderSkillsProfile(g) || g.players.every(p => FACTIONS.some(f => f.id === p.faction && f.expansion === 'base'))); (nativeExpansionLeaderSkillsProfile(g) || g.players.every(p => FACTIONS.some(f => f.id === p.faction && f.expansion === 'base')));
} }
export type SmugglerBattlePlan = { export type SmugglerBattlePlan = {
assignments: readonly BattleLeaderSkill[]; assignments: readonly BattleLeaderSkill[];
+257
View File
@@ -0,0 +1,257 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import {
advancedNativePlayer as player, advancedNativeStep, advanceAdvancedNativeSkillsToPhase,
assertAdvancedNativeCustody as custody, completeAdvancedNativeSkillsSetup,
completedAdvancedNativeSkillsGame, createAdvancedNativeSkillBattle,
finishAdvancedNativeSkillAftermath, initializeAdvancedNativeSkillsSetup,
openAdvancedNativeSkillBattle, rejectAdvancedNativeAction as reject,
} from './fixture-advanced-native-skills';
function trainer(game: Game, actor: string): string {
const assignment = game.leaderSkills!.assignments.find(a => a.owner === actor);
assert.ok(assignment);
return assignment.leader;
}
function untrainedLeader(game: Game, actor: string): string {
const p = game.players.find(p => p.id === actor)!;
const leaders = p.leaders.filter(l => !l.dead && l.id !== trainer(game, actor) && l.id !== 'choam-auditor');
leaders.sort((a, b) => a.strength - b.strength);
assert.ok(leaders[0]);
return leaders[0].id;
}
function resolvePlans(game: Game, actor: string, target: string): Game {
game = applyAction(game, actor, { type: 'traitorCall', call: false });
return applyAction(game, target, { type: 'traitorCall', call: false });
}
void test('Advanced native Ix typed support preserves paid cyborg casualties through skilled Suk and native substitution', () => {
let game = createAdvancedNativeSkillBattle({ family: 'ixians', requestedSkill: 'suk-graduate' });
const actor = player(game, 'ixians').id;
const target = player(game, 'emperor').id;
const funds = player(game, 'ixians').spice;
game = openAdvancedNativeSkillBattle(game, actor, target);
reject(game, actor, { type: 'battlePlan', dial: 5, support: 3, leader: trainer(game, actor) });
game = applyAction(game, actor, { type: 'battlePlan', dial: 5, support: 1, leader: trainer(game, actor) });
game = applyAction(game, target, { type: 'battlePlan', dial: 0, support: 0, leader: untrainedLeader(game, target) });
game = resolvePlans(game, actor, target);
if (game.decision?.kind === 'battleLosses') {
const choice = game.decision.options.findIndex(o => o.normal === 4 && o.elite === 2);
assert.ok(choice >= 0);
game = applyAction(game, actor, { type: 'decision', choice });
}
assert.equal(player(game, 'ixians').spice, funds - 1);
assert.equal(game.decision?.kind, 'sukRescue');
if (game.decision?.kind !== 'sukRescue') throw new Error('Expected native Suk rescue');
assert.deepEqual(game.pendingSukRescue!.losses, { normal: 4, elite: 2, paidNormal: 0, paidElite: 1 });
const pending = game;
const choice = game.decision.options.findIndex(o => o.normal === 1 && o.elite === 1 && o.kept?.kind === 'normal');
assert.ok(choice >= 0);
reject(game, target, { type: 'decision', event: game.decision.event, choice });
game = applyAction(game, actor, { type: 'decision', event: game.decision.event, choice });
assert.equal(player(game, 'ixians').forces['wind_pass:14'], 3);
assert.equal(player(game, 'ixians').reserves, 13);
assert.equal(player(game, 'ixians').tanks, 4);
assert.equal(player(game, 'ixians').elites!.reserves, 6);
assert.equal(player(game, 'ixians').elites!.tanks, 1);
assert.equal(game.decision?.kind, 'ixSubstitution');
reject(game, actor, { type: 'decision', sources: { 'wind_pass:14': 2 }, recover: { 'wind_pass:14': 2 } });
game = applyAction(game, actor, { type: 'decision', sources: { 'wind_pass:14': 1 }, recover: { 'wind_pass:14': 1 } });
game = finishAdvancedNativeSkillAftermath(game);
assert.equal(player(game, 'ixians').elites!.tanks, 0);
assert.equal(player(game, 'ixians').elites!.forces['wind_pass:14'], 1);
assert.equal(player(game, 'ixians').tanks, 4);
assert.equal(player(game, 'ixians').spice, funds - 1);
custody(game);
for (const difficulty of DIFFICULTIES) {
const view = viewGame(pending, actor);
view.players.find(p => p.id === actor)!.bot = difficulty;
const action = botActions(view)[0];
assert.ok(action, difficulty);
const rescued = applyAction(pending, actor, action);
assert.equal(rescued.pendingSukRescue, null);
assert.equal(player(rescued, 'ixians').spice, funds - 1);
custody(rescued);
}
});
void test('Advanced CHOAM cannot train its original Auditor; a real native trainer still takes the physical skill', () => {
const options = { family: 'choam' as const, requestedSkill: 'bureaucrat' as const };
let game = initializeAdvancedNativeSkillsSetup(options);
const actor = player(game, 'choam').id;
const offer = game.leaderSkills!.offers[actor];
reject(game, actor, { type: 'leaderSkill', event: offer.event, skill: 'bureaucrat', leader: 'choam-auditor' });
const native = viewGame(game, actor).leaderSkills!.eligibleLeaders[0];
assert.ok(native);
game = applyAction(game, actor, { type: 'leaderSkill', event: offer.event, skill: 'bureaucrat', leader: native.id });
assert.equal(game.leaderSkills!.assignments.find(a => a.owner === actor)!.leader, native.id);
game = completeAdvancedNativeSkillsSetup(game, options);
const auditor = player(game, 'choam').leaders.find(l => l.id === 'choam-auditor')!;
assert.ok(auditor && !auditor.dead);
custody(game);
});
void test('original Auditor paid revival returns that leader without drawing or replacing a skill', () => {
let game = completedAdvancedNativeSkillsGame({ family: 'choam', requestedSkill: 'bureaucrat' });
game = advanceAdvancedNativeSkillsToPhase(game, 4);
const owner = player(game, 'choam');
const auditor = owner.leaders.find(l => l.id === 'choam-auditor')!;
assert.ok(auditor && !auditor.dead);
// Controlled death position, preserving the original Auditor and trained leader.
auditor.dead = true;
auditor.deaths = 1;
const funds = owner.spice;
const skills = structuredClone(game.leaderSkills);
const revival = viewGame(game, owner.id).revival.leaders.find(l => l.id === auditor.id);
assert.ok(revival);
assert.equal(revival.cost, 2);
game = applyAction(game, owner.id, { type: 'reviveLeader', leader: auditor.id });
for (let i = 0; (game.response || game.decision || game.pendingRevival) && i < 100; i++) game = advancedNativeStep(game);
assert.equal(player(game, 'choam').leaders.find(l => l.id === auditor.id)!.dead, false);
assert.equal(player(game, 'choam').spice, funds - 2);
assert.equal(player(game, 'choam').leaderRevived, true);
assert.deepEqual(game.leaderSkills, skills);
reject(game, owner.id, { type: 'reviveLeader', leader: auditor.id });
custody(game);
});
void test('Advanced CHOAM own paid support does not generate its own income and Smuggler collects actual battlefield spice', () => {
let game = createAdvancedNativeSkillBattle({ family: 'choam', requestedSkill: 'smuggler' });
const actor = player(game, 'choam').id;
const target = player(game, 'emperor').id;
const ownFunds = player(game, 'choam').spice;
const enemyFunds = player(game, 'emperor').spice;
const collected = Math.min(5, player(game, 'choam').leaders.find(l => l.id === trainer(game, actor))!.strength);
game.spice['wind_pass:14'] = 5; // Explicit controlled battlefield-spice rule position.
game = openAdvancedNativeSkillBattle(game, actor, target);
reject(game, actor, { type: 'battlePlan', dial: 3, support: ownFunds + 1, leader: trainer(game, actor) });
game = applyAction(game, actor, { type: 'battlePlan', dial: 3, support: 2, leader: trainer(game, actor) });
game = applyAction(game, target, { type: 'battlePlan', dial: 2, support: 2, leader: untrainedLeader(game, target) });
game = resolvePlans(game, actor, target);
assert.equal(game.lastBattleContext?.smugglerCollection?.amount, collected);
assert.equal(game.lastBattleContext?.smugglerCollection?.stage, 'collected');
assert.equal(player(game, 'choam').spice, ownFunds - 2 + 1 + collected);
assert.equal(game.spice['wind_pass:14'], 5 - collected);
game = finishAdvancedNativeSkillAftermath(game);
assert.equal(player(game, 'choam').tanks, 4);
assert.equal(player(game, 'choam').forces['wind_pass:14'], 4);
assert.equal(player(game, 'emperor').tanks, 8);
assert.equal(player(game, 'emperor').spice, enemyFunds - 2);
assert.equal(player(game, 'choam').spice, ownFunds - 2 + 1 + 5,
'Original Collection collects the one spice left after the skilled leader, separately from support income.');
assert.equal(game.spice['wind_pass:14'] ?? 0, 0);
assert.equal(game.pendingChoamBattleIncome, null);
assert.equal(game.battle, null);
custody(game);
});
void test('CHOAM prepaid ally support consumes its original donor pool and excludes that share from native income', () => {
let game = createAdvancedNativeSkillBattle({ family: 'choam', skillOwner: 'atreides', requestedSkill: 'smuggler' });
const owner = player(game, 'atreides');
const donor = player(game, 'choam');
const enemy = player(game, 'emperor');
// Controlled mutual-alliance rule position; real native wallets fund the pledge.
owner.ally = donor.id;
donor.ally = owner.id;
const ownFunds = owner.spice;
const donorFunds = donor.spice;
const enemyFunds = enemy.spice;
game = applyAction(game, owner.id, { type: 'chooseBattle', territory: 'wind_pass', target: enemy.id });
while (game.decision?.kind === 'leaderSkillVisibility')
game = applyAction(game, game.decision.player,
{ type: 'leaderSkillVisibility', event: game.decision.event, hide: true });
assert.equal(game.decision?.kind, 'choamBattleFunding');
reject(game, owner.id, { type: 'decision', amount: 2 });
game = applyAction(game, donor.id, { type: 'decision', amount: 2 });
assert.equal(player(game, 'choam').spice, donorFunds - 2);
assert.equal(game.aid[donor.id].amount, 2);
for (let i = 0; (game.response || game.decision || game.battle?.preparation) && i < 100; i++) {
if (game.decision?.kind === 'leaderSkillVisibility')
game = applyAction(game, game.decision.player, { type: 'leaderSkillVisibility', event: game.decision.event, hide: true });
else game = advancedNativeStep(game);
}
reject(game, owner.id, { type: 'battlePlan', dial: 4, support: 4, allyPayment: 3, leader: trainer(game, owner.id) });
game = applyAction(game, owner.id, { type: 'battlePlan', dial: 4, support: 4, allyPayment: 2, leader: trainer(game, owner.id) });
game = applyAction(game, enemy.id, { type: 'battlePlan', dial: 2, support: 2, leader: untrainedLeader(game, enemy.id) });
game = finishAdvancedNativeSkillAftermath(resolvePlans(game, owner.id, enemy.id));
assert.equal(player(game, 'atreides').spice, ownFunds - 2);
assert.equal(player(game, 'choam').spice, donorFunds - 2 + 2,
'Income is floor((4 supported - 2 prepaid)/2) plus floor(2 opponent support/2).');
assert.equal(player(game, 'emperor').spice, enemyFunds - 2);
assert.equal(game.aid[donor.id]?.amount ?? 0, 0);
assert.equal(player(game, 'atreides').forces['wind_pass:14'], 4);
assert.equal(player(game, 'atreides').tanks, 4);
assert.equal(player(game, 'emperor').tanks, 8);
assert.equal(game.battle, null);
custody(game);
});
void test('native CHOAM Bureaucrat redirects only another payer while its native Worthless power remains usable', () => {
let game = createAdvancedNativeSkillBattle({ family: 'choam', requestedSkill: 'bureaucrat' });
Object.assign(game, { phase: 5, active: player(game, 'choam').id });
const owner = player(game, 'choam');
const payer = player(game, 'emperor');
const payee = player(game, 'atreides');
const funds = payer.spice;
const bribes = payee.bribes;
game = applyAction(game, payer.id, { type: 'bribe', target: payee.id, amount: 5 });
assert.equal(game.decision?.kind, 'bureaucratPayment');
assert.equal(game.decision?.player, owner.id);
const event = game.bureaucratPaymentEvent;
game = applyAction(game, owner.id, { type: 'decision', event, redirect: true });
assert.equal(player(game, 'emperor').spice, funds - 5);
assert.equal(player(game, 'atreides').bribes, bribes + 3);
reject(game, owner.id, { type: 'decision', event, redirect: true });
const index = game.deck.findIndex(c => c.name === 'Kulon');
assert.ok(index >= 0);
const kulon = game.deck.splice(index, 1)[0];
player(game, 'choam').hand.push(kulon); // Conserved canonical native card staging.
game = applyAction(game, owner.id, { type: 'card', mode: 'choam', card: kulon.id });
for (let i = 0; (game.response || game.pendingTreacheryDiscard) && i < 100; i++) game = advancedNativeStep(game);
assert.equal(game.choamMovement?.bonus, 1);
assert.equal(player(game, 'choam').hand.some(c => c.id === kulon.id), false);
assert.equal(game.discard.filter(c => c.id === kulon.id).length, 1);
custody(game);
});
void test('Advanced native admission preserves Atreides Suk and foreign-ghola unresolved boundaries without mutation', () => {
const options = { family: 'ixians' as const, skillOwner: 'atreides' as const, requestedSkill: 'suk-graduate' as const };
let game = initializeAdvancedNativeSkillsSetup(options);
while (game.decision?.kind === 'ixSetup') game = advancedNativeStep(game);
const actor = player(game, 'atreides').id;
const offer = game.leaderSkills!.offers[actor];
const leader = viewGame(game, actor).leaderSkills!.eligibleLeaders[0].id;
reject(game, actor, { type: 'leaderSkill', event: offer.event, skill: 'suk-graduate', leader }, /Suk Graduate is unavailable/);
game = completeAdvancedNativeSkillsSetup(game, options);
assert.ok(game.leaderSkills!.deck.includes('suk-graduate'));
assert.notEqual(game.leaderSkills!.assignments.find(a => a.owner === actor)!.skill, 'suk-graduate');
custody(game);
// Controlled original foreign-ghola custody boundary: use an untrained foreign
// leader, not a fabricated skill assignment or replacement deck.
const foreign = player(game, 'emperor').leaders.find(l => l.id !== trainer(game, player(game, 'emperor').id))!;
foreign.gholaBy = player(game, 'ixians').id;
reject(game, actor, { type: 'ready' }, /foreign gholas/);
});
void test('captured native trainer blocks replacement revival, retaining the physical skill and Tanks leader', () => {
let game = completedAdvancedNativeSkillsGame({ family: 'ixians', requestedSkill: 'warmaster', opponents: ['harkonnen', 'emperor'] });
game = advanceAdvancedNativeSkillsToPhase(game, 4);
const owner = player(game, 'ixians');
const trained = owner.leaders.find(l => l.id === trainer(game, owner.id))!;
trained.capturedBy = player(game, 'harkonnen').id;
const dead = owner.leaders.find(l => l.id !== trained.id)!;
for (const leader of owner.leaders) if (leader.id !== trained.id) {
leader.dead = true;
leader.deaths = 1;
} // Controlled captured trainer and all other native leaders in Tanks.
const skills = structuredClone(game.leaderSkills);
reject(game, owner.id, { type: 'reviveLeader', leader: dead.id }, /replacement-skill ruling/);
assert.equal(dead.dead, true);
assert.equal(trained.capturedBy, player(game, 'harkonnen').id);
assert.deepEqual(game.leaderSkills, skills);
custody(game);
});
+27 -15
View File
@@ -73,6 +73,7 @@ function assertFailedEvidence(
name: string, name: string,
expectedSeed: number, expectedSeed: number,
resumed: boolean, resumed: boolean,
expectedCount = 1,
) { ) {
const report = json<CliReport>(join(out, 'report.json')); const report = json<CliReport>(join(out, 'report.json'));
const results = json<{ results: CliResult[] }>(join(out, 'results.json')); const results = json<{ results: CliResult[] }>(join(out, 'results.json'));
@@ -82,9 +83,10 @@ function assertFailedEvidence(
assert.equal(report.before.tree, report.after.tree); assert.equal(report.before.tree, report.after.tree);
assert.match(report.before.commit, /\S/); assert.match(report.before.commit, /\S/);
assert.match(report.before.tree, /^[a-f\d]{64}$/); assert.match(report.before.tree, /^[a-f\d]{64}$/);
assert.equal(report.results.length, 1); assert.equal(report.results.length, expectedCount);
assert.equal(results.results.length, 1); assert.equal(results.results.length, expectedCount);
for (const result of [report.results[0], results.results[0]]) { for (const result of [report.results.find(row => row.name === name), results.results.find(row => row.name === name)]) {
assert.ok(result);
assert.equal(result.name, name); assert.equal(result.name, name);
assert.equal(result.seed, expectedSeed); assert.equal(result.seed, expectedSeed);
assert.equal(result.resumed, resumed); assert.equal(result.resumed, resumed);
@@ -280,10 +282,10 @@ void test('Tleilaxu Skills samples preserve their full module on saved continuat
assert.equal(existsSync(unsupported), false); assert.equal(existsSync(unsupported), false);
}); });
void test('Ixian Skills samples preserve their full module on saved continuation and reject Advanced before running', (t) => { void test('Ixian Skills samples preserve full Basic and Advanced native decks through saved continuation', (t) => {
const area = temporary(t); const area = temporary(t);
const out = join(area, 'skills'); const out = join(area, 'skills');
const result = run(out, '--profile', 'ix-skills', '--players', '2', '--seed', '1000', '--max-actions', '1'); const result = run(out, '--profile', 'ix-skills', '--rules', 'basic', '--players', '2', '--seed', '1000', '--max-actions', '1');
assert.equal(result.status, 1); assert.equal(result.status, 1);
assertFailedEvidence(out, 'ix-skills-2-basic', 1026, false); assertFailedEvidence(out, 'ix-skills-2-basic', 1026, false);
const snapshot = join(out, 'failed-ix-skills-2-basic.json'); const snapshot = join(out, 'failed-ix-skills-2-basic.json');
@@ -294,16 +296,22 @@ void test('Ixian Skills samples preserve their full module on saved continuation
const resumed = join(area, 'skills-resumed'); const resumed = join(area, 'skills-resumed');
assert.equal(run(resumed, '--resume', snapshot, '--seed', '1000', '--max-actions', '1').status, 1); assert.equal(run(resumed, '--resume', snapshot, '--seed', '1000', '--max-actions', '1').status, 1);
assertFailedEvidence(resumed, 'ix-skills-2-basic', 1026, true); assertFailedEvidence(resumed, 'ix-skills-2-basic', 1026, true);
const unsupported = join(area, 'unsupported'); const advanced = join(area, 'advanced');
const bad = run(unsupported, '--profile', 'ix-skills', '--rules', 'advanced'); assert.equal(run(advanced, '--profile', 'ix-skills', '--rules', 'advanced', '--players', '2', '--seed', '1000', '--max-actions', '1').status, 1);
assert.equal(bad.status, 1); for (const [name, seed] of [['ix-skills-2-advanced', 1220], ['ix-skills-ixians-choam-2-advanced', 1230]] as const) {
assert.equal(existsSync(unsupported), false); assertFailedEvidence(advanced, name, seed, false, 2);
const state = json<typeof game>(join(advanced, `failed-${name}.json`));
assert.equal(state.advanced, true);
const cards = [...state.leaderSkills.deck, ...Object.values(state.leaderSkills.offers).flatMap(o => o.cards), ...state.leaderSkills.assignments.map(a => a.skill)];
assert.equal(new Set(cards).size, 14);
assert.equal(cards.length, 14);
}
}); });
void test('CHOAM Skills samples preserve their full module on saved continuation and reject Advanced before running', (t) => { void test('CHOAM Skills samples preserve full Basic and Advanced native decks through saved continuation', (t) => {
const area = temporary(t); const area = temporary(t);
const out = join(area, 'skills'); const out = join(area, 'skills');
const result = run(out, '--profile', 'choam-skills', '--players', '2', '--seed', '1000', '--max-actions', '1'); const result = run(out, '--profile', 'choam-skills', '--rules', 'basic', '--players', '2', '--seed', '1000', '--max-actions', '1');
assert.equal(result.status, 1); assert.equal(result.status, 1);
assertFailedEvidence(out, 'choam-skills-2-basic', 1031, false); assertFailedEvidence(out, 'choam-skills-2-basic', 1031, false);
const snapshot = join(out, 'failed-choam-skills-2-basic.json'); const snapshot = join(out, 'failed-choam-skills-2-basic.json');
@@ -314,10 +322,14 @@ void test('CHOAM Skills samples preserve their full module on saved continuation
const resumed = join(area, 'skills-resumed'); const resumed = join(area, 'skills-resumed');
assert.equal(run(resumed, '--resume', snapshot, '--seed', '1000', '--max-actions', '1').status, 1); assert.equal(run(resumed, '--resume', snapshot, '--seed', '1000', '--max-actions', '1').status, 1);
assertFailedEvidence(resumed, 'choam-skills-2-basic', 1031, true); assertFailedEvidence(resumed, 'choam-skills-2-basic', 1031, true);
const unsupported = join(area, 'unsupported'); const advanced = join(area, 'advanced');
const bad = run(unsupported, '--profile', 'choam-skills', '--rules', 'advanced'); assert.equal(run(advanced, '--profile', 'choam-skills', '--rules', 'advanced', '--players', '2', '--seed', '1000', '--max-actions', '1').status, 1);
assert.equal(bad.status, 1); assertFailedEvidence(advanced, 'choam-skills-2-advanced', 1225, false);
assert.equal(existsSync(unsupported), false); const state = json<typeof game>(join(advanced, 'failed-choam-skills-2-advanced.json'));
assert.equal(state.advanced, true);
const cards = [...state.leaderSkills.deck, ...Object.values(state.leaderSkills.offers).flatMap(o => o.cards), ...state.leaderSkills.assignments.map(a => a.skill)];
assert.equal(new Set(cards).size, 14);
assert.equal(cards.length, 14);
}); });
void test('Homeworld and Nexus sample snapshots resume only their matching module and roster', (t) => { void test('Homeworld and Nexus sample snapshots resume only their matching module and roster', (t) => {
const area = temporary(t); const area = temporary(t);
+198
View File
@@ -0,0 +1,198 @@
import assert from 'node:assert/strict';
import {
applyAction, createGame, initializeLeaderSkillsGameForAudit, initializeSpiceBankerIncomeGameForAudit, joinGame, newPlayer,
viewGame, type Action, type Game, type Player,
} from '../game/engine';
import type { FactionId } from '../game/catalog';
import { LEADER_SKILL_CARDS, type LeaderSkillId } from '../game/leader-skill-cards';
import { validateLeaderSkills } from '../game/leader-skills';
import { nextSpiceBankerIncomeNativeStep } from './fixture-spice-banker-income';
export type AdvancedNativeSkillsOptions = {
family: 'ixians' | 'choam';
requestedSkill?: LeaderSkillId;
skillOwner?: FactionId;
opponents?: readonly FactionId[];
bankerIncome?: boolean;
/** Continue this actual authenticated lobby/setup without replacing its seats. */
initial?: Game;
};
export function advancedNativePlayer(game: Game, faction: FactionId): Player {
const player = game.players.find(p => p.faction === faction);
assert.ok(player, `Missing actual ${faction} seat`);
return player;
}
/** Only the first physical all14 shuffle is controlled. Later native shuffles
* use real entropy. Place the requested skill in its actual owner's offer. */
function withSkillShuffle<T>(skill: LeaderSkillId, ownerIndex: number, initialize: () => T): T {
const descriptor = Object.getOwnPropertyDescriptor(crypto, 'getRandomValues');
const original = crypto.getRandomValues.bind(crypto);
const target = LEADER_SKILL_CARDS.findIndex(card => card.id === skill);
const slot = ownerIndex * 2;
assert.ok(target >= 0 && slot < LEADER_SKILL_CARDS.length);
let index = LEADER_SKILL_CARDS.length - 1;
crypto.getRandomValues = <V extends ArrayBufferView | null>(array: V): V => {
if (index <= 0) return Reflect.apply(original, crypto, [array]) as V;
assert.ok(array instanceof Uint32Array && array.length === 1);
const swap = index === Math.max(target, slot) ? Math.min(target, slot) : index;
array[0] = swap === index ? 0xffffffff : Math.ceil(swap * 0x100000000 / (index + 1));
index--;
return array;
};
try { return initialize(); }
finally {
if (descriptor) Object.defineProperty(crypto, 'getRandomValues', descriptor);
else Reflect.deleteProperty(crypto, 'getRandomValues');
}
}
/** Genuine fresh Advanced native lobby, native starting-card decisions and
* physical skill offers. No played save is converted and no deck is cherry-picked. */
export function initializeAdvancedNativeSkillsSetup(options: AdvancedNativeSkillsOptions): Game {
const ownerFaction = options.skillOwner ?? options.family;
const expansion = options.family === 'ixians' ? 'ix' : 'choam';
let game: Game;
if (options.initial) game = structuredClone(options.initial);
else {
const factions = [ownerFaction, options.family, ...(options.opponents ?? ['atreides', 'emperor'] as FactionId[])]
.filter((faction, index, all) => all.indexOf(faction) === index);
game = createGame('ADVNATIVE', newPlayer(factions[0], factions[0], factions[0]), true, [expansion]);
for (const faction of factions.slice(1)) joinGame(game, newPlayer(faction, faction, faction));
}
assert.equal(game.advanced, true);
assert.ok(game.expansions.includes(expansion));
const owner = advancedNativePlayer(game, ownerFaction);
if (game.status === 'setup') {
assert.ok(game.leaderSkills && game.turn === 1 && game.phase === 0,
'Only the original fresh native skill setup may continue.');
return game;
}
assert.equal(game.status, 'lobby');
for (const player of game.players) if (!player.ready) game = applyAction(game, player.id, { type: 'ready' });
return withSkillShuffle(options.requestedSkill ?? 'suk-graduate', game.players.findIndex(p => p.id === owner.id),
() => options.bankerIncome ? initializeSpiceBankerIncomeGameForAudit(game) : initializeLeaderSkillsGameForAudit(game));
}
/** Finish original setup decisions from the actual own-seat projections. */
export function completeAdvancedNativeSkillsSetup(state: Game, options: AdvancedNativeSkillsOptions): Game {
let game = state;
const owner = advancedNativePlayer(game, options.skillOwner ?? options.family).id;
for (let i = 0; game.status === 'setup' && i < 200; i++) {
if (game.decision?.kind === 'ixSetup') {
const actor = game.decision.player;
const card = viewGame(game, actor).ixTechnology!.setup![0];
assert.ok(card);
game = applyAction(game, actor, { type: 'decision', card: card.id });
} else if (game.setupStage === 'leaderSkills') {
const actor = Object.keys(game.leaderSkills!.offers)[0];
const offer = game.leaderSkills!.offers[actor];
const view = viewGame(game, actor).leaderSkills!;
const requested = options.requestedSkill ?? 'suk-graduate';
const skill = actor === owner && offer.cards.includes(requested) && !view.unavailableSkills?.[requested]
? requested : offer.cards.find(card => !view.unavailableSkills?.[card]);
assert.ok(skill && view.eligibleLeaders[0]);
game = applyAction(game, actor, { type: 'leaderSkill', event: offer.event, skill, leader: view.eligibleLeaders[0].id });
} else game = advancedNativeStep(game);
}
assert.equal(game.status, 'playing');
return game;
}
export function completedAdvancedNativeSkillsGame(options: AdvancedNativeSkillsOptions): Game {
return completeAdvancedNativeSkillsSetup(initializeAdvancedNativeSkillsSetup(options), options);
}
export function advancedNativeStep(game: Game): Game {
const next = nextSpiceBankerIncomeNativeStep(game);
assert.ok(next);
return applyAction(game, next.actor, next.action);
}
export function advanceAdvancedNativeSkillsToPhase(state: Game, target: number): Game {
let game = state;
for (let i = 0; (game.phase !== target || game.phaseOpening || game.response || game.decision)
&& game.turn === state.turn && game.status === 'playing' && i < 1000; i++) game = advancedNativeStep(game);
assert.equal(game.turn, state.turn);
assert.equal(game.phase, target);
assert.ok(!game.phaseOpening && !game.response && !game.decision);
return game;
}
export function rejectAdvancedNativeAction(game: Game, actor: string, action: Action, pattern?: RegExp): void {
const before = structuredClone(game);
if (pattern) assert.throws(() => applyAction(game, actor, action), pattern);
else assert.throws(() => applyAction(game, actor, action));
assert.deepEqual(game, before);
}
export function assertAdvancedNativeCustody(game: Game): void {
validateLeaderSkills(game.leaderSkills!, game.players);
for (const p of game.players) {
assert.equal(p.reserves + p.tanks + Object.values(p.forces).reduce((sum, n) => sum + n, 0), 20);
if (p.elites) {
assert.equal(p.elites.reserves + p.elites.tanks + Object.values(p.elites.forces).reduce((sum, n) => sum + n, 0), p.faction === 'ixians' ? 7 : 5);
for (const [key, count] of Object.entries(p.elites.forces)) assert.ok(count <= (p.forces[key] ?? 0));
}
}
}
/** Explicit controlled rule-unit position AFTER genuine setup. Return every
* deployed piece to its own reserves, then transfer only the selected forces;
* preserve real wallets, leaders, traits and every physical skill/treachery card.
* This is not claimed to be a played turn history. */
export function createAdvancedNativeSkillBattle(options: AdvancedNativeSkillsOptions): Game {
const game = completedAdvancedNativeSkillsGame(options);
const owner = advancedNativePlayer(game, options.skillOwner ?? options.family);
const enemy = advancedNativePlayer(game, 'emperor');
for (const p of game.players) {
game.deck.push(...p.hand);
p.hand = [];
p.reserves += Object.values(p.forces).reduce((sum, n) => sum + n, 0);
p.forces = {};
if (p.elites) {
p.elites.reserves += Object.values(p.elites.forces).reduce((sum, n) => sum + n, 0);
p.elites.forces = {};
}
}
for (const p of [owner, enemy]) {
assert.ok(p.reserves >= 8);
p.reserves -= 8;
p.forces['wind_pass:14'] = 8;
}
if (owner.faction === 'ixians') {
owner.elites!.reserves -= 2;
owner.elites!.forces['wind_pass:14'] = 2;
}
Object.assign(game, { phase: 6, active: owner.id, order: [owner.id, enemy.id, ...game.players.filter(p => p !== owner && p !== enemy).map(p => p.id)],
storm: 18, ready: [], decision: null, response: null, phaseOpening: null });
assertAdvancedNativeCustody(game);
return game;
}
export function openAdvancedNativeSkillBattle(state: Game, actor: string, target: string): Game {
let game = applyAction(state, actor, { type: 'chooseBattle', territory: 'wind_pass', target });
for (let i = 0; (game.response || game.decision || game.battle?.preparation) && i < 100; i++) {
if (game.decision?.kind === 'leaderSkillVisibility') {
game = applyAction(game, game.decision.player, { type: 'leaderSkillVisibility', event: game.decision.event, hide: true });
} else game = advancedNativeStep(game);
}
assert.equal(game.decision, null);
assert.equal(game.response, null);
return game;
}
/** Resolve only native response/cleanup actions, stopping at optional Suk rescue. */
export function finishAdvancedNativeSkillAftermath(state: Game): Game {
let game = state;
for (let i = 0; (game.response || game.decision || game.battle || game.pendingTreacheryDiscard) && i < 100; i++) {
if (game.decision?.kind === 'sukRescue') break;
if (game.decision?.kind === 'ixSubstitution') {
game = applyAction(game, game.decision.player, { type: 'decision', decline: true });
} else if (game.decision?.kind === 'battleCards') {
game = applyAction(game, game.decision.player, { type: 'decision', discard: [] });
} else game = advancedNativeStep(game);
}
return game;
}
+30 -17
View File
@@ -237,20 +237,30 @@ const TLEILAXU_SKILLS_SCENARIOS: readonly Scenario[] = [2, 3, 4, 5, 6].map(playe
expansions: ['ix'], expansions: ['ix'],
roster: (['tleilaxu', 'emperor', 'guild', 'harkonnen', 'fremen', 'beneGesserit'] as FactionId[]).slice(0, players), roster: (['tleilaxu', 'emperor', 'guild', 'harkonnen', 'fremen', 'beneGesserit'] as FactionId[]).slice(0, players),
})); }));
const IX_SKILLS_SCENARIOS: readonly Scenario[] = [2, 3, 4, 5, 6].map(players => ({ const IX_SKILLS_SCENARIOS: readonly Scenario[] = [
ordinal: 26 + players - 2, ...[2, 3, 4, 5, 6].map((players): Scenario => ({
profile: 'ix-skills', ordinal: 26 + players - 2, profile: 'ix-skills', rules: 'basic', expansions: ['ix'],
rules: 'basic', roster: (['ixians', 'tleilaxu', 'emperor', 'guild', 'harkonnen', 'fremen'] as FactionId[]).slice(0, players),
expansions: ['ix'], })),
roster: (['ixians', 'tleilaxu', 'emperor', 'guild', 'harkonnen', 'fremen'] as FactionId[]).slice(0, players), ...[2, 3, 4, 5, 6].map((players): Scenario => ({
})); ordinal: 220 + players - 2, profile: 'ix-skills', rules: 'advanced', expansions: ['ix'],
const CHOAM_SKILLS_SCENARIOS: readonly Scenario[] = [2, 3, 4, 5, 6].map(players => ({ roster: (['ixians', 'emperor', 'guild', 'harkonnen', 'fremen', 'beneGesserit'] as FactionId[]).slice(0, players),
ordinal: 31 + players - 2, })),
profile: 'choam-skills', ...[2, 3, 4, 5, 6].map((players): Scenario => ({
rules: 'basic', ordinal: 230 + players - 2, profile: 'ix-skills', rules: 'advanced', expansions: ['ix', 'choam'],
expansions: ['choam'], roster: (['ixians', 'choam', 'emperor', 'guild', 'harkonnen', 'fremen'] as FactionId[]).slice(0, players),
roster: (['choam', 'emperor', 'guild', 'harkonnen', 'fremen', 'beneGesserit'] as FactionId[]).slice(0, players), })),
})); ];
const CHOAM_SKILLS_SCENARIOS: readonly Scenario[] = [
...[2, 3, 4, 5, 6].map((players): Scenario => ({
ordinal: 31 + players - 2, profile: 'choam-skills', rules: 'basic', expansions: ['choam'],
roster: (['choam', 'emperor', 'guild', 'harkonnen', 'fremen', 'beneGesserit'] as FactionId[]).slice(0, players),
})),
...[2, 3, 4, 5, 6].map((players): Scenario => ({
ordinal: 225 + players - 2, profile: 'choam-skills', rules: 'advanced', expansions: ['choam'],
roster: (['choam', 'emperor', 'guild', 'harkonnen', 'fremen', 'beneGesserit'] as FactionId[]).slice(0, players),
})),
];
const STRONGHOLD_FACTIONS_ROSTER: readonly FactionId[] = [ const STRONGHOLD_FACTIONS_ROSTER: readonly FactionId[] = [
'ixians', 'choam', 'emperor', 'fremen', 'harkonnen', 'beneGesserit', 'ixians', 'choam', 'emperor', 'fremen', 'harkonnen', 'beneGesserit',
]; ];
@@ -326,7 +336,8 @@ function usage() {
'[--rules both|basic|advanced] [--players all|2|3|4|5|6] [--max-actions POSITIVE] ' + '[--rules both|basic|advanced] [--players all|2|3|4|5|6] [--max-actions POSITIVE] ' +
'[--resume FAILED_GAME.json]\n' + '[--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.' + '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/Richese 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. Richese marker-only or ordinary pools retain original reveal timing; own mixed No-Field dialing, whole-plan inspection and optional overlays remain guarded.' '\nEcaz-occupy is Advanced only: native Ecaz plus classic/Ixian/Tleilaxu/CHOAM/Richese 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. Richese marker-only or ordinary pools retain original reveal timing; own mixed No-Field dialing, whole-plan inspection and optional overlays remain guarded.' +
'\nIx-skills and choam-skills retain their Basic samples and add Advanced native Ixian/CHOAM plus classic rosters, two through six seats; ix-skills also selects the Advanced mixed Ixian+CHOAM roster. All fourteen skills and distinct required family decks remain. Tleilaxu-skills and moritani-skills remain Basic only.'
); );
} }
@@ -367,6 +378,8 @@ function scenarioName(scenario: Scenario) {
if (native.length || scenario.ecazTreachery) if (native.length || scenario.ecazTreachery)
return `${scenario.profile}${scenario.ecazTreachery ? '-treachery' : ''}${native.length ? '-' + native.join('-') : ''}-${scenario.roster.length}-${scenario.rules}`; return `${scenario.profile}${scenario.ecazTreachery ? '-treachery' : ''}${native.length ? '-' + native.join('-') : ''}-${scenario.roster.length}-${scenario.rules}`;
} }
if (scenario.profile === 'ix-skills' && scenario.rules === 'advanced' && scenario.roster.includes('choam'))
return `${scenario.profile}-ixians-choam-${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' || 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) 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)
? `${scenario.profile}-${scenario.roster.length}-${scenario.rules}` ? `${scenario.profile}-${scenario.roster.length}-${scenario.rules}`
@@ -708,8 +721,8 @@ async function main() {
throw new Error('--players requires a roster or optional-module profile.'); throw new Error('--players requires a roster or optional-module profile.');
if ((profile === 'combined-nexus' || profile === 'combined-homeworld-nexus') && players !== 'all' && ![5, 6].includes(players)) if ((profile === 'combined-nexus' || profile === 'combined-homeworld-nexus') && players !== 'all' && ![5, 6].includes(players))
throw new Error(`--profile ${profile} supports only five or six players.`); throw new Error(`--profile ${profile} supports only five or six players.`);
if (skillProfile(profile) && rules === 'advanced') if ((profile === 'moritani-skills' || profile === 'tleilaxu-skills') && rules === 'advanced')
throw new Error('Expansion Leader Skills profiles currently support only Basic samples.'); throw new Error('Moritani and Tleilaxu Leader Skills profiles currently support only Basic samples.');
if (profile === 'moritani-assassinate' && rules === 'basic') if (profile === 'moritani-assassinate' && rules === 'basic')
throw new Error('Moritani assassination samples require Advanced rules.'); throw new Error('Moritani assassination samples require Advanced rules.');
if (profile === 'stronghold-factions' && rules === 'basic') if (profile === 'stronghold-factions' && rules === 'basic')
+2 -2
View File
@@ -22,9 +22,9 @@ async function main() {
}); });
if (values.help) { if (values.help) {
console.log( 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 base-deck roster, or supported Basic Ixian/Tleilaxu/CHOAM/Moritani skill roster with its single required deck, into automatic deferred normal income and native Mentat collection with all fourteen Leader Skills. Original bank payer costs exclude the separate Tleilaxu free-revival award; already-earned spice stays with its original faction under the existing provisional 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 base-deck roster, or Basic Tleilaxu plus classic opponents with the original Ix47 deck and no optional Sandtrout, into the exact normal-auction purchased-card replacement with Nexus alone. 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.', '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 base-deck roster, supported Basic Ixian/Tleilaxu/CHOAM/Moritani skill roster with its single required deck, or Advanced native Ixians and/or CHOAM plus classic roster with distinct required family decks, into automatic deferred normal income and native Mentat collection with all fourteen Leader Skills. Original bank payer costs exclude the separate Tleilaxu free-revival award; already-earned spice stays with its original faction under the existing provisional 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 base-deck roster, or Basic Tleilaxu plus classic opponents with the original Ix47 deck and no optional Sandtrout, into the exact normal-auction purchased-card replacement with Nexus alone. 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/Richese opponents (optional native Moritani). Distinct selected decks include ecaz and each native ix/choam family; ordinary33/47/35 cards retain real original offers/HMS/Auditor/Face Dancers and Richese cache. Explicit --ecaz-treachery independently adds the three physical variant cards, making36/50/38. Native Richese ordinary and marker-only pools compose with selected lead plans; original own mixed No-Field dialing, whole-plan disclosure and optional overlays remain guarded.' + '\nEcaz-occupy (--profile ecaz-occupy) opts only a fresh ready 2–6-seat Advanced lobby with native Ecaz and classic/Ixian/Tleilaxu/CHOAM/Richese opponents (optional native Moritani). Distinct selected decks include ecaz and each native ix/choam family; ordinary33/47/35 cards retain real original offers/HMS/Auditor/Face Dancers and Richese cache. Explicit --ecaz-treachery independently adds the three physical variant cards, making36/50/38. Native Richese ordinary and marker-only pools compose with selected lead plans; original own mixed No-Field dialing, whole-plan disclosure and optional overlays remain guarded.' +
'\nLeader-skills accepts explicit --mentat-question for the private uniform Mentat preview. Existing classic/CHOAM skill starts and supported Basic Ixian/Tleilaxu/CHOAM/Moritani native rosters retain their original setup and all fourteen skills. Public activation and the empty-hand ruling remain pending; no other modules or Advanced native skill rosters are enabled.', '\nLeader-skills retains the existing classic and Basic native entries and now composes Advanced native Ixians and/or CHOAM with classic opponents, their distinct required ix/choam decks and all fourteen skills. Original native setup, Auditor exclusion, typed force support and skill posture remain. Explicit --mentat-question retains its private uniform preview; banker-income retains separate normal deferred income. Public activation, empty-hand/captured-replacement/foreign-ghola rulings and other optional overlays remain pending.',
); );
return; return;
} }