feat(bots): add named bot profiles with ELO-based difficulty

- 5 bot profiles (Holzpferd Heinz to Meister Magnus, ELO 200-1400) in
  server/bots.ts and client/bots.ts with emoji + descriptions
- Bot users auto-created in DB on server startup (role='bot')
- AI page replaced difficulty buttons with bot profile cards
- Tournament host can add bots via dropdown (POST /:code/add-bot)
- Bot auto-moves triggered on joinLobby and after each human move
- Fixed clock start order so bot-as-white games initialize correctly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Michess
2026-04-14 13:32:43 +02:00
parent b29c467766
commit 157c14c11e
9 changed files with 191 additions and 20 deletions

View File

@@ -2,7 +2,8 @@
import InviteFriendsModal from "@/components/InviteFriendsModal";
import { useSession } from "@/context/session";
import { fetchTournament, joinTournament, startTournament } from "@/lib/tournament";
import { fetchTournament, joinTournament, startTournament, addBotToTournament } from "@/lib/tournament";
import { BOTS } from "@/bots";
import type { Tournament, TournamentRound } from "@michess/types";
import Link from "next/link";
import { useParams } from "next/navigation";
@@ -16,6 +17,7 @@ export default function TournamentDetailPage() {
const [tournament, setTournament] = useState<Tournament | null>(null);
const [loading, setLoading] = useState(true);
const [actionLoading, setActionLoading] = useState(false);
const [selectedBot, setSelectedBot] = useState(BOTS[0].name);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
async function load() {
@@ -47,6 +49,13 @@ export default function TournamentDetailPage() {
setActionLoading(false);
}
async function handleAddBot() {
setActionLoading(true);
const updated = await addBotToTournament(code, selectedBot);
if (updated) setTournament(updated);
setActionLoading(false);
}
async function handleStart() {
if (!user?.id) return;
setActionLoading(true);
@@ -123,6 +132,27 @@ export default function TournamentDetailPage() {
{tournament.status === "waiting" && isJoined && (
<InviteFriendsModal gameCode={`tournament/${code}`} label="Freunde einladen" />
)}
{isHost && tournament.status === "waiting" && (
<div className="flex gap-2 items-center">
<select
className="select select-sm select-bordered"
value={selectedBot}
onChange={(e) => setSelectedBot(e.target.value)}
disabled={actionLoading}
>
{BOTS.map((b) => (
<option key={b.name} value={b.name}>{b.emoji} {b.name} ({b.elo})</option>
))}
</select>
<button
className={"btn btn-outline btn-sm" + (actionLoading ? " loading" : "")}
onClick={handleAddBot}
disabled={actionLoading}
>
Bot hinzufügen
</button>
</div>
)}
{canStart && (
<button
className={"btn btn-primary btn-sm" + (actionLoading ? " loading" : "")}