affichage du plateau

This commit is contained in:
Didictateur 2025-10-18 20:44:19 +02:00
parent 18cfe05047
commit 758c4f4675
9 changed files with 167 additions and 38 deletions

View file

@ -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'; } if(Math.random() < 0.5){ p0.colorAssigned = 'white'; p1.colorAssigned = 'black'; } else { p0.colorAssigned = 'black'; p1.colorAssigned = 'white'; }
} }
assign(); 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; g.started = true;
// Initialize a minimal game.state to be consumed by frontend. // Initialize a minimal game.state to be consumed by frontend.
// We avoid importing engine/ here: state is a plain JS structure describing the board. // We avoid importing engine/ here: state is a plain JS structure describing the board.

View file

@ -79,7 +79,7 @@ class Board {
this.setPiece(5, 7, new Piece(PieceColor.BLACK, PieceType.BISHOP, [Movement.BishopMove])); this.setPiece(5, 7, new Piece(PieceColor.BLACK, PieceType.BISHOP, [Movement.BishopMove]));
// Queens and Kings // 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(4, 0, new Piece(PieceColor.WHITE, PieceType.KING, [Movement.KingMove]));
this.setPiece(3, 7, new Piece(PieceColor.BLACK, PieceType.QUEEN, [Movement.QueenMove])); this.setPiece(3, 7, new Piece(PieceColor.BLACK, PieceType.QUEEN, [Movement.QueenMove]));
this.setPiece(4, 7, new Piece(PieceColor.BLACK, PieceType.KING, [Movement.KingMove])); this.setPiece(4, 7, new Piece(PieceColor.BLACK, PieceType.KING, [Movement.KingMove]));

View file

@ -5,7 +5,7 @@ import Stack from './stack.js';
class GameState { class GameState {
constructor() { constructor() {
/** @type {Board} */ /** @type {Board} */
this.board = new Board(8, 9); this.board = new Board(8, 8);
/** @type {Stack} */ /** @type {Stack} */
this.stack = new Stack(); this.stack = new Stack();
/** @type {Team} */ /** @type {Team} */

View file

@ -22,6 +22,11 @@
<div class="game-page"> <div class="game-page">
<header><h1>Partie</h1></header> <header><h1>Partie</h1></header>
<div id="meta">Chargement...</div> <div id="meta">Chargement...</div>
<div id="player-color" style="margin-top:6px;font-weight:600">Couleur: —</div>
<div id="players-panel" style="margin-top:8px;font-size:14px">
<strong>Joueurs:</strong>
<ul id="players-list" style="list-style:none;padding-left:0;margin-top:6px"></ul>
</div>
<div id="server-load-indicator" style="margin-top:6px;color:#666">Chargement de l'état du serveur...</div> <div id="server-load-indicator" style="margin-top:6px;color:#666">Chargement de l'état du serveur...</div>
<div id="board-wrapper"><div id="board-container"></div></div> <div id="board-wrapper"><div id="board-container"></div></div>
<div style="margin-top:8px"> <div style="margin-top:8px">
@ -43,22 +48,65 @@
// fetch initial state: prefer explicit backend host then fallback to same-origin // fetch initial state: prefer explicit backend host then fallback to same-origin
(function(){ (function(){
function handleGameObj(g){ 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 // set globals expected by src/game.js so socket handlers attach correctly
try { try {
currentGameId = gid; currentGameId = gid;
if(g && g.players && g.players[0]) myPlayerId = g.players[0].id; // 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){ if(typeof socket === 'undefined' || !socket){
socket = io(); socket = io();
socket.on('connect', () => { socket.emit('join-game', gid); socket.emit('identify', { gameId: gid, playerId: myPlayerId }); }); 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('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('move', (m) => { if(typeof setMeta === 'function') setMeta('remote move ' + JSON.stringify(m)); });
socket.on('player-update', (g) => { if(typeof setMeta === 'function') setMeta('players updated'); }); 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); } } catch(e){ console.warn('socket init failed', e); }
} }

View file

@ -6,7 +6,7 @@ let myPlayerId = null;
let currentGameId = null; let currentGameId = null;
let selected = null; // {x,y} let selected = null; // {x,y}
function renderBoardFromState(state) { function renderBoardFromState(state, asWhite = null) {
const container = q('#board-container'); const container = q('#board-container');
if(!container){ console.error('renderBoardFromState: #board-container not found'); return; } if(!container){ console.error('renderBoardFromState: #board-container not found'); return; }
console.log('renderBoardFromState called, state present?', !!state); console.log('renderBoardFromState called, state present?', !!state);
@ -35,13 +35,37 @@ function renderBoardFromState(state) {
boardEl.style.maxWidth = 'min(90vmin, 80vw)'; boardEl.style.maxWidth = 'min(90vmin, 80vw)';
boardEl.style.width = '100%'; boardEl.style.width = '100%';
for (let r = 0; r < h; r++) { // determine orientation: explicit asWhite param > window.myColor > default true
for (let c = 0; c < w; c++) { 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'); const cell = document.createElement('div');
cell.className = 'cell'; cell.className = 'cell';
// alternate light/dark based on coordinates (classic checkerboard) // alternate light/dark based on visual coordinates so checkerboard looks proper
const isLight = ((r + c) % 2) === 0; const isLight = ((vr + vc) % 2) === 0;
cell.classList.add(isLight ? 'light' : 'dark'); cell.classList.add(isLight ? 'light' : 'dark');
// store state coordinates so click handlers send correct moves
cell.dataset.x = c; cell.dataset.x = c;
cell.dataset.y = r; cell.dataset.y = r;
const piece = document.createElement('img'); const piece = document.createElement('img');
@ -90,17 +114,35 @@ function renderBoardFromState(state) {
} }
} }
container.appendChild(boardEl); 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 + ')'); console.log('renderBoardFromState: board appended (w=' + w + ', h=' + h + ')');
} }
function setMeta(text) { q('#meta').textContent = text; } 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 () => { q('#create-only')?.addEventListener('click', async () => {
const name = q('#game-name').value || 'game-from-ui'; 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 res = await fetch('/api/create', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ name, ownerNickname: 'ui-host' }) });
const j = await res.json(); const j = await res.json();
setMeta('Created: ' + j.game.id); setMeta('Created: ' + j.game.id);
q('#game-id-input').value = 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 () => { q('#create-start')?.addEventListener('click', async () => {
@ -111,7 +153,13 @@ q('#create-start')?.addEventListener('click', async () => {
const id = createJson.game.id; const id = createJson.game.id;
setMeta('Created: ' + id + ' — joining as second player...'); setMeta('Created: ' + id + ' — joining as second player...');
// join 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...'); setMeta('Joined — starting...');
const startResp = await fetch('/api/start', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ id }) }); const startResp = await fetch('/api/start', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ id }) });
const startJson = await startResp.json(); 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 // try to capture the second player id from server game.players
const players = startJson.game.players || []; const players = startJson.game.players || [];
if (players[1]) myPlayerId = players[1].id; else if (players[0]) myPlayerId = players[0].id; 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) { if (!socket) {
socket = io(); socket = io();
socket.on('connect', () => { socket.emit('join-game', currentGameId); socket.emit('identify', { gameId: currentGameId, playerId: myPlayerId }); }); 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; currentGameId = id;
// choose a player id if any exists (by default pick first) // choose a player id if any exists (by default pick first)
if (j.players && j.players.length > 0) myPlayerId = j.players[0].id; 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) { if (!socket) {
socket = io(); socket = io();
socket.on('connect', () => { socket.emit('join-game', currentGameId); socket.emit('identify', { gameId: currentGameId, playerId: myPlayerId }); }); socket.on('connect', () => { socket.emit('join-game', currentGameId); socket.emit('identify', { gameId: currentGameId, playerId: myPlayerId }); });

View file

@ -11,7 +11,28 @@ function showBoard(gameId, role) {
q('#board-section')?.classList?.remove('hidden'); q('#board-section')?.classList?.remove('hidden');
q('#game-id').textContent = gameId; q('#game-id').textContent = gameId;
q('#player-role').textContent = role; q('#player-role').textContent = role;
// 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'); renderBoard(role === 'white');
});
})();
} }
q('#create-btn').addEventListener('click', () => { 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 id = 'game-' + Math.random().toString(36).slice(2,9);
const pass = q('#create-pass').value; const pass = q('#create-pass').value;
const name = q('#create-name').value || id; 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 nick = 'player';
const colorChoice = 'random'; 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 }; 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', () => { btn.addEventListener('click', () => {
console.log('[frontend] join clicked for', g.id); 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; } } 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: 'player' };
const payload = { id: g.id, pass: g.pass, nickname: nick };
// prefer backend host first // prefer backend host first
fetch('http://localhost:4000/api/join', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(payload) }) 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 = ''; container.innerHTML = '';
const board = document.createElement('div'); const board = document.createElement('div');
board.className = 'board'; board.className = 'board';
if (!asWhite) board.classList.add('flipped'); // render without CSS rotation: draw ranks/files in visual order
for (let vr = 0; vr < 8; vr++) {
for (let r = 0; r < 8; r++) {
const row = document.createElement('div'); const row = document.createElement('div');
row.className = 'row'; 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'); const cell = document.createElement('div');
cell.className = 'cell'; cell.className = 'cell';
const piece = document.createElement('img'); const piece = document.createElement('img');

View file

@ -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; } .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.light { background: #f0d9b5; }
.cell.dark { background: #b58863; } .cell.dark { background: #b58863; }
.board.flipped{transform:rotate(180deg)}
.piece{width:70%;height:70%;object-fit:contain} .piece{width:70%;height:70%;object-fit:contain}
.game-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px} .game-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
.controls button{margin-left:8px} .controls button{margin-left:8px}

View file

@ -34,7 +34,7 @@ function renderBoardFromState(state) {
const cell = document.createElement('div'); const cell = document.createElement('div');
cell.className = 'cell'; cell.className = 'cell';
// alternate light/dark based on coordinates (classic checkerboard) // 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.classList.add(isLight ? 'light' : 'dark');
cell.dataset.x = c; cell.dataset.x = c;
cell.dataset.y = r; cell.dataset.y = r;

View file

@ -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; } .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.light { background: #f0d9b5; }
.cell.dark { background: #b58863; } .cell.dark { background: #b58863; }
.board.flipped{transform:rotate(180deg)}
.piece{width:48px;height:48px} .piece{width:48px;height:48px}
.game-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px} .game-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
.controls button{margin-left:8px} .controls button{margin-left:8px}