darkmaria / templates /index.html
Docfile's picture
Update templates/index.html
294f52c verified
raw
history blame
24.4 kB
<!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>
// Configuration du jeu
let gameMode = 'pvp';
let playerColor = 'white';
let isPlayerTurn = true;
let gameBoard = {};
let selectedSquare = null;
let possibleMoves = [];
let lastMoveSquares = [];
// Éléments DOM
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');
// Symboles des pièces d'échecs
const pieceSymbols = {
'K': '♔', 'Q': '♕', 'R': '♖', 'B': '♗', 'N': '♘', 'P': '♙',
'k': '♚', 'q': '♛', 'r': '♜', 'b': '♝', 'n': '♞', 'p': '♟'
};
// Position initiale FEN
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); // a-h
const square = fileChar + rank;
const squareEl = document.createElement('div');
squareEl.className = `chess-square ${(rank + file) % 2 === 0 ? 'light' : 'dark'}`;
squareEl.dataset.square = square;
// Ajouter les coordonnées
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);
}
// Event listeners pour les interactions
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 = {};
// Effacer toutes les pièces actuelles
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)) {
// C'est une pièce
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 {
// C'est un nombre (cases vides)
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...";
// En mode IA, on pourrait désactiver les interactions ici
} 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) {
// Déselection
clearSelection();
} else if (possibleMoves.includes(square)) {
// Coup valide
makeMove(selectedSquare + square);
} else if (piece && isPlayerPiece(piece)) {
// Sélection d'une autre pièce
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');
// Simuler les coups possibles (ici on devrait faire appel au serveur)
showPossibleMoves(square);
}
function clearSelection() {
selectedSquare = null;
possibleMoves = [];
document.querySelectorAll('.chess-square').forEach(sq => {
sq.classList.remove('selected', 'possible-move');
});
}
function showPossibleMoves(fromSquare) {
// Pour l'instant, on simule quelques coups possibles
// Dans une vraie implémentation, on ferait appel au serveur
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) {
// Simulation basique - dans la vraie version, ceci viendrait du serveur
const moves = [];
const file = square.charCodeAt(0) - 97;
const rank = parseInt(square[1]);
// Exemple pour un pion
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);
// Double move from starting position
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);
}
// Gestion du drag & drop
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);
// Sélectionner la case pour montrer les coups possibles
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) {
// Effacer les anciens highlights
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];
}
}
// Communication avec le serveur (adapté de votre code original)
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.";
}
}
// Form de saisie manuelle
moveForm.addEventListener('submit', async (e) => {
e.preventDefault();
const move = moveInput.value.trim();
if (!move) return;
await makeMove(move);
moveInput.value = '';
});
// Boutons de configuration
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'));
// Initialisation
createChessBoard();
updateBoardFromFEN(currentFEN);
</script>
</body>
</html>