I recently struggled with implementing AI in Tic Tac Toe game. For better understanding of minimax algorithm I've decided to separate AI's logic. When I finished I thought that some may find it useful, so I packed AI in easy-to-use API and described its usage in README file on GitHub. I am wondering if my code uses any bad practices or something can be written cleaner and more efficient. I will be more than grateful for any feedback.
https://github.com/pietrzakacper/tic-tac-toe-ai
Code:
ai.js
import init from './init';
import minimax from './minimax';
import {isTerminated,getStateOfGame} from './game';
function getAIAction(data) {
const checkedData = {};
init(data, checkedData);
return minimax(checkedData.board, checkedData.aiCharacter, checkedData.aiCharacter, 0);
}
export default {
getAIMove: function(data) {
return (getAIAction(data)).move;
},
getBoardAfterAIMove: function(data) {
return (getAIAction(data)).board;
},
isTerminated: isTerminated,
getStateOfGame: getStateOfGame
};
init.js
import valid from './validation';
export default function initialization(data, target) {
for (let key in valid) {
if (data.hasOwnProperty(key)) {
target[key] = valid[key](data);
} else {
throw new Error('AI ERROR: Missing property : "' + key + '" in passed data object');
}
}
}
minimax.js
import Action from './action';
import * as GameTools from './game';
export default function calculateValue(board, character, aiCharacter, depth) {
const gameState = GameTools.getStateOfGame(board);
//if simulated game is terminated
if (gameState != 'not-end') {
const tmpAction = new Action(-1);
//evaluate game
tmpAction.score = GameTools.getGameScore(gameState, aiCharacter, depth);
return tmpAction;
}
const actions = [];
GameTools.getAllMoves(board).forEach(move => {
const action = new Action(move);
action.board = GameTools.getBoardAfterSimulatedMove(board, move, character);
action.score = calculateValue(action.board, character === 'x' ? 'o' : 'x', aiCharacter, depth + 1).score;
actions.push(action);
});
//sort actions depending on which player (MAX or MIN) is making move
actions.sort(character === aiCharacter ? ACTION_ASCENDING : ACTION_DESCENDING);
//return best action
return actions[0];
}
const ACTION_ASCENDING = (a, b) => b.score - a.score;
const ACTION_DESCENDING = (a, b) => a.score - b.score;
game.js
import {
whichIfAnyPlayerWon,
isDraw
} from './win';
function isTerminated(board) {
return getStateOfGame(board) === 'not-end' ? false : true;
}
function getStateOfGame(board) {
const supposedChampion = whichIfAnyPlayerWon(board);
if (supposedChampion != 'none') {
return supposedChampion + '-won';
}
if (isDraw(board)) {
return 'draw';
}
return 'not-end';
}
function getGameScore(msg, aiCharacter, depth) {
if (msg === aiCharacter + '-won') {
return 10 - depth;
} else if (msg === 'draw') {
return 0;
}
return depth - 10;
}
function getAllMoves(board) {
const possibleMoves = [];
board.forEach(function(value, index) {
if (value === 'e') {
possibleMoves.push(index);
}
});
return possibleMoves;
}
function getBoardAfterSimulatedMove(board, pos, char) {
const newBoard = board.slice();
newBoard[pos] = char;
return newBoard;
}
export {
getStateOfGame,
isTerminated,
getGameScore,
getAllMoves,
getBoardAfterSimulatedMove
};
win.js
function whichIfAnyPlayerWon(board) {
const characters = ['x', 'o'];
for (let i = 0; i < characters.length; ++i) {
if (hasWon(characters[i], board)) {
return characters[i];
}
}
return 'none';
}
function isDraw(board) {
for (let i = 0; i < board.length; ++i) {
if (board[i] === 'e') {
return false;
}
}
return true;
}
function hasWon(char, board) {
if (isColumnWin(char, board) || isRowWin(char, board) || isDiagonalWin(char, board)) {
return true;
}
return false;
}
function isColumnWin(char, board) {
for (let i = 0; i < 3; ++i) {
if (char === board[i] && char === board[i + 3] && char === board[i + 6]) {
return true;
}
}
return false;
}
function isRowWin(char, board) {
for (let i = 0; i <= 6; i += 3) {
if (char === board[i] && char === board[i + 1] && char === board[i + 2]) {
return true;
}
}
return false;
}
function isDiagonalWin(char, board) {
return char === board[0] && char === board[4] && char == board[8] ||
char === board[2] && char === board[4] && char == board[6];
}
export {
whichIfAnyPlayerWon,
isDraw
};