Prototype charity Truthtrance and coalition rule foundations
Verify and publish checkpoint container / container (push) Successful in 10m14s

This commit is contained in:
Quintine committed 2026-10-28 21:58:30 +11:00
1 parent e896570f07
commit cea310b030
17 files changed
+1024 -35

No files matched your search

+59 -3
View File
@@ -1,11 +1,13 @@
import type { Player } from './engine';
import { advisorStanceAfterSettlement } from './advisor-automatic-flip';
import type { ForcePresence } from './force-presence';
import { presenceByLocation } from './force-presence';
import { presenceAt, presenceByLocation } from './force-presence';
import { isHomeworldNoFieldLocation } from './richese-no-field';
import { fighterCount, isAdvisor } from './advisors';
import { quoteAidRefunds, PhaseResourceError } from './phase-resource-quote';
import { ecazOccupancyIdentity } from './ecaz-occupy';
import { ecazOccupancyIdentity, ecazOccupancyRelation } from './ecaz-occupy';
import { quoteAlliedSeparation } from './allied-separation';
import { areCoalitionAllies, coalitionAllies, coalitionEdgeTurn, coalitionGroups, type CoalitionState } from './coalition-alliance';
import type { SharedSpiceLot } from './ecaz-spice-allocation';
import {
gameDistance,
@@ -23,7 +25,7 @@ function requireBoard(condition: unknown, message: string): asserts condition {
}
const whole = (n: unknown): n is number =>
Number.isSafeInteger(n) && (n as number) >= 0;
export type BoardSeat = Pick<Player, 'id' | 'faction' | 'ally' | 'advisors'> &
export type BoardSeat = Pick<Player, 'id' | 'faction' | 'ally' | 'allySinceTurn' | 'advisors'> &
ForcePresence;
export type BoardContext = MobileBoard & {
advanced: boolean;
@@ -32,7 +34,20 @@ export type BoardContext = MobileBoard & {
storm: number;
order: readonly string[];
players: readonly BoardSeat[];
coalitionState?: CoalitionState;
};
/** Three-member board rules are provisionally restricted to Basic without Ecaz. */
function validateBoardCoalitions(g: BoardContext) {
if (g.coalitionState === undefined) return;
try {
for (const group of coalitionGroups(g.players, g.coalitionState))
if (group.members.length === 3 && (g.advanced || g.players.some(player => player.faction === 'ecaz')))
throw new Error('Three-member board rules require Basic play without Ecaz.');
} catch (error) {
throw new BoardResolutionError((error as Error).message);
}
}
export type AdvisorRelease = { player: string; territory: string };
function validKey(g: MobileBoard, key: string) {
const p = splitLocation(key);
@@ -95,6 +110,7 @@ export function settledBoard(g: BoardContext) {
id: p.id,
faction: p.faction,
ally: p.ally,
allySinceTurn: p.allySinceTurn,
forces: p.forces,
noField: p.noField,
advisors,
@@ -111,6 +127,7 @@ export type BattleBoardBattle = {
};
export function quoteBattleBoard(g: BoardContext) {
const { players, released } = settledBoard(g);
validateBoardCoalitions(g);
const battles: BattleBoardBattle[] = [];
const connected = (a: BoardSeat, b: BoardSeat, site: string) => {
const left = Object.entries(presenceByLocation(a))
@@ -137,6 +154,7 @@ export function quoteBattleBoard(g: BoardContext) {
if (
other.id === id ||
other.id === p.ally ||
(g.coalitionState !== undefined && areCoalitionAllies(players, g.coalitionState, id, other.id)) ||
!fighterCount(other, t.id) ||
g.order.indexOf(other.id) < g.order.indexOf(id)
)
@@ -156,6 +174,44 @@ export function quoteBattleBoard(g: BoardContext) {
}
return { released, battles };
}
/** Union the per-edge Basic separation obligations without inventing an age for
* an older pair when a third player joins. No forces are removed by this quote. */
export function quoteCoalitionAlliedSeparation(
g: BoardContext & { turn: number; remaining: readonly string[] },
player: string,
): string[] {
settledBoard(g);
validateBoardCoalitions(g);
requireBoard(whole(g.turn) && g.turn > 0 && Array.isArray(g.remaining) &&
new Set(g.remaining).size === g.remaining.length &&
g.remaining.every(id => g.order.includes(id)),
'Allied separation needs the current turn and remaining player order.');
const own = g.players.find(p => p.id === player);
requireBoard(own, 'Allied separation needs a seated player.');
const allies = g.coalitionState === undefined
? (own.ally ? [own.ally] : [])
: coalitionAllies(g.players, g.coalitionState, player);
const losses = new Set<string>();
for (const id of allies) {
const ally = g.players.find(p => p.id === id);
requireBoard(ally, 'Allied separation needs a seated ally.');
const edgeTurn = g.coalitionState === undefined ? null : coalitionEdgeTurn(g.players, g.coalitionState, player, id);
for (const site of quoteAlliedSeparation({
advanced: g.advanced, turn: g.turn, player, ally: id,
playerAllySinceTurn: g.coalitionState === undefined ? own.allySinceTurn : edgeTurn ?? undefined,
allySinceTurn: g.coalitionState === undefined ? ally.allySinceTurn : edgeTurn ?? undefined,
remaining: g.remaining,
territories: gameTerritories(g).map(t => ({
territory: t.id, polar: t.type === 'polar', ownPresent: presenceAt(own, t.id) > 0,
allyPresent: presenceAt(ally, t.id) > 0, ownAdvisors: isAdvisor(own, t.id),
allyAdvisors: isAdvisor(ally, t.id),
ecazCoexist: ecazOccupancyRelation(g.players, player, id, { kind: 'territory', id: t.id }) === 'ecazAlliance',
})),
})) losses.add(site);
}
return gameTerritories(g).map(t => t.id).filter(id => losses.has(id));
}
export type CollectionContext = Omit<BoardContext, 'players'> & {
players: readonly (BoardSeat & Pick<Player, 'spice' | 'elites'>)[];
spice: Readonly<Record<string, number>>;