Resolve advisor conversion after complete Face Dance replacement

Capture surviving alone BG groups at positive remote board sources and zero
replacement targets. Preserve returned armies, replacement, original leader
death, revelation and card cycle. Native Face Dance stays immune; consume the
publicly sourced parent before one original aftermath continuation.

Validation: 16 affected files201/201, types/lint/build, five native Face Dance
and GameTable SSR journeys, eight existing-profile response paths. Genuine
67/66-action sources earn original Ix47, native Face Dancers and actual battle.
Both conversion outcomes preserve nine returned survivors and one leader death;
actual BG city income is2/0. Correct a fixture's expected deck count43 to47
without changing cards or rules. Full combinations/modes remain unfinished.
Not deployed; no saved-room/D1, reset or restart operation. Runtime fingerprint
78239a9def5dd70ae404d5a614a21ea09ea615dd2a6cde7d05760715416d7401;
checkpoint prose/reference overlays are reviewed separately.
This commit is contained in:
Quintine committed 2026-10-24 21:00:58 +11:00
1 parent 0b3413fcd8
commit 9bce22abf5
12 files changed
+901 -29

No files matched your search

+80 -12
View File
@@ -48,6 +48,17 @@ export type AutomaticAdvisorBattle = {
result: 'normal' | 'traitor' | 'mutualTraitors' | 'explosion';
};
export type AutomaticAdvisorFaceDance = {
event: string;
battle: AutomaticAdvisorBattle;
player: string;
territory: string;
sector: number;
sources: Record<string, number>;
amount: number;
returned: Record<string, number>;
};
export type AutomaticAdvisorFlipSource = {
event: string;
owner: string;
@@ -61,6 +72,7 @@ export type AutomaticAdvisorFlipSource = {
| { producer: 'homeworldTransport'; transport: AutomaticAdvisorHomeworldTransport }
| { producer: 'gamont'; gamont: AutomaticAdvisorGamont }
| { producer: 'battleLosses'; battle: AutomaticAdvisorBattle }
| { producer: 'faceDance'; faceDance: AutomaticAdvisorFaceDance }
);
export type AutomaticAdvisorFlipPlayer = ForcePresence & {
@@ -275,25 +287,23 @@ function validateHomeworldTransportSource(
}
}
function validateBattleSource(
function validatePublicBattleIdentity(
ctx: AutomaticAdvisorFlipContext,
source: Extract<AutomaticAdvisorFlipSource, { producer: 'battleLosses' }>,
receipt: AutomaticAdvisorBattle,
turn: number,
) {
const receipt = source.battle;
requireFlip(record(receipt) &&
Object.keys(receipt).sort().join(',') === 'combatants,event,result,territory,turn,winner' &&
identifier(receipt.event) && positive(receipt.turn) && receipt.turn === source.turn &&
identifier(receipt.territory) && receipt.territory === source.territory &&
identifier(receipt.event) && positive(receipt.turn) && receipt.turn === turn &&
identifier(receipt.territory) &&
Array.isArray(receipt.combatants) && receipt.combatants.length === 2 &&
receipt.combatants.every(identifier) && new Set(receipt.combatants).size === 2 &&
receipt.combatants.every((id) => ctx.players.filter((player) => player.id === id).length === 1) &&
!receipt.combatants.includes(source.owner) &&
['normal', 'traitor', 'mutualTraitors', 'explosion'].includes(receipt.result) &&
(receipt.result === 'normal' || receipt.result === 'traitor'
? identifier(receipt.winner) && receipt.combatants.includes(receipt.winner)
: receipt.winner === null) &&
gameTerritories(ctx).some((place) => place.id === receipt.territory),
'Automatic advisor conversion needs the original public battle result and bystander group.');
: receipt.winner === null),
'Automatic advisor conversion needs the original public battle result.');
const current = ctx.lastBattleContext;
requireFlip(!ctx.battle && record(current) && current.event === receipt.event &&
current.turn === receipt.turn && current.territory === receipt.territory &&
@@ -303,6 +313,60 @@ function validateBattleSource(
'Automatic advisor conversion lost its original completed battle context.');
}
function validateBattleSource(
ctx: AutomaticAdvisorFlipContext,
source: Extract<AutomaticAdvisorFlipSource, { producer: 'battleLosses' }>,
) {
validatePublicBattleIdentity(ctx, source.battle, source.turn);
requireFlip(source.battle.territory === source.territory &&
!source.battle.combatants.includes(source.owner) &&
gameTerritories(ctx).some((place) => place.id === source.battle.territory),
'Automatic advisor conversion needs the original public battle result and bystander group.');
}
function validateFaceDanceSource(
ctx: AutomaticAdvisorFlipContext,
source: Extract<AutomaticAdvisorFlipSource, { producer: 'faceDance' }>,
) {
const receipt = source.faceDance;
requireFlip(record(receipt) &&
Object.keys(receipt).sort().join(',') === 'amount,battle,event,player,returned,sector,sources,territory' &&
identifier(receipt.event) && identifier(receipt.player) && identifier(receipt.territory) &&
nonnegative(receipt.sector) && nonnegative(receipt.amount) && record(receipt.sources) &&
Object.values(receipt.sources).every(positive) &&
Object.values(receipt.sources).reduce((total, count) => total + count, 0) === receipt.amount &&
record(receipt.returned) && Object.values(receipt.returned).every(positive) &&
Object.keys(receipt.returned).every((id) =>
identifier(id) && ctx.players.filter((player) => player.id === id).length === 1),
'Automatic advisor conversion lost its exact public Face Dance receipt.');
validatePublicBattleIdentity(ctx, receipt.battle, source.turn);
const actors = ctx.players.filter((player) => player.id === receipt.player);
const actor = actors[0];
requireFlip(actors.length === 1 && actor.faction === 'tleilaxu' &&
receipt.battle.winner !== null && receipt.player !== receipt.battle.winner &&
(receipt.battle.result === 'normal' || receipt.battle.result === 'traitor') &&
receipt.territory === receipt.battle.territory,
'Automatic advisor conversion needs the original other winner and native Face Dance actor.');
const places = gameTerritories(ctx);
const destination = places.find((place) => place.id === receipt.territory && place.sectors.includes(receipt.sector));
const worldDestination = receipt.territory === 'homeworld:tleilaxu' && receipt.sector === 0 &&
record(ctx.homeworlds) && record(ctx.homeworlds.custody);
requireFlip((!!destination || worldDestination) && Object.keys(receipt.sources).every((key) => {
if (key === 'reserves') return true;
const point = splitLocation(key);
return key === `${point.territory}:${point.sector}` &&
places.some((place) => place.id === point.territory && place.sectors.includes(point.sector));
}) && ((!!destination && source.territory === receipt.territory) || Object.keys(receipt.sources).some((key) =>
key !== 'reserves' && splitLocation(key).territory === source.territory)),
'Automatic advisor conversion must use the Face Dance destination or a positive board source.');
if (destination) {
const current = actor.forces[`${receipt.territory}:${receipt.sector}`];
const deposited = current === undefined ? 0 : current;
requireFlip(nonnegative(deposited) && deposited >= receipt.amount,
'Automatic advisor conversion lost the Face Dance board replacement.');
}
}
/** Validate the same committed producer and exact held group before either response outcome. */
export function validateAutomaticAdvisorFlip(
ctx: AutomaticAdvisorFlipContext,
@@ -318,10 +382,13 @@ export function validateAutomaticAdvisorFlip(
(source.producer === 'gamont' &&
Object.keys(source).sort().join(',') === 'event,forces,gamont,owner,phase,producer,territory,turn') ||
(source.producer === 'battleLosses' &&
Object.keys(source).sort().join(',') === 'battle,event,forces,owner,phase,producer,territory,turn')) &&
Object.keys(source).sort().join(',') === 'battle,event,forces,owner,phase,producer,territory,turn') ||
(source.producer === 'faceDance' &&
Object.keys(source).sort().join(',') === 'event,faceDance,forces,owner,phase,producer,territory,turn')) &&
identifier(source.event) && identifier(source.owner) && identifier(source.territory) &&
positive(source.turn) && source.turn === ctx.turn &&
source.phase === (source.producer === 'gamont' ? 8 : source.producer === 'battleLosses' ? 6 : 5) &&
source.phase === (source.producer === 'gamont' ? 8 :
source.producer === 'battleLosses' || source.producer === 'faceDance' ? 6 : 5) &&
currentWindow(ctx) && source.phase === ctx.phase,
'Automatic advisor conversion lost its original current-turn committed producer.');
const owners = ctx.players.filter((player) => player.id === source.owner);
@@ -338,7 +405,8 @@ export function validateAutomaticAdvisorFlip(
} else if (source.producer === 'guildTransport') validateTransportSource(ctx, source);
else if (source.producer === 'homeworldTransport') validateHomeworldTransportSource(ctx, source);
else if (source.producer === 'gamont') validateGamontSource(ctx, source);
else validateBattleSource(ctx, source);
else if (source.producer === 'battleLosses') validateBattleSource(ctx, source);
else validateFaceDanceSource(ctx, source);
const forces = physicalGroup(owner, source.territory);
requireFlip(record(source.forces) && Object.keys(forces).length > 0 &&
Object.values(forces).every(positive) && Object.values(source.forces).every(positive) &&
+76 -4
View File
@@ -12,7 +12,7 @@ import { advancedMoritaniLeaderSkillsProfile, selectedDeckLeaderSkillsProfile, h
import { bribeTimingBlock, maximumBribe, type BribeOptions } from './bribe-options';
import { quoteAtreidesFullPlan } from './atreides-full-plan';
import { quoteTleilaxuSpecialKarama } from './tleilaxu-special-karama';
import { quoteAutomaticAdvisorFlips, validateAutomaticAdvisorFlip, AutomaticAdvisorFlipError, type AdvisorStance, type AutomaticAdvisorFlipSource, type AutomaticAdvisorTransport, type AutomaticAdvisorGamont, type AutomaticAdvisorBattle, type AutomaticAdvisorHomeworldTransport } from './advisor-automatic-flip';
import { quoteAutomaticAdvisorFlips, validateAutomaticAdvisorFlip, AutomaticAdvisorFlipError, type AdvisorStance, type AutomaticAdvisorFlipSource, type AutomaticAdvisorTransport, type AutomaticAdvisorGamont, type AutomaticAdvisorBattle, type AutomaticAdvisorHomeworldTransport, type AutomaticAdvisorFaceDance } from './advisor-automatic-flip';
import { quoteSpicePlacement, stormExposesTerritory, stormSectorAfter, wormConsumesForces } from './disaster-rules';
import { isStormCardDistance, type StormCardComponent } from './storm-cards';
import { createLeaderSkills, validateLeaderSkills, dealLeaderSkills, chooseLeaderSkill, returnDeadLeaderSkills, offerRevivedLeaderSkill, drawRevivedLeaderSkills, declineRevivedLeaderSkill, LeaderSkillError, type LeaderSkillsState, type LeaderSkillsView } from './leader-skills';
@@ -1276,6 +1276,7 @@ export type Game = {
arrival?: { territory: string; sector: number; amount: number; elite: number };
signature: string;
};
pendingAutomaticAdvisorFaceDance?: AutomaticAdvisorFaceDance;
advisorPreparation?: AdvisorPreparationWindow;
ecazOccupyPreview?: boolean;
/** Fresh native Guild/Nexus payment-replacement profile, never inferred from a save. */
@@ -20322,6 +20323,12 @@ function currentAutomaticAdvisorFlip(g: Game, response: ResponseWindow) {
'The mandatory conversion has lost its original committed Homeworld transport.');
validateHomeworldAdvisorTransport(g, parent);
}
if (source.producer === 'faceDance') {
const parent = g.pendingAutomaticAdvisorFaceDance;
requireRule(parent && JSON.stringify(source.faceDance) === JSON.stringify(parent),
'The mandatory conversion has lost its original completed Face Dance.');
validateCompletedFaceDance(g, parent);
}
automaticAdvisorRule(() => validateAutomaticAdvisorFlip(g, source));
return source;
}
@@ -20533,6 +20540,59 @@ function finishAutomaticAdvisorHomeworldTransport(g: Game) {
openTerritoryEntry(g, actor, to, sector, amount, elite, 'shipment');
}
}
function prepareFaceDanceAdvisorFlips(g: Game, player: Player, to: string, sector: number,
sources: Record<string, number>, returned: Record<string, number>) {
const context = g.lastBattleContext;
if (!g.advanced || g.status !== 'playing' || g.phase !== 6 || !context ||
(context.result !== 'normal' && context.result !== 'traitor')) return false;
const changed = new Set([to, ...Object.entries(sources)
.filter(([key, count]) => key !== 'reserves' && count > 0).map(([key]) => splitLocation(key).territory)]);
const groups = automaticAdvisorRule(() => quoteAutomaticAdvisorFlips(g))
.filter(group => changed.has(group.territory));
if (!groups.length) return false;
requireRule(!g.pendingAutomaticAdvisorFlips?.length && !g.pendingAutomaticAdvisorFaceDance &&
!g.pendingAutomaticAdvisorBattle && !g.pendingAutomaticAdvisorTransport &&
!g.pendingAutomaticAdvisorHomeworldTransport && !g.pendingAutomaticAdvisorGamont,
'Finish the original advisor conversion before another Face Dance.');
const receipt: AutomaticAdvisorFaceDance = { event: crypto.randomUUID(),
battle: { event: context.event, turn: context.turn, territory: context.territory,
combatants: [...context.combatants], winner: context.winner, result: context.result },
player: player.id, territory: to, sector, sources, returned,
amount: Object.values(sources).reduce((sum, count) => sum + count, 0) };
g.pendingAutomaticAdvisorFaceDance = receipt;
for (const group of groups) {
const source: AutomaticAdvisorFlipSource = { event: crypto.randomUUID(), owner: group.player,
territory: group.territory, turn: g.turn, phase: g.phase, forces: group.forces,
producer: 'faceDance', faceDance: receipt };
getPlayer(g, group.player).advisors![group.territory].aloneFlipHold = source.event;
(g.pendingAutomaticAdvisorFlips ??= []).push(source);
}
return true;
}
function validateCompletedFaceDance(g: Game, receipt: AutomaticAdvisorFaceDance) {
requireRule(g.status === 'playing' && g.phase === 6 && !g.battle && !g.pendingFaceDance &&
receipt.battle.event === g.lastBattleContext?.event,
'Finish the original Face Dance reveal, replacement and card cycle before conversion.');
const player = getPlayer(g, receipt.player);
const deposited = receipt.territory.startsWith('homeworld:')
? homeworldRule(() => homeworldForceGroups(homeworldContext(g), g.homeworlds!.custody!))
.find(world => world.id === receipt.territory)?.forces[player.id]
: null;
const amount = receipt.territory.startsWith('homeworld:')
? (deposited?.normal ?? 0) + (deposited?.elite ?? 0)
: (player.forces[location(receipt.territory, receipt.sector)] ?? 0);
requireRule(amount >= receipt.amount && Object.entries(receipt.returned)
.every(([owner, count]) => getPlayer(g, owner).reserves >= count),
'The completed Face Dance lost its original returned or replacement counters.');
}
function finishAutomaticAdvisorFaceDance(g: Game) {
const receipt = g.pendingAutomaticAdvisorFaceDance;
if (!receipt) return;
requireRule(!g.pendingAutomaticAdvisorFlips?.length, 'Finish all original Face Dance conversions before aftermath.');
validateCompletedFaceDance(g, receipt);
delete g.pendingAutomaticAdvisorFaceDance;
finishBattle(g);
}
function applyAdvisorReleases(g: Game, released: AdvisorRelease[]) {
for (const entry of released)
delete getPlayer(g, entry.player).advisors?.[entry.territory];
@@ -22552,7 +22612,8 @@ function decideSharedSpice(g: Game, id: string, action: Action) {
function victory(g: Game, quote?: VictoryQuote) {
requireRule(!g.pendingAutomaticAdvisorFlips?.length && !g.pendingAutomaticAdvisorTransport &&
!g.pendingAutomaticAdvisorGamont && !g.pendingAutomaticAdvisorBattle && !g.pendingAutomaticAdvisorHomeworldTransport,
!g.pendingAutomaticAdvisorGamont && !g.pendingAutomaticAdvisorBattle && !g.pendingAutomaticAdvisorHomeworldTransport &&
!g.pendingAutomaticAdvisorFaceDance,
'Resolve mandatory advisor conversions before checking victory.');
quote ??= currentVictoryQuote(g);
applyAdvisorReleases(g, quote.released);
@@ -28374,7 +28435,7 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
}
if (automaticAdvisorFlip) {
if (canceled)
log(g, `${bg.name}'s mandatory conversion in ${territory(to).name} was prevented. The completed ${automaticAdvisorFlip.producer === 'guildTransport' || automaticAdvisorFlip.producer === 'homeworldTransport' ? 'transport and payment stay' : automaticAdvisorFlip.producer === 'gamont' ? 'Trip to Gamont effect and card use stay' : automaticAdvisorFlip.producer === 'battleLosses' ? 'battle losses and costs stay' : 'movement stays'} committed; this group remains advisors while continuously alone.`,
log(g, `${bg.name}'s mandatory conversion in ${territory(to).name} was prevented. The completed ${automaticAdvisorFlip.producer === 'guildTransport' || automaticAdvisorFlip.producer === 'homeworldTransport' ? 'transport and payment stay' : automaticAdvisorFlip.producer === 'gamont' ? 'Trip to Gamont effect and card use stay' : automaticAdvisorFlip.producer === 'battleLosses' ? 'battle losses and costs stay' : automaticAdvisorFlip.producer === 'faceDance' ? 'Face Dance replacement and reveal stay' : 'movement stays'} committed; this group remains advisors while continuously alone.`,
{ faction: bg.faction, name: 'Mandatory advisor conversion prevented' });
g.pendingAutomaticAdvisorFlips!.shift();
if (!g.pendingAutomaticAdvisorFlips!.length) delete g.pendingAutomaticAdvisorFlips;
@@ -28383,6 +28444,7 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
finishAutomaticAdvisorGamont(g);
finishAutomaticAdvisorBattle(g);
finishAutomaticAdvisorHomeworldTransport(g);
finishAutomaticAdvisorFaceDance(g);
} else openAutomaticAdvisorFlip(g);
} else finishAdvisorReaction(g, response);
}
@@ -33758,6 +33820,7 @@ function applyActionInner(
typeof action.reveal === 'boolean',
'Reveal a Face Dancer or decline.',
);
let heldConversion = false;
if (action.reveal) {
const dancer = p.faceDancers?.find(
(c) => !c.revealed && c.leader === decision.identity,
@@ -33810,6 +33873,12 @@ function applyActionInner(
if (!p.forces[source.key]) delete p.forces[source.key];
}
}
// Record the counters actually returned by the existing transfer. A
// co-side Tleilaxu army may already have supplied selected counters.
const returned = Object.fromEntries((home
? [[winner.id, army.normal + army.elite] as const]
: winningArmies.map(owner => [owner.id, at(owner, decision.territory)] as const))
.filter(([, count]) => count > 0));
if (home) {
const destination = `homeworld:${winner.faction}`;
const external = selected.filter((source) => source.key !== 'reserves').reduce((n, source) => n + source.count, 0);
@@ -33848,6 +33917,8 @@ function applyActionInner(
delete leader.concealed;
}
if (total && !home) place(p, decision.territory, sector, total);
heldConversion = prepareFaceDanceAdvisorFlips(g, p, decision.territory, sector,
Object.fromEntries(selected.filter(source => source.count > 0).map(source => [source.key, source.count])), returned);
observeOccupation(g);
dancer.revealed = true;
log(
@@ -33869,7 +33940,8 @@ function applyActionInner(
const harkonnen = byFaction(g, 'harkonnen');
if (harkonnen) returnCaptives(g, harkonnen);
settleAdvisors(g);
finishBattle(g);
if (heldConversion) openAutomaticAdvisorFlip(g);
else finishBattle(g);
} else if (decision.kind === 'diplomatRetreat') {
const receipt = g.battle?.diplomatRetreat;
const destination = action.destination;
+1
View File
@@ -185,6 +185,7 @@ const cancellationSources = {
gamont: response.automaticAdvisorFlip.producer === 'gamont' ? g.pendingAutomaticAdvisorGamont?.receipt ?? null : null,
battle: response.automaticAdvisorFlip.producer === 'battleLosses' ? g.pendingAutomaticAdvisorBattle?.receipt ?? null : null,
homeworld: response.automaticAdvisorFlip.producer === 'homeworldTransport' ? g.pendingAutomaticAdvisorHomeworldTransport?.receipt ?? null : null,
faceDance: response.automaticAdvisorFlip.producer === 'faceDance' ? g.pendingAutomaticAdvisorFaceDance ?? null : null,
group: g.players.find(p => p.id === response.owner)?.advisors?.[response.location ?? ''] ?? null }
: response.advisorPreparationSource
? { preparation: response.advisorPreparationSource, window: g.advisorPreparation ?? null }
+13 -6
View File
@@ -2094,7 +2094,8 @@ export const RULE_TOPICS: RuleTopic[] = [
'A winning Cheap Hero may be a Face Dancer. Kwisatz Haderach does not protect an accompanying leader against Face Dancing. Karama cannot cancel the reveal or the replacement army.',
'Keep revealed Face Dancers face up until all three have been used. Shuffle those cards back into the unused deck and draw three new ones. During Mentat Pause, you may instead replace one unrevealed Face Dancer; this once-per-turn exchange has a Karama response.',
'Zoal has variable battle strength: he copies the opposing leader disc, without any KH bonus, or counts zero against a Cheap Hero. His death bounty uses that copied strength. His ordinary revival value is three spice before discounts.',
'The core setup, reveal sequence, card cycling, Zoal effects and AI controls are tested. Tleilaxu revival commerce also has tested support. Foreign ghola acquisition and buyback controls are also implemented. Their special Karama now also has tested support; its timing and the complete expansion interaction audit are still unfinished, so expansion starts remain disabled.',
'After completed replacement, advisors left alone at remote sources or a zero-replacement target have a separate BG conversion response. Face Dance remains complete and immune; prevention keeps its returns, replacement, leader death and revelation/cycle, then resumes the original aftermath once.',
'The core setup, reveal sequence, card cycling, Zoal effects and AI controls are tested. Tleilaxu revival commerce also has tested support. Foreign ghola acquisition and buyback controls are also implemented. Their special Karama now also has tested support; its timing and the complete expansion interaction audit are still unfinished, so starts remain labelled previews.',
],
related: [
'battle',
@@ -2102,6 +2103,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'tech-tokens',
'mentat',
'ix-modules',
'advanced-advisors',
],
},
@@ -4317,6 +4319,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'A committed Guild cross-shipment or return to reserves can also leave advisors alone. Original transport permission, rate and any Guild income resolve first; payment, force transfer and shipment use stay committed if the later conversion is prevented. The ordinary ground move remains unused. Cross-shipment arrival follows the conversion once; a reserve return has no arrival.',
'After actual battle losses leave surviving bystanding advisors alone, Karama may prevent their conversion without undoing losses or costs. Mandatory card disposal, original skills and substitution finish first. Provisionally resolve this before optional winner-card cleanup and later aftermath, then resume the original cleanup once. An explosion that destroys the advisors creates no conversion.',
'Native Guild board-to-Homeworld and Junction board transfers can leave advisors alone after payment and physical transfer. Karama prevents only the resulting BG conversion; Junction permission and tariff stay immune. Original income resolves first, then any Junction board arrival resumes once. Original declaration snapshots stay private.',
'A completed Face Dance can leave advisors alone at remote board sources or, with zero replacement, at the battle target. Original force returns, replacement, leader death, revelation and any card cycle finish first. Karama can then prevent only the BG conversion; native Face Dance remains immune. Afterward the original aftermath resumes once.',
'Provisional cancellation duration: a prevented advisor group stays advisors while continuously alone, including across phase and turn changes. Another faction returning or the group becoming empty ends that held occurrence. Later arrivals joining that same group match its stance. Other non-ground automatic conversions and matching-group flip counters remain unfinished.',
'When another faction ships, moves or worm rides into your fighters, immediately choose whether to become advisors. The choice opens a Karama response. Repeated entry can offer a new choice.',
'After another faction’s off-planet shipment, choose a free force in the Polar Sink or an accompanying force at the shipment destination. An accompanying advisor group cannot flip to fighters in that turn while other factions remain.',
@@ -4328,7 +4331,7 @@ export const RULE_TOPICS: RuleTopic[] = [
{
"area": "Implementation",
"status": "Partial",
"detail": "Homeworld/Junction board transfers now capture alone groups before occupation/settlement, retain completed typed transfer/payment and shipment use, keep original snapshot events private and resume only original Junction board arrival after the consumed parent. Existing ground/Guild transport/Gamont/battle/preparation sources retain separate exact receipts and timing. Other automatic producers and matching-group counters remain unfinished.",
"detail": "Completed Face Dance now captures alone advisors at all positive board sources and its target, preserving return/replacement, leader death and revelation/cycling before one original aftermath. Its public physical/battle receipt omits Face Dancer identities and does not reuse pre-reveal availability. Existing ground, Guild/Homeworld/Junction transport, Gamont, battle and preparation sources retain their contracts. Storm and other special relocation/matching-group causes remain unfinished.",
"evidence": [
"game/advisor-automatic-flip.ts",
"game/advisors.ts",
@@ -4343,7 +4346,7 @@ export const RULE_TOPICS: RuleTopic[] = [
{
"area": "Player controls",
"status": "Partial",
"detail": "Standard Karama/Allow controls distinguish completed Homeworld transport/payment from the pending BG conversion and remaining arrival. Existing ground, ordinary transport, Gamont, battle and phase2–5 preparation guidance retain their contracts. No new confirmation or timer.",
"detail": "Standard Karama/Allow controls distinguish completed Face Dance from the pending BG conversion. Existing transport, ground, Gamont, battle and phase2–5 preparation guidance remain. No new confirmation or timer.",
"evidence": [
"docs/ADVISOR_AUTOMATIC_FLIPS.md",
"components/advisor-preparation.tsx",
@@ -4354,8 +4357,10 @@ export const RULE_TOPICS: RuleTopic[] = [
{
"area": "AI",
"status": "Partial",
"detail": "Sixteen existing-profile native Homeworld/Junction response paths accept legal actions without replaying transfer/payment or consuming ordinary movement. Prior ground/Guild transport/Gamont/battle/preparation paths retain separate evidence. No strategy refinement or difficulty calibration.",
"detail": "Eight existing-profile native remote/zero Face Dance response paths resolve legally through actual Collection, preserving completed return, death, revelation and replacement. Earlier producer paths retain separate evidence. No strategy refinement or difficulty calibration.",
"evidence": [
"tests/advisor-face-dance-flip-engine.test.ts",
"tests/fixture-advisor-face-dance-flip.ts",
"tests/advisor-homeworld-flip-engine.test.ts",
"tests/fixture-advisor-homeworld-flip.ts",
"tests/advisor-battle-flip-engine.test.ts",
@@ -4370,7 +4375,7 @@ export const RULE_TOPICS: RuleTopic[] = [
{
"area": "Documentation",
"status": "Partial",
"detail": "Physical17/28 E composes the group counter with mandatory conversion. Homeworld/Junction guidance distinguishes immune transport from BG conversion, private original snapshots, public physical evidence and one original arrival. Continuously-alone duration and earlier battle priority remain provisional. Remaining producers and combinations stay explicit.",
"detail": "Physical17/28 E composes the group counter with physical38 A after completed return/replacement. Face Dance immunity, actual returns, remote sources, zero replacement, original card cycle and private holdings are distinguished from the BG response. Continuously-alone duration remains provisional; other producers/combinations stay explicit.",
"evidence": [
"docs/ADVISOR_AUTOMATIC_FLIPS.md",
"docs/ADVISOR_PREPARATION.md",
@@ -4381,8 +4386,10 @@ export const RULE_TOPICS: RuleTopic[] = [
{
"area": "Verification",
"status": "Partial",
"detail": "Homeworld/Junction passes frozen18-file218/218, types/lint/build, nine native transport/response/GameTable SSR journeys and sixteen profile paths. Genuine37/38/42/36-action sources earn original native/Junction returns, destination Intrusion and BG empty arrival. Costs3/5/10/1 remain spent through both outcomes; earlier native permission prevention stays uncommitted. A fixture-variable lint repair preserves strict source validation. Earlier producer proofs remain linked; wider contributor/occupation/Nexus/Semuta, remote Face Dance/storm/other producers, full modes and browser/mobile/deployed/comprehensive acceptance remain open.",
"detail": "Face Dance passes frozen16-file201/201, types/lint/build, five native Face Dance/response/GameTable SSR journeys and eight profile paths. Genuine67/66-action sources earn original Ix47, native draw, shipment, moves and battle. Both outcomes preserve nine returned survivors and one leader death; actual BG city income is2/0. Original decline stays intact. A fixture deck-count correction changes no cards/rules. Full cycle/Skills/Ecaz co-side/Homeworld/Nexus combinations, storm/special relocation, complete modes and browser/mobile/deployed/comprehensive acceptance remain open.",
"evidence": [
"tests/advisor-face-dance-flip-engine.test.ts",
"tests/fixture-advisor-face-dance-flip.ts",
"tests/advisor-homeworld-flip-engine.test.ts",
"tests/fixture-advisor-homeworld-flip.ts",
"tests/advisor-battle-flip-engine.test.ts",