Files
Dune/components/seat-autopilot.tsx
T
Quintine 8c922b446b Add administrator pause and joining-lock controls
Provide authenticated owner/operator room controls with live account binding, durable audit and exact saved retries. Pause gates human, bot and automatic writes while preserving recovery, discussion and taking back control. Resume keeps decisions and restores AI pacing. Add directory availability filters and player notices.

Verified types, lint, 5223 offline tests, 52 HTTP tests and build. Independent persistence/privacy review and authenticated HTTP acceptance passed. Desktop/mobile browser QA confirmed lost-response refresh/retry makes one audit row and resume restores controls. Existing games preserved. Authenticated deployed admin acceptance remains pending explicit QA access confirmation.
2026-09-22 10:41:09 +10:00

120 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useId, useState } from 'react';
import { Bot, UserRound } from 'lucide-react';
import { Button } from '@/components/ui/button';
import type { Action, GameView } from '@/game/engine';
import {
BOT_DESCRIPTIONS,
DIFFICULTIES,
type Difficulty,
} from '@/game/bot-profiles';
export type SeatAutopilotProps = {
game: GameView;
act: (action: Action) => void;
busy: boolean;
};
/** Voluntary control of the current human seat; the action never names another player. */
export function SeatAutopilot({ game, act, busy }: SeatAutopilotProps) {
const id = useId();
const [difficulty, setDifficulty] = useState<Difficulty>('Medium');
const own = game.players.find((player) => player.id === game.me);
if (!own || own.bot || !['setup', 'playing'].includes(game.status))
return null;
if (own.autopilot)
return (
<section
className="notice flex min-w-0 flex-wrap items-center gap-4"
aria-label="Your seat autopilot"
>
<div className="flex min-w-0 flex-1 basis-60 flex-col gap-2">
<output className="flex items-center gap-2 text-base font-semibold text-[#efd9a8]">
<Bot aria-hidden="true" className="size-5 shrink-0" />
AI autopilot · {own.autopilot}
</output>
<p className="m-0 text-sm leading-6 text-[#e0e4d8]">
{game.roomControl?.paused ? 'AI decisions are paused with the room. On resume, AI uses only this seat’s available information, with about 1.5 seconds between decisions. You can take back control now.' : <>AI is making this seat’s decisions using only its available
information, with about 1.5 seconds between decisions. You keep the
seat and its recovery setup. Taking back control stops future AI
decisions; completed decisions remain in the game.</>}
</p>
</div>
<Button
variant="outline"
className="min-h-11 w-full motion-reduce:transition-none sm:w-auto"
disabled={busy}
onClick={() => {
if (!busy) act({ type: 'setAutopilot', difficulty: null });
}}
>
<UserRound aria-hidden="true" />
Take back control
</Button>
</section>
);
if (game.roomControl?.paused)
return <p className="notice">The room is paused. AI can be enabled after an administrator resumes play.</p>;
return (
<details className="notice min-w-0">
<summary className="min-h-11 cursor-pointer py-3 text-base font-semibold text-[#efd9a8] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#ddbc77]">
Let AI play for me
</summary>
<div className="flex min-w-0 flex-col gap-4 pb-2">
<p id={`${id}-impact`} className="m-0 text-sm leading-6 text-[#e0e4d8]">
Autopilot makes setup and game decisions for your seat using only
information that seat can see, with about 1.5 seconds between
decisions. It may make irreversible decisions, including spending
spice, moving forces and committing battle plans. You retain your seat
and recovery setup and can take back control. Completed decisions
cannot be undone by turning autopilot off.
</p>
<div className="flex min-w-0 flex-col gap-2">
<label htmlFor={`${id}-difficulty`} className="text-sm font-semibold">
AI difficulty
</label>
<select
id={`${id}-difficulty`}
value={difficulty}
disabled={busy}
aria-describedby={`${id}-difficulty-help`}
className="min-h-11 w-full min-w-0 rounded-md border border-[#454b3c] bg-[#141814] px-3 text-base text-[#eeeae0] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#ddbc77] disabled:opacity-50"
onChange={(event) => {
const value = event.target.value;
if (DIFFICULTIES.some((choice) => choice === value))
setDifficulty(value as Difficulty);
}}
>
{DIFFICULTIES.map((choice) => (
<option key={choice} value={choice}>
{choice}
</option>
))}
</select>
<p
id={`${id}-difficulty-help`}
className="m-0 text-sm leading-6 text-[#c1c5b8]"
>
{BOT_DESCRIPTIONS[difficulty]}
</p>
</div>
<Button
className="min-h-11 w-full motion-reduce:transition-none sm:w-auto sm:self-start"
disabled={busy}
aria-describedby={`${id}-impact`}
onClick={() => {
if (!busy) act({ type: 'setAutopilot', difficulty });
}}
>
<Bot aria-hidden="true" />
Start autopilot
</Button>
</div>
</details>
);
}