From d0cc1029eca380de2ebea7f55cfcfcce6d98fd9a Mon Sep 17 00:00:00 2001 From: spelgubbe <4411767+spelgubbe@users.noreply.github.com> Date: Sat, 5 Mar 2022 16:33:57 +0100 Subject: [PATCH] Make GameNode abstract (#1) Old GameNode is called TicTacToeGameNode as it is used to represent tic tac toe game states. The idea is that any game implementing the GameNode interface should be able to use the Minimax algorithm. --- src/algorithms/ai/GameNode.js | 132 ++-------------- src/algorithms/ai/TicTacToeGameNode.js | 145 ++++++++++++++++++ src/algorithms/ai/minimax/Minimax.js | 2 +- .../ai/minimax/__test__/Minimax.test.js | 4 +- 4 files changed, 159 insertions(+), 124 deletions(-) create mode 100644 src/algorithms/ai/TicTacToeGameNode.js diff --git a/src/algorithms/ai/GameNode.js b/src/algorithms/ai/GameNode.js index 70630eaa..c0c50524 100644 --- a/src/algorithms/ai/GameNode.js +++ b/src/algorithms/ai/GameNode.js @@ -1,140 +1,30 @@ -export const playerMark = 'x'; -export const opponentMark = 'o'; class GameNode { - /** - * - * @param {Array.>} board Board of a Tic-Tac-Toe game. - * @param {string} currentPlayer Current player mark: 'x' or 'o' - */ - constructor(board, currentPlayer) { - if (!GameNode.isBoardValid(board)) throw new Error('Invalid board object!'); - if (currentPlayer !== 'x' && currentPlayer !== 'o') throw new Error('Invalid player: \'x\' or \'o\''); - - this.board = board; - this.currentPlayer = currentPlayer; - this.move = null; - } - - /** - * Check whether a board object is valid: 3*3 matrix of 'x', 'o' or '_' - * @param {Array.>} board Board of a Tic-Tac-Toe game. - * @returns {boolean} - If the board is valid. - */ - static isBoardValid(board) { - let validCount = 0; - if (Array.isArray(board) && board.length === 3) { - for (let i = 0; i < 3; i += 1) { - const row = board[i]; - if (Array.isArray(row) && row.length === 3) { - for (let j = 0; j < 3; j += 1) { - if (row[j] === 'x' || row[j] === 'o' || row[j] === '_') { - validCount += 1; - } - } - } - } - } - return validCount === 9; - } - /** * @returns {boolean} - If it is the opponent's turn */ isOpponentPlaying() { - return this.currentPlayer === opponentMark; + throw new Error('GameNode::isOpponentPlaying must be implemented'); } /** - * @returns {boolean} - If the game is terminated + * @returns {boolean} - If the game is terminated in the current state */ - isTerminated() { - if (!this.hasMoreMoves() || this.checkWin() !== null) return true; - return false; - } - - /** - * @returns {boolean} - If there are empty grids on the board - */ - hasMoreMoves() { - for (let i = 0; i < 3; i += 1) { - for (let j = 0; j < 3; j += 1) { - if (this.board[i][j] === '_') return true; - } - } - return false; - } - - /** - * - * @returns {string} - If the game is end with three consecutive mark on the board. - * Return the player mark of winning ('x' or 'o'), otherwise return null. - */ - checkWin() { - const { board } = this; - // Checking for rows - for (let row = 0; row < 3; row += 1) { - if (board[row][0] === board[row][1] && board[row][1] === board[row][2]) { - if (board[row][0] === playerMark) return playerMark; - if (board[row][0] === opponentMark) return opponentMark; - } - } - - // Checking for columns - for (let col = 0; col < 3; col += 1) { - if (board[0][col] === board[1][col] && board[1][col] === board[2][col]) { - if (board[0][col] === playerMark) return playerMark; - if (board[0][col] === opponentMark) return opponentMark; - } - } - - // Checking for diagonals - if (board[0][0] === board[1][1] && board[1][1] === board[2][2]) { - if (board[0][0] === playerMark) return playerMark; - if (board[0][0] === opponentMark) return opponentMark; - } - - if (board[0][2] === board[1][1] && board[1][1] === board[2][0]) { - if (board[0][2] === playerMark) return playerMark; - if (board[0][2] === opponentMark) return opponentMark; - } - - // Otherwise none of the players have won, return null - return null; - } - - /** - * @returns {Number} - Score of current state - */ - evaluate() { - const win = this.checkWin(); - if (win === playerMark) return 10; - if (win === opponentMark) return -10; - return 0; + isTerminalState() { + throw new Error('GameNode::isTerminalState must be implemented'); } /** * @returns {Array.} - Possible next states of the game tree */ computeNextStates() { - if (this.isTerminated()) return []; + throw new Error('GameNode::computeNextStates must be implemented'); + } - const nextPlayerMark = this.currentPlayer === playerMark ? opponentMark : playerMark; - const nextStates = []; - - for (let i = 0; i < 3; i += 1) { - for (let j = 0; j < 3; j += 1) { - if (this.board[i][j] === '_') { - const newBoard = JSON.parse(JSON.stringify(this.board)); // Deep clone the board array - newBoard[i][j] = this.currentPlayer; // Make the move - - const newNode = new GameNode(newBoard, nextPlayerMark); - newNode.move = [i, j]; // Record the move - nextStates.push(newNode); // Add the new state to result - } - } - } - - return nextStates; + /** + * @returns {Number} - Score of current state + */ + evaluateState() { + throw new Error('GameNode::evaluateState must be implemented'); } } diff --git a/src/algorithms/ai/TicTacToeGameNode.js b/src/algorithms/ai/TicTacToeGameNode.js new file mode 100644 index 00000000..a830f273 --- /dev/null +++ b/src/algorithms/ai/TicTacToeGameNode.js @@ -0,0 +1,145 @@ +import GameNode from './GameNode'; + +export const playerMark = 'x'; +export const opponentMark = 'o'; +class TicTacToeGameNode extends GameNode { + /** + * + * @param {Array.>} board Board of a Tic-Tac-Toe game. + * @param {string} currentPlayer Current player mark: 'x' or 'o' + */ + constructor(board, currentPlayer) { + super(); + + if (!TicTacToeGameNode.isBoardValid(board)) throw new Error('Invalid board object!'); + if (currentPlayer !== 'x' && currentPlayer !== 'o') throw new Error('Invalid player: \'x\' or \'o\''); + + this.board = board; + this.currentPlayer = currentPlayer; + this.move = null; + } + + /** + * Check whether a board object is valid: 3*3 matrix of 'x', 'o' or '_' + * @param {Array.>} board Board of a Tic-Tac-Toe game. + * @returns {boolean} - If the board is valid. + */ + static isBoardValid(board) { + let validCount = 0; + if (Array.isArray(board) && board.length === 3) { + for (let i = 0; i < 3; i += 1) { + const row = board[i]; + if (Array.isArray(row) && row.length === 3) { + for (let j = 0; j < 3; j += 1) { + if (row[j] === 'x' || row[j] === 'o' || row[j] === '_') { + validCount += 1; + } + } + } + } + } + return validCount === 9; + } + + /** + * @returns {boolean} - If it is the opponent's turn + */ + isOpponentPlaying() { + return this.currentPlayer === opponentMark; + } + + /** + * @returns {boolean} - If the game is terminated + */ + isTerminalState() { + if (!this.hasMoreMoves() || this.checkWin() !== null) return true; + return false; + } + + /** + * @returns {boolean} - If there are empty grids on the board + */ + hasMoreMoves() { + for (let i = 0; i < 3; i += 1) { + for (let j = 0; j < 3; j += 1) { + if (this.board[i][j] === '_') return true; + } + } + return false; + } + + /** + * + * @returns {string} - If the game is end with three consecutive mark on the board. + * Return the player mark of winning ('x' or 'o'), otherwise return null. + */ + checkWin() { + const { board } = this; + // Checking for rows + for (let row = 0; row < 3; row += 1) { + if (board[row][0] === board[row][1] && board[row][1] === board[row][2]) { + if (board[row][0] === playerMark) return playerMark; + if (board[row][0] === opponentMark) return opponentMark; + } + } + + // Checking for columns + for (let col = 0; col < 3; col += 1) { + if (board[0][col] === board[1][col] && board[1][col] === board[2][col]) { + if (board[0][col] === playerMark) return playerMark; + if (board[0][col] === opponentMark) return opponentMark; + } + } + + // Checking for diagonals + if (board[0][0] === board[1][1] && board[1][1] === board[2][2]) { + if (board[0][0] === playerMark) return playerMark; + if (board[0][0] === opponentMark) return opponentMark; + } + + if (board[0][2] === board[1][1] && board[1][1] === board[2][0]) { + if (board[0][2] === playerMark) return playerMark; + if (board[0][2] === opponentMark) return opponentMark; + } + + // Otherwise none of the players have won, return null + return null; + } + + /** + * @returns {Number} - Score of current state + */ + evaluate() { + const win = this.checkWin(); + if (win === playerMark) return 10; + if (win === opponentMark) return -10; + return 0; + } + + /** + * @returns {Array.} - Possible next states of the game tree + */ + computeNextStates() { + if (this.isTerminalState()) return []; + + const nextPlayerMark = this.currentPlayer === playerMark ? opponentMark : playerMark; + const nextStates = []; + + for (let i = 0; i < 3; i += 1) { + for (let j = 0; j < 3; j += 1) { + if (this.board[i][j] === '_') { + const newBoard = JSON.parse(JSON.stringify(this.board)); // Deep clone the board array + newBoard[i][j] = this.currentPlayer; // Make the move + + const newNode = new TicTacToeGameNode(newBoard, nextPlayerMark); + newNode.move = [i, j]; // Record the move + nextStates.push(newNode); // Add the new state to result + } + } + } + + return nextStates; + } +} + +export default TicTacToeGameNode; diff --git a/src/algorithms/ai/minimax/Minimax.js b/src/algorithms/ai/minimax/Minimax.js index bd249846..80753a34 100644 --- a/src/algorithms/ai/minimax/Minimax.js +++ b/src/algorithms/ai/minimax/Minimax.js @@ -39,7 +39,7 @@ class MinimaxPlayer extends Player { * @returns {Number} - Best score the player can reach under this state */ minimax(node, depth) { - if (depth === 0 || node.isTerminated()) { + if (depth === 0 || node.isTerminalState()) { return this.heuristic(node); } diff --git a/src/algorithms/ai/minimax/__test__/Minimax.test.js b/src/algorithms/ai/minimax/__test__/Minimax.test.js index 46bbd1c7..918a49fc 100644 --- a/src/algorithms/ai/minimax/__test__/Minimax.test.js +++ b/src/algorithms/ai/minimax/__test__/Minimax.test.js @@ -1,4 +1,4 @@ -import GameNode, { playerMark } from '../../GameNode'; +import TicTacToeGameNode, { playerMark } from '../../TicTacToeGameNode'; import Minimax from '../Minimax'; describe('Minimax', () => { @@ -8,7 +8,7 @@ describe('Minimax', () => { ['o', 'o', 'x'], ['_', '_', '_'], ]; - const initialNode = new GameNode(board, playerMark); + const initialNode = new TicTacToeGameNode(board, playerMark); const player = new Minimax(); expect(player.findBestMove(initialNode)).toStrictEqual([2, 2]);