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; /** 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(); 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; }