var http = require('http'); var fs = require('fs'); var path = require('path'); // model constants var NUM_VALUES = 15; var TABLE_SIZE = Math.pow(NUM_VALUES, 6); var NUM_PATTERNS = 8; var PATTERNS = [ [[0, 0], [1, 0], [2, 0], [3, 0], [0, 1], [1, 1]], [[0, 0], [1, 0], [0, 1], [1, 1], [0, 2], [1, 2]], [[0, 0], [1, 0], [2, 0], [0, 1], [1, 1], [2, 1]], [[0, 0], [1, 0], [0, 1], [1, 1], [2, 1], [3, 1]], [[0, 0], [1, 0], [2, 0], [3, 0], [2, 1], [3, 1]], [[0, 0], [1, 0], [2, 0], [1, 1], [2, 1], [3, 1]], [[0, 0], [1, 0], [0, 1], [1, 1], [2, 1], [2, 2]], [[0, 0], [1, 0], [2, 0], [0, 1], [1, 1], [1, 2]], ]; var SYMMETRY_FNS = [ function (x, y) { return [x, y] }, function (x, y) { return [3 - x, y] }, function (x, y) { return [x, 3 - y] }, function (x, y) { return [3 - x, 3 - y] }, function (x, y) { return [y, x] }, function (x, y) { return [3 - y, x] }, function (x, y) { return [y, 3 - x] }, function (x, y) { return [3 - y, 3 - x] }, ]; // precompute tuple lookups var NUM_TUPLES = NUM_PATTERNS * 8; var tupleCoords = new Int32Array(NUM_TUPLES * 6); var tupleTable = new Int32Array(NUM_TUPLES); var ti = 0; for (var p = 0; p < NUM_PATTERNS; p++) { for (var s = 0; s < 8; s++) { tupleTable[ti] = p; for (var i = 0; i < 6; i++) { var c = SYMMETRY_FNS[s](PATTERNS[p][i][0], PATTERNS[p][i][1]); tupleCoords[ti * 6 + i] = c[0] * 4 + c[1]; } ti++; } } // tile index mapping var TILE_TO_IDX = new Uint8Array(65536); TILE_TO_IDX[0] = 0; for (var i = 1; i <= 16; i++) { TILE_TO_IDX[Math.min(1 << i, 65535)] = Math.min(i, NUM_VALUES - 1); } // allocate + load weights console.log('Loading model weights...'); var weights = []; for (var i = 0; i < NUM_PATTERNS; i++) { weights.push(new Float32Array(TABLE_SIZE)); } // check local dir first, then parent (works both standalone and in-tree) var WEIGHTS_FILE = fs.existsSync(path.join(__dirname, 'model_weights.bin')) ? path.join(__dirname, 'model_weights.bin') : path.join(__dirname, '..', 'model_weights.bin'); var META_FILE = fs.existsSync(path.join(__dirname, 'model_meta.json')) ? path.join(__dirname, 'model_meta.json') : path.join(__dirname, '..', 'model_meta.json'); if (!fs.existsSync(WEIGHTS_FILE)) { console.error('model_weights.bin not found. Train the model first.'); process.exit(1); } var meta = JSON.parse(fs.readFileSync(META_FILE, 'utf8')); var buf = fs.readFileSync(WEIGHTS_FILE); var off = 0; for (var i = 0; i < NUM_PATTERNS; i++) { var src = new Float32Array(buf.buffer, buf.byteOffset + off, TABLE_SIZE); weights[i].set(src); off += TABLE_SIZE * 4; } console.log('Model loaded (' + meta.gamesPlayed.toLocaleString() + ' games, max tile ' + meta.maxTile + ')'); function evaluate(board) { var score = 0; var ci = 0; for (var t = 0; t < NUM_TUPLES; t++) { var idx = board[tupleCoords[ci]]; idx = idx * NUM_VALUES + board[tupleCoords[ci + 1]]; idx = idx * NUM_VALUES + board[tupleCoords[ci + 2]]; idx = idx * NUM_VALUES + board[tupleCoords[ci + 3]]; idx = idx * NUM_VALUES + board[tupleCoords[ci + 4]]; idx = idx * NUM_VALUES + board[tupleCoords[ci + 5]]; score += weights[tupleTable[t]][idx]; ci += 6; } return score; } var _merged = new Uint8Array(16); var _tempBoards = [new Uint8Array(16), new Uint8Array(16), new Uint8Array(16), new Uint8Array(16)]; var VECTORS_X = [0, 1, 0, -1]; var VECTORS_Y = [-1, 0, 1, 0]; function simulateMove(board, dir, out) { out.set(board); _merged.fill(0); var reward = 0, moved = false; var vx = VECTORS_X[dir], vy = VECTORS_Y[dir]; var tx = vx === 1 ? [3, 2, 1, 0] : [0, 1, 2, 3]; var ty = vy === 1 ? [3, 2, 1, 0] : [0, 1, 2, 3]; for (var i = 0; i < 4; i++) { for (var j = 0; j < 4; j++) { var cx = tx[i], cy = ty[j]; var ci = cx * 4 + cy; var val = out[ci]; if (val === 0) continue; var px = cx, py = cy; var nx = cx + vx, ny = cy + vy; while (nx >= 0 && nx < 4 && ny >= 0 && ny < 4 && out[nx * 4 + ny] === 0) { px = nx; py = ny; nx += vx; ny += vy; } var ni = nx * 4 + ny, pi = px * 4 + py; if (nx >= 0 && nx < 4 && ny >= 0 && ny < 4 && out[ni] === val && !_merged[ni]) { out[ci] = 0; var nv = val + 1; out[ni] = nv; _merged[ni] = 1; reward += (nv < NUM_VALUES) ? (1 << nv) : (1 << (NUM_VALUES - 1)); moved = true; } else if (pi !== ci) { out[ci] = 0; out[pi] = val; moved = true; } } } return { reward: reward, moved: moved }; } function getBestMove(board) { var bestScore = -Infinity; var bestDir = -1; for (var d = 0; d < 4; d++) { var r = simulateMove(board, d, _tempBoards[d]); if (!r.moved) continue; var sc = r.reward + evaluate(_tempBoards[d]); if (sc > bestScore) { bestScore = sc; bestDir = d; } } return bestDir; } // convert browser cell format to internal indices function boardFromCells(cells) { var board = new Uint8Array(16); for (var x = 0; x < 4; x++) for (var y = 0; y < 4; y++) { var v = cells[x][y]; board[x * 4 + y] = v === 0 ? 0 : (TILE_TO_IDX[v] || 0); } return board; } // static file server var MIME = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.json': 'application/json', '.ico': 'image/x-icon', '.png': 'image/png', '.jpg': 'image/jpeg', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.svg': 'image/svg+xml', }; var RUNNER_DIR = path.resolve(__dirname); var PROJECT_DIR = path.resolve(__dirname, '..'); // HF Spaces uses port 7860 var PORT = parseInt(process.env.PORT, 10) || 3000; function serveStatic(reqPath, res) { var safePath = reqPath === '/' ? '/index.html' : reqPath; safePath = decodeURIComponent(safePath); // try runner dir first (overrides), then project root var runnerFile = path.resolve(RUNNER_DIR, '.' + safePath); var projectFile = path.resolve(PROJECT_DIR, '.' + safePath); var filePath; if (runnerFile.startsWith(RUNNER_DIR) && fs.existsSync(runnerFile) && fs.statSync(runnerFile).isFile()) { filePath = runnerFile; } else if (projectFile.startsWith(PROJECT_DIR)) { filePath = projectFile; } else { res.writeHead(403); res.end('Forbidden'); return; } fs.readFile(filePath, function (err, data) { if (err) { res.writeHead(404); res.end('Not found'); return; } var ext = path.extname(filePath).toLowerCase(); res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' }); res.end(data); }); } function readBody(req) { return new Promise(function (resolve, reject) { var chunks = []; var size = 0; req.on('data', function (chunk) { size += chunk.length; if (size > 102400) { reject(new Error('too big')); req.destroy(); return; } chunks.push(chunk); }); req.on('end', function () { resolve(Buffer.concat(chunks).toString()); }); req.on('error', reject); }); } var server = http.createServer(function (req, res) { var url = new (require('url').URL)(req.url, 'http://localhost'); var pathname = url.pathname; // single API endpoint - get best move for a board state if (pathname === '/api/move' && req.method === 'POST') { readBody(req).then(function (raw) { var body = JSON.parse(raw); var board = boardFromCells(body.cells); var dir = getBestMove(board); res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end(JSON.stringify({ direction: dir })); }).catch(function (e) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message })); }); return; } if (req.method === 'OPTIONS') { res.writeHead(204, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type' }); res.end(); return; } serveStatic(pathname, res); }); server.listen(PORT, function () { console.log('2048 running at http://localhost:' + PORT); });