|
<!DOCTYPE html> |
|
<html lang="fr"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Jeu d'Échecs Tactile</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<style> |
|
.chess-board { |
|
display: grid; |
|
grid-template-columns: repeat(8, 1fr); |
|
grid-template-rows: repeat(8, 1fr); |
|
aspect-ratio: 1; |
|
max-width: 600px; |
|
margin: 0 auto; |
|
border: 3px solid #374151; |
|
border-radius: 8px; |
|
overflow: hidden; |
|
user-select: none; |
|
} |
|
|
|
.chess-square { |
|
position: relative; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
cursor: pointer; |
|
transition: all 0.2s ease; |
|
} |
|
|
|
.chess-square.light { |
|
background-color: #f0d9b5; |
|
} |
|
|
|
.chess-square.dark { |
|
background-color: #b58863; |
|
} |
|
|
|
.chess-square.selected { |
|
box-shadow: inset 0 0 0 4px #fbbf24; |
|
z-index: 2; |
|
} |
|
|
|
.chess-square.possible-move { |
|
box-shadow: inset 0 0 0 3px #10b981; |
|
} |
|
|
|
.chess-square.possible-move:after { |
|
content: ''; |
|
position: absolute; |
|
width: 30%; |
|
height: 30%; |
|
background-color: #10b981; |
|
border-radius: 50%; |
|
opacity: 0.7; |
|
} |
|
|
|
.chess-square.last-move { |
|
box-shadow: inset 0 0 0 3px #eab308; |
|
} |
|
|
|
.chess-piece { |
|
font-size: clamp(2rem, 6vw, 3.5rem); |
|
font-family: 'Segoe UI Symbol', 'Apple Symbols', sans-serif; |
|
cursor: grab; |
|
transition: transform 0.1s ease; |
|
z-index: 1; |
|
} |
|
|
|
.chess-piece:hover { |
|
transform: scale(1.1); |
|
} |
|
|
|
.chess-piece.dragging { |
|
cursor: grabbing; |
|
transform: scale(1.2); |
|
z-index: 10; |
|
pointer-events: none; |
|
} |
|
|
|
.coordinates { |
|
color: #6b7280; |
|
font-size: 0.75rem; |
|
font-weight: bold; |
|
} |
|
|
|
.coord-file { |
|
position: absolute; |
|
bottom: 2px; |
|
right: 4px; |
|
} |
|
|
|
.coord-rank { |
|
position: absolute; |
|
top: 2px; |
|
left: 4px; |
|
} |
|
|
|
@media (max-width: 640px) { |
|
.chess-piece { |
|
font-size: 2rem; |
|
} |
|
.coordinates { |
|
font-size: 0.6rem; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-gray-900 text-gray-100 min-h-screen flex flex-col items-center p-4"> |
|
|
|
<div class="container mx-auto max-w-6xl"> |
|
<h1 class="text-4xl font-bold text-center my-6 text-teal-400">Jeu d'Échecs Tactile</h1> |
|
|
|
<div class="bg-gray-800 p-6 rounded-lg shadow-xl mb-6"> |
|
<h2 class="text-2xl font-semibold mb-3 text-sky-400">Configuration</h2> |
|
<div class="flex flex-wrap gap-4 items-center"> |
|
<button id="setPvP" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"> |
|
Humain vs Humain |
|
</button> |
|
<div> |
|
<button id="setPvAIWhite" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"> |
|
Jouer vs IA (Blancs) |
|
</button> |
|
<button id="setPvAIBlack" class="bg-purple-600 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline mt-2 sm:mt-0 sm:ml-2"> |
|
Jouer vs IA (Noirs) |
|
</button> |
|
</div> |
|
<button id="resetGame" class="bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"> |
|
Nouvelle Partie |
|
</button> |
|
</div> |
|
<p class="mt-3 text-sm text-gray-400">Mode: <span id="currentMode" class="font-semibold">PVP</span></p> |
|
<p id="playerColorInfo" class="mt-1 text-sm text-gray-400 hidden"> |
|
Vous jouez: <span id="currentPlayerColor" class="font-semibold">Blancs</span> |
|
</p> |
|
</div> |
|
|
|
<div class="grid lg:grid-cols-4 gap-6"> |
|
<div class="lg:col-span-3 bg-gray-800 p-4 rounded-lg shadow-xl"> |
|
<div id="chessBoard" class="chess-board"></div> |
|
<div class="mt-4 text-center text-sm text-gray-400"> |
|
<p>Cliquez sur une pièce puis sur la destination, ou glissez-déposez</p> |
|
</div> |
|
</div> |
|
|
|
<div class="bg-gray-800 p-6 rounded-lg shadow-xl"> |
|
<h3 class="text-xl font-semibold mb-3 text-sky-400">Informations</h3> |
|
<p id="turnDisplay" class="mb-2">Tour: <span class="font-bold text-white">Blancs</span></p> |
|
<p id="status" class="text-yellow-400 font-semibold mb-4 min-h-6"></p> |
|
|
|
<div id="outcomeDisplay" class="mb-4 text-lg font-bold text-center"></div> |
|
|
|
<div class="space-y-3"> |
|
<div> |
|
<p class="text-sm text-gray-400">Dernier coup:</p> |
|
<p id="lastMove" class="text-gray-200 font-mono text-sm">-</p> |
|
</div> |
|
<div> |
|
<p class="text-sm text-gray-400">Dernier coup IA:</p> |
|
<p id="lastAIMove" class="text-gray-200 font-mono text-sm">-</p> |
|
</div> |
|
</div> |
|
|
|
<div class="mt-6"> |
|
<h4 class="text-lg font-semibold mb-2 text-sky-400">Notation manuelle</h4> |
|
<form id="moveForm" class="space-y-3"> |
|
<input type="text" id="moveInput" |
|
class="w-full bg-gray-700 border border-gray-600 rounded-md py-2 px-3 focus:outline-none focus:ring-sky-500 focus:border-sky-500 text-white text-sm" |
|
placeholder="e.g., e2e4, Nf3"> |
|
<button type="submit" |
|
class="w-full bg-teal-600 hover:bg-teal-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline transition duration-150 ease-in-out"> |
|
Jouer |
|
</button> |
|
</form> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
|
|
let gameMode = 'pvp'; |
|
let playerColor = 'white'; |
|
let isPlayerTurn = true; |
|
let gameBoard = {}; |
|
let selectedSquare = null; |
|
let possibleMoves = []; |
|
let lastMoveSquares = []; |
|
|
|
|
|
const chessBoardEl = document.getElementById('chessBoard'); |
|
const moveForm = document.getElementById('moveForm'); |
|
const moveInput = document.getElementById('moveInput'); |
|
const statusDisplay = document.getElementById('status'); |
|
const turnDisplay = document.getElementById('turnDisplay').querySelector('span'); |
|
const outcomeDisplay = document.getElementById('outcomeDisplay'); |
|
const lastMoveDisplay = document.getElementById('lastMove'); |
|
const lastAIMoveDisplay = document.getElementById('lastAIMove'); |
|
const currentModeDisplay = document.getElementById('currentMode'); |
|
const playerColorInfoDisplay = document.getElementById('playerColorInfo'); |
|
const currentPlayerColorDisplay = document.getElementById('currentPlayerColor'); |
|
|
|
|
|
const pieceSymbols = { |
|
'K': '♔', 'Q': '♕', 'R': '♖', 'B': '♗', 'N': '♘', 'P': '♙', |
|
'k': '♚', 'q': '♛', 'r': '♜', 'b': '♝', 'n': '♞', 'p': '♟' |
|
}; |
|
|
|
|
|
let currentFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; |
|
|
|
function createChessBoard() { |
|
chessBoardEl.innerHTML = ''; |
|
|
|
for (let rank = 8; rank >= 1; rank--) { |
|
for (let file = 0; file < 8; file++) { |
|
const fileChar = String.fromCharCode(97 + file); |
|
const square = fileChar + rank; |
|
|
|
const squareEl = document.createElement('div'); |
|
squareEl.className = `chess-square ${(rank + file) % 2 === 0 ? 'light' : 'dark'}`; |
|
squareEl.dataset.square = square; |
|
|
|
|
|
if (file === 0) { |
|
const rankCoord = document.createElement('div'); |
|
rankCoord.className = 'coordinates coord-rank'; |
|
rankCoord.textContent = rank; |
|
squareEl.appendChild(rankCoord); |
|
} |
|
if (rank === 1) { |
|
const fileCoord = document.createElement('div'); |
|
fileCoord.className = 'coordinates coord-file'; |
|
fileCoord.textContent = fileChar; |
|
squareEl.appendChild(fileCoord); |
|
} |
|
|
|
|
|
squareEl.addEventListener('click', handleSquareClick); |
|
squareEl.addEventListener('dragover', handleDragOver); |
|
squareEl.addEventListener('drop', handleDrop); |
|
|
|
chessBoardEl.appendChild(squareEl); |
|
} |
|
} |
|
} |
|
|
|
function updateBoardFromFEN(fen) { |
|
const [boardPart] = fen.split(' '); |
|
const ranks = boardPart.split('/'); |
|
|
|
gameBoard = {}; |
|
|
|
|
|
document.querySelectorAll('.chess-piece').forEach(piece => piece.remove()); |
|
|
|
for (let rankIdx = 0; rankIdx < 8; rankIdx++) { |
|
const rank = 8 - rankIdx; |
|
const rankStr = ranks[rankIdx]; |
|
let fileIdx = 0; |
|
|
|
for (let char of rankStr) { |
|
if (isNaN(char)) { |
|
|
|
const file = String.fromCharCode(97 + fileIdx); |
|
const square = file + rank; |
|
gameBoard[square] = char; |
|
|
|
const squareEl = document.querySelector(`[data-square="${square}"]`); |
|
if (squareEl) { |
|
const pieceEl = document.createElement('div'); |
|
pieceEl.className = 'chess-piece'; |
|
pieceEl.textContent = pieceSymbols[char]; |
|
pieceEl.draggable = true; |
|
pieceEl.dataset.piece = char; |
|
pieceEl.dataset.square = square; |
|
|
|
pieceEl.addEventListener('dragstart', handleDragStart); |
|
pieceEl.addEventListener('dragend', handleDragEnd); |
|
|
|
squareEl.appendChild(pieceEl); |
|
} |
|
fileIdx++; |
|
} else { |
|
|
|
fileIdx += parseInt(char); |
|
} |
|
} |
|
} |
|
|
|
currentFEN = fen; |
|
updateTurnDisplay(); |
|
} |
|
|
|
function updateTurnDisplay() { |
|
const turn = currentFEN.split(' ')[1]; |
|
turnDisplay.textContent = turn === 'w' ? 'Blancs' : 'Noirs'; |
|
|
|
if (gameMode === 'ai') { |
|
const aiIsWhite = (playerColor === 'black'); |
|
const aiIsBlack = (playerColor === 'white'); |
|
|
|
if ((aiIsWhite && turn === 'w') || (aiIsBlack && turn === 'b')) { |
|
isPlayerTurn = false; |
|
statusDisplay.textContent = "L'IA réfléchit..."; |
|
|
|
} else { |
|
isPlayerTurn = true; |
|
statusDisplay.textContent = "À vous de jouer"; |
|
} |
|
} else { |
|
isPlayerTurn = true; |
|
statusDisplay.textContent = ""; |
|
} |
|
} |
|
|
|
function handleSquareClick(e) { |
|
if (!isPlayerTurn) return; |
|
|
|
const square = e.currentTarget.dataset.square; |
|
const piece = gameBoard[square]; |
|
|
|
if (selectedSquare) { |
|
if (selectedSquare === square) { |
|
|
|
clearSelection(); |
|
} else if (possibleMoves.includes(square)) { |
|
|
|
makeMove(selectedSquare + square); |
|
} else if (piece && isPlayerPiece(piece)) { |
|
|
|
selectSquare(square); |
|
} else { |
|
clearSelection(); |
|
} |
|
} else if (piece && isPlayerPiece(piece)) { |
|
selectSquare(square); |
|
} |
|
} |
|
|
|
function selectSquare(square) { |
|
clearSelection(); |
|
selectedSquare = square; |
|
|
|
const squareEl = document.querySelector(`[data-square="${square}"]`); |
|
squareEl.classList.add('selected'); |
|
|
|
|
|
showPossibleMoves(square); |
|
} |
|
|
|
function clearSelection() { |
|
selectedSquare = null; |
|
possibleMoves = []; |
|
|
|
document.querySelectorAll('.chess-square').forEach(sq => { |
|
sq.classList.remove('selected', 'possible-move'); |
|
}); |
|
} |
|
|
|
function showPossibleMoves(fromSquare) { |
|
|
|
|
|
const piece = gameBoard[fromSquare]; |
|
possibleMoves = getPossibleMovesForPiece(fromSquare, piece); |
|
|
|
possibleMoves.forEach(square => { |
|
const squareEl = document.querySelector(`[data-square="${square}"]`); |
|
if (squareEl) { |
|
squareEl.classList.add('possible-move'); |
|
} |
|
}); |
|
} |
|
|
|
function getPossibleMovesForPiece(square, piece) { |
|
|
|
const moves = []; |
|
const file = square.charCodeAt(0) - 97; |
|
const rank = parseInt(square[1]); |
|
|
|
|
|
if (piece.toLowerCase() === 'p') { |
|
const direction = piece === 'P' ? 1 : -1; |
|
const newRank = rank + direction; |
|
if (newRank >= 1 && newRank <= 8) { |
|
const newSquare = String.fromCharCode(97 + file) + newRank; |
|
if (!gameBoard[newSquare]) { |
|
moves.push(newSquare); |
|
|
|
|
|
if ((piece === 'P' && rank === 2) || (piece === 'p' && rank === 7)) { |
|
const doubleSquare = String.fromCharCode(97 + file) + (rank + 2 * direction); |
|
if (!gameBoard[doubleSquare]) { |
|
moves.push(doubleSquare); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
return moves; |
|
} |
|
|
|
function isPlayerPiece(piece) { |
|
if (gameMode === 'pvp') return true; |
|
|
|
const isWhitePiece = piece === piece.toUpperCase(); |
|
return (playerColor === 'white' && isWhitePiece) || |
|
(playerColor === 'black' && !isWhitePiece); |
|
} |
|
|
|
|
|
function handleDragStart(e) { |
|
if (!isPlayerTurn) { |
|
e.preventDefault(); |
|
return; |
|
} |
|
|
|
const piece = e.target.dataset.piece; |
|
if (!isPlayerPiece(piece)) { |
|
e.preventDefault(); |
|
return; |
|
} |
|
|
|
e.target.classList.add('dragging'); |
|
e.dataTransfer.setData('text/plain', e.target.dataset.square); |
|
|
|
|
|
selectSquare(e.target.dataset.square); |
|
} |
|
|
|
function handleDragEnd(e) { |
|
e.target.classList.remove('dragging'); |
|
} |
|
|
|
function handleDragOver(e) { |
|
e.preventDefault(); |
|
} |
|
|
|
function handleDrop(e) { |
|
e.preventDefault(); |
|
const fromSquare = e.dataTransfer.getData('text/plain'); |
|
const toSquare = e.currentTarget.dataset.square; |
|
|
|
if (fromSquare && toSquare && fromSquare !== toSquare) { |
|
if (possibleMoves.includes(toSquare)) { |
|
makeMove(fromSquare + toSquare); |
|
} |
|
} |
|
|
|
clearSelection(); |
|
} |
|
|
|
function highlightLastMove(move) { |
|
|
|
document.querySelectorAll('.chess-square').forEach(sq => { |
|
sq.classList.remove('last-move'); |
|
}); |
|
|
|
if (move && move.length >= 4) { |
|
const fromSquare = move.substring(0, 2); |
|
const toSquare = move.substring(2, 4); |
|
|
|
const fromEl = document.querySelector(`[data-square="${fromSquare}"]`); |
|
const toEl = document.querySelector(`[data-square="${toSquare}"]`); |
|
|
|
if (fromEl) fromEl.classList.add('last-move'); |
|
if (toEl) toEl.classList.add('last-move'); |
|
|
|
lastMoveSquares = [fromSquare, toSquare]; |
|
} |
|
} |
|
|
|
|
|
async function makeMove(moveStr) { |
|
statusDisplay.textContent = 'Traitement...'; |
|
clearSelection(); |
|
|
|
try { |
|
const response = await fetch('/make_move', { |
|
method: 'POST', |
|
headers: { 'Content-Type': 'application/json' }, |
|
body: JSON.stringify({ move: moveStr }) |
|
}); |
|
const data = await response.json(); |
|
|
|
if (data.error) { |
|
statusDisplay.textContent = `Erreur: ${data.error}`; |
|
} else { |
|
updateBoardFromFEN(data.fen); |
|
lastMoveDisplay.textContent = moveStr; |
|
highlightLastMove(moveStr); |
|
|
|
if (data.ai_move_uci) { |
|
lastAIMoveDisplay.textContent = data.ai_move_uci; |
|
highlightLastMove(data.ai_move_uci); |
|
} |
|
|
|
if (data.game_over) { |
|
statusDisplay.textContent = "Partie terminée!"; |
|
outcomeDisplay.innerHTML = `<p class="text-green-400">${data.outcome}</p>`; |
|
isPlayerTurn = false; |
|
} |
|
} |
|
} catch (error) { |
|
console.error("Erreur lors de la communication:", error); |
|
statusDisplay.textContent = "Erreur de communication avec le serveur."; |
|
} |
|
} |
|
|
|
|
|
moveForm.addEventListener('submit', async (e) => { |
|
e.preventDefault(); |
|
const move = moveInput.value.trim(); |
|
if (!move) return; |
|
|
|
await makeMove(move); |
|
moveInput.value = ''; |
|
}); |
|
|
|
|
|
document.getElementById('resetGame').addEventListener('click', async () => { |
|
statusDisplay.textContent = 'Réinitialisation...'; |
|
try { |
|
const response = await fetch('/reset_game', { method: 'POST' }); |
|
const data = await response.json(); |
|
|
|
updateBoardFromFEN(data.fen); |
|
lastMoveDisplay.textContent = "-"; |
|
lastAIMoveDisplay.textContent = "-"; |
|
outcomeDisplay.innerHTML = ''; |
|
clearSelection(); |
|
|
|
currentModeDisplay.textContent = data.game_mode ? data.game_mode.toUpperCase() : 'PVP'; |
|
if (data.game_mode === 'ai' && data.player_color) { |
|
currentPlayerColorDisplay.textContent = data.player_color.charAt(0).toUpperCase() + data.player_color.slice(1); |
|
playerColorInfoDisplay.classList.remove('hidden'); |
|
} else { |
|
playerColorInfoDisplay.classList.add('hidden'); |
|
} |
|
|
|
gameMode = data.game_mode || 'pvp'; |
|
playerColor = data.player_color || 'white'; |
|
updateTurnDisplay(); |
|
|
|
} catch (error) { |
|
console.error("Erreur lors de la réinitialisation:", error); |
|
statusDisplay.textContent = "Erreur lors de la réinitialisation."; |
|
} |
|
}); |
|
|
|
async function setGameMode(mode, pColor = 'white') { |
|
statusDisplay.textContent = `Changement de mode vers ${mode.toUpperCase()}...`; |
|
try { |
|
const response = await fetch('/set_mode', { |
|
method: 'POST', |
|
headers: { 'Content-Type': 'application/json' }, |
|
body: JSON.stringify({ game_mode: mode, player_color: pColor }) |
|
}); |
|
const data = await response.json(); |
|
|
|
if (data.error) { |
|
statusDisplay.textContent = `Erreur: ${data.error}`; |
|
} else { |
|
gameMode = data.game_mode; |
|
playerColor = data.player_color; |
|
|
|
updateBoardFromFEN(data.fen); |
|
clearSelection(); |
|
|
|
currentModeDisplay.textContent = gameMode.toUpperCase(); |
|
if (gameMode === 'ai') { |
|
currentPlayerColorDisplay.textContent = playerColor.charAt(0).toUpperCase() + playerColor.slice(1); |
|
playerColorInfoDisplay.classList.remove('hidden'); |
|
} else { |
|
playerColorInfoDisplay.classList.add('hidden'); |
|
} |
|
|
|
statusDisplay.textContent = data.message || ''; |
|
if (data.initial_ai_move_uci) { |
|
lastAIMoveDisplay.textContent = data.initial_ai_move_uci; |
|
highlightLastMove(data.initial_ai_move_uci); |
|
} |
|
} |
|
} catch (error) { |
|
console.error("Erreur de changement de mode:", error); |
|
statusDisplay.textContent = "Erreur de changement de mode."; |
|
} |
|
} |
|
|
|
document.getElementById('setPvP').addEventListener('click', () => setGameMode('pvp')); |
|
document.getElementById('setPvAIWhite').addEventListener('click', () => setGameMode('ai', 'white')); |
|
document.getElementById('setPvAIBlack').addEventListener('click', () => setGameMode('ai', 'black')); |
|
|
|
|
|
createChessBoard(); |
|
updateBoardFromFEN(currentFEN); |
|
</script> |
|
</body> |
|
</html> |