From 758c4f4675d7cadcd2a1ef129546e8ab0c695876 Mon Sep 17 00:00:00 2001 From: Didictateur Date: Sat, 18 Oct 2025 20:44:19 +0200 Subject: [PATCH] affichage du plateau --- backend/index.js | 10 +++++ engine/core/board.js | 2 +- engine/core/game_state.js | 2 +- frontend/public/game.html | 82 +++++++++++++++++++++++++++------- frontend/public/src/game.js | 68 ++++++++++++++++++++++++---- frontend/public/src/main.js | 37 +++++++++++---- frontend/public/src/styles.css | 1 - frontend/src/game.js | 2 +- frontend/src/styles.css | 1 - 9 files changed, 167 insertions(+), 38 deletions(-) diff --git a/backend/index.js b/backend/index.js index 61dfa68..0b9ac8f 100644 --- a/backend/index.js +++ b/backend/index.js @@ -288,6 +288,16 @@ app.post('/api/start', async (req, res) => { if(Math.random() < 0.5){ p0.colorAssigned = 'white'; p1.colorAssigned = 'black'; } else { p0.colorAssigned = 'black'; p1.colorAssigned = 'white'; } } assign(); + // Safety: if both players somehow received the same color (bug/regression), force distinct colors + try{ + if (p0.colorAssigned && p1.colorAssigned && p0.colorAssigned === p1.colorAssigned) { + log('api/start: duplicate colorAssigned detected, forcing distinct assignment', { gameId: g.id, p0: p0.id, p1: p1.id, current: p0.colorAssigned }); + p0.colorAssigned = 'white'; + p1.colorAssigned = 'black'; + } + }catch(e){ log('api/start: safety assignment check failed', { err: e && e.stack }); } + // Notify clients about players/colors immediately + try{ io.to(g.id).emit('player-update', g); }catch(e){ log('failed to emit player-update after assign', { err: e && e.stack }); } g.started = true; // Initialize a minimal game.state to be consumed by frontend. // We avoid importing engine/ here: state is a plain JS structure describing the board. diff --git a/engine/core/board.js b/engine/core/board.js index 291ddb9..fd20c08 100644 --- a/engine/core/board.js +++ b/engine/core/board.js @@ -79,7 +79,7 @@ class Board { this.setPiece(5, 7, new Piece(PieceColor.BLACK, PieceType.BISHOP, [Movement.BishopMove])); // Queens and Kings - this.setPiece(3, 4, new Piece(PieceColor.WHITE, PieceType.QUEEN, [Movement.QueenMove])); + this.setPiece(3, 0, new Piece(PieceColor.WHITE, PieceType.QUEEN, [Movement.QueenMove])); this.setPiece(4, 0, new Piece(PieceColor.WHITE, PieceType.KING, [Movement.KingMove])); this.setPiece(3, 7, new Piece(PieceColor.BLACK, PieceType.QUEEN, [Movement.QueenMove])); this.setPiece(4, 7, new Piece(PieceColor.BLACK, PieceType.KING, [Movement.KingMove])); diff --git a/engine/core/game_state.js b/engine/core/game_state.js index 4d459c9..145aa7a 100644 --- a/engine/core/game_state.js +++ b/engine/core/game_state.js @@ -5,7 +5,7 @@ import Stack from './stack.js'; class GameState { constructor() { /** @type {Board} */ - this.board = new Board(8, 9); + this.board = new Board(8, 8); /** @type {Stack} */ this.stack = new Stack(); /** @type {Team} */ diff --git a/frontend/public/game.html b/frontend/public/game.html index 7e89c72..3bdb11d 100644 --- a/frontend/public/game.html +++ b/frontend/public/game.html @@ -22,6 +22,11 @@

Partie

Chargement...
+
Couleur: —
+
+ Joueurs: +
    +
    Chargement de l'état du serveur...
    @@ -43,23 +48,66 @@ // fetch initial state: prefer explicit backend host then fallback to same-origin (function(){ function handleGameObj(g){ - // render board from state or empty board - if(g && g.state && typeof renderBoardFromState === 'function') renderBoardFromState(g.state); - else if(typeof renderBoardFromState === 'function') renderBoardFromState(null); - meta.textContent = 'Partie ' + (g && g.id ? g.id : gid); - const sli = document.getElementById('server-load-indicator'); if(sli) sli.style.display = 'none'; - // set globals expected by src/game.js so socket handlers attach correctly - try { - currentGameId = gid; - if(g && g.players && g.players[0]) myPlayerId = g.players[0].id; - if(typeof socket === 'undefined' || !socket){ - socket = io(); - socket.on('connect', () => { socket.emit('join-game', gid); socket.emit('identify', { gameId: gid, playerId: myPlayerId }); }); - socket.on('game-state', (s) => { if(typeof renderBoardFromState === 'function') renderBoardFromState(s); if(typeof setMeta === 'function') setMeta('game-state updated'); }); - socket.on('move', (m) => { if(typeof setMeta === 'function') setMeta('remote move ' + JSON.stringify(m)); }); - socket.on('player-update', (g) => { if(typeof setMeta === 'function') setMeta('players updated'); }); - } - } catch(e){ console.warn('socket init failed', e); } + // set globals expected by src/game.js so socket handlers attach correctly + try { + currentGameId = gid; + // prefer a session-scoped stored player id for this game (persisted at join/create) + try{ + // support multiple possible sessionStorage key formats used across the app + const candidateKeys = [ + 'chessnut:game:' + gid + ':playerId', // older/other modules + 'playerId:' + gid // newer lightweight key used elsewhere + ]; + let stored = null; + for (const k of candidateKeys) { + stored = sessionStorage.getItem(k); + if (stored) { myPlayerId = stored; break; } + } + // if still not found, fall back to first player from server and persist under both keys + if (!stored && g && g.players && g.players[0]) { + myPlayerId = g.players[0].id; + try{ + sessionStorage.setItem('chessnut:game:' + gid + ':playerId', myPlayerId); + sessionStorage.setItem('playerId:' + gid, myPlayerId); + }catch(e){} + } + }catch(e){} + // remember the last received game object and compute this client's assigned color (if any) + window.lastGameObj = g; + if (g && g.players && myPlayerId) { + const me = g.players.find(p => p.id === myPlayerId); + window.myColor = me && me.colorAssigned ? me.colorAssigned : null; + const pc = document.getElementById('player-color'); if(pc) pc.textContent = 'Couleur: ' + (window.myColor || 'non assignée'); + } + // render board from state or empty board (after computing myColor so render can honor orientation) + if(g && g.state && typeof renderBoardFromState === 'function') renderBoardFromState(g.state); + else if(typeof renderBoardFromState === 'function') renderBoardFromState(null); + const sli = document.getElementById('server-load-indicator'); if(sli) sli.style.display = 'none'; + + if(typeof socket === 'undefined' || !socket){ + socket = io(); + socket.on('connect', () => { socket.emit('join-game', gid); socket.emit('identify', { gameId: gid, playerId: myPlayerId }); }); + socket.on('game-state', (s) => { if(typeof renderBoardFromState === 'function') renderBoardFromState(s); if(typeof setMeta === 'function') setMeta('game-state updated'); }); + socket.on('move', (m) => { if(typeof setMeta === 'function') setMeta('remote move ' + JSON.stringify(m)); }); + socket.on('player-update', (g) => { + // update local cached game object, render players list and recompute myColor when players list changes + try{ + window.lastGameObj = g; + if (g) { try{ renderPlayersList(g); }catch(e){} } + if (g && g.players && myPlayerId) { + const me = g.players.find(p => p.id === myPlayerId); + window.myColor = me && me.colorAssigned ? me.colorAssigned : window.myColor; + const pc = document.getElementById('player-color'); if(pc) pc.textContent = 'Couleur: ' + (window.myColor || 'non assignée'); + } + // re-render board to apply orientation change if colorAssigned arrived late + if (typeof renderBoardFromState === 'function' && g && g.state) renderBoardFromState(g.state); + }catch(e){} + if(typeof setMeta === 'function') setMeta('players updated'); + }); + } + // ensure players list is rendered on initial load + try{ if(window.lastGameObj) renderPlayersList(window.lastGameObj); }catch(e){} + } catch(e){ console.warn('socket init failed', e); } } fetch('http://localhost:4000/api/game/' + gid).then(r => r.ok ? r.json() : null).then(g => { diff --git a/frontend/public/src/game.js b/frontend/public/src/game.js index caf9561..4944138 100644 --- a/frontend/public/src/game.js +++ b/frontend/public/src/game.js @@ -6,7 +6,7 @@ let myPlayerId = null; let currentGameId = null; let selected = null; // {x,y} -function renderBoardFromState(state) { +function renderBoardFromState(state, asWhite = null) { const container = q('#board-container'); if(!container){ console.error('renderBoardFromState: #board-container not found'); return; } console.log('renderBoardFromState called, state present?', !!state); @@ -35,13 +35,37 @@ function renderBoardFromState(state) { boardEl.style.maxWidth = 'min(90vmin, 80vw)'; boardEl.style.width = '100%'; - for (let r = 0; r < h; r++) { - for (let c = 0; c < w; c++) { + // determine orientation: explicit asWhite param > window.myColor > default true + try{ + let useWhite = true; + if (asWhite === null) { + if (window && window.myColor) useWhite = (String(window.myColor).toLowerCase() === 'white'); + } else { + useWhite = !!asWhite; + } + console.log('[renderBoardFromState] myPlayerId=', myPlayerId, 'window.myColor=', window && window.myColor, 'asWhite param=', asWhite, 'computed useWhite=', useWhite); + // set dataset for easier inspection in DevTools + try { boardEl.dataset.orientation = useWhite ? 'white' : 'black'; container.dataset.myPlayerId = myPlayerId || ''; } catch(e) {} + if (!useWhite) boardEl.classList.add('flipped'); + }catch(e){} + + // Render cells in visual order depending on orientation. We do not rotate the element; instead we map + // visual coordinates -> state coordinates so piece images remain upright. + const useWhite = (boardEl.dataset.orientation || 'white') === 'white' || (window && window.myColor && String(window.myColor).toLowerCase() === 'white'); + for (let vr = 0; vr < h; vr++) { + for (let vc = 0; vc < w; vc++) { + // Map visual row/col to state row/col depending on orientation + // For white perspective we want white pieces at the bottom (state row 0 -> visual bottom), + // so state row = h-1 - vr. For black perspective, state row = vr. + const r = useWhite ? (h - 1 - vr) : vr; + // For white perspective, file order is left->right = state col 0..w-1; for black it's mirrored. + const c = useWhite ? vc : (w - 1 - vc); const cell = document.createElement('div'); - cell.className = 'cell'; - // alternate light/dark based on coordinates (classic checkerboard) - const isLight = ((r + c) % 2) === 0; - cell.classList.add(isLight ? 'light' : 'dark'); + cell.className = 'cell'; + // alternate light/dark based on visual coordinates so checkerboard looks proper + const isLight = ((vr + vc) % 2) === 0; + cell.classList.add(isLight ? 'light' : 'dark'); + // store state coordinates so click handlers send correct moves cell.dataset.x = c; cell.dataset.y = r; const piece = document.createElement('img'); @@ -90,17 +114,35 @@ function renderBoardFromState(state) { } } container.appendChild(boardEl); + // ensure player color display is in sync + try{ const pc = document.getElementById('player-color'); if(pc && window && window.myColor) pc.textContent = 'Couleur: ' + window.myColor; }catch(e){} console.log('renderBoardFromState: board appended (w=' + w + ', h=' + h + ')'); } function setMeta(text) { q('#meta').textContent = text; } +function renderPlayersList(gameObj){ + try{ + const ul = document.getElementById('players-list'); if(!ul) return; + ul.innerHTML = ''; + const players = (gameObj && gameObj.players) ? gameObj.players : []; + players.forEach(p => { + const li = document.createElement('li'); + li.textContent = (p.nickname || p.id) + ' — ' + (p.colorAssigned || 'non assignée'); + if(window && myPlayerId && p.id === myPlayerId) li.textContent += ' (vous)'; + ul.appendChild(li); + }); + }catch(e){} +} + q('#create-only')?.addEventListener('click', async () => { const name = q('#game-name').value || 'game-from-ui'; const res = await fetch('/api/create', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ name, ownerNickname: 'ui-host' }) }); const j = await res.json(); setMeta('Created: ' + j.game.id); q('#game-id-input').value = j.game.id; + // store owner player id in session so host remembers which player they are + try{ if (j.player && j.game && j.player.id) { sessionStorage.setItem('chessnut:game:' + j.game.id + ':playerId', j.player.id); sessionStorage.setItem('playerId:' + j.game.id, j.player.id); } }catch(e){} }); q('#create-start')?.addEventListener('click', async () => { @@ -111,7 +153,13 @@ q('#create-start')?.addEventListener('click', async () => { const id = createJson.game.id; setMeta('Created: ' + id + ' — joining as second player...'); // join as second player - await fetch('/api/join', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ id, nickname: 'ui-guest' }) }); + const joinResp = await fetch('/api/join', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ id, nickname: 'ui-guest' }) }); + const joinJson = await joinResp.json(); + // if join returned a player object, use it as this client's player id (we're acting as the guest here) + if (joinJson && joinJson.player && joinJson.player.id) { + myPlayerId = joinJson.player.id; + try{ sessionStorage.setItem('chessnut:game:' + id + ':playerId', myPlayerId); sessionStorage.setItem('playerId:' + id, myPlayerId); }catch(e){} + } setMeta('Joined — starting...'); const startResp = await fetch('/api/start', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ id }) }); const startJson = await startResp.json(); @@ -122,6 +170,8 @@ q('#create-start')?.addEventListener('click', async () => { // try to capture the second player id from server game.players const players = startJson.game.players || []; if (players[1]) myPlayerId = players[1].id; else if (players[0]) myPlayerId = players[0].id; + // persist player id in sessionStorage (per-tab) so reloads can identify this client without colliding across tabs + try{ if(myPlayerId && id) { sessionStorage.setItem('chessnut:game:' + id + ':playerId', myPlayerId); sessionStorage.setItem('playerId:' + id, myPlayerId); } }catch(e){} if (!socket) { socket = io(); socket.on('connect', () => { socket.emit('join-game', currentGameId); socket.emit('identify', { gameId: currentGameId, playerId: myPlayerId }); }); @@ -144,6 +194,8 @@ q('#load')?.addEventListener('click', async () => { currentGameId = id; // choose a player id if any exists (by default pick first) if (j.players && j.players.length > 0) myPlayerId = j.players[0].id; + // prefer stored player id if present (sessionStorage is per-tab so two tabs won't collide) + try{ const stored = sessionStorage.getItem('chessnut:game:' + id + ':playerId') || sessionStorage.getItem('playerId:' + id); if(stored) myPlayerId = stored; else if(myPlayerId && id) { sessionStorage.setItem('chessnut:game:' + id + ':playerId', myPlayerId); sessionStorage.setItem('playerId:' + id, myPlayerId); } }catch(e){} if (!socket) { socket = io(); socket.on('connect', () => { socket.emit('join-game', currentGameId); socket.emit('identify', { gameId: currentGameId, playerId: myPlayerId }); }); diff --git a/frontend/public/src/main.js b/frontend/public/src/main.js index 01a556c..bf19f18 100644 --- a/frontend/public/src/main.js +++ b/frontend/public/src/main.js @@ -11,7 +11,28 @@ function showBoard(gameId, role) { q('#board-section')?.classList?.remove('hidden'); q('#game-id').textContent = gameId; q('#player-role').textContent = role; - renderBoard(role === 'white'); + // determine this client's assigned color (if any) so we can render board from that perspective + (function(){ + const stored = sessionStorage.getItem(`playerId:${gameId}`) || sessionStorage.getItem('chessnut:game:' + gameId + ':playerId'); + // prefer server-provided state to compute orientation when available + fetch(`/api/game/${gameId}`).then(r => { if(!r.ok) throw new Error('no api'); return r.json(); }).then(g => { + try{ + let asWhite = true; + if (stored && g && g.players) { + const me = g.players.find(p => p.id === stored); + if (me && me.colorAssigned) asWhite = (String(me.colorAssigned).toLowerCase() === 'white'); + } else if (role) { + // fall back to role passed by caller + asWhite = (role === 'white'); + } + if (g && g.state && typeof renderBoardFromState === 'function') renderBoardFromState(g.state, asWhite); + else renderBoard(asWhite); + }catch(e){ renderBoard(role === 'white'); } + }).catch(() => { + // backend not reachable: fall back to role param + renderBoard(role === 'white'); + }); + })(); } q('#create-btn').addEventListener('click', () => { @@ -19,7 +40,6 @@ q('#create-btn').addEventListener('click', () => { const id = 'game-' + Math.random().toString(36).slice(2,9); const pass = q('#create-pass').value; const name = q('#create-name').value || id; - // minimal create: we don't prompt for a pseudo; server will assign an internal id const nick = 'player'; const colorChoice = 'random'; const game = { id, name, pass: pass || null, owner: 'you', created: Date.now(), players: [{ id: 'p-' + Math.random().toString(36).slice(2,6), nickname: nick, colorChoice, colorAssigned: null }], started: false }; @@ -116,8 +136,7 @@ function renderGameListItem(ul, g){ btn.addEventListener('click', () => { console.log('[frontend] join clicked for', g.id); if(g.pass){ const entered = prompt('Mot de passe pour rejoindre la partie'); if(entered !== g.pass){ alert('Mot de passe incorrect'); return; } } - const nick = prompt('Ton pseudo') || 'Player'; - const payload = { id: g.id, pass: g.pass, nickname: nick }; + const payload = { id: g.id, pass: g.pass, nickname: 'player' }; // prefer backend host first fetch('http://localhost:4000/api/join', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(payload) }) @@ -148,12 +167,14 @@ function renderBoard(asWhite=true) { container.innerHTML = ''; const board = document.createElement('div'); board.className = 'board'; - if (!asWhite) board.classList.add('flipped'); - - for (let r = 0; r < 8; r++) { + // render without CSS rotation: draw ranks/files in visual order + for (let vr = 0; vr < 8; vr++) { const row = document.createElement('div'); row.className = 'row'; - for (let c = 0; c < 8; c++) { + for (let vc = 0; vc < 8; vc++) { + // For white perspective we want white at bottom -> state row = 7 - vr + const r = asWhite ? (7 - vr) : vr; + const c = asWhite ? vc : (7 - vc); const cell = document.createElement('div'); cell.className = 'cell'; const piece = document.createElement('img'); diff --git a/frontend/public/src/styles.css b/frontend/public/src/styles.css index d048bc0..61b3f44 100644 --- a/frontend/public/src/styles.css +++ b/frontend/public/src/styles.css @@ -17,7 +17,6 @@ main{padding:16px} .board > .cell { width:100%; height:100%; display:flex; align-items:center; justify-content:center; background:#f0d9b5; border:1px solid #b58863; margin:0; } .cell.light { background: #f0d9b5; } .cell.dark { background: #b58863; } -.board.flipped{transform:rotate(180deg)} .piece{width:70%;height:70%;object-fit:contain} .game-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px} .controls button{margin-left:8px} diff --git a/frontend/src/game.js b/frontend/src/game.js index 5a1a3d3..c9e951c 100644 --- a/frontend/src/game.js +++ b/frontend/src/game.js @@ -34,7 +34,7 @@ function renderBoardFromState(state) { const cell = document.createElement('div'); cell.className = 'cell'; // alternate light/dark based on coordinates (classic checkerboard) - const isLight = ((r + c) % 2) === 0; + const isLight = ((r + c) % 2) === 1; cell.classList.add(isLight ? 'light' : 'dark'); cell.dataset.x = c; cell.dataset.y = r; diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 0f15aa9..4715668 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -16,7 +16,6 @@ main{padding:16px} .board > .cell { width:100%; height:100%; display:flex; align-items:center; justify-content:center; background:#f0d9b5; border:1px solid #b58863; margin:0; } .cell.light { background: #f0d9b5; } .cell.dark { background: #b58863; } -.board.flipped{transform:rotate(180deg)} .piece{width:48px;height:48px} .game-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px} .controls button{margin-left:8px}