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:
@@ -5,20 +5,13 @@ import { Chessboard } from "react-chessboard";
|
||||
import { Chess } from "chess.js";
|
||||
import { API_URL } from "@/config";
|
||||
import { useSession } from "@/context/session";
|
||||
import { BOTS } from "@/bots";
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
const AI_LEVELS = [
|
||||
{ level: 1, name: "Anfänger" },
|
||||
{ level: 2, name: "Leicht" },
|
||||
{ level: 3, name: "Mittel" },
|
||||
{ level: 4, name: "Fortgeschritten" },
|
||||
{ level: 5, name: "Experte" },
|
||||
{ level: 6, name: "Meister" },
|
||||
];
|
||||
|
||||
export default function AiGamePage() {
|
||||
const { user } = useSession();
|
||||
const [selectedLevel, setSelectedLevel] = useState(3);
|
||||
const [selectedBot, setSelectedBot] = useState(BOTS[2]);
|
||||
const selectedLevel = selectedBot.level;
|
||||
const [gameStarted, setGameStarted] = useState(false);
|
||||
const [game, setGame] = useState(new Chess());
|
||||
const [playerColor, setPlayerColor] = useState<"white" | "black">("white");
|
||||
@@ -152,15 +145,20 @@ export default function AiGamePage() {
|
||||
|
||||
<div className="card bg-base-200 shadow p-6 w-full flex flex-col gap-5">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold mb-3">Schwierigkeitsgrad</h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{AI_LEVELS.map((l) => (
|
||||
<h2 className="text-lg font-semibold mb-3">Gegner wählen</h2>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{BOTS.map((b) => (
|
||||
<button
|
||||
key={l.level}
|
||||
className={`btn ${selectedLevel === l.level ? "btn-primary" : "btn-outline"}`}
|
||||
onClick={() => setSelectedLevel(l.level)}
|
||||
key={b.name}
|
||||
className={`flex items-center gap-3 p-3 rounded-lg border-2 text-left transition-colors ${selectedBot.name === b.name ? "border-primary bg-primary/10" : "border-base-300 hover:border-primary/50"}`}
|
||||
onClick={() => setSelectedBot(b)}
|
||||
>
|
||||
{l.level}. {l.name}
|
||||
<span className="text-2xl">{b.emoji}</span>
|
||||
<div className="flex-1">
|
||||
<div className="font-semibold">{b.name}</div>
|
||||
<div className="text-xs opacity-60">{b.description}</div>
|
||||
</div>
|
||||
<span className="badge badge-ghost badge-sm font-mono">{b.elo}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -196,7 +194,7 @@ export default function AiGamePage() {
|
||||
|
||||
<div className="flex flex-col gap-4 min-w-[200px]">
|
||||
<div className="card bg-base-200 shadow p-4">
|
||||
<p className="font-semibold text-sm opacity-70">Niveau: {AI_LEVELS.find(l => l.level === selectedLevel)?.name}</p>
|
||||
<p className="font-semibold text-sm opacity-70">Gegner: {selectedBot.emoji} {selectedBot.name}</p>
|
||||
<p className="font-semibold text-sm opacity-70">Du spielst: {playerColor === "white" ? "Weiß ♔" : "Schwarz ♚"}</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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" : "")}
|
||||
|
||||
8
client/src/bots.ts
Normal file
8
client/src/bots.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const BOTS = [
|
||||
{ name: "Holzpferd Heinz", elo: 200, level: 1, emoji: "🐴", description: "Perfekt für Einsteiger" },
|
||||
{ name: "Bauernschubser Bert", elo: 500, level: 2, emoji: "♟", description: "Lernt die Grundzüge" },
|
||||
{ name: "Taktiker Theo", elo: 800, level: 3, emoji: "🧩", description: "Kennt taktische Muster" },
|
||||
{ name: "Kombinationskarl", elo: 1100, level: 4, emoji: "⚡", description: "Gefährlicher Angreifer" },
|
||||
{ name: "Meister Magnus", elo: 1400, level: 5, emoji: "👑", description: "Fast unschlagbar" },
|
||||
];
|
||||
export type Bot = typeof BOTS[0];
|
||||
@@ -34,3 +34,13 @@ export const startTournament = async (code: string): Promise<Tournament | null>
|
||||
if (!res.ok) return null;
|
||||
return res.json();
|
||||
};
|
||||
|
||||
export const addBotToTournament = async (code: string, botName: string): Promise<Tournament | null> => {
|
||||
const res = await fetch(`${API_URL}/v1/tournaments/${code}/add-bot`, {
|
||||
method: "POST", credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ botName })
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
return res.json();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user