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 @@
Chargement...
+
Couleur: —
+
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}