|
<!DOCTYPE html> |
|
<html lang="fr"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Chess vs Stockfish</title> |
|
|
|
|
|
<link rel="stylesheet" href="https://unpkg.com/@chrisoakman/[email protected]/dist/chessboard-1.0.0.min.css"> |
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> |
|
<script src="https://unpkg.com/[email protected]/chess.min.js"></script> |
|
<script src="https://unpkg.com/@chrisoakman/[email protected]/dist/chessboard-1.0.0.min.js"></script> |
|
|
|
<style> |
|
body { |
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
max-width: 1200px; |
|
margin: 0 auto; |
|
padding: 20px; |
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
min-height: 100vh; |
|
box-sizing: border-box; |
|
} |
|
|
|
.container { |
|
background: rgba(255, 255, 255, 0.95); |
|
border-radius: 20px; |
|
padding: 30px; |
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1); |
|
backdrop-filter: blur(10px); |
|
} |
|
|
|
h1 { |
|
text-align: center; |
|
color: #333; |
|
margin-bottom: 30px; |
|
font-size: 2.5em; |
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.1); |
|
} |
|
|
|
.game-container { |
|
display: flex; |
|
gap: 30px; |
|
justify-content: center; |
|
align-items: flex-start; |
|
flex-wrap: wrap; |
|
} |
|
|
|
.board-container { |
|
flex-shrink: 0; |
|
} |
|
|
|
#myBoard { |
|
border: 4px solid #8B4513; |
|
border-radius: 10px; |
|
box-shadow: 0 10px 25px rgba(0,0,0,0.3); |
|
} |
|
|
|
.info-panel { |
|
background: #f8f9fa; |
|
border-radius: 15px; |
|
padding: 25px; |
|
min-width: 300px; |
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1); |
|
} |
|
|
|
.status { |
|
font-size: 1.2em; |
|
font-weight: bold; |
|
margin-bottom: 15px; |
|
padding: 15px; |
|
border-radius: 10px; |
|
text-align: center; |
|
} |
|
|
|
.status.playing { |
|
background: linear-gradient(45deg, #4CAF50, #45a049); |
|
color: white; |
|
} |
|
|
|
.status.game-over { |
|
background: linear-gradient(45deg, #f44336, #d32f2f); |
|
color: white; |
|
} |
|
|
|
.controls { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 15px; |
|
margin-top: 20px; |
|
} |
|
|
|
button { |
|
background: linear-gradient(45deg, #667eea, #764ba2); |
|
color: white; |
|
border: none; |
|
padding: 12px 20px; |
|
border-radius: 8px; |
|
cursor: pointer; |
|
font-size: 1em; |
|
font-weight: bold; |
|
transition: all 0.3s ease; |
|
box-shadow: 0 4px 10px rgba(0,0,0,0.2); |
|
} |
|
|
|
button:hover:not(:disabled) { |
|
transform: translateY(-2px); |
|
box-shadow: 0 6px 15px rgba(0,0,0,0.3); |
|
} |
|
|
|
button:disabled { |
|
background: #ccc; |
|
cursor: not-allowed; |
|
transform: none; |
|
box-shadow: none; |
|
} |
|
|
|
.evaluation { |
|
background: #e3f2fd; |
|
padding: 15px; |
|
border-radius: 10px; |
|
margin: 15px 0; |
|
border-left: 4px solid #2196F3; |
|
} |
|
|
|
.evaluation h3 { |
|
margin: 0 0 10px 0; |
|
color: #1976D2; |
|
} |
|
|
|
.move-history { |
|
background: #fff3e0; |
|
padding: 15px; |
|
border-radius: 10px; |
|
margin: 15px 0; |
|
border-left: 4px solid #FF9800; |
|
max-height: 200px; |
|
overflow-y: auto; |
|
} |
|
|
|
.move-history h3 { |
|
margin: 0 0 10px 0; |
|
color: #F57C00; |
|
} |
|
|
|
.loading { |
|
display: none; |
|
text-align: center; |
|
color: #666; |
|
font-style: italic; |
|
margin: 10px 0; |
|
} |
|
|
|
.loading.show { |
|
display: block; |
|
} |
|
|
|
.error { |
|
background: #ffebee; |
|
color: #c62828; |
|
padding: 10px; |
|
border-radius: 5px; |
|
margin: 10px 0; |
|
border-left: 4px solid #f44336; |
|
} |
|
|
|
@media (max-width: 768px) { |
|
.game-container { |
|
flex-direction: column; |
|
align-items: center; |
|
} |
|
|
|
.info-panel { |
|
min-width: auto; |
|
width: 100%; |
|
max-width: 500px; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<h1>🏁 Chess vs Stockfish</h1> |
|
|
|
<div class="game-container"> |
|
<div class="board-container"> |
|
<div id="myBoard" style="width: 400px"></div> |
|
</div> |
|
|
|
<div class="info-panel"> |
|
<div id="status" class="status playing">À vous de jouer !</div> |
|
|
|
<div class="loading" id="loading"> |
|
Stockfish réfléchit... ⏳ |
|
</div> |
|
|
|
<div class="evaluation"> |
|
<h3>📊 Évaluation</h3> |
|
<div id="evaluation">Position de départ</div> |
|
</div> |
|
|
|
<div class="move-history"> |
|
<h3>📝 Derniers coups</h3> |
|
<div id="moveHistory">Nouvelle partie</div> |
|
</div> |
|
|
|
<div class="controls"> |
|
<button id="newGameBtn">🔄 Nouvelle partie</button> |
|
<button id="analyzeBtn">🔍 Analyser position</button> |
|
<button id="flipBtn">🔄 Retourner échiquier</button> |
|
</div> |
|
|
|
<div id="error" class="error" style="display: none;"></div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
let game = new Chess(); |
|
let board = null; |
|
let gameOver = false; |
|
let moveHistory = []; |
|
|
|
|
|
const config = { |
|
draggable: true, |
|
position: 'start', |
|
onDragStart: onDragStart, |
|
onDrop: onDrop, |
|
onSnapEnd: onSnapEnd |
|
}; |
|
|
|
board = Chessboard('myBoard', config); |
|
|
|
function onDragStart(source, piece, position, orientation) { |
|
|
|
if (gameOver) return false; |
|
|
|
|
|
if (piece.search(/^b/) !== -1) return false; |
|
|
|
|
|
if (game.turn() === 'b') return false; |
|
} |
|
|
|
function onDrop(source, target) { |
|
|
|
const move = game.move({ |
|
from: source, |
|
to: target, |
|
promotion: 'q' |
|
}); |
|
|
|
|
|
if (move === null) return 'snapback'; |
|
|
|
|
|
makeMove(move.san); |
|
} |
|
|
|
function onSnapEnd() { |
|
board.position(game.fen()); |
|
} |
|
|
|
function makeMove(userMove) { |
|
if (gameOver) return; |
|
|
|
showLoading(true); |
|
hideError(); |
|
|
|
fetch('/api/move', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
}, |
|
body: JSON.stringify({ |
|
fen: game.fen(), |
|
move: userMove |
|
}) |
|
}) |
|
.then(response => response.json()) |
|
.then(data => { |
|
showLoading(false); |
|
|
|
if (data.error) { |
|
showError(data.error); |
|
|
|
game.undo(); |
|
board.position(game.fen()); |
|
return; |
|
} |
|
|
|
|
|
game.load(data.fen); |
|
board.position(game.fen()); |
|
|
|
|
|
if (data.stockfish_move) { |
|
addToMoveHistory(userMove, data.stockfish_move); |
|
} else { |
|
addToMoveHistory(userMove, null); |
|
} |
|
|
|
|
|
updateEvaluation(data.evaluation); |
|
|
|
|
|
if (data.game_over) { |
|
gameOver = true; |
|
updateStatus(data.result || 'Partie terminée', true); |
|
} else { |
|
updateStatus('À vous de jouer !', false); |
|
} |
|
}) |
|
.catch(error => { |
|
showLoading(false); |
|
showError('Erreur de communication: ' + error.message); |
|
|
|
game.undo(); |
|
board.position(game.fen()); |
|
}); |
|
} |
|
|
|
function showLoading(show) { |
|
const loading = document.getElementById('loading'); |
|
if (show) { |
|
loading.classList.add('show'); |
|
} else { |
|
loading.classList.remove('show'); |
|
} |
|
} |
|
|
|
function showError(message) { |
|
const errorDiv = document.getElementById('error'); |
|
errorDiv.textContent = message; |
|
errorDiv.style.display = 'block'; |
|
} |
|
|
|
function hideError() { |
|
document.getElementById('error').style.display = 'none'; |
|
} |
|
|
|
function updateStatus(message, isGameOver = false) { |
|
const statusDiv = document.getElementById('status'); |
|
statusDiv.textContent = message; |
|
statusDiv.className = isGameOver ? 'status game-over' : 'status playing'; |
|
} |
|
|
|
function updateEvaluation(evaluation) { |
|
const evalDiv = document.getElementById('evaluation'); |
|
|
|
if (evaluation.type === 'cp') { |
|
const centipawns = evaluation.value; |
|
const pawns = (centipawns / 100).toFixed(2); |
|
if (pawns > 0) { |
|
evalDiv.textContent = `Blancs +${pawns}`; |
|
} else if (pawns < 0) { |
|
evalDiv.textContent = `Noirs +${Math.abs(pawns)}`; |
|
} else { |
|
evalDiv.textContent = 'Position équilibrée'; |
|
} |
|
} else if (evaluation.type === 'mate') { |
|
const mateIn = evaluation.value; |
|
if (mateIn > 0) { |
|
evalDiv.textContent = `Mat en ${mateIn} pour les blancs`; |
|
} else if (mateIn < 0) { |
|
evalDiv.textContent = `Mat en ${Math.abs(mateIn)} pour les noirs`; |
|
} else { |
|
evalDiv.textContent = 'Mat ou Pat'; |
|
} |
|
} |
|
} |
|
|
|
function addToMoveHistory(userMove, stockfishMove) { |
|
moveHistory.push({user: userMove, stockfish: stockfishMove}); |
|
|
|
const historyDiv = document.getElementById('moveHistory'); |
|
const lastMoves = moveHistory.slice(-5); |
|
|
|
let historyText = ''; |
|
lastMoves.forEach((moves, index) => { |
|
const moveNum = moveHistory.length - lastMoves.length + index + 1; |
|
historyText += `${moveNum}. ${moves.user}`; |
|
if (moves.stockfish) { |
|
historyText += ` ${moves.stockfish}`; |
|
} |
|
historyText += '\n'; |
|
}); |
|
|
|
historyDiv.textContent = historyText || 'Aucun coup joué'; |
|
} |
|
|
|
|
|
document.getElementById('newGameBtn').addEventListener('click', function() { |
|
fetch('/api/new_game', {method: 'POST'}) |
|
.then(response => response.json()) |
|
.then(data => { |
|
game = new Chess(); |
|
board.start(); |
|
gameOver = false; |
|
moveHistory = []; |
|
updateStatus('À vous de jouer !', false); |
|
updateEvaluation({type: 'cp', value: 0}); |
|
document.getElementById('moveHistory').textContent = 'Nouvelle partie'; |
|
hideError(); |
|
}) |
|
.catch(error => { |
|
showError('Erreur lors du démarrage: ' + error.message); |
|
}); |
|
}); |
|
|
|
document.getElementById('analyzeBtn').addEventListener('click', function() { |
|
fetch('/api/analyze', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
}, |
|
body: JSON.stringify({fen: game.fen()}) |
|
}) |
|
.then(response => response.json()) |
|
.then(data => { |
|
if (data.error) { |
|
showError(data.error); |
|
return; |
|
} |
|
|
|
updateEvaluation(data.evaluation); |
|
if (data.best_move) { |
|
showError(`Meilleur coup suggéré: ${data.best_move}`); |
|
} |
|
}) |
|
.catch(error => { |
|
showError('Erreur d\'analyse: ' + error.message); |
|
}); |
|
}); |
|
|
|
document.getElementById('flipBtn').addEventListener('click', function() { |
|
board.flip(); |
|
}); |
|
|
|
|
|
updateEvaluation({type: 'cp', value: 0}); |
|
</script> |
|
</body> |
|
</html> |