2048 / server.js
yethdev's picture
Upload folder using huggingface_hub
b11323b verified
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);
});