mirror of
https://github.moeyy.xyz/https://github.com/trekhleb/javascript-algorithms.git
synced 2024-11-10 11:09:43 +08:00
Complete minimax algorithm and relevant classes
This commit is contained in:
parent
33695ce766
commit
a41a2747ce
141
src/algorithms/ai/GameNode.js
Normal file
141
src/algorithms/ai/GameNode.js
Normal file
@ -0,0 +1,141 @@
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} - If the game is terminated
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nextStates;
|
||||
}
|
||||
}
|
||||
|
||||
export default GameNode;
|
@ -1,58 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* @param {*} node description of game state
|
||||
* @param {Number} depth limit to the search depth
|
||||
* @param {Number} player player id, either 0 or 1
|
||||
* @returns game state resulting from the best found move
|
||||
*/
|
||||
function minimax(node, depth, player) {
|
||||
if (depth === 0 || terminal_state(node)) {
|
||||
return this.heuristic(node, player), node; // pretend there is a heuristic function supplies
|
||||
}
|
||||
opt = player == 0 ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
|
||||
|
||||
opt_node = undefined;
|
||||
|
||||
for (const child in node.possibleStates()) { // pretend there is node.possibleStates
|
||||
nextPlayer = player === 1 ? 0 : 1;
|
||||
|
||||
value, _ = minimax(move, depth - 1, nextPlayer);
|
||||
|
||||
if (player === 0) {
|
||||
if (value < optimal) {
|
||||
optimal = value;
|
||||
opt_node = child;
|
||||
}
|
||||
} else {
|
||||
if (value > optimal) {
|
||||
optimal = value;
|
||||
opt_node = child;
|
||||
}
|
||||
}
|
||||
}
|
||||
return optimal, opt_node;
|
||||
}
|
||||
|
||||
/* inspired by Python...
|
||||
|
||||
def minimax(self, node: Node, depth: int, player: int):
|
||||
if depth == 0 or self.is_terminal_state(node):
|
||||
return self.h(node, player), node
|
||||
|
||||
opt = math.inf if (player != 0) else -math.inf
|
||||
opt_node = None
|
||||
for child in node.compute_and_get_children():
|
||||
# test all possible moves
|
||||
h, _ = self.minimax(child, depth-1, player ^ 1)
|
||||
if player != 0:
|
||||
if h > opt:
|
||||
opt = h
|
||||
opt_node = child
|
||||
else:
|
||||
if h < opt:
|
||||
opt = h
|
||||
opt_node = child
|
||||
|
||||
return opt, opt_node
|
||||
|
||||
*/
|
8
src/algorithms/ai/Player.js
Normal file
8
src/algorithms/ai/Player.js
Normal file
@ -0,0 +1,8 @@
|
||||
export default class Player {
|
||||
/**
|
||||
* @param {GameNode} node One node of the game tree. Description of a game state
|
||||
*/
|
||||
findBestMove() {
|
||||
throw new Error('Player::findBestMove must be implemented');
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import Minimax from '../Minimax';
|
||||
|
||||
describe('Minimax', () => {
|
||||
|
||||
if('exists', () => {
|
||||
expect(true);
|
||||
});
|
||||
|
||||
});
|
66
src/algorithms/ai/minimax/Minimax.js
Normal file
66
src/algorithms/ai/minimax/Minimax.js
Normal file
@ -0,0 +1,66 @@
|
||||
import Player from '../Player';
|
||||
|
||||
class MinimaxPlayer extends Player {
|
||||
/**
|
||||
* Find the best move to perform in the current game state.
|
||||
* @param {GameNode} node - Description of game state
|
||||
*/
|
||||
findBestMove(node) {
|
||||
let bestScore = Number.NEGATIVE_INFINITY;
|
||||
let bestMove = null;
|
||||
|
||||
const nextNodes = node.computeNextStates();
|
||||
for (let i = 0; i < nextNodes.length; i += 1) {
|
||||
const nextNode = nextNodes[i];
|
||||
const score = this.minimax(nextNode, 10);
|
||||
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestMove = nextNode.move;
|
||||
}
|
||||
}
|
||||
|
||||
return bestMove;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the heuristic value of current state.
|
||||
* @param {GameNode} node - Description of game state
|
||||
* @returns
|
||||
*/
|
||||
heuristic(node) {
|
||||
return node.evaluate();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} node - Description of game state
|
||||
* @param {Number} depth - Limit to the search depth
|
||||
* @returns {Number} - Best score the player can reach under this state
|
||||
*/
|
||||
minimax(node, depth) {
|
||||
if (depth === 0 || node.isTerminated()) {
|
||||
return this.heuristic(node);
|
||||
}
|
||||
|
||||
let optimal = node.isOpponentPlaying() ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
|
||||
const nextNodes = node.computeNextStates();
|
||||
for (let i = 0; i < nextNodes.length; i += 1) {
|
||||
const nextNode = nextNodes[i];
|
||||
const score = this.minimax(nextNode, depth - 1);
|
||||
|
||||
if (node.isOpponentPlaying()) {
|
||||
if (score < optimal) {
|
||||
optimal = score;
|
||||
}
|
||||
} else {
|
||||
if (score > optimal) {
|
||||
optimal = score;
|
||||
}
|
||||
}
|
||||
}
|
||||
return optimal;
|
||||
}
|
||||
}
|
||||
|
||||
export default MinimaxPlayer;
|
16
src/algorithms/ai/minimax/__test__/Minimax.test.js
Normal file
16
src/algorithms/ai/minimax/__test__/Minimax.test.js
Normal file
@ -0,0 +1,16 @@
|
||||
import GameNode, { playerMark } from '../../GameNode';
|
||||
import Minimax from '../Minimax';
|
||||
|
||||
describe('Minimax', () => {
|
||||
it('make decision under a normal state', () => {
|
||||
const board = [
|
||||
['x', 'o', 'x'],
|
||||
['o', 'o', 'x'],
|
||||
['_', '_', '_'],
|
||||
];
|
||||
const initialNode = new GameNode(board, playerMark);
|
||||
const player = new Minimax();
|
||||
|
||||
expect(player.findBestMove(initialNode)).toStrictEqual([2, 2]);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user