Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
cr fixes
  • Loading branch information
krzysztof-grzybek committed Apr 25, 2020
commit 4ae9ce070755b6825777cedf94ad1f18bff30979
182 changes: 92 additions & 90 deletions src/others/minimax.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,116 +3,118 @@
/* eslint max-params: 0 */

/**
* Minimax (sometimes MinMax, MM[1] or saddle point[2]) is a decision rule used in artificial intelligence,
* decision theory, game theory, statistics, and philosophy for minimizing the possible loss for a worst case (maximum loss) scenario.
* Optimized with alpha-beta pruning.
* {@link https://en.wikipedia.org/wiki/Minimax}
* {@link https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning}
*
* @public
* @module others/minimax
*
* @example
*
* var miniMax =
* require('path-to-algorithms/src/others/minimax').minimax;
* var result = minimax(
* [1, 2, 3],
* true,
* 5,
* -Infinity,
* Infinity,
* state => ({ move: 0, state: [2, 3, 4] }),
* state => state[1] < 3,
* state => state[1]
* );
*
* @param {*} state Current game state
* @param {Boolean} maximize Defines if the result should be maximized or minimized
* @param {Number} depth Defines the maximum depth search
* @param {Number} alpha Maximum score that the minimizing player is assured
* @param {Number} beta Minimum score that the maximizing player is assured
* @param {Function} getPossibleNextStatesFn Function which returns all possible next moves with states .
* @param {Function} isGameOverFn Function which returns if game is over.
* @param {Function} getScoreFn Function which returns score.
* @return {{score: Number, move: *}} which contains the minimum coins from the given
* list, required for the change.
* @param {Function} getPossibleNextStatesFn Function which returns all possible next moves with states .
* @param {Function} isGameOverFn Function which returns if game is over.
* @param {Function} getScoreFn Function which returns score.
* @return {Function} minimax function
*/
function minimax(
state,
maximize,
depth,
alpha,
beta,
function minimaxBuilder(
getPossibleNextStatesFn,
isGameOverFn,
getScoreFn
) {
if (depth === 0 || isGameOverFn(state)) {
const score = getScoreFn(state);
return {score, move: null};
}
/**
* Minimax (sometimes MinMax, MM[1] or saddle point[2]) is a decision rule used in artificial intelligence,
* decision theory, game theory, statistics, and philosophy for minimizing the possible loss for a worst case (maximum loss) scenario.
* Optimized with alpha-beta pruning.
* {@link https://en.wikipedia.org/wiki/Minimax}
* {@link https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning}
*
* @public
* @module others/minimax
*
* @example
*
* var miniMax =
* require('path-to-algorithms/src/others/minimax').minimax;
* var result = minimax(
* [1, 2, 3],
* true,
* 5,
* -Infinity,
* Infinity,
* state => ({ move: 0, state: [2, 3, 4] }),
* state => state[1] < 3,
* state => state[1]
* );
*
* @param {*} state Current game state
* @param {Boolean} maximize Defines if the result should be maximized or minimized
* @param {Number} depth Defines the maximum depth search
* @param {Number} alpha Maximum score that the minimizing player is assured
* @param {Number} beta Minimum score that the maximizing player is assured
* @return {{score: Number, move: *}} which contains the minimum coins from the given
* list, required for the change.
*/
const minimax = (
state,
maximize,
depth,
alpha,
beta
) => {
if (depth === 0 || isGameOverFn(state)) {
const score = getScoreFn(state);
return {score, move: null};
}

const possibleMoveResults = getPossibleNextStatesFn(state);
const possibleMoveResults = getPossibleNextStatesFn(state);

if (maximize) {
if (maximize) {

let maxResult = {score: -Infinity, move: null};
let maxResult = {score: -Infinity, move: null};

for (const next of possibleMoveResults) {
const result = minimax(
next.state,
false,
depth - 1,
alpha,
beta,
getPossibleNextStatesFn,
isGameOverFn,
getScoreFn
);
for (const next of possibleMoveResults) {
const result = minimax(
next.state,
false,
depth - 1,
alpha,
beta,
);

if (result.score > maxResult.score) {
maxResult = {score: result.score, move: next.move};
}
if (result.score > maxResult.score) {
maxResult = {score: result.score, move: next.move};
}

alpha = Math.max(alpha, result.score);
alpha = Math.max(alpha, result.score);

if (alpha >= beta) {
break;
if (alpha >= beta) {
break;
}
}
}

return maxResult;
} else {
let minResult = {score: Infinity, move: null};

for (const next of possibleMoveResults) {
const result = minimax(
next.state,
true,
depth - 1,
alpha,
beta,
getPossibleNextStatesFn,
isGameOverFn,
getScoreFn
);

if (result.score < minResult.score) {
minResult = {score: result.score, move: next.move};
}
return maxResult;
} else {
let minResult = {score: Infinity, move: null};

beta = Math.min(beta, result.score);
for (const next of possibleMoveResults) {
const result = minimax(
next.state,
true,
depth - 1,
alpha,
beta,
);

if (beta <= alpha) {
break;
if (result.score < minResult.score) {
minResult = {score: result.score, move: next.move};
}

beta = Math.min(beta, result.score);

if (beta <= alpha) {
break;
}
}
}

return minResult;
return minResult;
}
}

return minimax;
}

exports.minimax = minimax;
exports.minimaxBuilder = minimaxBuilder;

})(typeof window === 'undefined' ? module.exports : window);
50 changes: 23 additions & 27 deletions test/others/minimax.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
var minimax = require('../../src/others/minimax.js').minimax;
const minimaxBuilder = require('../../src/others/minimax.js').minimaxBuilder;

describe('Minimax with tic tac toe', function () {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a fantastic example! As you suggested in your comment, it's leaning a bit towards an integration test.

I think, having a simpler game is fine as well. This way we won't need that much logic to setup our tests and they'll be less error prone. Consider a game where each next step is a binary decision, and the leafs of the binary tree determine which player is the winner.

I really like this example, so I'd love to keep it, and also add another test group which tests the minimax example with a simpler binary game.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm glad you like it!
Sounds reasonable. I'm not sure if I follow the idea of the simpler game. What do you mean by the leafs of the binary tree determine which player is the winner? Should I create some binary tree with one "winning leaf"?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't have to be a real game, it can be any game. My proposal is for something like this:

     o
   /    \
  o      o
 /  \   /  \
1   -1 1   -1

If we get 1 player "A" wins, otherwise, if we get -1 player "B" wins. It's pretty much pre-defined, depends on who would start first. It'll be easier to reason about IMO.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I got it. Indeed, it's much simpler.

'use strict';
Expand All @@ -14,33 +14,28 @@ describe('Minimax with tic tac toe', function () {
}));
}

function minimaxFor(player, state) {
const getScoreFn = player === 'x'
? state => game.getScore(state).x - game.getScore(state).o
: state => game.getScore(state).o - game.getScore(state).x;

return minimax(
state,
true,
5,
-Infinity,
Infinity,
getAllNextStates,
state => game.isGameOver(state),
getScoreFn
);
}
const minimaxForX = minimaxBuilder(
getAllNextStates,
state => game.isGameOver(state),
state => game.getScore(state).x - game.getScore(state).o
)

const minimaxForO = minimaxBuilder(
getAllNextStates,
state => game.isGameOver(state),
state => game.getScore(state).o - game.getScore(state).x
)

it('should be defined', function () {
expect(minimax).toBeDefined();
expect(minimaxBuilder).toBeDefined();
});

it('should win versus dumb agent as first player', function () {
let state = game.newState('x');

while (!game.isGameOver(state)) {
if (state.turn === 'x') {
state = game.nextState(state, minimaxFor(state.turn, state).move);
state = game.nextState(state, minimaxForX(state, true, 5, -Infinity, Infinity).move);
} else {
const move = game.emptyCells(state)[0];
state = game.nextState(state, move);
Expand All @@ -56,7 +51,7 @@ describe('Minimax with tic tac toe', function () {

while (!game.isGameOver(state)) {
if (state.turn === 'o') {
state = game.nextState(state, minimaxFor(state.turn, state).move);
state = game.nextState(state, minimaxForO(state, true, 5, -Infinity, Infinity).move);
} else {
const move = game.emptyCells(state)[0];
state = game.nextState(state, move);
Expand All @@ -72,7 +67,11 @@ describe('Minimax with tic tac toe', function () {
let state = game.newState('x');

while (!game.isGameOver(state)) {
state = game.nextState(state, minimaxFor(state.turn, state).move);
if (state.turn === 'o') {
state = game.nextState(state, minimaxForO(state, true, 5, -Infinity, Infinity).move);
} else {
state = game.nextState(state, minimaxForX(state, true, 5, -Infinity, Infinity).move);
}
}
expect(game.isGameOver(state)).toBe(true);
expect(game.getScore(state)).toEqual({x: 0, o: 0});
Expand Down Expand Up @@ -149,13 +148,10 @@ function ticTacToe() {
}

function nextState(state, move) {
const board = state.board;
const newBoard = state.board.map(row => row.slice());
newBoard[move.y][move.x] = state.turn;
return {
board: [
...board.slice(0, move.y),
[...board[move.y].slice(0, move.x), state.turn, ...board[move.y].slice(move.x + 1)],
...board.slice(move.y + 1)
],
board: newBoard,
turn: state.turn === 'x' ? 'o' : 'x',
};
}
Expand Down