diff --git a/public/game.html b/public/game.html index 278dafc..edb1de9 100644 --- a/public/game.html +++ b/public/game.html @@ -12,6 +12,19 @@
+ @@ -19,16 +32,29 @@ function qs(name){ const url = new URL(window.location.href); return url.searchParams.get(name); } const roomId = qs('roomId'); const playerId = qs('playerId') || undefined; - document.getElementById('roomId').textContent = roomId || '-'; - document.getElementById('playerId').textContent = playerId || '-'; + const roomIdEl = document.getElementById('roomId'); + const playerIdEl = document.getElementById('playerId'); + if(roomIdEl) roomIdEl.textContent = roomId || '-'; + if(playerIdEl) playerIdEl.textContent = playerId || '-'; + // optional UI elements: support removing the sidebar/sections without breaking JS const logEl = document.getElementById('log'); const fenEl = document.getElementById('fen'); const playersEl = document.getElementById('players'); const boardEl = document.getElementById('board'); let socket = null; - function log(...args){ logEl.textContent += '\n' + args.map(a=>typeof a==='object'?JSON.stringify(a):a).join(' '); logEl.scrollTop = logEl.scrollHeight; } + // safe logger: prefer on-page log if present, otherwise console + function log(...args){ + if(logEl){ + try{ + logEl.textContent += '\n' + args.map(a=>typeof a==='object'?JSON.stringify(a):a).join(' '); + logEl.scrollTop = logEl.scrollHeight; + }catch(e){ console.log(...args); } + } else { + console.log(...args); + } + } if(!roomId){ alert('roomId manquant dans l\'URL'); } @@ -38,19 +64,29 @@ socket.on('room:update', (data)=>{ log('room:update', data); - fenEl.textContent = data.fen || '-'; - playersEl.innerHTML = ''; - (data.players||[]).forEach(p=>{ - const li = document.createElement('li'); - li.textContent = p.id + ' (' + p.color + ')'; - playersEl.appendChild(li); - }); + if(fenEl) fenEl.textContent = data.fen || '-'; + if(playersEl){ + playersEl.innerHTML = ''; + (data.players||[]).forEach(p=>{ + const li = document.createElement('li'); + li.textContent = p.id + ' (' + p.color + ')'; + playersEl.appendChild(li); + }); + } + if(data.size){ + // ensure grid layout matches requested size and build squares + buildGrid(data.size); + // adjust board width/height responsively (keep squares roughly proportional) + const side = Math.min(800, Math.max(320, data.size * 80)); + boardEl.style.width = side + 'px'; + boardEl.style.height = side + 'px'; + } if(data.fen) renderFen(data.fen); }); - socket.on('move:moved', (data)=>{ log('move:moved', data); fenEl.textContent = data.fen; renderFen(data.fen); }); - socket.on('game:over', (data)=>{ log('game:over', data); fenEl.textContent = data.fen; renderFen(data.fen); }); - socket.on('game:started', (data)=>{ log('game:started', data); fenEl.textContent = data.fen || '-'; renderFen(data.fen || ''); }); + socket.on('move:moved', (data)=>{ log('move:moved', data); if(fenEl) fenEl.textContent = data.fen; renderFen(data.fen); }); + socket.on('game:over', (data)=>{ log('game:over', data); if(fenEl) fenEl.textContent = data.fen; renderFen(data.fen); }); + socket.on('game:started', (data)=>{ log('game:started', data); if(fenEl) fenEl.textContent = data.fen || '-'; renderFen(data.fen || ''); }); socket.emit('room:join', { roomId, playerId }, (resp)=>{ if(resp && resp.error){ log('join error', resp); alert(resp.error); return; } @@ -60,56 +96,104 @@ connectAndJoin(); - document.getElementById('move').addEventListener('click', ()=>{ - if(!socket){ alert('non connecté'); return; } - const from = document.getElementById('from').value.trim(); - const to = document.getElementById('to').value.trim(); - const promotion = document.getElementById('promo').value.trim() || undefined; - socket.emit('game:move', { roomId, from, to, promotion }, (resp)=>{ - if(resp && resp.error) { log('move error', resp); alert(resp.error); return; } - log('move accepted', resp); + // allow clicking on the board to request legal moves for a square + if(boardEl){ + boardEl.addEventListener('click', (ev)=>{ + const sqEl = ev.target.closest && ev.target.closest('.square'); + if(!sqEl) return; + const square = sqEl.getAttribute('data-square'); + if(!square) return; + if(socket){ + socket.emit('game:legalMoves', { roomId, square }, (resp)=>{ + if(resp && resp.error){ log('legalMoves error', resp); return; } + log('legalMoves for', square, resp.moves || resp); + }); + } }); - }); + } + + const moveBtn = document.getElementById('move'); + if(moveBtn){ + moveBtn.addEventListener('click', ()=>{ + if(!socket){ alert('non connecté'); return; } + const fromEl = document.getElementById('from'); + const toEl = document.getElementById('to'); + const promoEl = document.getElementById('promo'); + const from = fromEl ? fromEl.value.trim() : ''; + const to = toEl ? toEl.value.trim() : ''; + const promotion = promoEl ? promoEl.value.trim() || undefined : undefined; + socket.emit('game:move', { roomId, from, to, promotion }, (resp)=>{ + if(resp && resp.error) { log('move error', resp); alert(resp.error); return; } + log('move accepted', resp); + }); + }); + } // Board rendering using provided SVG assets. Accepts FEN. - const pieceSetPath = '/assets/chess-pieces/chess_1Kbyte_gambit'; + const pieceSetPath = '/assets/chess-pieces/chess_maestro_bw'; // board visual defaults are handled by CSS (background, size, border, position) function renderFen(fen){ if(!fen) return; + // only handle standard FEN for 8x8; for other sizes server should send a 'board' structure const parts = fen.split(' '); const rows = parts[0].split('/'); - boardEl.innerHTML = ''; - for(let r=0;r<8;r++){ + // clear existing pieces but keep squares if present + const squares = boardEl.querySelectorAll('.square'); + squares.forEach(s=> s.innerHTML = ''); + + // place pieces according to FEN (assume 8x8) + for(let r=0;r= 1; rank--){ + for(let file = 0; file < size; file++){ + const fileLetter = String.fromCharCode('a'.charCodeAt(0) + file); + const squareName = `${fileLetter}${rank}`; + const sq = document.createElement('div'); + // color squares in checker pattern + const dark = ((file + rank) % 2) === 0; + sq.className = 'square ' + (dark ? 'dark' : 'light'); + sq.setAttribute('data-square', squareName); + boardEl.appendChild(sq); + } + } + } diff --git a/public/styles/style.css b/public/styles/style.css index b1beb23..04760f7 100644 --- a/public/styles/style.css +++ b/public/styles/style.css @@ -47,6 +47,11 @@ a:hover{text-decoration:underline} .square{width:100%;height:100%;display:flex;align-items:center;justify-content:center} .square img, .piece{max-width:78%;max-height:78%;user-select:none} +/* basic checkerboard coloring */ +.square.light{background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));} +.square.dark{background: linear-gradient(180deg, rgba(0,0,0,0.18), rgba(0,0,0,0.12));} +.square{border:1px solid rgba(0,0,0,0.06)} + /* Lobby / hero styles - decorative and fun */ .hero{ display:flex;align-items:center;justify-content:space-between;padding:28px;border-radius:12px;margin-bottom:18px;overflow:hidden;position:relative;border:1px solid rgba(255,255,255,0.04); diff --git a/public/styles/style.scss b/public/styles/style.scss index 089ecfd..82f484e 100644 --- a/public/styles/style.scss +++ b/public/styles/style.scss @@ -59,6 +59,11 @@ a:hover{text-decoration:underline} .square{width:100%;height:100%;display:flex;align-items:center;justify-content:center} .square img, .piece{max-width:78%;max-height:78%;user-select:none} +/* basic checkerboard coloring */ +.square.light{background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));} +.square.dark{background: linear-gradient(180deg, rgba(0,0,0,0.18), rgba(0,0,0,0.12));} +.square{border:1px solid rgba(0,0,0,0.06)} + /* Lobby / hero styles - decorative and fun */ .hero{ display:flex;align-items:center;justify-content:space-between;padding:28px;border-radius:12px;margin-bottom:18px;overflow:hidden;position:relative;border:1px solid rgba(255,255,255,0.04); diff --git a/server.js b/server.js index 757cc54..7ca35eb 100644 --- a/server.js +++ b/server.js @@ -20,16 +20,20 @@ const rooms = new Map(); app.post('/rooms', (req, res) => { const id = uuidv4().slice(0, 8); - const chess = new Chess(); + // allow optional board size in request body (default 8) + const size = (req.body && parseInt(req.body.size, 10)) || 8; + // For standard 8x8 chess we use chess.js. For other sizes we keep a placeholder state + const chess = size === 8 ? new Chess() : null; // hostId will be set when the first player joins - rooms.set(id, { id, chess, players: [], status: 'waiting', hostId: null }); - res.json({ roomId: id }); + rooms.set(id, { id, chess, players: [], status: 'waiting', hostId: null, size, cards: {}, playedCards: [] }); + res.json({ roomId: id, size }); }); app.get('/rooms/:id', (req, res) => { const room = rooms.get(req.params.id); if (!room) return res.status(404).json({ error: 'room not found' }); - res.json({ id: room.id, fen: room.chess.fen(), players: room.players.map(p => ({ id: p.id, color: p.color })), status: room.status, hostId: room.hostId }); + const fen = room.chess ? room.chess.fen() : null; + res.json({ id: room.id, fen, size: room.size, players: room.players.map(p => ({ id: p.id, color: p.color })), status: room.status, hostId: room.hostId, cards: Object.keys(room.cards || {}) }); }); io.on('connection', (socket) => { @@ -52,10 +56,12 @@ io.on('connection', (socket) => { io.to(roomId).emit('room:update', { - fen: room.chess.fen(), + fen: room.chess ? room.chess.fen() : null, players: room.players.map(p => ({ id: p.id, color: p.color })), status: room.status, - hostId: room.hostId + hostId: room.hostId, + size: room.size, + cards: Object.keys(room.cards || {}) }); cb && cb({ ok: true, color, roomId, playerId: assignedId, hostId: room.hostId }); @@ -67,6 +73,9 @@ io.on('connection', (socket) => { const chess = room.chess; + // If we have a chess.js instance (standard 8x8) use it for validation + if (!chess) return cb && cb({ error: 'custom board sizes not yet supported for moves' }); + // Validate move const move = chess.move({ from, to, promotion }); if (!move) return cb && cb({ error: 'invalid move' }); @@ -99,10 +108,12 @@ io.on('connection', (socket) => { room.status = 'playing'; io.to(roomId).emit('game:started', { roomId, fen: room.chess.fen() }); io.to(roomId).emit('room:update', { - fen: room.chess.fen(), + fen: room.chess ? room.chess.fen() : null, players: room.players.map(p => ({ id: p.id, color: p.color })), status: room.status, - hostId: room.hostId + hostId: room.hostId, + size: room.size, + cards: Object.keys(room.cards || {}) }); cb && cb({ ok: true }); @@ -123,12 +134,50 @@ io.on('connection', (socket) => { } io.to(roomId).emit('room:update', { - fen: room.chess.fen(), + fen: room.chess ? room.chess.fen() : null, players: room.players.map(p => ({ id: p.id, color: p.color })), status: room.status, - hostId: room.hostId + hostId: room.hostId, + size: room.size, + cards: Object.keys(room.cards || {}) }); }); + + // Provide legal moves for a given square (for standard 8x8 via chess.js) + socket.on('game:legalMoves', ({ roomId, square }, cb) => { + const room = rooms.get(roomId); + if (!room) return cb && cb({ error: 'room not found' }); + if (!room.chess) return cb && cb({ error: 'legal moves not supported for custom board sizes yet', moves: [] }); + try{ + const moves = room.chess.moves({ square, verbose: true }) || []; + return cb && cb({ ok: true, moves }); + }catch(e){ + return cb && cb({ error: 'invalid square', moves: [] }); + } + }); + + // Simple cards API via sockets: list/play + socket.on('card:list', ({ roomId }, cb) => { + const room = rooms.get(roomId); + if(!room) return cb && cb({ error: 'room not found' }); + // return available card types (placeholder) + const available = [ + { id: 'invert', name: 'Invert Turn', description: 'Swap movement directions for one move' }, + { id: 'teleport', name: 'Teleport', description: 'Move one piece to any empty square' } + ]; + cb && cb({ ok: true, cards: available }); + }); + + socket.on('card:play', ({ roomId, playerId, cardId, payload }, cb) => { + const room = rooms.get(roomId); + if(!room) return cb && cb({ error: 'room not found' }); + // store played card (placeholder behaviour) and broadcast + const played = { id: uuidv4().slice(0,8), playerId, cardId, payload, ts: Date.now() }; + room.playedCards = room.playedCards || []; + room.playedCards.push(played); + io.to(roomId).emit('card:played', played); + cb && cb({ ok: true, played }); + }); }); const PORT = process.env.PORT || 3000;