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:
spelgubbe 2022-03-05 18:23:40 +01:00 committed by Cheney Ni
parent d0cc1029ec
commit b8a5d0d7fc
4 changed files with 81 additions and 20 deletions

View File

@ -13,6 +13,13 @@ class GameNode {
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
*/

View File

@ -68,6 +68,13 @@ class TicTacToeGameNode extends GameNode {
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.

View File

@ -5,22 +5,17 @@ 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;
findBestMove(node, depth = 5) {
// Default depth set low
const nextNodes = node.computeNextStates();
for (let i = 0; i < nextNodes.length; i += 1) {
const nextNode = nextNodes[i];
const score = this.minimax(nextNode, 10);
let [_, best_node] = this.minimax(node, depth);
if (score > bestScore) {
bestScore = score;
bestMove = nextNode.move;
}
// If current state is terminal or depth == 0, then no move is possible
if (best_node === null) {
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
* @returns {Number} - Best score the player can reach under this state
*/
minimax(node, depth) {
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 = null
const nextNodes = node.computeNextStates();
for (let i = 0; i < nextNodes.length; i += 1) {
const nextNode = nextNodes[i];
const score = this.minimax(nextNode, depth - 1);
const [score, _] = this.minimax(nextNode, depth - 1);
if (node.isOpponentPlaying()) {
if (score < optimal) {
optimal = score;
optimal_node = nextNode;
}
} else {
if (score > optimal) {
optimal = score;
optimal_node = nextNode;
}
}
}
return optimal;
return [optimal, optimal_node];
}
}

View File

@ -1,16 +1,64 @@
import TicTacToeGameNode, { playerMark } from '../../TicTacToeGameNode';
import TicTacToeGameNode, { opponentMark, playerMark } from '../../TicTacToeGameNode';
import Minimax from '../Minimax';
describe('Minimax', () => {
it('make decision under a normal state', () => {
it('player makes right decision under a normal state', () => {
const board = [
['x', 'o', 'x'],
['o', 'o', 'x'],
['_', '_', '_'],
];
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);
});
});