forked from CBGamesdev/chilibowlflash
-
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
1 parent
eb28f41
commit 43ffcdd
Showing
8 changed files
with
1,112 additions
and
0 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 |
---|---|---|
@@ -0,0 +1,150 @@ | ||
function AI(grid) { | ||
this.grid = grid; | ||
} | ||
|
||
// static evaluation function | ||
AI.prototype.eval = function() { | ||
var emptyCells = this.grid.availableCells().length; | ||
|
||
var smoothWeight = 0.1, | ||
//monoWeight = 0.0, | ||
//islandWeight = 0.0, | ||
mono2Weight = 1.0, | ||
emptyWeight = 2.7, | ||
maxWeight = 1.0; | ||
|
||
return this.grid.smoothness() * smoothWeight | ||
//+ this.grid.monotonicity() * monoWeight | ||
//- this.grid.islands() * islandWeight | ||
+ this.grid.monotonicity2() * mono2Weight | ||
+ Math.log(emptyCells) * emptyWeight | ||
+ this.grid.maxValue() * maxWeight; | ||
}; | ||
|
||
// 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); | ||
} | ||
} | ||
|
||
// 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) } ); | ||
} | ||
} | ||
} | ||
|
||
// 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 }; | ||
} | ||
} | ||
} | ||
|
||
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) { | ||
break; | ||
} else { | ||
best = newBest; | ||
} | ||
depth++; | ||
} while ( (new Date()).getTime() - start < minSearchTime); | ||
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
(function() { | ||
var lastTime = 0; | ||
var vendors = ['webkit', 'moz']; | ||
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { | ||
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; | ||
window.cancelAnimationFrame = | ||
window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; | ||
} | ||
|
||
if (!window.requestAnimationFrame) { | ||
window.requestAnimationFrame = function(callback, element) { | ||
var currTime = new Date().getTime(); | ||
var timeToCall = Math.max(0, 16 - (currTime - lastTime)); | ||
var id = window.setTimeout(function() { callback(currTime + timeToCall); }, | ||
timeToCall); | ||
lastTime = currTime + timeToCall; | ||
return id; | ||
}; | ||
} | ||
|
||
if (!window.cancelAnimationFrame) { | ||
window.cancelAnimationFrame = function(id) { | ||
clearTimeout(id); | ||
}; | ||
} | ||
}()); |
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,7 @@ | ||
animationDelay = 100; | ||
minSearchTime = 100; | ||
|
||
// Wait till the browser is ready to render the game (avoids glitches) | ||
window.requestAnimationFrame(function () { | ||
var manager = new GameManager(4, KeyboardInputManager, HTMLActuator); | ||
}); |
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,97 @@ | ||
function GameManager(size, InputManager, Actuator) { | ||
this.size = size; // Size of the grid | ||
this.inputManager = new InputManager; | ||
this.actuator = new Actuator; | ||
|
||
this.running = false; | ||
|
||
this.inputManager.on("move", this.move.bind(this)); | ||
this.inputManager.on("restart", this.restart.bind(this)); | ||
|
||
this.inputManager.on('think', function() { | ||
var best = this.ai.getBest(); | ||
this.actuator.showHint(best.move); | ||
}.bind(this)); | ||
|
||
|
||
this.inputManager.on('run', function() { | ||
if (this.running) { | ||
this.running = false; | ||
this.actuator.setRunButton('Auto-run'); | ||
} else { | ||
this.running = true; | ||
this.run() | ||
this.actuator.setRunButton('Stop'); | ||
} | ||
}.bind(this)); | ||
|
||
this.setup(); | ||
} | ||
|
||
// Restart the game | ||
GameManager.prototype.restart = function () { | ||
this.actuator.restart(); | ||
this.running = false; | ||
this.actuator.setRunButton('Auto-run'); | ||
this.setup(); | ||
}; | ||
|
||
// Set up the game | ||
GameManager.prototype.setup = function () { | ||
this.grid = new Grid(this.size); | ||
this.grid.addStartTiles(); | ||
|
||
this.ai = new AI(this.grid); | ||
|
||
this.score = 0; | ||
this.over = false; | ||
this.won = false; | ||
|
||
// Update the actuator | ||
this.actuate(); | ||
}; | ||
|
||
|
||
// Sends the updated grid to the actuator | ||
GameManager.prototype.actuate = function () { | ||
this.actuator.actuate(this.grid, { | ||
score: this.score, | ||
over: this.over, | ||
won: this.won | ||
}); | ||
}; | ||
|
||
// makes a given move and updates state | ||
GameManager.prototype.move = function(direction) { | ||
var result = this.grid.move(direction); | ||
this.score += result.score; | ||
|
||
if (!result.won) { | ||
if (result.moved) { | ||
this.grid.computerMove(); | ||
} | ||
} else { | ||
this.won = true; | ||
} | ||
|
||
//console.log(this.grid.valueSum()); | ||
|
||
if (!this.grid.movesAvailable()) { | ||
this.over = true; // Game over! | ||
} | ||
|
||
this.actuate(); | ||
} | ||
|
||
// moves continuously until game is over | ||
GameManager.prototype.run = function() { | ||
var best = this.ai.getBest(); | ||
this.move(best.move); | ||
var timeout = animationDelay; | ||
if (this.running && !this.over && !this.won) { | ||
var self = this; | ||
setTimeout(function(){ | ||
self.run(); | ||
}, timeout); | ||
} | ||
} |
Oops, something went wrong.