Skip to content

Instantly share code, notes, and snippets.

@NTT123
Created August 5, 2025 16:33
Show Gist options
  • Save NTT123/16dd26863f7ef3f77926b306dc82f91b to your computer and use it in GitHub Desktop.
Save NTT123/16dd26863f7ef3f77926b306dc82f91b to your computer and use it in GitHub Desktop.
llm-play-chess.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Chess Arena - Gemini API Chess Battle</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0 auto;
padding: 20px;
background-color: #1a1a1a;
color: #e0e0e0;
}
.container {
background: #2d2d2d;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
max-width: 1200px;
margin: 0 auto;
box-sizing: border-box;
}
h1 {
color: #f0f0f0;
text-align: center;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #b0b0b0;
}
input[type="password"] {
width: 100%;
padding: 12px;
border: 2px solid #555;
border-radius: 5px;
font-size: 16px;
box-sizing: border-box;
background-color: #3a3a3a;
color: #e0e0e0;
}
input[type="password"]:focus {
outline: none;
border-color: #4285f4;
}
button {
background-color: #4285f4;
color: white;
padding: 12px 24px;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
width: 100%;
margin-top: 10px;
}
button:hover {
background-color: #3367d6;
}
button:disabled {
background-color: #555;
cursor: not-allowed;
}
.response-area {
margin-top: 30px;
padding: 20px;
background-color: #3a3a3a;
border-radius: 5px;
border-left: 4px solid #4285f4;
display: none;
}
.response-area.show {
display: block;
}
.error {
border-left-color: #ea4335;
background-color: #4a2c2c;
}
.loading {
text-align: center;
color: #b0b0b0;
}
.response-text {
white-space: pre-wrap;
word-wrap: break-word;
}
/* Chess Game Styles */
.chess-section {
margin-top: 40px;
padding-top: 30px;
border-top: 2px solid #555;
}
.chess-section h2 {
text-align: center;
color: #f0f0f0;
margin-bottom: 30px;
}
.game-layout {
display: grid;
grid-template-columns: 1fr 320px;
gap: 40px;
width: 100%;
align-items: start;
max-width: 100%;
overflow: hidden;
margin-bottom: 30px;
}
.bottom-panels {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-top: 30px;
padding-top: 20px;
border-top: 2px solid #555;
}
.chess-board-container {
display: flex;
justify-content: center;
position: relative;
padding: 10px;
min-width: 420px;
max-width: 100%;
box-sizing: border-box;
}
.chess-board {
display: grid;
grid-template-columns: repeat(8, 50px);
grid-template-rows: repeat(8, 50px);
width: 400px;
height: 400px;
border: 3px solid #654321;
background-color: #4a4a4a;
}
.chess-square {
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
cursor: pointer;
user-select: none;
transition: background-color 0.2s ease;
}
.chess-square.light {
background-color: #8b7355;
}
.chess-square.dark {
background-color: #5d4e37;
}
.chess-square:hover {
opacity: 0.8;
}
.chess-square.highlight {
box-shadow: inset 0 0 0 3px #4285f4;
}
.chess-square.selected {
box-shadow: inset 0 0 0 4px #4285f4;
background-color: #87ceeb !important;
}
.chess-square.valid-move {
position: relative;
}
.chess-square.valid-move::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
background-color: #4285f4;
border-radius: 50%;
opacity: 0.6;
transform: translate(-50%, -50%);
}
.chess-square.valid-move:not(:empty)::after {
width: 100%;
height: 100%;
background-color: transparent;
border: 3px solid #ff4444;
border-radius: 0;
opacity: 0.8;
}
.chess-square.last-move {
background-color: #ffff99 !important;
}
.chess-square.move-from {
background-color: #ff6b6b !important;
box-shadow: inset 0 0 0 3px #ff4444;
}
.chess-square.move-to {
background-color: #4ecdc4 !important;
box-shadow: inset 0 0 0 3px #2ecc71;
}
/* New animated move highlighting */
.chess-square.move-preparing {
background-color: #ffd700 !important;
box-shadow: inset 0 0 0 3px #ffb300;
animation: pulsePiece 1s ease-in-out infinite alternate;
}
.chess-square {
transition: background-color 0.3s ease, box-shadow 0.3s ease;
}
@keyframes pulsePiece {
0% {
background-color: #ffd700 !important;
box-shadow: inset 0 0 0 3px #ffb300;
}
100% {
background-color: #ffec8b !important;
box-shadow: inset 0 0 0 5px #ffa000;
}
}
/* Arrow container */
.move-arrow {
position: absolute;
pointer-events: none;
z-index: 10;
}
.move-arrow svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.move-arrow line {
stroke: #2c3e50;
stroke-width: 4;
marker-end: url(#arrowhead);
}
.move-arrow marker {
fill: #2c3e50;
}
.game-controls {
width: 100%;
background-color: #3a3a3a;
padding: 20px;
border-radius: 10px;
border: 1px solid #555;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
position: sticky;
top: 20px;
box-sizing: border-box;
max-width: 100%;
}
.bottom-panel {
background-color: #3a3a3a;
padding: 20px;
border-radius: 10px;
border: 1px solid #555;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.game-controls h3, .game-controls h4 {
margin-top: 0;
color: #f0f0f0;
}
.game-button {
width: 100%;
margin-bottom: 10px;
padding: 10px;
border: none;
border-radius: 5px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s ease;
}
#startGameButton {
background-color: #007bff;
color: white;
}
#startGameButton:hover:not(:disabled) {
background-color: #0056b3;
}
#resetBoardButton {
background-color: #6c757d;
color: white;
}
#resetBoardButton:hover {
background-color: #5a6268;
}
#soundToggleButton {
background-color: #17a2b8;
color: white;
transition: opacity 0.2s ease;
}
#soundToggleButton:hover {
background-color: #138496;
}
.game-button:disabled {
background-color: #555;
cursor: not-allowed;
}
.game-status {
margin-top: 20px;
}
.game-status h4 {
margin-bottom: 10px;
font-size: 16px;
}
#gameStatus {
padding: 10px;
background-color: #4a4a4a;
border: 1px solid #666;
border-radius: 4px;
font-weight: 500;
color: #e0e0e0;
}
.bottom-panel h3 {
margin-top: 0;
margin-bottom: 15px;
color: #f0f0f0;
font-size: 18px;
}
#moveHistory {
height: 300px;
overflow-y: auto;
padding: 15px;
background-color: #4a4a4a;
border: 1px solid #666;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.5;
color: #e0e0e0;
}
.ai-thinking-content {
height: 300px;
overflow-y: auto;
padding: 15px;
background-color: #2a3a4a;
border: 1px solid #4285f4;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.4;
color: #e0e0e0;
white-space: pre-wrap;
word-wrap: break-word;
max-width: 100%;
}
/* Loading animations */
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
.loading-dots {
display: inline-block;
font-weight: bold;
}
.loading-dots::after {
content: '';
animation: dots 1.5s steps(4, end) infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes dots {
0%, 20% { content: ''; }
40% { content: '.'; }
60% { content: '..'; }
80%, 100% { content: '...'; }
}
.game-starting {
text-align: center;
padding: 20px;
background-color: #3a3a3a;
border-radius: 8px;
margin: 10px 0;
border: 2px solid #007bff;
}
.game-starting .loading-spinner {
width: 30px;
height: 30px;
border-width: 4px;
margin-right: 12px;
}
/* Victory Popup */
.victory-popup {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
animation: fadeIn 0.3s ease-in;
}
.victory-content {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px;
border-radius: 20px;
text-align: center;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
animation: slideIn 0.5s ease-out;
max-width: 400px;
margin: 20px;
}
.victory-content.draw {
background: linear-gradient(135deg, #ff9a56 0%, #ffad56 100%);
}
.victory-content.forfeit {
background: linear-gradient(135deg, #ff6b6b 0%, #ff8e53 100%);
}
.victory-title {
font-size: 48px;
margin-bottom: 10px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.victory-winner {
font-size: 32px;
font-weight: bold;
margin-bottom: 20px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.victory-reason {
font-size: 18px;
margin-bottom: 30px;
opacity: 0.9;
}
.victory-stats {
font-size: 14px;
margin-bottom: 20px;
opacity: 0.8;
}
.victory-buttons {
display: flex;
gap: 15px;
justify-content: center;
}
.victory-button {
padding: 12px 24px;
border: none;
border-radius: 25px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.2);
color: white;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.victory-button:hover {
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.5);
transform: translateY(-2px);
}
.victory-button.primary {
background: rgba(255, 255, 255, 0.9);
color: #333;
}
.victory-button.primary:hover {
background: white;
transform: translateY(-2px);
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-50px) scale(0.8);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* Responsive Design */
@media (max-width: 1200px) {
.container {
max-width: 95%;
padding: 20px;
}
.game-layout {
grid-template-columns: 1fr;
gap: 30px;
justify-content: center;
}
.chess-board-container {
min-width: auto;
padding: 10px;
}
.game-controls {
position: static;
max-width: 500px;
margin: 0 auto;
}
.bottom-panels {
grid-template-columns: 1fr;
gap: 20px;
}
}
@media (max-width: 768px) {
.chess-board {
grid-template-columns: repeat(8, 40px);
grid-template-rows: repeat(8, 40px);
width: 320px;
height: 320px;
}
.chess-square {
font-size: 24px;
}
.game-controls {
padding: 20px;
}
}
</style>
</head>
<body>
<div class="container">
<h1>🤖 AI Chess Arena</h1>
<p style="text-align: center; color: #b0b0b0; margin-bottom: 30px;">
Watch two Gemini AI agents battle it out on the chess board!
</p>
<form id="apiTestForm">
<div class="form-group">
<label for="apiKey">Gemini API Key:</label>
<input
type="password"
id="apiKey"
placeholder="Enter your Gemini API key..."
value=""
required
>
</div>
<button type="submit" id="testButton">Test Connection</button>
<button type="button" id="clearApiButton" style="background-color: #dc3545; margin-top: 10px;">Clear Saved API Key</button>
</form>
<div id="responseArea" class="response-area">
<h3 id="responseTitle">Response:</h3>
<div id="responseText" class="response-text"></div>
</div>
<!-- Chess Game Section - Hidden until API connection -->
<div class="chess-section" id="chessSection" style="display: none;">
<div class="game-layout">
<div class="chess-board-container">
<div id="chessBoard" class="chess-board"></div>
</div>
<div class="game-controls">
<h3>Game Controls</h3>
<button id="startGameButton" class="game-button" disabled>Start AI vs AI Game</button>
<button id="resetBoardButton" class="game-button">Reset Board</button>
<button id="soundToggleButton" class="game-button sound-button">🔊 Sound On</button>
<div class="game-status">
<h4>Game Status:</h4>
<div id="gameStatus">Ready to start</div>
</div>
</div>
</div>
<!-- Bottom Panels for Move History and AI Thinking -->
<div class="bottom-panels">
<div class="bottom-panel">
<h3>Move History</h3>
<div id="moveHistory"></div>
</div>
<div class="bottom-panel">
<h3>AI Thinking</h3>
<div id="aiThinking" class="ai-thinking-content" style="display: none;"></div>
</div>
</div>
</div>
</div>
<!-- Chess.js Library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.10.3/chess.min.js"></script>
<script type="importmap">
{
"imports": {
"@google/generative-ai": "https://esm.run/@google/generative-ai"
}
}
</script>
<script type="module">
import { GoogleGenerativeAI } from '@google/generative-ai';
const form = document.getElementById('apiTestForm');
const apiKeyInput = document.getElementById('apiKey');
const testButton = document.getElementById('testButton');
const responseArea = document.getElementById('responseArea');
const responseTitle = document.getElementById('responseTitle');
const responseText = document.getElementById('responseText');
// Chess game elements
const chessBoard = document.getElementById('chessBoard');
const startGameButton = document.getElementById('startGameButton');
const resetBoardButton = document.getElementById('resetBoardButton');
const soundToggleButton = document.getElementById('soundToggleButton');
const gameStatus = document.getElementById('gameStatus');
const moveHistory = document.getElementById('moveHistory');
// Chess game state
let gameActive = false;
let currentApiKey = null;
let game = null; // Chess.js instance
let selectedSquare = null;
let isHumanVsHuman = false;
// AI vs AI system
let whiteAgent = null;
let blackAgent = null;
let aiGameActive = false;
let currentAITries = 0;
let maxAITries = 3;
let aiGameSpeed = 2000; // milliseconds between moves
let gameStartTime = null;
// Audio system
let audioContext = null;
let soundEnabled = true;
// Initialize audio context
function initAudioContext() {
if (!audioContext) {
try {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
} catch (e) {
console.log('Web Audio API not supported');
soundEnabled = false;
}
}
return audioContext;
}
// Base sound generation function
function playSound(frequency, duration, volume = 0.3, type = 'sine') {
if (!soundEnabled) return;
const ctx = initAudioContext();
if (!ctx) return;
// Resume audio context if needed (browser autoplay policy)
if (ctx.state === 'suspended') {
ctx.resume();
}
const oscillator = ctx.createOscillator();
const gainNode = ctx.createGain();
// Connect nodes
oscillator.connect(gainNode);
gainNode.connect(ctx.destination);
// Configure oscillator
oscillator.type = type;
oscillator.frequency.setValueAtTime(frequency, ctx.currentTime);
// Create smooth envelope to avoid clicks
gainNode.gain.setValueAtTime(0, ctx.currentTime);
gainNode.gain.linearRampToValueAtTime(volume, ctx.currentTime + 0.01);
gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + duration);
// Start and stop
oscillator.start(ctx.currentTime);
oscillator.stop(ctx.currentTime + duration);
}
// Chess-specific sound effects
const chessSounds = {
move: () => playSound(250, 0.15, 0.25, 'sine'), // Soft thud
capture: () => playSound(400, 0.18, 0.35, 'square'), // Sharp clack
check: () => { // Alert sound
playSound(800, 0.1, 0.3, 'sine');
setTimeout(() => playSound(800, 0.1, 0.2, 'sine'), 100);
},
checkmate: () => { // Victory fanfare
playSound(523, 0.2, 0.3, 'sine'); // C
setTimeout(() => playSound(659, 0.2, 0.3, 'sine'), 100); // E
setTimeout(() => playSound(784, 0.4, 0.4, 'sine'), 200); // G
},
select: () => playSound(1000, 0.08, 0.15, 'sine'), // Subtle tick
castle: () => { // Double sound
playSound(300, 0.15, 0.25, 'sine');
setTimeout(() => playSound(280, 0.15, 0.25, 'sine'), 80);
},
gameStart: () => playSound(440, 0.2, 0.3, 'sine') // Game start
};
// Toggle sound on/off
function toggleSound() {
soundEnabled = !soundEnabled;
const button = document.getElementById('soundToggleButton');
if (button) {
button.textContent = soundEnabled ? '🔊 Sound On' : '🔇 Sound Off';
button.style.opacity = soundEnabled ? '1' : '0.6';
}
// Save preference
localStorage.setItem('chessSound', soundEnabled.toString());
// Play confirmation sound if enabled
if (soundEnabled) {
chessSounds.select();
}
}
// Load sound preference
function loadSoundPreference() {
const saved = localStorage.getItem('chessSound');
if (saved !== null) {
soundEnabled = saved === 'true';
}
}
// Save API key to localStorage
function saveApiKey(apiKey) {
if (apiKey && apiKey.trim()) {
localStorage.setItem('geminiApiKey', apiKey.trim());
}
}
// Load API key from localStorage
function loadApiKey() {
const saved = localStorage.getItem('geminiApiKey');
if (saved) {
const apiKeyInput = document.getElementById('apiKey');
if (apiKeyInput) {
apiKeyInput.value = saved;
}
}
}
// Clear API key from localStorage
function clearApiKey() {
localStorage.removeItem('geminiApiKey');
}
// AI Agent System
function initializeAIAgents(apiKey) {
try {
const genAI = new GoogleGenerativeAI(apiKey);
// Create separate chat sessions for White and Black agents
whiteAgent = genAI.getGenerativeModel({
model: "gemini-2.5-pro",
generationConfig: {
temperature: 0.7,
topP: 0.9,
topK: 40,
maxOutputTokens: 1024,
}
}).startChat({
history: [],
generationConfig: {
thinkingConfig: {
includeThoughts: true,
thinkingBudget: 1024
}
}
});
blackAgent = genAI.getGenerativeModel({
model: "gemini-2.5-pro",
generationConfig: {
temperature: 0.7,
topP: 0.9,
topK: 40,
maxOutputTokens: 1024,
}
}).startChat({
history: [],
generationConfig: {
thinkingConfig: {
includeThoughts: true,
thinkingBudget: 1024
}
}
});
return true;
} catch (error) {
console.error('Failed to initialize AI agents:', error);
return false;
}
}
// Convert chess board to readable text format for AI
function convertBoardToText() {
const board = game.board();
const pieceNames = {
'p': 'Pawn', 'r': 'Rook', 'n': 'Knight',
'b': 'Bishop', 'q': 'Queen', 'k': 'King'
};
let result = "Current Chess Board (8x8 grid, a-h files, 8-1 ranks):\n";
result += " a b c d e f g h\n";
for (let rank = 0; rank < 8; rank++) {
result += `${8-rank} `;
for (let file = 0; file < 8; file++) {
const square = board[rank][file];
if (square) {
const color = square.color === 'w' ? 'White' : 'Black';
const piece = pieceNames[square.type] || square.type;
result += ` ${color[0]}${piece[0]} `;
} else {
result += ` .. `;
}
}
result += ` ${8-rank}\n`;
}
result += " a b c d e f g h\n";
// Add FEN for precise position representation
result += `\nFEN Position: ${game.fen()}\n`;
// Add piece list for clarity
result += "\nPieces on board:\n";
const files = 'abcdefgh';
for (let rank = 0; rank < 8; rank++) {
for (let file = 0; file < 8; file++) {
const square = board[rank][file];
if (square) {
const squareNotation = files[file] + (8 - rank);
const color = square.color === 'w' ? 'White' : 'Black';
const piece = pieceNames[square.type] || square.type;
result += `- ${color} ${piece} on ${squareNotation}\n`;
}
}
}
return result;
}
// Build game context for AI agents
function buildGameContext(isFirstMove = false, lastMove = null) {
const currentPlayer = game.turn() === 'w' ? 'White' : 'Black';
const moveNumber = Math.floor(game.history().length / 2) + 1;
const totalMoves = game.history().length;
let context = `\nGame Status:\n`;
context += `- You are playing as ${currentPlayer}\n`;
context += `- Move ${moveNumber} (${totalMoves} total moves played)\n`;
if (isFirstMove) {
context += `- This is the first move of the game\n`;
} else if (lastMove) {
context += `- Opponent's last move: ${lastMove}\n`;
}
// Add game state information
if (game.in_check()) {
context += `- WARNING: You are in CHECK!\n`;
}
// Add castling availability
const fen = game.fen();
const castlingRights = fen.split(' ')[2]; // Extract castling rights from FEN
if (castlingRights !== '-') {
context += `- Castling available: `;
const availableCastling = [];
if (currentPlayer === 'White') {
if (castlingRights.includes('K')) availableCastling.push('kingside');
if (castlingRights.includes('Q')) availableCastling.push('queenside');
} else {
if (castlingRights.includes('k')) availableCastling.push('kingside');
if (castlingRights.includes('q')) availableCastling.push('queenside');
}
context += availableCastling.length > 0 ? availableCastling.join(', ') : 'none';
context += `\n`;
}
// Add move format reminder
context += `\nMove Format: Respond with ONLY your move in the correct format:\n`;
context += `- Regular: "[piece] [from] to [to]" (e.g., "pawn e2 to e4")\n`;
context += `- Castling: "castle [side]" or "O-O"/"O-O-O" (e.g., "castle kingside")\n`;
context += `\nYour move:`;
return context;
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const apiKey = apiKeyInput.value.trim();
if (!apiKey) {
showError('Please enter your API key');
return;
}
await testGeminiConnection(apiKey);
});
// Chess game event listeners
startGameButton.addEventListener('click', () => {
if (currentApiKey) {
startAIGame();
}
});
resetBoardButton.addEventListener('click', () => {
resetBoard();
});
soundToggleButton.addEventListener('click', () => {
toggleSound();
});
// Clear API key button event listener
document.getElementById('clearApiButton').addEventListener('click', () => {
clearApiKey();
document.getElementById('apiKey').value = '';
currentApiKey = null;
startGameButton.disabled = true;
updateGameStatus('API connection required to start game');
// Show API form and hide chess section
document.getElementById('apiTestForm').style.display = 'block';
document.getElementById('chessSection').style.display = 'none';
responseArea.style.display = 'none';
});
// Initialize chess board on page load
document.addEventListener('DOMContentLoaded', () => {
loadSoundPreference();
loadApiKey(); // Load saved API key
createChessBoard();
initializeGame();
// Initialize sound button state
const button = document.getElementById('soundToggleButton');
if (button) {
button.textContent = soundEnabled ? '🔊 Sound On' : '🔇 Sound Off';
button.style.opacity = soundEnabled ? '1' : '0.6';
}
// Auto-test API connection if key exists
const apiKeyInput = document.getElementById('apiKey');
if (apiKeyInput && apiKeyInput.value.trim()) {
// Small delay to ensure UI is ready
setTimeout(() => {
testGeminiConnection(apiKeyInput.value.trim());
}, 500);
}
});
async function testGeminiConnection(apiKey) {
testButton.disabled = true;
testButton.textContent = 'Testing...';
showLoading();
try {
const genAI = new GoogleGenerativeAI(apiKey);
const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
const result = await model.generateContent("Hello world! Please respond with a friendly greeting and confirm that you're ready to help with chess.");
const response = await result.response;
const text = response.text();
showSuccess(text);
// Enable chess game if API connection successful
currentApiKey = apiKey;
saveApiKey(apiKey); // Save API key to localStorage
startGameButton.disabled = false;
updateGameStatus('API connected - Ready to start chess game!');
// Show chess section and hide API key form after successful connection
setTimeout(() => {
document.getElementById('apiTestForm').style.display = 'none';
responseArea.style.display = 'none';
document.getElementById('chessSection').style.display = 'block';
}, 2000); // Show chess and hide API form after 2 seconds
} catch (error) {
console.error('Error:', error);
let errorMessage = 'Failed to connect to Gemini API. ';
if (error.message.includes('API_KEY_INVALID')) {
errorMessage += 'Please check your API key.';
} else if (error.message.includes('PERMISSION_DENIED')) {
errorMessage += 'API key does not have permission.';
} else {
errorMessage += `Error: ${error.message}`;
}
showError(errorMessage);
// Disable chess game if API connection failed
currentApiKey = null;
startGameButton.disabled = true;
updateGameStatus('API connection required to start game');
} finally {
testButton.disabled = false;
testButton.textContent = 'Test Connection';
}
}
function showLoading() {
responseArea.className = 'response-area show';
responseTitle.textContent = 'Testing connection...';
responseText.innerHTML = '<div class="loading">🔄 Connecting to Gemini API...</div>';
}
function showSuccess(text) {
responseArea.className = 'response-area show';
responseTitle.textContent = '✅ Connection Successful!';
responseText.textContent = text;
}
function showError(message) {
responseArea.className = 'response-area show error';
responseTitle.textContent = '❌ Connection Failed';
responseText.textContent = message;
}
// Chess piece Unicode symbols
const chessPieces = {
'wK': '♔', 'wQ': '♕', 'wR': '♖', 'wB': '♗', 'wN': '♘', 'wP': '♙',
'bK': '♚', 'bQ': '♛', 'bR': '♜', 'bB': '♝', 'bN': '♞', 'bP': '♟'
};
// Initial chess board position
const initialBoard = [
['bR', 'bN', 'bB', 'bQ', 'bK', 'bB', 'bN', 'bR'],
['bP', 'bP', 'bP', 'bP', 'bP', 'bP', 'bP', 'bP'],
['', '', '', '', '', '', '', ''],
['', '', '', '', '', '', '', ''],
['', '', '', '', '', '', '', ''],
['', '', '', '', '', '', '', ''],
['wP', 'wP', 'wP', 'wP', 'wP', 'wP', 'wP', 'wP'],
['wR', 'wN', 'wB', 'wQ', 'wK', 'wB', 'wN', 'wR']
];
let currentBoard = JSON.parse(JSON.stringify(initialBoard));
function createChessBoard() {
chessBoard.innerHTML = '';
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const square = document.createElement('div');
square.className = 'chess-square';
square.id = getSquareId(row, col);
// Alternate square colors
if ((row + col) % 2 === 0) {
square.classList.add('light');
} else {
square.classList.add('dark');
}
// Add click event listener for piece selection and moves
square.addEventListener('click', () => handleSquareClick(getSquareId(row, col)));
chessBoard.appendChild(square);
}
}
}
function getSquareId(row, col) {
const files = 'abcdefgh';
const ranks = '87654321';
return files[col] + ranks[row];
}
function initializeGame() {
// Initialize Chess.js game
game = new Chess();
selectedSquare = null;
isHumanVsHuman = true;
renderBoard();
updateGameStatus(getCurrentPlayerDisplay() + ' to move');
updateTurnDisplay();
}
function renderBoard() {
const board = game.board();
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const square = document.getElementById(getSquareId(row, col));
const piece = board[row][col];
if (piece) {
const pieceCode = piece.color + piece.type.toUpperCase();
square.textContent = chessPieces[pieceCode] || '';
} else {
square.textContent = '';
}
// Remove all highlighting classes
square.classList.remove('selected', 'valid-move', 'last-move');
}
}
}
function resetBoard() {
gameActive = false;
aiGameActive = false;
currentAITries = 0;
initializeGame();
clearMoveHistory();
clearMoveHighlights();
clearAIThinking();
// Reset button states
startGameButton.disabled = !currentApiKey;
addMoveToHistory('Game reset. ' + getCurrentPlayerDisplay() + ' to move.');
}
function updateGameStatus(status, showSpinner = false) {
if (showSpinner) {
gameStatus.innerHTML = `<span class="loading-spinner"></span>${status}`;
} else {
gameStatus.textContent = status;
}
}
function addMoveToHistory(move) {
const moveElement = document.createElement('div');
moveElement.textContent = move;
moveHistory.appendChild(moveElement);
moveHistory.scrollTop = moveHistory.scrollHeight;
}
function clearMoveHistory() {
moveHistory.innerHTML = '';
}
// Handle square clicks for piece selection and moves
function handleSquareClick(square) {
if (!isHumanVsHuman) return;
if (selectedSquare === null) {
// Try to select a piece
selectPiece(square);
} else if (selectedSquare === square) {
// Deselect current piece
deselectPiece();
} else {
// Try to make a move
attemptMove(selectedSquare, square);
}
}
function selectPiece(square) {
const piece = game.get(square);
// Check if there's a piece on this square and it belongs to current player
if (piece && piece.color === game.turn()) {
selectedSquare = square;
highlightSelectedPiece(square);
highlightValidMoves(square);
chessSounds.select(); // Play selection sound
}
}
function deselectPiece() {
selectedSquare = null;
clearHighlights();
}
function attemptMove(from, to) {
try {
// Attempt the move using Chess.js
const move = game.move({
from: from,
to: to,
promotion: 'q' // Always promote to queen for simplicity
});
if (move) {
// Move was successful - but don't render board yet
deselectPiece();
// Create callback for final rendering and game state updates
const moveCallback = () => {
renderBoard(); // NOW render the board with new positions
addMoveToHistory(move.san);
updateGameStatus(getCurrentPlayerDisplay() + ' to move');
updateTurnDisplay();
checkGameState();
};
// Start animated move with callback for final board render
animateMove(from, to, moveCallback);
// Play appropriate sound based on move type
if (move.flags.includes('c')) {
chessSounds.capture(); // Capture move
} else if (move.flags.includes('k') || move.flags.includes('q')) {
chessSounds.castle(); // Castling
} else {
chessSounds.move(); // Regular move
}
} else {
// Invalid move
updateGameStatus('Invalid move! Try again.');
}
} catch (error) {
// Invalid move caught by Chess.js
updateGameStatus('Invalid move! ' + error.message);
}
}
function highlightSelectedPiece(square) {
const squareElement = document.getElementById(square);
squareElement.classList.add('selected');
}
function highlightValidMoves(square) {
const moves = game.moves({ square: square, verbose: true });
moves.forEach(move => {
const targetSquare = document.getElementById(move.to);
targetSquare.classList.add('valid-move');
});
}
function clearHighlights() {
document.querySelectorAll('.chess-square').forEach(square => {
square.classList.remove('selected', 'valid-move', 'last-move', 'move-from', 'move-to', 'move-preparing');
});
}
// Move visualization functions - all moves now use animateMove with callbacks
// New animated move visualization with step-by-step sequence
function animateMove(fromSquare, toSquare, moveCallback = null) {
const fromElement = document.getElementById(fromSquare);
const toElement = document.getElementById(toSquare);
if (!fromElement || !toElement) {
// If animation can't run, execute callback immediately
if (moveCallback) moveCallback();
return;
}
// Step 1: Clear previous position highlights (immediate)
clearMoveHighlights();
// Step 2: Remove current position highlights (300ms delay)
setTimeout(() => {
clearHighlights(); // Clear any existing game highlights
}, 300);
// Step 3: Highlight piece that we are going to move with pulsing effect (600ms delay)
setTimeout(() => {
fromElement.classList.add('move-preparing');
}, 600);
// Step 4: DELAY - Let the piece pulsing be visible (1200ms delay)
setTimeout(() => {
// Keep pulsing - this is just a timing step for better visibility
}, 1200);
// Step 5: Highlight the destination square (1400ms delay)
setTimeout(() => {
toElement.classList.add('move-to');
}, 1400);
// Step 6: FINAL STEP - Render the board with new piece positions (1800ms delay)
setTimeout(() => {
// Execute the callback to render board and update game state
if (moveCallback) moveCallback();
// Remove preparing animation - NO highlighting of old position
fromElement.classList.remove('move-preparing');
// Only highlight the new position
toElement.classList.add('move-to');
// Draw arrow to show move path
drawMoveArrow(fromSquare, toSquare);
}, 1800);
}
function clearMoveHighlights() {
document.querySelectorAll('.chess-square').forEach(square => {
square.classList.remove('move-from', 'move-to', 'move-preparing');
});
// Remove existing arrows
const existingArrows = document.querySelectorAll('.move-arrow');
existingArrows.forEach(arrow => arrow.remove());
}
function drawMoveArrow(fromSquare, toSquare) {
const boardContainer = document.querySelector('.chess-board-container');
const chessBoard = document.getElementById('chessBoard');
const fromElement = document.getElementById(fromSquare);
const toElement = document.getElementById(toSquare);
if (!fromElement || !toElement || !boardContainer || !chessBoard) return;
// Get the board's position and size
const boardRect = chessBoard.getBoundingClientRect();
const containerRect = boardContainer.getBoundingClientRect();
// Calculate square positions relative to the board container
const fromRect = fromElement.getBoundingClientRect();
const toRect = toElement.getBoundingClientRect();
// Calculate centers of squares relative to the board container
const fromX = fromRect.left - containerRect.left + fromRect.width / 2;
const fromY = fromRect.top - containerRect.top + fromRect.height / 2;
const toX = toRect.left - containerRect.left + toRect.width / 2;
const toY = toRect.top - containerRect.top + toRect.height / 2;
// Create SVG arrow that matches the board container size
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.style.position = 'absolute';
svg.style.top = '0';
svg.style.left = '0';
svg.style.width = '100%';
svg.style.height = '100%';
svg.style.pointerEvents = 'none';
svg.style.zIndex = '10';
// Create unique arrow marker ID to avoid conflicts
const markerId = 'arrowhead-' + Date.now();
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
marker.setAttribute('id', markerId);
marker.setAttribute('markerWidth', '10');
marker.setAttribute('markerHeight', '7');
marker.setAttribute('refX', '9');
marker.setAttribute('refY', '3.5');
marker.setAttribute('orient', 'auto');
const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
polygon.setAttribute('points', '0 0, 10 3.5, 0 7');
polygon.setAttribute('fill', '#2c3e50');
marker.appendChild(polygon);
defs.appendChild(marker);
svg.appendChild(defs);
// Create arrow line
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', fromX);
line.setAttribute('y1', fromY);
line.setAttribute('x2', toX);
line.setAttribute('y2', toY);
line.setAttribute('stroke', '#2c3e50');
line.setAttribute('stroke-width', '4');
line.setAttribute('marker-end', `url(#${markerId})`);
svg.appendChild(line);
// Add arrow to board container
const arrowContainer = document.createElement('div');
arrowContainer.className = 'move-arrow';
arrowContainer.appendChild(svg);
boardContainer.appendChild(arrowContainer);
}
function getCurrentPlayerDisplay() {
return game.turn() === 'w' ? 'White' : 'Black';
}
function updateTurnDisplay() {
// Update button text to show whose turn it is
const turnText = 'Current Turn: ' + getCurrentPlayerDisplay();
// You can add a turn display element if needed
}
function checkGameState() {
if (game.in_checkmate()) {
const winner = game.turn() === 'w' ? 'Black' : 'White';
updateGameStatus('Checkmate! ' + winner + ' wins!');
addMoveToHistory('Game Over: ' + winner + ' wins by checkmate.');
chessSounds.checkmate(); // Play checkmate sound
showVictoryPopup(winner, 'Victory by Checkmate!', 'normal');
endGame();
} else if (game.in_stalemate()) {
updateGameStatus('Stalemate! Game is a draw.');
addMoveToHistory('Game Over: Draw by stalemate.');
showVictoryPopup('Draw', 'Game ended in Stalemate', 'draw');
endGame();
} else if (game.in_threefold_repetition()) {
updateGameStatus('Draw by threefold repetition.');
addMoveToHistory('Game Over: Draw by threefold repetition.');
showVictoryPopup('Draw', 'Draw by Threefold Repetition', 'draw');
endGame();
} else if (game.insufficient_material()) {
updateGameStatus('Draw by insufficient material.');
addMoveToHistory('Game Over: Draw by insufficient material.');
showVictoryPopup('Draw', 'Draw by Insufficient Material', 'draw');
endGame();
} else if (game.in_check()) {
updateGameStatus(getCurrentPlayerDisplay() + ' is in check!');
chessSounds.check(); // Play check sound
} else {
updateGameStatus(getCurrentPlayerDisplay() + ' to move');
}
}
function endGame() {
isHumanVsHuman = false;
gameActive = false;
// Re-enable game start buttons
startGameButton.disabled = !currentApiKey;
}
function endAIGame() {
aiGameActive = false;
gameActive = false;
// Re-enable game start buttons
startGameButton.disabled = !currentApiKey;
const gameTime = gameStartTime ? ((Date.now() - gameStartTime) / 1000).toFixed(1) : 'unknown';
addMoveToHistory(`Game ended. Duration: ${gameTime} seconds.`);
hideAIThinking();
}
// AI Thinking Display Functions
function showAIThinking() {
const thinkingDiv = document.getElementById('aiThinking');
if (thinkingDiv) {
thinkingDiv.style.display = 'block';
}
}
function hideAIThinking() {
const thinkingDiv = document.getElementById('aiThinking');
if (thinkingDiv) {
thinkingDiv.style.display = 'none';
}
}
function clearAIThinking() {
const thinkingDiv = document.getElementById('aiThinking');
if (thinkingDiv) {
thinkingDiv.textContent = '';
thinkingDiv.style.display = 'none';
}
}
function updateAIThinking(text, showSpinner = false) {
const thinkingDiv = document.getElementById('aiThinking');
if (thinkingDiv) {
// Always append in concatenate mode with timestamp
const timestamp = new Date().toLocaleTimeString();
const separator = thinkingDiv.textContent.length > 0 ? '\n\n--- ' + timestamp + ' ---\n' : '--- ' + timestamp + ' ---\n';
if (showSpinner) {
thinkingDiv.textContent += separator + `[LOADING] ${text}...`;
} else {
thinkingDiv.textContent += separator + text;
}
thinkingDiv.scrollTop = thinkingDiv.scrollHeight;
showAIThinking();
}
}
function appendAIThinking(text) {
const thinkingDiv = document.getElementById('aiThinking');
if (thinkingDiv) {
thinkingDiv.textContent += '\n' + text;
thinkingDiv.scrollTop = thinkingDiv.scrollHeight;
showAIThinking();
}
}
// Victory Popup Functions
function showVictoryPopup(winner, reason, type = 'normal') {
// Clear any existing victory popups first
document.querySelectorAll('.victory-popup').forEach(el => el.remove());
// Calculate game duration
const gameTime = gameStartTime ? ((Date.now() - gameStartTime) / 1000).toFixed(1) : 'unknown';
const totalMoves = game.history().length;
// Create popup HTML
const popupHTML = `
<div class="victory-popup" id="victoryPopup">
<div class="victory-content ${type}">
<div class="victory-title">${getVictoryEmoji(type)}</div>
<div class="victory-winner">${winner === 'Draw' ? 'Draw!' : winner + ' Wins!'}</div>
<div class="victory-reason">${reason}</div>
<div class="victory-stats">
Game Duration: ${gameTime}s<br>
Total Moves: ${totalMoves}
</div>
<div class="victory-buttons">
<button class="victory-button primary" onclick="closeVictoryPopup(); resetBoard();">New Game</button>
<button class="victory-button" onclick="closeVictoryPopup();">Close</button>
</div>
</div>
</div>
`;
// Add popup to page
document.body.insertAdjacentHTML('beforeend', popupHTML);
// Auto-close after delay if user doesn't interact
setTimeout(() => {
const popup = document.getElementById('victoryPopup');
if (popup) {
popup.style.opacity = '0.7';
}
}, 10000); // Fade after 10 seconds
}
function getVictoryEmoji(type) {
switch(type) {
case 'draw': return '🤝';
case 'forfeit': return '⚠️';
default: return '🎉';
}
}
function closeVictoryPopup() {
const popup = document.getElementById('victoryPopup');
if (popup) {
// Force fade out animation with higher specificity
popup.style.animation = 'fadeOut 0.3s ease-out forwards !important';
popup.style.opacity = '0';
popup.style.pointerEvents = 'none';
// Ensure complete removal after animation
setTimeout(() => {
// Double-check and force removal
const stillThere = document.getElementById('victoryPopup');
if (stillThere) {
stillThere.remove();
}
// Clean up any leftover victory popups
document.querySelectorAll('.victory-popup').forEach(el => el.remove());
}, 350); // Slightly longer to ensure animation completes
}
}
// Make functions globally accessible for onclick handlers
window.closeVictoryPopup = closeVictoryPopup;
window.resetBoard = resetBoard;
// Add fadeOut animation to CSS (will be handled by removing element)
document.head.insertAdjacentHTML('beforeend', `
<style>
@keyframes fadeOut {
0% {
opacity: 1;
transform: translateY(0) scale(1);
}
100% {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
}
.victory-popup.closing {
animation: fadeOut 0.3s ease-out forwards !important;
pointer-events: none !important;
}
</style>
`);
async function startAIGame() {
if (!currentApiKey) {
updateGameStatus('API key required!');
return;
}
updateGameStatus('Initializing AI agents...', true);
// Initialize AI agents
if (!initializeAIAgents(currentApiKey)) {
updateGameStatus('Failed to initialize AI agents!');
return;
}
// Reset to starting position for AI game
game.reset();
renderBoard();
isHumanVsHuman = false;
gameActive = true;
aiGameActive = true;
currentAITries = 0;
gameStartTime = Date.now();
updateGameStatus('AI vs AI game starting...', true);
clearMoveHistory();
clearAIThinking();
// Update button states
startGameButton.disabled = true;
// Initialize agents with their roles
try {
await initializeAgentRoles();
addMoveToHistory('AI vs AI game started! White agent to move.');
updateGameStatus('White AI agent thinking...', true);
// Start the AI game loop
setTimeout(() => playAITurn(), 1000);
} catch (error) {
updateGameStatus('Failed to start AI game: ' + error.message);
endAIGame();
}
}
// Initialize AI agents with their chess roles
async function initializeAgentRoles() {
const whitePrompt = `You are a chess AI agent playing as WHITE pieces. You are about to start a chess game against another AI agent playing BLACK.
Key Rules:
1. You play as White pieces (uppercase letters: K=King, Q=Queen, R=Rook, B=Bishop, N=Knight, P=Pawn)
2. You will receive the board state in 8x8 text format before each move
3. Analyze the position carefully using your strategic thinking
4. Respond with ONLY your move in the correct format
Move Formats:
- Regular moves: "[piece] [from] to [to]"
Examples: "pawn e2 to e4", "knight g1 to f3", "bishop f1 to c4"
- Castling moves: "castle [side]" or standard notation
Examples: "castle kingside", "castle queenside", "O-O", "O-O-O"
- Alternative castling: You can also use king moves: "king e1 to g1" (kingside), "king e1 to c1" (queenside)
Important: Castling is only legal when:
- King and rook haven't moved yet
- No pieces between king and rook
- King is not in check, doesn't pass through check, and doesn't end in check
Your goal is to play strong, strategic chess and try to win the game. Think about opening principles, tactical opportunities, long-term strategy, and consider castling for king safety.
Ready to begin!`;
const blackPrompt = `You are a chess AI agent playing as BLACK pieces. You are about to start a chess game against another AI agent playing WHITE.
Key Rules:
1. You play as Black pieces (lowercase letters: k=king, q=queen, r=rook, b=bishop, n=knight, p=pawn)
2. You will receive the board state in 8x8 text format before each move
3. Analyze the position carefully using your strategic thinking
4. Respond with ONLY your move in the correct format
Move Formats:
- Regular moves: "[piece] [from] to [to]"
Examples: "pawn e7 to e5", "knight g8 to f6", "bishop f8 to c5"
- Castling moves: "castle [side]" or standard notation
Examples: "castle kingside", "castle queenside", "O-O", "O-O-O"
- Alternative castling: You can also use king moves: "king e8 to g8" (kingside), "king e8 to c8" (queenside)
Important: Castling is only legal when:
- King and rook haven't moved yet
- No pieces between king and rook
- King is not in check, doesn't pass through check, and doesn't end in check
Your goal is to play strong, strategic chess and try to win the game. Think about responding to White's moves, tactical opportunities, long-term strategy, and consider castling for king safety.
Ready to begin!`;
// Send initialization messages to both agents
await whiteAgent.sendMessage(whitePrompt);
await blackAgent.sendMessage(blackPrompt);
}
// Main AI turn processing function
async function playAITurn() {
if (!aiGameActive || game.game_over()) {
return;
}
const currentPlayer = game.turn() === 'w' ? 'White' : 'Black';
const currentAgent = game.turn() === 'w' ? whiteAgent : blackAgent;
const isFirstMove = game.history().length === 0;
const lastMove = game.history().length > 0 ? game.history()[game.history().length - 1] : null;
updateGameStatus(`${currentPlayer} AI agent thinking...`, true);
try {
// Build the prompt for the AI agent
const boardText = convertBoardToText();
const gameContext = buildGameContext(isFirstMove, lastMove);
const fullPrompt = boardText + gameContext;
// DEBUG: Log simplified prompt info
console.log(`=== ${currentPlayer} AI Turn (Move ${Math.floor(game.history().length / 2) + 1}) ===`);
// Send message to current agent
const response = await currentAgent.sendMessage(fullPrompt);
// Process the response and extract move
await processAIResponse(response, currentPlayer);
} catch (error) {
console.error('AI turn error:', error);
updateGameStatus(`${currentPlayer} AI agent error: ${error.message}`);
addMoveToHistory(`Error: ${currentPlayer} agent failed - ${error.message}`);
endAIGame();
}
}
// Process AI response and extract move
async function processAIResponse(response, playerColor) {
let thinkingText = '';
let moveText = '';
// DEBUG: Simplified response logging
console.log(`${playerColor} AI Response:`);
// Extract thinking and move from response - handle nested response structure
let actualResponse = response;
if (response.response) {
actualResponse = response.response; // Handle nested response structure from chat API
console.log('FOUND NESTED RESPONSE STRUCTURE - using response.response');
}
if (actualResponse.candidates && actualResponse.candidates[0] && actualResponse.candidates[0].content) {
const parts = actualResponse.candidates[0].content.parts;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part.thought && part.text) {
thinkingText += part.text + '\n';
} else if (part.text && !part.thought) {
moveText += part.text;
}
}
} else if (typeof actualResponse.text === 'function') {
moveText = actualResponse.text();
} else if (typeof response.text === 'function') {
moveText = response.text();
} else {
// Fallback: try to extract any text from nested structure
if (actualResponse.candidates && actualResponse.candidates[0] && actualResponse.candidates[0].content && actualResponse.candidates[0].content.parts && actualResponse.candidates[0].content.parts[0] && actualResponse.candidates[0].content.parts[0].text) {
moveText = actualResponse.candidates[0].content.parts[0].text;
} else {
moveText = String(response);
}
}
console.log(`Move: "${moveText}" (${thinkingText.length > 0 ? 'with thinking' : 'no thinking'})`);
// Show loading while processing response
updateAIThinking(`${playerColor} AI is thinking`, true);
// Display thinking process if available
if (thinkingText.trim()) {
setTimeout(() => {
updateAIThinking(`${playerColor} AI THINKING:\n\n${thinkingText.trim()}\n\n---\n\nMOVE DECISION: "${moveText}"`);
}, 1000);
addMoveToHistory(`${playerColor} AI thinking... (see panel)`);
}
// Move extraction debugging
const extractedMove = extractMoveFromText(moveText);
console.log(`Parsed: ${extractedMove ? extractedMove.original : 'FAILED'}`);
if (!extractedMove) {
await handleInvalidMove(playerColor, 'Could not parse move from response', moveText);
return;
}
// Attempt to make the move
const success = await attemptAIMove(extractedMove, playerColor);
if (success) {
currentAITries = 0; // Reset tries on successful move
// Check game state and continue or end
checkGameState();
if (!game.game_over() && aiGameActive) {
// Schedule next AI turn
setTimeout(() => playAITurn(), aiGameSpeed);
} else {
endAIGame();
}
}
}
// Extract move from AI response text
function extractMoveFromText(text) {
// Remove quotes and extra whitespace
text = text.replace(/["']/g, '').trim();
// Check for castling moves first
// Pattern 1: "castle kingside" or "castle queenside"
const castlePattern = /castle\s+(kingside|queenside)/i;
const castleMatch = text.match(castlePattern);
if (castleMatch) {
return {
type: 'castle',
side: castleMatch[1].toLowerCase(),
original: castleMatch[0]
};
}
// Pattern 2: Standard chess notation "O-O" (kingside) or "O-O-O" (queenside)
if (text.includes('O-O-O')) {
return {
type: 'castle',
side: 'queenside',
original: 'O-O-O'
};
}
if (text.includes('O-O')) {
return {
type: 'castle',
side: 'kingside',
original: 'O-O'
};
}
// Pattern 3: Regular moves [piece] [from] to [to]
const movePattern = /(\w+)\s+([a-h][1-8])\s+to\s+([a-h][1-8])/i;
const match = text.match(movePattern);
if (match) {
const piece = match[1].toLowerCase();
const from = match[2].toLowerCase();
const to = match[3].toLowerCase();
// Check if this is a castling move disguised as a king move
if (piece === 'king') {
if ((from === 'e1' && to === 'g1') || (from === 'e8' && to === 'g8')) {
return {
type: 'castle',
side: 'kingside',
original: match[0]
};
}
if ((from === 'e1' && to === 'c1') || (from === 'e8' && to === 'c8')) {
return {
type: 'castle',
side: 'queenside',
original: match[0]
};
}
}
return {
type: 'regular',
piece: piece,
from: from,
to: to,
original: match[0]
};
}
return null;
}
// Attempt to make an AI move
async function attemptAIMove(extractedMove, playerColor) {
try {
let move = null;
if (extractedMove.type === 'castle') {
// Handle castling moves
const currentTurn = game.turn();
let from, to;
if (extractedMove.side === 'kingside') {
from = currentTurn === 'w' ? 'e1' : 'e8';
to = currentTurn === 'w' ? 'g1' : 'g8';
} else { // queenside
from = currentTurn === 'w' ? 'e1' : 'e8';
to = currentTurn === 'w' ? 'c1' : 'c8';
}
// Try to make the castling move (Chess.js handles castling automatically when king moves to castling square)
move = game.move({
from: from,
to: to
});
} else {
// Handle regular moves
move = game.move({
from: extractedMove.from,
to: extractedMove.to,
promotion: 'q' // Always promote to queen for simplicity
});
}
if (move) {
// Move successful
console.log(`✅ ${playerColor}: ${extractedMove.original} → ${move.san}`);
// Create callback for final rendering and game state updates
const moveCallback = () => {
renderBoard(); // NOW render the board with new positions
addMoveToHistory(`${playerColor}: ${extractedMove.original} (${move.san})`);
updateGameStatus(`${playerColor} played ${move.san}`);
};
// Highlight the AI move with animation
if (extractedMove.type === 'castle') {
// For castling, highlight the king's movement
const currentTurn = game.turn() === 'w' ? 'b' : 'w'; // Previous turn (since move was just made)
const from = currentTurn === 'w' ? 'e1' : 'e8';
const to = extractedMove.side === 'kingside'
? (currentTurn === 'w' ? 'g1' : 'g8')
: (currentTurn === 'w' ? 'c1' : 'c8');
animateMove(from, to, moveCallback);
} else {
animateMove(extractedMove.from, extractedMove.to, moveCallback);
}
// Play appropriate sound
if (move.flags.includes('c')) {
chessSounds.capture();
} else if (move.flags.includes('k') || move.flags.includes('q')) {
chessSounds.castle();
} else {
chessSounds.move();
}
return true;
} else {
// Chess.js returned null - invalid move
console.log(`❌ ${playerColor}: ${extractedMove.original} - Invalid move (Chess.js returned null)`);
await handleInvalidMove(playerColor, `Invalid move - Chess.js validation failed`, extractedMove.original);
return false;
}
} catch (error) {
// Move failed
console.log(`❌ ${playerColor}: ${extractedMove.original} - ${error.message}`);
await handleInvalidMove(playerColor, `Invalid move: ${error.message}`, extractedMove.original);
return false;
}
return false;
}
// Handle invalid moves with 3-try system
async function handleInvalidMove(playerColor, errorMessage, attemptedMove) {
currentAITries++;
const remainingTries = maxAITries - currentAITries;
addMoveToHistory(`${playerColor} invalid move: "${attemptedMove}" - ${errorMessage}`);
if (remainingTries > 0) {
updateGameStatus(`${playerColor} AI made invalid move. ${remainingTries} tries remaining...`, true);
addMoveToHistory(`${playerColor} has ${remainingTries} tries remaining.`);
// Give the agent another chance with additional guidance
const currentAgent = game.turn() === 'w' ? whiteAgent : blackAgent;
const helpPrompt = `Your last move "${attemptedMove}" was invalid: ${errorMessage}\n\nYou have ${remainingTries} tries remaining. Please provide a valid move.\n\nValid moves available: ${game.moves().join(', ')}\n\nRemember the format: "[piece] [from] to [to]"\n\nYour move:`;
try {
const response = await currentAgent.sendMessage(helpPrompt);
await processAIResponse(response, playerColor);
} catch (error) {
console.error('Error in retry attempt:', error);
endAIGame();
}
} else {
// Agent has exhausted all tries
const winner = playerColor === 'White' ? 'Black' : 'White';
updateGameStatus(`${playerColor} AI exhausted all tries. ${winner} wins by forfeit!`);
addMoveToHistory(`Game Over: ${winner} wins - ${playerColor} made too many invalid moves.`);
chessSounds.checkmate();
showVictoryPopup(winner, `${playerColor} AI made too many invalid moves`, 'forfeit');
endAIGame();
}
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment