2
\$\begingroup\$

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
};
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.