-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Minimax implementation #171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| (function (exports) { | ||
| 'use strict'; | ||
| /* 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. | ||
| */ | ||
| function minimax( | ||
| state, | ||
| maximize, | ||
| depth, | ||
| alpha, | ||
| beta, | ||
| getPossibleNextStatesFn, | ||
| isGameOverFn, | ||
| getScoreFn | ||
| ) { | ||
| if (depth === 0 || isGameOverFn(state)) { | ||
| const score = getScoreFn(state); | ||
| return {score, move: null}; | ||
| } | ||
|
|
||
| const possibleMoveResults = getPossibleNextStatesFn(state); | ||
|
|
||
| if (maximize) { | ||
|
|
||
| let maxResult = {score: -Infinity, move: null}; | ||
|
|
||
| for (const next of possibleMoveResults) { | ||
| const result = minimax( | ||
| next.state, | ||
| false, | ||
| depth - 1, | ||
| alpha, | ||
| beta, | ||
| getPossibleNextStatesFn, | ||
| isGameOverFn, | ||
| getScoreFn | ||
| ); | ||
|
|
||
| if (result.score > maxResult.score) { | ||
| maxResult = {score: result.score, move: next.move}; | ||
| } | ||
|
|
||
| alpha = Math.max(alpha, result.score); | ||
|
|
||
| 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}; | ||
| } | ||
|
|
||
| beta = Math.min(beta, result.score); | ||
|
|
||
| if (beta <= alpha) { | ||
|
||
| break; | ||
| } | ||
| } | ||
|
|
||
| return minResult; | ||
| } | ||
| } | ||
|
|
||
krzysztof-grzybek marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| exports.minimax = minimax; | ||
|
|
||
| })(typeof window === 'undefined' ? module.exports : window); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| var minimax = require('../../src/others/minimax.js').minimax; | ||
|
|
||
| describe('Minimax with tic tac toe', function () { | ||
|
||
| 'use strict'; | ||
|
|
||
| let game = ticTacToe(); | ||
|
|
||
| function getAllNextStates(state) { | ||
| const possibleMoves = game.emptyCells(state); | ||
|
|
||
| return possibleMoves.map(move => ({ | ||
| move, | ||
| state: game.nextState(state, move), | ||
| })); | ||
| } | ||
|
|
||
| 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 | ||
| ); | ||
| } | ||
|
|
||
| it('should be defined', function () { | ||
| expect(minimax).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); | ||
| } else { | ||
| const move = game.emptyCells(state)[0]; | ||
| state = game.nextState(state, move); | ||
| } | ||
| } | ||
|
|
||
| expect(game.isGameOver(state)).toBe(true); | ||
| expect(game.getScore(state)).toEqual({x: 1, o: 0}); | ||
| }); | ||
|
|
||
| it('should win versus dumb agent as second player', function () { | ||
| let state = game.newState('x'); | ||
|
|
||
| while (!game.isGameOver(state)) { | ||
| if (state.turn === 'o') { | ||
| state = game.nextState(state, minimaxFor(state.turn, state).move); | ||
| } else { | ||
| const move = game.emptyCells(state)[0]; | ||
| state = game.nextState(state, move); | ||
| } | ||
| } | ||
|
|
||
| expect(game.isGameOver(state)).toBe(true); | ||
| expect(game.getScore(state)).toEqual({x: 0, o: 1}); | ||
| }); | ||
|
|
||
|
|
||
| it('should be a tie for two minimax agents', function () { | ||
| let state = game.newState('x'); | ||
|
|
||
| while (!game.isGameOver(state)) { | ||
| state = game.nextState(state, minimaxFor(state.turn, state).move); | ||
| } | ||
| expect(game.isGameOver(state)).toBe(true); | ||
| expect(game.getScore(state)).toEqual({x: 0, o: 0}); | ||
| }); | ||
| }); | ||
|
|
||
| function ticTacToe() { | ||
| 'use strict'; | ||
|
|
||
| function newState(turn) { | ||
| return { | ||
| board: [[0, 0, 0], | ||
| [0, 0, 0], | ||
| [0, 0, 0]], | ||
| turn | ||
| }; | ||
| } | ||
|
|
||
| function emptyCells(state) { | ||
| const result = []; | ||
| state.board.forEach((row, y) => { | ||
| row.forEach((cell, x) => { | ||
| if (cell === 0) { | ||
| result.push({x, y}) | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| function getWinner(state) { | ||
| const winVariants = [ | ||
| [{x: 0, y: 0}, {x: 0, y: 1}, {x: 0, y: 2}], | ||
| [{x: 1, y: 0}, {x: 1, y: 1}, {x: 1, y: 2}], | ||
| [{x: 2, y: 0}, {x: 2, y: 1}, {x: 2, y: 2}], | ||
|
|
||
| [{x: 0, y: 0}, {x: 1, y: 0}, {x: 2, y: 0}], | ||
| [{x: 0, y: 1}, {x: 1, y: 1}, {x: 2, y: 1}], | ||
| [{x: 0, y: 2}, {x: 1, y: 0}, {x: 2, y: 2}], | ||
|
|
||
| [{x: 0, y: 0}, {x: 1, y: 1}, {x: 2, y: 2}], | ||
| [{x: 2, y: 0}, {x: 1, y: 1}, {x: 2, y: 0}], | ||
| ]; | ||
|
|
||
| for (const variant of winVariants) { | ||
| const combo = variant.map(cell => state.board[cell.y][cell.x]).join(''); | ||
| if (combo === 'xxx') { | ||
| return 'x'; | ||
| } else if (combo === 'ooo') { | ||
| return 'o'; | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| function allFieldsMarked(state) { | ||
| return state.board.every(row => row.every(cell => cell !== 0)); | ||
| } | ||
|
|
||
| function isGameOver(state) { | ||
| return allFieldsMarked(state) || getWinner(state) !== null; | ||
| } | ||
|
|
||
| function getScore(state) { | ||
| if (getWinner(state) === 'x') { | ||
| return {x: 1, o: 0}; | ||
| } else if (getWinner(state) === 'o') { | ||
| return {x: 0, o: 1}; | ||
| } | ||
|
|
||
| return {x: 0, o: 0}; | ||
| } | ||
|
|
||
| function nextState(state, move) { | ||
| const board = state.board; | ||
| 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) | ||
| ], | ||
| turn: state.turn === 'x' ? 'o' : 'x', | ||
| }; | ||
| } | ||
|
|
||
| return { | ||
| newState, | ||
| getScore, | ||
| nextState, | ||
| isGameOver, | ||
| emptyCells, | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are some parameters which don't change over time:
getPossibleNextStatesFnisGameOverFngetScoreFnWe can have a higher-order (builder) function, which accepts them and returns a new function, which does the rest:
This way we'll change the public API a bit:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great idea! Fixed.