affichage du plateau
This commit is contained in:
parent
18cfe05047
commit
758c4f4675
9 changed files with 167 additions and 38 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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]));
|
||||
|
|
|
|||
|
|
@ -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} */
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@
|
|||
<div class="game-page">
|
||||
<header><h1>Partie</h1></header>
|
||||
<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="board-wrapper"><div id="board-container"></div></div>
|
||||
<div style="margin-top:8px">
|
||||
|
|
@ -43,22 +48,65 @@
|
|||
// 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;
|
||||
// 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) => { 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); }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
// 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 }); });
|
||||
|
|
|
|||
|
|
@ -11,7 +11,28 @@ function showBoard(gameId, role) {
|
|||
q('#board-section')?.classList?.remove('hidden');
|
||||
q('#game-id').textContent = gameId;
|
||||
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');
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
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');
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in a new issue