Compose native Richese skills and Advanced contribution receipts

This commit is contained in:
Quintine committed 2026-10-03 17:46:10 +10:00
1 parent 10d74f8db8
commit 032b41aa87
23 files changed
+1132 -111

No files matched your search

+36 -13
View File
@@ -89,6 +89,16 @@ const ids = (v: unknown): v is string[] =>
const object = (v: unknown): v is Record<string, unknown> =>
!!v && typeof v === 'object' && !Array.isArray(v);
/** The original Emperor/bank-routed invoice, not the buyer-to-Richese leg. */
export function auctionEmperorIncomeAmount(
sale: NonNullable<Game['currentAuctionSale']>,
): number {
if (sale.free || sale.recipient === 'bank') return 0;
return sale.origin === 'normal' || sale.seller === sale.winner
? sale.amount
: sale.richeseContribution ?? 0;
}
/** Deterministic paid-sale continuation only. A returned response, Richese
* transition or normal phase-end is a boundary, not a simulated future action.
* Never publish this internal quote: returned lot cards remain private. */
@@ -270,6 +280,16 @@ function calculate(
count(sale.amount) && typeof sale.free === 'boolean',
'The paid auction amount or free flag is invalid.',
);
requireAuction(
sale.richeseContribution === undefined ||
(input.advanced &&
sale.origin !== 'normal' &&
sale.seller !== sale.winner &&
count(sale.richeseContribution) &&
sale.richeseContribution > 0 &&
sale.richeseContribution <= sale.amount),
'A Richese contribution requires a positive bounded Advanced other-buyer sale.',
);
if (operation.kind === 'sale')
requireAuction(
typeof operation.free === 'boolean' && operation.free === sale.free,
@@ -310,9 +330,8 @@ function calculate(
} else if (r.kind === 'emperorIncome') {
requireAuction(
seated(r.owner)?.faction === 'emperor' &&
r.owner !== winner.id &&
!sale.free &&
(sale.origin === 'normal' || sale.seller === winner.id),
auctionEmperorIncomeAmount(sale) > 0 &&
(r.owner !== winner.id || sale.richeseContribution !== undefined),
'The canceled Emperor income is not payable by this sale.',
);
stage = 'bonus';
@@ -347,25 +366,29 @@ function calculate(
if (sale.origin !== 'normal' && sale.seller !== winner.id) {
if (sale.recipient !== 'bank') {
const seller = seated(sale.seller!)!;
const amount = sale.amount - (sale.richeseContribution ?? 0);
requireAuction(
count(seller.spice) && count(seller.spice + sale.amount),
count(seller.spice) && count(seller.spice + amount),
'The seller credit would overflow its current balance.',
);
steps.push({
kind: 'sellerCredit',
player: seller.id,
amount: sale.amount,
balance: seller.spice + sale.amount,
amount,
balance: seller.spice + amount,
});
}
} else {
const emperor = input.players.find((p) => p.faction === 'emperor');
if (!sale.free && emperor && emperor.id !== winner.id)
return result({
kind: 'response',
response: { kind: 'emperorIncome', owner: emperor.id, passed: [] },
});
}
const emperor = input.players.find((p) => p.faction === 'emperor');
if (
emperor &&
auctionEmperorIncomeAmount(sale) > 0 &&
(emperor.id !== winner.id || sale.richeseContribution !== undefined)
)
return result({
kind: 'response',
response: { kind: 'emperorIncome', owner: emperor.id, passed: [] },
});
stage = 'bonus';
}
if (
+45 -9
View File
@@ -234,6 +234,7 @@ import {
} from './choam-storm-quote';
import {
quoteAuctionContinuation,
auctionEmperorIncomeAmount,
AuctionContinuationError,
type AuctionContinuationOperation,
type AuctionContinuationQuote,
@@ -1549,6 +1550,8 @@ export type Game = {
seller: string | null;
/** Original seller remains the source owner when the invoice is sent to bank. */
recipient?: 'bank';
/** Actual Richese donor share of another buyer's lot; paid to Emperor or bank. */
richeseContribution?: number;
} | null;
pendingAmbassador?: {
revivalEvent?: string;
@@ -7852,7 +7855,8 @@ function spiceBankerIncomeIntegrity(g: Game): void {
for (const carrier of [g.currentAuctionSale, ...responses]) {
const payment = carrier?.spiceBankerIncomePayment;
if (!payment) {
requireRule(!carrier || ('winner' in carrier ? carrier.free || carrier.origin !== 'normal' :
requireRule(!carrier || ('winner' in carrier ? carrier.free ||
(carrier.recipient !== 'bank' && auctionEmperorIncomeAmount(carrier) === 0) :
carrier.kind !== 'guildIncome' &&
!(carrier.kind === 'revivalIncome' && carrier.recipient !== carrier.owner && (carrier.amount ?? 0) > 1)),
'The original paid transaction lost its native bank invoice.');
@@ -7864,12 +7868,17 @@ function spiceBankerIncomeIntegrity(g: Game): void {
Number.isSafeInteger(leg.amount) && leg.amount > 0 && ['bank', 'player'].includes(leg.recipient)) &&
new Set(payment.legs.map(leg => leg.payer)).size === payment.legs.length,
'The original paid bank transaction changed its event or contribution legs.');
if (carrier && 'winner' in carrier)
requireRule(payment.kind === 'auction' && !carrier.free && carrier.origin === 'normal' &&
payment.legs.reduce((sum, leg) => sum + leg.amount, 0) === carrier.amount &&
payment.legs.every(leg => leg.payer === carrier.winner || leg.payer === getPlayer(g, carrier.winner).ally),
'The deferred bank invoice no longer matches its original normal auction.');
else if (carrier?.kind === 'guildIncome')
if (carrier && 'winner' in carrier) {
const amount = carrier.recipient === 'bank' ? carrier.amount : auctionEmperorIncomeAmount(carrier);
const contributionOnly = carrier.origin !== 'normal' && carrier.seller !== carrier.winner && carrier.recipient !== 'bank';
requireRule(payment.kind === 'auction' && !carrier.free &&
payment.legs.reduce((sum, leg) => sum + leg.amount, 0) === amount &&
(contributionOnly
? g.advanced && !!carrier.richeseContribution && payment.legs.length === 1 &&
payment.legs[0].payer === carrier.seller && payment.legs[0].amount === carrier.richeseContribution
: payment.legs.every(leg => leg.payer === carrier.winner || leg.payer === getPlayer(g, carrier.winner).ally)),
'The deferred bank invoice no longer matches its original auction payer legs.');
} else if (carrier?.kind === 'guildIncome')
requireRule(payment.kind === 'shipment' &&
payment.legs.filter(leg => leg.recipient === 'player').reduce((sum, leg) => sum + leg.amount, 0) === carrier.amount,
'The deferred bank invoice no longer matches its original Guild income.');
@@ -10783,6 +10792,15 @@ function settleRicheseLot(g: Game) {
function settleRicheseSoldLot(g: Game, quote: Extract<RicheseSettlementQuote,{kind:'sold'}>, bank: boolean) {
const lot = g.richeseAuction!, owner = getPlayer(g,lot.owner);
const winner = getPlayer(g, quote.winner), card = quote.card;
const richeseContribution = g.advanced && winner.id !== owner.id && winner.ally === owner.id
? quote.allyPayment : 0;
const bankAmount = bank || winner.id === owner.id ? quote.amount : richeseContribution;
const emperor = byFaction(g, 'emperor');
const incomePayment = g.spiceBankerIncomePreview && bankAmount > 0 ? bankerIncomePayment(g, 'auction',
bank || winner.id === owner.id
? [{payer:winner.id,amount:quote.ownPayment,recipient:bank || !emperor ? 'bank' : 'player'},
...(quote.allyPayment ? [{payer:winner.ally!,amount:quote.allyPayment,recipient:bank || !emperor ? 'bank' as const : 'player' as const}] : [])]
: [{payer:owner.id,amount:richeseContribution,recipient:emperor ? 'player' : 'bank'}]) : undefined;
payWithAlly(g, winner, quote.amount, quote.allyPayment);
g.richeseFunding = {};
if (lot.source === 'cache')
@@ -10790,6 +10808,8 @@ function settleRicheseSoldLot(g: Game, quote: Extract<RicheseSettlementQuote,{ki
else owner.hand = owner.hand.filter((c) => c.id !== card.id);
winner.hand.push(card);
g.currentAuctionSale = {
...(richeseContribution ? {richeseContribution} : {}),
...(incomePayment ? {spiceBankerIncomePayment:incomePayment} : {}),
winner: winner.id,
amount: quote.amount,
free: false,
@@ -10798,6 +10818,7 @@ function settleRicheseSoldLot(g: Game, quote: Extract<RicheseSettlementQuote,{ki
...(bank ? {recipient:'bank' as const} : {}),
...stampAuctionBureaucrat(g,winner,quote.amount,quote.allyPayment,owner.id),
};
if (bank || !emperor) awardBankerIncome(g, incomePayment);
log(
g,
`${winner.name} bought ${lot.source === 'cache' ? card.name : 'the concealed Black Market card'} for ${quote.amount} spice. The funded bid was paid automatically.`,
@@ -15557,7 +15578,8 @@ function currentFactionPayment(g: Game) {
'This Richese purchase income is no longer current.');
}
const gross = response.kind === 'emperorIncome'
? response.source === 'ambassador' ? response.amount! : g.currentAuctionSale?.amount ?? g.auction?.bid
? response.source === 'ambassador' ? response.amount! :
g.currentAuctionSale ? auctionEmperorIncomeAmount(g.currentAuctionSale) : g.auction?.bid
: response.amount;
requireRule(gross !== undefined, 'The faction payment is missing its original amount.');
if (kind === 'shipment' &&
@@ -23460,7 +23482,7 @@ function finishResponse(g: Game, canceled: boolean,bureaucratDiversion?:number)
} else if (response.kind === 'emperorIncome') {
if (!canceled) {
const owner = getPlayer(g, response.owner);
const amount = g.currentAuctionSale?.amount ?? g.auction!.bid;
const amount = g.currentAuctionSale ? auctionEmperorIncomeAmount(g.currentAuctionSale) : g.auction!.bid;
const payment = creditFactionPayment(g, owner.id, 'treachery', amount-(bureaucratDiversion ?? 0));
log(
g,
@@ -23862,6 +23884,20 @@ function leaderSkillNoFieldIntegrity(g: Game): boolean {
const responses = savedNoFieldResponses(g);
const decisions = homeworldSavedDecisions(g);
const shipment = g.pendingShipment;
if (g.leaderSkills && shipment?.alliedNoField) {
requireRule(ordinaryLeaderSkillModeSupported(g) && !shipment.noField &&
shipment.smuggler === undefined && shipment.smugglerCompanion === undefined &&
shipment.noFieldSkillProof === undefined,
'Allied No-Field skill composition retains its original ordinary shipment only.');
const initial = responses.filter(r => r.kind === 'richeseNoField');
const guild = decisions.filter(d => d.kind === 'guildShipment');
requireRule(initial.length + guild.length === 1,
'The allied No-Field must retain its original cancellation or Guild continuation.');
for (const response of initial) requireRule(noFieldCancellationQuote(g, response)?.kind === 'allied',
'The allied No-Field lost its original owner, recipient or token declaration.');
for (const decision of guild) validateGuildShipmentDecision(g, decision);
return true;
}
if (!(shipment?.smugglerCompanion !== undefined || shipment?.noFieldSkillProof !== undefined ||
(g.leaderSkills && shipment?.noField) || responses.some(r => r.noFieldSkillProof !== undefined) ||
decisions.some(d => 'noFieldSkillProof' in d))) return false;
+15 -7
View File
@@ -6,6 +6,7 @@ import {
weaponTypes,
} from './battle-cards';
import type { LeaderSkillId } from './leader-skill-cards';
import { RICHESE_CARD_DEFINITIONS } from './richese-cards';
export type DirectLeaderSkillId =
| 'warmaster'
@@ -66,19 +67,26 @@ export const PLANETOLOGIST_IX_SPECIALS = [
{ id: 'ix-amal', name: 'Amal', effect: 'amal' },
] as const;
const planetologistBaseSpecialById = new Map<
string,
(typeof PLANETOLOGIST_BASE_SPECIALS)[number] | (typeof PLANETOLOGIST_IX_SPECIALS)[number]
>(
[...PLANETOLOGIST_BASE_SPECIALS, ...PLANETOLOGIST_IX_SPECIALS].map((card) => [card.id, card]),
);
// Build once from physical faces. Richese's cache also transports its red/blue
// components as `special`, so only printed green types join the role lookup.
const planetologistSpecialById: Partial<Record<string, Pick<Card, 'name' | 'effect'>>> =
Object.create(null);
for (const card of [...PLANETOLOGIST_BASE_SPECIALS, ...PLANETOLOGIST_IX_SPECIALS])
planetologistSpecialById[card.id] = card;
for (const definition of RICHESE_CARD_DEFINITIONS) {
if (
definition.printedType === 'Special' ||
definition.printedType === 'Special - Movement'
)
planetologistSpecialById[definition.card.id] = definition.card;
}
/** Shared engine/UI/bot eligibility: exact printed green component, no native use. */
export function isPlanetologistBattleSpecialCard(
card: Pick<Card, 'id' | 'name' | 'kind' | 'effect'> | undefined,
): boolean {
if (!card || card.kind !== 'special') return false;
const canonical = planetologistBaseSpecialById.get(card.id);
const canonical = planetologistSpecialById[card.id];
return (
canonical?.name === card.name &&
canonical.effect === card.effect
+29 -3
View File
@@ -90,9 +90,34 @@ export function basicChoamLeaderSkillsProfile(game: LeaderSkillProfile): boolean
FACTIONS.some(faction => faction.id === player.faction && faction.expansion === 'base'));
}
/** Native Basic Richese/CHOAM setup, without unrelated optional overlays. */
export function basicRicheseLeaderSkillsProfile(game: LeaderSkillProfile): boolean {
const players = game.players;
if (game.advanced !== false || game.expansions.length !== 1 ||
game.expansions[0] !== 'choam' || !players || players.length < 2 || players.length > 6 ||
!noOtherLeaderSkillModules(game) || game.semutaPreview || game.moritaniAssassinatePreview ||
game.advancedPreview || game.kullPreview || game.nexusKullPreview ||
game.guildBetrayalPreview || game.richeseBetrayalPreview ||
game.nexusIxianReplacementPreview || game.nexusIxianBetrayalPreview ||
game.nexusHarkonnenBetrayalPreview) return false;
let native = false;
for (let i = 0; i < players.length; i++) {
const player = players[i];
for (let j = 0; j < i; j++) if (players[j].faction === player.faction) return false;
if (player.faction === 'richese') native = true;
else if (player.faction !== 'choam') {
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 basicExpansionLeaderSkillsProfile(game: LeaderSkillProfile): boolean {
return basicMoritaniLeaderSkillsProfile(game) || basicTleilaxuLeaderSkillsProfile(game) ||
basicIxLeaderSkillsProfile(game) || basicChoamLeaderSkillsProfile(game);
basicIxLeaderSkillsProfile(game) || basicChoamLeaderSkillsProfile(game) || basicRicheseLeaderSkillsProfile(game);
}
/** Source-clear Advanced native factions; optional decks never imply a seated faction. */
@@ -114,8 +139,9 @@ 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 === 'tleilaxu' || player.faction === 'choam') {
if (!game.expansions.includes(player.faction === 'choam' ? 'choam' : 'ix')) return false;
if (player.faction === 'ixians' || player.faction === 'tleilaxu' ||
player.faction === 'choam' || player.faction === 'richese') {
if (!game.expansions.includes(player.faction === 'choam' || player.faction === 'richese' ? 'choam' : 'ix')) return false;
native = true;
} else {
let classic = false;
+27 -3
View File
@@ -4476,6 +4476,30 @@ export const RULE_TOPICS: RuleTopic[] = [
{ 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'] },
],
},
{
id: 'richese-leader-skills',
title: 'Richese with Leader Skills',
category: 'Advanced & expansions',
coverage: 'Partial',
developmentStage: 'Prototyped',
summary: 'Native cache setup, fourteen skills, capped concealed-force aftermath and exact green-card substitution in bounded development tables.',
steps: [
'Basic or Advanced native Richese retains its required CHOAM deck, separate ten-card cache and all fourteen skills. Native CHOAM may share the table; Advanced selected Ix decks and Tleilaxu retain their original inventory and explicit combination limits. Starting cards precede skills, public assignment and traitors. Public starts remain gated.',
'Marker-only zero, three and five battles use the owning private reserve-capped pool and reveal only after both plans seal. Actual force loss then feeds original Suk rescue; no token value is a second casualty or a phantom reserve. Ordinary forces are separate. Own mixed marker/physical battle stays guarded; voluntary reveal must occur before Battle.',
'A selected surviving Planetologist may use a canonical printed green Richese Special or Special - Movement in its weapon slot for two strength. Exact physical identity is required. Stone Burner, Mirror Weapon and Portable Snooper do not qualify. The green card is discarded once without its native effect, fee, search or early leader kill.',
'The normal Smuggler own companion is one separate force alongside the marker, not added to its hidden value. Ordinary allied No-Field cancellation and Guild stages retain their exact owner/recipient/token frame; an allied free companion is not granted.',
'The authorized Advanced recipient cutover keeps Richese ally contributions distinct: buyer four goes to Richese, funded Richese four goes to Emperor or bank. This also applies when Emperor is the buyer. Cancellation redirects only the original Emperor share, not unrelated buyer credit, with no second donor debit or card transfer. Basic keeps its publisher receipt.',
'Separate normal Banker income uses only actual bank-routed payer legs and remains unavailable until actual Mentat. Allowed Emperor income and ordinary seller credit are not bank payments. Native Ixian Technology on Richese lots, exhausted-cache arithmetic, positive Black Market self-bidding, Bureaucrat split funding and other module/ruling boundaries remain explicit.',
],
related: ['leader-skills', 'richese-cards', 'richese-no-field', 'spice-banker-income', 'implementation-checklist'],
checklist: [
{area:'Implementation',status:'Partial',detail:'Original native setup, exact green role lookup, private capped marker outcomes, ordinary allied declaration and Advanced original payer/recipient continuation compose. Existing rule guards and Basic source remain.',evidence:['game/leader-skill-profile.ts','game/leader-skill-combat.ts','game/auction-continuation-quote.ts','game/engine.ts']},
{area:'Player controls',status:'Partial',detail:'Original skill offers, cache bidding, concealed shipment, original preparation and plan slots, physical rescue, partial income and deferred grant controls are reused. Actual human evidence is recorded separately.',evidence:['components/leader-skills.tsx','components/richese-no-field.tsx','components/game-table.tsx']},
{area:'AI',status:'Partial',detail:'Shared exact roles and native private pools supply legal candidates; all four policies perform actual capped Suk rescue. No new strategy or calibration. Source questions can still stop full games.',evidence:['game/bots.ts','game/leader-skill-combat.ts']},
{area:'Documentation',status:'Partial',detail:'Exact native skill, green disposal and Basic-versus-Advanced contribution source are documented without full-family certification.',evidence:['docs/LEADER_SKILLS_RUNTIME.md','docs/RICHESE_AUCTION_RULES.md','docs/SMUGGLER_NO_FIELD.md']},
{area:'Verification',status:'Partial',detail:'Final affected rule/CLI union151/151, types/lint/build and eleven controlled programs pass. Genuine29-game batch is not green:28 finish11,788 actions/303 JSON continuations/130 battles; one exhausted-cache guard is captured. Actual390px green battle358WLPYA v16 and funded sale/native Mentat E2K2U5ZW v26 retain original controls and refresh without repeated payment. Broader/full/deployed acceptance remains open.',evidence:['tests/native-richese-skills.test.ts','tests/native-richese-green-battle.test.ts','tests/richese-contribution-payments.test.ts','tests/faction-games-cli.test.ts']},
],
},
{
id: 'moritani-leader-skills',
title: 'Moritani with Leader Skills',
@@ -4507,7 +4531,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 two-to-six classic factions in Basic or Advanced with the base deck, the supported Basic native skill families, bounded Advanced Ixian/Tleilaxu/CHOAM plus classic seats with distinct required decks, or the separate Advanced Moritani/non-Harkonnen classic assassination roster with the ecaz deck. All fourteen skills and native setup remain. Foreign revival never assigns a skill, and Terror placement remains free. Unrelated native families and optional overlays stay guarded; no public start or old-game retrofit.',
'This separate fresh profile admits classic Basic/Advanced, supported Basic native families, bounded Advanced Ixian/Tleilaxu/CHOAM/Richese plus classic seats with required decks, or separate non-Harkonnen Advanced Moritani. All fourteen skills and native setup remain. Richese cache self-payments and authorized Advanced donor legs count only when actually paid to bank; allowed Emperor or seller income does not. Basic receipts and unrelated module/public gates remain.',
'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.',
@@ -4538,7 +4562,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: [
'Basic Moritani retains base opponents including Harkonnen. Its separate bounded Advanced classic/non-Harkonnen roster now composes the original assassination preview and all fourteen skills: winner Suk rescue and Rihani choice finish first, then an eligible different trained disc may be assassinated with its exact card return and printed bounty. No casualties repeat; original set-aside and one private Mentat replacement remain. See Moritani with Leader Skills for the exact scope.',
'Fresh Advanced 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.',
'Fresh Advanced profiles compose native Ixians, Tleilaxu, CHOAM and/or Richese with classic opponents and distinct required family decks. All fourteen skills and native starts remain. Auditor stays unskilled; cyborg casualties, original CHOAM support and Richese capped marker revelation remain physical. Foreign revival creates no new skill. Native Ixian Technology on Richese lots, own mixed No-Field, captured replacement, Advanced Atreides Suk/KH and unrelated overlays retain their guards.',
'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.',
@@ -4559,7 +4583,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Other special Sandmaster relocation remains missing. Normal Banker income has separate fresh classic or supported native source-to-Mentat previews with provisional original-earned-faction custody, not general activation. Diplomat retreat retains its documented conservative outcome order. Native Advanced foreign gholas/Face Dancers and skill-first Moritani assassination follow their bounded contracts; Ecaz/Duke assignment and other combined modules remain unfinished. This 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'],
related: ['spice-banker-income', 'choam-leader-skills', 'ixian-leader-skills', 'tleilaxu-leader-skills', 'richese-leader-skills', 'moritani-leader-skills', 'setup','choam-modules','implementation-checklist'],
checklist: [
{ area:'Implementation',status:'Partial',detail:'Exact fourteen-card custody, source-ordered setup, public capture, concealment, death and own revival connect five role bonuses, Planetologist movement/Special substitution and Suk Graduate casualty rescue. Rihani inspection/exchange and lower Mentat/Bureaucrat/Sandmaster effects are connected. Smuggler normal reserve shipping, the owned Richese No-Field companion, unmodified battle collection and optional Sandmaster ground-route and native Fremen worm-ride collection are connected. A separately opted-in Mentat preview connects private pre-plan questions and historical observations while ordinary activation awaits the uniform-response decision. Spice Banker lower spending and native Diplomat base Shield/Snooper copying are connected; skilled Diplomat retreat now transfers undialed normal/elite forces before losing casualties in supported battles. Normal Banker income connects only in the separate fresh classic or supported Basic native banker-income profile; other remaining bands and combined modules remain missing. Bureaucrat redirects supported third-party auction, shipment and bribe payments with exact saved once-per-phase use.',evidence:['game/leader-skill-cards.ts','game/leader-skills.ts','game/leader-skill-combat.ts','game/suk-graduate.ts','game/rihani-decipherer.ts','game/mentat-question.ts','game/bureaucrat-payment.ts','game/leader-skill-battle-board.ts','game/smuggler-shipment.ts','game/smuggler-no-field.ts','game/smuggler-battle.ts','game/spice-banker.ts','game/spice-banker-income.ts','game/sandmaster-movement.ts','game/sandmaster-worm.ts','game/diplomat-defense.ts','game/diplomat-retreat.ts','game/engine.ts'] },
{ 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'] },
+3 -10
View File
@@ -1,20 +1,13 @@
import type { Game, GameView } from './engine';
import { quoteSmugglerShipment } from './smuggler-shipment';
import { nativeExpansionLeaderSkillsProfile } from './leader-skill-profile';
/** The No-Field remains the priced marker; this is one separate physical force. */
export type SmugglerNoFieldCompanion = { leader: string; amount: 1 };
export function smugglerNoFieldModeSupported(g: Game | GameView): boolean {
return (
g.expansions.length === 1 &&
g.expansions[0] === 'choam' &&
!g.homeworlds &&
!g.nexusCards &&
!g.discoveries &&
!('discoveryEnabled' in g && g.discoveryEnabled) &&
!g.strongholdCards &&
!g.techTokens
);
return nativeExpansionLeaderSkillsProfile(g) &&
g.players.some(player => player.faction === 'richese');
}
/** Public custody only: the hidden token value never affects this free companion. */