'use client'; import { useId, useState } from 'react'; import type { Action, GameView } from '@/game/engine'; import { gameTerritories, location, splitLocation, } from '@/game/board'; import { quoteSandmasterMovement, sandmasterActionOrder, sandmasterCollectible, sandmasterDefaultChoice, sandmasterLeader, sandmasterModeSupported, sandmasterPathBlocked, sandmasterAdjacent, sandmasterRouteDistance, type SandmasterChoice, } from '@/game/sandmaster-movement'; import { Button } from './ui/button'; type RouteDraft = { key: string; routes: Record; declined: string[]; }; export function sandmasterMoveDraft( game: GameView, move: Action, routes?: Record, declined: readonly string[] = [], ): { action: Action | null; choice: SandmasterChoice | null; collectible: string[]; blocked: string | null; } { const defaults = sandmasterDefaultChoice(game, game.me, move); const order = sandmasterActionOrder(game, game.me, move); if (!defaults || !order) return { action: null, choice: null, collectible: [], blocked: 'Sandmaster needs a supported ordinary force movement and its living native trainer.', }; const selectedRoutes = routes ?? defaults.routes; const collectible = sandmasterCollectible(game, selectedRoutes); const choice = { routes: selectedRoutes, collect: collectible.filter((key) => !declined.includes(key)), }; try { quoteSandmasterMovement(game, game.me, order, choice); return { action: { ...move, sandmaster: choice }, choice, collectible, blocked: null, }; } catch (error) { return { action: null, choice, collectible, blocked: error instanceof Error ? error.message : 'Choose a legal Sandmaster route.', }; } } function placeName(game: GameView, key: string) { const at = splitLocation(key); const name = gameTerritories(game).find((candidate) => candidate.id === at.territory) ?.name ?? at.territory; return `${name}, sector ${at.sector}`; } export function SandmasterMovement({ game, move, act, busy, unavailableReason, }: { game: GameView; move: Action | null; act: (action: Action) => void; busy: boolean; unavailableReason?: string | null; }) { const id = useId(); const me = game.players.find((player) => player.id === game.me); const leader = sandmasterLeader(game, game.me); const defaults = move ? sandmasterDefaultChoice(game, game.me, move) : null; const order = move ? sandmasterActionOrder(game, game.me, move) : null; const draftKey = JSON.stringify([ game.turn, me?.moved ?? 0, move, defaults?.routes, ]); const [saved, setSaved] = useState(null); if ( !me || !leader || !sandmasterModeSupported(game) || !move || !defaults || !order ) return null; const routes = saved?.key === draftKey ? saved.routes : defaults.routes; const declined = saved?.key === draftKey ? saved.declined : []; const draft = sandmasterMoveDraft(game, move, routes, declined); const updateRoutes = (next: Record) => setSaved({ key: draftKey, routes: next, declined }); const blocked = unavailableReason ?? draft.blocked; const leaderName = game.allLeaders.find((candidate) => candidate.id === leader)?.name ?? leader; const destination = location(order.to, order.sector); return (

Sandmaster movement

{leaderName} may collect 1 spice from one unambiguous pile in each distinct territory these forces enter or pass through. Choose the exact sector route for every moving source.

{order.group.map(([from]) => { const route = routes[from] ?? [from]; const last = route.at(-1)!; const extensions = last === destination ? [] : sandmasterAdjacent(game, last).filter( (next) => !route.includes(next) && !sandmasterPathBlocked(game, game.me, from, next) && sandmasterRouteDistance(game, [...route, next]) <= order.range, ); return (
{placeName(game, from)} route

{route.map((key) => placeName(game, key)).join(' → ')} ·{' '} {sandmasterRouteDistance(game, route)} / {order.range} territories

{last !== destination && ( )}
); })} {draft.collectible.length ? (
Optional spice collection {draft.collectible.map((key) => ( ))}
) : (

This route enters no territory with one unambiguous positive spice pile. You may still declare the route without collecting.

)} {blocked && ( {blocked} )}

Use the normal Move forces button to decline Sandmaster. A canceled movement collects no spice.

); }