Writing an AI: The function next_move will be called every turn with the game board as the first argument and console.log as the second.



After editing, you must click reload AI to save your changes.

function log_2(f) { return Math.log(f) / Math.log(2); } function monotonicity(grid) { var totals = [0, 0, 0, 0]; for (var x=0; x<4; x++) { var current = 0; var next = current+1; while (next<4) { while (next<4 && !grid.cellOccupied(grid.indexes[x][next])) { next++; } if (next>=4) { next--; } var currentValue = grid.cellOccupied({x:x, y:current}) ? log_2(grid.cellContent(grid.indexes[x][current]).value) : 0; var nextValue = grid.cellOccupied({x:x, y:next}) ? log_2(grid.cellContent( grid.indexes[x][next] ).value) : 0; if (currentValue > nextValue) { totals[0] += nextValue - currentValue; } else if (nextValue > currentValue) { totals[1] += currentValue - nextValue; } current = next; next++; } } for (var y=0; y<4; y++) { var current = 0; var next = current+1; while ( next<4 ) { while ( next<4 && !grid.cellOccupied( grid.indexes[next][y] )) { next++; } if (next>=4) { next--; } var currentValue = grid.cellOccupied({x:current, y:y}) ? log_2(grid.cellContent( grid.indexes[current][y] ).value) / Math.log(2) : 0; var nextValue = grid.cellOccupied({x:next, y:y}) ? log_2(grid.cellContent( grid.indexes[next][y] ).value) / Math.log(2) : 0; if (currentValue > nextValue) { totals[2] += nextValue - currentValue; } else if (nextValue > currentValue) { totals[3] += currentValue - nextValue; } current = next; next++; } } return Math.max(totals[0], totals[1]) + Math.max(totals[2], totals[3]); } function AI(grid) { this.grid = grid; } AI.prototype.eval = function() { var emptyCells = this.grid.availableCells().length; var smoothWeight = 0.1, mono2Weight = 1.0, emptyWeight = 2.7, maxWeight = 1.0; return this.grid.smoothness() * smoothWeight + monotonicity(this.grid) * mono2Weight + Math.log(emptyCells) * emptyWeight + this.grid.maxValue() * maxWeight; }; AI.prototype.search = function(depth, alpha, beta, positions, cutoffs) { var bestScore; var bestMove = -1; var result; if (this.grid.playerTurn) { bestScore = alpha; for (var direction in [0, 1, 2, 3]) { var newGrid = this.grid.clone(); var moved = newGrid.move(direction); if (moved.moved) { positions++; var newAI = new AI(newGrid); if (depth == 0) { result = { move: direction, score: newAI.eval() }; } else { result = newAI.search(depth-1, bestScore, beta, positions, cutoffs); positions = result.positions; cutoffs = result.cutoffs; } if (result.score > bestScore) { bestScore = result.score; bestMove = direction; } if (bestScore > beta) { cutoffs++ return { move: bestMove, score: beta, positions: positions, cutoffs: cutoffs }; } } } } else { bestScore = beta; // try a 2 and 4 in each cell and measure how annoying it is with metrics from eval var candidates = []; var cells = this.grid.availableCells(); var scores = { 2: [], 4: [] }; for (var value in scores) { for (var i in cells) { scores[value].push(null); var cell = cells[i]; var tile = new Tile(cell, parseInt(value, 10)); this.grid.insertTile(tile); scores[value][i] = -this.grid.smoothness() + this.grid.islands(); this.grid.removeTile(cell); } } // now just pick out the most annoying moves var maxScore = Math.max(Math.max.apply(null, scores[2]), Math.max.apply(null, scores[4])); for (var value in scores) { // 2 and 4 for (var i=0; i<scores[value].length; i++) { if (scores[value][i] == maxScore) { candidates.push( { position: cells[i], value: parseInt(value, 10) } ); } } } for (var i=0; i<candidates.length; i++) { var position = candidates[i].position; var value = candidates[i].value; var newGrid = this.grid.clone(); var tile = new Tile(position, value); newGrid.insertTile(tile); newGrid.playerTurn = true; positions++; newAI = new AI(newGrid); result = newAI.search(depth, alpha, bestScore, positions, cutoffs); positions = result.positions; cutoffs = result.cutoffs; if (result.score < bestScore) { bestScore = result.score; } if (bestScore < alpha) { cutoffs++; return { move: null, score: alpha, positions: positions, cutoffs: cutoffs }; } } } return { move: bestMove, score: bestScore, positions: positions, cutoffs: cutoffs }; } AI.prototype.iterativeDeep = function(minSearchTime) { var start = (new Date()).getTime(); var depth = 0; var best; do { var newBest = this.search(depth, -10000, 10000, 0 ,0); if (newBest.move == -1) { break; //breaking early } else { best = newBest; } depth++; } while ( (new Date()).getTime() - start < minSearchTime); return best } var an_ai; function next_move(grid, console_log) { an_ai = new AI(grid); var res = an_ai.iterativeDeep(100); if (res.move == 0) { return 'up'; } else if (res.move == 1) { return 'right'; } else if (res.move == 2) { return 'down'; } else { return 'left'; } } function on_move(grid, console_log) { var an_ai = new AI(grid); console_log("smoothness: " + an_ai.grid.smoothness() + " monotonicity: " + monotonicity(an_ai.grid)); }

console