I'm having trouble implementing this TicTacToe AI that I found here. I'm relatively new to javascript so I'm sure I'm doing something wrong with variable scoping.
The code wont run in the snippet but heres my codepen
choices = {
0: '#ul',
1: '#um',
2: '#ur',
3: '#ml',
4: '#mm',
5: '#mr',
6: '#ll',
7: '#lm',
8: '#lr'
}
function getGrid() {
var divs = []
for (var i = 0; i < 9; i++) {
divs.push($(choices[i]).html())
}
return divs
}
function getGame() {
var divs = []
for (var i = 0; i < 9; i++) {
divs.push([$(choices[i]).html(), i])
}
return divs
}
function convertGameToGrid(game) {
var divs = []
for (var i = 0; i < game.length; i++) {
divs.push(game[i][0])
}
return divs
}
function checkGrid(divs) {
var options = [
[divs[0], divs[1], divs[2]],
[divs[3], divs[4], divs[5]],
[divs[6], divs[7], divs[8]],
[divs[0], divs[3], divs[6]],
[divs[1], divs[4], divs[7]],
[divs[2], divs[5], divs[8]],
[divs[0], divs[4], divs[8]],
[divs[2], divs[4], divs[6]]
]
for (var i = 0; i < options.length; i++) {
if (options[i][0] == 'X' && options[i][1] == 'X' && options[i][2] == 'X') {
return 'X'
} else if (options[i][0] == 'O' && options[i][1] == 'O' && options[i][2] == 'O') {
return 'O'
}
}
for (var i = 0; i < 9; i++) {
if (divs[i] == '') {
return false //still moves
}
}
return 'Tie' //no winner and no moves
}
var player = 'O'
var ai = 'X'
$(document).ready(function() {
function playerTurn(i) {
return function() {
var g = getGrid()
var cG = checkGrid(g)
if (!cG) {
if ($(choices[i]).html() == '') {
$(choices[i]).html(player)
var g = getGrid()
var cG = checkGrid(g)
if (cG == player) {
console.log('You win')
} else if (cG == 'Tie') {
console.log('Tie')
} else {
aiTurn()
}
}
}
}
}
for (var i = 0; i < 9; i++) {
$(choices[i]).on('click', playerTurn(i));
}
function score(g, depth) {
var cG = checkGrid(g)
console.log(cG, g)
if (cG == ai) {
return 10 - depth
} else if (cG == player) {
return depth - 10
} else {
return 0
}
}
function minimax(game, depth) {
var g = convertGameToGrid(game)
if (checkGrid(g)) {
return score(g, depth)
}
depth += 1
var scores = []
var moves = []
var availMoves = getAvailMoves(game)
console.log('moves', availMoves)
for (var i = 0; i < availMoves.length; i++) {
var possibleGame = game
if (depth % 2 == 0) {
possibleGame[availMoves[i]][0] = ai
} else {
possibleGame[availMoves[i]][0] = player
}
var m = minimax(possibleGame, depth)
scores.push(m)
console.log('mm: ', depth, i, scores)
moves.push(availMoves[i])
}
//even depths are ai, odd are player
if (depth % 2 == 0) {
var max_score_index = 0
var max_score = -100000000
for (var i = 0; i < scores.length; i++) {
if (scores[i] > max_score) {
max_score_index = i
max_score = scores[i]
}
}
if (depth == 0) { //we need the best move
return moves[max_score_index]
} else { //otherwise this function needs scores
return scores[max_score_index]
}
} else {
var min_score_index = 0
var min_score = 100000000
for (var i = 0; i < scores.length; i++) {
if (scores[i] < min_score) {
min_score_index = i
min_score = scores[i]
}
}
return scores[max_score_index]
}
}
function getAvailMoves(game) {
var moves = []
for (var i = 0; i < game.length; i++) {
if (game[i][0] == '') {
moves.push(game[i][1])
}
}
return moves
}
function aiTurn() {
//Dumb ai
// c = Math.floor(Math.random()*9)
// while ($(choices[c]).html()) {
// c = Math.floor(Math.random()*9)
// }
//new strategy taken from http://neverstopbuilding.com/minimax
console.log('ai')
var c;
game = getGame()
c = minimax(game, -1)
$(choices[c]).html('X')
var g = getGrid()
var cG = checkGrid(g)
if (cG == ai) {
console.log('You lose')
} else if (cG == 'Tie') {
console.log('Tie')
}
}
})
#ttt-box {
position: relative;
height: 304px;
width: 304px;
margin: 30px auto;
background-color: #bbb;
border: solid #000 4px;
border-radius: 20%;
}
#l1,
#l2,
#l3,
#l4 {
position: absolute;
background-color: #000;
}
#l1 {
left: 99px;
width: 3px;
height: 296px;
}
#l2 {
left: 199px;
width: 3px;
height: 296px;
}
#l3 {
top: 99px;
width: 296px;
height: 3px;
}
#l4 {
top: 199px;
width: 296px;
height: 3px;
}
#ul,
#um,
#ur,
#ml,
#mm,
#mr,
#ll,
#lm,
#lr {
cursor: pointer;
position: absolute;
width: 99px;
height: 99px;
font-size: 70px;
text-align: center;
}
#ul {
top: 0;
left: 0;
}
#um {
top: 0;
left: 101px;
}
#ur {
top: 0;
left: 201px;
}
#ml {
top: 101px;
left: 0;
}
#mm {
top: 101px;
left: 101px;
}
#mr {
top: 101px;
left: 201px;
}
#ll {
top: 201px;
left: 0;
}
#lm {
top: 201px;
left: 101px;
}
#lr {
top: 201px;
left: 201px;
}
<body>
<div class="container">
<div id="content">
<div id="ttt-box">
<div id="l1"></div>
<div id="l2"></div>
<div id="l3"></div>
<div id="l4"></div>
<div id="boxes">
<div id="ul"></div>
<div id="um"></div>
<div id="ur"></div>
<div id="ml"></div>
<div id="mm"></div>
<div id="mr"></div>
<div id="ll"></div>
<div id="lm"></div>
<div id="lr"></div>
</div>
</div>
</div>
</div>
</body>
The code in particular that I think is breaking is the section below. After the first player's move, I think the console.log should print out 8! times because of all the different paths that should be taken, instead it only prints 8 times as if it went down a single path.
var availMoves = getAvailMoves(game)
console.log('moves',availMoves)
for (var i=0;i<availMoves.length;i++) {
var possibleGame = game
if (depth%2==0) {
possibleGame[availMoves[i]][0] = ai
} else {
possibleGame[availMoves[i]][0] = player
}
var m = minimax(possibleGame,depth)
scores.push(m)
console.log('mm: ', depth,i, scores)
moves.push(availMoves[i])
}
Edit: What I'm noticing is that sometimes the minimax recursion is returning undefined. I've tried finding why that is (see my codepen) but I've been unsuccessful.
Edit2: It appears to be returning undefined because it is completely skipping these recursions. I still can't find a way to fix this though.
First a suggestion, since you're just getting started: learn how to use the debugger. It will be invaluable in cases like these and most modern browsers have them built in.
Regarding your issue, I haven't traced through all your code but I did notice one thing that could be causing the problem in your minmax
function. Towards the bottom of that function you have this code:
//even depths are ai, odd are player
if (depth % 2 == 0) {
var max_score_index = 0
// snip...
} else {
var min_score_index = 0
// snip...
return scores[max_score_index]
}
Note that you are declaring and assigning max_score_index
in the if
block, but also using it in the else
block (without assigning it). That will cause it to return undefined from the else
block.