mirror of
https://github.moeyy.xyz/https://github.com/trekhleb/javascript-algorithms.git
synced 2024-09-20 07:43:04 +08:00
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.
This commit is contained in:
parent
a41a2747ce
commit
d0cc1029ec
@ -1,140 +1,30 @@
|
||||
export const playerMark = 'x';
|
||||
export const opponentMark = 'o';
|
||||
class GameNode {
|
||||
/**
|
||||
*
|
||||
* @param {Array.<Array.<string>>} 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.<Array.<string>>} 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.<GameNode>} - Possible next states of the game tree
|
||||
*/
|
||||
computeNextStates() {
|
||||
if (this.isTerminated()) 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 GameNode(newBoard, nextPlayerMark);
|
||||
newNode.move = [i, j]; // Record the move
|
||||
nextStates.push(newNode); // Add the new state to result
|
||||
}
|
||||
}
|
||||
throw new Error('GameNode::computeNextStates must be implemented');
|
||||
}
|
||||
|
||||
return nextStates;
|
||||
/**
|
||||
* @returns {Number} - Score of current state
|
||||
*/
|
||||
evaluateState() {
|
||||
throw new Error('GameNode::evaluateState must be implemented');
|
||||
}
|
||||
}
|
||||
|
||||
|
145
src/algorithms/ai/TicTacToeGameNode.js
Normal file
145
src/algorithms/ai/TicTacToeGameNode.js
Normal file
@ -0,0 +1,145 @@
|
||||
import GameNode from './GameNode';
|
||||
|
||||
export const playerMark = 'x';
|
||||
export const opponentMark = 'o';
|
||||
class TicTacToeGameNode extends GameNode {
|
||||
/**
|
||||
*
|
||||
* @param {Array.<Array.<string>>} 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.<Array.<string>>} 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.<GameNode>} - 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;
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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]);
|
||||
|
Loading…
Reference in New Issue
Block a user