Preserve Homeworld and Junction transfers through advisor conversion

Capture surviving alone BG groups after committed native board-to-world and
Junction board transfers. Keep typed payment/custody and shipment use, private
original declaration snapshots and one original Junction arrival. Preserve
Junction immunity independently of the BG conversion counter.

Validation: 18 affected files218/218, types/lint/build, nine native transport
and GameTable SSR journeys, sixteen existing-profile response paths. Genuine
37/38/42/36-action sources earn original routes and costs3/5/10/1. Rename two
fixture variables rejected by lint without changing rule validation. Broader
combinations/full modes remain unfinished. Not deployed; no saved-room/D1,
reset or restart operation. Runtime fingerprint
5365a5d6fba829c6bcee1cb1e00977ec41769912a32180062e52af0ca59645da;
checkpoint prose/reference overlays are reviewed separately.
This commit is contained in:
Quintine committed 2026-10-24 20:49:40 +11:00
1 parent d83db397c0
commit 0b3413fcd8
12 files changed
+849 -15

No files matched your search

+60
View File
@@ -18,6 +18,17 @@ export type AutomaticAdvisorTransport = {
group: Record<string, number>;
};
export type AutomaticAdvisorHomeworldTransport = {
event: string;
kind: 'native' | 'junction';
player: string;
origin: string;
destination: string;
amount: number;
elite: number;
group: Record<string, number>;
};
export type AutomaticAdvisorGamont = {
event: string;
player: string;
@@ -47,6 +58,7 @@ export type AutomaticAdvisorFlipSource = {
} & (
| { producer: 'groundMovement'; mover: string; move: number }
| { producer: 'guildTransport'; transport: AutomaticAdvisorTransport }
| { producer: 'homeworldTransport'; transport: AutomaticAdvisorHomeworldTransport }
| { producer: 'gamont'; gamont: AutomaticAdvisorGamont }
| { producer: 'battleLosses'; battle: AutomaticAdvisorBattle }
);
@@ -79,6 +91,7 @@ export type AutomaticAdvisorFlipContext = AdvisorSettlementContext & MobileBoard
result: string;
} | null;
battle?: unknown;
homeworlds?: unknown;
};
export class AutomaticAdvisorFlipError extends Error {
@@ -218,6 +231,50 @@ function validateGamontSource(
'Automatic advisor conversion lost its final CHOAM Mentat opportunity.');
}
function validateHomeworldTransportSource(
ctx: AutomaticAdvisorFlipContext,
source: Extract<AutomaticAdvisorFlipSource, { producer: 'homeworldTransport' }>,
) {
const transport = source.transport;
requireFlip(record(ctx.homeworlds) && record(ctx.homeworlds.custody) && record(transport) &&
Object.keys(transport).sort().join(',') === 'amount,destination,elite,event,group,kind,origin,player' &&
identifier(transport.event) && (transport.kind === 'native' || transport.kind === 'junction') &&
identifier(transport.player) && identifier(transport.origin) && identifier(transport.destination) &&
positive(transport.amount) && nonnegative(transport.elite) && transport.elite <= transport.amount &&
record(transport.group) && Object.values(transport.group).every(positive) &&
Object.values(transport.group).reduce((total, count) => total + count, 0) === transport.amount,
'Automatic advisor conversion lost its public Homeworld transport receipt or module.');
const actors = ctx.players.filter((player) => player.id === transport.player);
const actor = actors[0];
requireFlip(actors.length === 1 && actor.shipped === true,
'Automatic advisor conversion lost its completed Homeworld transport slot.');
const places = gameTerritories(ctx);
const origin = places.find((place) => place.id === transport.origin);
const point = splitLocation(transport.destination);
const boardDestination = places.find((place) =>
transport.destination === `${point.territory}:${point.sector}` &&
place.id === point.territory && place.sectors.includes(point.sector));
const worldDestination = ctx.players.some((player) =>
transport.destination === `homeworld:${player.faction}` ||
(player.faction === 'emperor' && transport.destination === 'homeworld:emperor:salusa'));
requireFlip(origin && Object.keys(transport.group).every((key) => {
const selected = splitLocation(key);
return key === `${selected.territory}:${selected.sector}` && selected.territory === transport.origin &&
origin.sectors.includes(selected.sector);
}) && (source.territory === transport.origin ||
(boardDestination && source.territory === boardDestination.id)) &&
(transport.kind === 'native'
? actor.faction === 'guild' && worldDestination
: ctx.players.filter((player) => player.faction === 'guild').length === 1 &&
(worldDestination || !!boardDestination)),
'Automatic advisor conversion needs the original native or Junction Homeworld transport locations.');
if (boardDestination) {
const deposited = actor.forces[transport.destination];
requireFlip(nonnegative(deposited) && deposited >= transport.amount,
'Automatic advisor conversion lost the Homeworld transport board deposit.');
}
}
function validateBattleSource(
ctx: AutomaticAdvisorFlipContext,
source: Extract<AutomaticAdvisorFlipSource, { producer: 'battleLosses' }>,
@@ -256,6 +313,8 @@ export function validateAutomaticAdvisorFlip(
Object.keys(source).sort().join(',') === 'event,forces,move,mover,owner,phase,producer,territory,turn') ||
(source.producer === 'guildTransport' &&
Object.keys(source).sort().join(',') === 'event,forces,owner,phase,producer,territory,transport,turn') ||
(source.producer === 'homeworldTransport' &&
Object.keys(source).sort().join(',') === 'event,forces,owner,phase,producer,territory,transport,turn') ||
(source.producer === 'gamont' &&
Object.keys(source).sort().join(',') === 'event,forces,gamont,owner,phase,producer,territory,turn') ||
(source.producer === 'battleLosses' &&
@@ -277,6 +336,7 @@ export function validateAutomaticAdvisorFlip(
requireFlip(movers.length === 1 && positive(source.move) && movers[0].moved === source.move,
'Automatic advisor conversion lost its original completed movement.');
} 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);
const forces = physicalGroup(owner, source.territory);