diff --git a/src/core/MancalaGame.ts b/src/core/MancalaGame.ts new file mode 100644 index 0000000..5b49546 --- /dev/null +++ b/src/core/MancalaGame.ts @@ -0,0 +1,177 @@ +import { Board, PitType } from './Board'; +import { GameRule } from './GameRule'; +import { Player } from './Player'; + +export type GameState = 'initial' | 'playing' | 'ended'; + +export class MancalaGame { + board: Board; + player1: Player; + player2: Player; + turnPlayerId: string; + state: GameState; + gameRules: GameRule[]; + + constructor( + board: Board, + player1: Player, + player2: Player, + turnPlayerId: string, + gameRules: GameRule[] + ) { + this.board = board; + this.player1 = player1; + this.player2 = player2; + this.turnPlayerId = turnPlayerId; + this.state = 'initial'; + this.gameRules = gameRules; + this.listenBoardMoveEvents(); + } + + listenBoardMoveEvents() { + this.board.onGameMoveStart = (index: number) => { + this.gameRules.forEach((gameRule) => { + gameRule.onGameMoveStart(this, index); + }); + }; + this.board.onGameMove = (index: number) => { + this.gameRules.forEach((gameRule) => { + gameRule.onGameMove(this, index); + }); + }; + this.board.onGameMoveEnd = (index: number) => { + this.gameRules.forEach((gameRule) => { + gameRule.onGameMoveEnd(this, index); + }); + }; + } + + changePlayerTurn() { + if (this.turnPlayerId === this.player1.id) { + this.turnPlayerId = this.player2.id; + } else { + this.turnPlayerId = this.player1.id; + } + } + + isTurnPlayer1() { + return this.player1.id === this.turnPlayerId; + } + + isTurnPlayer2() { + return this.player2.id === this.turnPlayerId; + } + + getPlayerByPitType(pitType: PitType): Player { + if (pitType === 'player1Pit' || pitType === 'player1Bank') { + return this.player1; + } else if (pitType === 'player2Pit' || pitType === 'player2Bank') { + return this.player2; + } else { + throw new Error('Unknown pit type : ' + pitType); + } + } + + getPlayerByIndex(index: number): Player { + const pitType = this.board.getPitTypeByIndex(index); + return this.getPlayerByPitType(pitType); + } + + checkIsPlayerTurnByIndex(index: number): boolean { + const player = this.getPlayerByIndex(index); + return this.checkIsPlayerTurn(player); + } + + getBoardIndexByPlayer(player: Player, pitIndex: number) { + if (this.player1.id === player.id) { + return this.board.player1PitStartIndex() + pitIndex; + } else if (this.player2.id === player.id) { + return this.board.player2PitStartIndex() + pitIndex; + } else { + return -1; // throwing an error might be better + } + } + + public checkIsPlayerTurn(player: Player) { + return player.id === this.turnPlayerId; + } + + public checkPitIndexForPlayer(player: Player, pitIndex: number) { + const foundPlayer = this.getPlayerByIndex( + this.getBoardIndexByPlayer(player, pitIndex) + ); + return player.id === foundPlayer.id; + } + + public checkIsPitIndexBank(player: Player, pitIndex: number) { + const pitType = this.board.getPitTypeByIndex( + this.getBoardIndexByPlayer(player, pitIndex) + ); + return pitType === 'player1Bank' || pitType === 'player2Bank'; + } + + public canPlayerMove(player: Player, pitIndex: number) { + const isPitIndexCorrect = this.checkPitIndexForPlayer(player, pitIndex); + const isPitIndexBank = this.checkIsPitIndexBank(player, pitIndex); + const isPlayerTurn = this.checkIsPlayerTurn(player); + return isPitIndexCorrect && !isPitIndexBank && isPlayerTurn; + } + + public moveByPlayerPit(player: Player, pitIndex: number) { + if (this.state === 'ended') return; + if (this.state === 'initial') { + this.state = 'playing'; + } + if (this.canPlayerMove(player, pitIndex)) { + this.board.move(this.getBoardIndexByPlayer(player, pitIndex)); + if (this.checkGameIsEnded()) { + this.state = 'ended'; + } + } else { + const isPitIndexCorrect = this.checkPitIndexForPlayer(player, pitIndex); + const isPlayerTurn = this.checkIsPlayerTurn(player); + throw new Error( + `Player cannot move reason : isPitIndexCorrect = ${isPitIndexCorrect} isPlayerTurn = ${isPlayerTurn}` + ); + } + } + + getPlayer1StoneCountInPits(): number { + return this.getPlayerStoneCountInPits( + this.board.player1PitStartIndex(), + this.board.player1BankIndex() + ); + } + + getPlayer2StoneCountInPits(): number { + return this.getPlayerStoneCountInPits( + this.board.player2PitStartIndex(), + this.board.player2BankIndex() + ); + } + + getPlayerStoneCountInPits( + playerPitStartIndex: number, + playerBankIndex: number + ): number { + const player2Pits = this.board.pits.slice( + playerPitStartIndex, + playerBankIndex + ); + return player2Pits + .map((pit) => pit.stoneCount) + .reduce( + (previousStoneCount, stoneCount) => previousStoneCount + stoneCount + ); + } + + checkGameIsEnded(): boolean { + if ( + this.getPlayer1StoneCountInPits() === 0 || + this.getPlayer2StoneCountInPits() === 0 + ) { + return true; + } + return false; + } +} diff --git a/tests/MancalaGame.test.ts b/tests/MancalaGame.test.ts new file mode 100644 index 0000000..fb56785 --- /dev/null +++ b/tests/MancalaGame.test.ts @@ -0,0 +1,109 @@ +import { GRLastStoneInBank } from '../src/common/game_rules/GRLastStoneInBank'; +import { GRLastStoneInEmptyPit } from '../src/common/game_rules/GRLastStoneInEmptyPit'; +import { Board } from '../src/core/Board'; +import { MancalaGame } from '../src/core/MancalaGame'; +import { Player } from '../src/core/Player'; + +function createGame(): MancalaGame { + const board = new Board(6, 4); + const player1 = new Player('0', 'player1'); + const player2 = new Player('1', 'player2'); + const game = new MancalaGame(board, player1, player2, player1.id, [ + new GRLastStoneInEmptyPit(), + new GRLastStoneInBank() + ]); + return game; +} + +describe('Game Test', () => { + test('test getPlayerByIndex', () => { + const game = createGame(); + const player1 = new Player('0', 'player1'); + const player2 = new Player('1', 'player2'); + expect(game.getPlayerByIndex(0)).toStrictEqual(player1); + expect(game.getPlayerByIndex(1)).toStrictEqual(player1); + expect(game.getPlayerByIndex(2)).toStrictEqual(player1); + expect(game.getPlayerByIndex(3)).toStrictEqual(player1); + expect(game.getPlayerByIndex(4)).toStrictEqual(player1); + expect(game.getPlayerByIndex(5)).toStrictEqual(player1); + expect(game.getPlayerByIndex(6)).toStrictEqual(player1); + expect(game.getPlayerByIndex(7)).toStrictEqual(player2); + expect(game.getPlayerByIndex(8)).toStrictEqual(player2); + expect(game.getPlayerByIndex(9)).toStrictEqual(player2); + expect(game.getPlayerByIndex(10)).toStrictEqual(player2); + expect(game.getPlayerByIndex(11)).toStrictEqual(player2); + expect(game.getPlayerByIndex(12)).toStrictEqual(player2); + expect(game.getPlayerByIndex(13)).toStrictEqual(player2); + }); + test('test canPlayerMove', () => { + const game = createGame(); + const player1 = new Player('0', 'player1'); + const player2 = new Player('1', 'player2'); + expect(game.canPlayerMove(player1, 0)).toBe(true); + expect(game.canPlayerMove(player1, 1)).toBe(true); + expect(game.canPlayerMove(player1, 2)).toBe(true); + expect(game.canPlayerMove(player1, 3)).toBe(true); + expect(game.canPlayerMove(player1, 4)).toBe(true); + expect(game.canPlayerMove(player1, 5)).toBe(true); + expect(game.canPlayerMove(player1, 6)).toBe(false); + expect(game.canPlayerMove(player2, 0)).toBe(false); + }); + + test('test moveByPlayerPit', () => { + const game = createGame(); + const player1 = new Player('0', 'player1'); + const player2 = new Player('1', 'player2'); + expect(game.state).toBe('initial'); + expect(game.turnPlayerId).toBe(player1.id); + + game.moveByPlayerPit(player1, 0); + expect(game.state).toBe('playing'); + expect(game.turnPlayerId).toBe(player2.id); + + game.moveByPlayerPit(player2, 0); + expect(game.state).toBe('playing'); + expect(game.turnPlayerId).toBe(player1.id); + }); + + test('test game end test', () => { + const game = createGame(); + const player1 = new Player('0', 'player1'); + const player2 = new Player('1', 'player2'); + expect(game.state).toBe('initial'); + game.board.pits[0].stoneCount = 0; + game.board.pits[1].stoneCount = 0; + game.board.pits[2].stoneCount = 0; + game.board.pits[3].stoneCount = 0; + game.board.pits[4].stoneCount = 0; + game.board.pits[5].stoneCount = 1; + game.moveByPlayerPit(player1, 5); + expect(game.state).toBe('ended'); + }); + + test('test last stone in bank', () => { + const game = createGame(); + const player1 = new Player('0', 'player1'); + const player2 = new Player('1', 'player2'); + expect(game.state).toBe('initial'); + expect(game.turnPlayerId).toBe(player1.id); + + game.moveByPlayerPit(player1, 3); + expect(game.state).toBe('playing'); + expect(game.turnPlayerId).toBe(player1.id); + }); + + test('test empty pit 1', () => { + const game = createGame(); + const player1 = new Player('0', 'player1'); + const player2 = new Player('1', 'player2'); + expect(game.state).toBe('initial'); + game.board.pits[0].stoneCount = 1; + game.board.pits[1].stoneCount = 0; + game.moveByPlayerPit(player1, 0); + expect(game.state).toBe('playing'); + expect(game.turnPlayerId).toBe(player2.id); + expect(game.board.getStoneArray()).toStrictEqual([ + 0, 0, 4, 4, 4, 4, 5, 4, 0, 4, 4, 4, 4, 0 + ]); + }); +});