tictactoe-agent
Version:
TicTacToe Agent - Suggests a good tictactoe move based on a heuristic
242 lines (197 loc) • 6.14 kB
JavaScript
var SCORES = [];
SCORES[0] = 0;
SCORES[1] = 1;
SCORES[2] = 81;
SCORES[3] = (81 * 81);
var NEXT_TURN_MULTIPLIER = 1;
var FIRST_GO_CORNER_PERCENTAGE = 0.7;
var MAX_INCEPTION_LEVEL = 1;
var SEARCHES = {
'X': /X/g,
'O': /O/g
};
function scoreForCount(myCount, thierCount) {
if (myCount > 0) {
if (thierCount == 0) {
return SCORES[myCount];
}
}
if (thierCount > 0) {
if (myCount == 0) {
return -(SCORES[thierCount] * NEXT_TURN_MULTIPLIER);
}
}
return 0;
}
function sample(items) {
return items[Math.floor(Math.random() * items.length)];
}
function countMarks(line, mark) {
var matches = line.match(SEARCHES[mark]);
return matches ? matches.length : 0;
}
function recommendationFrom(possibilities) {
if (!possibilities || possibilities.length == 0) {
return null
}
var recommendations = {
score: NaN,
options: []
}
// Randomly pick at corner at start of game
// FIRST_GO_CORNER_PERCENTAGE of the time
//
if (possibilities.length == 9 && (Math.random() < FIRST_GO_CORNER_PERCENTAGE)) {
[0, 2, 6, 8].forEach(function(corner_index) {
recommendations.options.push(possibilities[corner_index]);
});
} else {
possibilities.forEach(function(spot) {
if (isNaN(recommendations.score) ||
spot.score > recommendations.score) {
recommendations.score = spot.score;
recommendations.options = [spot];
} else if (spot.score == recommendations.score) {
recommendations.options.push(spot);
}
});
}
return sample(recommendations.options);
}
function TicTacToeModel(state, currentTurn) {
this.state = state;
this.currentTurn = currentTurn;
this.inceptionLevel = ("number" == typeof arguments[2] ? arguments[2] : 0);
}
TicTacToeModel.prototype.currentPlayer = function() {
return this.currentTurn;
};
TicTacToeModel.prototype.otherPlayer = function() {
return 'X' == this.currentPlayer() ? 'O' : 'X';
};
TicTacToeModel.prototype.imagineStateIfIPlay = function(index) {
return this.state.substr(0, index) + this.currentPlayer() + this.state.substr(index + 1);
}
TicTacToeModel.prototype.imagineRecommendedMoveScore = function() {
var recommendation = this.getRecommendation();
var recommendedState = this.imagineStateIfIPlay(recommendation.index);
return this.imagineScore(recommendedState, this.otherPlayer()) / this.inceptionLevel;
}
TicTacToeModel.prototype.preview = function(){
return [this.currentPlayer(),this.row(0),this.row(1),this.row(2)].join("\n")
}
TicTacToeModel.prototype.imagineScore = function(imaginedState, nextTurn) {
var imaginedBoard = new TicTacToeModel(imaginedState, nextTurn, this.inceptionLevel + 1);
var summary = imaginedBoard.boardSummary();
if(summary.complete){
return -(summary.score);
}else if (this.inceptionLevel < MAX_INCEPTION_LEVEL) {
return -imaginedBoard.imagineRecommendedMoveScore();
} else {
return -(summary.score);
}
}
TicTacToeModel.prototype.possibilities = function() {
var spots = this.state.split("");
var that = this;
return spots.map(function(spot, index) {
var score = NaN;
if (spot == "-") {
var imaginedState = that.imagineStateIfIPlay(index);
score = that.imagineScore(imaginedState, that.otherPlayer());
}
return {
index: index,
spot: spot,
score: score
}
}).filter(function(spot) {
return !isNaN(spot.score);
});
}
TicTacToeModel.prototype.lines = function() {
var me = this.currentPlayer();
var them = this.otherPlayer();
return [
this.row(0),
this.row(1),
this.row(2),
this.col(0),
this.col(1),
this.col(2),
this.diagonal1(),
this.diagonal2(),
].map(function(line) {
var myCount = countMarks(line, me);
var thierCount = countMarks(line, them);
return {
line: line,
mine: myCount,
thiers: thierCount
};
}).map(function(lineSummary) {
lineSummary.score = scoreForCount(lineSummary.mine, lineSummary.thiers);
return lineSummary;
});
}
TicTacToeModel.prototype.winnerInfo = function(lines) {
var me = this.currentPlayer();
var them = this.otherPlayer();
return lines.reduce(function(winner, line, index) {
if (winner) {
return winner;
}
if (line.mine == 3) {
return {
winner: me,
line: index
};
} else if (line.thiers == 3) {
return {
winner: them,
line: index
};
} else {
return null;
}
}, null);
}
TicTacToeModel.prototype.boardSummary = function() {
var l = this.lines();
var c = this.winnerInfo(l);
var scoreFromLines = l.reduce(function(memo, line) {
return memo + line.score;
}, 0);
var scoreFromTraps = 0;
var s = scoreFromLines + scoreFromTraps;
return {
complete: c,
lines: l,
score: s
}
}
TicTacToeModel.prototype.getRecommendation = function() {
var p = this.possibilities();
var r = recommendationFrom(p);
var result = {
possibilities: p
}
if (r) {
result.index = r.index;
result.score = r.score;
}
return result;
}
TicTacToeModel.prototype.row = function(i) {
return this.state.slice(i * 3, (i * 3) + 3);
}
TicTacToeModel.prototype.col = function(i) {
return this.state.charAt(i) + this.state.charAt(i + 3) + this.state.charAt(i + 6);
}
TicTacToeModel.prototype.diagonal1 = function() {
return this.state.charAt(0) + this.state.charAt(4) + this.state.charAt(8);
}
TicTacToeModel.prototype.diagonal2 = function() {
return this.state.charAt(2) + this.state.charAt(4) + this.state.charAt(6);
}
module.exports = TicTacToeModel;