Files
Dune/game/fremen-victory.ts
T

100 lines
3.1 KiB
TypeScript

import { fighterCount } from './advisors';
import { settledBoard, type BoardContext } from './board-resolution-quote';
import { ecazOccupancyIdentity } from './ecaz-occupy';
import { presenceAt } from './force-presence';
import { VictoryProgressError } from './victory-progress';
const PROTECTED_SIETCHES = ['sietch_tabr', 'habbanya_ridge_sietch'] as const;
export type FremenVictoryProgress = {
player: string;
members: string[];
qualifies: boolean;
sietches: {
territory: string;
blockers: string[];
ecazCooccupation: boolean;
}[];
tueksBlockers: string[];
};
/** Public settled-board facts. Basic keeps the publisher's Tabr-only Ecaz
* exception; supplied Advanced PDF p32 permits coexisting allies in both
* protected sietches. Timing, earlier winners and Guild fallback remain
* the victory quote's responsibility. */
export function fremenSpecialVictory(
g: BoardContext,
): FremenVictoryProgress | null {
try {
const { players } = settledBoard(g);
if (!players.length)
throw new VictoryProgressError(
'Fremen victory progress requires seated players.',
);
// Reuse the public occupancy validator for faction uniqueness and exact
// reciprocal alliances without reading any private player state.
ecazOccupancyIdentity(players, players[0].id, {
kind: 'territory',
id: 'polar_sink',
});
const fremen = players.find((player) => player.faction === 'fremen');
if (!fremen) return null;
const ally = players.find((player) => player.id === fremen.ally);
const ecaz = ally?.faction === 'ecaz' ? ally : null;
const members = g.order.filter(
(player) => player === fremen.id || player === ally?.id,
);
const sietches = PROTECTED_SIETCHES.map((territory) => {
const ecazCooccupation = !!(
ecaz &&
fighterCount(fremen, territory) > 0 &&
fighterCount(ecaz, territory) > 0
);
const blockers = g.order.filter((id) => {
const player = players.find((candidate) => candidate.id === id)!;
if (player.id === fremen.id || fighterCount(player, territory) === 0)
return false;
return !(
(g.advanced || territory === 'sietch_tabr') &&
ecazCooccupation &&
player.id === ecaz!.id
);
});
return { territory, blockers, ecazCooccupation };
});
const prohibited = new Set([
'atreides',
'harkonnen',
'emperor',
...(g.advanced ? ['richese'] : []),
]);
const tueksBlockers = g.order.filter((id) => {
const player = players.find((candidate) => candidate.id === id)!;
return (
prohibited.has(player.faction) && presenceAt(player, 'tueks_sietch') > 0
);
});
return {
player: fremen.id,
members,
qualifies:
sietches.every((sietch) => sietch.blockers.length === 0) &&
tueksBlockers.length === 0,
sietches,
tueksBlockers,
};
} catch (error) {
if (error instanceof VictoryProgressError) throw error;
throw new VictoryProgressError(
error instanceof Error
? error.message
: 'Invalid public Fremen victory state.',
);
}
}