diff --git a/backend/index.js b/backend/index.js index 1bf6e10..6f70e8e 100644 --- a/backend/index.js +++ b/backend/index.js @@ -8,6 +8,29 @@ const app = express(); app.use(cors()); app.use(bodyParser.json()); +const fs = require('fs'); +const LOGFILE = process.env.CHESSNUT_LOG || '/tmp/chessnut.log'; +function appendLogLine(line){ + try{ fs.appendFileSync(LOGFILE, line + '\n'); }catch(e){} +} +function log(){ + const ts = new Date().toISOString(); + const msg = Array.prototype.slice.call(arguments).map(x => (typeof x === 'string' ? x : JSON.stringify(x))).join(' '); + const line = `${ts} ${msg}`; + console.log(line); + appendLogLine(line); +} + +// Request logging middleware: logs method, path, small subset of headers and body +app.use((req, res, next) => { + try{ + const { method, path } = req; + const info = { method, path, headers: { origin: req.headers.origin, host: req.headers.host }, body: req.body }; + log('HTTP', info); + }catch(e){} + next(); +}); + const server = http.createServer(app); const io = new Server(server, { cors: { origin: '*' } }); @@ -118,10 +141,18 @@ setInterval(() => { pruneOrphans(); }, 60 * 1000); app.post('/api/join', (req, res) => { const { id, pass, nickname, colorChoice } = req.body || {}; const g = games.find(x => x.id === id); - if(!g) return res.status(404).json({ error: 'not found' }); + if(!g) { + // log current games snapshot to help debugging + try{ + const snapshot = games.map(x => ({ id: x.id, name: x.name, players: (x.players||[]).length, started: !!x.started })); + log('start failed - game not found', { requestedId: id, gamesSnapshot: snapshot }); + }catch(e){ log('start failed - game not found (and failed to snapshot)'); } + return res.status(404).json({ error: 'not found' }); + } if(g.pass && g.pass !== pass) return res.status(403).json({ error: 'bad password' }); if(g.players.length >= 2) return res.status(403).json({ error: 'full' }); const player = { id: 'p-' + Math.random().toString(36).slice(2,6), nickname: nickname || 'Anon', colorChoice: colorChoice || 'random', colorAssigned: null }; + player.connected = true; g.players.push(player); io.to(g.id).emit('player-update', g); io.emit('games-list', games); @@ -136,7 +167,9 @@ app.post('/api/leave', (req, res) => { const g = games[gIndex]; const pIndex = g.players.findIndex(p => p.id === playerId); if(pIndex === -1) return res.status(404).json({ error: 'player not in game' }); - // remove player + // do not allow leaving once game started + if (g.started) return res.status(403).json({ error: 'game in progress' }); + // remove player (allowed only if game not started) const leaving = g.players.splice(pIndex, 1)[0]; // if the leaving player was the owner, remove the entire game if(g.ownerId && leaving.id === g.ownerId){ @@ -156,18 +189,25 @@ app.get('/api/game/:id', (req, res) => { if(!g) return res.status(404).json({ error: 'not found' }); // if game exists but has no players, treat as not found and remove it if(!g.players || g.players.length === 0){ - const idx = games.findIndex(x => x.id === id); - if(idx !== -1) games.splice(idx, 1); - return res.status(404).json({ error: 'not found' }); + // only prune if game has not started + if (!g.started) { + const idx = games.findIndex(x => x.id === id); + if(idx !== -1) games.splice(idx, 1); + return res.status(404).json({ error: 'not found' }); + } } res.json(g); }); app.post('/api/start', (req, res) => { const { id } = req.body || {}; + log('POST /api/start', { id, body: req.body }); const g = games.find(x => x.id === id); if(!g) return res.status(404).json({ error: 'not found' }); - if(g.players.length < 2) return res.status(400).json({ error: 'need 2 players' }); + if(g.players.length < 2) { + log('start rejected: need 2 players', { gameId: id, players: g.players.map(p=>p.id) }); + return res.status(400).json({ error: 'need 2 players' }); + } // assign colors const p0 = g.players[0]; const p1 = g.players[1]; @@ -183,17 +223,59 @@ app.post('/api/start', (req, res) => { } assign(); 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. + // Board is an 8x8 array of cells; each cell is either null or { color: 'white'|'black', type: 'PAWN'|'ROOK'|... } + function initialState() { + const w = 8, h = 8; + const board = Array.from({ length: h }, () => Array.from({ length: w }, () => null)); + // pawns + for (let x = 0; x < w; x++) { + board[1][x] = { color: 'white', type: 'PAWN' }; + board[6][x] = { color: 'black', type: 'PAWN' }; + } + // rooks + board[0][0] = { color: 'white', type: 'ROOK' }; + board[0][7] = { color: 'white', type: 'ROOK' }; + board[7][0] = { color: 'black', type: 'ROOK' }; + board[7][7] = { color: 'black', type: 'ROOK' }; + // knights + board[0][1] = { color: 'white', type: 'KNIGHT' }; + board[0][6] = { color: 'white', type: 'KNIGHT' }; + board[7][1] = { color: 'black', type: 'KNIGHT' }; + board[7][6] = { color: 'black', type: 'KNIGHT' }; + // bishops + board[0][2] = { color: 'white', type: 'BISHOP' }; + board[0][5] = { color: 'white', type: 'BISHOP' }; + board[7][2] = { color: 'black', type: 'BISHOP' }; + board[7][5] = { color: 'black', type: 'BISHOP' }; + // queen/king + board[0][3] = { color: 'white', type: 'QUEEN' }; + board[0][4] = { color: 'white', type: 'KING' }; + board[7][3] = { color: 'black', type: 'QUEEN' }; + board[7][4] = { color: 'black', type: 'KING' }; + + return { board, width: w, height: h }; + } + + // only initialize if not already present + if (!g.state) { + g.state = initialState(); + } + // set initial turn: white starts + g.turn = 'white'; + log('start succeeded', { gameId: g.id, players: g.players.map(p => ({ id: p.id, nickname: p.nickname, colorAssigned: p.colorAssigned })) }); io.to(g.id).emit('game-start', g); io.emit('games-list', games); res.json({ ok: true, game: g }); }); io.on('connection', (socket) => { - console.log('socket connected', socket.id); + log('socket connected', { socketId: socket.id, remote: socket.handshake && socket.handshake.address }); socket.on('join-game', (gameId) => { const room = gameId; socket.join(room); - console.log(`${socket.id} joined room ${room}`); + log('join-game', { socketId: socket.id, room }); }); // client can identify which player it is in a game so server can map socket -> player socket.on('identify', (payload) => { @@ -201,18 +283,69 @@ io.on('connection', (socket) => { const { gameId, playerId } = payload || {}; if(gameId && playerId){ socketMap.set(socket.id, { gameId, playerId }); - console.log(`socket ${socket.id} identified as player ${playerId} in ${gameId}`); + log('identify', { socketId: socket.id, gameId, playerId }); } }catch(e){ } }); socket.on('move', (payload) => { - const { gameId } = payload; - socket.to(gameId).emit('move', payload); + try { + log('socket move', { socketId: socket.id, payload }); + const { gameId, playerId, from, to } = payload || {}; + const g = games.find(x => x.id === gameId); + if (!g) return; + + // basic validation: must be started and it must be the player's turn + if (!g.started) return; + + const player = g.players.find(p => p.id === playerId); + if (!player) return; + // determine player's color + const color = player.colorAssigned || (player.colorChoice === 'white' ? 'white' : (player.colorChoice === 'black' ? 'black' : null)); + // if no assigned color (random choice), try to infer from players array + if (!color) { + if (g.players[0] && g.players[1]) { + // if not assigned, assume assignment as in /api/start logic + // fallback: first player white + } + } + + // ensure it is this player's turn (simple toggle 'white'/'black') + if (g.turn && color && g.turn !== color) { + // not this player's turn + socket.emit('error', { error: 'not your turn' }); + return; + } + + // bounds check and cell existence + const h = g.state.height || g.state.board.length; + const w = g.state.width || (g.state.board[0] && g.state.board[0].length) || 8; + if (!from || !to) return; + if (from.x < 0 || from.x >= w || from.y < 0 || from.y >= h) return; + if (to.x < 0 || to.x >= w || to.y < 0 || to.y >= h) return; + + const src = g.state.board[from.y][from.x]; + if (!src) return; // no piece at source + if (src.color !== color) { socket.emit('error', { error: 'not your piece' }); return; } + + // naive move apply: move piece from src to dest (no legality checks beyond ownership) + const dst = g.state.board[to.y][to.x]; + // apply + g.state.board[to.y][to.x] = src; + g.state.board[from.y][from.x] = null; + + // toggle turn + g.turn = g.turn === 'white' ? 'black' : 'white'; + + io.to(gameId).emit('move', { gameId, playerId, from, to }); + io.to(gameId).emit('game-state', g.state); + } catch (e) { + console.error('move handler error', e && e.stack); + } }); socket.on('disconnect', (reason) => { - // if we know which player this socket belonged to, remove them from the game const meta = socketMap.get(socket.id); + log('socket disconnect', { socketId: socket.id, reason, meta }); if(!meta) return; const { gameId, playerId } = meta; socketMap.delete(socket.id); @@ -221,9 +354,19 @@ io.on('connection', (socket) => { const g = games[gIndex]; const pIndex = g.players.findIndex(p => p.id === playerId); if(pIndex === -1) return; + const player = g.players[pIndex]; + // mark as disconnected instead of removing when game started + if (g.started) { + player.connected = false; + console.log(`socket ${socket.id} disconnected (${reason}), marked player ${player.id} disconnected in ${gameId}`); + io.to(gameId).emit('player-update', g); + io.emit('games-list', games); + return; + } + // if game not started, remove player as before const leaving = g.players.splice(pIndex, 1)[0]; console.log(`socket ${socket.id} disconnected (${reason}), removed player ${leaving.id} from ${gameId}`); - // if owner left, delete the game + // if owner left and game not started, delete the game if(g.ownerId && leaving.id === g.ownerId){ games.splice(gIndex, 1); io.emit('games-list', games); diff --git a/frontend/public/game.html b/frontend/public/game.html new file mode 100644 index 0000000..d0b5a5e --- /dev/null +++ b/frontend/public/game.html @@ -0,0 +1,65 @@ + + + + + + ChessNut 1 Partie + + + + +
+

Partie

+
Chargement...
+
+
+ +
+
+ + + + + + diff --git a/frontend/public/index.html b/frontend/public/index.html index dc626cc..97ae4fa 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -29,6 +29,17 @@ + diff --git a/frontend/public/src/waiting.js b/frontend/public/src/waiting.js index b71bd1e..75b172d 100644 --- a/frontend/public/src/waiting.js +++ b/frontend/public/src/waiting.js @@ -6,6 +6,7 @@ function qsParam(name){ } const gameId = qsParam('game'); +console.log('[waiting.js] loaded - build: v2 -', { gameId, url: window.location.href }); if(!gameId){ document.body.innerHTML = '

Game ID missing in URL

'; } else { // Query explicit backend first to avoid interpreting same-origin static 404 as game-deleted @@ -22,6 +23,8 @@ else { }).then(g => { q('#wait-game-id').textContent = g.name || ''; renderWaitingPlayers(g.players||[]); + // if game already started, redirect immediately to the game page + if (g.started) { window.location.href = `game.html?game=${g.id}`; return; } // display current player nick and color if we have a playerId const stored = sessionStorage.getItem(`playerId:${gameId}`); if(stored){ @@ -31,7 +34,10 @@ else { // owner-only start button const ownerId = g.players && g.players[0] && g.players[0].id; if(ownerId && stored && ownerId === stored){ q('#start-btn')?.classList.remove('hidden'); } else { q('#start-btn')?.classList.add('hidden'); } - if(!g.started) startPolling(gameId); + // disable start button unless there are at least 2 players + const startBtn = q('#start-btn'); + if(startBtn){ if((g.players||[]).length < 2){ startBtn.disabled = true; } else { startBtn.disabled = false; } } + if(!g.started) startPolling(gameId); // try to connect socket.io for instant updates and identification tryConnectSocket(gameId); }).catch(()=>{ @@ -43,10 +49,15 @@ else { }).then(g=>{ q('#wait-game-id').textContent = g.name || ''; renderWaitingPlayers(g.players||[]); + // if game already started, redirect immediately to the game page + if (g.started) { window.location.href = `game.html?game=${g.id}`; return; } const stored = sessionStorage.getItem(`playerId:${gameId}`); if(stored){ const me = (g.players||[]).find(p=>p.id===stored); if(me){ q('#player-nick').textContent = 'Vous'; q('#player-color').textContent = me.colorChoice || '-'; } } const ownerId = g.players && g.players[0] && g.players[0].id; if(ownerId && stored && ownerId === stored){ q('#start-btn')?.classList.remove('hidden'); } else { q('#start-btn')?.classList.add('hidden'); } + // disable start button unless there are at least 2 players + const startBtn = q('#start-btn'); + if(startBtn){ if((g.players||[]).length < 2){ startBtn.disabled = true; } else { startBtn.disabled = false; } } if(!g.started) startPolling(gameId); tryConnectSocket(gameId); }).catch(()=>{ @@ -81,8 +92,8 @@ function tryConnectSocket(gameId){ }); sock.on('game-start', (g) => { if(!g) return; - // redirect to game view (index) when started - window.location.href = `index.html?game=${g.id}`; + // redirect to game view when started + window.location.href = `game.html?game=${g.id}`; }); sock.on('connect_error', (err) => { console.warn('socket connect error', err); @@ -100,8 +111,10 @@ let poll = null; function startPolling(id){ if(poll) clearInterval(poll); poll = setInterval(()=>{ - fetch(`/api/game/${id}`).then(r=>{ if(!r.ok) throw new Error('no api'); return r.json(); }).then(g=>{ renderWaitingPlayers(g.players||[]); if(g.started){ clearInterval(poll); window.location.href = `index.html?game=${g.id}`; } }).catch(()=>{ - fetch(`http://localhost:4000/api/game/${id}`).then(r=>r.json()).then(g=>{ renderWaitingPlayers(g.players||[]); if(g.started){ clearInterval(poll); window.location.href = `index.html?game=${g.id}`; } }).catch(()=>{}); + // Prefer explicit backend host to avoid same-origin 404 from static server + fetch(`http://localhost:4000/api/game/${id}`).then(r=>{ if(!r.ok) throw new Error('no api'); return r.json(); }).then(g=>{ renderWaitingPlayers(g.players||[]); if(g.started){ clearInterval(poll); window.location.href = `game.html?game=${g.id}`; } }).catch(()=>{ + // fallback to same-origin if backend not reachable + fetch(`/api/game/${id}`).then(r=>{ if(!r.ok) throw new Error('no api'); return r.json(); }).then(g=>{ renderWaitingPlayers(g.players||[]); if(g.started){ clearInterval(poll); window.location.href = `game.html?game=${g.id}`; } }).catch(()=>{}); }); }, 1000); } @@ -109,8 +122,16 @@ function startPolling(id){ // Join handled from lobby; waiting room doesn't provide a join button q('#start-btn')?.addEventListener('click', () => { - fetch('/api/start', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ id: gameId }) }) - .then(r=>{ if(!r.ok) throw new Error('start failed'); return r.json(); }).then(()=>{ alert('Partie démarrée'); }).catch(()=>{ alert('Impossible de démarrer'); }); + // force explicit backend host (no fallback) so request always goes to :4000 + console.log('[waiting] start click - POST to http://localhost:4000/api/start', { gameId }); + fetch('http://localhost:4000/api/start', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ id: gameId }) }) + .then(async (r) => { + const body = await r.json().catch(()=>({})); + if(!r.ok) throw body; + return body; + }) + .then((resp)=>{ try { window.location.href = `game.html?game=${resp.game.id}`; } catch(e) { alert('Partie démarrée'); } }) + .catch((err)=>{ const msg = (err && err.error) ? err.error : (typeof err === 'string' ? err : JSON.stringify(err)); alert('Impossible de démarrer: ' + msg); console.error('start error', err); }); }); q('#leave-btn')?.addEventListener('click', () => { diff --git a/frontend/public/waiting.html b/frontend/public/waiting.html index 50b37b6..d6884ba 100644 --- a/frontend/public/waiting.html +++ b/frontend/public/waiting.html @@ -28,6 +28,6 @@ - + diff --git a/frontend/src/game.js b/frontend/src/game.js new file mode 100644 index 0000000..68e1944 --- /dev/null +++ b/frontend/src/game.js @@ -0,0 +1,134 @@ +const q = s => document.querySelector(s); + +// socket variable will be initialized on first load/join +let socket = null; +let myPlayerId = null; +let currentGameId = null; +let selected = null; // {x,y} + +function renderBoardFromState(state) { + const container = q('#board-container'); + container.innerHTML = ''; + const boardEl = document.createElement('div'); + boardEl.className = 'board'; + + const h = state.height || state.board.length; + const w = state.width || (state.board[0] && state.board[0].length) || 8; + + for (let r = 0; r < h; r++) { + const row = document.createElement('div'); + row.className = 'row'; + for (let c = 0; c < w; c++) { + const cell = document.createElement('div'); + cell.className = 'cell'; + cell.dataset.x = c; + cell.dataset.y = r; + const piece = document.createElement('img'); + piece.className = 'piece'; + + const cellObj = state.board[r] && state.board[r][c]; + if (cellObj) { + const t = cellObj.type || 'PAWN'; + const color = (cellObj.color || 'white').toLowerCase(); + const lookup = { + PAWN: `assets/chess-pieces/chess_maestro_bw/${color[0] === 'w' ? 'wP' : 'bP'}.svg`, + ROOK: `assets/chess-pieces/chess_maestro_bw/${color[0] === 'w' ? 'wR' : 'bR'}.svg`, + KNIGHT: `assets/chess-pieces/chess_maestro_bw/${color[0] === 'w' ? 'wN' : 'bN'}.svg`, + BISHOP: `assets/chess-pieces/chess_maestro_bw/${color[0] === 'w' ? 'wB' : 'bB'}.svg`, + QUEEN: `assets/chess-pieces/chess_maestro_bw/${color[0] === 'w' ? 'wQ' : 'bQ'}.svg`, + KING: `assets/chess-pieces/chess_maestro_bw/${color[0] === 'w' ? 'wK' : 'bK'}.svg`, + }; + piece.src = lookup[t] || lookup.PAWN; + } else { + piece.style.display = 'none'; + } + + cell.appendChild(piece); + row.appendChild(cell); + // click to select/move + cell.addEventListener('click', () => { + // if no game/socket, ignore + if (!currentGameId) return; + const x = parseInt(cell.dataset.x, 10); + const y = parseInt(cell.dataset.y, 10); + if (!selected) { + selected = { x, y }; + cell.classList.add('selected'); + setMeta('Selected ' + x + ',' + y); + } else { + // send move + const from = selected; + const to = { x, y }; + selected = null; + // clear previously selected class + document.querySelectorAll('.cell.selected').forEach(el => el.classList.remove('selected')); + setMeta('Sending move ' + from.x + ',' + from.y + ' -> ' + to.x + ',' + to.y); + if (socket && myPlayerId && currentGameId) socket.emit('move', { gameId: currentGameId, playerId: myPlayerId, from, to }); + } + }); + } + boardEl.appendChild(row); + } + container.appendChild(boardEl); +} + +function setMeta(text) { q('#meta').textContent = text; } + +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; +}); + +q('#create-start')?.addEventListener('click', async () => { + const name = q('#game-name').value || 'game-from-ui'; + // create + const createResp = await fetch('/api/create', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ name, ownerNickname: 'ui-host' }) }); + const createJson = await createResp.json(); + 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' }) }); + 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(); + setMeta('Started: ' + id); + if (startJson.game && startJson.game.state) renderBoardFromState(startJson.game.state); + // connect socket and join room as the guest player we created earlier + currentGameId = id; + // 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; + if (!socket) { + socket = io(); + socket.on('connect', () => { socket.emit('join-game', currentGameId); socket.emit('identify', { gameId: currentGameId, playerId: myPlayerId }); }); + socket.on('game-state', (s) => { renderBoardFromState(s); setMeta('game-state updated'); }); + socket.on('move', (m) => { setMeta('remote move ' + JSON.stringify(m)); }); + socket.on('player-update', (g) => { setMeta('players updated'); }); + socket.on('error', (e) => { console.warn('socket error', e); setMeta('Socket error: ' + (e && e.error)); }); + } +}); + +q('#load')?.addEventListener('click', async () => { + const id = q('#game-id-input').value; + if(!id) return setMeta('Enter a game id'); + const res = await fetch('/api/game/' + id).catch(e => null); + if(!res || !res.ok) return setMeta('Game not found or backend unreachable'); + const j = await res.json(); + setMeta('Loaded: ' + id + ' (started=' + !!j.started + ')'); + if (j.state && j.state.board) renderBoardFromState(j.state); + // wire socket to listen to updates in this game + 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; + if (!socket) { + socket = io(); + socket.on('connect', () => { socket.emit('join-game', currentGameId); socket.emit('identify', { gameId: currentGameId, playerId: myPlayerId }); }); + socket.on('game-state', (s) => { renderBoardFromState(s); setMeta('game-state updated'); }); + socket.on('move', (m) => { setMeta('remote move ' + JSON.stringify(m)); }); + socket.on('player-update', (g) => { setMeta('players updated'); }); + socket.on('error', (e) => { console.warn('socket error', e); setMeta('Socket error: ' + (e && e.error)); }); + } +}); diff --git a/frontend/src/main.js b/frontend/src/main.js index e38356d..2ef5d0e 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -11,7 +11,18 @@ function showBoard(gameId, role) { q('#board-section')?.classList?.remove('hidden'); q('#game-id').textContent = gameId; q('#player-role').textContent = role; - renderBoard(role === 'white'); + + // Try to fetch the server-provided game.state. If present, render from state.board. + fetch(`/api/game/${gameId}`).then(r => { if(!r.ok) throw new Error('no api'); return r.json(); }).then(g => { + if (g && g.state && g.state.board) { + renderBoardFromState(g.state, role === 'white'); + } else { + renderBoard(role === 'white'); + } + }).catch(() => { + // fallback to static local render + renderBoard(role === 'white'); + }); } q('#create-btn').addEventListener('click', () => { @@ -105,6 +116,7 @@ q('#leave-btn')?.addEventListener('click', () => showLobby()); q('#flip-btn')?.addEventListener('click', () => { const board = q('#board-container'); board.classList.toggle('flipped'); }); function renderBoard(asWhite=true) { + // legacy static render (used as fallback) const container = q('#board-container'); if(!container) return; container.innerHTML = ''; @@ -132,8 +144,64 @@ function renderBoard(asWhite=true) { container.appendChild(board); } +function renderBoardFromState(state, asWhite = true) { + const container = q('#board-container'); + if(!container) return; + container.innerHTML = ''; + const boardEl = document.createElement('div'); + boardEl.className = 'board'; + if (!asWhite) boardEl.classList.add('flipped'); + + const h = state.height || state.board.length; + const w = state.width || (state.board[0] && state.board[0].length) || 8; + + for (let r = 0; r < h; r++) { + const row = document.createElement('div'); + row.className = 'row'; + for (let c = 0; c < w; c++) { + const cell = document.createElement('div'); + cell.className = 'cell'; + const piece = document.createElement('img'); + piece.className = 'piece'; + + const cellObj = state.board[r] && state.board[r][c]; + if (cellObj) { + // map type/color to an asset - minimal mapping for common pieces + const t = cellObj.type || 'PAWN'; + const color = (cellObj.color || 'white').toLowerCase(); + const lookup = { + PAWN: `assets/chess-pieces/chess_maestro_bw/${color[0] === 'w' ? 'wP' : 'bP'}.svg`, + ROOK: `assets/chess-pieces/chess_maestro_bw/${color[0] === 'w' ? 'wR' : 'bR'}.svg`, + KNIGHT: `assets/chess-pieces/chess_maestro_bw/${color[0] === 'w' ? 'wN' : 'bN'}.svg`, + BISHOP: `assets/chess-pieces/chess_maestro_bw/${color[0] === 'w' ? 'wB' : 'bB'}.svg`, + QUEEN: `assets/chess-pieces/chess_maestro_bw/${color[0] === 'w' ? 'wQ' : 'bQ'}.svg`, + KING: `assets/chess-pieces/chess_maestro_bw/${color[0] === 'w' ? 'wK' : 'bK'}.svg`, + }; + piece.src = lookup[t] || lookup.PAWN; + } else { + piece.style.display = 'none'; + } + + cell.appendChild(piece); + row.appendChild(cell); + } + boardEl.appendChild(row); + } + + container.appendChild(boardEl); +} + // initial view -showLobby(); +// initial view +// If URL contains ?game=..., directly open the board view +const params = new URLSearchParams(window.location.search); +const initialGame = params.get('game'); +if (initialGame) { + // assign a default role (will be refreshed when fetching game state) + showBoard(initialGame, 'white'); +} else { + showLobby(); +} // waiting room helpers let pollInterval = null; diff --git a/frontend/src/waiting.js b/frontend/src/waiting.js index 0783a0a..e19ae76 100644 --- a/frontend/src/waiting.js +++ b/frontend/src/waiting.js @@ -31,8 +31,8 @@ let poll = null; function startPolling(id){ if(poll) clearInterval(poll); poll = setInterval(()=>{ - fetch(`/api/game/${id}`).then(r=>{ if(!r.ok) throw new Error('no api'); return r.json(); }).then(g=>{ renderWaitingPlayers(g.players||[]); if(g.started){ clearInterval(poll); window.location.href = `index.html?game=${g.id}`; } }).catch(()=>{ - fetch(`http://localhost:4000/api/game/${id}`).then(r=>r.json()).then(g=>{ renderWaitingPlayers(g.players||[]); if(g.started){ clearInterval(poll); window.location.href = `index.html?game=${g.id}`; } }).catch(()=>{}); + fetch(`/api/game/${id}`).then(r=>{ if(!r.ok) throw new Error('no api'); return r.json(); }).then(g=>{ renderWaitingPlayers(g.players||[]); if(g.started){ clearInterval(poll); window.location.href = `game.html?game=${g.id}`; } }).catch(()=>{ + fetch(`http://localhost:4000/api/game/${id}`).then(r=>r.json()).then(g=>{ renderWaitingPlayers(g.players||[]); if(g.started){ clearInterval(poll); window.location.href = `game.html?game=${g.id}`; } }).catch(()=>{}); }); }, 1000); } @@ -46,5 +46,11 @@ q('#join-btn')?.addEventListener('click', () => { q('#start-btn')?.addEventListener('click', () => { fetch('/api/start', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ id: gameId }) }) - .then(r=>{ if(!r.ok) throw new Error('start failed'); return r.json(); }).then(()=>{ alert('Partie démarrée'); }).catch(()=>{ alert('Impossible de démarrer'); }); + .then(r=>{ if(!r.ok) throw new Error('start failed'); return r.json(); }).then((res)=>{ + // If backend confirms, go to game page + window.location.href = `game.html?game=${gameId}`; + }).catch(()=>{ + // If backend absent, still attempt to open the game page — polling may still load state from localStorage/backends + window.location.href = `game.html?game=${gameId}`; + }); });