sounds + layout
This commit is contained in:
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
404: Not Found
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,6 +2,8 @@ import type { Metadata } from "next";
|
|||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
|
import Footer from "@/components/Footer";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
@@ -28,6 +30,7 @@ export default function RootLayout({
|
|||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased flex flex-col min-h-screen`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased flex flex-col min-h-screen`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
<Footer />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
+199
-125
@@ -13,7 +13,7 @@ import { SupportedLanguage } from "@/lib/i18n/translations";
|
|||||||
import { lookupOpening, OpeningMetadata } from "@/lib/openings";
|
import { lookupOpening, OpeningMetadata } from "@/lib/openings";
|
||||||
import { GameAnalysisModal } from "./GameAnalysisModal";
|
import { GameAnalysisModal } from "./GameAnalysisModal";
|
||||||
import { GameOverModal, MoveHistoryItem } from "./GameOverModal";
|
import { GameOverModal, MoveHistoryItem } from "./GameOverModal";
|
||||||
import { Brain } from "lucide-react";
|
import { Brain, ArrowLeft } from "lucide-react";
|
||||||
import { CapturedPieces } from "./CapturedPieces";
|
import { CapturedPieces } from "./CapturedPieces";
|
||||||
|
|
||||||
interface ChessGameProps {
|
interface ChessGameProps {
|
||||||
@@ -60,6 +60,35 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality,
|
|||||||
const [gameOverState, setGameOverState] = useState<{ result: string, winner: "White" | "Black" | "Draw" } | null>(null);
|
const [gameOverState, setGameOverState] = useState<{ result: string, winner: "White" | "Black" | "Draw" } | null>(null);
|
||||||
const [moveHistory, setMoveHistory] = useState<MoveHistoryItem[]>([]);
|
const [moveHistory, setMoveHistory] = useState<MoveHistoryItem[]>([]);
|
||||||
const [selectedPersonality, setSelectedPersonality] = useState<Personality>(initialPersonality);
|
const [selectedPersonality, setSelectedPersonality] = useState<Personality>(initialPersonality);
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Sound Refs
|
||||||
|
const moveSound = useRef<HTMLAudioElement | null>(null);
|
||||||
|
const captureSound = useRef<HTMLAudioElement | null>(null);
|
||||||
|
const checkSound = useRef<HTMLAudioElement | null>(null);
|
||||||
|
const victorySound = useRef<HTMLAudioElement | null>(null);
|
||||||
|
const defeatSound = useRef<HTMLAudioElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
moveSound.current = new Audio('/sounds/move.wav');
|
||||||
|
captureSound.current = new Audio('/sounds/capture.wav');
|
||||||
|
checkSound.current = new Audio('/sounds/check.wav');
|
||||||
|
victorySound.current = new Audio('/sounds/victory.wav');
|
||||||
|
defeatSound.current = new Audio('/sounds/defeat.wav');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const playMoveSound = (captured: boolean) => {
|
||||||
|
if (captured) {
|
||||||
|
captureSound.current?.play().catch(e => console.error("Audio play failed", e));
|
||||||
|
} else {
|
||||||
|
moveSound.current?.play().catch(e => console.error("Audio play failed", e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Scroll to bottom of move history
|
||||||
|
useEffect(() => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}, [moveHistory]);
|
||||||
|
|
||||||
// Captured Pieces State
|
// Captured Pieces State
|
||||||
const [capturedWhitePieces, setCapturedWhitePieces] = useState<string[]>([]);
|
const [capturedWhitePieces, setCapturedWhitePieces] = useState<string[]>([]);
|
||||||
@@ -129,7 +158,6 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality,
|
|||||||
language,
|
language,
|
||||||
selectedPersonality,
|
selectedPersonality,
|
||||||
apiKey,
|
apiKey,
|
||||||
apiKey,
|
|
||||||
playerColor, // Save player color too
|
playerColor, // Save player color too
|
||||||
pgn: gameRef.current.pgn()
|
pgn: gameRef.current.pgn()
|
||||||
};
|
};
|
||||||
@@ -147,9 +175,13 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality,
|
|||||||
if (game.turn() === 'w') {
|
if (game.turn() === 'w') {
|
||||||
result = "Checkmate! You lost.";
|
result = "Checkmate! You lost.";
|
||||||
winner = "Black";
|
winner = "Black";
|
||||||
|
if (playerColor === 'white') defeatSound.current?.play().catch(e => console.error(e));
|
||||||
|
else victorySound.current?.play().catch(e => console.error(e));
|
||||||
} else {
|
} else {
|
||||||
result = "Checkmate! You won!";
|
result = "Checkmate! You won!";
|
||||||
winner = "White";
|
winner = "White";
|
||||||
|
if (playerColor === 'white') victorySound.current?.play().catch(e => console.error(e));
|
||||||
|
else defeatSound.current?.play().catch(e => console.error(e));
|
||||||
}
|
}
|
||||||
} else if (game.isDraw()) {
|
} else if (game.isDraw()) {
|
||||||
result = "Draw!";
|
result = "Draw!";
|
||||||
@@ -157,11 +189,13 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality,
|
|||||||
} else if (game.isStalemate()) {
|
} else if (game.isStalemate()) {
|
||||||
result = "Stalemate!";
|
result = "Stalemate!";
|
||||||
winner = "Draw";
|
winner = "Draw";
|
||||||
|
} else if (game.inCheck()) {
|
||||||
|
checkSound.current?.play().catch(e => console.error(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
setGameOverState({ result, winner });
|
setGameOverState({ result, winner });
|
||||||
}
|
}
|
||||||
}, [fen]);
|
}, [fen, playerColor]);
|
||||||
|
|
||||||
// Pre-Analysis (P0)
|
// Pre-Analysis (P0)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -206,6 +240,7 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality,
|
|||||||
const newFen = game.fen();
|
const newFen = game.fen();
|
||||||
setFen(newFen);
|
setFen(newFen);
|
||||||
updateCapturedPieces();
|
updateCapturedPieces();
|
||||||
|
playMoveSound(!!result.captured);
|
||||||
|
|
||||||
// If it was computer's move, update state
|
// If it was computer's move, update state
|
||||||
if (game.turn() === 'w') { // Computer just moved (assuming computer is Black? No, wait)
|
if (game.turn() === 'w') { // Computer just moved (assuming computer is Black? No, wait)
|
||||||
@@ -318,42 +353,52 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality,
|
|||||||
const whiteAdvantage = materialScore.black - materialScore.white;
|
const whiteAdvantage = materialScore.black - materialScore.white;
|
||||||
const blackAdvantage = materialScore.white - materialScore.black;
|
const blackAdvantage = materialScore.white - materialScore.black;
|
||||||
|
|
||||||
|
const [showStrengthSlider, setShowStrengthSlider] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header language={language} />
|
<Header language={language} />
|
||||||
<div className="flex-grow flex flex-col md:flex-row gap-8 w-full max-w-6xl mx-auto p-4">
|
<div className="flex-grow grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-8 w-full max-w-6xl mx-auto p-4">
|
||||||
<div className="w-full md:w-2/3 flex flex-col gap-4">
|
|
||||||
{/* Header with Back Button */}
|
|
||||||
<div className="flex justify-between items-center">
|
{/* 1. Header (Col 1-3) */}
|
||||||
<button
|
<div className="md:col-span-3 flex justify-between items-center">
|
||||||
onClick={onBack}
|
<button
|
||||||
className="px-4 py-2 bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 text-sm font-medium transition-colors"
|
onClick={onBack}
|
||||||
>
|
className="p-2 md:px-4 md:py-2 bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 text-sm font-medium transition-colors"
|
||||||
{t.game.backToMenu}
|
aria-label={t.game.backToMenu}
|
||||||
</button>
|
>
|
||||||
<div className="text-sm text-gray-500">
|
<span className="hidden md:inline">{t.game.backToMenu}</span>
|
||||||
{t.game.playingAs} {playerColor === 'white' ? t.game.white : t.game.black} {t.game.vs} {selectedPersonality?.name}
|
<ArrowLeft className="md:hidden" size={20} />
|
||||||
</div>
|
</button>
|
||||||
|
<div className="text-sm text-gray-500 hidden md:block">
|
||||||
|
{t.game.playingAs} {playerColor === 'white' ? t.game.white : t.game.black} {t.game.vs} {selectedPersonality?.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 2. Board Area (Col 1-2) */}
|
||||||
|
<div className="md:col-span-2 bg-white dark:bg-gray-800 p-4 rounded-lg shadow-lg flex flex-col md:flex-row gap-4 relative">
|
||||||
|
{/* Desktop Eval Bar (Vertical) */}
|
||||||
|
<div className="hidden md:block h-[560px]">
|
||||||
|
<EvaluationBar
|
||||||
|
score={isAnalyzing ? null : evalP0?.score}
|
||||||
|
mate={isAnalyzing ? null : evalP0?.mate}
|
||||||
|
isPlayerWhite={playerColor === 'white'}
|
||||||
|
orientation="vertical"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-lg flex gap-4">
|
<div className="flex-1 flex flex-col justify-center">
|
||||||
<div className="h-[560px]">
|
{/* Opponent's Captured Pieces (Top) */}
|
||||||
<EvaluationBar
|
<div className="mb-2 h-8">
|
||||||
score={isAnalyzing ? null : evalP0?.score}
|
<CapturedPieces
|
||||||
mate={isAnalyzing ? null : evalP0?.mate}
|
captured={playerColor === 'white' ? capturedWhitePieces : capturedBlackPieces}
|
||||||
isPlayerWhite={playerColor === 'white'}
|
color={playerColor === 'white' ? 'w' : 'b'}
|
||||||
|
score={playerColor === 'white' ? (blackAdvantage > 0 ? blackAdvantage : null) : (whiteAdvantage > 0 ? whiteAdvantage : null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex flex-col justify-center">
|
|
||||||
{/* Opponent's Captured Pieces (Top) */}
|
|
||||||
<div className="mb-2 h-8">
|
|
||||||
<CapturedPieces
|
|
||||||
captured={playerColor === 'white' ? capturedWhitePieces : capturedBlackPieces}
|
|
||||||
color={playerColor === 'white' ? 'w' : 'b'} // If player is white, opponent is black. Show White's lost pieces (capturedWhitePieces)
|
|
||||||
score={playerColor === 'white' ? (blackAdvantage > 0 ? blackAdvantage : null) : (whiteAdvantage > 0 ? whiteAdvantage : null)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div className="bg-[#779954] p-[2px] rounded-sm">
|
||||||
<Chessboard
|
<Chessboard
|
||||||
options={{
|
options={{
|
||||||
position: fen,
|
position: fen,
|
||||||
@@ -364,23 +409,43 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality,
|
|||||||
boardOrientation: playerColor
|
boardOrientation: playerColor
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Player's Captured Pieces (Bottom) */}
|
|
||||||
<div className="mt-2 h-8">
|
|
||||||
<CapturedPieces
|
|
||||||
captured={playerColor === 'white' ? capturedBlackPieces : capturedWhitePieces}
|
|
||||||
color={playerColor === 'white' ? 'b' : 'w'} // If player is white, show Black's lost pieces (capturedBlackPieces)
|
|
||||||
score={playerColor === 'white' ? (whiteAdvantage > 0 ? whiteAdvantage : null) : (blackAdvantage > 0 ? blackAdvantage : null)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-lg">
|
{/* Player's Captured Pieces (Bottom) */}
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="mt-2 h-8">
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
<CapturedPieces
|
||||||
Stockfish Strength (Depth: {stockfishDepth})
|
captured={playerColor === 'white' ? capturedBlackPieces : capturedWhitePieces}
|
||||||
</label>
|
color={playerColor === 'white' ? 'b' : 'w'}
|
||||||
|
score={playerColor === 'white' ? (whiteAdvantage > 0 ? whiteAdvantage : null) : (blackAdvantage > 0 ? blackAdvantage : null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Board Footer: Controls */}
|
||||||
|
<div className="mt-2 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowStrengthSlider(!showStrengthSlider)}
|
||||||
|
className="hover:text-gray-700 dark:hover:text-gray-200 underline decoration-dotted underline-offset-2"
|
||||||
|
>
|
||||||
|
Stockfish Level: {stockfishDepth}
|
||||||
|
</button>
|
||||||
|
{showStrengthSlider && (
|
||||||
|
<div className="absolute bottom-full left-0 mb-2 w-48 bg-white dark:bg-gray-700 p-3 rounded shadow-xl border border-gray-200 dark:border-gray-600 z-10">
|
||||||
|
<label className="block text-xs font-bold mb-1 text-gray-700 dark:text-gray-200">
|
||||||
|
Strength (Depth: {stockfishDepth})
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="20"
|
||||||
|
value={stockfishDepth}
|
||||||
|
onChange={(e) => setStockfishDepth(parseInt(e.target.value))}
|
||||||
|
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const game = gameRef.current;
|
const game = gameRef.current;
|
||||||
@@ -394,72 +459,31 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality,
|
|||||||
setOpeningData(null);
|
setOpeningData(null);
|
||||||
updateCapturedPieces();
|
updateCapturedPieces();
|
||||||
}}
|
}}
|
||||||
className="px-3 py-1 text-sm bg-red-100 text-red-700 rounded hover:bg-red-200 dark:bg-red-900 dark:text-red-200 transition-colors"
|
className="flex items-center gap-1 hover:text-red-600 dark:hover:text-red-400 transition-colors"
|
||||||
>
|
>
|
||||||
Undo Last Move
|
<ArrowLeft size={12} /> Undo
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="1"
|
|
||||||
max="20"
|
|
||||||
value={stockfishDepth}
|
|
||||||
onChange={(e) => setStockfishDepth(parseInt(e.target.value))}
|
|
||||||
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-lg flex-1 min-h-0 flex flex-col">
|
{/* Mobile Eval Bar (Horizontal) */}
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="md:hidden w-full">
|
||||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">Game History</h3>
|
<EvaluationBar
|
||||||
<button
|
score={isAnalyzing ? null : evalP0?.score}
|
||||||
onClick={() => setShowAnalysisModal(true)}
|
mate={isAnalyzing ? null : evalP0?.mate}
|
||||||
className="text-xs bg-purple-100 text-purple-700 px-2 py-1 rounded hover:bg-purple-200 dark:bg-purple-900 dark:text-purple-200 flex items-center gap-1"
|
isPlayerWhite={playerColor === 'white'}
|
||||||
>
|
orientation="horizontal"
|
||||||
<Brain size={12} /> Analyze
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 overflow-y-auto border border-gray-200 dark:border-gray-700 rounded bg-gray-50 dark:bg-gray-900 p-2">
|
|
||||||
<table className="w-full text-sm text-left">
|
|
||||||
<thead>
|
|
||||||
<tr className="text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<th className="py-1 px-2 w-12">#</th>
|
|
||||||
<th className="py-1 px-2">White</th>
|
|
||||||
<th className="py-1 px-2">Black</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{(() => {
|
|
||||||
const history = gameRef.current.history();
|
|
||||||
const rows = [];
|
|
||||||
for (let i = 0; i < history.length; i += 2) {
|
|
||||||
rows.push(
|
|
||||||
<tr key={i} className="border-b border-gray-100 dark:border-gray-800 last:border-0">
|
|
||||||
<td className="py-1 px-2 text-gray-500 dark:text-gray-500">{Math.floor(i / 2) + 1}.</td>
|
|
||||||
<td className="py-1 px-2 font-medium text-gray-900 dark:text-gray-200">{history[i]}</td>
|
|
||||||
<td className="py-1 px-2 font-medium text-gray-900 dark:text-gray-200">{history[i + 1] || ""}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (rows.length === 0) {
|
|
||||||
return (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={3} className="py-4 text-center text-gray-500 italic">
|
|
||||||
No moves yet.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return rows;
|
|
||||||
})()}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div ref={(el) => el?.scrollIntoView({ behavior: "smooth" })} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full md:w-1/3">
|
{/* 3. Tutor (Col 3) - Side by side with Board on Desktop */}
|
||||||
|
<div className="md:col-span-1 h-[400px] md:h-auto">
|
||||||
|
{/* Note: We rely on Tutor's internal height styling or pass a class.
|
||||||
|
The Tutor component has 'h-[400px] md:h-full'.
|
||||||
|
Since it's in a grid cell that might stretch, 'h-full' should work if the row has height.
|
||||||
|
However, the Board Area defines the row height.
|
||||||
|
*/}
|
||||||
<Tutor
|
<Tutor
|
||||||
currentFen={fen}
|
currentFen={fen}
|
||||||
userMove={userMove}
|
userMove={userMove}
|
||||||
@@ -476,28 +500,78 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality,
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showAnalysisModal && (
|
{/* 4. History (Col 1-3) - Full width at bottom */}
|
||||||
<GameAnalysisModal
|
<div className="md:col-span-3 bg-white dark:bg-gray-800 p-4 rounded-lg shadow-lg flex flex-col">
|
||||||
fen={fen}
|
<div className="flex items-center justify-between mb-2">
|
||||||
stockfish={stockfish}
|
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">Game History</h3>
|
||||||
apiKey={apiKey}
|
<button
|
||||||
language={language}
|
onClick={() => setShowAnalysisModal(true)}
|
||||||
onClose={() => setShowAnalysisModal(false)}
|
className="text-xs bg-purple-100 text-purple-700 px-2 py-1 rounded hover:bg-purple-200 dark:bg-purple-900 dark:text-purple-200 flex items-center gap-1"
|
||||||
/>
|
>
|
||||||
)}
|
<Brain size={12} /> Analyze
|
||||||
|
</button>
|
||||||
{gameOverState && (
|
</div>
|
||||||
<GameOverModal
|
<div className="overflow-y-auto border border-gray-200 dark:border-gray-700 rounded bg-gray-50 dark:bg-gray-900 p-2 max-h-40">
|
||||||
result={gameOverState.result}
|
<table className="w-full text-sm text-left">
|
||||||
winner={gameOverState.winner}
|
<thead>
|
||||||
history={moveHistory}
|
<tr className="text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
|
||||||
apiKey={apiKey}
|
<th className="py-1 px-2 w-12">#</th>
|
||||||
language={language}
|
<th className="py-1 px-2">White</th>
|
||||||
onClose={() => setGameOverState(null)}
|
<th className="py-1 px-2">Black</th>
|
||||||
onNewGame={handleNewGame}
|
</tr>
|
||||||
/>
|
</thead>
|
||||||
)}
|
<tbody>
|
||||||
|
{(() => {
|
||||||
|
const history = gameRef.current.history();
|
||||||
|
const rows = [];
|
||||||
|
for (let i = 0; i < history.length; i += 2) {
|
||||||
|
rows.push(
|
||||||
|
<tr key={i} className="border-b border-gray-100 dark:border-gray-800 last:border-0">
|
||||||
|
<td className="py-1 px-2 text-gray-500 dark:text-gray-500">{Math.floor(i / 2) + 1}.</td>
|
||||||
|
<td className="py-1 px-2 font-medium text-gray-900 dark:text-gray-200">{history[i]}</td>
|
||||||
|
<td className="py-1 px-2 font-medium text-gray-900 dark:text-gray-200">{history[i + 1] || ""}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={3} className="py-4 text-center text-gray-500 italic">
|
||||||
|
No moves yet.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
})()}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showAnalysisModal && (
|
||||||
|
<GameAnalysisModal
|
||||||
|
fen={fen}
|
||||||
|
stockfish={stockfish}
|
||||||
|
apiKey={apiKey}
|
||||||
|
language={language}
|
||||||
|
onClose={() => setShowAnalysisModal(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{gameOverState && (
|
||||||
|
<GameOverModal
|
||||||
|
result={gameOverState.result}
|
||||||
|
winner={gameOverState.winner}
|
||||||
|
history={moveHistory}
|
||||||
|
apiKey={apiKey}
|
||||||
|
language={language}
|
||||||
|
onClose={() => setGameOverState(null)}
|
||||||
|
onNewGame={handleNewGame}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,29 +6,30 @@ interface EvaluationBarProps {
|
|||||||
score?: number | null; // centipawns
|
score?: number | null; // centipawns
|
||||||
mate?: number | null; // moves to mate
|
mate?: number | null; // moves to mate
|
||||||
isPlayerWhite: boolean;
|
isPlayerWhite: boolean;
|
||||||
|
orientation?: 'vertical' | 'horizontal';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EvaluationBar({ score, mate, isPlayerWhite }: EvaluationBarProps) {
|
export function EvaluationBar({ score, mate, isPlayerWhite, orientation = 'vertical' }: EvaluationBarProps) {
|
||||||
// Calculate white's percentage height
|
// Calculate white's percentage height/width
|
||||||
// Using sigmoid-like function for score: P = 1 / (1 + 10^(-score/400))
|
// Using sigmoid-like function for score: P = 1 / (1 + 10^(-score/400))
|
||||||
// This is a standard way to visualize CP advantage.
|
// This is a standard way to visualize CP advantage.
|
||||||
let whiteHeightPercent = 50;
|
let whitePercent = 50;
|
||||||
let label = "0.0";
|
let label = "0.0";
|
||||||
|
|
||||||
if (mate !== null && mate !== undefined) {
|
if (mate !== null && mate !== undefined) {
|
||||||
// Mate detected
|
// Mate detected
|
||||||
if (mate > 0) {
|
if (mate > 0) {
|
||||||
whiteHeightPercent = 100;
|
whitePercent = 100;
|
||||||
label = `M${Math.abs(mate)}`;
|
label = `M${Math.abs(mate)}`;
|
||||||
} else {
|
} else {
|
||||||
whiteHeightPercent = 0;
|
whitePercent = 0;
|
||||||
label = `M${Math.abs(mate)}`;
|
label = `M${Math.abs(mate)}`;
|
||||||
}
|
}
|
||||||
} else if (score !== null && score !== undefined) {
|
} else if (score !== null && score !== undefined) {
|
||||||
// Score is in centipawns. 100 cp = 1 pawn.
|
// Score is in centipawns. 100 cp = 1 pawn.
|
||||||
// We clamp the visual score somewhat to avoid extreme compression
|
// We clamp the visual score somewhat to avoid extreme compression
|
||||||
const winChance = 1 / (1 + Math.pow(10, -score / 400));
|
const winChance = 1 / (1 + Math.pow(10, -score / 400));
|
||||||
whiteHeightPercent = winChance * 100;
|
whitePercent = winChance * 100;
|
||||||
|
|
||||||
// Format label: +1.5 or -0.3
|
// Format label: +1.5 or -0.3
|
||||||
// If player is NOT white, we invert the score for display (so + means Player advantage)
|
// If player is NOT white, we invert the score for display (so + means Player advantage)
|
||||||
@@ -41,22 +42,37 @@ export function EvaluationBar({ score, mate, isPlayerWhite }: EvaluationBarProps
|
|||||||
if (score === 0) label = "0.0";
|
if (score === 0) label = "0.0";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const isVertical = orientation === 'vertical';
|
||||||
<div className="w-8 h-full bg-gray-800 border border-gray-400 relative overflow-hidden rounded shadow-inner">
|
|
||||||
{/* Black background is the container (h-full) */}
|
|
||||||
|
|
||||||
{/* White bar grows from bottom if player is white, from top if player is black */}
|
return (
|
||||||
|
<div className={clsx(
|
||||||
|
"bg-gray-800 border border-gray-400 relative overflow-hidden rounded shadow-inner",
|
||||||
|
isVertical ? "w-8 h-full" : "w-full h-6"
|
||||||
|
)}>
|
||||||
|
{/* Black background is the container */}
|
||||||
|
|
||||||
|
{/* White bar grows from bottom/left if player is white, from top/right if player is black */}
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"absolute w-full bg-white transition-all duration-500 ease-in-out",
|
"absolute bg-white transition-all duration-500 ease-in-out",
|
||||||
isPlayerWhite ? "bottom-0" : "top-0"
|
isVertical ? "w-full" : "h-full",
|
||||||
|
// Vertical positioning
|
||||||
|
isVertical && (isPlayerWhite ? "bottom-0" : "top-0"),
|
||||||
|
// Horizontal positioning
|
||||||
|
!isVertical && (isPlayerWhite ? "left-0" : "right-0")
|
||||||
)}
|
)}
|
||||||
style={{ height: `${whiteHeightPercent}%` }}
|
style={{
|
||||||
|
height: isVertical ? `${whitePercent}%` : '100%',
|
||||||
|
width: !isVertical ? `${whitePercent}%` : '100%'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Score Label */}
|
{/* Score Label */}
|
||||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||||
<span className="text-xs font-bold text-white mix-blend-difference select-none">
|
<span className={clsx(
|
||||||
|
"font-bold text-white mix-blend-difference select-none",
|
||||||
|
isVertical ? "text-xs" : "text-sm"
|
||||||
|
)}>
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ React to this exchange as the player.
|
|||||||
if (!apiKey) return null;
|
if (!apiKey) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 h-[600px] flex flex-col">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 h-[400px] md:h-[600px] flex flex-col">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex items-center gap-3 bg-gray-50 dark:bg-gray-900 rounded-t-lg">
|
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex items-center gap-3 bg-gray-50 dark:bg-gray-900 rounded-t-lg">
|
||||||
<div className="text-2xl">{personality.image}</div>
|
<div className="text-2xl">{personality.image}</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user