| var http = require('http');
|
| var fs = require('fs');
|
| var path = require('path');
|
|
|
|
|
| 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] },
|
| ];
|
|
|
|
|
| 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++;
|
| }
|
| }
|
|
|
|
|
| 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);
|
| }
|
|
|
|
|
| console.log('Loading model weights...');
|
| var weights = [];
|
| for (var i = 0; i < NUM_PATTERNS; i++) {
|
| weights.push(new Float32Array(TABLE_SIZE));
|
| }
|
|
|
|
|
| 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;
|
| }
|
|
|
|
|
| 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;
|
| }
|
|
|
|
|
|
|
| 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, '..');
|
|
|
|
|
| var PORT = parseInt(process.env.PORT, 10) || 3000;
|
|
|
| function serveStatic(reqPath, res) {
|
| var safePath = reqPath === '/' ? '/index.html' : reqPath;
|
| safePath = decodeURIComponent(safePath);
|
|
|
|
|
| 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;
|
|
|
|
|
| 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);
|
| });
|
|
|