Merge pull request #3 from jhalitaksoy/feature/game-step

Feature/game step
This commit is contained in:
Halit Aksoy 2022-05-12 23:05:37 +03:00 committed by GitHub
commit c645f62e91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 2431 additions and 2269 deletions

View File

@ -1,17 +1,52 @@
import { Board } from '../../core/Board';
import { GameRule } from '../../core/GameRule'; import { GameRule } from '../../core/GameRule';
import { GameStep } from '../../core/HistoryItem';
import { MancalaGame } from '../../core/MancalaGame'; import { MancalaGame } from '../../core/MancalaGame';
export const GAME_STEP_BOARD_CLEARED = 'GAME_STEP_BOARD_CLEARED';
export type ClearBoardAtEndData = { pitIndexesThatHasStone: number[] };
export class GRClearBoardAtEnd implements GameRule { export class GRClearBoardAtEnd implements GameRule {
onGameMoveStart(game: MancalaGame, index: number): void {} onGameMoveStart(game: MancalaGame, index: number): void {}
onGameMove(game: MancalaGame, index: number): void {} onGameMove(game: MancalaGame, index: number): void {}
onGameMoveEnd(game: MancalaGame, index: number): void { onGameMoveEnd(game: MancalaGame, index: number): void {
if (game.getPlayer1StoneCountInPits() === 0) { if (game.getPlayer1StoneCountInPits() === 0) {
const clearBoardAtEndData = {
pitIndexesThatHasStone: this.getPitIndexesThatHasStone(game.board)
};
game.board.player1Bank.stoneCount += game.getPlayer2StoneCountInPits(); game.board.player1Bank.stoneCount += game.getPlayer2StoneCountInPits();
game.board.clearPlayer2Pits(); game.board.clearPlayer2Pits();
} this.addGameStep(game, index, clearBoardAtEndData);
if (game.getPlayer2StoneCountInPits() === 0) { } else if (game.getPlayer2StoneCountInPits() === 0) {
const clearBoardAtEndData = {
pitIndexesThatHasStone: this.getPitIndexesThatHasStone(game.board)
};
game.board.player2Bank.stoneCount += game.getPlayer1StoneCountInPits(); game.board.player2Bank.stoneCount += game.getPlayer1StoneCountInPits();
game.board.clearPlayer1Pits(); game.board.clearPlayer1Pits();
this.addGameStep(game, index, clearBoardAtEndData);
} }
} }
private getPitIndexesThatHasStone(board: Board): number[] {
let index = 0;
const indexList = [];
for (const stoneCount of board.getStoneArray()) {
if (stoneCount > 0 && board.checkPitTypeIsNormalPitByIndex(index)) {
indexList.push(index);
}
index++;
}
return indexList;
}
private addGameStep(
game: MancalaGame,
index: number,
clearBoardAtEndData: ClearBoardAtEndData
) {
game.addGameStep(
new GameStep(index, GAME_STEP_BOARD_CLEARED, clearBoardAtEndData)
);
}
} }

View File

@ -1,6 +1,9 @@
import { GameRule } from '../../core/GameRule'; import { GameRule } from '../../core/GameRule';
import { GameStep } from '../../core/HistoryItem';
import { MancalaGame } from '../../core/MancalaGame'; import { MancalaGame } from '../../core/MancalaGame';
export const GAME_STEP_LAST_STONE_IN_BANK = 'GAME_STEP_LAST_STONE_IN_BANK';
export class GRLastStoneInBank implements GameRule { export class GRLastStoneInBank implements GameRule {
onGameMoveStart(game: MancalaGame, index: number): void {} onGameMoveStart(game: MancalaGame, index: number): void {}
onGameMove(game: MancalaGame, index: number): void {} onGameMove(game: MancalaGame, index: number): void {}
@ -11,6 +14,7 @@ export class GRLastStoneInBank implements GameRule {
(pitType === 'player1Bank' && game.isTurnPlayer1()) || (pitType === 'player1Bank' && game.isTurnPlayer1()) ||
(pitType === 'player2Bank' && game.isTurnPlayer2()) (pitType === 'player2Bank' && game.isTurnPlayer2())
) { ) {
game.addGameStep(new GameStep(index, GAME_STEP_LAST_STONE_IN_BANK));
} else { } else {
game.changePlayerTurn(); game.changePlayerTurn();
} }

View File

@ -1,6 +1,12 @@
import { GameRule } from '../../core/GameRule'; import { GameRule } from '../../core/GameRule';
import { GameStep } from '../../core/HistoryItem';
import { MancalaGame } from '../../core/MancalaGame'; import { MancalaGame } from '../../core/MancalaGame';
export const GAME_STEP_LAST_STONE_IN_EMPTY_PIT =
'GAME_STEP_LAST_STONE_IN_EMPTY_PIT';
export type LastStoneInEmptyPitData = { oppositeIndex: number };
export class GRLastStoneInEmptyPit implements GameRule { export class GRLastStoneInEmptyPit implements GameRule {
onGameMoveStart(game: MancalaGame, index: number): void {} onGameMoveStart(game: MancalaGame, index: number): void {}
onGameMove(game: MancalaGame, index: number): void {} onGameMove(game: MancalaGame, index: number): void {}
@ -10,26 +16,38 @@ export class GRLastStoneInEmptyPit implements GameRule {
const pitType = game.board.getPitTypeByIndex(index); const pitType = game.board.getPitTypeByIndex(index);
if (pit.stoneCount === 1) { if (pit.stoneCount === 1) {
if (pitType === 'player1Pit' && game.isTurnPlayer1()) { if (pitType === 'player1Pit' && game.isTurnPlayer1()) {
const oppositePit = const oppositeIndex = game.board.getOppositePitIndex(index);
game.board.pits[game.board.getOppositePitIndex(index)]; const oppositePit = game.board.pits[oppositeIndex];
if (oppositePit.stoneCount > 0) { if (oppositePit.stoneCount > 0) {
const player1BankIndex = const player1BankIndex =
game.board.pits[game.board.player1BankIndex()]; game.board.pits[game.board.player1BankIndex()];
player1BankIndex.stoneCount += 1 + oppositePit.stoneCount; player1BankIndex.stoneCount += 1 + oppositePit.stoneCount;
oppositePit.stoneCount = 0; oppositePit.stoneCount = 0;
pit.stoneCount = 0; pit.stoneCount = 0;
this.addGameStep(game, index, { oppositeIndex });
} }
} else if (pitType === 'player2Pit' && game.isTurnPlayer2()) { } else if (pitType === 'player2Pit' && game.isTurnPlayer2()) {
const oppositePit = const oppositeIndex = game.board.getOppositePitIndex(index);
game.board.pits[game.board.getOppositePitIndex(index)]; const oppositePit = game.board.pits[oppositeIndex];
if (oppositePit.stoneCount > 0) { if (oppositePit.stoneCount > 0) {
const player2BankIndex = const player2BankIndex =
game.board.pits[game.board.player2BankIndex()]; game.board.pits[game.board.player2BankIndex()];
player2BankIndex.stoneCount += 1 + oppositePit.stoneCount; player2BankIndex.stoneCount += 1 + oppositePit.stoneCount;
oppositePit.stoneCount = 0; oppositePit.stoneCount = 0;
pit.stoneCount = 0; pit.stoneCount = 0;
this.addGameStep(game, index, { oppositeIndex });
} }
} }
} }
} }
private addGameStep(
game: MancalaGame,
index: number,
data: LastStoneInEmptyPitData
) {
game.addGameStep(
new GameStep(index, GAME_STEP_LAST_STONE_IN_EMPTY_PIT, data)
);
}
} }

View File

@ -35,9 +35,9 @@ export class Board {
for (let index = 0; index < this.totalPitCount(); index++) { for (let index = 0; index < this.totalPitCount(); index++) {
const pitType = this.getPitTypeByIndex(index); const pitType = this.getPitTypeByIndex(index);
if (pitType === 'player1Pit' || pitType === 'player2Pit') { if (pitType === 'player1Pit' || pitType === 'player2Pit') {
pitArray[index] = new Pit(initialStoneCountInPits); pitArray[index] = new Pit(index, initialStoneCountInPits);
} else if (pitType === 'player1Bank' || pitType === 'player2Bank') { } else if (pitType === 'player1Bank' || pitType === 'player2Bank') {
pitArray[index] = new Bank(0); pitArray[index] = new Bank(index, 0);
} }
} }
return pitArray; return pitArray;
@ -83,6 +83,18 @@ export class Board {
} }
} }
public checkPitTypeIsNormalPitByIndex(index: number): boolean {
this.checkIndeAndMaybeThrowError(index);
const pitType = this.getPitTypeByIndex(index);
return pitType === 'player1Pit' || pitType === 'player2Pit';
}
public checkPitTypeIsBankByIndex(index: number): boolean {
this.checkIndeAndMaybeThrowError(index);
const pitType = this.getPitTypeByIndex(index);
return pitType === 'player1Bank' || pitType === 'player2Bank';
}
public move(index: number) { public move(index: number) {
this.checkIndeAndMaybeThrowError(index); this.checkIndeAndMaybeThrowError(index);
const pitType = this.getPitTypeByIndex(index); const pitType = this.getPitTypeByIndex(index);

View File

@ -1,16 +1,34 @@
export class HistoryItem { export class HistoryItem {
boardSnapshot: number[]; boardSnapshot: number[];
constructor(boardSnapshot: number[]) { gameSteps: GameStep[];
constructor(boardSnapshot: number[], gameSteps: GameStep[]) {
this.boardSnapshot = boardSnapshot; this.boardSnapshot = boardSnapshot;
this.gameSteps = gameSteps;
} }
} }
export class MoveHistoryItem extends HistoryItem { export class MoveHistoryItem extends HistoryItem {
playerId: string; playerId: string;
moveIndex: number; moveIndex: number;
constructor(playerId: string, moveIndex: number, boardSnapshot: number[]) { constructor(
super(boardSnapshot); playerId: string,
moveIndex: number,
boardSnapshot: number[],
gameSteps: GameStep[]
) {
super(boardSnapshot, gameSteps);
this.playerId = playerId; this.playerId = playerId;
this.moveIndex = moveIndex; this.moveIndex = moveIndex;
} }
} }
export class GameStep {
index: number;
type: string;
data: any | null;
constructor(index: number, type: string, data: any = null) {
this.index = index;
this.type = type;
this.data = data;
}
}

View File

@ -1,9 +1,11 @@
import { Board, PitType } from './Board'; import { Board, PitType } from './Board';
import { GameRule } from './GameRule'; import { GameRule } from './GameRule';
import { HistoryItem, MoveHistoryItem } from './HistoryItem'; import { HistoryItem, MoveHistoryItem, GameStep } from './HistoryItem';
export type GameState = 'initial' | 'playing' | 'ended'; export type GameState = 'initial' | 'playing' | 'ended';
export const GAME_STEP_GAME_MOVE = 'GAME_STEP_GAME_MOVE';
export class MancalaGame { export class MancalaGame {
id: string; id: string;
board: Board; board: Board;
@ -13,6 +15,7 @@ export class MancalaGame {
state: GameState; state: GameState;
gameRules: GameRule[]; gameRules: GameRule[];
history: HistoryItem[]; history: HistoryItem[];
currentHistoryItem: HistoryItem | null = null;
constructor( constructor(
id: string, id: string,
@ -42,6 +45,7 @@ export class MancalaGame {
}); });
}; };
this.board.onGameMove = (index: number) => { this.board.onGameMove = (index: number) => {
this.addGameStep(new GameStep(index, GAME_STEP_GAME_MOVE));
this.gameRules.forEach((gameRule) => { this.gameRules.forEach((gameRule) => {
gameRule.onGameMove(this, index); gameRule.onGameMove(this, index);
}); });
@ -131,10 +135,15 @@ export class MancalaGame {
} }
if (this.canPlayerMove(playerId, pitIndex)) { if (this.canPlayerMove(playerId, pitIndex)) {
const moveIndex = this.getBoardIndexByPlayerId(playerId, pitIndex); const moveIndex = this.getBoardIndexByPlayerId(playerId, pitIndex);
this.board.move(moveIndex); this.currentHistoryItem = new MoveHistoryItem(
this.history.push( playerId,
new MoveHistoryItem(playerId, moveIndex, this.board.getStoneArray()) moveIndex,
[],
[]
); );
this.board.move(moveIndex);
this.currentHistoryItem.boardSnapshot = this.board.getStoneArray();
this.history.push(this.currentHistoryItem);
if (this.checkGameIsEnded()) { if (this.checkGameIsEnded()) {
this.state = 'ended'; this.state = 'ended';
} }
@ -218,4 +227,12 @@ export class MancalaGame {
mancalaGame.state mancalaGame.state
); );
} }
public getCurrentHistoryItem(): HistoryItem | null {
return this.currentHistoryItem;
}
public addGameStep(gameStep: GameStep) {
this.getCurrentHistoryItem()?.gameSteps.push(gameStep);
}
} }

View File

@ -1,7 +1,9 @@
export class Pit { export class Pit {
index: number;
stoneCount: number; stoneCount: number;
constructor(stoneCount = 0) { constructor(index: number, stoneCount = 0) {
this.index = index;
this.stoneCount = stoneCount; this.stoneCount = stoneCount;
} }
@ -19,8 +21,8 @@ export class Pit {
} }
export class Bank extends Pit { export class Bank extends Pit {
constructor(stoneCount = 0) { constructor(index: number, stoneCount = 0) {
super(stoneCount); super(index, stoneCount);
} }
override get isBank(): boolean { override get isBank(): boolean {

View File

@ -1,6 +1,23 @@
import { Board } from '../src/core/Board'; import { Board } from '../src/core/Board';
describe('Board Test', () => { describe('Board Test', () => {
test('test pit index', () => {
const board = new Board(6, 4);
expect(board.pits[0].index).toBe(0);
expect(board.pits[1].index).toBe(1);
expect(board.pits[2].index).toBe(2);
expect(board.pits[3].index).toBe(3);
expect(board.pits[4].index).toBe(4);
expect(board.pits[5].index).toBe(5);
expect(board.pits[6].index).toBe(6);
expect(board.pits[7].index).toBe(7);
expect(board.pits[8].index).toBe(8);
expect(board.pits[9].index).toBe(9);
expect(board.pits[10].index).toBe(10);
expect(board.pits[11].index).toBe(11);
expect(board.pits[12].index).toBe(12);
expect(board.pits[13].index).toBe(13);
});
test('test getPitTypeByIndex', () => { test('test getPitTypeByIndex', () => {
const board = new Board(6, 4); const board = new Board(6, 4);
expect(board.getPitTypeByIndex(0)).toBe('player1Pit'); expect(board.getPitTypeByIndex(0)).toBe('player1Pit');

View File

@ -1,29 +1,6 @@
import { GRClearBoardAtEnd } from '../src/common/game_rules/GRClearBoardAtEnd'; import { GameStep, MoveHistoryItem } from '../src/core/HistoryItem';
import { GRLastStoneInBank } from '../src/common/game_rules/GRLastStoneInBank'; import { GAME_STEP_GAME_MOVE, MancalaGame } from '../src/core/MancalaGame';
import { GRLastStoneInEmptyPit } from '../src/common/game_rules/GRLastStoneInEmptyPit'; import { createGame } from './TestUtil';
import { Board } from '../src/core/Board';
import { MoveHistoryItem } from '../src/core/HistoryItem';
import { MancalaGame } from '../src/core/MancalaGame';
function createGame(): MancalaGame {
const board = new Board(6, 4);
const player1Id = '0';
const player2Id = '1';
const game = new MancalaGame(
'0',
board,
player1Id,
player2Id,
player1Id,
[
new GRLastStoneInEmptyPit(),
new GRLastStoneInBank(),
new GRClearBoardAtEnd()
],
[]
);
return game;
}
describe('Game Test', () => { describe('Game Test', () => {
test('test getPlayerIdByIndex', () => { test('test getPlayerIdByIndex', () => {
@ -125,27 +102,55 @@ describe('Game Test', () => {
game.moveByPlayerPit(player2Id, 0); game.moveByPlayerPit(player2Id, 0);
game.moveByPlayerPit(player1Id, 1); game.moveByPlayerPit(player1Id, 1);
game.moveByPlayerPit(player2Id, 1); game.moveByPlayerPit(player2Id, 1);
expect(game.history).toStrictEqual([ const historyItem1 = new MoveHistoryItem(
new MoveHistoryItem(
player1Id, player1Id,
0, 0,
[1, 5, 5, 5, 4, 4, 0, 4, 4, 4, 4, 4, 4, 0] [1, 5, 5, 5, 4, 4, 0, 4, 4, 4, 4, 4, 4, 0],
), [
new MoveHistoryItem( new GameStep(0, GAME_STEP_GAME_MOVE),
new GameStep(1, GAME_STEP_GAME_MOVE),
new GameStep(2, GAME_STEP_GAME_MOVE),
new GameStep(3, GAME_STEP_GAME_MOVE)
]
);
const historyItem2 = new MoveHistoryItem(
player2Id, player2Id,
7, 7,
[1, 5, 5, 5, 4, 4, 0, 1, 5, 5, 5, 4, 4, 0] [1, 5, 5, 5, 4, 4, 0, 1, 5, 5, 5, 4, 4, 0],
), [
new MoveHistoryItem( new GameStep(7, GAME_STEP_GAME_MOVE),
new GameStep(8, GAME_STEP_GAME_MOVE),
new GameStep(9, GAME_STEP_GAME_MOVE),
new GameStep(10, GAME_STEP_GAME_MOVE)
]
);
const historyItem3 = new MoveHistoryItem(
player1Id, player1Id,
1, 1,
[1, 1, 6, 6, 5, 5, 0, 1, 5, 5, 5, 4, 4, 0] [1, 1, 6, 6, 5, 5, 0, 1, 5, 5, 5, 4, 4, 0],
), [
new MoveHistoryItem( new GameStep(1, GAME_STEP_GAME_MOVE),
new GameStep(2, GAME_STEP_GAME_MOVE),
new GameStep(3, GAME_STEP_GAME_MOVE),
new GameStep(4, GAME_STEP_GAME_MOVE),
new GameStep(5, GAME_STEP_GAME_MOVE)
]
);
const historyItem4 = new MoveHistoryItem(
player2Id, player2Id,
8, 8,
[1, 1, 6, 6, 5, 5, 0, 1, 1, 6, 6, 5, 5, 0] [1, 1, 6, 6, 5, 5, 0, 1, 1, 6, 6, 5, 5, 0],
) [
]); new GameStep(8, GAME_STEP_GAME_MOVE),
new GameStep(9, GAME_STEP_GAME_MOVE),
new GameStep(10, GAME_STEP_GAME_MOVE),
new GameStep(11, GAME_STEP_GAME_MOVE),
new GameStep(12, GAME_STEP_GAME_MOVE)
]
);
expect(game.history[0]).toStrictEqual(historyItem1);
expect(game.history[1]).toStrictEqual(historyItem2);
expect(game.history[2]).toStrictEqual(historyItem3);
expect(game.history[3]).toStrictEqual(historyItem4);
}); });
}); });

25
tests/TestUtil.ts Normal file
View File

@ -0,0 +1,25 @@
import { GRClearBoardAtEnd } from '../src/common/game_rules/GRClearBoardAtEnd';
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';
export function createGame(): MancalaGame {
const board = new Board(6, 4);
const player1Id = '0';
const player2Id = '1';
const game = new MancalaGame(
'0',
board,
player1Id,
player2Id,
player1Id,
[
new GRLastStoneInEmptyPit(),
new GRLastStoneInBank(),
new GRClearBoardAtEnd()
],
[]
);
return game;
}

View File

@ -1,28 +1,19 @@
import { GRLastStoneInBank } from '../../src/common/game_rules/GRLastStoneInBank'; import {
import { GRLastStoneInEmptyPit } from '../../src/common/game_rules/GRLastStoneInEmptyPit'; GAME_STEP_LAST_STONE_IN_BANK,
import { GRClearBoardAtEnd } from '../../src/common/game_rules/GRClearBoardAtEnd'; GRLastStoneInBank
} from '../../src/common/game_rules/GRLastStoneInBank';
import {
GAME_STEP_LAST_STONE_IN_EMPTY_PIT,
GRLastStoneInEmptyPit
} from '../../src/common/game_rules/GRLastStoneInEmptyPit';
import {
GAME_STEP_BOARD_CLEARED,
GRClearBoardAtEnd
} from '../../src/common/game_rules/GRClearBoardAtEnd';
import { Board } from '../../src/core/Board'; import { Board } from '../../src/core/Board';
import { MancalaGame } from '../../src/core/MancalaGame'; import { GAME_STEP_GAME_MOVE, MancalaGame } from '../../src/core/MancalaGame';
import { GameStep } from '../../src/core/HistoryItem';
function createGame(): MancalaGame { import { createGame } from '../TestUtil';
const board = new Board(6, 4);
const player1Id = '0';
const player2Id = '1';
const game = new MancalaGame(
'0',
board,
player1Id,
player2Id,
player1Id,
[
new GRLastStoneInEmptyPit(),
new GRLastStoneInBank(),
new GRClearBoardAtEnd()
],
[]
);
return game;
}
describe('GRClearBoardAtEnd Test', () => { describe('GRClearBoardAtEnd Test', () => {
test('test GRClearBoardAtEnd 1', () => { test('test GRClearBoardAtEnd 1', () => {
@ -44,5 +35,12 @@ describe('GRClearBoardAtEnd Test', () => {
.map((pit) => pit.stoneCount) .map((pit) => pit.stoneCount)
.reduce((sum, stoneCount) => sum + stoneCount, 0) .reduce((sum, stoneCount) => sum + stoneCount, 0)
).toBe(0); ).toBe(0);
expect(game.history[0].gameSteps).toStrictEqual([
new GameStep(6, GAME_STEP_GAME_MOVE),
new GameStep(6, GAME_STEP_LAST_STONE_IN_BANK),
new GameStep(6, GAME_STEP_BOARD_CLEARED, {
pitIndexesThatHasStone: [12]
})
]);
}); });
}); });

View File

@ -0,0 +1,27 @@
import { GAME_STEP_GAME_MOVE } from '../../src/core/MancalaGame';
import { GameStep } from '../../src/core/HistoryItem';
import { createGame } from '../TestUtil';
import { GAME_STEP_LAST_STONE_IN_EMPTY_PIT } from '../../src/common/game_rules/GRLastStoneInEmptyPit';
describe('GRClearBoardAtEnd Test', () => {
test('test GRClearBoardAtEnd 1', () => {
const game = createGame();
const board = game.board;
const initialBoard = [4, 4, 4, 4, 4, 4, 0, 4, 4, 4, 4, 4, 4, 0];
expect(board.getStoneArray()).toStrictEqual(initialBoard);
game.board.player1Pits[0].stoneCount = 1;
game.board.player1Pits[1].stoneCount = 0;
expect(board.getStoneArray()).toStrictEqual([
1, 0, 4, 4, 4, 4, 0, 4, 4, 4, 4, 4, 4, 0
]);
game.moveByPlayerPit('0', 0);
expect(board.getStoneArray()).toStrictEqual([
0, 0, 4, 4, 4, 4, 5, 4, 4, 4, 4, 0, 4, 0
]);
expect(game.history[0].gameSteps).toStrictEqual([
new GameStep(1, GAME_STEP_GAME_MOVE),
new GameStep(1, GAME_STEP_LAST_STONE_IN_EMPTY_PIT, { oppositeIndex: 11 })
]);
});
});

4342
yarn.lock

File diff suppressed because it is too large Load Diff