Files
Dune/game/richese-reveal-foreign-movement.ts
T
Quintine 605cb2a32f
Verify and publish checkpoint container / container (push) Canceled after 0s
Expand Basic Richese movement and worm interactions
2026-10-29 09:54:24 +11:00

86 lines
4.9 KiB
TypeScript

import { location, splitLocation, validLocation } from './board';
import type { Game } from './engine';
import { presenceAt } from './force-presence';
type MovementPending = 'pendingFremenMove' | 'pendingIxMove' | 'pendingChoamMove';
type MovementOrder = NonNullable<Game['pendingIxMove']>;
/**
* Identify a single ordinary foreign movement awaiting its native prevention
* response. Revealing Richese's already-present marker cannot add an occupying
* faction, so another faction's declared route and destination remain legal.
* The caller retains its own common profile and Truthtrance/commitment guards.
*/
export function richeseRevealForeignMovementPending(g: Game): MovementPending | null {
if (g.status !== 'playing' || g.advanced || g.phase !== 5 ||
!Number.isSafeInteger(g.turn) || g.turn < 1 || g.phaseOpening || g.truthtrance ||
g.ornithopter || g.choamMarket || !Array.isArray(g.players)) return null;
const richese = g.players.find(player => player.faction === 'richese');
const deployed = richese?.noField?.deployed;
if (!richese || !deployed || deployed.controller !== richese.id ||
!deployed.location || !('territory' in deployed.location)) return null;
let key: MovementPending;
let move: MovementOrder;
const response = g.response;
if (response?.kind === 'fremenMovement' && g.pendingFremenMove && !g.decision) {
key = 'pendingFremenMove';
move = g.pendingFremenMove.order;
if (g.pendingFremenMove.turn !== g.turn) return null;
} else if (response?.kind === 'ixMovement' && g.pendingIxMove && !g.decision) {
key = 'pendingIxMove';
move = g.pendingIxMove;
} else if (!response && g.decision?.kind === 'choamMovement' && g.pendingChoamMove) {
key = 'pendingChoamMove';
move = g.pendingChoamMove;
} else return null;
if (Object.entries(g).some(([name, value]) => name.startsWith('pending') && name !== key &&
(Array.isArray(value) ? value.length > 0 : value !== null && value !== undefined && value !== false))) return null;
if (!move || move.player === richese.id || move.player !== g.active ||
move.source || move.ambassadorEvent || move.noField || move.planetologist || move.sandmaster ||
move.ambassadorSandmaster || move.ornithopterEvent || move.ornithopterRange || move.discoveryFlight ||
!Array.isArray(move.group) || move.group.length === 0 || !move.eliteGroup ||
typeof move.origin !== 'string' || typeof move.to !== 'string' ||
!Number.isSafeInteger(move.sector) || !validLocation(move.to, move.sector) ||
!Number.isSafeInteger(move.total) || move.total <= 0 ||
!Number.isSafeInteger(move.elite) || move.elite < 0 || move.elite > move.total ||
(move.origins && (move.origins.length !== 1 || move.origins[0] !== move.origin))) return null;
const mover = g.players.find(player => player.id === move.player);
if (!mover || !Number.isSafeInteger(mover.moved) || mover.moved < 0) return null;
const seen = new Set<string>();
let total = 0, elite = 0;
for (const [source, amount] of move.group) {
if (typeof source !== 'string' || seen.has(source) || splitLocation(source).territory !== move.origin ||
!validLocation(splitLocation(source).territory, splitLocation(source).sector) ||
!Number.isSafeInteger(amount) || amount <= 0 || amount > (mover.forces[source] ?? 0)) return null;
seen.add(source);
const selected = move.eliteGroup[source];
if (!Number.isSafeInteger(selected) || selected < 0 || selected > amount ||
selected > (mover.elites?.forces[source] ?? 0) ||
amount - selected > (mover.forces[source] ?? 0) - (mover.elites?.forces[source] ?? 0)) return null;
total += amount;
elite += selected;
}
if (Object.keys(move.eliteGroup).some(source => !seen.has(source)) ||
total !== move.total || elite !== move.elite) return null;
if (key === 'pendingChoamMove') {
const decision = g.decision;
const choam = g.players.find(player => player.faction === 'choam');
return decision?.kind === 'choamMovement' && choam && presenceAt(choam, move.to) > 0 && decision.player === choam.id &&
decision.mover === move.player && decision.territory === move.to &&
decision.sector === move.sector && decision.amount === move.total ? key : null;
}
if (!response || response.owner !== move.player ||
response.location !== location(move.to, move.sector) || response.amount !== move.total ||
!Array.isArray(response.passed) || new Set(response.passed).size !== response.passed.length ||
response.passed.some(id => !g.players.some(player => player.id === id))) return null;
const keys = Object.keys(response).sort().join(',');
if (key === 'pendingIxMove')
return mover.faction === 'ixians' && keys === 'amount,elite,kind,location,owner,passed' &&
response.elite === move.elite && move.elite > 0 ? key : null;
return mover.faction === 'fremen' && keys === 'amount,kind,location,owner,passed' &&
g.pendingFremenMove?.move === mover.moved ? key : null;
}