diff --git a/README.md b/README.md index 34868f2f..2986a9b8 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,10 @@ For the provisional two-person setup, the host enables **Two players, two factions each** in the lobby. Each person joins with one faction, then adds a second and uses **Active faction** to switch seats. Ready each faction before the host starts. The factions are independent unless they form an alliance; -the ordinary victory target is four strongholds per side. The prototype -requires human-controlled seats and cannot be disabled after a second faction -is added. Counts for optional modules and wider interactions remain provisional. +the ordinary victory target is four strongholds per side. The host may add an +AI opponent and choose its two factions; each uses its own current AI policy +and difficulty. The mode cannot be disabled while a controller has a second +faction. Counts for optional modules and wider interactions remain provisional. See [the implementation scope](docs/TWO_PLAYER_TWO_FACTIONS.md). New ordinary-auction lots show a **ten-second bidding countdown**. Native diff --git a/app/page.tsx b/app/page.tsx index c3fa0a3d..44d20352 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -861,6 +861,8 @@ export default function Home() { onModeChange={(enabled) => void send({ type: 'twoPlayerMode', enabled })} onSwitch={switchFaction} onAdd={(name, factionId) => void addControlledFaction(name, factionId)} + onAddBot={(faction, difficulty) => void send({ type: 'addBot', faction, difficulty })} + onAddControlledBot={(controller, faction, difficulty) => void send({ type: 'addControlledBot', controller, faction, difficulty })} onRetry={() => { if (twoPlayerEntry) void dispatchControlledFaction(twoPlayerEntry, false); }} retryDisabled={busy || needsReconcile || checkingConnection || handoverPending || aiPermissionPending || claimUncertain || !!twoPlayerEntryProblem || twoPlayerEntry?.room !== game.code} />} diff --git a/components/two-player-controls.tsx b/components/two-player-controls.tsx index 34bd0414..c01456d8 100644 --- a/components/two-player-controls.tsx +++ b/components/two-player-controls.tsx @@ -2,13 +2,14 @@ import { useState } from 'react'; import { FACTIONS } from '@/game/catalog'; +import { DIFFICULTIES, type Difficulty } from '@/game/bot-profiles'; import type { GameView } from '@/game/engine'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; export function TwoPlayerControls({ game, disabled, pendingEntry, entryRoom, entryProblem, retryDisabled, - onModeChange, onSwitch, onAdd, onRetry, + onModeChange, onSwitch, onAdd, onRetry, onAddBot, onAddControlledBot, }: { game: GameView; disabled: boolean; @@ -20,12 +21,22 @@ export function TwoPlayerControls({ onSwitch: (seat: string) => void; onAdd: (name: string, faction: string) => void; onRetry: () => void; + onAddBot: (faction: string, difficulty: Difficulty) => void; + onAddControlledBot: (controllerId: string, faction: string, difficulty: Difficulty) => void; }) { const [name, setName] = useState(() => game.players.find((player) => player.id === game.me)?.name ?? ''); const [choice, setChoice] = useState(''); + const [botChoice, setBotChoice] = useState(''); + const [botDifficulty, setBotDifficulty] = useState('Medium'); const mode = game.twoPlayerControllers; const controlled = game.controlledFactionSeats ?? []; const modeLocked = !!mode?.controllers.some((controller) => controller.seats.length > 1); + const botController = mode?.controllers.find((controller) => + controller.seats.length === 1 && game.players.some((player) => + player.id === controller.id && !!player.bot)); + const canAddBotController = !!mode && mode.controllers.length < 2; + const showBotForm = game.status === 'lobby' && game.host === game.me && + !!mode && (canAddBotController || !!botController); const available = FACTIONS.filter((item) => (item.expansion === 'base' || game.expansions.includes(item.expansion)) && !game.players.some((player) => player.faction === item.id), @@ -39,13 +50,13 @@ export function TwoPlayerControls({ Two players, two factions each )} - {game.status === 'lobby' && game.host === game.me && modeLocked &&

This mode cannot be turned off after a second faction has been added.

} + {game.status === 'lobby' && game.host === game.me && modeLocked &&

This mode cannot be turned off while a player controls two factions.

} {mode && <>

Two-player factions

Each player controls two separate factions. They act independently and are not automatically allied; this interpretation is provisional. Switch between your faction seats to make their decisions and mark each one ready.

{game.status === 'lobby' &&