Skip to content

Commit

Permalink
AI.
Browse files Browse the repository at this point in the history
  • Loading branch information
moverlan committed Mar 11, 2014
1 parent a422799 commit ded6498
Show file tree
Hide file tree
Showing 11 changed files with 677 additions and 187 deletions.
17 changes: 6 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
# 2048
A small clone of [1024](https://play.google.com/store/apps/details?id=com.veewo.a1024), based on [Saming's 2048](http://saming.fr/p/2048/) (also a clone).
# 2048 AI

Made just for fun. [Play it here!](http://gabrielecirulli.github.io/2048/)
AI for [2048](https://github.com/gabrielecirulli/2048).

[![Screenshot](http://pictures.gabrielecirulli.com/2048-20140309-234100.png)](http://pictures.gabrielecirulli.com/2048-20140309-234100.png)
The algorithm is iterative deepening depth first alpha-beta search. The evaluation function tries to minimize the number of tiles on the grid while keeping same/similar tiles in line with each other.

That screenshot is fake by, the way. I never reached 2048 :smile:
You can tweak the thinking time via global var `animationDelay`. Higher = more time/deeper search.

## Contributing
Changes and improvements are more than welcome! Feel free to fork and open a pull request. Please make your changes in a specifically made branch and request to pull on `master`! If you can, please make sure the game fully works before sending the PR, as that will help speed up the process.
I think there are still some bugs as it tends to make some weird moves and die during the endgame, but in my testing it almost always gets 1024 and usually gets very close to 2048, achieving scores of roughly 8-10k.

You can find the same information in the [contributing guide.](https://github.com/gabrielecirulli/2048/blob/master/CONTRIBUTING.md)

## License
2048 is licensed under the [MIT license.](https://github.com/gabrielecirulli/2048/blob/master/LICENSE.txt)
Code is real sloppy, sorry.
Binary file added img/spinner.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 13 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
<title>2048</title>

<link href="style/main.css" rel="stylesheet" type="text/css">
<link href="style/ai.css" rel="stylesheet" type="text/css">

<script src="js/hammer.min.js"></script>
<script src="js/keyboard_input_manager.js"></script>
<script src="js/html_actuator.js"></script>
<script src="js/grid.js"></script>
<script src="js/tile.js"></script>
<script src="js/ai.js"></script>
<script src="js/game_manager.js"></script>
<script src="js/application.js"></script>

Expand All @@ -26,6 +28,16 @@ <h1 class="title">2048</h1>
</div>
<p class="game-intro">Join the numbers and get to the <strong>2048 tile!</strong></p>

<div class='controls'>
<div id='hint-button-container'>
<button id='hint-button' class='ai-button'>Get Hint</button>
</div>
<div id='feedback-container'> </div>
<div id='run-button-container'>
<button id='run-button' class='ai-button'>Auto-run</button>
</div>
</div>

<div class="game-container">
<div class="game-message">
<p></p>
Expand Down Expand Up @@ -71,7 +83,7 @@ <h1 class="title">2048</h1>
</p>
<hr>
<p>
Created by <a href="http://gabrielecirulli.com" target="_blank">Gabriele Cirulli.</a> Based on <a href="https://itunes.apple.com/us/app/1024!/id823499224" target="_blank">1024 by Veewo Studio.</a>
Created by <a href="http://gabrielecirulli.com" target="_blank">Gabriele Cirulli.</a> Based on <a href="https://itunes.apple.com/us/app/1024!/id823499224" target="_blank">1024 by Veewo Studio.</a> AI by Matt Overlan
</p>
</div>
</body>
Expand Down
225 changes: 225 additions & 0 deletions js/ai.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
function AI(grid) {
this.grid = grid;
}

// static evaluation function
AI.prototype.eval = function() {
var empty = this.grid.availableCells().length;
return -this.grid.islands()/(16.0-empty)/100.0 + this.grid.smoothness()/240.0 + empty;
};

//AI.prototype.cache = {}

// alpha-beta depth first search
AI.prototype.search = function(depth, alpha, beta, positions, cutoffs) {
var bestScore;
var bestMove = -1;
var result;

// the maxing player
if (this.grid.playerTurn) {
bestScore = alpha;
for (var direction in [0, 1, 2, 3]) {
var newGrid = this.grid.clone();
if (newGrid.move(direction).moved) {
positions++;
if (newGrid.isWin()) {
return { move: direction, score: 10000, positions: positions, cutoffs: cutoffs };
}
var newAI = new AI(newGrid);

if (depth == 0) {
result = { move: direction, score: newAI.eval() };
} else {
result = newAI.search(depth-1, bestScore, beta, positions, cutoffs);
if (result.score > 9900) { // win
result.score--; // to slightly penalize higher depth from win
}
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 { // computer's turn, we'll do heavy pruning to keep the branching factor low
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);
}
}




/*
var candidates = [];
var cells = this.grid.availableCells();
var scores = {2:[], 4:[]};
var i = 0;
for (var value in scores) {
for (var i=0; i<cells.length; i++) {
scores[value].push(0);
var cell = cells[i];
for (var direction in [0,1,2,3]) {
var vector = this.grid.getVector(direction);
var target = this.grid.findFarthestPosition(cell, vector);
if (this.grid.cellOccupied(target.next)) {
var targetValue = this.grid.cells[target.next.x][target.next.y].value;
if (targetValue == value) {
scores[value][i] -= 4;
} else {
scores[value][i] += Math.log(value) / Math.log(2);
}
}
}
}
}
//*/

// 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) {
for (var i=0; i<scores[value].length; i++) {
if (scores[value][i] == maxScore) {
candidates.push( { position: cells[i], value: parseInt(value, 10) } );
}
}
}

// search on each candidate
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 };
}
}
//*/

/*
for (var samples=0; samples<4; samples++) {
var newGrid = this.grid.clone();
newGrid.computerMove();
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) {
//console.log('cutoff')
cutoffs++;
return { move: bestMove, score: bestScore, positions: positions, cutoffs: cutoffs };
}
}
//*/
/*
for (var x=0; x<4; x++) {
for (var y=0; y<4; y++) {
var position = {x:x, y:y};
if (this.grid.cellAvailable(position)) {
for (var value in [2, 4]) {
//for (var value in [2]) {
var newGrid = this.grid.clone();
var tile = new Tile(position, value);
newGrid.insertTile(tile);
newGrid.playerTurn = true;
positions++;
newAI = new AI(newGrid);
//console.log('inserted tile, players turn is', newGrid.playerTurn);
result = newAI.search(depth, alpha, bestScore, positions, cutoffs);
positions = result.positions;
cutoffs = result.cutoffs;
if (result.score < bestScore) {
bestScore = result.score;
}
if (bestScore < alpha) {
//console.log('cutoff')
cutoffs++;
return { move: bestMove, score: bestScore, positions: positions, cutoffs: cutoffs };
}
}
}
}
}
//*/
}

return { move: bestMove, score: bestScore, positions: positions, cutoffs: cutoffs };
}

// performs a search and returns the best move
AI.prototype.getBest = function() {
return this.iterativeDeep();
}

// performs iterative deepening over the alpha-beta search
AI.prototype.iterativeDeep = function() {
var start = (new Date()).getTime();
var depth = 0;
var best;
do {
var newBest = this.search(depth, -10000, 10000, 0 ,0);
if (newBest.move == -1) {
//console.log('BREAKING EARLY');
break;
} else {
best = newBest;
}
depth++;
} while ( (new Date()).getTime() - start < minSearchTime);
//console.log('depth', --depth);
//console.log(this.translate(best.move));
//console.log(best);
return best
}

AI.prototype.translate = function(move) {
return {
0: 'up',
1: 'right',
2: 'down',
3: 'left'
}[move];
}

3 changes: 3 additions & 0 deletions js/application.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
animationDelay = 150;
minSearchTime = 100;

document.addEventListener("DOMContentLoaded", function () {
// Wait till the browser is ready to render the game (avoids glitches)
window.requestAnimationFrame(function () {
Expand Down
Loading

0 comments on commit ded6498

Please sign in to comment.