Files
Dune/game/basic-pending-movement.ts
T
Quintine ab7a947833
Verify and publish checkpoint container / container (push) Successful in 10m49s
Support bribes during Basic movement decisions
2026-10-29 10:19:31 +11:00

78 lines
4.5 KiB
TypeScript

import { location, splitLocation, validLocation } from './board';
import type { Game } from './engine';
import { presenceAt } from './force-presence';
export type MovementPending = 'pendingFremenMove' | 'pendingIxMove' | 'pendingChoamMove';
type MovementOrder = NonNullable<Game['pendingIxMove']>;
/** Identify a single, well-formed ordinary Basic movement awaiting its native reaction. */
export function basicPendingMovement(g: Game): { key: MovementPending; player: string } | 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;
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 !== 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;
const result = { key, player: move.player };
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 ? result : 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 ? result : null;
return mover.faction === 'fremen' && keys === 'amount,kind,location,owner,passed' &&
g.pendingFremenMove?.move === mover.moved ? result : null;
}