Connect bounded Diplomat retreat and record live deployment

This commit is contained in:
Quintine committed 2026-09-28 09:18:36 +10:00
1 parent 4801ce003b
commit c9a3625ff2
18 files changed
+601 -28

No files matched your search

+1
View File
@@ -25,6 +25,7 @@ const nullableCount = (value: unknown) => value === null || count(value);
const decisions = {
harassWithdraw: true,
diplomatDefense: true,
diplomatRetreat: true,
leaderSkillVisibility: true,
leaderSkillRevival: true,
homeworldRevivalDeployment: true,
+14
View File
@@ -1547,6 +1547,20 @@ function policyActions(g: GameView): Action[] {
stronghold,
}));
}
if (d.kind === 'diplomatRetreat') {
const destination = d.destinations[0];
let choice = destination?.choices[0];
for (const candidate of destination?.choices ?? [])
if (!choice || candidate.normal + candidate.elite > choice.normal + choice.elite)
choice = candidate;
return [{
type: 'decision',
event: d.event,
destination: destination && choice ? destination.location : null,
normal: choice?.normal ?? 0,
elite: choice?.elite ?? 0,
}];
}
if (d.kind === 'diplomatDefense') {
const battle = g.battle;
const opponent = battle
+24
View File
@@ -0,0 +1,24 @@
import { casualtyOptions, type CombatForces } from './combat';
export type RetreatForces = { normal: number; elite: number };
export type DiplomatRetreatSelection = RetreatForces & { destination: string | null };
export type DiplomatRetreatDestination = { location: string; choices: RetreatForces[] };
/** A retreat may take only counters not needed for any legal physical dial commitment. */
export function diplomatRetreatChoices(
forces: CombatForces,
dial: number,
support: number,
strength: number,
available: RetreatForces = forces,
): RetreatForces[] {
if (!Number.isSafeInteger(strength) || strength < 0) return [];
const choices: RetreatForces[] = [];
for (let normal = 0; normal <= Math.min(strength, forces.normal, available.normal); normal++)
for (let elite = 0; elite <= Math.min(strength - normal, forces.elite, available.elite); elite++) {
if (!normal && !elite) continue;
if (casualtyOptions({ ...forces, normal: forces.normal - normal, elite: forces.elite - elite }, dial, support).length)
choices.push({ normal, elite });
}
return choices;
}
+135 -4
View File
@@ -21,6 +21,7 @@ import { quoteSandmasterMovement, validateSandmasterMovement, sandmasterRouteDis
import { sandmasterWormCollection } from './sandmaster-worm';
import { spiceBankerModeSupported, validateSpiceBankerSpend } from './spice-banker';
import { quoteDiplomatDefense, diplomatDefenseModeSupported, type DiplomatDefenseQuote } from './diplomat-defense';
import { diplomatRetreatChoices, type DiplomatRetreatSelection, type DiplomatRetreatDestination } from './diplomat-retreat';
import { ECAZ_START_FORCES, quoteEcazStartingForces } from './ecaz-setup';
import { battleCardSlotEligible, battleCategoryInspectionValue, fixedBattleInspectionMatches, validBattleSlotPair } from './battle-card-slots';
import { HARASS_WITHDRAW_CARD, HarassWithdrawError, isHarassWithdraw, quoteHarassWithdraw, defaultHarassWithdrawAllocation, harassWithdrawNeedsAllocation, type HarassWithdrawSelection, type HarassWithdrawContext, type HarassWithdrawPreview } from './harass-withdraw';
@@ -295,6 +296,7 @@ import {
quoteBattleResolution,
BattleResolutionQuoteError,
type ResolutionCombatant,
type BattleResolutionQuote,
} from './battle-resolution-quote';
import {
quoteRevivalCancellation,
@@ -540,6 +542,7 @@ import {
import {
TERRITORIES,
MOBILE_STRONGHOLD,
MOBILE_LOCATION,
gameTerritories,
gameDistance,
mobileRouteDistance,
@@ -651,6 +654,12 @@ export type Battle = {
diplomatDefenseVersion?: 1;
diplomatDefenseEvent?: string;
diplomatDefense?: DiplomatDefenseReceipt;
/** New battles may pause after the outcome is known, before loser losses. */
diplomatRetreatVersion?: 1;
diplomatRetreat?: {
player: string; event: string; battle: string; turn: number; frame: string;
destinations: DiplomatRetreatDestination[];
};
/** Public face-up/behind-shield choice precedes faction battle powers. */
leaderSkillHidden?: Record<string, boolean>;
nexusInspection?: NexusInspection;
@@ -713,6 +722,7 @@ export type Auction = {
export type Decision =
| { kind: 'harassWithdraw'; player: string; event: string }
| { kind: 'diplomatDefense'; player: string; event: string; cards: string[]; source: string }
| { kind: 'diplomatRetreat'; player: string; event: string; destinations: DiplomatRetreatDestination[] }
| { kind: 'leaderSkillVisibility'; player: string; event: string; resumePowers?: boolean }
| { kind: 'leaderSkillRevival'; player: string; event: string }
| { kind: 'homeworldRevivalDeployment'; player: string; event: string }
@@ -5796,6 +5806,7 @@ function leaderSkillsIntegrity(g: Game) {
bureaucratPaymentIntegrity(g);
mentatQuestionIntegrity(g);
diplomatDefenseIntegrity(g);
diplomatRetreatIntegrity(g);
harassAllocationIntegrity(g);
const noFieldShipment = leaderSkillNoFieldIntegrity(g);
sandmasterIntegrity(g);
@@ -16192,7 +16203,7 @@ function advanceHomeworldReveal(g: Game): boolean {
if (owner) {
requireRule(b.event, 'The Homeworld defense opportunity needs its battle event.');
g.decision = {kind: 'homeworldDefense', player: owner, event: b.event};
} else resolveBattle(g);
} else advanceBattleOutcome(g);
return true;
}
function traitorVoters(g: Game, b: Battle) {
@@ -16352,8 +16363,89 @@ function currentBattleResolutionQuote(g: Game, canceledVoter?: string) {
throw error;
}
}
function resolveBattle(g: Game) {
function diplomatRetreatBlocked(g: Game, key: string) {
const sector = key === MOBILE_LOCATION ? splitLocation(g.mobileStronghold?.location ?? '').sector
: splitLocation(key).sector;
return sector > 0 && sector === g.storm;
}
function diplomatRetreatOffer(g: Game, quote: BattleResolutionQuote) {
const b = g.battle!;
if (b.diplomatRetreatVersion !== 1 || quote.result !== 'normal' || !quote.winner ||
b.territory.startsWith('homeworld:')) return null;
const player = quote.winner === b.attacker ? b.defender : b.attacker;
const p = getPlayer(g, player);
const plan = b.plans[player];
const leader = controlledLeaders(g, p).find(candidate => candidate.id === plan.leader && !candidate.dead);
if (!leader || (player === b.attacker ? quote.leaderDeaths.attacker : quote.leaderDeaths.defender) ||
!battleLeaderSkills(g, p).some(skill => skill.skill === 'diplomat' && skill.leader === leader.id &&
(!skill.faceUp || skill.captured)) || p.noField?.deployed?.location.territory === b.territory) return null;
const opponent = getPlayer(g, quote.winner);
const forces = combatForces(g, p, b.territory, opponent);
const strength = battleLeaderStrength(leader,
controlledLeaders(g, opponent).find(candidate => candidate.id === b.plans[opponent.id].leader));
const origins = Object.entries(p.forces).filter(([key, count]) =>
count > 0 && splitLocation(key).territory === b.territory);
const destinations = TERRITORIES.filter(candidate => candidate.type !== 'stronghold' &&
candidate.id !== b.territory &&
!g.players.some(side => Object.entries(side.forces).some(([key, count]) =>
count > 0 && splitLocation(key).territory === candidate.id)))
.flatMap(candidate => candidate.sectors.map(sector => location(candidate.id, sector)))
.sort()
.map(key => {
const available = origins.reduce((sum, [origin, count]) => {
if (gameDistance(g, origin, key, candidate => diplomatRetreatBlocked(g, candidate)) !== 1) return sum;
const elite = p.elites?.forces[origin] ?? 0;
return { normal: sum.normal + count - elite, elite: sum.elite + elite };
}, { normal: 0, elite: 0 });
return { location: key, choices: diplomatRetreatChoices(forces, plan.dial, plan.support, strength, available) };
})
.filter(option => option.choices.length);
return destinations.length ? { player, destinations } : null;
}
function diplomatRetreatFrame(g: Game) {
const b = g.battle!;
return JSON.stringify({ battle: b.event, turn: g.turn, territory: b.territory,
plans: b.plans, hidden: b.leaderSkillHidden, traitors: b.traitorCalls,
forces: g.players.map(p => [p.id, p.forces, p.elites?.forces]),
skills: g.leaderSkills?.assignments, storm: g.storm });
}
function diplomatRetreatIntegrity(g: Game) {
const b = g.battle, receipt = b?.diplomatRetreat;
const decision = g.decision;
if (!receipt) {
requireRule(decision?.kind !== 'diplomatRetreat', 'The Diplomat retreat lost its battle.');
return;
}
requireRule(b?.diplomatRetreatVersion === 1 && g.status === 'playing' && g.phase === 6 &&
b.revealed && b.event === receipt.battle && g.turn === receipt.turn &&
receipt.event && receipt.frame === diplomatRetreatFrame(g) &&
JSON.stringify(diplomatRetreatOffer(g, currentBattleResolutionQuote(g))) ===
JSON.stringify({ player: receipt.player, destinations: receipt.destinations }) &&
decision?.kind === 'diplomatRetreat' && decision.event === receipt.event &&
decision.player === receipt.player &&
JSON.stringify(decision.destinations) === JSON.stringify(receipt.destinations),
'The saved Diplomat retreat changed its battle, forces, outcome or legal destinations.');
}
function advanceBattleOutcome(g: Game) {
const b = g.battle!;
if (b.diplomatRetreatVersion !== 1 || ![b.attacker, b.defender].some(id =>
battleLeaderSkills(g, getPlayer(g, id)).some(skill => skill.skill === 'diplomat' &&
b.plans[id].leader === skill.leader && (!skill.faceUp || skill.captured)))) {
resolveBattle(g);
return;
}
const offer = diplomatRetreatOffer(g, currentBattleResolutionQuote(g));
if (!offer) { resolveBattle(g); return; }
requireRule(b.event && !b.diplomatRetreat, 'The Diplomat retreat needs one current battle.');
const event = crypto.randomUUID();
b.diplomatRetreat = { ...offer, event, battle: b.event, turn: g.turn, frame: diplomatRetreatFrame(g) };
g.decision = { kind: 'diplomatRetreat', event, ...offer };
log(g, `${getPlayer(g, offer.player).name} may retreat surviving undialed forces with Diplomat before battle losses.`,
{ faction: getPlayer(g, offer.player).faction, name: 'Diplomat retreat' });
}
function resolveBattle(g: Game, retreat?: DiplomatRetreatSelection) {
const b = g.battle!;
requireRule(!!b.diplomatRetreat === !!retreat, 'Resolve the pending Diplomat retreat before battle losses.');
const quote = currentBattleResolutionQuote(g);
const a = getPlayer(g, b.attacker),
d = getPlayer(g, b.defender),
@@ -16522,6 +16614,30 @@ function resolveBattle(g: Game) {
log(g, `${player.name} gained ${bonus.amount} battle strength from ${leaderSkillCard(bonus.skill).name}${bonus.mode === 'normal' ? ' while their skilled leader remained face up' : ' by using their surviving skilled leader'}.`, {faction:player.faction,name:'Leader Skill'});
const loser = winner === a ? d : a;
if (quote.bounty) winner!.spice += quote.bounty.amount;
if (retreat?.destination) {
let normal = retreat.normal, elite = retreat.elite;
for (const key of Object.keys(loser.forces).sort()) {
if (splitLocation(key).territory !== b.territory || (!normal && !elite) ||
gameDistance(g, key, retreat.destination, candidate => diplomatRetreatBlocked(g, candidate)) !== 1) continue;
const availableElite = loser.elites?.forces[key] ?? 0;
const takenNormal = Math.min(normal, loser.forces[key] - availableElite);
const takenElite = Math.min(elite, availableElite);
loser.forces[key] -= takenNormal + takenElite;
if (!loser.forces[key]) delete loser.forces[key];
if (takenElite && loser.elites) {
loser.elites.forces[key] -= takenElite;
if (!loser.elites.forces[key]) delete loser.elites.forces[key];
}
normal -= takenNormal; elite -= takenElite;
}
requireRule(!normal && !elite, 'The Diplomat retreat lost its undialed physical counters.');
loser.forces[retreat.destination] = (loser.forces[retreat.destination] ?? 0) + retreat.normal + retreat.elite;
if (retreat.elite && loser.elites)
loser.elites.forces[retreat.destination] = (loser.elites.forces[retreat.destination] ?? 0) + retreat.elite;
log(g, `${loser.name} retreated ${retreat.normal} normal and ${retreat.elite} elite undialed forces with Diplomat to ${territory(splitLocation(retreat.destination).territory).name}.`,
{ faction: loser.faction, name: 'Diplomat retreat' });
observeOccupation(g);
}
killTerritory(g, loser, b.territory, Infinity, true);
observeOccupation(g);
if (quote.basicWinnerLosses !== null)
@@ -19079,7 +19195,7 @@ function finishResponse(g: Game, canceled: boolean,bureaucratDiversion?:number)
if (
traitorVoters(g, b).every((voter) => b.traitorCalls[voter] !== undefined)
)
resolveBattle(g);
advanceBattleOutcome(g);
} else if (response.kind === 'richesePurchaseIncome') {
const pending = g.pendingRichesePurchaseIncome;
requireRule(
@@ -22708,6 +22824,20 @@ function applyActionInner(
if (harkonnen) returnCaptives(g, harkonnen);
settleAdvisors(g);
finishBattle(g);
} else if (decision.kind === 'diplomatRetreat') {
const receipt = g.battle?.diplomatRetreat;
const destination = action.destination;
const normal = action.normal, elite = action.elite;
requireRule(Object.keys(action).every(key => ['type', 'event', 'destination', 'normal', 'elite'].includes(key)) &&
receipt?.event === decision.event && action.event === decision.event && receipt.player === id &&
(destination === null && normal === 0 && elite === 0 ||
typeof destination === 'string' && receipt.destinations.some(option => option.location === destination &&
option.choices.some(choice => choice.normal === normal && choice.elite === elite))),
'Choose a legal empty adjacent territory and undialed force count, or decline this Diplomat retreat.');
g.decision = null;
if (destination === null)
log(g, `${p.name} declined the Diplomat retreat.`, { faction: p.faction, name: 'Diplomat retreat' });
resolveBattle(g, { destination: destination as string | null, normal: normal as number, elite: elite as number });
} else if (decision.kind === 'harassWithdraw') {
const receipt = g.battle?.harassAllocation, offer = harassAllocationOffer(g);
requireRule(Object.keys(action).every(key => ['type', 'event', 'returns'].includes(key)) &&
@@ -24916,6 +25046,7 @@ function applyActionInner(
...(g.leaderSkills ? { smugglerCollectionVersion: 1 as const } : {}),
...(g.mentatQuestionPreview === true && g.leaderSkills && mentatQuestionModeSupported(g) ? { mentatQuestionVersion: 1 as const } : {}),
...(g.leaderSkills && diplomatDefenseModeSupported(g) ? { diplomatDefenseVersion: 1 as const } : {}),
...(g.leaderSkills && ordinaryLeaderSkillModeSupported(g) ? { diplomatRetreatVersion: 1 as const } : {}),
...(g.ecazTreachery ? { harassAllocationVersion: 1 as const } : {}),
...(byFaction(g, 'richese')
? { preLeader: { event: battleEvent, ready: [], closed: false } }
@@ -25149,7 +25280,7 @@ function applyActionInner(
if (
traitorVoters(g, b).every((voter) => b.traitorCalls[voter] !== undefined)
)
if (!b.territory.startsWith('homeworld:')) resolveBattle(g);
if (!b.territory.startsWith('homeworld:')) advanceBattleOutcome(g);
else advanceHomeworldReveal(g);
return g;
}
+6 -6
View File
@@ -4168,20 +4168,20 @@ export const RULE_TOPICS: RuleTopic[] = [
'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 remains unfinished.',
'Diplomat may use one committed Worthless card as the opponent’s base Shield or Snooper if you played no defense. After both plans reveal, choose the card or decline before traitor decisions. A native Diplomat may protect its own selected trained leader this way; a captive grants no upper effect. The chosen Worthless must be discarded afterward, even if you win. Lower retreat and modified defenses remain unfinished.',
'Diplomat may use one committed Worthless card as the opponent’s base Shield or Snooper if you played no defense. After both plans reveal, choose the card or decline before traitor decisions. A native Diplomat may protect its own selected trained leader this way; a captive grants no upper effect. The chosen Worthless must be discarded afterward, even if you win. A surviving trained leader that loses a supported battle may retreat selected undialed physical counters up to its printed/copied disc strength into one empty adjacent non-stronghold territory before ordinary loser losses; modified defenses remain unfinished.',
'Mentat has a private developer preview for naming a specific weapon before posture and faction powers. The target privately shows the named held card or chooses another held card. It stays in hand 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 records 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 and Richese sales have connected payment paths; split funding, 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, Spice Banker normal income and Diplomat retreat remain missing. Foreign gholas, Face Dancers and combined modules remain unfinished. This development prototype does not enable public module starts.',
'Other special Sandmaster relocation and Spice Banker normal income remain missing. Diplomat retreat uses a documented conservative outcome-order interpretation in standalone Leader Skills play. Foreign gholas, Face Dancers and combined modules remain unfinished. This development prototype does not enable public module starts.',
...LEADER_SKILL_CARDS.map(card => `${card.name}. Normal: ${card.normal.join(' ')} Skilled battle: ${card.battle.join(' ')}`),
],
related: ['choam-leader-skills', 'ixian-leader-skills', 'tleilaxu-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; Spice Banker normal income, other remaining bands, Diplomat retreat 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/sandmaster-movement.ts','game/sandmaster-worm.ts','game/diplomat-defense.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. 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/game-table.tsx'] },
{ area:'AI',status:'Partial',detail:'All four profiles use private offers, legal visibility/revival actions and Planetologist movement/battle candidates and legal Suk Graduate rescues, Rihani exchanges, shared battle scoring, Smuggler reserve/owned No-Field shipments and unmodified collection plans, Sandmaster route and worm collection, funded Banker commitments and optional Diplomat copying from public revealed defenses. Bureaucrat uses the public payee alliance to redirect enemy income or preserve allied income. These are legal prototype paths; complete skill strategy and difficulty calibration remain unfinished.',evidence:['game/bots.ts'] },
{ area:'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. Spice Banker normal income, 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/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'] },
{ area:'AI',status:'Partial',detail:'All four profiles use private offers, legal visibility/revival actions and Planetologist movement/battle candidates and legal Suk Graduate rescues, Rihani exchanges, shared battle scoring, Smuggler reserve/owned No-Field shipments and unmodified collection plans, Sandmaster route and worm collection, funded Banker commitments and optional Diplomat copying from public revealed defenses or choosing legal losing-battle retreat. Bureaucrat uses the public payee alliance to redirect enemy income or preserve allied income. These are legal prototype paths; complete skill strategy and difficulty calibration remain unfinished.',evidence:['game/bots.ts'] },
{ area:'Documentation',status:'Partial',detail:'All fourteen faces and common rules are recorded. Skilled capture follows public physical-card custody; replacement entitlement and other material timing interpretations remain explicit.',evidence:['docs/LEADER_SKILLS_RULES.md','docs/LEADER_SKILLS_CAPTURE.md','docs/LEADER_SKILLS_RUNTIME.md','docs/PLANETOLOGIST_RULES.md','docs/SUK_GRADUATE_RULES.md','docs/LEADER_BATTLE_EFFECTS.md','docs/MENTAT_QUESTION.md','docs/BUREAUCRAT_PAYMENTS.md','docs/SMUGGLER_SHIPMENT.md','docs/SPICE_BANKER_RUNTIME.md','docs/SANDMASTER_WORM.md','docs/DIPLOMAT_DEFENSE.md'] },
{ area:'Verification',status:'Partial',detail:'Focused inventory, scoring, Voice, capture, saved private setup/revival Planetologist movement/cancellation and Suk Graduate casualty/SQLite checks, plus Rihani privacy/concurrent recovery battle-effect regressions and Smuggler shipment and battle collection, losing/captured survival, Sandmaster order, explosion, legacy and concurrent recovery, and Banker funding, death, traitor, privacy and concurrent settlement checks cover this boundary. Diplomat checks cover self-protection, mandatory discard, public-choice privacy, concurrent saved decisions and legacy continuation. Bureaucrat checks cover once-per-phase use, private source receipts and resumed payment custody. Sandmaster worm checks cover typed forces, decline, saved arrival reactions, concurrent writes, repeated distinct rides and special-Karama origin. Sample games and browser continuation are recorded separately; full module acceptance remains open.',evidence:['tests/leader-skills-engine.test.ts','tests/leader-skills-capture.test.ts','tests/leader-skills-recovery.test.ts','tests/leader-skills-controls.test.ts','tests/suk-graduate.test.ts','tests/suk-graduate-recovery.test.ts','tests/leader-battle-effects.test.ts','tests/leader-battle-effects-recovery.test.ts','tests/leader-battle-effects-controls.test.tsx','tests/mentat-question.test.ts','tests/mentat-question-bots.test.ts','tests/mentat-question-controls.test.tsx','tests/mentat-question-recovery.test.ts','tests/bureaucrat-payment.test.ts','tests/bureaucrat-payment-controls.test.tsx','tests/bureaucrat-payment-bots.test.ts','tests/bureaucrat-payment-recovery.test.ts','tests/smuggler-battle.test.ts','tests/smuggler-battle-recovery.test.ts','tests/smuggler-shipment.test.ts','tests/smuggler-shipment-controls.test.tsx','tests/smuggler-shipment-recovery.test.ts','tests/smuggler-no-field.test.ts','tests/smuggler-no-field-controls.test.tsx','tests/smuggler-no-field-recovery.test.ts','tests/spice-banker.test.ts','tests/spice-banker-recovery.test.ts','tests/spice-banker-controls.test.tsx','tests/sandmaster-movement.test.ts','tests/sandmaster-movement-recovery.test.ts','tests/sandmaster-movement-controls.test.tsx','tests/sandmaster-worm.test.ts','tests/sandmaster-worm-controls.test.tsx','tests/sandmaster-worm-recovery.test.ts','tests/diplomat-defense.test.ts','tests/diplomat-defense-controls.test.tsx','tests/diplomat-defense-recovery.test.ts'] },
{ area:'Verification',status:'Partial',detail:'Focused inventory, scoring, Voice, capture, saved private setup/revival Planetologist movement/cancellation and Suk Graduate casualty/SQLite checks, plus Rihani privacy/concurrent recovery battle-effect regressions and Smuggler shipment and battle collection, losing/captured survival, Sandmaster order, explosion, legacy and concurrent recovery, and Banker funding, death, traitor, privacy and concurrent settlement checks cover this boundary. Diplomat checks cover self-protection, mandatory discard, public-choice privacy, concurrent saved decisions, legacy continuation, losing-battle ordinary/elite retreat and concurrent saved force custody. Bureaucrat checks cover once-per-phase use, private source receipts and resumed payment custody. Sandmaster worm checks cover typed forces, decline, saved arrival reactions, concurrent writes, repeated distinct rides and special-Karama origin. Sample games and browser continuation are recorded separately; full module acceptance remains open.',evidence:['tests/leader-skills-engine.test.ts','tests/leader-skills-capture.test.ts','tests/leader-skills-recovery.test.ts','tests/leader-skills-controls.test.ts','tests/suk-graduate.test.ts','tests/suk-graduate-recovery.test.ts','tests/leader-battle-effects.test.ts','tests/leader-battle-effects-recovery.test.ts','tests/leader-battle-effects-controls.test.tsx','tests/mentat-question.test.ts','tests/mentat-question-bots.test.ts','tests/mentat-question-controls.test.tsx','tests/mentat-question-recovery.test.ts','tests/bureaucrat-payment.test.ts','tests/bureaucrat-payment-controls.test.tsx','tests/bureaucrat-payment-bots.test.ts','tests/bureaucrat-payment-recovery.test.ts','tests/smuggler-battle.test.ts','tests/smuggler-battle-recovery.test.ts','tests/smuggler-shipment.test.ts','tests/smuggler-shipment-controls.test.tsx','tests/smuggler-shipment-recovery.test.ts','tests/smuggler-no-field.test.ts','tests/smuggler-no-field-controls.test.tsx','tests/smuggler-no-field-recovery.test.ts','tests/spice-banker.test.ts','tests/spice-banker-recovery.test.ts','tests/spice-banker-controls.test.tsx','tests/sandmaster-movement.test.ts','tests/sandmaster-movement-recovery.test.ts','tests/sandmaster-movement-controls.test.tsx','tests/sandmaster-worm.test.ts','tests/sandmaster-worm-controls.test.tsx','tests/sandmaster-worm-recovery.test.ts','tests/diplomat-defense.test.ts','tests/diplomat-defense-controls.test.tsx','tests/diplomat-defense-recovery.test.ts','tests/diplomat-retreat-engine.test.ts','tests/diplomat-retreat-controls.test.tsx','tests/diplomat-retreat-recovery.test.ts'] },
],
},
{
+1
View File
@@ -12,6 +12,7 @@ const DECISIONS = {
sukRescue: true,
rihani: true,
diplomatDefense: true,
diplomatRetreat: true,
harassWithdraw: true,
choamAudit: true,
choamAuditPayment: true,