Skip to content

Commit 01caa0d

Browse files
krzysztof-grzybekmgechev
andauthoredApr 27, 2020
Minimax implementation (mgechev#171)
* minimax * remove blank line Co-Authored-By: Minko Gechev <[email protected]> * cr fixes * simple game tests added Co-authored-by: Minko Gechev <[email protected]>
1 parent d534212 commit 01caa0d

File tree

3 files changed

+399
-2
lines changed

3 files changed

+399
-2
lines changed
 

‎src/others/minimax.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
(function (exports) {
2+
'use strict';
3+
/* eslint max-params: 0 */
4+
5+
/**
6+
* @param {Function} getPossibleNextStatesFn Function which returns all possible next moves with states .
7+
* @param {Function} isGameOverFn Function which returns if game is over.
8+
* @param {Function} getScoreFn Function which returns score.
9+
* @return {Function} minimax function
10+
*/
11+
function minimaxBuilder(
12+
getPossibleNextStatesFn,
13+
isGameOverFn,
14+
getScoreFn
15+
) {
16+
/**
17+
* Minimax (sometimes MinMax, MM[1] or saddle point[2]) is a decision rule used in artificial intelligence,
18+
* decision theory, game theory, statistics, and philosophy for minimizing the possible loss for a worst case (maximum loss) scenario.
19+
* Optimized with alpha-beta pruning.
20+
* {@link https://en.wikipedia.org/wiki/Minimax}
21+
* {@link https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning}
22+
*
23+
* @public
24+
* @module others/minimax
25+
*
26+
* @example
27+
*
28+
* var miniMax =
29+
* require('path-to-algorithms/src/others/minimax').minimax;
30+
* var result = minimax(
31+
* [1, 2, 3],
32+
* true,
33+
* 5,
34+
* -Infinity,
35+
* Infinity,
36+
* state => ({ move: 0, state: [2, 3, 4] }),
37+
* state => state[1] < 3,
38+
* state => state[1]
39+
* );
40+
*
41+
* @param {*} state Current game state
42+
* @param {Boolean} maximize Defines if the result should be maximized or minimized
43+
* @param {Number} depth Defines the maximum depth search
44+
* @param {Number} alpha Maximum score that the minimizing player is assured
45+
* @param {Number} beta Minimum score that the maximizing player is assured
46+
* @return {{score: Number, move: *}} which contains the minimum coins from the given
47+
* list, required for the change.
48+
*/
49+
const minimax = (
50+
state,
51+
maximize,
52+
depth,
53+
alpha,
54+
beta
55+
) => {
56+
if (depth === 0 || isGameOverFn(state)) {
57+
const score = getScoreFn(state);
58+
return {score, move: null};
59+
}
60+
61+
const possibleMoveResults = getPossibleNextStatesFn(state);
62+
63+
if (maximize) {
64+
65+
let maxResult = {score: -Infinity, move: null};
66+
67+
for (const next of possibleMoveResults) {
68+
const result = minimax(
69+
next.state,
70+
false,
71+
depth - 1,
72+
alpha,
73+
beta,
74+
);
75+
76+
if (result.score > maxResult.score) {
77+
maxResult = {score: result.score, move: next.move};
78+
}
79+
80+
alpha = Math.max(alpha, result.score);
81+
82+
if (alpha >= beta) {
83+
break;
84+
}
85+
}
86+
87+
return maxResult;
88+
} else {
89+
let minResult = {score: Infinity, move: null};
90+
91+
for (const next of possibleMoveResults) {
92+
const result = minimax(
93+
next.state,
94+
true,
95+
depth - 1,
96+
alpha,
97+
beta,
98+
);
99+
100+
if (result.score < minResult.score) {
101+
minResult = {score: result.score, move: next.move};
102+
}
103+
104+
beta = Math.min(beta, result.score);
105+
106+
if (beta <= alpha) {
107+
break;
108+
}
109+
}
110+
111+
return minResult;
112+
}
113+
}
114+
115+
return minimax;
116+
}
117+
118+
exports.minimaxBuilder = minimaxBuilder;
119+
120+
})(typeof window === 'undefined' ? module.exports : window);

‎test/data-structures/avl-tree.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,8 @@ describe('AVL Tree', function () {
178178
avlTree.insert(6);
179179
avlTree.remove(32);
180180
avlTree.remove(11);
181-
avlTree.remove(25);
182-
181+
avlTree.remove(25);
182+
183183
// depth 1
184184
expect(avlTree._root.value).toBe(37);
185185
expect(avlTree._root._height).toBe(4);

‎test/others/minimax.spec.js

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
const minimaxBuilder = require('../../src/others/minimax.js').minimaxBuilder;
2+
3+
describe('Minimax', function () {
4+
'use strict';
5+
6+
it('builder should be defined', function () {
7+
expect(minimaxBuilder).toBeDefined();
8+
});
9+
10+
describe('with tic tac toe', function () {
11+
let game = ticTacToe();
12+
13+
function getAllNextStates(state) {
14+
const possibleMoves = game.emptyCells(state);
15+
16+
return possibleMoves.map(move => ({
17+
move,
18+
state: game.nextState(state, move),
19+
}));
20+
}
21+
22+
const minimaxForX = minimaxBuilder(
23+
getAllNextStates,
24+
state => game.isGameOver(state),
25+
state => game.getScore(state).x - game.getScore(state).o
26+
)
27+
28+
const minimaxForO = minimaxBuilder(
29+
getAllNextStates,
30+
state => game.isGameOver(state),
31+
state => game.getScore(state).o - game.getScore(state).x
32+
)
33+
34+
it('should win versus dumb agent as first player', function () {
35+
let state = game.newState('x');
36+
37+
while (!game.isGameOver(state)) {
38+
if (state.turn === 'x') {
39+
state = game.nextState(state, minimaxForX(state, true, 5, -Infinity, Infinity).move);
40+
} else {
41+
const move = game.emptyCells(state)[0];
42+
state = game.nextState(state, move);
43+
}
44+
}
45+
46+
expect(game.isGameOver(state)).toBe(true);
47+
expect(game.getScore(state)).toEqual({x: 1, o: 0});
48+
});
49+
50+
it('should win versus dumb agent as second player', function () {
51+
let state = game.newState('x');
52+
53+
while (!game.isGameOver(state)) {
54+
if (state.turn === 'o') {
55+
state = game.nextState(state, minimaxForO(state, true, 5, -Infinity, Infinity).move);
56+
} else {
57+
const move = game.emptyCells(state)[0];
58+
state = game.nextState(state, move);
59+
}
60+
}
61+
62+
expect(game.isGameOver(state)).toBe(true);
63+
expect(game.getScore(state)).toEqual({x: 0, o: 1});
64+
});
65+
66+
67+
it('should be a tie for two minimax agents', function () {
68+
let state = game.newState('x');
69+
70+
while (!game.isGameOver(state)) {
71+
if (state.turn === 'o') {
72+
state = game.nextState(state, minimaxForO(state, true, 5, -Infinity, Infinity).move);
73+
} else {
74+
state = game.nextState(state, minimaxForX(state, true, 5, -Infinity, Infinity).move);
75+
}
76+
}
77+
expect(game.isGameOver(state)).toBe(true);
78+
expect(game.getScore(state)).toEqual({x: 0, o: 0});
79+
});
80+
});
81+
82+
describe('with simple game', function () {
83+
let game = simpleGame();
84+
85+
const minimaxForA = minimaxBuilder(
86+
state => [true, false].map(move => ({ move, state: game.nextState(state, move)})),
87+
state => game.isGameOver(state),
88+
state => game.getScore(state).A - game.getScore(state).B
89+
);
90+
const minimaxForB = minimaxBuilder(
91+
state => [true, false].map(move => ({ move, state: game.nextState(state, move)})),
92+
state => game.isGameOver(state),
93+
state => game.getScore(state).B - game.getScore(state).A
94+
);
95+
96+
it('should win versus dumb agent as a first player', function () {
97+
/* o
98+
/ \
99+
o o
100+
/ \ / \
101+
o o o o
102+
/ \ / \ / \ / \
103+
-1 1 1 1 1 -1 1 -1
104+
*/
105+
const binaryTree = [0, 0, 0, 0, 0, 0, 0, -1, 1, 1, 1, 1, -1, 1, -1];
106+
let state = game.newState(binaryTree);
107+
108+
while (!game.isGameOver(state)) {
109+
if (state.turn === 'A') {
110+
state = game.nextState(state, minimaxForA(state, true, 5, -Infinity, Infinity).move);
111+
} else {
112+
state = game.nextState(state, false);
113+
}
114+
}
115+
116+
expect(game.isGameOver(state)).toBe(true);
117+
expect(game.getScore(state)).toEqual({A: 1, B: -1});
118+
});
119+
120+
it('should win versus dumb agent as a second player', function () {
121+
/* o
122+
/ \
123+
o o
124+
/ \ / \
125+
-1 -1 -1 1
126+
*/
127+
const binaryTree = [0, 0, 0, -1, -1, -1, 1];
128+
let state = game.newState(binaryTree);
129+
130+
while (!game.isGameOver(state)) {
131+
if (state.turn === 'B') {
132+
state = game.nextState(state, minimaxForB(state, true, 5, -Infinity, Infinity).move);
133+
} else {
134+
state = game.nextState(state, false);
135+
}
136+
}
137+
138+
expect(game.isGameOver(state)).toBe(true);
139+
expect(game.getScore(state)).toEqual({A: -1, B: 1});
140+
});
141+
});
142+
});
143+
144+
function ticTacToe() {
145+
'use strict';
146+
147+
function newState(turn) {
148+
return {
149+
board: [[0, 0, 0],
150+
[0, 0, 0],
151+
[0, 0, 0]],
152+
turn
153+
};
154+
}
155+
156+
function emptyCells(state) {
157+
const result = [];
158+
state.board.forEach((row, y) => {
159+
row.forEach((cell, x) => {
160+
if (cell === 0) {
161+
result.push({x, y})
162+
}
163+
});
164+
});
165+
166+
return result;
167+
}
168+
169+
function getWinner(state) {
170+
const winVariants = [
171+
[{x: 0, y: 0}, {x: 0, y: 1}, {x: 0, y: 2}],
172+
[{x: 1, y: 0}, {x: 1, y: 1}, {x: 1, y: 2}],
173+
[{x: 2, y: 0}, {x: 2, y: 1}, {x: 2, y: 2}],
174+
175+
[{x: 0, y: 0}, {x: 1, y: 0}, {x: 2, y: 0}],
176+
[{x: 0, y: 1}, {x: 1, y: 1}, {x: 2, y: 1}],
177+
[{x: 0, y: 2}, {x: 1, y: 0}, {x: 2, y: 2}],
178+
179+
[{x: 0, y: 0}, {x: 1, y: 1}, {x: 2, y: 2}],
180+
[{x: 2, y: 0}, {x: 1, y: 1}, {x: 2, y: 0}],
181+
];
182+
183+
for (const variant of winVariants) {
184+
const combo = variant.map(cell => state.board[cell.y][cell.x]).join('');
185+
if (combo === 'xxx') {
186+
return 'x';
187+
} else if (combo === 'ooo') {
188+
return 'o';
189+
}
190+
}
191+
192+
return null;
193+
}
194+
195+
function allFieldsMarked(state) {
196+
return state.board.every(row => row.every(cell => cell !== 0));
197+
}
198+
199+
function isGameOver(state) {
200+
return allFieldsMarked(state) || getWinner(state) !== null;
201+
}
202+
203+
function getScore(state) {
204+
if (getWinner(state) === 'x') {
205+
return {x: 1, o: 0};
206+
} else if (getWinner(state) === 'o') {
207+
return {x: 0, o: 1};
208+
}
209+
210+
return {x: 0, o: 0};
211+
}
212+
213+
function nextState(state, move) {
214+
const newBoard = state.board.map(row => row.slice());
215+
newBoard[move.y][move.x] = state.turn;
216+
return {
217+
board: newBoard,
218+
turn: state.turn === 'x' ? 'o' : 'x',
219+
};
220+
}
221+
222+
return {
223+
newState,
224+
getScore,
225+
nextState,
226+
isGameOver,
227+
emptyCells,
228+
}
229+
}
230+
231+
232+
/* A simple game made for the purpose of minimax testing. The game has a binary tree with end values: 1 for player A win and -1 for player B win.
233+
Game starts from the root node and each player has a binary choose - "false" moves to the left child and "true" moves to the right child.
234+
The game ends when the very bottom leaf is reached.
235+
o
236+
/ \
237+
o o
238+
/ \ / \
239+
1 -1 -1 -1
240+
*/
241+
function simpleGame() {
242+
'use strict';
243+
244+
function newState(binaryTree) {
245+
return {
246+
turn: 'A',
247+
tree: binaryTree,
248+
position: 0,
249+
};
250+
}
251+
252+
function nextState(state, move) {
253+
return {
254+
tree: state.tree,
255+
position: move ? state.position * 2 + 2 : state.position * 2 + 1,
256+
turn: state.turn === 'A' ? 'B' : 'A',
257+
};
258+
}
259+
260+
function isGameOver(state) {
261+
return state.tree[state.position] !== 0;
262+
}
263+
264+
function getScore(state) {
265+
return {
266+
A: state.tree[state.position],
267+
B: state.tree[state.position] === 0 ? 0 : -state.tree[state.position],
268+
}
269+
}
270+
271+
return {
272+
newState,
273+
nextState,
274+
isGameOver,
275+
getScore,
276+
}
277+
}

0 commit comments

Comments
 (0)
Please sign in to comment.