diff --git a/backend/Dockerfile b/backend/Dockerfile index 10b078c..9df819b 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -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 diff --git a/backend/engine-loader.mjs b/backend/engine-loader.mjs new file mode 100644 index 0000000..9ad169a --- /dev/null +++ b/backend/engine-loader.mjs @@ -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(); diff --git a/backend/index.js b/backend/index.js index ca75dc1..61dfa68 100644 --- a/backend/index.js +++ b/backend/index.js @@ -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'; diff --git a/backend/test-import-engine.mjs b/backend/test-import-engine.mjs new file mode 100644 index 0000000..a76c943 --- /dev/null +++ b/backend/test-import-engine.mjs @@ -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); + } +})(); \ No newline at end of file diff --git a/deploy/docker-compose.dev.yml b/deploy/docker-compose.dev.yml index 22ae99e..7ce5f85 100644 --- a/deploy/docker-compose.dev.yml +++ b/deploy/docker-compose.dev.yml @@ -6,6 +6,7 @@ services: - '4000:4000' volumes: - ../backend:/app + - ../engine:/app/engine frontend: image: nginx:stable-alpine diff --git a/engine/core/board.js b/engine/core/board.js index a511755..291ddb9 100644 --- a/engine/core/board.js +++ b/engine/core/board.js @@ -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 -}; \ No newline at end of file +export { Cell, Movement, Piece, PieceColor, PieceType }; +export default Board; \ No newline at end of file diff --git a/engine/core/game_state.js b/engine/core/game_state.js index b8b7d11..4d459c9 100644 --- a/engine/core/game_state.js +++ b/engine/core/game_state.js @@ -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} */ diff --git a/engine/core/piece.js b/engine/core/piece.js index ac55a1f..12c5ae6 100644 --- a/engine/core/piece.js +++ b/engine/core/piece.js @@ -46,4 +46,5 @@ class Piece { } } -export default Piece; \ No newline at end of file +export default Piece; +export { PieceColor, PieceType }; \ No newline at end of file diff --git a/engine/core/team.js b/engine/core/team.js index 81f4475..7f4183b 100644 --- a/engine/core/team.js +++ b/engine/core/team.js @@ -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; } diff --git a/frontend/public/game.html b/frontend/public/game.html index 3790192..7e89c72 100644 --- a/frontend/public/game.html +++ b/frontend/public/game.html @@ -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); }); + } })();