render a real game

This commit is contained in:
Didictateur 2025-10-18 16:21:37 +02:00
parent 15208d0bab
commit 6853fcc4ec
10 changed files with 176 additions and 19 deletions

View file

@ -1,4 +1,4 @@
FROM node:18-alpine
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --only=production || npm install --only=production

29
backend/engine-loader.mjs Normal file
View file

@ -0,0 +1,29 @@
// Small ESM helper run by backend to instantiate engine GameState and print a minimal serialized view.
import fs from 'fs';
import path from 'path';
async function main(){
try{
const enginePath = 'file:///app/engine/core/game_state.js';
const mod = await import(enginePath);
const GameState = mod && (mod.default || mod.GameState);
if(typeof GameState !== 'function'){
console.error(JSON.stringify({ ok:false, error: 'GameState not found in module', keys: Object.keys(mod||{}) }));
process.exit(2);
}
const gs = new GameState();
// attempt a minimal serialization similar to server's serializeEngineState
let boardObj = (typeof gs.getBoard === 'function') ? gs.getBoard() : gs.board;
const w = (typeof boardObj.getWidth === 'function') ? boardObj.getWidth() : (boardObj.width || 8);
const h = (typeof boardObj.getHeight === 'function') ? boardObj.getHeight() : (boardObj.height || 8);
const board = Array.from({ length: h }, () => Array.from({ length: w }, () => null));
// best-effort: leave board nulls—engine may have complex pieces
const out = { ok:true, width: w, height: h };
console.log(JSON.stringify(out));
}catch(e){
console.error(JSON.stringify({ ok:false, error: (e && e.stack) || String(e) }));
process.exit(1);
}
}
main();

View file

@ -9,6 +9,7 @@ app.use(cors());
app.use(bodyParser.json());
const fs = require('fs');
const path = require('path');
const LOGFILE = process.env.CHESSNUT_LOG || '/tmp/chessnut.log';
function appendLogLine(line){
try{ fs.appendFileSync(LOGFILE, line + '\n'); }catch(e){}
@ -45,6 +46,64 @@ const pendingDisconnects = new Map();
function pdKey(gameId, playerId){ return `${gameId}:${playerId}`; }
// Diagnostic helper: attempt to dynamically import the engine GameState module
async function tryImportEngine(){
const candidates = [];
// common possibilities inside container /app
candidates.push(path.join(__dirname, 'engine', 'core', 'game_state.js'));
candidates.push(path.resolve(__dirname, '../engine/core/game_state.js'));
candidates.push('/app/engine/core/game_state.js');
candidates.push('/engine/core/game_state.js');
const attemptErrors = {};
for (const candidate of candidates) {
try {
const urlStr = 'file://' + candidate;
const mod = await import(urlStr);
log('tryImportEngine: import OK', { tried: candidate, keys: Object.keys(mod) });
return { ok: true, mod, tried: candidate };
} catch (e) {
attemptErrors[candidate] = (e && e.stack) || String(e);
}
}
// if we get here, all attempts failed — gather snippets from likely engine dir(s)
const files = ['game_state.js','board.js','piece.js','cell.js'];
const snippets = {};
const probeDirs = [path.join(__dirname, 'engine', 'core'), path.resolve(__dirname, '../engine/core'), '/app/engine/core', '/engine/core'];
for (const d of probeDirs) {
for (const f of files) {
const p = path.join(d, f);
try { const txt = fs.readFileSync(p, 'utf8'); snippets[p] = txt.slice(0, 2000); } catch(err) { snippets[p] = `read-failed: ${err && err.message}`; }
}
}
log('tryImportEngine: all attempts failed', { attempts: Object.keys(attemptErrors).length, attemptErrors: Object.keys(attemptErrors).reduce((acc,k)=>{acc[k]=attemptErrors[k].split('\n')[0];return acc;},{}) });
try{ fs.writeFileSync('/tmp/chessnut-engine-import-error.log', JSON.stringify({ time: new Date().toISOString(), attemptErrors, snippets }, null, 2)); }catch(err){}
// try fallback by running engine-loader.mjs (ESM) via node
try{
const loaderRes = await runEngineLoader();
log('tryImportEngine: runEngineLoader result', { loaderRes });
if(loaderRes && loaderRes.ok) return { ok: true, loader: loaderRes };
}catch(e){ log('tryImportEngine: runEngineLoader failed', { err: e && e.stack || e }); }
return { ok: false, error: attemptErrors, snippets };
}
// Fallback: try to run engine-loader.mjs via node (spawn) to let node use ESM loader
const { spawnSync } = require('child_process');
async function runEngineLoader(){
try{
const loaderPath = path.join(__dirname, 'engine-loader.mjs');
const res = spawnSync('node', [loaderPath], { encoding: 'utf8', maxBuffer: 200000 });
if(res.error){ return { ok:false, error: res.error.message }; }
if(res.status !== 0){
return { ok:false, stdout: res.stdout, stderr: res.stderr, status: res.status };
}
try{ const j = JSON.parse(res.stdout || '{}'); return { ok:true, result: j, raw: res.stdout }; }catch(e){ return { ok:true, raw: res.stdout }; }
}catch(e){ return { ok:false, error: e && e.stack }; }
}
app.post('/api/create', (req, res) => {
console.log('[backend] POST /api/create received', req.body && { name: req.body.name, owner: req.body.ownerNickname });
const id = 'game-' + Math.random().toString(36).slice(2,9);
@ -206,7 +265,7 @@ app.get('/api/game/:id', (req, res) => {
res.json(g);
});
app.post('/api/start', (req, res) => {
app.post('/api/start', async (req, res) => {
const { id } = req.body || {};
log('POST /api/start', { id, body: req.body });
const g = games.find(x => x.id === id);
@ -267,7 +326,45 @@ app.post('/api/start', (req, res) => {
// only initialize if not already present
if (!g.state) {
g.state = initialState();
// attempt to import engine GameState for richer state; on failure we log detailed diagnostics
const importResult = await tryImportEngine();
if (importResult.ok) {
try {
const GameState = importResult.mod && (importResult.mod.default || importResult.mod.GameState);
if (typeof GameState === 'function') {
const gs = new GameState();
g._engineState = gs;
// best-effort serialize
try{
const serialized = (typeof gs.getBoard === 'function') ? (function(){
// serialize Board -> plain structure { board: [rows], width, height }
const boardObj = gs.getBoard();
const w = (typeof boardObj.getWidth === 'function') ? boardObj.getWidth() : (boardObj.width || 8);
const h = (typeof boardObj.getHeight === 'function') ? boardObj.getHeight() : (boardObj.height || 8);
const board = Array.from({ length: h }, (v, y) => Array.from({ length: w }, (v2, x) => {
try {
const cell = (typeof boardObj.getCell === 'function') ? boardObj.getCell(x, y) : (boardObj.grid && boardObj.grid[y] && boardObj.grid[y][x]);
if (!cell || !cell.piece) return null;
const p = cell.piece;
const color = (typeof p.getColor === 'function' ? p.getColor() : p.color) || p.color;
const type = (typeof p.getType === 'function' ? p.getType() : p.type) || p.type;
return { color: String(color).toLowerCase(), type: String(type).toUpperCase() };
} catch (e) { return null; }
}));
return { board, width: w, height: h };
})() : null;
g.state = serialized || initialState();
}catch(e){ log('engine serialization failed, falling back', { err: e && e.stack }); g.state = initialState(); }
} else {
log('imported engine module does not expose GameState constructor, falling back to plain state');
g.state = initialState();
}
} catch(e){ log('failed to instantiate GameState, falling back', { err: e && e.stack }); g.state = initialState(); }
} else {
// import failed: return 500 with short message, detailed info written to logs/file by tryImportEngine
log('api/start aborting due to engine import failure', { gameId: g.id });
return res.status(500).json({ error: 'engine import failed - see server logs' });
}
}
// set initial turn: white starts
g.turn = 'white';

View file

@ -0,0 +1,33 @@
import fs from 'fs';
import path from 'path';
import url from 'url';
(async function(){
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
const p = path.resolve(__dirname, '../engine/core/game_state.js');
console.log('Trying to load:', p);
try{
const content = fs.readFileSync(p, 'utf8');
console.log('File exists, first 200 chars:\n', content.slice(0,200));
}catch(e){ console.error('Cannot read file', e && e.message); }
try{
console.log('\nAttempting dynamic import() with file:// path...');
const mod = await import('file://' + p);
console.log('Imported module keys:', Object.keys(mod));
console.log('Default export type:', typeof mod.default);
}catch(e){
console.error('import(file://) failed:');
console.error(e && e.stack || e);
}
try{
console.log('\nAttempting import with relative path from backend:');
const mod2 = await import('../engine/core/game_state.js');
console.log('Imported module keys (rel):', Object.keys(mod2));
console.log('Default export type (rel):', typeof mod2.default);
}catch(e){
console.error('relative import failed:');
console.error(e && e.stack || e);
}
})();

View file

@ -6,6 +6,7 @@ services:
- '4000:4000'
volumes:
- ../backend:/app
- ../engine:/app/engine
frontend:
image: nginx:stable-alpine

View file

@ -79,18 +79,12 @@ class Board {
this.setPiece(5, 7, new Piece(PieceColor.BLACK, PieceType.BISHOP, [Movement.BishopMove]));
// Queens and Kings
this.setPiece(3, 0, new Piece(PieceColor.WHITE, PieceType.QUEEN, [Movement.QueenMove]));
this.setPiece(3, 4, 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]));
}
}
export default {
Cell,
Movement,
Piece,
PieceColor,
PieceType,
Board
};
export { Cell, Movement, Piece, PieceColor, PieceType };
export default Board;

View file

@ -5,7 +5,7 @@ import Stack from './stack.js';
class GameState {
constructor() {
/** @type {Board} */
this.board = new Board();
this.board = new Board(8, 9);
/** @type {Stack} */
this.stack = new Stack();
/** @type {Team} */

View file

@ -46,4 +46,5 @@ class Piece {
}
}
export default Piece;
export default Piece;
export { PieceColor, PieceType };

View file

@ -1,5 +1,4 @@
import Piece from './piece.js';
import PieceColor from './piece.js';
import Piece, { PieceColor } from './piece.js';
import Hand from './hand.js';
class Team {
@ -12,8 +11,8 @@ class Team {
this.color = color;
/** @type {Piece} */
this.king = king;
/** @type {Hand} */
this.hand = new this.hand();
/** @type {Hand} */
this.hand = new Hand();
/** @type {boolean} */
this.hasMadeAction = false;
}

View file

@ -73,7 +73,10 @@
});
})();
document.getElementById('leave').addEventListener('click', ()=>{ window.location.href = 'waiting.html?game=' + encodeURIComponent(gid); });
const leaveBtn = document.getElementById('leave');
if (leaveBtn) {
leaveBtn.addEventListener('click', ()=>{ window.location.href = 'waiting.html?game=' + encodeURIComponent(gid); });
}
})();
</script>
</body>