78 lines
4.5 KiB
TypeScript
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;
|
|
}
|