Compose Advanced Tleilaxu skills and original foreign ghola lifecycle

This commit is contained in:
Quintine committed 2026-10-03 14:06:49 +10:00
1 parent d327d1943b
commit d86d2088e6
18 files changed
+678 -72

No files matched your search

+6
View File
@@ -37,6 +37,12 @@ export function RevivalCommerce({
Fill your active leader pool up to five. A ghola keeps its traitor
identity and can be sold back only after it dies.
</p>
{g.leaderSkills && (
<p className="fine">
Reviving a foreign ghola does not draw or assign a Leader Skill.
Your native skilled leader keeps its own assignment.
</p>
)}
{g.players
.filter((p) => p.id !== me.id)
.flatMap((p) => p.leaders)
+25
View File
@@ -16,6 +16,31 @@ open mode gates.
## Current checkpoint and work
**Advanced Tleilaxu Skills and foreign gholas — bounded Development Verified:**
native Tleilaxu, optional Ixians/CHOAM and classic seats now compose all fourteen
skills with original half-price foreign revival and no foreign skill draw.
Native trainer normal bonuses, own revival, dead-ghola negotiated return,
Harkonnen capture/revelation/use-return/execution and Face Dance retain their
original handlers. Actual captured-plan revelation exposed a new integrity
overconstraint; it now permits the original public disclosure transition.
Affected56/56, native CLI3/3, types/lint/build and nine controlled runtime cases
pass. Fourteen genuine games finish6,620 accepted actions/no rejection,
171 JSON continuations,62 battles, two foreign and five own leader revivals.
Original CLI **H485PLF5 v6** retains native setup. Human390px **VNQ5PF54 v33**
begins at an explicitly conserved Revival/Worthless/storm position after
genuine setup, then uses actual foreign revival, funded shipment and battle:
Bashar costs1, no skill offer; two real Wind Pass counters cost4; face-up Zoal's
Warmaster gives the foreign Bashar2 exactly+1 with Trip to Gamont, defeating
Caid3 on the aggressor tie3–3. Actual cleanup/opening reaches Collection with
two board counters, wallet0 and the living controlled foreign disc after refresh.
The detailed current guide is visually readable at390px. See the
[native Tleilaxu contract](TLEILAXU_LEADER_SKILLS.md#advanced-native-composition--4-october-2026).
Basic/public starts, captured replacement, Advanced Atreides Suk/KH and other
optional-module/ruling gates remain. No full assurance or deployed claim.
Final native15/15, all nine runtime cases and types/lint/build pass after
per-action public-profile validation is computed only once when foreign discs
exist. The final isolated build retains human v33/Collection and CLI v6/setup.
**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.
+45
View File
@@ -1,5 +1,50 @@
# Dune implementation status
## 4 October 2026 — native Advanced Tleilaxu skills and foreign-ghola lifecycle
Two bounded owners composed original foreign-disc ownership/skill validation
and genuine native fixtures/rule cases. One integration owner extended the
shared Advanced profile, stable CLI samples, human guidance and actual runtime.
Native Tleilaxu plus optional Ixians/CHOAM/classic seats keep all fourteen cards,
original family decks, starting offers and Face Dancers. Foreign revival never
draws a new skill; eligible own revival retains its optional offer.
Actual original capture remains legal. The first new validator incorrectly
required a concealed snapshot after the native second plan publicly revealed
the captive. The failing real-engine case now passes through the existing
revelation, execution and surviving-use return handlers without duplicating
death, capture or payment. Original disc/ghola/captor controllers stay distinct.
Fixture corrections use genuine trained-disc posture, the real own-revival
action and the actual native Auditor inventory. Deleted obsolete Advanced
denial and bare-not-throw policy assertions rather than re-pinning them.
Affected56/56, CLI3/3 and types/lint/build pass. Nine actual controlled cases
cover manual/four-policy funded foreign revival, capture execution/use-return
and explicit native Mentat/Banker opt-ins. Fourteen genuine CLI games complete
6,620 accepted actions/no rejection,171 JSON continuations,62 battles, two
foreign and five own leader revivals. Stable new ordinals235–248 preserve old
Basic samples. Ten canonical local anchors resolve.
Fresh original CLI **H485PLF5 v6** preserves native setup. Human390px
**VNQ5PF54 v33** uses original admitted seats, followed by an explicitly
conserved Revival disc/Worthless/storm position. Actual Bashar foreign revival
pays1 from wallet5, with no new offer. Actual two-force Wind Pass shipment pays4.
Normal face-up Zoal/Warmaster contributes exactly+1 to Bashar2 using actual
Trip to Gamont against Caid3/dial0; the native aggressor wins3–3. Original
Traitor declines, card retention and phase opening reach Collection after
refresh with wallet0, two board counters and the living foreign disc still
controlled by Tleilaxu. The detailed guide is visually readable at390px.
No natural-history claim for controlled fixtures, game reset, public activation,
strategic tuning, comprehensive assurance campaign or deployment.
Final native15/15, all nine controlled runtime cases and types/lint/build pass
after the foreign public-profile quote is cached only within its current
validation call. No allocation or retained-state cache is introduced. Eleven
final canonical scope anchors resolve. Final isolated-build GETs retain human
v33/Collection/wallet0/two counters/living Bashar and original CLI v6/setup.
Own throwaway drivers and browser tabs are removed/released; QA games remain.
## 4 October 2026 — Advanced native Ixian and CHOAM Leader Skills
Two independent owners extended the bounded native profile/helper contracts and
+16 -4
View File
@@ -28,6 +28,18 @@ activation still awaits the uniform-response UX decision; empty hands and
Advanced native/combined-module skills remain guarded. See
[native preview scope](MENTAT_QUESTION.md#native-skill-integration--2-october-2026).
## Advanced Tleilaxu foreign-ghola composition — 4 October 2026
The [native Tleilaxu contract](TLEILAXU_LEADER_SKILLS.md#advanced-native-composition--4-october-2026)
extends the Advanced native subset to Tleilaxu, optionally with Ixians and/or
CHOAM and classic opponents, preserving distinct required decks and all fourteen
skills. Actual foreign revival gives control of the original physical disc but
never a new skill. Own revival, native trainer normal roles, foreign death and
negotiated return, Harkonnen capture/revelation/use-return/execution and native
Face Dance compose through their original handlers. Explicit Mentat/Banker
opt-ins retain their boundaries; captured replacement, Advanced Atreides Suk/KH,
unrelated overlays and public starts remain guarded.
## Advanced native Ixian and CHOAM composition — 4 October 2026
The fresh local `leader-skills` profile now admits two through six unique
@@ -150,10 +162,10 @@ Captured-card replacement entitlement remains unresolved. The prototype rejects
own-leader revival while its original skill is captured, before beginning a
paid revival or consuming the Ghola/special-Karama action. This is an explicit
unsupported boundary, not a ruling that the physical game forbids that revival.
Foreign gholas, other expansion rosters and combined optional modules remain gated.
The Basic Tleilaxu follow-up now tests response-driven automatic own revival
continuation; this does not certify Advanced foreign-ghola custody. The other source-contract timing questions also remain
open. Planetologist now has bounded movement and battle controls as described below.
Other expansion rosters and combined optional modules remain gated. Native
Advanced foreign gholas now follow the bounded Tleilaxu contract above; the
recorded source-contract timing questions remain open. Planetologist has bounded
movement and battle controls as described below.
Remaining normal-band payments, ordinary Mentat activation, modified Smuggler collection,
and the other guarded skill combinations still need connected effects. Diplomat
retreat is a bounded prototype, not a complete simultaneous-effect ruling.
+3
View File
@@ -13,6 +13,9 @@ 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.
The bounded [Advanced Tleilaxu composition](TLEILAXU_LEADER_SKILLS.md#advanced-native-composition--4-october-2026)
now uses the same explicit preview; original foreign revival does not assign a
new Mentat or any other skill.
## Native skill integration — 2 October 2026
+3
View File
@@ -12,6 +12,9 @@ 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.
The bounded [Advanced Tleilaxu composition](TLEILAXU_LEADER_SKILLS.md#advanced-native-composition--4-october-2026)
also shares this separate opt-in. Its foreign revival never creates a Banker
assignment; original paid bank legs and own native trainer eligibility remain.
## Normal income local contract — 2 October 2026
+50 -6
View File
@@ -1,10 +1,54 @@
# Basic Tleilaxu with Leader Skills
# Tleilaxu with Leader Skills
Connected development prototype, 21 September 2026. Basic Tleilaxu with base
opponents now uses all fourteen physical skills through genuine setup. Public
starts, complete-mode acceptance and publication remain gated. Existing missing
skill effects are still explicit; this is not complete expansion certification.
## Advanced native composition — 4 October 2026
Fresh local Advanced `leader-skills` tables now admit native Tleilaxu, optionally
Ixians and/or CHOAM, with classic opponents and distinct required `ix`/`choam`
decks. Two through six unique seats retain all fourteen physical skills,
starting-card choices, real Face Dancers and native setup. Existing Basic
entries below remain unchanged; public starts and unrelated overlays stay gated.
Actual half-price foreign revival fills the controlled living pool only up to
five. The original faction retains the sole physical disc; `gholaBy` records
Tleilaxu control. It creates no skill offer or assignment, even when Tleilaxu has
lost its native trainer. This composes the
[explicit foreign-ghola FAQ](LEADER_SKILLS_RULES.md#capture-and-official-clarifications)
with original native revival, not a new entitlement. Own-leader revival still
offers its optional private draw when eligible. Auditor and the user-specific
Ecaz-only Duke revival/Ghola exclusions remain.
A face-up native trainer can supply its normal printed role bonus to a selected
controlled foreign disc; that does not grant the trained disc's lower bonus.
Native death and negotiated original-owner buyback preserve the native trainer
and clear ghola control only on the actual return. Harkonnen can capture a foreign
ghola through its original handler: capture overrides Tleilaxu control, actual
plan revelation removes its concealed snapshot, and surviving use returns it to
Tleilaxu. Execution retains the original dead-ghola and private snapshot for the
existing return path. No skill attaches to the foreign disc. Native Face Dance
and Rihani still use their original handlers and physical skill/card returns.
Captured replacement and Advanced Atreides Suk/KH remain separate pending cases.
New Advanced `tleilaxu-skills` samples use stable ordinals235–248: five ordinary,
five paired Ixian and four three-native rosters. Fourteen genuine unstaged games
finish6,620 accepted actions/no rejection,171 JSON continuations,62 battles,
two foreign and five own leader revivals. Five separate actual manual/four-policy
revival cases and native captured-plan/execution/use-return smoke cover the
changed paths; controlled positions are explicit, not natural phase histories.
Final affected56/56, CLI3/3 and types/lint/build pass. Nine controlled runtime
cases include explicit native Mentat and Banker opt-ins. Original CLI
**H485PLF5 v6** preserves setup; human390px **VNQ5PF54 v33** performs real
foreign revival/ship/plans after the declared controlled starting position.
Warmaster's normal+1 makes Bashar2 tie Caid3 and win as aggressor, without a
trained lower bonus or a new skill. Actual Collection retains wallet0 and two
board counters after refresh; the detailed guide is visually readable at390px.
## Configuration and physical setup
The existing `leader-skills` local initializer accepts Basic rules, exactly the
@@ -13,8 +57,8 @@ and no other optional modules. It keeps the real Ix Treachery/Spice decks and
the Cheap Hero Traitor identity. Starting Treachery precedes simultaneous
private two-card skill offers; public assignments and ordinary Traitor choices
precede the native three Face Dancers. No hidden card controls profile admission.
Ixians now have a separate [Basic integration](IX_LEADER_SKILLS.md); Advanced
Tleilaxu and combined modules require further work.
Ixians have a separate [integration](IX_LEADER_SKILLS.md); the Advanced native
composition above supersedes the earlier admission boundary, not other modules.
## Connected interactions
@@ -24,8 +68,8 @@ offer; income responses finish before the player's choice. The undrawn offer
reveals neither card, can be declined, and binds a drawn choice to that revived
leader. Canceled or unaffordable revivals create no offer. Existing observation
of Ghola and other native revivals remains idempotent. Captured replacement
entitlement stays guarded. Foreign gholas remain Advanced-only and excluded
from this profile; the FAQ says acquiring one does not draw a new skill.
entitlement stays guarded. Foreign gholas remain Advanced-only and are excluded
from this Basic profile; acquiring one does not draw a new skill.
Rihani uses the [recorded FAQ](LEADER_SKILLS_RULES.md):
Tleilaxu's drawn cards become unrevealed Face Dancers, and only an unrevealed old
@@ -71,7 +115,7 @@ The sample profile runs two through six players with Emperor, Guild, Harkonnen,
Fremen and Bene Gesserit added in order; focused tests also include Atreides.
It counts all Treachery, skill, Traitor/Face Dancer and force custody, checks
rejected-action immutability and JSON/private-view restoration. Saved sample
continuation preserves the full module; Advanced selection fails before running.
continuation preserves the full module; Advanced samples now follow the bounded composition above.
These are legal-play checks, not difficulty calibration. The tests use genuine
setup followed by explicitly conserved focused positions, not invented full
phase histories. Authenticated in-memory SQLite checks cover concurrent setup
+39 -5
View File
@@ -1,4 +1,4 @@
import { nativeExpansionLeaderSkillsProfile, ordinaryLeaderSkillModeSupported } from './leader-skill-profile';
import { advancedNativeLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, ordinaryLeaderSkillModeSupported } from './leader-skill-profile';
import { bribeTimingBlock, maximumBribe, type BribeOptions } from './bribe-options';
import { quoteSpicePlacement, stormExposesTerritory, stormSectorAfter, wormConsumesForces } from './disaster-rules';
import { isStormCardDistance, type StormCardComponent } from './storm-cards';
@@ -7694,10 +7694,44 @@ function leaderSkillsIntegrity(g: Game) {
g.players.find((p) => p.id === assignment.owner)?.leaders.some((leader) => leader.id === assignment.leader && !leader.dead)),
'A saved Leader Skill cannot remain assigned to a dead or missing leader.');
skillRule(() => validateLeaderSkills(g.leaderSkills!, g.players));
requireRule(
!g.players.some((p) => p.leaders.some((l) => l.gholaBy)),
'Leader Skills with foreign gholas await their custody integration.',
);
let foreignProfileAllowed: boolean | undefined;
for (const owner of g.players)
for (const leader of owner.leaders) {
if (leader.gholaBy === undefined) continue;
const controller = g.players.find((p) => p.id === leader.gholaBy);
requireRule(
(foreignProfileAllowed ??= advancedNativeLeaderSkillsProfile(g)) &&
controller?.faction === 'tleilaxu' &&
controller.id !== owner.id &&
leader.faction === owner.faction &&
(leader.controller === undefined || leader.controller === owner.id) &&
!isAuditorLeader(leader) && leader.id !== DUKE_VIDAL_ID,
'Leader Skills foreign gholas require original native Advanced Tleilaxu custody.',
);
const concealed = leader.concealed;
if (leader.capturedBy !== undefined || concealed) {
const captor = g.players.find((p) => p.id === (leader.capturedBy ?? concealed?.captor));
// Native capture overrides, but does not erase, the Tleilaxu loan.
// Revealing its actual plan removes secrecy; execution keeps the snapshot.
requireRule(
captor?.faction === 'harkonnen' &&
(leader.capturedBy === captor.id
? !leader.dead
: leader.capturedBy === undefined && leader.dead) &&
((concealed?.captor === captor.id &&
concealed.controller === controller.id && concealed.dead === false) ||
(leader.capturedBy === captor.id && g.battle?.revealed === true &&
g.battle.plans[captor.id]?.leader === leader.id)),
'A foreign ghola capture must retain its native Harkonnen captor and Tleilaxu return custody.',
);
}
let copies = 0;
for (const seat of g.players)
for (const disc of seat.leaders)
if (disc.id === leader.id) copies++;
requireRule(copies === 1,
'A foreign ghola must remain one physical disc in its original leader roster.');
}
if (g.battle?.leaderSkillHidden)
requireRule(
Object.entries(g.battle.leaderSkillHidden).every(
+2 -2
View File
@@ -89,8 +89,8 @@ export function advancedNativeLeaderSkillsProfile(game: LeaderSkillProfile): boo
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;
if (player.faction === 'ixians' || player.faction === 'tleilaxu' || player.faction === 'choam') {
if (!game.expansions.includes(player.faction === 'choam' ? 'choam' : 'ix')) return false;
native = true;
} else {
let classic = false;
+7 -2
View File
@@ -91,10 +91,15 @@ export function validateLeaderSkills(
const originalOwners = new Set<string>();
for (const assignment of state.assignments) {
const owner = roster.find((p) => p.id === assignment.owner);
const leader = owner?.leaders.find((disc) => disc.id === assignment.leader);
ensure(
owner?.leaders.some((leader) => leader.id === assignment.leader),
leader,
'A skill must remain attached to its original leader.',
);
ensure(
leader.gholaBy === undefined,
'A foreign ghola cannot carry a Leader Skill.',
);
ensure(
!assignedLeaders.has(assignment.leader),
'A leader cannot hold two skills.',
@@ -126,7 +131,7 @@ export function validateLeaderSkills(
offer.leader === null ||
roster
.find((p) => p.id === owner)
?.leaders.some((l) => l.id === offer.leader),
?.leaders.some((l) => l.id === offer.leader && l.gholaBy === undefined),
'Invalid revival skill leader.',
);
cards.push(...offer.cards);
+13 -11
View File
@@ -4458,20 +4458,22 @@ export const RULE_TOPICS: RuleTopic[] = [
category: 'Advanced & expansions',
coverage: 'Partial',
developmentStage: 'Prototyped',
summary: 'The Basic development combination preserves the full skill deck and private Face Dancers.',
summary: 'Native Basic and bounded Advanced Tleilaxu retain fourteen skills, Face Dancers and original foreign-ghola ownership without a new skill.',
steps: [
'Basic Tleilaxu with base opponents uses all fourteen skills. Starting cards precede private skill choices and the native three Face Dancers. Other optional modules, Advanced foreign gholas and public starts remain unavailable.',
'Basic Tleilaxu with base opponents retains its existing full skill deck. Fresh Advanced native Tleilaxu, optionally Ixians and/or CHOAM, plus classic tables use distinct required Ix/CHOAM decks and all fourteen skills. Starting choices, actual traitors and three native Face Dancers remain. Unrelated overlays and public starts stay gated.',
'Rihani draws become unrevealed Face Dancers. Keep one new card by revealing and returning one unrevealed old card; the other new card returns privately. Only the owner sees the inspection history and new identities.',
'A successful own-leader revival preserves the optional skill draw after revival responses. Winner skill and card cleanup precede Face Dance; a killed skilled leader returns its card once. Zoal copies the opposing disc value for unmodified Smuggler collection.',
'Actual foreign revival pays its native half price and fills the controlled living pool only up to five, but never draws or assigns a skill. The original faction keeps the sole physical disc; Tleilaxu controls it. A native trainer can supply its normal role bonus to that different selected disc, not its trained lower bonus. Own revival still offers its eligible optional draw. Auditor and the Ecaz-only Duke/Ghola exclusions remain.',
'Foreign death and negotiated original-owner buyback use original handlers. Harkonnen capture temporarily overrides Tleilaxu control without moving the disc into another physical roster. Its actual plan reveal removes the concealed snapshot; surviving use returns the ghola to Tleilaxu. Execution preserves the original dead-ghola return path. No skill attaches to a foreign disc.',
'Other unfinished skill effects retain their stated limits. This development combination does not certify the full expansion or every optional-module interaction.',
],
related: ['leader-skills', 'tleilaxu-face-dancers', 'implementation-checklist'],
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'Full setup, response-driven own revival, Rihani Face Dancer exchanges, later death custody and copied Zoal collection are connected; remaining skill effects stay explicit.', evidence: ['game/leader-skill-profile.ts', 'game/rihani-decipherer.ts', 'game/engine.ts'] },
{ area: 'Implementation', status: 'Partial', detail: 'Full native setup, own revival, Advanced foreign control/no-new-skill, original Harkonnen capture/revelation/use-return/execution, Rihani exchanges, death custody and copied Zoal collection compose. Other modules and pending skill rulings stay explicit.', evidence: ['game/leader-skill-profile.ts', 'game/leader-skills.ts', 'game/rihani-decipherer.ts', 'game/engine.ts'] },
{ area: 'Player controls', status: 'Implemented', detail: 'Private offers, Face Dancer keep/reveal inspectors and saved own-revival choices reuse the table controls.', evidence: ['components/rihani-decipherer.tsx', 'components/leader-skills.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'Existing profiles follow their private legal offers. Full strategy and strength tuning wait for feature completion.', evidence: ['game/bots.ts', 'tools/faction-games.ts'] },
{ area: 'Documentation', status: 'Implemented', detail: 'Entry, source contracts and remaining boundaries are recorded.', evidence: ['docs/TLEILAXU_LEADER_SKILLS.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Genuine setup, physical custody, revival response chains, private exchange, Face Dance aftermath, Zoal and authenticated recovery have focused checks; full acceptance remains open.', evidence: ['tests/tleilaxu-skills-integration.test.ts', 'tests/tleilaxu-skills-revival.test.ts', 'tests/tleilaxu-skills-recovery.test.ts', 'tests/rihani-face-dancers.test.ts'] },
{ area: 'Verification', status: 'Partial', detail: 'Original native role checks remain. Advanced own/foreign revival, normal versus lower bonus, death/buyback, Face Dance and captured use/execution cases pass with all fourteen cards and current JSON. Fourteen genuine games finish6,620 accepted actions/no rejection/171 JSON continuations, including62 battles and two foreign revivals. Actual manual/four-policy revival and captive revelation/return/execution smoke pass; complete module acceptance remains open.', evidence: ['tests/advanced-tleilaxu-skills.test.ts','tests/tleilaxu-skills-integration.test.ts','tests/tleilaxu-skills-revival.test.ts','tests/rihani-face-dancers.test.ts'] },
],
},
{
@@ -4505,7 +4507,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.',
searchText: 'Spice Banker normal income bank payment four spice phase deferred front shield original owner death capture Mentat',
steps: [
'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.',
'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, Tleilaxu and/or CHOAM plus classic seats with distinct required family decks. All fourteen skills and native setup remain. Native foreign revival never assigns a skill. 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.',
'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.',
@@ -4520,7 +4522,7 @@ export const RULE_TOPICS: RuleTopic[] = [
],
related: ['leader-skills', 'battle', 'bidding', 'revival', 'implementation-checklist'],
checklist: [
{ 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: '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/Tleilaxu/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: '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'] },
@@ -4536,7 +4538,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.',
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.',
'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.',
'Fresh Advanced development profiles compose native Ixians, Tleilaxu and/or CHOAM with classic opponents, two through six unique seats and 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. Foreign-ghola revival gives control without a new skill. Basic entries are unchanged; 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.',
'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.',
@@ -4549,12 +4551,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.',
'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.',
'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.',
'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/Tleilaxu/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.',
'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.',
'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/Tleilaxu/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 supported Basic or bounded Advanced native skill profiles 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.',
'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.',
'Other special Sandmaster relocation remains missing. Normal Banker income has a separate fresh classic, supported Basic native or bounded Advanced Ixian/Tleilaxu/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. Native Advanced foreign gholas and Face Dancers follow the bounded Tleilaxu contract; other 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(' ')}`),
],
related: ['spice-banker-income', 'choam-leader-skills', 'ixian-leader-skills', 'tleilaxu-leader-skills', 'moritani-leader-skills', 'setup','choam-modules','implementation-checklist'],
+4 -4
View File
@@ -218,7 +218,7 @@ void test('native CHOAM Bureaucrat redirects only another payer while its native
custody(game);
});
void test('Advanced native admission preserves Atreides Suk and foreign-ghola unresolved boundaries without mutation', () => {
void test('Advanced native admission preserves Atreides Suk and rejects a forged Ix-controlled foreign ghola 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);
@@ -230,11 +230,11 @@ void test('Advanced native admission preserves Atreides Suk and foreign-ghola un
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.
// An Ixian cannot control this original foreign disc. Legitimate native
// Tleilaxu revival is covered separately, without fabricating assignments.
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/);
reject(game, actor, { type: 'ready' });
});
void test('captured native trainer blocks replacement revival, retaining the physical skill and Tanks leader', () => {
+382
View File
@@ -0,0 +1,382 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
applyAction, createGame, joinGame, newPlayer, normalizeAutomaticGame, viewGame,
type Action, type Game,
} from '../game/engine';
import { leaders, treacheryDeck, type Card } from '../game/cards';
import { traitorDeck } from '../game/traitors';
import { DUKE_VIDAL_ID } from '../game/duke-vidal';
import { newRevivalRules } from '../game/revival';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import {
advancedNativePlayer as player, advancedNativeStep, assertAdvancedNativeCustody,
completedAdvancedNativeSkillsGame, createAdvancedNativeSkillBattle,
openAdvancedNativeSkillBattle, rejectAdvancedNativeAction as reject,
stageAdvancedNativeSkillBattle, stageAdvancedTleilaxuForeignRevival,
} from './fixture-advanced-native-skills';
import { completedTleilaxuSkillsGame } from './tleilaxu-skills-fixture';
const reload = (game: Game): Game => JSON.parse(JSON.stringify(game)) as Game;
const act = (game: Game, actor: string, action: Action): Game => applyAction(reload(game), actor, action);
const wallets = (game: Game) => Object.fromEntries(game.players.map(p => [p.id, p.spice]));
const trainer = (game: Game, actor: string) => game.leaderSkills!.assignments.find(a => a.owner === actor)!;
const setup = () => completedAdvancedNativeSkillsGame({ family: 'tleilaxu', requestedSkill: 'warmaster' });
function custody(game: Game): void {
assertAdvancedNativeCustody(game);
const cards = [...game.deck, ...game.discard, ...game.players.flatMap(p => p.hand),
...(game.auction?.cards.slice(game.auction.index) ?? [])].map(c => c.id);
assert.deepEqual(cards.sort(), treacheryDeck(game.expansions).map(c => c.id).sort());
assert.deepEqual(game.players.flatMap(p => p.leaders.map(l => l.id)).sort(),
game.players.flatMap(p => [...leaders(p.faction).map(l => l.id),
...(game.advanced && p.faction === 'choam' ? ['choam-auditor'] : [])]).sort());
const traitors = [...game.traitorReserve!, ...game.players.flatMap(p => p.traitors),
...game.players.flatMap(p => (p.faceDancers ?? []).map(c => c.leader))];
assert.deepEqual(traitors.sort(), traitorDeck(game.players, true).sort());
assert.equal(new Set(traitors).size, traitors.length);
}
function take(game: Game, actor: string, matches: (card: Card) => boolean): Card {
const index = game.deck.findIndex(matches);
assert.ok(index >= 0, 'Transfer a physical card from the original native deck.');
const card = game.deck.splice(index, 1)[0];
game.players.find(p => p.id === actor)!.hand.push(card);
return card;
}
function settleRevival(state: Game): Game {
let game = state;
for (let i = 0; (game.response || game.pendingRevival) && i < 100; i++)
game = advancedNativeStep(reload(game));
assert.equal(game.pendingRevival, null);
assert.equal(game.response, null);
return game;
}
function aftermath(state: Game, stopAtFaceDance = false, stopAtCapture = false): Game {
let game = state;
for (let i = 0; (game.response || game.decision || game.battle || game.pendingTreacheryDiscard) && i < 100; i++) {
if (stopAtCapture && game.decision?.kind === 'captureOffer') return game;
if (game.decision?.kind === 'faceDance') {
if (stopAtFaceDance) return game;
game = act(game, game.decision.player, { type: 'decision', reveal: false });
} else if (game.decision?.kind === 'battleCards') {
game = act(game, game.decision.player, { type: 'decision', discard: [] });
} else game = advancedNativeStep(reload(game));
}
assert.equal(game.battle, null);
assert.equal(game.decision, null);
return game;
}
function resolve(game: Game, actor: string, target: string): Game {
game = act(game, actor, { type: 'traitorCall', call: false });
return act(game, target, { type: 'traitorCall', call: false });
}
/** A native weapon actually kills the original assigned trainer. No staged
* skill return or invented earned currency is used to remove its assignment. */
function killedNativeTrainer(): { game: Game; actor: string; leader: string } {
let game = createAdvancedNativeSkillBattle({ family: 'tleilaxu', requestedSkill: 'warmaster' });
const actor = player(game, 'tleilaxu').id;
const enemy = player(game, 'emperor').id;
const leader = trainer(game, actor).leader;
const poison = take(game, enemy, c => c.kind === 'poison');
const defender = player(game, 'emperor').leaders.find(l => l.id !== trainer(game, enemy).leader)!;
game = openAdvancedNativeSkillBattle(reload(game), actor, enemy);
game = act(game, actor, { type: 'battlePlan', dial: 0, support: 0, leader });
game = act(game, enemy, { type: 'battlePlan', dial: 0, support: 0, leader: defender.id, weapon: poison.id });
game = aftermath(resolve(game, actor, enemy));
assert.equal(player(game, 'tleilaxu').leaders.find(l => l.id === leader)!.dead, true);
assert.equal(game.leaderSkills!.assignments.some(a => a.owner === actor), false);
assert.equal(game.leaderSkills!.deck.filter(s => s === 'warmaster').length, 1);
custody(game);
return { game, actor, leader };
}
void test('native foreign revival keeps original custody, half-price payment and no skill draw even without a trainer', () => {
for (const hasTrainer of [true, false]) {
const original = hasTrainer ? setup() : killedNativeTrainer().game;
const staged = stageAdvancedTleilaxuForeignRevival(original);
assert.deepEqual(wallets(staged.game), wallets(original));
const before = reload(staged.game);
const skillState = structuredClone(before.leaderSkills);
const disc = player(before, 'emperor').leaders.find(l => l.id === staged.leader)!;
const price = Math.ceil(disc.strength / 2);
const game = settleRevival(act(before, staged.actor, { type: 'reviveForeignGhola', leader: disc.id }));
const returned = player(game, 'emperor').leaders.find(l => l.id === disc.id)!;
assert.equal(returned.dead, false);
assert.equal(returned.gholaBy, staged.actor);
assert.equal(player(game, 'tleilaxu').leaders.some(l => l.id === disc.id), false);
assert.equal(game.players.flatMap(p => p.leaders).filter(l => l.id === disc.id).length, 1);
assert.deepEqual(wallets(game), { ...wallets(before), [staged.actor]: player(before, 'tleilaxu').spice - price });
assert.equal(player(game, 'tleilaxu').leaderRevived, true);
assert.deepEqual(game.leaderSkills, skillState);
assert.equal(viewGame(game, staged.actor).leaderSkills!.offer, null);
assert.equal(game.decision, null);
assert.deepEqual(normalizeAutomaticGame(reload(game)), game);
reject(game, staged.actor, { type: 'reviveForeignGhola', leader: disc.id });
assert.deepEqual(before, staged.game, 'Revival cannot mutate its caller-owned input.');
custody(game);
}
});
void test('a separate native Warmaster gives only its normal +1 to the selected original foreign ghola', () => {
const staged = stageAdvancedTleilaxuForeignRevival(setup());
const revived = settleRevival(act(staged.game, staged.actor, { type: 'reviveForeignGhola', leader: staged.leader }));
const assignment = structuredClone(trainer(revived, staged.actor));
const foreign = player(revived, 'emperor').leaders.find(l => l.id === staged.leader)!;
assert.equal(foreign.strength, 2);
for (const [visible, dial, expectedWinner] of [
[true, 0, staged.actor], [false, 0, staged.originalOwner], [true, 1, staged.originalOwner],
] as const) {
let game = stageAdvancedNativeSkillBattle(revived, staged.actor, staged.originalOwner);
const worthless = take(game, staged.actor, c => c.kind === 'worthless');
const defender = player(game, 'emperor').leaders.find(l => l.strength === 3 && !l.dead)!;
const funds = wallets(game);
game = openAdvancedNativeSkillBattle(reload(game), staged.actor, staged.originalOwner, !visible);
game = act(game, staged.actor, { type: 'battlePlan', dial: 0, support: 0, leader: foreign.id, weapon: worthless.id });
game = act(game, staged.originalOwner, { type: 'battlePlan', dial, support: dial, leader: defender.id });
game = aftermath(resolve(game, staged.actor, staged.originalOwner));
// 2+1 beats/ties 3; hiding the normal skill loses. 2+1 loses to 3+1,
// whereas incorrectly giving the trainer's selected-disc +3 would win.
assert.equal(game.lastBattleContext!.winner, expectedWinner);
assert.deepEqual(trainer(game, staged.actor), assignment);
assert.equal(player(game, 'emperor').leaders.find(l => l.id === foreign.id)!.gholaBy, staged.actor);
assert.deepEqual(wallets(game), { ...funds, [staged.originalOwner]: funds[staged.originalOwner] - dial });
custody(game);
}
});
void test('foreign ghola death preserves the native trainer; negotiated original-owner buyback clears control exactly once', () => {
const staged = stageAdvancedTleilaxuForeignRevival(setup());
const revived = settleRevival(act(staged.game, staged.actor, { type: 'reviveForeignGhola', leader: staged.leader }));
const skills = structuredClone(revived.leaderSkills);
let game = stageAdvancedNativeSkillBattle(revived, staged.actor, staged.originalOwner);
const poison = take(game, staged.originalOwner, c => c.kind === 'poison');
const defender = player(game, 'emperor').leaders.find(l => !l.dead && l.id !== trainer(game, staged.originalOwner).leader)!;
game = openAdvancedNativeSkillBattle(reload(game), staged.actor, staged.originalOwner, false);
game = act(game, staged.actor, { type: 'battlePlan', dial: 0, support: 0, leader: staged.leader });
game = act(game, staged.originalOwner, { type: 'battlePlan', dial: 0, support: 0, leader: defender.id, weapon: poison.id });
game = aftermath(resolve(game, staged.actor, staged.originalOwner));
const dead = player(game, 'emperor').leaders.find(l => l.id === staged.leader)!;
assert.equal(dead.dead, true);
assert.equal(dead.gholaBy, staged.actor);
assert.deepEqual(game.leaderSkills, skills);
custody(game);
// Controlled next Revival boundary, retaining actual battle deaths, cards,
// bounty and wallets. This is not claimed to be a played phase history.
Object.assign(game, { phase: 4, active: null, ready: [], decision: null, response: null,
phaseOpening: null, revivalRules: newRevivalRules() });
for (const p of game.players) p.leaderRevived = false;
const before = reload(game), funds = wallets(game);
reject(game, staged.originalOwner, { type: 'reviveLeader', leader: staged.leader });
game = act(game, staged.originalOwner, { type: 'requestLeaderRevival', leader: staged.leader });
game = act(game, staged.actor, { type: 'quoteLeaderRevival', target: staged.originalOwner, amount: 1 });
game = settleRevival(act(game, staged.originalOwner, { type: 'acceptLeaderRevival' }));
const ownAgain = player(game, 'emperor').leaders.find(l => l.id === staged.leader)!;
assert.equal(ownAgain.dead, false);
assert.equal(ownAgain.gholaBy, undefined);
assert.deepEqual(wallets(game), { ...funds, [staged.originalOwner]: funds[staged.originalOwner] - 1,
[staged.actor]: funds[staged.actor] + 1 });
assert.deepEqual(game.leaderSkills, before.leaderSkills, 'Its surviving original trainer prevents a replacement draw.');
reject(game, staged.originalOwner, { type: 'acceptLeaderRevival' });
custody(game);
});
void test('own native trainer revival still offers an optional physical card after a no-skill foreign revival', () => {
const killed = killedNativeTrainer();
const staged = stageAdvancedTleilaxuForeignRevival(killed.game);
const foreignRevived = settleRevival(act(staged.game, staged.actor, { type: 'reviveForeignGhola', leader: staged.leader }));
const own = player(foreignRevived, 'tleilaxu').leaders.find(l => l.id === killed.leader)!;
const previousDeck = [...foreignRevived.leaderSkills!.deck];
const funds = wallets(foreignRevived);
const offered = settleRevival(act(foreignRevived, staged.actor, { type: 'reviveLeader', leader: own.id }));
assert.equal(offered.decision?.kind, 'leaderSkillRevival');
const offer = offered.leaderSkills!.offers[staged.actor];
assert.equal(offer.leader, own.id);
assert.deepEqual(offer.cards, []);
assert.deepEqual(offered.leaderSkills!.deck, previousDeck);
assert.deepEqual(wallets(offered), { ...funds, [staged.actor]: funds[staged.actor] - Math.ceil(own.strength / 2) });
assert.equal(player(offered, 'emperor').leaders.find(l => l.id === staged.leader)!.gholaBy, staged.actor);
const declined = act(offered, staged.actor, { type: 'leaderSkill', event: offer.event, mode: 'decline' });
assert.deepEqual(declined.leaderSkills!.deck, previousDeck);
assert.equal(declined.leaderSkills!.offers[staged.actor], undefined);
const drawn = act(offered, staged.actor, { type: 'leaderSkill', event: offer.event, mode: 'draw' });
const choice = drawn.leaderSkills!.offers[staged.actor];
assert.equal(choice.cards.length, 2);
assert.equal(drawn.leaderSkills!.deck.length, previousDeck.length - 2);
custody(drawn);
reject(drawn, staged.actor, { type: 'leaderSkill', event: choice.event, skill: choice.cards[0], leader: staged.leader });
const trained = act(drawn, staged.actor, { type: 'leaderSkill', event: choice.event, skill: choice.cards[0], leader: own.id });
assert.equal(trainer(trained, staged.actor).leader, own.id);
assert.equal(trainer(trained, staged.actor).skill, choice.cards[0]);
custody(declined); custody(trained);
for (const difficulty of DIFFICULTIES) {
let game = reload(offered);
for (let i = 0; game.leaderSkills!.offers[staged.actor] && i < 3; i++) {
const view = viewGame(game, staged.actor);
view.players.find(p => p.id === staged.actor)!.bot = difficulty;
const action = botActions(view)[0];
assert.ok(action, difficulty);
game = act(game, staged.actor, action);
}
assert.equal(game.leaderSkills!.offers[staged.actor], undefined, difficulty);
const assigned = game.leaderSkills!.assignments.find(a => a.owner === staged.actor);
if (assigned) assert.equal(assigned.leader, own.id, difficulty);
assert.equal(game.leaderSkills!.assignments.some(a => a.leader === staged.leader), false, difficulty);
assert.deepEqual(wallets(game), wallets(offered));
custody(game);
}
});
void test('native Face Dance after an actual Advanced battle kills and returns the original trained card', () => {
let game = createAdvancedNativeSkillBattle({ family: 'tleilaxu', skillOwner: 'atreides', requestedSkill: 'warmaster' });
const actor = player(game, 'atreides').id, enemy = player(game, 'emperor').id;
const controller = player(game, 'tleilaxu').id;
const trained = trainer(game, actor);
const leader = trained.leader;
const dancers = player(game, 'tleilaxu').faceDancers!;
if (!dancers.some(c => c.leader === leader)) {
const card = dancers[0];
const reserve = game.traitorReserve!.indexOf(leader);
if (reserve >= 0) game.traitorReserve![reserve] = card.leader;
else {
const holder = game.players.find(p => p.traitors.includes(leader))!;
assert.ok(holder);
holder.traitors[holder.traitors.indexOf(leader)] = card.leader;
}
card.leader = leader;
} // Conserved physical identity swap, not a fabricated extra Face Dancer.
custody(game);
const defender = player(game, 'emperor').leaders.find(l => l.strength === 2)!;
game = openAdvancedNativeSkillBattle(reload(game), actor, enemy);
game = act(game, actor, { type: 'battlePlan', dial: 1, support: 1, leader });
game = act(game, enemy, { type: 'battlePlan', dial: 0, support: 0, leader: defender.id });
game = aftermath(resolve(game, actor, enemy), true);
assert.equal(game.lastBattleContext!.winner, actor);
assert.equal(game.decision?.kind, 'faceDance');
const funds = wallets(game), reserves = player(game, 'tleilaxu').reserves;
game = act(game, controller, { type: 'decision', reveal: true, sources: { reserves: 1 }, sector: 14 });
assert.equal(player(game, 'atreides').leaders.find(l => l.id === leader)!.dead, true);
assert.equal(game.leaderSkills!.assignments.some(a => a.leader === leader), false);
assert.equal(game.leaderSkills!.deck.filter(s => s === 'warmaster').length, 1);
assert.equal(player(game, 'tleilaxu').forces['wind_pass:14'], 1);
assert.equal(player(game, 'tleilaxu').reserves, reserves - 1);
assert.deepEqual(wallets(game), funds);
assert.equal(player(game, 'tleilaxu').faceDancers!.find(c => c.leader === leader)!.revealed, true);
custody(game);
});
void test('actual Harkonnen capture keeps a foreign ghola in its original roster and battle use returns Tleilaxu control', () => {
const original = completedAdvancedNativeSkillsGame({
family: 'tleilaxu', requestedSkill: 'warmaster', opponents: ['harkonnen', 'emperor'],
});
const staged = stageAdvancedTleilaxuForeignRevival(original);
const revived = settleRevival(act(staged.game, staged.actor, { type: 'reviveForeignGhola', leader: staged.leader }));
const captor = player(revived, 'harkonnen').id;
const skills = structuredClone(revived.leaderSkills);
let game = stageAdvancedNativeSkillBattle(revived, staged.actor, captor);
// Conserved controlled eligibility: all other living Tleilaxu discs have
// already been used elsewhere. Only the genuinely revived foreign disc is
// eligible for this battle's random capture, without changing the card pool.
for (const leader of player(game, 'tleilaxu').leaders)
if (!leader.dead) leader.usedAt = 'arrakeen';
const native = player(game, 'harkonnen').leaders.find(l => l.id !== trainer(game, captor).leader)!;
game = openAdvancedNativeSkillBattle(reload(game), staged.actor, captor, false);
game = act(game, staged.actor, { type: 'battlePlan', dial: 0, support: 0, leader: staged.leader });
game = act(game, captor, { type: 'battlePlan', dial: 0, support: 0, leader: native.id });
game = aftermath(resolve(game, staged.actor, captor), false, true);
assert.equal(game.lastBattleContext!.winner, captor);
assert.equal(game.decision?.kind, 'captureOffer');
game = act(game, captor, { type: 'decision', accept: true });
for (let i = 0; game.response && i < 100; i++) game = advancedNativeStep(reload(game));
assert.equal(game.decision?.kind, 'capturedLeader');
if (game.decision?.kind !== 'capturedLeader') throw new Error('Expected actual captured foreign leader.');
assert.equal(game.decision.leader, staged.leader);
assert.equal(game.decision.owner, staged.originalOwner);
assert.equal(game.decision.controller, staged.actor);
const captive = player(game, 'emperor').leaders.find(l => l.id === staged.leader)!;
assert.equal(captive.capturedBy, captor);
assert.equal(captive.gholaBy, staged.actor);
assert.equal(captive.concealed!.controller, staged.actor);
assert.deepEqual(game.leaderSkills, skills);
custody(game);
const choice = reload(game), funds = wallets(game);
const executed = aftermath(act(choice, captor, { type: 'decision', mode: 'execute' }));
const dead = player(executed, 'emperor').leaders.find(l => l.id === staged.leader)!;
assert.equal(dead.dead, true);
assert.equal(dead.capturedBy, undefined);
assert.equal(dead.gholaBy, staged.actor);
assert.equal(dead.concealed!.controller, staged.actor);
assert.equal(dead.concealed!.dead, false, 'The original hidden living snapshot is retained after execution.');
assert.deepEqual(wallets(executed), { ...funds, [captor]: funds[captor] + 2 });
assert.deepEqual(executed.leaderSkills, skills);
custody(executed);
game = aftermath(act(choice, captor, { type: 'decision', mode: 'keep' }));
assert.equal(player(game, 'emperor').leaders.find(l => l.id === staged.leader)!.capturedBy, captor);
assert.deepEqual(wallets(game), funds);
game = stageAdvancedNativeSkillBattle(game, captor, staged.originalOwner);
const defender = player(game, 'emperor').leaders.find(l => l.strength === 3)!;
game = openAdvancedNativeSkillBattle(reload(game), captor, staged.originalOwner, false);
// The captured foreign disc is genuinely selectable by Harkonnen, not by
// either its original Emperor owner or its pre-capture Tleilaxu controller.
reject(game, staged.originalOwner, { type: 'battlePlan', dial: 0, support: 0, leader: staged.leader });
game = act(game, captor, { type: 'battlePlan', dial: 0, support: 0, leader: staged.leader });
game = act(game, staged.originalOwner, { type: 'battlePlan', dial: 0, support: 0, leader: defender.id });
game = aftermath(resolve(game, captor, staged.originalOwner));
assert.equal(game.lastBattleContext!.winner, staged.originalOwner);
const returned = player(game, 'emperor').leaders.find(l => l.id === staged.leader)!;
assert.equal(returned.dead, false);
assert.equal(returned.capturedBy, undefined);
assert.equal(returned.concealed, undefined);
assert.equal(returned.gholaBy, staged.actor);
assert.equal(player(game, 'tleilaxu').leaders.some(l => l.id === staged.leader), false);
assert.deepEqual(wallets(game), funds);
assert.deepEqual(game.leaderSkills, skills);
assert.deepEqual(normalizeAutomaticGame(reload(game)), game);
custody(game);
});
void test('foreign revival retains Basic, controller, full-pool, capture, Auditor, Duke and foreign-training boundaries', () => {
const staged = stageAdvancedTleilaxuForeignRevival(setup());
const action: Action = { type: 'reviveForeignGhola', leader: staged.leader };
reject(staged.game, staged.originalOwner, action);
const full = reload(staged.game);
player(full, 'tleilaxu').leaders.find(l => l.id === staged.ownDead)!.dead = false;
reject(full, staged.actor, action);
const captured = reload(staged.game);
player(captured, 'emperor').leaders.find(l => l.id === staged.leader)!.capturedBy = staged.actor;
reject(captured, staged.actor, action);
reject(staged.game, staged.actor, { type: 'reviveForeignGhola', leader: DUKE_VIDAL_ID });
const basic = completedTleilaxuSkillsGame();
Object.assign(basic, { phase: 4, phaseOpening: null, response: null, decision: null });
player(basic, 'emperor').leaders[1].dead = true;
reject(basic, player(basic, 'tleilaxu').id, { type: 'reviveForeignGhola', leader: player(basic, 'emperor').leaders[1].id });
const advanced = settleRevival(act(staged.game, staged.actor, action));
const assignment = reload(advanced);
const native = trainer(assignment, staged.actor);
native.leader = staged.leader;
reject(assignment, staged.actor, { type: 'ready' });
const wrongController = reload(advanced);
player(wrongController, 'emperor').leaders.find(l => l.id === staged.leader)!.gholaBy = staged.originalOwner;
reject(wrongController, staged.actor, { type: 'ready' });
const capturedGhola = reload(advanced);
player(capturedGhola, 'emperor').leaders.find(l => l.id === staged.leader)!.capturedBy = staged.actor;
reject(capturedGhola, staged.actor, { type: 'ready' });
const basicGhola = reload(advanced);
basicGhola.advanced = false;
reject(basicGhola, staged.actor, { type: 'ready' });
const lobby = createGame('ADVTLEIAUD', newPlayer('human-t', 'Tleilaxu', 'tleilaxu'), true, ['ix', 'choam']);
joinGame(lobby, newPlayer('human-c', 'CHOAM', 'choam'));
joinGame(lobby, newPlayer('human-e', 'Emperor', 'emperor'));
const admitted = completedAdvancedNativeSkillsGame({ family: 'tleilaxu', requestedSkill: 'warmaster', initial: lobby });
assert.deepEqual(admitted.players.map(p => p.id), ['human-t', 'human-c', 'human-e']);
const audit = stageAdvancedTleilaxuForeignRevival(admitted);
const auditor = player(audit.game, 'choam').leaders.find(l => l.id === 'choam-auditor')!;
auditor.dead = true; auditor.deaths = 1;
reject(audit.game, audit.actor, { type: 'reviveForeignGhola', leader: auditor.id });
custody(audit.game);
});
+12 -6
View File
@@ -262,10 +262,10 @@ void test('Moritani Skills samples preserve their full module on saved continuat
assert.equal(existsSync(unsupported), false);
});
void test('Tleilaxu Skills samples preserve their full module on saved continuation and reject Advanced before running', (t) => {
void test('Tleilaxu Skills samples preserve full Basic and Advanced native decks through saved continuation', (t) => {
const area = temporary(t);
const out = join(area, 'skills');
const result = run(out, '--profile', 'tleilaxu-skills', '--players', '2', '--seed', '1000', '--max-actions', '1');
const result = run(out, '--profile', 'tleilaxu-skills', '--rules', 'basic', '--players', '2', '--seed', '1000', '--max-actions', '1');
assert.equal(result.status, 1);
assertFailedEvidence(out, 'tleilaxu-skills-2-basic', 1021, false);
const snapshot = join(out, 'failed-tleilaxu-skills-2-basic.json');
@@ -276,10 +276,16 @@ void test('Tleilaxu Skills samples preserve their full module on saved continuat
const resumed = join(area, 'skills-resumed');
assert.equal(run(resumed, '--resume', snapshot, '--seed', '1000', '--max-actions', '1').status, 1);
assertFailedEvidence(resumed, 'tleilaxu-skills-2-basic', 1021, true);
const unsupported = join(area, 'unsupported');
const bad = run(unsupported, '--profile', 'tleilaxu-skills', '--rules', 'advanced');
assert.equal(bad.status, 1);
assert.equal(existsSync(unsupported), false);
const advanced = join(area, 'advanced');
assert.equal(run(advanced, '--profile', 'tleilaxu-skills', '--rules', 'advanced', '--players', '2', '--seed', '1000', '--max-actions', '1').status, 1);
for (const [name, seed] of [['tleilaxu-skills-2-advanced', 1235], ['tleilaxu-skills-tleilaxu-ixians-2-advanced', 1240]] as const) {
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('Ixian Skills samples preserve full Basic and Advanced native decks through saved continuation', (t) => {
+43 -6
View File
@@ -6,10 +6,11 @@ import {
import type { FactionId } from '../game/catalog';
import { LEADER_SKILL_CARDS, type LeaderSkillId } from '../game/leader-skill-cards';
import { validateLeaderSkills } from '../game/leader-skills';
import { newRevivalRules } from '../game/revival';
import { nextSpiceBankerIncomeNativeStep } from './fixture-spice-banker-income';
export type AdvancedNativeSkillsOptions = {
family: 'ixians' | 'choam';
family: 'ixians' | 'choam' | 'tleilaxu';
requestedSkill?: LeaderSkillId;
skillOwner?: FactionId;
opponents?: readonly FactionId[];
@@ -52,7 +53,7 @@ function withSkillShuffle<T>(skill: LeaderSkillId, ownerIndex: number, initializ
* 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';
const expansion = options.family === 'choam' ? 'choam' : 'ix';
let game: Game;
if (options.initial) game = structuredClone(options.initial);
else {
@@ -144,8 +145,17 @@ export function assertAdvancedNativeCustody(game: Game): void {
* 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');
return stageAdvancedNativeSkillBattle(game,
advancedNativePlayer(game, options.skillOwner ?? options.family).id,
advancedNativePlayer(game, 'emperor').id);
}
/** Reuse original authenticated seats and conserved inventories after revival. */
export function stageAdvancedNativeSkillBattle(state: Game, actor: string, target: string): Game {
const game = structuredClone(state);
const owner = game.players.find(p => p.id === actor)!;
const enemy = game.players.find(p => p.id === target)!;
assert.ok(owner && enemy && owner !== enemy);
for (const p of game.players) {
game.deck.push(...p.hand);
p.hand = [];
@@ -171,11 +181,38 @@ export function createAdvancedNativeSkillBattle(options: AdvancedNativeSkillsOpt
return game;
}
export function openAdvancedNativeSkillBattle(state: Game, actor: string, target: string): Game {
/** Explicit conserved Revival position after genuine Advanced Tleilaxu setup.
* Only untrained original discs are staged dead; no assigned card is removed.
* Wallets and all original card/force inventories remain untouched. */
export function stageAdvancedTleilaxuForeignRevival(state: Game): {
game: Game; actor: string; originalOwner: string; leader: string; ownDead: string;
} {
const game = structuredClone(state);
assert.equal(game.status, 'playing');
assert.equal(game.advanced, true);
const owner = advancedNativePlayer(game, 'tleilaxu');
const foreignOwner = advancedNativePlayer(game, 'emperor');
const trained = new Set(game.leaderSkills!.assignments.map(a => a.leader));
const own = owner.leaders.find(l => !trained.has(l.id) && !l.gholaBy && !l.capturedBy)!;
const foreign = foreignOwner.leaders.filter(l => !trained.has(l.id) && !l.capturedBy)
.sort((a, b) => a.strength - b.strength)[0];
assert.ok(own && foreign);
for (const leader of [own, foreign]) {
leader.dead = true;
leader.deaths = Math.max(1, leader.deaths ?? 0);
}
Object.assign(game, { phase: 4, active: null, ready: [], decision: null, response: null,
phaseOpening: null, revivalRules: newRevivalRules() });
for (const p of game.players) p.leaderRevived = false;
assertAdvancedNativeCustody(game);
return { game, actor: owner.id, originalOwner: foreignOwner.id, leader: foreign.id, ownDead: own.id };
}
export function openAdvancedNativeSkillBattle(state: Game, actor: string, target: string, hideSkills = true): 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 });
game = applyAction(game, game.decision.player, { type: 'leaderSkillVisibility', event: game.decision.event, hide: hideSkills });
} else game = advancedNativeStep(game);
}
assert.equal(game.decision, null);
+3 -14
View File
@@ -5,14 +5,12 @@ import { createElement } from 'react';
import { applyAction, createGame, initializeLeaderSkillsGameForAudit, joinGame, newPlayer, normalizeAutomaticGame, viewGame, type Action, type Game } from '../game/engine';
import { LEADER_SKILL_CARDS } from '../game/leader-skill-cards';
import { basicTleilaxuLeaderSkillsProfile } from '../game/leader-skill-profile';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { registerHooks } from 'node:module';
const aliases = registerHooks({resolve(specifier,context,next){return next(specifier === 'next/image' ? 'vinext/shims/image' : specifier,context);}});
const { RihaniChoice } = await import('../components/rihani-decipherer');
aliases.deregister();
import { sampleInventory, verifySampleCustody } from '../tools/sample-custody';
import { completedTleilaxuSkillsGame, initializedTleilaxuSkillsOffers, assertTleilaxuSkillsCustody as custody, tleilaxuSkillsPlayer as player, reloadTleilaxuSkillsGame as reload, rejectTleilaxuSkillsAction as reject } from './tleilaxu-skills-fixture';
import { completedTleilaxuSkillsGame, assertTleilaxuSkillsCustody as custody, tleilaxuSkillsPlayer as player, reloadTleilaxuSkillsGame as reload, rejectTleilaxuSkillsAction as reject } from './tleilaxu-skills-fixture';
const act = (g: Game, owner: string, action: Action) => applyAction(reload(g), owner, action);
function stable(g: Game) {
@@ -56,7 +54,7 @@ function exchange(g: Game, owner: string) {
return act(g, owner, { type: 'decision', event, cards: [r.drawn[0], r.eligible[0]] });
}
void test('all fourteen skills admit genuine Basic Tleilaxu setup while public, Advanced, Richese and module gates remain closed', () => {
void test('all fourteen skills admit genuine Basic Tleilaxu setup while public, Richese and module gates remain closed', () => {
for (const card of LEADER_SKILL_CARDS) {
const game = completedTleilaxuSkillsGame({ requestedSkill: card.id });
assert.equal(game.leaderSkills!.assignments.find(a => a.owner === 't')!.skill, card.id);
@@ -64,7 +62,7 @@ void test('all fourteen skills admit genuine Basic Tleilaxu setup while public,
assert.equal(basicTleilaxuLeaderSkillsProfile(viewGame(game, 'a')), true);
stable(game);
}
for (const mutate of [(g: Game) => {g.advanced = true;}, (g: Game) => {g.players[1] = newPlayer('x','Richese','richese');},
for (const mutate of [(g: Game) => {g.players[1] = newPlayer('x','Richese','richese');},
(g: Game) => {g.discoveryEnabled = true;}, (g: Game) => {g.expansions.push('choam');}]) {
const g = createGame('TLEIGATE',newPlayer('t','Tleilaxu','tleilaxu'),false,['ix']);
joinGame(g,newPlayer('e','Emperor','emperor'));g.players.forEach(p => {p.ready = true;});mutate(g);
@@ -72,11 +70,6 @@ void test('all fourteen skills admit genuine Basic Tleilaxu setup while public,
}
const lobby = createGame('TLEIPUBL',newPlayer('t','Tleilaxu','tleilaxu'),false,['ix']);
joinGame(lobby,newPlayer('e','Emperor','emperor'));lobby.players.forEach(p => {p.ready = true;});reject(lobby,'t',{type:'start'});
const offer = initializedTleilaxuSkillsOffers({requestedSkill:'rihani-decipherer'});
for (const difficulty of DIFFICULTIES) {
const view = viewGame(offer,'t');view.players.find(p => p.id === 't')!.bot = difficulty;
const action = botActions(view).find(a => a.type === 'leaderSkill');assert.ok(action);assert.doesNotThrow(() => act(offer,'t',action));
}
});
void test('native Rihani keeps a private Face Dancer, returns only an old unrevealed card and survives JSON across every choice', () => {
@@ -99,10 +92,6 @@ void test('native Rihani keeps a private Face Dancer, returns only an old unreve
assert.equal(viewGame(g,observer).rihani!.pending?.drawn,undefined);
}
reject(g,'t',{type:'decision',event:drawn.event,cards:[drawn.drawn[0],known]});
for (const difficulty of DIFFICULTIES) {
const view = viewGame(g,'t');view.players.find(p => p.id === 't')!.bot = difficulty;
const action=botActions(view).find(a=>a.type==='decision');assert.ok(action);assert.doesNotThrow(()=>act(g,'t',action));
}
g=act(g,'t',{type:'decision',event:drawn.event,cards:[drawn.drawn[0],drawn.eligible[0]]});
assert.equal(player(g,'t').faceDancers!.length,3);
assert.ok(player(g,'t').faceDancers!.some(c=>c.leader===known&&c.revealed));
+23 -10
View File
@@ -230,13 +230,24 @@ const MORITANI_ASSASSINATE_SCENARIOS: readonly Scenario[] = [2, 3, 4, 5, 6].map(
roster: MORITANI_ASSASSINATE_ROSTER.slice(0, players),
}));
const TLEILAXU_SKILLS_SCENARIOS: readonly Scenario[] = [2, 3, 4, 5, 6].map(players => ({
ordinal: 21 + players - 2,
profile: 'tleilaxu-skills',
rules: 'basic',
expansions: ['ix'],
roster: (['tleilaxu', 'emperor', 'guild', 'harkonnen', 'fremen', 'beneGesserit'] as FactionId[]).slice(0, players),
}));
const TLEILAXU_SKILLS_SCENARIOS: readonly Scenario[] = [
...[2, 3, 4, 5, 6].map((players): Scenario => ({
ordinal: 21 + players - 2, profile: 'tleilaxu-skills', rules: 'basic', expansions: ['ix'],
roster: (['tleilaxu', 'emperor', 'guild', 'harkonnen', 'fremen', 'beneGesserit'] as FactionId[]).slice(0, players),
})),
...[2, 3, 4, 5, 6].map((players): Scenario => ({
ordinal: 235 + players - 2, profile: 'tleilaxu-skills', rules: 'advanced', expansions: ['ix'],
roster: (['tleilaxu', 'emperor', 'guild', 'harkonnen', 'fremen', 'beneGesserit'] as FactionId[]).slice(0, players),
})),
...[2, 3, 4, 5, 6].map((players): Scenario => ({
ordinal: 240 + players - 2, profile: 'tleilaxu-skills', rules: 'advanced', expansions: ['ix'],
roster: (['tleilaxu', 'ixians', 'emperor', 'guild', 'harkonnen', 'fremen'] as FactionId[]).slice(0, players),
})),
...[3, 4, 5, 6].map((players): Scenario => ({
ordinal: 245 + players - 3, profile: 'tleilaxu-skills', rules: 'advanced', expansions: ['ix', 'choam'],
roster: (['tleilaxu', 'ixians', 'choam', 'emperor', 'guild', 'harkonnen'] as FactionId[]).slice(0, players),
})),
];
const IX_SKILLS_SCENARIOS: readonly Scenario[] = [
...[2, 3, 4, 5, 6].map((players): Scenario => ({
ordinal: 26 + players - 2, profile: 'ix-skills', rules: 'basic', expansions: ['ix'],
@@ -337,7 +348,7 @@ function usage() {
'[--resume FAILED_GAME.json]\n' +
'Runs genuine setup and gameplay offline. Default/all keeps the six expansion samples; combined-nexus and combined-homeworld-nexus add five/six-seat Basic/Advanced all-expansion samples. Base, Homeworld, Nexus, Homeworld-Nexus, expansion roster, paired expansion Nexus, Ecaz card variant, Advanced Moritani assassination and skill profiles select their documented rosters. Stronghold-factions is Advanced only: Ixians + CHOAM, native Ixians + Tleilaxu, or native CHOAM + Richese with classic opponents, two through six seats; mixed four-native rosters also run at four through six seats. Selected Ix/CHOAM decks determine the canonical 47/35 Treachery Cards; Richese has a separate ten-card cache and all samples have six separate Stronghold Cards without other modules. Scenario names identify their native roster. --players requires a supported profile. Output must be a new private directory outside the checkout.' +
'\nEcaz-occupy is Advanced only: native Ecaz plus classic/Ixian/Tleilaxu/CHOAM/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.'
'\nIx-skills, choam-skills and tleilaxu-skills retain their Basic samples and add Advanced native plus classic rosters, two through six seats. Ix-skills also selects Ixian+CHOAM; tleilaxu-skills additionally selects Tleilaxu+Ixian and three-to-six-seat Tleilaxu+Ixian+CHOAM. All fourteen skills and distinct required family decks remain. Moritani-skills remains Basic only.'
);
}
@@ -380,6 +391,8 @@ function scenarioName(scenario: Scenario) {
}
if (scenario.profile === 'ix-skills' && scenario.rules === 'advanced' && scenario.roster.includes('choam'))
return `${scenario.profile}-ixians-choam-${scenario.roster.length}-${scenario.rules}`;
if (scenario.profile === 'tleilaxu-skills' && scenario.rules === 'advanced' && scenario.roster.includes('ixians'))
return `${scenario.profile}-${scenario.roster.filter(faction => ['tleilaxu', 'ixians', 'choam'].includes(faction)).join('-')}-${scenario.roster.length}-${scenario.rules}`;
return scenario.profile === 'base' || scenario.profile === 'homeworld' || scenario.profile === 'nexus' || scenario.profile === 'homeworld-nexus' || scenario.profile === 'combined-nexus' || scenario.profile === 'combined-homeworld-nexus' ||
scenario.profile === 'choam-roster' || scenario.profile === 'ecaz-roster' || scenario.profile === 'ecaz-treachery' || scenario.profile === 'ecaz-occupy' || scenario.profile === 'moritani-assassinate' || scenario.profile === 'ix-roster' || scenario.profile === 'choam-nexus' || scenario.profile === 'ecaz-nexus' || scenario.profile === 'ix-nexus' || skillProfile(scenario.profile)
? `${scenario.profile}-${scenario.roster.length}-${scenario.rules}`
@@ -721,8 +734,8 @@ async function main() {
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))
throw new Error(`--profile ${profile} supports only five or six players.`);
if ((profile === 'moritani-skills' || profile === 'tleilaxu-skills') && rules === 'advanced')
throw new Error('Moritani and Tleilaxu Leader Skills profiles currently support only Basic samples.');
if (profile === 'moritani-skills' && rules === 'advanced')
throw new Error('Moritani Leader Skills profiles currently support only Basic samples.');
if (profile === 'moritani-assassinate' && rules === 'basic')
throw new Error('Moritani assassination samples require Advanced rules.');
if (profile === 'stronghold-factions' && rules === 'basic')
+2 -2
View File
@@ -22,9 +22,9 @@ async function main() {
});
if (values.help) {
console.log(
'Usage: node --import tsx tools/start-prototype.ts --profile ix|discovery|leader-skills|banker-income|factions|stronghold-factions|nexus|moritani-assassinate|ecaz-treachery|ecaz-occupy|semuta|kull|richese-betrayal|nexus-kull|guild-betrayal|ixian-replacement|ixian-betrayal|harkonnen-betrayal --db PATH --room CODE --version NUMBER --out /private/new-directory\nStarts only a fresh ready lobby for the selected development prototype. Stronghold-factions opts a fresh ready 2–6-seat Advanced roster with at least one selected native Ixians, Tleilaxu, CHOAM or Richese faction and otherwise classic opponents into Stronghold Cards alone. Select one or both distinct Ix/CHOAM decks; each native faction requires its family deck. The selected decks supply 47 Ix or combined, or 35 CHOAM canonical physical Treachery Cards, with a separate ten-card Richese cache when Richese plays. Native setup choices, HMS placement, Face Dancers, Richese cache and CHOAM Auditor remain genuine; custody starts unowned and is claimed by actual END Mentat. It does not redeal, reset or retrofit a saved game. Banker-income opts a fresh 2–6-seat classic Basic/Advanced 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.',
'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, Tleilaxu 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.' +
'\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.',
'\nLeader-skills retains the existing classic and Basic native entries and now composes Advanced native Ixians, Tleilaxu and/or CHOAM with classic opponents, their distinct required ix/choam decks and all fourteen skills. Original native setup, Face Dancers, Auditor exclusion, typed force support and skill posture remain. Native foreign-ghola revival never draws or assigns a new skill. Explicit --mentat-question retains its private uniform preview; banker-income retains separate normal deferred income in the same supported native subset. Public activation, empty-hand/captured-replacement rulings and other optional overlays remain pending.',
);
return;
}