234 lines
11 KiB
JavaScript
234 lines
11 KiB
JavaScript
// Minimal frontend to create/join a game and render a static board using chess_maestro pieces
|
|
const q = s => document.querySelector(s);
|
|
|
|
function showLobby() {
|
|
q('#lobby').classList.remove('hidden');
|
|
q('#board-section')?.classList?.add('hidden');
|
|
}
|
|
|
|
function showBoard(gameId, role) {
|
|
q('#lobby').classList.add('hidden');
|
|
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', () => {
|
|
console.log('[frontend] create-btn clicked');
|
|
const id = 'game-' + Math.random().toString(36).slice(2,9);
|
|
const pass = q('#create-pass').value;
|
|
const name = q('#create-name').value || 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 };
|
|
|
|
// try backend first (explicit host). If it fails, save locally and redirect to waiting page.
|
|
// include a neutral ownerNickname so backend creates the owner player, but we do not surface any pseudo in the UI
|
|
fetch('http://localhost:4000/api/create', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({name, pass, ownerNickname: nick, ownerColorChoice: colorChoice}) })
|
|
.then(r => { console.log('[frontend] backend /api/create response', r.status); if(!r.ok) throw new Error('no api'); return r.json(); })
|
|
.then(resp => { console.log('[frontend] created (backend)', resp); /* backend now returns { ok: true, game, player } for the owner */
|
|
if(resp.player && resp.player.id){ sessionStorage.setItem(`playerId:${resp.game.id}`, resp.player.id); }
|
|
alert('Partie créée: ' + (resp.game.name || 'Partie')); window.location.href = `waiting.html?game=${resp.game.id}`; })
|
|
.catch((err) => {
|
|
console.warn('[frontend] backend create failed, saving locally and redirecting', err && err.message);
|
|
// save to localStorage so waiting page can pick it up
|
|
const all = JSON.parse(localStorage.getItem('games') || '[]');
|
|
all.push(game);
|
|
localStorage.setItem('games', JSON.stringify(all));
|
|
alert('Partie créée localement: ' + (game.name || 'Partie'));
|
|
window.location.href = `waiting.html?game=${game.id}`;
|
|
});
|
|
});
|
|
|
|
q('#show-list-btn').addEventListener('click', () => {
|
|
const list = q('#games-list');
|
|
list.classList.toggle('hidden');
|
|
renderGameList();
|
|
});
|
|
|
|
function saveGame(game){
|
|
const all = JSON.parse(localStorage.getItem('games') || '[]');
|
|
all.push(game);
|
|
localStorage.setItem('games', JSON.stringify(all));
|
|
}
|
|
|
|
function renderGameList(){
|
|
const ul = q('#games-ul');
|
|
ul.innerHTML = '';
|
|
console.log('[frontend] renderGameList called');
|
|
// prefer backend host in dev
|
|
fetch('http://localhost:4000/api/list')
|
|
.then(r => { if(!r.ok) throw new Error('no remote api'); return r.json(); })
|
|
.then(all => { if(!all || all.length === 0){ ul.innerHTML = '<li>Aucune partie disponible</li>'; return; } all.forEach(g => renderGameListItem(ul, g)); })
|
|
.catch(() => {
|
|
// try same-origin then localStorage
|
|
fetch('/api/list')
|
|
.then(r => { if(!r.ok) throw new Error('no local api'); return r.json(); })
|
|
.then(all => { if(!all || all.length === 0){ ul.innerHTML = '<li>Aucune partie disponible</li>'; return; } all.forEach(g => renderGameListItem(ul, g)); })
|
|
.catch(() => {
|
|
const all = JSON.parse(localStorage.getItem('games') || '[]');
|
|
if(all.length === 0){ ul.innerHTML = '<li>Aucune partie disponible</li>'; return; }
|
|
all.forEach(g => renderGameListItem(ul, g));
|
|
});
|
|
});
|
|
}
|
|
|
|
// Socket: listen for global games-list updates from backend to refresh lobby list live
|
|
if(typeof io !== 'undefined'){
|
|
try{
|
|
const sock = io('http://localhost:4000', { transports: ['websocket', 'polling'] });
|
|
sock.on('connect', () => { console.log('lobby socket connected', sock.id); });
|
|
sock.on('games-list', (list) => { console.log('games-list event received', list); try { renderGameList(); } catch(e){} });
|
|
sock.on('connect_error', (e) => { console.warn('lobby socket connect error', e); });
|
|
}catch(e){ console.warn('failed to init lobby socket', e); }
|
|
}
|
|
|
|
function renderGameListItem(ul, g){
|
|
const li = document.createElement('li');
|
|
// display only human-friendly name; avoid showing internal ids
|
|
li.textContent = g.name || 'Partie';
|
|
|
|
// detect if this client already joined this game (we store playerId in sessionStorage)
|
|
const storedPlayerId = sessionStorage.getItem(`playerId:${g.id}`);
|
|
// If we have a stored player id but the server-side game no longer contains that player,
|
|
// cleanup the sessionStorage entry (it means the player was removed / left).
|
|
let alreadyJoined = false;
|
|
if (storedPlayerId) {
|
|
const playerStillInGame = (g.players || []).some(p => p.id === storedPlayerId);
|
|
if (!playerStillInGame) {
|
|
// stale stored id: remove it
|
|
sessionStorage.removeItem(`playerId:${g.id}`);
|
|
} else {
|
|
alreadyJoined = true;
|
|
}
|
|
}
|
|
|
|
if(alreadyJoined){
|
|
const span = document.createElement('span');
|
|
span.textContent = ' — Vous êtes dans cette partie';
|
|
span.style.marginLeft = '8px';
|
|
li.appendChild(span);
|
|
} else {
|
|
const btn = document.createElement('button');
|
|
btn.textContent = 'Rejoindre';
|
|
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 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) })
|
|
.then(r => { if(!r.ok) throw new Error('join failed'); return r.json(); })
|
|
.then(resp => { if(resp && resp.player && resp.player.id){ sessionStorage.setItem(`playerId:${g.id}`, resp.player.id); } window.location.href = `waiting.html?game=${g.id}`; })
|
|
.catch(() => {
|
|
fetch('/api/join', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(payload) })
|
|
.then(r => { if(!r.ok) throw new Error('join failed'); return r.json(); })
|
|
.then(() => { window.location.href = `waiting.html?game=${g.id}`; })
|
|
.catch(() => { saveGame(g); window.location.href = `waiting.html?game=${g.id}`; });
|
|
});
|
|
|
|
});
|
|
|
|
li.appendChild(btn);
|
|
}
|
|
|
|
ul.appendChild(li);
|
|
}
|
|
|
|
q('#leave-btn')?.addEventListener('click', () => showLobby());
|
|
|
|
q('#flip-btn')?.addEventListener('click', () => { const board = q('#board-container'); board.classList.toggle('flipped'); });
|
|
|
|
function renderBoard(asWhite=true) {
|
|
const container = q('#board-container');
|
|
if(!container) return;
|
|
container.innerHTML = '';
|
|
const board = document.createElement('div');
|
|
board.className = 'board';
|
|
// 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 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');
|
|
piece.className = 'piece';
|
|
if (r === 1) piece.src = 'assets/chess-pieces/chess_maestro_bw/wP.svg';
|
|
else if (r === 6) piece.src = 'assets/chess-pieces/chess_maestro_bw/bP.svg';
|
|
else piece.style.display = 'none';
|
|
cell.appendChild(piece);
|
|
row.appendChild(cell);
|
|
}
|
|
board.appendChild(row);
|
|
}
|
|
|
|
container.appendChild(board);
|
|
}
|
|
|
|
// initial view
|
|
showLobby();
|
|
|
|
// waiting room helpers
|
|
let pollInterval = null;
|
|
function showWaiting(game){
|
|
q('#lobby').classList.add('hidden');
|
|
q('#board-section')?.classList?.add('hidden');
|
|
q('#lobby-waiting').classList.remove('hidden');
|
|
// show the game name instead of internal id
|
|
q('#wait-game-id').textContent = game.name || '';
|
|
renderWaitingPlayers(game.players || []);
|
|
}
|
|
|
|
function renderWaitingPlayers(players){
|
|
const ul = q('#wait-players'); ul.innerHTML = '';
|
|
if(!players || players.length === 0) ul.innerHTML = '<li>Aucun joueur</li>';
|
|
(players||[]).forEach(p => { const li = document.createElement('li'); li.textContent = `${p.nickname} (${p.colorAssigned || p.colorChoice})`; ul.appendChild(li); });
|
|
}
|
|
|
|
function startPolling(gameId){
|
|
if(pollInterval) clearInterval(pollInterval);
|
|
pollInterval = setInterval(() => {
|
|
fetch(`/api/game/${gameId}`).then(r => { if(!r.ok) throw new Error('no api'); return r.json(); }).then(g => {
|
|
renderWaitingPlayers(g.players || []);
|
|
if(g.started){ clearInterval(pollInterval); q('#lobby-waiting').classList.add('hidden'); showBoard(g.id, 'white'); }
|
|
}).catch(() => {
|
|
fetch(`http://localhost:4000/api/game/${gameId}`).then(r => r.json()).then(g => { renderWaitingPlayers(g.players || []); if(g.started){ clearInterval(pollInterval); q('#lobby-waiting').classList.add('hidden'); showBoard(g.id, 'white'); } }).catch(()=>{});
|
|
});
|
|
}, 1000);
|
|
}
|
|
|
|
q('#start-btn')?.addEventListener('click', () => {
|
|
const id = q('#wait-game-id').textContent;
|
|
fetch('/api/start', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ id }) })
|
|
.then(r => { if(!r.ok) throw new Error('start failed'); return r.json(); })
|
|
.then(() => alert('Partie démarrée'))
|
|
.catch(() => { fetch('http://localhost:4000/api/start', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ id }) }).then(()=>alert('Partie démarrée')).catch(()=>alert('Impossible de démarrer la partie')); });
|
|
});
|
|
|
|
q('#back-to-lobby')?.addEventListener('click', () => { if(pollInterval) clearInterval(pollInterval); q('#lobby-waiting').classList.add('hidden'); showLobby(); });
|