mirror of
https://github.moeyy.xyz/https://github.com/trekhleb/javascript-algorithms.git
synced 2024-09-20 07:43:04 +08:00
Fix minimax bug (#3)
Changes findBestMove and minimax to be correct regardless of player associated with a game state. Adds to the GameNode interface a required method getMove, which should return the move which caused the game state defined by the GameNode. Adds a test which exemplifies the previous issue and a few tests checking the functionality of minimax when used on a Tic Tac Toe game.
This commit is contained in:
parent
d0cc1029ec
commit
b8a5d0d7fc
@ -13,6 +13,13 @@ class GameNode {
|
|||||||
throw new Error('GameNode::isTerminalState must be implemented');
|
throw new Error('GameNode::isTerminalState must be implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Object} - Get the move which caused this game state
|
||||||
|
*/
|
||||||
|
getMove() {
|
||||||
|
throw new Error('GameNode::getMove must be implemented');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {Array.<GameNode>} - Possible next states of the game tree
|
* @returns {Array.<GameNode>} - Possible next states of the game tree
|
||||||
*/
|
*/
|
||||||
|
@ -68,6 +68,13 @@ class TicTacToeGameNode extends GameNode {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Object} - Get the move which caused this game state
|
||||||
|
*/
|
||||||
|
getMove() {
|
||||||
|
return this.move;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @returns {string} - If the game is end with three consecutive mark on the board.
|
* @returns {string} - If the game is end with three consecutive mark on the board.
|
||||||
|
@ -5,22 +5,17 @@ class MinimaxPlayer extends Player {
|
|||||||
* Find the best move to perform in the current game state.
|
* Find the best move to perform in the current game state.
|
||||||
* @param {GameNode} node - Description of game state
|
* @param {GameNode} node - Description of game state
|
||||||
*/
|
*/
|
||||||
findBestMove(node) {
|
findBestMove(node, depth = 5) {
|
||||||
let bestScore = Number.NEGATIVE_INFINITY;
|
// Default depth set low
|
||||||
let bestMove = null;
|
|
||||||
|
|
||||||
const nextNodes = node.computeNextStates();
|
let [_, best_node] = this.minimax(node, depth);
|
||||||
for (let i = 0; i < nextNodes.length; i += 1) {
|
|
||||||
const nextNode = nextNodes[i];
|
|
||||||
const score = this.minimax(nextNode, 10);
|
|
||||||
|
|
||||||
if (score > bestScore) {
|
// If current state is terminal or depth == 0, then no move is possible
|
||||||
bestScore = score;
|
if (best_node === null) {
|
||||||
bestMove = nextNode.move;
|
return null;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return bestMove;
|
return best_node.getMove();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,32 +29,36 @@ class MinimaxPlayer extends Player {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {*} node - Description of game state
|
* @param {GameNode} node - Description of game state
|
||||||
* @param {Number} depth - Limit to the search depth
|
* @param {Number} depth - Limit to the search depth
|
||||||
* @returns {Number} - Best score the player can reach under this state
|
* @returns {Number} - Best score the player can reach under this state
|
||||||
*/
|
*/
|
||||||
minimax(node, depth) {
|
minimax(node, depth) {
|
||||||
if (depth === 0 || node.isTerminalState()) {
|
if (depth === 0 || node.isTerminalState()) {
|
||||||
return this.heuristic(node);
|
return [this.heuristic(node), null];
|
||||||
}
|
}
|
||||||
|
|
||||||
let optimal = node.isOpponentPlaying() ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
|
let optimal = node.isOpponentPlaying() ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
|
||||||
|
let optimal_node = null
|
||||||
const nextNodes = node.computeNextStates();
|
const nextNodes = node.computeNextStates();
|
||||||
|
|
||||||
for (let i = 0; i < nextNodes.length; i += 1) {
|
for (let i = 0; i < nextNodes.length; i += 1) {
|
||||||
const nextNode = nextNodes[i];
|
const nextNode = nextNodes[i];
|
||||||
const score = this.minimax(nextNode, depth - 1);
|
const [score, _] = this.minimax(nextNode, depth - 1);
|
||||||
|
|
||||||
if (node.isOpponentPlaying()) {
|
if (node.isOpponentPlaying()) {
|
||||||
if (score < optimal) {
|
if (score < optimal) {
|
||||||
optimal = score;
|
optimal = score;
|
||||||
|
optimal_node = nextNode;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (score > optimal) {
|
if (score > optimal) {
|
||||||
optimal = score;
|
optimal = score;
|
||||||
|
optimal_node = nextNode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return optimal;
|
return [optimal, optimal_node];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,16 +1,64 @@
|
|||||||
import TicTacToeGameNode, { playerMark } from '../../TicTacToeGameNode';
|
import TicTacToeGameNode, { opponentMark, playerMark } from '../../TicTacToeGameNode';
|
||||||
import Minimax from '../Minimax';
|
import Minimax from '../Minimax';
|
||||||
|
|
||||||
describe('Minimax', () => {
|
describe('Minimax', () => {
|
||||||
it('make decision under a normal state', () => {
|
it('player makes right decision under a normal state', () => {
|
||||||
const board = [
|
const board = [
|
||||||
['x', 'o', 'x'],
|
['x', 'o', 'x'],
|
||||||
['o', 'o', 'x'],
|
['o', 'o', 'x'],
|
||||||
['_', '_', '_'],
|
['_', '_', '_'],
|
||||||
];
|
];
|
||||||
const initialNode = new TicTacToeGameNode(board, playerMark);
|
const initialNode = new TicTacToeGameNode(board, playerMark);
|
||||||
const player = new Minimax();
|
const minimax = new Minimax();
|
||||||
|
|
||||||
expect(player.findBestMove(initialNode)).toStrictEqual([2, 2]);
|
expect(minimax.findBestMove(initialNode)).toStrictEqual([2, 2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opponent makes right decision under a normal state', () => {
|
||||||
|
const board = [
|
||||||
|
['x', 'o', 'x'],
|
||||||
|
['o', 'o', 'x'],
|
||||||
|
['_', '_', '_'],
|
||||||
|
];
|
||||||
|
const initialNode = new TicTacToeGameNode(board, opponentMark);
|
||||||
|
const minimax = new Minimax();
|
||||||
|
|
||||||
|
expect(minimax.findBestMove(initialNode)).toStrictEqual([2, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a finished game finds no moves', () => {
|
||||||
|
const board = [
|
||||||
|
['x', 'o', 'x'],
|
||||||
|
['o', 'o', 'x'],
|
||||||
|
['_', 'o', '_'],
|
||||||
|
];
|
||||||
|
const initialNode = new TicTacToeGameNode(board, playerMark);
|
||||||
|
const minimax = new Minimax();
|
||||||
|
|
||||||
|
expect(minimax.findBestMove(initialNode)).toStrictEqual(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a game far from a terminating state finds moves', () => {
|
||||||
|
const board = [
|
||||||
|
['_', '_', '_'],
|
||||||
|
['_', '_', '_'],
|
||||||
|
['_', '_', '_'],
|
||||||
|
];
|
||||||
|
const initialNode = new TicTacToeGameNode(board, opponentMark);
|
||||||
|
const minimax = new Minimax();
|
||||||
|
|
||||||
|
expect(minimax.findBestMove(initialNode)).not.toStrictEqual(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a full game board finds no moves', () => {
|
||||||
|
const board = [
|
||||||
|
['x', 'o', 'o'],
|
||||||
|
['o', 'x', 'x'],
|
||||||
|
['x', 'x', 'o'],
|
||||||
|
];
|
||||||
|
const initialNode = new TicTacToeGameNode(board, opponentMark);
|
||||||
|
const minimax = new Minimax();
|
||||||
|
|
||||||
|
expect(minimax.findBestMove(initialNode)).toStrictEqual(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user