diff --git a/core/src/__tests__/__snapshots__/humanVsAi.ts.snap b/core/src/__tests__/__snapshots__/humanVsAi.ts.snap new file mode 100644 index 0000000..253c082 --- /dev/null +++ b/core/src/__tests__/__snapshots__/humanVsAi.ts.snap @@ -0,0 +1,119 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`PlayerHuman vs PlayerAi > Issue #3 1`] = ` +[ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + [ + 0, + 0, + 0, + 1, + 0, + 0, + 0, + ], + [ + 0, + 0, + 0, + 1, + 0, + 1, + 0, + ], + [ + 0, + 0, + 0, + 2, + 1, + 2, + 1, + ], + [ + 0, + 0, + 0, + 1, + 2, + 2, + 2, + ], + [ + 0, + 0, + 0, + 1, + 2, + 2, + 1, + ], +] +`; + +exports[`PlayerHuman vs PlayerAi > Issue #3 2`] = ` +[ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + [ + 0, + 0, + 0, + 1, + 0, + 0, + 0, + ], + [ + 0, + 0, + 0, + 1, + 0, + 1, + 2, + ], + [ + 0, + 0, + 0, + 2, + 1, + 2, + 1, + ], + [ + 0, + 0, + 0, + 1, + 2, + 2, + 2, + ], + [ + 0, + 0, + 0, + 1, + 2, + 2, + 1, + ], +] +`; diff --git a/core/src/__tests__/humanVsAi.ts b/core/src/__tests__/humanVsAi.ts new file mode 100644 index 0000000..e205dde --- /dev/null +++ b/core/src/__tests__/humanVsAi.ts @@ -0,0 +1,98 @@ +import { TestGame } from '../__testHelpers/test-game' +import { BoardPiece, BoardBase } from '../board' +import { TestPlayer } from '../__testHelpers/test-player' +import { clone } from '../utils' +import { describe, test, expect } from 'vitest' +import { PlayerAi } from '../player' + +describe('PlayerHuman vs PlayerAi', () => { + const testPlayer = new TestPlayer(BoardPiece.PLAYER_1) + const aiPlayer = new PlayerAi(BoardPiece.PLAYER_2) + const players = [testPlayer, aiPlayer] + test('Issue #3', async () => { + const board = new BoardBase() + const game = new TestGame(players, board) + + /* + Next move is Player 2 (AI) + + Current board: + + 0 0 0 0 0 0 0 + 0 0 0 1 0 0 0 + 0 0 0 1 0 1 0 + 0 0 0 2 1 2 1 + 0 0 0 1 2 2 2 + 0 0 0 1 2 2 1 + + Should probably choose column 2 to block immediate Player 1 from winning + */ + board.map = [ + [ + BoardPiece.EMPTY, + BoardPiece.EMPTY, + BoardPiece.EMPTY, + BoardPiece.EMPTY, + BoardPiece.EMPTY, + BoardPiece.EMPTY, + BoardPiece.EMPTY, + ], + [ + BoardPiece.EMPTY, + BoardPiece.EMPTY, + BoardPiece.EMPTY, + BoardPiece.PLAYER_1, + BoardPiece.EMPTY, + BoardPiece.EMPTY, + BoardPiece.EMPTY, + ], + [ + BoardPiece.EMPTY, + BoardPiece.EMPTY, + BoardPiece.EMPTY, + BoardPiece.PLAYER_1, + BoardPiece.EMPTY, + BoardPiece.PLAYER_1, + BoardPiece.EMPTY, + ], + [ + BoardPiece.EMPTY, + BoardPiece.EMPTY, + BoardPiece.EMPTY, + BoardPiece.PLAYER_2, + BoardPiece.PLAYER_1, + BoardPiece.PLAYER_2, + BoardPiece.PLAYER_1, + ], + [ + BoardPiece.EMPTY, + BoardPiece.EMPTY, + BoardPiece.EMPTY, + BoardPiece.PLAYER_1, + BoardPiece.PLAYER_2, + BoardPiece.PLAYER_2, + BoardPiece.PLAYER_2, + ], + [ + BoardPiece.EMPTY, + BoardPiece.EMPTY, + BoardPiece.EMPTY, + BoardPiece.PLAYER_1, + BoardPiece.PLAYER_2, + BoardPiece.PLAYER_2, + BoardPiece.PLAYER_1, + ], + ] + + board.debug() + expect(board.map).toMatchSnapshot() + game.currentPlayerId = 1 + game.start() + board.debug() + console.log() + await game.afterMovePromise + + expect(board.map).toMatchSnapshot() + board.debug() + }) +}) diff --git a/core/src/player/player-ai.ts b/core/src/player/player-ai.ts index 3899d09..0bd9038 100644 --- a/core/src/player/player-ai.ts +++ b/core/src/player/player-ai.ts @@ -100,11 +100,6 @@ export class PlayerAi extends Player { ): number { const isWon = winnerBoardPiece === this.boardPiece const isLost = winnerBoardPiece === this.enemyBoardPiece - - // value is slightly higher than BIG_NEGATIVE_NUMBER & lower than BIG_POSITIVE_NUMBER - // so that minState(...) and maxState(...) could "catch"" this value and AI take this move - // This is just my hypothesis, I haven't tested without it yet. - // My point is that this AI implementation is basically a heuristic function :P if (isWon) { returnValue = BIG_POSITIVE_NUMBER - 100 } else if (isLost) { @@ -169,16 +164,17 @@ export class PlayerAi extends Player { } // alpha-beta pruning - if (value > beta) { - return { - value: value, - move: choose(moveQueue), - } - } + // if (value > beta) { + // return { + // value: value, + // move: choose(moveQueue), + // } + // } alpha = Math.max(alpha, value) } } + console.log('[maxState] depth', depth, value, moveQueue) return { value: value, move: choose(moveQueue), @@ -211,15 +207,17 @@ export class PlayerAi extends Player { } // alpha-beta pruning - if (value < alpha) { - return { - value: value, - move: choose(moveQueue), - } - } + // if (value < alpha) { + // return { + // value: value, + // move: choose(moveQueue), + // } + // } beta = Math.min(beta, value) } } + + console.log('[minState] depth', depth, value, moveQueue) return { value: value, move: choose(moveQueue), diff --git a/core/src/player/player.ts b/core/src/player/player.ts index 956bdb9..9344e05 100644 --- a/core/src/player/player.ts +++ b/core/src/player/player.ts @@ -2,6 +2,7 @@ import { BoardBase, BoardPiece } from '../board' export abstract class Player { boardPiece: BoardPiece + /** @return {number} column number (0-index) */ abstract getAction(board: BoardBase): Promise<number> constructor(boardPiece: BoardPiece) { this.boardPiece = boardPiece diff --git a/core/src/utils.ts b/core/src/utils.ts index ad7ee78..ecf54fb 100644 --- a/core/src/utils.ts +++ b/core/src/utils.ts @@ -37,12 +37,12 @@ export function getRandomColumnNumber(): number { return Math.floor(Math.random() * BoardBase.COLUMNS) } -export function choose(choice: Array<any>): any { +export function choose<T>(choice: Array<T>): T { return choice[Math.floor(Math.random() * choice.length)] } -export function clone(array: Array<Array<any>>): Array<Array<any>> { - const arr: Array<Array<any>> = [] +export function clone<T>(array: Array<Array<T>>): Array<Array<T>> { + const arr: Array<Array<T>> = [] for (let i: number = 0; i < array.length; i++) arr[i] = array[i].slice() @@ -57,7 +57,7 @@ export function getMockPlayerAction( success: boolean map: Array<Array<number>> } { - const clonedMap: Array<Array<any>> = clone(map) + const clonedMap: Array<Array<number>> = clone(map) if ( clonedMap[0][column] !== BoardPiece.EMPTY ||