forked from ovolve/2048-AI
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
677 additions
and
187 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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). | ||
|
||
[](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. |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]; | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.