Prototype native Diplomat base-defense copying

Allow one committed Worthless to copy an opposing base Shield or Snooper when no actual defense is played, including protection of the selected native trained leader. Expose an optional named public choice after both plans reveal, before traitor calls, and show the activated protection in revealed plans. Discard the chosen physical Worthless once after battle, including winner and traitor-preempted outcomes. Original physical card identities and plans stay intact. Timing and explicit-discard precedence are documented implementation inferences; lower retreat, modified defenses, combined modes and public starts remain guarded.

Reuse legal Worthless planning candidates and add a public-information decision path for all four AI profiles. Bind new battle choices to revealed plans, public skill stance, canonical card custody and an independent event, validate saved continuations before reads/actions, omit internal proof fields from views, and retain unmarked legacy-battle behavior. Independent rules/privacy/persistence review found no blocker.

Validation: focused Diplomat rules/UI/bot/JSON/SQLite checks plus 80 related legacy checks passed; typecheck, lint, 4537 offline tests, production build and 43 HTTP tests passed. Concurrent copy versus decline, stale/corrupt decisions, self-protection and mandatory traitor-winner discard are covered. Browser genuine assignment and a conserved staged battle exercised human plan submission, refresh of the pending choice, actual Snooper copying with Baliset, Hasimir survival, exactly one Baliset discard, ordinary Crysknife retention and final refresh with private cards and four board forces, fifteen reserves, one Tank and eight spice. Basic/Advanced four-profile samples completed in 210/223 actions with six/seven JSON restores and no rejected candidates; neither encountered an eligible copy, so targeted checks establish the effect.

The server was healthy after the power outage. All 563 opening rooms and original seat/recovery records were preserved (597 after QA/HTTP); SQLite integrity and foreign keys passed. No restart, reset or automation was needed.

Verified source tree: 868bb7484cef244e6ab20f8d08d62e118629e257870cf91a436bf42651b968f5
This commit is contained in:
Quintine committed 2026-09-13 22:05:03 +10:00
1 parent 788453e07e
commit f067950f5e
21 files changed
+1463 -25

No files matched your search

+24 -5
View File
@@ -31,6 +31,7 @@ import { auditCount } from './choam-auditor';
import { sukGraduateSkill, type SukGraduateSkill } from './suk-graduate';
import { validateSpiceBankerSpend } from './spice-banker';
import { rihaniVictorySkill, type RihaniSkill } from './rihani-decipherer';
import { copiedDiplomatDefense, quoteDiplomatDefense, type DiplomatDefenseQuote } from './diplomat-defense';
import {
quoteHomeworldBattleRules,
type HomeworldBattleRules,
@@ -71,6 +72,7 @@ export type ResolutionParticipant = {
noFieldAtTerritory?: boolean;
};
export type ResolutionCombatant = ResolutionParticipant & {
diplomatDefense?: Pick<DiplomatDefenseQuote, 'leader' | 'source' | 'kind'> & { card: string };
spice: number;
hand: readonly Card[];
plan: ResolutionPlan;
@@ -346,6 +348,19 @@ function calculate(input: BattleResolutionInput): BattleResolutionQuote {
dw = card(d, d.plan.weapon),
ad = card(a, a.lateDefense ?? a.plan.defense),
dd = card(d, d.lateDefense ?? d.plan.defense);
const effectiveDefense = (side: ResolutionCombatant, other: ResolutionCombatant, defense: Card | undefined) => {
const copy = side.diplomatDefense;
if (copy === undefined) return defense;
const quote = quoteDiplomatDefense({ assignments: side.leaderSkills ?? [], selectedLeader: side.plan.leader,
weapon: card(side, side.plan.weapon), defense: card(side, side.plan.defense),
opposingDefense: card(other, other.plan.defense) });
requireQuote(copy && quote && !side.lateDefense && !other.lateDefense &&
!side.stronghold && !other.stronghold && !input.homeworld &&
copy.leader === quote.leader && copy.source === quote.source && copy.kind === quote.kind && quote.cards.includes(copy.card),
'The copied Diplomat defense lost its native trainer, committed Worthless or opposing base defense.');
return copiedDiplomatDefense(quote, copy.card);
};
const effectiveAd = effectiveDefense(a, d, ad), effectiveDd = effectiveDefense(d, a, dd);
const tie =
d.stronghold === 'habbanya_ridge_sietch' ? 'defender' : 'attacker';
const stone =
@@ -511,9 +526,9 @@ function calculate(input: BattleResolutionInput): BattleResolutionQuote {
} else {
effects = strongholdBattleEffects(
aw,
ad,
effectiveAd,
dw,
dd,
effectiveDd,
a.poisonTooth,
d.poisonTooth,
a.stronghold,
@@ -536,6 +551,7 @@ function calculate(input: BattleResolutionInput): BattleResolutionQuote {
selectedLeader: selectedLeader(a),
weapon: aw,
defense: ad,
effectiveDefense: effectiveAd,
skilledLeaderSurvives: !deaths.attacker,
bankerSpice: a.plan.bankerSpice,
}),
@@ -544,6 +560,7 @@ function calculate(input: BattleResolutionInput): BattleResolutionQuote {
selectedLeader: selectedLeader(d),
weapon: dw,
defense: dd,
effectiveDefense: effectiveDd,
skilledLeaderSurvives: !deaths.defender,
bankerSpice: d.plan.bankerSpice,
}),
@@ -697,12 +714,14 @@ function calculate(input: BattleResolutionInput): BattleResolutionQuote {
selectedLeader: side.plan.leader,
card: card(side, selected),
});
const mandatorySkillDiscard = (side: ResolutionCombatant, selected: string) =>
mandatoryPlanetologistDiscard(side, selected) || side.diplomatDefense?.card === selected;
const moritani = input.participants.find((p) => p.faction === 'moritani');
if (winner && moritani) {
const loser = winner === a ? d : a,
loserPlayed = loser === a ? played.attacker : played.defender;
const retainablePlayed = loserPlayed.filter(
(selected) => !mandatoryPlanetologistDiscard(loser, selected),
(selected) => !mandatorySkillDiscard(loser, selected),
);
const eligible = retainablePlayed.filter(
(selected) =>
@@ -740,9 +759,9 @@ function calculate(input: BattleResolutionInput): BattleResolutionQuote {
!(
retention?.player === s.id &&
retention.played.includes(selected) &&
!mandatoryPlanetologistDiscard(s, selected)
!mandatorySkillDiscard(s, selected)
) &&
(mandatoryPlanetologistDiscard(s, selected) ||
(mandatorySkillDiscard(s, selected) ||
winner !== s ||
!canRetainBattleCard(
card(s, selected)!,
+26
View File
@@ -1481,6 +1481,32 @@ function policyActions(g: GameView): Action[] {
stronghold,
}));
}
if (d.kind === 'diplomatDefense') {
const battle = g.battle;
const opponent = battle
? battle.attacker === me.id
? battle.defender
: battle.attacker
: null;
const enemyWeapon = battle?.cards.find(
(card) => card.id === (opponent ? battle.plans[opponent]?.weapon : null),
);
const copiedDefense = battle?.cards.find(
(card) => card.id === d.source,
);
const useful =
!!enemyWeapon &&
!!copiedDefense &&
weaponKills(enemyWeapon) &&
!weaponKills(enemyWeapon, copiedDefense);
return [
{
type: 'decision',
event: d.event,
card: useful ? (d.cards[0] ?? null) : null,
},
];
}
if (d.kind === 'richeseAllyOpportunity') {
const offer = alliedNoFieldOffer(g);
return offer
+90
View File
@@ -0,0 +1,90 @@
import { baseDeck, type Card } from './cards';
import type { BattleLeaderSkill } from './leader-skill-combat';
export type DiplomatDefenseQuote = {
leader: string;
cards: string[];
source: string;
kind: 'shield' | 'snooper';
};
/** Planning eligibility does not inspect the opponent's unsubmitted cards. */
export function diplomatTrainer(
assignments: readonly BattleLeaderSkill[],
selectedLeader: string | null | undefined,
): BattleLeaderSkill | undefined {
return assignments.find(
(a) =>
a.skill === 'diplomat' &&
!a.captured &&
(a.faceUp || a.leader === selectedLeader),
);
}
const baseDefenses = new Map(
baseDeck()
.filter((card) => card.kind === 'shield' || card.kind === 'snooper')
.map((card) => [card.id, card]),
);
/** A copied role does not change either physical card or either sealed plan. */
export function quoteDiplomatDefense(input: {
assignments: readonly BattleLeaderSkill[];
selectedLeader: string | null | undefined;
weapon?: Card;
defense?: Card;
opposingDefense?: Card;
}): DiplomatDefenseQuote | null {
const assignment = diplomatTrainer(input.assignments, input.selectedLeader);
if (!assignment || (input.defense && input.defense.kind !== 'worthless'))
return null;
const source = input.opposingDefense;
const printed = source && baseDefenses.get(source.id);
if (
!source ||
!printed ||
printed.kind !== source.kind ||
printed.name !== source.name
)
return null;
const cards = [input.weapon, input.defense]
.filter((card): card is Card => card?.kind === 'worthless')
.map((card) => card.id);
if (!cards.length || new Set(cards).size !== cards.length) return null;
return {
leader: assignment.leader,
cards,
source: source.id,
kind: source.kind as 'shield' | 'snooper',
};
}
/** Native skill inputs must already exclude dead or foreign-ghola trainers. */
export function copiedDiplomatDefense(
quote: DiplomatDefenseQuote,
card: string,
): Card {
if (!quote.cards.includes(card))
throw new Error('Choose one committed Worthless card for Diplomat.');
return { id: card, name: 'Diplomat defense', kind: quote.kind };
}
export function diplomatDefenseModeSupported(game: {
expansions: readonly string[];
homeworlds?: unknown;
nexusCards?: unknown;
discoveries?: unknown;
discoveryEnabled?: unknown;
strongholdCards?: unknown;
techTokens?: unknown;
}): boolean {
return (
!game.expansions.length &&
!game.homeworlds &&
!game.nexusCards &&
!game.discoveries &&
!game.discoveryEnabled &&
!game.strongholdCards &&
!game.techTokens
);
}
+130
View File
@@ -14,6 +14,7 @@ import { quoteSmugglerNoField, smugglerNoFieldModeSupported, type SmugglerNoFiel
import { quoteSmugglerShipment, type SmugglerShipment } from './smuggler-shipment';
import { quoteSandmasterMovement, validateSandmasterMovement, type SandmasterMovement, type SandmasterOrder } from './sandmaster-movement';
import { spiceBankerModeSupported, validateSpiceBankerSpend } from './spice-banker';
import { quoteDiplomatDefense, diplomatDefenseModeSupported, type DiplomatDefenseQuote } from './diplomat-defense';
import { discoveryChoices, discoveryStashSignature, type DiscoveryStash } from './discovery-actions';
import { createStormSource, validateStormSource, discoveryStormOffer, createDiscoveryStorm, chooseDiscoveryStorm, validateDiscoveryStorm, finishDiscoveryStorm, type StormMovementSource, type DiscoveryStorm } from './discovery-storm';
import { completeDiscoveryFlight, discoveryFlightOffer, quoteDiscoveryFlight, validateDiscoveryFlight, type DiscoveryFlightReceipt } from './discovery-flight';
@@ -578,7 +579,23 @@ export type PlanField = Exclude<
keyof Plan,
'support' | 'kwisatz' | 'allyPayment' | 'bankerSpice'
>;
export type DiplomatDefenseReceipt = DiplomatDefenseQuote & {
event: string;
player: string;
battle: string;
turn: number;
territory: string;
/** Only the revealed plans, played cards and public skill stance are bound. */
frame: string;
stage: 'offered' | 'copied' | 'declined';
card: string | null;
signature: string;
};
export type Battle = {
/** New battles opt into the revealed Diplomat step; legacy battles are not reopened. */
diplomatDefenseVersion?: 1;
diplomatDefenseEvent?: string;
diplomatDefense?: DiplomatDefenseReceipt;
/** Public face-up/behind-shield choice precedes faction battle powers. */
leaderSkillHidden?: Record<string, boolean>;
nexusInspection?: NexusInspection;
@@ -639,6 +656,7 @@ export type Auction = {
allyPayment?: number;
};
export type Decision =
| { kind: 'diplomatDefense'; player: string; event: string; cards: string[]; source: string }
| { kind: 'leaderSkillVisibility'; player: string; event: string; resumePowers?: boolean }
| { kind: 'leaderSkillRevival'; player: string; event: string }
| { kind: 'homeworldRevivalDeployment'; player: string; event: string }
@@ -4927,6 +4945,7 @@ function leaderSkillAssignmentUnavailable(
: null;
}
function leaderSkillsIntegrity(g: Game) {
diplomatDefenseIntegrity(g);
const noFieldShipment = leaderSkillNoFieldIntegrity(g);
sandmasterIntegrity(g);
sukRescueIntegrity(g);
@@ -14072,8 +14091,95 @@ function stoneBurnerIntegrity(g: Game) {
);
}
}
function diplomatDefenseOffer(g: Game): (DiplomatDefenseQuote & { player: string }) | null {
const b = g.battle;
if (!b?.revealed || !g.leaderSkills || !diplomatDefenseModeSupported(g) ||
b.lateDefense && Object.keys(b.lateDefense).length) return null;
for (const player of [b.attacker, b.defender]) {
const p = getPlayer(g, player), plan = b.plans[player];
const other = getPlayer(g, b.attacker === player ? b.defender : b.attacker);
const opposingPlan = b.plans[other.id];
if (!plan || !opposingPlan) continue;
const quote = quoteDiplomatDefense({
assignments: g.leaderSkills.assignments.filter(a => a.owner === player && a.skill === 'diplomat' &&
p.leaders.some(l => l.id === a.leader && !l.dead && !l.capturedBy && !l.gholaBy))
.map(a => ({ skill: a.skill, leader: a.leader, faceUp: !b.leaderSkillHidden?.[player], captured: false })),
selectedLeader: plan.leader,
weapon: cardOf(p, plan.weapon), defense: cardOf(p, plan.defense),
opposingDefense: cardOf(other, opposingPlan.defense),
});
if (quote) return { ...quote, player };
}
return null;
}
function diplomatDefenseFrame(g: Game): string {
const b = g.battle!;
return JSON.stringify({ event: b.event, turn: g.turn, territory: b.territory,
attacker: b.attacker, defender: b.defender, plans: b.plans, hidden: b.leaderSkillHidden,
cards: [b.attacker, b.defender].map(id => {
const p = getPlayer(g, id), plan = b.plans[id];
return [plan.weapon, plan.defense, plan.leader].map(selected => cardOf(p, selected) ?? null);
}) });
}
function diplomatDefenseSignature(receipt: DiplomatDefenseReceipt): string {
return JSON.stringify({ ...receipt, signature: undefined });
}
function diplomatDefenseIntegrity(g: Game) {
const b = g.battle;
const decisions = homeworldSavedDecisions(g).filter(d => d.kind === 'diplomatDefense');
if (b?.diplomatDefenseVersion === undefined && b?.diplomatDefense === undefined &&
b?.diplomatDefenseEvent === undefined && !decisions.length) return;
requireRule(b && b.diplomatDefenseVersion === 1,
'The saved Diplomat choice lost its original battle version.');
const receipt = b.diplomatDefense;
const offer = diplomatDefenseOffer(g);
if (!receipt) {
requireRule(receipt === undefined && b.diplomatDefenseEvent === undefined && !decisions.length && !offer,
'The revealed Diplomat defense lost its original choice.');
return;
}
requireRule(g.status === 'playing' && g.phase === 6 && b.revealed && offer &&
receipt.battle === b.event && receipt.turn === g.turn && receipt.territory === b.territory &&
typeof receipt.event === 'string' && receipt.event.length > 0 && receipt.event === b.diplomatDefenseEvent &&
receipt.frame === diplomatDefenseFrame(g) && receipt.signature === diplomatDefenseSignature(receipt) &&
JSON.stringify({leader: receipt.leader, cards: receipt.cards, source: receipt.source, kind: receipt.kind, player: receipt.player}) === JSON.stringify(offer),
'The saved Diplomat defense changed its revealed plans, trainer, source or physical card choices.');
requireRule(['offered', 'copied', 'declined'].includes(receipt.stage) &&
(receipt.stage === 'copied' ? typeof receipt.card === 'string' && receipt.cards.includes(receipt.card) : receipt.card === null),
'The saved Diplomat defense changed its optional choice or selected Worthless card.');
const physical = physicalTreacheryCards(g);
for (const card of [...receipt.cards, receipt.source]) requireRule(physical.filter(c => c.id === card).length === 1,
'The committed Diplomat cards must retain unique physical custody.');
requireRule(decisions.length === (receipt.stage === 'offered' ? 1 : 0),
'The saved Diplomat defense lost or reopened its unique public decision.');
if (receipt.stage === 'offered') requireRule(Object.keys(b.traitorCalls).length === 0,
'Diplomat defense must be chosen before any traitor decisions.');
for (const decision of decisions) requireRule(decision.player === receipt.player &&
decision.event === receipt.event && decision.source === receipt.source &&
JSON.stringify(decision.cards) === JSON.stringify(receipt.cards),
'The Diplomat decision no longer matches its original public choices.');
}
function offerDiplomatDefense(g: Game): boolean {
const b = g.battle!;
if (b.diplomatDefenseVersion !== 1 || b.diplomatDefense) return false;
const offer = diplomatDefenseOffer(g);
if (!offer) return false;
requireRule(b.event && !Object.keys(b.traitorCalls).length,
'Diplomat defense needs the current revealed battle before traitor decisions.');
const event = crypto.randomUUID();
const receipt: DiplomatDefenseReceipt = { ...offer, event, battle: b.event, turn: g.turn,
territory: b.territory, frame: diplomatDefenseFrame(g), stage: 'offered', card: null, signature: '' };
receipt.signature = diplomatDefenseSignature(receipt);
b.diplomatDefense = receipt;
b.diplomatDefenseEvent = event;
g.decision = { kind: 'diplomatDefense', player: offer.player, event, cards: [...offer.cards], source: offer.source };
log(g, `${getPlayer(g, offer.player).name} may use Diplomat to make one committed Worthless card copy the opposing ${offer.kind === 'shield' ? 'Shield' : 'Snooper'} before traitor decisions.`,
{ faction: getPlayer(g, offer.player).faction, name: 'Diplomat defense' });
return true;
}
function nextRevealedDecision(g: Game) {
const b = g.battle!;
if (offerDiplomatDefense(g)) return;
const stoneOwner = [b.attacker, b.defender].find(
(id) =>
isStoneBurner(cardOf(getPlayer(g, id), b.plans[id].weapon)) &&
@@ -14150,6 +14256,9 @@ function currentBattleResolutionQuote(g: Game, canceledVoter?: string) {
forces: combatForces(g, p, b.territory, opponent),
stronghold: strongholdEffect(g, p.id),
lateDefense: b.lateDefense?.[p.id],
...(b.diplomatDefense?.stage === 'copied' && b.diplomatDefense.player === p.id
? { diplomatDefense: { leader: b.diplomatDefense.leader, card: b.diplomatDefense.card!,
source: b.diplomatDefense.source, kind: b.diplomatDefense.kind } } : {}),
poisonTooth: b.poisonTooth?.[p.id],
stoneMode: b.stoneBurner?.[p.id],
...(aid ? { aid: { donor: p.ally!, amount: aid.amount } } : {}),
@@ -20350,6 +20459,20 @@ function applyActionInner(
if (harkonnen) returnCaptives(g, harkonnen);
settleAdvisors(g);
finishBattle(g);
} else if (decision.kind === 'diplomatDefense') {
const receipt = g.battle?.diplomatDefense;
requireRule(Object.keys(action).every(key => ['type', 'event', 'card'].includes(key)) &&
action.event === decision.event && receipt?.event === decision.event && receipt.player === id &&
receipt.stage === 'offered' && (action.card === null || typeof action.card === 'string' && receipt.cards.includes(action.card)),
'Choose one offered Worthless card or decline this exact Diplomat defense.');
receipt.card = action.card as string | null;
receipt.stage = receipt.card ? 'copied' : 'declined';
receipt.signature = diplomatDefenseSignature(receipt);
log(g, receipt.card
? `${p.name} used Diplomat: ${cardOf(p, receipt.card)!.name} copies the opposing ${receipt.kind === 'shield' ? 'Shield' : 'Snooper'} for this battle and must be discarded afterward. The original plans and physical card identities remain unchanged.`
: `${p.name} declined to copy the opposing defense with Diplomat.`,
{ faction: p.faction, name: 'Diplomat defense' });
nextRevealedDecision(g);
} else if (decision.kind === 'stoneBurner') {
requireRule(
Object.keys(action).every((key) =>
@@ -22330,6 +22453,7 @@ function applyActionInner(
g.battle = {
...choice,
event: battleEvent,
...(g.leaderSkills && diplomatDefenseModeSupported(g) ? { diplomatDefenseVersion: 1 as const } : {}),
...(byFaction(g, 'richese')
? { preLeader: { event: battleEvent, ready: [], closed: false } }
: {}),
@@ -23609,6 +23733,12 @@ export function viewGame(state: Game, id: string) {
}
: null,
lateDefense: b.revealed ? (b.lateDefense ?? {}) : {},
diplomatDefense: b.revealed && b.diplomatDefense
? { player: b.diplomatDefense.player, event: b.diplomatDefense.event,
leader: b.diplomatDefense.leader, cards: [...b.diplomatDefense.cards],
source: b.diplomatDefense.source, kind: b.diplomatDefense.kind,
stage: b.diplomatDefense.stage, card: b.diplomatDefense.card }
: null,
fullPlan: b.fullPlan ?? null,
fullPlanInsight:
b.fullPlan?.owner === id &&
+6 -3
View File
@@ -142,6 +142,7 @@ function qualifies(
skill: DirectLeaderSkillId,
weapon: Card | undefined,
defense: Card | undefined,
effectiveDefense: Card | undefined,
) {
if (skill === 'warmaster')
return [weapon, defense].some((card) => card?.kind === 'worthless');
@@ -150,8 +151,8 @@ function qualifies(
if (skill === 'swordmaster-of-ginaz')
return weaponTypes(weapon).includes('projectile');
if (skill === 'killer-medic')
return defenseTypes(defense).includes('snooper');
return defenseTypes(defense).includes('shield');
return defenseTypes(effectiveDefense ?? defense).includes('snooper');
return defenseTypes(effectiveDefense ?? defense).includes('shield');
}
/**
@@ -164,6 +165,8 @@ export function leaderSkillBattleBonus(input: {
selectedLeader: { id: string; kind: 'disc' | 'hero' } | undefined;
weapon: Card | undefined;
defense: Card | undefined;
/** A copied defense role leaves the played Worthless card available to Warmaster. */
effectiveDefense?: Card;
skilledLeaderSurvives: boolean;
bankerSpice?: number;
}): LeaderSkillBattleBonus {
@@ -191,7 +194,7 @@ export function leaderSkillBattleBonus(input: {
});
for (const assignment of input.assignments) {
if (!isDirectSkill(assignment.skill)) continue;
if (!qualifies(assignment.skill, input.weapon, input.defense)) continue;
if (!qualifies(assignment.skill, input.weapon, input.defense, input.effectiveDefense)) continue;
if (
input.selectedLeader.kind === 'disc' &&
assignment.leader === input.selectedLeader.id &&
+8 -7
View File
@@ -3563,7 +3563,7 @@ export const RULE_TOPICS: RuleTopic[] = [
category: 'Advanced & expansions',
coverage: 'Partial',
developmentStage: 'Prototyped',
summary: 'Fourteen physical skills, private assignment choices, five battle disciplines, Planetologist movement/battle effects and Suk Graduate rescues, Mentat/Bureaucrat scoring, Sandmaster victory spice, Rihani exchanges, optional Smuggler shipping, Sandmaster route collection and sealed Spice Banker battle spending in development tables. Other effects and combined-module acceptance remain unfinished.',
summary: 'Fourteen physical skills, private assignment choices, five battle disciplines, Planetologist movement/battle effects and Suk Graduate rescues, Mentat/Bureaucrat scoring, Sandmaster victory spice, Rihani exchanges, optional Smuggler shipping, Sandmaster route collection sealed Spice Banker battle spending and optional Diplomat base-defense copying in development tables. Other effects and combined-module acceptance remain unfinished.',
steps: [
'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.',
@@ -3578,16 +3578,17 @@ 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.',
'Normal Mentat questioning, Bureaucrat payments, special Sandmaster relocation and Smuggler battle collection, Spice Banker normal income and all Diplomat effects remain missing. Foreign gholas, Face Dancers and combined modules remain unfinished. This development prototype does not enable public module starts.',
'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.',
'Normal Mentat questioning, Bureaucrat payments, special Sandmaster relocation and Smuggler battle collection, 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.',
...LEADER_SKILL_CARDS.map(card => `${card.name}. Normal: ${card.normal.join(' ')} Skilled battle: ${card.battle.join(' ')}`),
],
related: ['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 and optional Sandmaster ground-route collection are connected. Spice Banker lower spending is connected; normal income, other remaining bands, Diplomat and combined modules remain missing.',evidence:['game/leader-skill-cards.ts','game/leader-skills.ts','game/leader-skill-combat.ts','game/suk-graduate.ts','game/rihani-decipherer.ts','game/leader-skill-battle-board.ts','game/smuggler-shipment.ts','game/smuggler-no-field.ts','game/spice-banker.ts','game/sandmaster-movement.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. Banker exposes separate sealed spending and authorized inspection. Sandmaster exposes explicit legal routes and per-territory collection choices. Remaining effect controls are missing.',evidence:['components/leader-skills.tsx','components/planetologist-movement.tsx','components/suk-graduate.tsx','components/rihani-decipherer.tsx','components/leader-skill-battle-guide.tsx','components/smuggler-shipment.tsx','components/spice-banker.tsx','components/sandmaster-movement.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, Sandmaster route collection and funded Banker commitments. This is a legal prototype path; 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/SMUGGLER_SHIPMENT.md','docs/SPICE_BANKER_RUNTIME.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 quote/decision/legacy recovery and Banker funding, death, traitor, privacy and concurrent settlement checks cover this boundary. 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/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'] },
{ 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 and optional Sandmaster ground-route collection are connected. Spice Banker lower spending and native Diplomat base Shield/Snooper copying are connected; normal income, other remaining bands, Diplomat retreat and combined modules remain missing.',evidence:['game/leader-skill-cards.ts','game/leader-skills.ts','game/leader-skill-combat.ts','game/suk-graduate.ts','game/rihani-decipherer.ts','game/leader-skill-battle-board.ts','game/smuggler-shipment.ts','game/smuggler-no-field.ts','game/spice-banker.ts','game/sandmaster-movement.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. Banker exposes separate sealed spending and authorized inspection. Sandmaster exposes explicit legal routes and per-territory collection choices. Diplomat offers a named committed Worthless choice or decline after public reveal. Remaining effect controls are missing.',evidence:['components/leader-skills.tsx','components/planetologist-movement.tsx','components/suk-graduate.tsx','components/rihani-decipherer.tsx','components/leader-skill-battle-guide.tsx','components/smuggler-shipment.tsx','components/spice-banker.tsx','components/sandmaster-movement.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, Sandmaster route collection funded Banker commitments and optional Diplomat copying from public revealed defenses. This is a legal prototype path; 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/SMUGGLER_SHIPMENT.md','docs/SPICE_BANKER_RUNTIME.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 quote/decision/legacy 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. 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/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/diplomat-defense.test.ts','tests/diplomat-defense-controls.test.tsx','tests/diplomat-defense-recovery.test.ts'] },
],
},
...[
+1
View File
@@ -8,6 +8,7 @@ const DECISIONS = {
leaderSkillRevival: true,
sukRescue: true,
rihani: true,
diplomatDefense: true,
choamAudit: true,
choamAuditPayment: true,
strongholdCopy: true,