Support board-wide Truthtrance force questions
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-29 03:41:29 +11:00
1 parent f1007a4ea8
commit 89589878fc
12 files changed
+144 -29

No files matched your search

+33 -14
View File
@@ -1,4 +1,4 @@
import { location, territory } from './board';
import { location, territory, validLocation } from './board';
import type { FactionId } from './catalog';
export type ForceCountFact = {
@@ -6,6 +6,7 @@ export type ForceCountFact = {
zone:
| { kind: 'reserves' }
| { kind: 'tanks' }
| { kind: 'board' }
| { kind: 'territory'; territory: string }
| { kind: 'location'; territory: string; sector: number };
counter: 'total' | 'normal' | 'elite';
@@ -80,9 +81,9 @@ export function parseForceCountFact(
const zone = value.zone;
if (!record(zone)) throw new Error('Choose a current force pool.');
let parsedZone: ForceCountFact['zone'];
if (zone.kind === 'reserves' || zone.kind === 'tanks') {
if (zone.kind === 'reserves' || zone.kind === 'tanks' || zone.kind === 'board') {
if (!exactKeys(zone, ['kind']))
throw new Error('Reserve and Tanks facts cannot name a board location.');
throw new Error('Reserve, Tanks and board facts cannot name a location.');
parsedZone = { kind: zone.kind };
} else if (zone.kind === 'territory') {
if (
@@ -110,7 +111,7 @@ export function parseForceCountFact(
sector: zone.sector as number,
};
} else {
throw new Error('Choose reserves, the Tanks, a current territory, or a board location.');
throw new Error('Choose reserves, the Tanks, the board, a current territory, or a board location.');
}
return {
kind: 'forceCount',
@@ -133,7 +134,7 @@ export function forceCountCounterError(
: null;
}
function chosenPool(player: ForceCountPlayer, fact: ForceCountFact) {
function chosenPool(player: ForceCountPlayer, fact: ForceCountFact, boardLocations?: readonly ValidLocation[]) {
const eliteState = player.elites;
if (eliteState !== undefined && !record(eliteState))
throw new Error('The tracked special-force split is malformed.');
@@ -151,12 +152,27 @@ function chosenPool(player: ForceCountPlayer, fact: ForceCountFact) {
throw new Error('The selected board force pool is malformed.');
if (eliteState !== undefined && !record(eliteState.forces))
throw new Error('The selected elite board force pool is malformed.');
const sectors = fact.zone.kind === 'territory'
? territory(fact.zone.territory).sectors
: [fact.zone.sector];
const zone = fact.zone;
const selected = zone.kind === 'board'
? boardLocations
: zone.kind === 'territory'
? territory(zone.territory).sectors.map(sector => ({ territory: zone.territory, sector }))
: [{ territory: zone.territory, sector: zone.sector }];
if (!selected || !selected.length) throw new Error('Current board locations are unavailable.');
if (zone.kind === 'board') {
const seen = new Set<string>();
for (const entry of selected) {
if (!entry || typeof entry.territory !== 'string' || !Number.isSafeInteger(entry.sector) ||
!validLocation(entry.territory, entry.sector))
throw new Error('Current board locations are malformed.');
const key = location(entry.territory, entry.sector);
if (seen.has(key)) throw new Error('Current board locations contain a duplicate.');
seen.add(key);
}
}
let total = 0, elite = 0;
for (const sector of sectors) {
const key = location(fact.zone.territory, sector);
for (const selectedLocation of selected) {
const key = location(selectedLocation.territory, selectedLocation.sector);
const sectorTotal = count(player.forces[key] === undefined ? 0 : player.forces[key], 'The selected physical force count');
const storedElite = eliteState?.forces[key];
const sectorElite = count(storedElite === undefined ? 0 : storedElite, 'The selected elite force count');
@@ -181,10 +197,11 @@ function chosenPool(player: ForceCountPlayer, fact: ForceCountFact) {
export function forceCountFactMatches(
player: ForceCountPlayer,
fact: ForceCountFact,
boardLocations?: readonly ValidLocation[],
): boolean {
const unavailable = forceCountCounterError(player, fact.counter);
if (unavailable) throw new Error(unavailable);
const pool = chosenPool(player, fact);
const pool = chosenPool(player, fact, boardLocations);
const actual =
fact.counter === 'total'
? pool.total
@@ -217,8 +234,10 @@ export function forceCountFactText(fact: ForceCountFact): string {
? 'in your reserves'
: fact.zone.kind === 'tanks'
? 'in the Tleilaxu Tanks'
: fact.zone.kind === 'territory'
? `in ${territory(fact.zone.territory).name} across all sectors`
: `in ${territory(fact.zone.territory).name}, sector ${fact.zone.sector}`;
: fact.zone.kind === 'board'
? 'on the current Arrakis board'
: fact.zone.kind === 'territory'
? `in ${territory(fact.zone.territory).name} across all sectors`
: `in ${territory(fact.zone.territory).name}, sector ${fact.zone.sector}`;
return `you currently have ${comparison} ${fact.value} ${counters} ${zone}`;
}