refactored the code
This commit is contained in:
parent
7688954ab4
commit
6f4537ebd6
5 changed files with 221 additions and 20 deletions
21
Makefile
21
Makefile
|
|
@ -1,6 +1,23 @@
|
|||
all: docker
|
||||
.PHONY: sync-frontend dev-sync
|
||||
|
||||
all: docker sync-frontend dev-sync
|
||||
|
||||
docker:
|
||||
docker-compose -f deploy/docker-compose.dev.yml up --build -d
|
||||
@echo "Frontend is running at http://localhost:3000"
|
||||
@echo "Backend is running at http://localhost:4000"
|
||||
@echo "Backend is running at http://localhost:4000"
|
||||
|
||||
sync-frontend:
|
||||
@echo "Syncing frontend source -> public for dev..."
|
||||
@rsync -av --exclude node_modules --exclude .git --delete frontend/src/ frontend/public/src/
|
||||
|
||||
dev-sync: sync-frontend
|
||||
@echo "Done. You can run 'make dev-sync' whenever you changed frontend/src files."
|
||||
|
||||
.PHONY: dev-watch
|
||||
dev-watch:
|
||||
@echo "Watching frontend/src for changes (requires inotifywait). Press Ctrl-C to stop."
|
||||
@which inotifywait > /dev/null || (echo "Please install inotify-tools (apt install inotify-tools)" && exit 1)
|
||||
while inotifywait -e modify,create,delete -r frontend/src; do \
|
||||
$(MAKE) dev-sync; \
|
||||
done
|
||||
|
|
@ -6,3 +6,20 @@ From repo root run:
|
|||
docker-compose -f docker-compose.dev.yml up --build -d
|
||||
|
||||
Then open http://localhost:3000 for the frontend. Backend API listens on http://localhost:4000
|
||||
|
||||
If you edit files under `frontend/src/` during development, run:
|
||||
|
||||
make dev-sync
|
||||
|
||||
This will copy changed files into `frontend/public/src/` so the static server (http://localhost:3000) serves the latest JS/CSS without rebuilding containers.
|
||||
|
||||
Development workflow note
|
||||
-------------------------
|
||||
|
||||
Two strategies are supported during development to ensure the static server serves the latest frontend sources:
|
||||
|
||||
- Symlink (used in this repo): `frontend/public/src/game.js` is a symbolic link to `../../src/game.js` so there's one source of truth. This avoids accidental drift between `src/` and `public/src/` when editing locally. Symlinks are convenient for local development but may not be appropriate for all deployment or CI environments.
|
||||
|
||||
- Rsync / dev-sync: Run `make dev-sync` to copy files from `frontend/src/` into `frontend/public/src/`. This works in environments where symlinks are undesirable or when preparing files for containers.
|
||||
|
||||
Choose the approach that fits your workflow. This repository currently keeps a regular copy of `game.js` in `frontend/public/src/` (to avoid symlink issues in containers/CI), and `make dev-sync` remains available as the recommended way to update the public copy during development.
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
<div class="game-page">
|
||||
<header><h1>Partie</h1></header>
|
||||
<div id="meta">Chargement...</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">
|
||||
<button id="leave">Quitter</button>
|
||||
|
|
@ -41,25 +42,42 @@
|
|||
const qs = new URLSearchParams(window.location.search);
|
||||
const gid = qs.get('game');
|
||||
const meta = document.getElementById('meta');
|
||||
if(!gid){ meta.textContent = 'Game ID manquant'; return; }
|
||||
meta.textContent = 'Chargement de la partie ' + gid + '...';
|
||||
// fetch initial state
|
||||
fetch('/api/game/' + gid).then(r => r.ok ? r.json() : null).then(g => {
|
||||
if(g && g.state && typeof renderBoardFromState === 'function') renderBoardFromState(g.state);
|
||||
meta.textContent = 'Partie ' + (g && g.id ? g.id : gid);
|
||||
// set globals expected by src/game.js so socket handlers attach correctly
|
||||
try {
|
||||
if(typeof currentGameId !== 'undefined') currentGameId = gid;
|
||||
if(typeof myPlayerId !== 'undefined' && 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'); });
|
||||
if(!gid){ meta.textContent = 'Game ID manquant'; return; }
|
||||
meta.textContent = 'Affichage du plateau...';
|
||||
// render an empty board immediately so user always sees a board even if backend is slow/unavailable
|
||||
try { if(typeof renderBoardFromState === 'function') renderBoardFromState(null); } catch(e){ console.warn('initial empty board render failed', e); }
|
||||
// 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); }
|
||||
}
|
||||
|
||||
fetch('http://localhost:4000/api/game/' + gid).then(r => r.ok ? r.json() : null).then(g => {
|
||||
if(g) { handleGameObj(g); }
|
||||
else {
|
||||
// try same-origin
|
||||
fetch('/api/game/' + gid).then(r => r.ok ? r.json() : null).then(handleGameObj).catch(()=>{ handleGameObj(null); const sli = document.getElementById('server-load-indicator'); if(sli) sli.textContent = 'Serveur injoignable'; });
|
||||
}
|
||||
} catch(e){ console.warn('socket init failed', e); }
|
||||
}).catch(()=>{ meta.textContent = 'Impossible de charger la partie'; });
|
||||
}).catch(()=>{
|
||||
fetch('/api/game/' + gid).then(r => r.ok ? r.json() : null).then(handleGameObj).catch(()=>{ handleGameObj(null); const sli = document.getElementById('server-load-indicator'); if(sli) sli.textContent = 'Serveur injoignable'; });
|
||||
});
|
||||
})();
|
||||
|
||||
document.getElementById('leave').addEventListener('click', ()=>{ window.location.href = 'waiting.html?game=' + encodeURIComponent(gid); });
|
||||
})();
|
||||
|
|
|
|||
146
frontend/public/src/game.js
Normal file
146
frontend/public/src/game.js
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
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');
|
||||
if(!container){ console.error('renderBoardFromState: #board-container not found'); return; }
|
||||
console.log('renderBoardFromState called, state present?', !!state);
|
||||
container.innerHTML = '';
|
||||
const boardEl = document.createElement('div');
|
||||
boardEl.className = 'board';
|
||||
|
||||
// if no state provided, create an empty 8x8 board
|
||||
if(!state || !state.board){
|
||||
const w = 8, h = 8;
|
||||
const board = Array.from({ length: h }, () => Array.from({ length: w }, () => null));
|
||||
state = { board, width: w, height: h };
|
||||
}
|
||||
|
||||
const h = state.height || state.board.length;
|
||||
const w = state.width || (state.board[0] && state.board[0].length) || 8;
|
||||
|
||||
// Use CSS grid so cells distribute evenly and keep the board square via wrapper's aspect-ratio
|
||||
boardEl.style.display = 'grid';
|
||||
boardEl.style.gridTemplateColumns = `repeat(${w}, 1fr)`;
|
||||
boardEl.style.gridTemplateRows = `repeat(${h}, 1fr)`;
|
||||
|
||||
for (let r = 0; r < h; r++) {
|
||||
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);
|
||||
boardEl.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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
container.appendChild(boardEl);
|
||||
console.log('renderBoardFromState: board appended (w=' + w + ', h=' + h + ')');
|
||||
}
|
||||
|
||||
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)); });
|
||||
}
|
||||
});
|
||||
|
|
@ -8,6 +8,8 @@ let selected = null; // {x,y}
|
|||
|
||||
function renderBoardFromState(state) {
|
||||
const container = q('#board-container');
|
||||
if(!container){ console.error('renderBoardFromState: #board-container not found'); return; }
|
||||
console.log('renderBoardFromState called, state present?', !!state);
|
||||
container.innerHTML = '';
|
||||
const boardEl = document.createElement('div');
|
||||
boardEl.className = 'board';
|
||||
|
|
@ -79,6 +81,7 @@ function renderBoardFromState(state) {
|
|||
}
|
||||
}
|
||||
container.appendChild(boardEl);
|
||||
console.log('renderBoardFromState: board appended (w=' + w + ', h=' + h + ')');
|
||||
}
|
||||
|
||||
function setMeta(text) { q('#meta').textContent = text; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue