Files
Dune/game/richese-homeworld-marker-shipment.ts
T
Quintine 7e3d745807
Verify and publish checkpoint container / container (push) Canceled after 0s
Prototype native Homeworld No-Field return
2026-10-29 04:55:44 +11:00

153 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
HomeworldCustodyError, homeworldForceGroups, type HomeworldCustody,
} from './homeworld-custody';
import { quoteHomeworldShipment, type HomeworldShipmentContext } from './homeworld-shipment';
import type { HomeworldForces } from './homeworld-custody';
import { deployRicheseNoField, isHomeworldNoFieldLocation, moveRicheseNoField, type RicheseNoField } from './richese-no-field';
export type RicheseHomeworldMarkerShipmentIntent = {
player: string;
destination: string;
tokenId: string;
/** Optional physical companions withdrawn from native Richese reserves. */
forces?: HomeworldForces;
};
export type RicheseHomeworldMarkerTransferIntent = RicheseHomeworldMarkerShipmentIntent & {
source: string;
forces?: HomeworldForces;
};
const record = (value: unknown): value is Record<string, unknown> =>
!!value && typeof value === 'object' && !Array.isArray(value);
function requireMarker(condition: unknown, message: string): asserts condition {
if (!condition) throw new HomeworldCustodyError(message);
}
/** A fresh concealed marker counts as one for the whole shipment invoice. Any
* physical companions are independently withdrawn from native Richese reserves;
* the marker's denomination never enters the price or physical custody. */
export function quoteRicheseHomeworldMarkerShipment(
context: HomeworldShipmentContext,
custody: HomeworldCustody,
intent: RicheseHomeworldMarkerShipmentIntent,
noField: RicheseNoField,
) {
// This validates all physical reserve and foreign garrison custody first.
const worlds = homeworldForceGroups(context, custody);
requireMarker(record(intent) &&
(Object.keys(intent).sort().join(',') === 'destination,player,tokenId' ||
Object.keys(intent).sort().join(',') === 'destination,forces,player,tokenId') &&
typeof intent.player === 'string' && typeof intent.destination === 'string' && typeof intent.tokenId === 'string',
'Choose a Richese player, one active Homeworld and one physical No-Field.');
if ('forces' in intent) {
requireMarker(record(intent.forces) && Object.keys(intent.forces).sort().join(',') === 'elite,normal' &&
Number.isSafeInteger(intent.forces.normal) && Number.isSafeInteger(intent.forces.elite) &&
intent.forces.normal >= 0 && intent.forces.elite >= 0,
'Choose a valid physical normal and special counter group.');
}
requireMarker(context.players.every(seat => seat.ally === null ||
typeof seat.ally === 'string' && seat.ally !== seat.id && context.players.some(other => other.id === seat.ally)),
'Homeworld shipment needs valid seated alliance identities.');
const player = context.players.find(seat => seat.id === intent.player);
const source = worlds.find(world => world.id === 'homeworld:richese');
const destination = worlds.find(world => world.id === intent.destination);
requireMarker(player?.faction === 'richese' && source?.native === player.id,
'Only native Richese can ship its concealed No-Field.');
requireMarker(destination && destination.native !== player.id,
'Choose another seated faction’s Homeworld.');
const native = context.players.find(seat => seat.id === destination.native)!;
requireMarker(!(player.ally === native.id && native.ally === player.id),
'You cannot ship to your ally’s Homeworld.');
let deployed: RicheseNoField;
try {
deployed = deployRicheseNoField(noField, {
tokenId: intent.tokenId,
controller: player.id,
location: { homeworld: destination.id },
});
} catch (error) {
throw new HomeworldCustodyError(error instanceof Error ? error.message : 'Invalid No-Field shipment.');
}
if (intent.forces && intent.forces.normal + intent.forces.elite > 0) {
const physical = quoteHomeworldShipment(context, custody, {
player: player.id, destination: destination.id, sources: { [source.id]: intent.forces },
});
const amount = 1 + physical.amount;
return { player: player.id, tokenId: intent.tokenId, source: source.id,
destination: destination.id, amount, elite: physical.elite,
cost: context.purchasedGuildRate ? Math.ceil(amount / 2) : amount, noField: deployed,
state: physical.state, players: physical.players, sources: physical.sources };
}
return {
player: player.id,
tokenId: intent.tokenId,
source: source.id,
destination: destination.id,
amount: 1,
elite: 0,
cost: 1,
noField: deployed,
};
}
/** A deployed marker can be transported like one force between eligible
* Homeworlds, provisionally alongside physical forces from that same source.
* Advanced permits a native Homeworld endpoint; Basic remains foreign-only.
* Its denomination never enters the price or physical custody. */
export function quoteRicheseHomeworldMarkerTransfer(
context: HomeworldShipmentContext,
custody: HomeworldCustody,
intent: RicheseHomeworldMarkerTransferIntent,
noField: RicheseNoField,
) {
const worlds = homeworldForceGroups(context, custody);
requireMarker(record(intent) &&
(Object.keys(intent).sort().join(',') === 'destination,player,source,tokenId' ||
Object.keys(intent).sort().join(',') === 'destination,forces,player,source,tokenId') &&
typeof intent.player === 'string' && typeof intent.source === 'string' &&
typeof intent.destination === 'string' && typeof intent.tokenId === 'string',
'Choose the current Homeworld marker, its source and another Homeworld.');
if ('forces' in intent) {
requireMarker(record(intent.forces) && Object.keys(intent.forces).sort().join(',') === 'elite,normal' &&
Number.isSafeInteger(intent.forces.normal) && Number.isSafeInteger(intent.forces.elite) &&
intent.forces.normal >= 0 && intent.forces.elite >= 0,
'Choose a valid physical normal and special counter group.');
}
requireMarker(context.players.every(seat => seat.ally === null ||
typeof seat.ally === 'string' && seat.ally !== seat.id && context.players.some(other => other.id === seat.ally)),
'Homeworld shipment needs valid seated alliance identities.');
const player = context.players.find(seat => seat.id === intent.player);
const source = worlds.find(world => world.id === intent.source);
const destination = worlds.find(world => world.id === intent.destination);
requireMarker(player?.faction === 'richese' && source && destination && source.id !== destination.id &&
(context.advanced || (source.native !== player.id && destination.native !== player.id)),
'Choose two different active Homeworlds; Basic marker transfers require foreign Homeworlds.');
const native = context.players.find(seat => seat.id === destination.native)!;
requireMarker(!(player.ally === native.id && native.ally === player.id),
'You cannot ship to your ally’s Homeworld.');
const marker = noField?.deployed;
requireMarker(marker?.controller === player.id && marker.tokenId === intent.tokenId &&
isHomeworldNoFieldLocation(marker.location) && marker.location.homeworld === source.id,
'The chosen concealed marker is no longer on its original Homeworld.');
let moved: RicheseNoField;
try {
moved = moveRicheseNoField(noField, intent.tokenId, { homeworld: destination.id });
} catch (error) {
throw new HomeworldCustodyError(error instanceof Error ? error.message : 'Invalid No-Field transfer.');
}
const forces = intent.forces;
if (forces && forces.normal + forces.elite > 0) {
const physical = quoteHomeworldShipment(context, custody, {
player: player.id, destination: destination.id, sources: { [source.id]: forces },
});
const amount = 1 + physical.amount;
return { player: player.id, tokenId: intent.tokenId, source: source.id,
destination: destination.id, amount, elite: physical.elite,
cost: context.purchasedGuildRate ? Math.ceil(amount / 2) : amount, noField: moved,
state: physical.state, players: physical.players, sources: physical.sources };
}
return { player: player.id, tokenId: intent.tokenId, source: source.id,
destination: destination.id, amount: 1, elite: 0, cost: 1, noField: moved };
}