Preserve permanent Traitor markers through legal replacement recycling

This commit is contained in:
Quintine committed 2026-10-15 19:08:16 +11:00
1 parent 1cd7db7d0b
commit e265af3cb3
29 files changed
+1273 -68

No files matched your search

+9 -11
View File
@@ -3282,10 +3282,15 @@ function nexusTraitorUniverse(g: Game) {
] })), g.expansions.includes('ix')), g.ecazLoyalty);
}
function nexusTraitorSnapshot(g: Game): NexusTraitorSnapshot {
return { reserve: [...(g.traitorReserve ?? [])], players: g.players.map(p => ({
const snapshot: NexusTraitorSnapshot = { reserve: [...(g.traitorReserve ?? [])], players: g.players.map(p => ({
id: p.id, faction: p.faction, traitors: [...p.traitors],
...(p.faceDancers ? { faceDancers: structuredClone(p.faceDancers) } : {}),
})) };
if (g.moritaniAssassinate) {
for (const receipt of g.moritaniAssassinate.opportunities)
if (receipt.stage === 'replaced') (snapshot.removed ??= []).push(receipt.card!);
}
return snapshot;
}
function pendingNexusTraitors(g: Game) {
return g.nexusTraitorExchanges?.find(exchange => exchange.stage === 'return') ?? null;
@@ -3371,6 +3376,7 @@ function nexusTraitorOffer(g: Game, id: string) {
return { event: JSON.stringify(['nexusTraitors', g.turn, g.phase, g.nexusTraitorExchanges?.length ?? 0, id]), mode, draw, blocked };
}
function applyNexusTraitorSnapshot(g: Game, snapshot: NexusTraitorSnapshot) {
// Permanently removed cards are derived from original game receipts, not rewritten by an exchange.
g.traitorReserve = [...snapshot.reserve];
for (const entry of snapshot.players) {
const player = getPlayer(g, entry.id);
@@ -8288,16 +8294,8 @@ function moritaniAssassinateIntegrity(g: Game) {
'Assassination must preserve every physical Traitor Card in hand, deck or face-up set-aside custody.');
for (const r of state.opportunities.filter(r => r.stage === 'revealed'))
requireRule(getPlayer(g,r.owner).traitors.includes(r.card!), 'The revealed assassination card must remain held until Mentat replacement.');
const replacements = new Set<string>();
for (const [index,r] of state.opportunities.entries()) if (r.stage === 'replaced') {
const laterUses = state.opportunities.slice(index+1).filter(next => next.card === r.replacement &&
(next.stage === 'revealed' || next.stage === 'replaced'));
const ownedReplacement = laterUses.length ? laterUses.every(next => next.turn > r.turn)
: getPlayer(g,r.owner).traitors.includes(r.replacement!);
requireRule(!replacements.has(r.replacement!) && ownedReplacement,
'Each assassination replacement must stay with Moritani or be consumed by its later assassination.');
replacements.add(r.replacement!);
}
// Replacement IDs record historical draws, not permanent hand ownership.
// Later legal exchanges/recycled draws retain the complete physical census above.
}
}
function syncMoritaniAssassinateReceipt(g: Game, receipt: MoritaniAssassinateReceipt) {
+2 -1
View File
@@ -1,6 +1,7 @@
import { CHOAM_AUDITOR_ID, createAuditorLeader, leaders as printedLeaders, type Leader } from './cards';
import { FACTIONS, type FactionId } from './catalog';
import { DUKE_VIDAL_ID } from './duke-vidal';
import { CHEAP_HERO_TRAITOR } from './traitors';
export type MoritaniAssassinateReceipt = {
event: string;
@@ -267,7 +268,7 @@ export function validateMoritaniAssassinate(
else
requireAssassinate(
identifier(r.replacement) &&
printedDiscs.has(r.replacement) &&
(printedDiscs.has(r.replacement) || r.replacement === CHEAP_HERO_TRAITOR) &&
r.replacement !== r.card &&
(!custody ||
(custody.removed.includes(r.card!) &&
+1
View File
@@ -108,6 +108,7 @@ function signature(receipt: Omit<NexusFaceDancerReceipt, 'signature'>): string {
receipt.replaced,
receipt.drawn,
receipt.settledReserve,
...(Object.hasOwn(receipt.source, 'removed') ? [receipt.source.removed] : []),
]);
}
function resultingState(receipt: NexusFaceDancerReceipt): NexusTraitorSnapshot {
+6
View File
@@ -440,6 +440,11 @@ function validateReplacement(
(replacement.drawn === null || receipt.source.universe.includes(replacement.drawn)) &&
replacement.signature === replacementSignature(replacement),
'Harkonnen Betrayal replacement lost its exact successful Use obligation or actual drawn identity.');
if (replacement.drawn !== null) {
const [, originalTraitors] = JSON.parse(receipt.source.inventory) as [unknown, NexusTraitorSnapshot];
requireBetrayal(!originalTraitors.removed?.includes(replacement.drawn),
'Permanently set-aside Traitor Cards cannot fund a Betrayal replacement.');
}
}
export function validateHarkonnenBetrayalReplacement(
replacement: HarkonnenBetrayalReplacement, history: readonly HarkonnenBetrayalReceipt[],
@@ -458,6 +463,7 @@ export function drawHarkonnenBetrayalReplacement(
'Harkonnen Betrayal replacement draws exactly once from its actual canonical Traitor Deck.');
const result: HarkonnenBetrayalReplacement = { ...replacement, status: 'drawn', drawn, signature: '' };
result.signature = replacementSignature(result);
validateReplacement(result, history);
return result;
}
export function validateHarkonnenBetrayalReplacements(
+16 -3
View File
@@ -10,6 +10,8 @@ export type NexusExchangePlayer = {
export type NexusTraitorSnapshot = {
reserve: string[];
players: NexusExchangePlayer[];
/** Physical cards permanently set aside, not available for any draw or return. */
removed?: string[];
};
export type NexusTraitorExchange = {
version: 1;
@@ -26,6 +28,7 @@ export type NexusTraitorExchange = {
ownerFaction: FactionId;
reserve: string[];
others: NexusExchangePlayer[];
removed?: string[];
};
settledReserve?: string[];
signature: string;
@@ -119,8 +122,9 @@ export function validateNexusTraitorSnapshot(
requireExchange(
ids(universe) &&
universe.length > 0 &&
exact(state, ['reserve', 'players']) &&
exact(state, ['reserve', 'players', ...(Object.hasOwn(state, 'removed') ? ['removed'] : [])]) &&
ids(state.reserve) &&
(!Object.hasOwn(state, 'removed') || ids(state.removed)) &&
Array.isArray(state.players) &&
state.players.length >= 2 &&
state.players.length <= 6,
@@ -135,6 +139,7 @@ export function validateNexusTraitorSnapshot(
);
const physical = [
...state.reserve,
...(state.removed ?? []),
...state.players.flatMap((p) => [
...p.traitors,
...(p.faceDancers ?? []).map((c) => c.leader),
@@ -198,6 +203,7 @@ function signature(exchange: Omit<NexusTraitorExchange, 'signature'>): string {
exchange.source.reserve,
exchange.source.others.map(playerTuple),
exchange.settledReserve ?? null,
...(Object.hasOwn(exchange.source, 'removed') ? [exchange.source.removed] : []),
]);
}
function validateMode(
@@ -252,8 +258,10 @@ export function validateNexusTraitorExchange(
ids(exchange.drawn) &&
ids(exchange.returned) &&
Array.isArray(exchange.before) &&
exact(exchange.source, ['ownerFaction', 'reserve', 'others']) &&
exact(exchange.source, ['ownerFaction', 'reserve', 'others',
...(Object.hasOwn(exchange.source, 'removed') ? ['removed'] : [])]) &&
ids(exchange.source.reserve) &&
(!Object.hasOwn(exchange.source, 'removed') || ids(exchange.source.removed)) &&
Array.isArray(exchange.source.others),
'Invalid saved Nexus traitor exchange.',
);
@@ -275,6 +283,7 @@ export function validateNexusTraitorExchange(
validatePlayer(originalOwner);
const original: NexusTraitorSnapshot = {
reserve: exchange.source.reserve,
...(Object.hasOwn(exchange.source, 'removed') ? { removed: exchange.source.removed } : {}),
players: state.players.map((p) =>
p.id === exchange.owner
? originalOwner
@@ -282,7 +291,9 @@ export function validateNexusTraitorExchange(
),
};
requireExchange(
exchange.source.others.length === state.players.length - 1 &&
Object.hasOwn(state, 'removed') === Object.hasOwn(exchange.source, 'removed') &&
equal(state.removed, exchange.source.removed) &&
exchange.source.others.length === state.players.length - 1 &&
equal(
exchange.source.others.map(playerTuple),
state.players.filter((p) => p.id !== exchange.owner).map(playerTuple),
@@ -357,6 +368,7 @@ export function beginNexusTraitorExchange(
source: {
ownerFaction: player.faction,
reserve: [...state.reserve],
...(Object.hasOwn(state, 'removed') ? { removed: [...state.removed!] } : {}),
others: structuredClone(
state.players.filter((p) => p.id !== input.owner),
),
@@ -454,6 +466,7 @@ export function validateNexusTraitorHistory(
validateNexusTraitorExchange(
{
reserve: exchange.settledReserve!,
...(Object.hasOwn(exchange.source, 'removed') ? { removed: [...exchange.source.removed!] } : {}),
players: [owner, ...structuredClone(exchange.source.others)],
},
universe,
+23 -11
View File
@@ -1964,6 +1964,7 @@ export const RULE_TOPICS: RuleTopic[] = [
title: 'Cheap Hero and Heroine traitor',
category: 'Advanced & expansions',
coverage: 'Partial',
developmentStage: 'Prototyped',
summary:
'One expansion traitor card can betray either Cheap Hero card repeatedly.',
steps: [
@@ -1971,14 +1972,23 @@ export const RULE_TOPICS: RuleTopic[] = [
'If an opponent uses either hero as their battle leader, the holder may reveal this traitor. The ordinary traitor outcome applies: the opponent loses its army and played cards; the hero has zero strength and grants no leader bounty.',
'Keep the traitor after revealing it. You can call it again against either hero in later battles, including against a different opponent. Revealed traitor identities remain visible in the player panels; unrevealed holdings remain private.',
'Harkonnen may call this traitor against its ally’s opponent through the ordinary ally-power response. Kwisatz Haderach protects an accompanying hero against a traitor call.',
'When this original expansion identity is in the actual Traitor Deck, Moritani may draw it as a normal Mentat assassination replacement. It remains a private Traitor Card, not a physical opposing leader disc or native-faction assassination target. The retired victim stays permanently out of the deck.',
'The matching, outcome, public memory and AI support are implemented and tested. Full expansion deck setup and later expansion interactions remain gated with the rest of the expansion.',
],
checklist: [
{area:'Implementation',status:'Partial',detail:'Canonical Hero is accepted as a physical Moritani replacement, separately from printed leader-disc and faction-target lookup. Full active/retired census still prevents fabrication or copying; no Hero leader or Duke Traitor is added.',evidence:['game/traitors.ts','game/moritani-assassinate.ts','game/engine.ts']},
{area:'Player controls',status:'Partial',detail:'Actual390px final Collection readiness and original owned Mentat initialization produce one Hero in the private hand; its real inspector shows Any faction and strength0 with normal traitor guidance. No overflow/browser errors; no extra replacement selector.',evidence:['components/game-table.tsx']},
{area:'AI',status:'Partial',detail:'All four actual Ready policies advance through owned initialization before the automatic Hero draw. The card choice comes from the original reserve, not AI invention; strategic calibration remains deferred.',evidence:['game/bots.ts']},
{area:'Documentation',status:'Partial',detail:'One physical expansion Traitor identity, two matching Hero Treachery cards and no leader disc are distinct. Replacement legality does not make Hero a faction assassination target; permanent victims stay out.',evidence:['docs/MORITANI_ASSASSINATE_LEADERS.md','docs/NEXUS_CARD_RULES.md']},
{area:'Verification',status:'Partial',detail:'Affected15-file134/134, five original Hero API/policy paths, next-Storm continuation and actual390px inspector pass. Illegal physical-disc/target use and one-card census boundaries are covered. No saved/deployed or complete-expansion acceptance.',evidence:['tests/fixture-moritani-hero-replacement.ts','tests/moritani-hero-replacement.test.ts']},
],
related: [
'setup',
'battle',
'faction-harkonnen',
'advanced-atreides',
'ix-modules',
'moritani-assassinate-leaders',
],
},
{
@@ -3150,16 +3160,17 @@ export const RULE_TOPICS: RuleTopic[] = [
'After you lose a battle, this advanced advantage is available only if the opposing leader disc survived and no Traitor was called.',
'You may reveal a Traitor Card for that opposing faction, but it must name a different leader from the one you just fought. If that named leader is not in the Tanks, kill it and collect spice for its value.',
'An already-dead named leader remains a legal reveal, awarding no death or spice. During Mentat Pause, set the revealed card aside face up as a marker and draw a new Traitor Card. You can use this advantage only once against each faction in the game.',
'A Mentat replacement is an ordinary physical Traitor Card, including the original Ix Hero identity when present. After original owned initialization, retire the victim permanently. The replacement itself may later be legally exchanged, returned and drawn again; keep its old receipt unchanged and count one live card, not one per historical reference.',
'The authorized revised Advanced PDF, physical page35, requires that you have not revealed your Traitor Card normally this game. A normal own-card reveal therefore forfeits Assassinate Leaders for the rest of this game, across turns and opponents. Existing state preserves that forfeiture; Karama has no effect against the ability. This is the authorized source, not a new GF9 erratum.',
'The surviving actual played disc need not be native to its victorious controller. Native Ecaz/Duke6, Tleilaxu/foreign Guild5, CHOAM/Auditor2 and Harkonnen captives use the winner’s printed-faction targets. Captivity or Ghola control never rewrites native identity. Auditor has a matching Traitor outside CHOAM’s ordinary five; it can be a different native target for two but not the disc just played. Duke has no Traitor, and Cheap Hero is not a physical opposing disc. Exceptional captured/Ghola targets remain separate.',
'Terror Assassination chooses a random leader when its token is triggered. It does not use this battle-loss condition, different-leader Traitor Card or once-per-faction allowance. Moritani starts remain unavailable in Basic and Advanced.',
],
checklist: [
{area:'Implementation',status:'Partial',detail:'Cached canonical printed discs including actual Auditor distinguish played identity from victorious faction. Actual pre-release surviving control/plan binds the opportunity; native target lookup includes Auditor2 but excludes the played disc. State/signature, once-use, game-long forfeiture, custody guards and original physical Mentat replacement remain.',evidence:['game/moritani-assassinate.ts','game/engine.ts']},
{area:'Player controls',status:'Partial',detail:'Actual390px Sanya4 after Duke6, Zoal3 after foreign Guild5, Frankos4 after Auditor2, Auditor target2 and played-Auditor private Continue are usable. Original Mentat replacement remains private. Native Guild-funded Ambassador shipment/permission/acquisition also connects; private-step UX remains separate.',evidence:['components/moritani-assassinate.tsx','components/game-table.tsx']},
{area:'AI',status:'Partial',detail:'All four owner profiles make accepted target/decline decisions in all five original positions; ordinary Guild-income Ambassador candidate quote is clear. Minimal legal participation, no strategic tuning or calibration claim.',evidence:['game/bots.ts','game/bot-arrival.ts']},
{area:'Documentation',status:'Partial',detail:'Authorized p35 conditions, native component identity, Auditor matching Traitor and printed bounty are explicit. Separate provisional Terror policies do not define this ability. Exceptional targets, private-step UX, Skills/Harkonnen and broader arrival gates remain open.',evidence:['docs/MORITANI_ASSASSINATE_LEADERS.md','docs/CHOAM_AUDITOR.md','docs/RULE_DECISIONS.md']},
{area:'Verification',status:'Partial',detail:'Affected15-file134/134, types/lint and25 original API/policy cases pass. Real acquisition, projectile deaths/foreign return, native battles, public reveal/private replacement and actual390px controls are exercised. No wallet/hand/army/leader/phase/battle staging; only original entropy and conserved unplayed ordering. No saved/deployed or full-mode acceptance.',evidence:['tests/moritani-ecaz-duke-winner.test.ts','tests/moritani-foreign-ghola-winner.test.ts','tests/moritani-auditor-winner.test.ts','tests/moritani-harkonnen-assassinate.test.ts','tests/moritani-assassinate-quote.test.ts']},
{area:'Implementation',status:'Partial',detail:'Canonical played-disc/native target semantics remain. Moritani replacement accepts physical Hero separately; permanent victim cards join the shared removed census, while historical replacement IDs may move/recur legally. Exchange, Face Dancer, Betrayal and Rihani consumers bind their own source zone. Current authorization/census, revealed-card reservation and original parent remain.',evidence:['game/moritani-assassinate.ts','game/nexus-traitor-exchange.ts','game/nexus-face-dancers.ts','game/nexus-harkonnen-betrayal.ts','game/rihani-decipherer.ts','game/engine.ts']},
{area:'Player controls',status:'Partial',detail:'Actual390px readiness reaches private Hero inspector. Original Secret Ally returns historical Hasimir6/Caid3, keeps Esmar3 and restores Mori placement; passing reaches next Storm. Later actual Esmar3 reveal draws one Hasimir6 again, while two victim markers remain public and out of play. No overflow/browser errors.',evidence:['components/moritani-assassinate.tsx','components/nexus-traitors.tsx','components/game-table.tsx']},
{area:'AI',status:'Partial',detail:'Four Ready policies preserve owned initialization before Hero draw; four mandatory-return policies legally finish the actual augmented hand and restore parent. Each return policy keeps Hasimir in this position; human/scripted old-card return is distinct. No strategic calibration claim.',evidence:['game/bots.ts','game/nexus-traitor-options.ts']},
{area:'Documentation',status:'Partial',detail:'Permanent victim markers, active physical cards and historical draw references are separate. Old absent-zone tuple grammar remains; later markers do not rewrite completed history. Receipt consistency does not authenticate every past save. Target exceptions, private-step UX, Skills/Harkonnen and broader modes remain open.',evidence:['docs/MORITANI_ASSASSINATE_LEADERS.md','docs/NEXUS_HARKONNEN_RULES.md','docs/NEXUS_TLEILAXU_RULES.md','docs/RULE_DECISIONS.md']},
{area:'Verification',status:'Partial',detail:'Affected15-file134/134, eleven original API/policy cases and three actual390px journeys pass. Native original Hero, old-card return, restored placement, second paid battle/retirement and repeated physical draw preserve source history and one live card. Older fixtures now consume actual borrowed Voice before native powers/plans; false ownership/uniqueness and wording/markup pins removed. No saved/deployed/full-mode acceptance.',evidence:['tests/moritani-hero-replacement.test.ts','tests/moritani-replacement-exchange.test.ts','tests/nexus-traitor-exchange.test.ts','tests/nexus-face-dancers.test.ts','tests/nexus-harkonnen-betrayal.test.ts','tests/rihani-face-dancers.test.ts']},
],
related: [
'faction-moritani',
@@ -4600,8 +4611,9 @@ export const RULE_TOPICS: RuleTopic[] = [
'Harkonnen Cunning draws one Traitor Card before you choose one held card to return. It can pause another action, including a response or decision. Finish the private return before that action continues; active Truthtrance takes priority. Returning an identity already declared as a traitor does not cancel that declaration.',
'With Harkonnen absent, Secret Ally draws two Traitor Cards during Mentat Pause, then you choose exactly two distinct held identities to return. Newly drawn cards may be returned. Tleilaxu receives Face Dancers and can return newly drawn or existing unrevealed dancers; previously revealed dancers stay protected.',
'The Harkonnen exchange spends the Nexus Card when you draw and shuffles the returned cards into the Traitor Deck after your selection. Your original hand size is restored. Other players see that the exchange is pending, without learning your drawn or returned identities.',
'Moritani victim cards permanently set aside are outside reserve, ordinary hands and Face Dancers, so Nexus draws and returns cannot recycle them. A card merely drawn as an earlier Moritani replacement is different: it may be legally returned and later drawn again, with the older receipt unchanged. Actual Hasimir6 return, resumed Mori placement and a later Esmar3 retirement/redraw are connected; two historical references still represent one physical card.',
'Harkonnen Betrayal has a separate fresh local harkonnen-betrayal profile for an actual personal or allied native traitor call. After any original allied counter allows it, uniform public possible held-Nexus owners acknowledge the actually declared traitor before its outcome. Use returns that exact physical traitor immediately and schedules one private actual Mentat replacement; all-pass keeps the original call. This provisional bounded convention does not answer the outstanding universal reaction policy; see its dedicated guide for bounded verified native/private/recovery/CLI/phone evidence and unchanged module/public/deployment gates.',
'Tleilaxu Cunning sets aside all your revealed Face Dancers, draws their replacements secretly, and only then shuffles the set-aside cards into the Traitor Deck. Unrevealed dancers stay in your hand. This single operation does not use or renew your ordinary Mentat replacement; the native all-three-revealed reset keeps its separate shuffle-before-draw order.',
'Tleilaxu Cunning sets aside all your revealed Face Dancers, draws replacements secretly, then shuffles that temporary group into the Traitor Deck. Unrevealed dancers stay held; permanent Moritani victim markers never enter that draw/return group. Original native Mentat and all-three-revealed usage/order remain distinct. Recorded source zones and old absent-zone history remain unchanged by later live markers.',
'Ixian Cunning is used in your battle before submitting your Battle Plan. Your Suboids then count as strength one without spice support in every battle for the rest of the turn, in Basic and Advanced. Cyborg strength and support are unchanged. Earlier inspected or promised plan elements remain binding; you cannot activate the card if the new strength would make them impossible.',
'Ixian Secret Ally has a fresh local ixian-replacement profile for normal paid or printed Karama-paid auction purchases in classic base-deck/Nexus Basic or Advanced games. A supported unallied buyer may spend its actual Nexus to discard exactly the just-purchased card and draw the real deck top privately; Pass keeps both cards. Full-hand replacement and empty-deck same-card redraw are allowed, and original payment/income/auction continuation occur once. Harkonnen buyers, cache/Black Market/special origins and combined modules remain guarded. The neutral buyer choice is an inferred privacy convention, not publisher timing. See the dedicated guide for bounded verified native, private, recovery and phone evidence; this is not full-module certification.',
'Ixian Betrayal has a separate fresh native-Ixian ixian-betrayal profile for both original Bidding inspection/draw and Advanced Technology attempts. Native counters resolve first; then every publicly possible unallied rival held-Nexus seat acknowledges uniformly before the effect. A legal Use spends one physical Ixian Nexus to deny only that attempt; all-pass preserves the native effect once. No completed-card undo or whole-phase ban is added. This bounded ordering/privacy is an inference, not universal publisher priority; see its bounded verified native/private/recovery/phone evidence and unchanged Richese/module/public/deployment gates.',
@@ -4646,11 +4658,11 @@ export const RULE_TOPICS: RuleTopic[] = [
],
related: ['ecaz-modules', 'spice-blow', 'alliance-funding', 'choam-worthless', 'choam-kull', 'nexus-choam-kull', 'nexus-richese-betrayal', 'nexus-guild-betrayal', 'nexus-ixian-replacement', 'nexus-ixian-betrayal'],
checklist: [
{area:"Implementation",status:'Partial',detail:"Printed current-turn Ecaz Cunning now uses any original quiet phase rather than an artificial Battle-only fence. Existing no-phase guard preserves committed auctions, Battle, decisions and physical continuations. New version3 binds actual phase and ordinary/captured custody; legacy ordinary/version2 Battle grammar stays strict. Prior capture, Ecaz-only Ghola composition, purchase/revival sources and roster/shared-training gates remain; full combinations and assurance stay open.",evidence:['game/nexus-cards.ts', 'game/nexus-card-phase.ts', 'game/nexus-traitor-exchange.ts', 'game/traitor-declarations.ts', 'game/nexus-face-dancers.ts', 'game/nexus-suboids.ts', 'game/nexus-advisors.ts', 'game/nexus-sardaukar-options.ts', 'game/nexus-fremen-revival.ts', 'game/nexus-fremen-cunning.ts', 'game/nexus-ecaz-betrayal.ts', 'game/nexus-ecaz-inquiry.ts', 'game/nexus-ecaz-duke.ts', 'game/duke-vidal.ts', 'game/nexus-emperor-secret-ally.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal.ts', 'game/nexus-richese-options.ts', 'game/nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/nexus-guild-secret-ally-options.ts', 'game/nexus-module-profile.ts', 'game/engine.ts', 'game/nexus-tleilaxu-revival.ts', 'game/nexus-borrowed-voice.ts', 'game/combat-response-quote.ts', 'game/karama-context.ts', 'game/nexus-fremen-protection.ts', 'game/admin-seat-ai-shape.ts', 'game/richese-cancellation.ts', 'game/bot-mobility.ts', 'game/planetologist-movement.ts', 'game/revival.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts', 'game/nexus-emperor-secret-ally.ts', 'game/revival-resume.ts', 'game/revival-cancellation.ts', 'game/homeworld-revival-deployment.ts', 'game/homeworld-revival-return.ts', "game/nexus-emperor-purchase.ts", "game/auction-continuation-quote.ts", "game/nexus-emperor-purchase.ts", "game/ambassador-phase.ts", "game/ambassador-resume.ts", "game/battle-aftermath-quote.ts", "game/ecaz-duke-acquisition.ts", "game/ecaz-duke-revival.ts", "game/nexus-ecaz-duke-options.ts", "game/nexus-play-boundary.ts"]},
{area:"Player controls",status:'Partial',detail:"Actual canonical-styled390px controls spend Cunning in Revival10 to10, return an executed Duke at deaths2 without ordinary slot use, and act during Harkonnen Shipment without stealing its turn. Original Ambassador placement and Guild timing block until their own controls resolve. Original battle release and unused expiry follow. No overflow/browser errors; prior capture/purchase controls remain. Broader journeys stay open.",evidence:['components/nexus-cards.tsx', 'components/nexus-traitors.tsx', 'components/nexus-tleilaxu.tsx', 'components/nexus-suboids.tsx', 'components/nexus-advisors.tsx', 'components/nexus-sardaukar.tsx', 'components/choam-power-cost.tsx', 'components/moritani-terror.tsx', 'components/nexus-moritani-betrayal.tsx', 'components/nexus-ecaz-betrayal.tsx', 'components/nexus-ecaz-inquiry.tsx', 'components/nexus-ecaz-duke.tsx', 'components/nexus-richese-betrayal.tsx', 'components/game-table.tsx', 'components/nexus-tleilaxu-revival.tsx', 'components/battle-preparation.tsx', 'components/nexus-fremen-revival.tsx','components/nexus-emperor-payment.tsx', 'components/nexus-emperor-secret-ally.tsx', "components/nexus-emperor-seller-purchase.tsx", "components/nexus-emperor-ambassador-purchase.tsx", "components/richese-special-karama.tsx"]},
{area:"AI",status:'Partial',detail:"All four minimal legal profiles spend early Cunning in actual Revival and Shipment for captured and returned-Tanks sources, preserving original chooser, wallets and physical disc. No hidden rival information or second roster is used. Prior purchase/revival adapters remain; strategic AI and four-tier calibration are deferred.",evidence:['game/nexus-card-options.ts', 'game/nexus-traitor-options.ts', 'game/nexus-tleilaxu-options.ts', 'game/nexus-suboid-options.ts', 'game/nexus-advisor-options.ts', 'game/nexus-sardaukar-options.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal-options.ts', 'game/nexus-ecaz-betrayal-options.ts', 'game/nexus-ecaz-inquiry-options.ts', 'game/nexus-ecaz-duke-options.ts', 'game/nexus-richese-options.ts', 'game/bot-nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/bots.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts', 'game/nexus-emperor-secret-ally-options.ts']},
{area:"Documentation",status:'Partial',detail:"Printed no-phase/current-turn Cunning is separated from conservative quiet-boundary ordering, provisional capture return and binding Ecaz-only revival. New actual-phase history and strict old Battle formats are explicit. No foreign Ghola, roster or shared-training widening; prior source interpretations and full-mode gates remain.",evidence:['docs/NEXUS_CARD_RULES.md', 'docs/NEXUS_ATREIDES_RULES.md', 'docs/NEXUS_HARKONNEN_RULES.md', 'docs/NEXUS_TLEILAXU_RULES.md', 'docs/NEXUS_IXIAN_CUNNING_RULES.md', 'docs/NEXUS_CUNNING_RUNTIME.md', 'docs/NEXUS_BENE_GESSERIT_RULES.md', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'docs/NEXUS_EMPEROR_RULES.md', 'docs/NEXUS_SARDAUKAR_RUNTIME.md', 'docs/NEXUS_FREMEN_RULES.md', 'docs/NEXUS_ECAZ_RULES.md', 'docs/NEXUS_CHOAM_RULES.md', 'docs/NEXUS_CHOAM_RUNTIME.md', 'docs/NEXUS_MORITANI_RULES.md', 'docs/NEXUS_MORITANI_RUNTIME.md', 'docs/NEXUS_RICHESE_RUNTIME.md', 'docs/NEXUS_RICHESE_RULES.md', 'docs/NEXUS_GUILD_CUNNING_RUNTIME.md', 'docs/NEXUS_GUILD_RULES.md', "docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md", "docs/RULE_DECISIONS.md", "docs/RICHESE_AMBASSADOR_RULES.md", "docs/RICHESE_AMBASSADOR_IMPLEMENTATION.md", "docs/RICHESE_ACQUISITION_RULES.md", "docs/DUKE_VIDAL_RULES.md", "docs/ECAZ_DUKE_ACQUISITION.md", "docs/ECAZ_REVIVAL_RULES.md"]},
{area:"Verification",status:'Partial',detail:"Affected19-file union189/189, types/lint and thirty-one actual native scenarios pass: both custody sources across six original phases, all four profiles in Revival/Shipment, both expiries and original battle release. Actual styled390px Revival, other-player Shipment and returned Tanks preserve native blockers, costs/allowances and turn. Actual previous ordinary/version2 QA receipts remain accepted. Obsolete phase/blanket Harkonnen/held-hand pins are deleted. No saved/deployed rooms touched; broader assurance and full acceptance stay open.",evidence:['tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', 'tests/nexus-cards.test.ts', 'tests/nexus-card-engine.test.ts', 'tests/nexus-fremen-cunning.test.ts', 'tests/nexus-fremen-cunning-recovery.test.ts', 'tests/nexus-fremen-cunning-summon.test.ts', 'tests/nexus-fremen-cunning-summon-recovery.test.ts', 'tests/nexus-ecaz-betrayal-quotes.test.ts', 'tests/nexus-ecaz-betrayal.test.ts', 'tests/nexus-ecaz-betrayal-recovery.test.ts', 'tests/nexus-ecaz-inquiry.test.ts', 'tests/nexus-ecaz-duke-quote.test.ts', 'tests/nexus-ecaz-duke-engine.test.ts', 'tests/nexus-ecaz-duke-recovery.test.ts', 'tests/nexus-traitor-recovery.test.ts', 'tests/nexus-face-dancer-engine.test.ts', 'tests/nexus-suboids-engine.test.ts', 'tests/normal-free-support.test.ts', 'tests/nexus-suboid-controls.test.ts', 'tests/nexus-advisor-controls.test.ts', 'tests/nexus-advisor-bots.test.ts', 'tests/nexus-sardaukar-controls.test.ts', 'tests/nexus-sardaukar-bots.test.ts', 'tests/nexus-choam-controls.test.ts', 'tests/nexus-choam-bots.test.ts', 'tests/nexus-moritani-controls.test.ts', 'tests/nexus-moritani-bots.test.ts', 'tests/nexus-moritani-betrayal.test.ts', 'tests/nexus-richese-controls.test.ts', 'tests/nexus-richese-bots.test.ts', 'tests/nexus-richese-betrayal-engine.test.ts', 'tests/nexus-richese-betrayal-recovery.test.ts', 'tests/nexus-guild-cunning-controls.test.ts', 'tests/nexus-guild-cunning-bots.test.ts', 'tests/nexus-module-payments-runtime.test.ts', 'tests/nexus-module-battles-runtime.test.ts', 'tests/paired-ix-nexus-modules-runtime.test.ts', 'tests/paired-choam-nexus-modules-runtime.test.ts', 'tests/classic-nexus-skills-payments-runtime.test.ts', 'tests/classic-nexus-skills-battles-runtime.test.ts', 'tests/nexus-skills-modules-payments-runtime.test.ts', 'tests/nexus-skills-modules-battles-runtime.test.ts', 'tests/paired-ix-nexus-skills-modules-runtime.test.ts', 'tests/paired-choam-nexus-skills-modules-runtime.test.ts', 'tests/ecaz-nexus-skills-modules-runtime.test.ts', 'tests/moritani-nexus-skills-modules-runtime.test.ts', 'docs/NEXUS_CARD_RULES.md', 'tests/nexus-tleilaxu-revival.test.ts', 'tests/fixture-nexus-tleilaxu-secret-ally.ts', 'tests/nexus-tleilaxu-secret-ally-runtime.test.ts', 'tests/nexus-borrowed-voice.test.ts', 'tests/fixture-nexus-borrowed-voice.ts', 'tests/nexus-borrowed-voice-runtime.test.ts', 'tests/nexus-fremen-protection.test.ts', 'tests/fixture-nexus-fremen-protection.ts', 'tests/nexus-fremen-protection-runtime.test.ts', 'tests/fixture-stronghold-factions.ts', 'tests/fixture-nexus-advisor-fresh.ts', 'tests/nexus-advisor-fresh-runtime.test.ts', 'tests/fixture-nexus-advisor-storm.ts', 'tests/nexus-advisor-storm-runtime.test.ts', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'tests/nexus-fremen-free-quota.test.ts', 'tests/fixture-nexus-fremen-free-quota.ts', 'tests/nexus-fremen-free-quota-runtime.test.ts', 'tests/nexus-fremen-revival.test.ts','tests/nexus-emperor-payment.test.ts','tests/fixture-nexus-emperor-payment.ts','tests/nexus-emperor-payment-runtime.test.ts','docs/NEXUS_EMPEROR_RULES.md', 'tests/nexus-emperor-extra-source.test.ts', 'tests/nexus-emperor-extra-continuation.test.ts', 'tests/fixture-nexus-emperor-extra.ts', 'tests/nexus-emperor-extra-runtime.test.ts', 'tests/fixture-single-nexus-e1.ts', 'docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md', "tests/fixture-nexus-emperor-seller.ts", "tests/nexus-emperor-seller-purchase.test.ts", "tests/nexus-emperor-seller-runtime.test.ts", "tests/fixture-nexus-emperor-ambassador.ts", "tests/nexus-emperor-ambassador-purchase.test.ts", "tests/nexus-emperor-ambassador-runtime.test.ts", "tests/fixture-nexus-emperor-special.ts", "tests/nexus-emperor-special-purchase.test.ts", "tests/nexus-emperor-special-runtime.test.ts", "tests/karama-intents.test.ts", "tests/fixture-nexus-emperor-bank.ts", "tests/nexus-emperor-bank-profile.test.ts", "tests/nexus-emperor-bank-runtime.test.ts", "tests/duke-captured-custody.test.ts", "tests/nexus-ecaz-duke-captured.test.ts", "tests/fixture-nexus-duke-capture.ts", "tests/nexus-dukLine truncated
{area:"Implementation",status:'Partial',detail:"Shared Traitor snapshots now account for permanent Moritani markers outside reserve/hands/Face Dancers, retaining existing Loyalty exclusion. Exchange, Face Dancer, Betrayal and Rihani consumers bind their own source zone; absent-zone grammar stays unchanged. Historical replacements may be legally returned and drawn again. Existing Cunning timing, costs, custody, actor/parent and roster gates remain; full combinations stay open.",evidence:['game/nexus-cards.ts', 'game/nexus-card-phase.ts', 'game/nexus-traitor-exchange.ts', 'game/traitor-declarations.ts', 'game/nexus-face-dancers.ts', 'game/nexus-suboids.ts', 'game/nexus-advisors.ts', 'game/nexus-sardaukar-options.ts', 'game/nexus-fremen-revival.ts', 'game/nexus-fremen-cunning.ts', 'game/nexus-ecaz-betrayal.ts', 'game/nexus-ecaz-inquiry.ts', 'game/nexus-ecaz-duke.ts', 'game/duke-vidal.ts', 'game/nexus-emperor-secret-ally.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal.ts', 'game/nexus-richese-options.ts', 'game/nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/nexus-guild-secret-ally-options.ts', 'game/nexus-module-profile.ts', 'game/engine.ts', 'game/nexus-tleilaxu-revival.ts', 'game/nexus-borrowed-voice.ts', 'game/combat-response-quote.ts', 'game/karama-context.ts', 'game/nexus-fremen-protection.ts', 'game/admin-seat-ai-shape.ts', 'game/richese-cancellation.ts', 'game/bot-mobility.ts', 'game/planetologist-movement.ts', 'game/revival.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts', 'game/nexus-emperor-secret-ally.ts', 'game/revival-resume.ts', 'game/revival-cancellation.ts', 'game/homeworld-revival-deployment.ts', 'game/homeworld-revival-return.ts', "game/nexus-emperor-purchase.ts", "game/auction-continuation-quote.ts", "game/nexus-emperor-purchase.ts", "game/ambassador-phase.ts", "game/ambassador-resume.ts", "game/battle-aftermath-quote.ts", "game/ecaz-duke-acquisition.ts", "game/ecaz-duke-revival.ts", "game/nexus-ecaz-duke-options.ts", "game/nexus-play-boundary.ts"]},
{area:"Player controls",status:'Partial',detail:"Actual390px Secret Ally draws two, returns historical Hasimir6 plus Caid3, keeps Esmar3 and restores the original Mori placement. Passing it reaches the next Storm. A later genuine Esmar3 retirement draws one physical Hasimir again while older exchange history stays unchanged. Hero replacement inspector is private; no overflow/browser errors. Earlier Cunning/purchase controls remain bounded evidence, not full acceptance.",evidence:['components/nexus-cards.tsx', 'components/nexus-traitors.tsx', 'components/nexus-tleilaxu.tsx', 'components/nexus-suboids.tsx', 'components/nexus-advisors.tsx', 'components/nexus-sardaukar.tsx', 'components/choam-power-cost.tsx', 'components/moritani-terror.tsx', 'components/nexus-moritani-betrayal.tsx', 'components/nexus-ecaz-betrayal.tsx', 'components/nexus-ecaz-inquiry.tsx', 'components/nexus-ecaz-duke.tsx', 'components/nexus-richese-betrayal.tsx', 'components/game-table.tsx', 'components/nexus-tleilaxu-revival.tsx', 'components/battle-preparation.tsx', 'components/nexus-fremen-revival.tsx','components/nexus-emperor-payment.tsx', 'components/nexus-emperor-secret-ally.tsx', "components/nexus-emperor-seller-purchase.tsx", "components/nexus-emperor-ambassador-purchase.tsx", "components/richese-special-karama.tsx"]},
{area:"AI",status:'Partial',detail:"All four profiles legally complete the actual augmented-hand return, retaining the original parent; in this position each keeps Hasimir rather than following the scripted old-card return. All four also Ready through original owned Mentat initialization before automatic Hero draw. Existing other module adapters remain; strategic AI and calibration are deferred.",evidence:['game/nexus-card-options.ts', 'game/nexus-traitor-options.ts', 'game/nexus-tleilaxu-options.ts', 'game/nexus-suboid-options.ts', 'game/nexus-advisor-options.ts', 'game/nexus-sardaukar-options.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal-options.ts', 'game/nexus-ecaz-betrayal-options.ts', 'game/nexus-ecaz-inquiry-options.ts', 'game/nexus-ecaz-duke-options.ts', 'game/nexus-richese-options.ts', 'game/bot-nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/bots.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts', 'game/nexus-emperor-secret-ally-options.ts']},
{area:"Documentation",status:'Partial',detail:"Permanent victim markers, live Traitor custody and historical replacement references are distinct. Hero is a Traitor replacement, not a leader disc or native faction target. Sources/receipts keep their own optional removed zone; old absent formats and existing Loyalty exclusion remain. No past-save authenticity, new numeric ruling or profile/public gate widening is claimed.",evidence:['docs/NEXUS_CARD_RULES.md', 'docs/NEXUS_ATREIDES_RULES.md', 'docs/NEXUS_HARKONNEN_RULES.md', 'docs/NEXUS_TLEILAXU_RULES.md', 'docs/NEXUS_IXIAN_CUNNING_RULES.md', 'docs/NEXUS_CUNNING_RUNTIME.md', 'docs/NEXUS_BENE_GESSERIT_RULES.md', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'docs/NEXUS_EMPEROR_RULES.md', 'docs/NEXUS_SARDAUKAR_RUNTIME.md', 'docs/NEXUS_FREMEN_RULES.md', 'docs/NEXUS_ECAZ_RULES.md', 'docs/NEXUS_CHOAM_RULES.md', 'docs/NEXUS_CHOAM_RUNTIME.md', 'docs/NEXUS_MORITANI_RULES.md', 'docs/NEXUS_MORITANI_RUNTIME.md', 'docs/NEXUS_RICHESE_RUNTIME.md', 'docs/NEXUS_RICHESE_RULES.md', 'docs/NEXUS_GUILD_CUNNING_RUNTIME.md', 'docs/NEXUS_GUILD_RULES.md', "docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md", "docs/RULE_DECISIONS.md", "docs/RICHESE_AMBASSADOR_RULES.md", "docs/RICHESE_AMBASSADOR_IMPLEMENTATION.md", "docs/RICHESE_ACQUISITION_RULES.md", "docs/DUKE_VIDAL_RULES.md", "docs/ECAZ_DUKE_ACQUISITION.md", "docs/ECAZ_REVIVAL_RULES.md"]},
{area:"Verification",status:'Partial',detail:"Current affected15-file134/134, eleven original API/policy cases and three actual390px journeys pass: physical Hero/next Storm, historical replacement return/resumed placement, then the same physical draw after a later second marker. Adjacent old fixtures now consume original borrowed Voice before native Prescience/plans. Earlier Cunning/native evidence stays historical; no saved/deployed or full-module assurance claim.",evidence:['tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', 'tests/nexus-cards.test.ts', 'tests/nexus-card-engine.test.ts', 'tests/nexus-fremen-cunning.test.ts', 'tests/nexus-fremen-cunning-recovery.test.ts', 'tests/nexus-fremen-cunning-summon.test.ts', 'tests/nexus-fremen-cunning-summon-recovery.test.ts', 'tests/nexus-ecaz-betrayal-quotes.test.ts', 'tests/nexus-ecaz-betrayal.test.ts', 'tests/nexus-ecaz-betrayal-recovery.test.ts', 'tests/nexus-ecaz-inquiry.test.ts', 'tests/nexus-ecaz-duke-quote.test.ts', 'tests/nexus-ecaz-duke-engine.test.ts', 'tests/nexus-ecaz-duke-recovery.test.ts', 'tests/nexus-traitor-recovery.test.ts', 'tests/nexus-face-dancer-engine.test.ts', 'tests/nexus-suboids-engine.test.ts', 'tests/normal-free-support.test.ts', 'tests/nexus-suboid-controls.test.ts', 'tests/nexus-advisor-controls.test.ts', 'tests/nexus-advisor-bots.test.ts', 'tests/nexus-sardaukar-controls.test.ts', 'tests/nexus-sardaukar-bots.test.ts', 'tests/nexus-choam-controls.test.ts', 'tests/nexus-choam-bots.test.ts', 'tests/nexus-moritani-controls.test.ts', 'tests/nexus-moritani-bots.test.ts', 'tests/nexus-moritani-betrayal.test.ts', 'tests/nexus-richese-controls.test.ts', 'tests/nexus-richese-bots.test.ts', 'tests/nexus-richese-betrayal-engine.test.ts', 'tests/nexus-richese-betrayal-recovery.test.ts', 'tests/nexus-guild-cunning-controls.test.ts', 'tests/nexus-guild-cunning-bots.test.ts', 'tests/nexus-module-payments-runtime.test.ts', 'tests/nexus-module-battles-runtime.test.ts', 'tests/paired-ix-nexus-modules-runtime.test.ts', 'tests/paired-choam-nexus-modules-runtime.test.ts', 'tests/classic-nexus-skills-payments-runtime.test.ts', 'tests/classic-nexus-skills-battles-runtime.test.ts', 'tests/nexus-skills-modules-payments-runtime.test.ts', 'tests/nexus-skills-modules-battles-runtime.test.ts', 'tests/paired-ix-nexus-skills-modules-runtime.test.ts', 'tests/paired-choam-nexus-skills-modules-runtime.test.ts', 'tests/ecaz-nexus-skills-modules-runtime.test.ts', 'tests/moritani-nexus-skills-modules-runtime.test.ts', 'docs/NEXUS_CARD_RULES.md', 'tests/nexus-tleilaxu-revival.test.ts', 'tests/fixture-nexus-tleilaxu-secret-ally.ts', 'tests/nexus-tleilaxu-secret-ally-runtime.test.ts', 'tests/nexus-borrowed-voice.test.ts', 'tests/fixture-nexus-borrowed-voice.ts', 'tests/nexus-borrowed-voice-runtime.test.ts', 'tests/nexus-fremen-protection.test.ts', 'tests/fixture-nexus-fremen-protection.ts', 'tests/nexus-fremen-protection-runtime.test.ts', 'tests/fixture-stronghold-factions.ts', 'tests/fixture-nexus-advisor-fresh.ts', 'tests/nexus-advisor-fresh-runtime.test.ts', 'tests/fixture-nexus-advisor-storm.ts', 'tests/nexus-advisor-storm-runtime.test.ts', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'tests/nexus-fremen-free-quota.test.ts', 'tests/fixture-nexus-fremen-free-quota.ts', 'tests/nexus-fremen-free-quota-runtime.test.ts', 'tests/nexus-fremen-revival.test.ts','tests/nexus-emperor-payment.test.ts','tests/fixture-nexus-emperor-payment.ts','tests/nexus-emperor-payment-runtime.test.ts','docs/NEXUS_EMPEROR_RULES.md', 'tests/nexus-emperor-extra-source.test.ts', 'tests/nexus-emperor-extra-continuation.test.ts', 'tests/fixture-nexus-emperor-extra.ts', 'tests/nexus-emperor-extra-runtime.test.ts', 'tests/fixture-single-nexus-e1.ts', 'docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md', "tests/fixture-nexus-emperor-seller.ts", "tests/nexus-emperor-seller-purchase.test.ts", "tests/nexus-emperor-seller-runtime.test.ts", "tests/fixture-nexus-emperor-ambassador.ts", "tests/nexus-emperor-ambassador-purchase.test.ts", "tests/nexus-emperor-ambassador-runtime.test.ts", "tests/fixture-nexus-emperor-special.ts", "tests/nexus-emperor-special-purchase.test.ts", "tests/nexus-emperor-special-runtime.test.ts", "tests/karama-intents.test.ts", "tests/fixture-nexus-emperor-bank.ts", "tests/nexus-emperor-bank-profile.test.ts", "tests/nexus-emperor-bank-runtime.test.ts", "tests/duke-captured-custody.test.ts", "tests/nexus-ecaz-duke-captured.test.ts", "tests/fixture-nexus-duke-capture.ts", "tests/nexus-duke-capture-runtime.test.ts", "tests/nexus-duke-timing-runtime.test.ts"]},
],
},
{
+2 -1
View File
@@ -91,8 +91,9 @@ export function validateRihani(receipt: RihaniReceipt, universe: readonly string
const old = receipt.before.players.find((p) => p.id === receipt.owner);
const owner = receipt.state.players.find((p) => p.id === receipt.owner);
requireRihani(old && owner && owner.faction === old.faction &&
same(receipt.before.removed, receipt.state.removed) &&
same(receipt.before.players.filter((p) => p.id !== receipt.owner), receipt.state.players.filter((p) => p.id !== receipt.owner)),
'Rihani changed another player’s physical Traitors.');
'Rihani changed immutable physical Traitor custody.');
requireRihani(Array.isArray(receipt.used) && new Set(receipt.used).size === receipt.used.length &&
members(receipt.deckAfterPeek, receipt.before.reserve) &&
(receipt.skill.normal || same(receipt.deckAfterPeek, receipt.before.reserve)) &&