Created
August 5, 2025 16:33
-
-
Save NTT123/16dd26863f7ef3f77926b306dc82f91b to your computer and use it in GitHub Desktop.
llm-play-chess.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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