Release: Quarkus backend, Keycloak login, game history, clock, bot, tutorial and rematch #3

Merged
jhalitaksoy merged 88 commits from develop into main 2026-10-17 03:23:38 +03:00
5 changed files with 57 additions and 21 deletions
Showing only changes of commit 27f13e2f9a - Show all commits

View File

@ -64,14 +64,11 @@ export function describeMove(moves: PlayedMove[], moveNumber: number): MoveDescr
}; };
} }
// Board indexes the stones of the given pit are dropped into, in order, as Board.move sows them: a single stone goes // Board indexes the stones of the given pit are dropped into, in order, as Board.move sows them (see
// to the next pit; otherwise the first stone goes back into the played pit. Every pit and store is passed, the // Board.getSowingPath): the opponent's store is skipped. The last index is where the last stone lands. Empty for an
// opponent's store too. The last index is where the last stone lands. Empty for an empty pit or a store. // empty pit or a store.
export function getSowingPath(stones: number[], pitIndex: number): number[] { export function getSowingPath(stones: number[], pitIndex: number): number[] {
const stoneCount = stones[pitIndex]; return referenceBoard.getSowingPath(pitIndex, stones[pitIndex] ?? 0);
if (!stoneCount || referenceBoard.checkPitTypeIsBankByIndex(pitIndex)) return [];
const start = stoneCount === 1 ? pitIndex + 1 : pitIndex;
return Array.from({ length: stoneCount }, (_, step) => (start + step) % stones.length);
} }
// The game as it was after the given move, for showing or animating that position (e.g. with PitAnimator, // The game as it was after the given move, for showing or animating that position (e.g. with PitAnimator,

View File

@ -145,12 +145,12 @@ describe("getSowingPath", () => {
expect(getSowingPath(stones, 4)).toEqual([5]); expect(getSowingPath(stones, 4)).toEqual([5]);
}); });
it("passes both stores and wraps around the board", () => { it("passes the player's own store, skips the opponent's and wraps around the board", () => {
const stones = getInitialBoard(); const stones = getInitialBoard();
stones[12] = 4; stones[12] = 4;
expect(getSowingPath(stones, 12)).toEqual([12, 13, 0, 1]); expect(getSowingPath(stones, 12)).toEqual([12, 13, 0, 1]);
stones[5] = 10; stones[5] = 10;
expect(getSowingPath(stones, 5)).toEqual([5, 6, 7, 8, 9, 10, 11, 12, 13, 0]); expect(getSowingPath(stones, 5)).toEqual([5, 6, 7, 8, 9, 10, 11, 12, 0, 1]);
}); });
it("is empty for an empty pit or a store", () => { it("is empty for an empty pit or a store", () => {

View File

@ -107,19 +107,34 @@ export class Board {
`StoneNotFound => index : ${index} stoneCount = ${pit.stoneCount}` `StoneNotFound => index : ${index} stoneCount = ${pit.stoneCount}`
); );
} }
const stepCount = pit.stoneCount; const path = this.getSowingPath(index);
this.pits[index].stoneCount = 0; pit.stoneCount = 0;
const startIndex = stepCount === 1 ? index + 1 : index; this.fireOnGameMoveStart(path[0]);
this.moveInternal(startIndex, stepCount); path.forEach((pathIndex) => this.onGameMoveStep(pathIndex));
this.fireOnGameMoveEnd(path[path.length - 1]);
} }
private moveInternal(startIndex: number, stepCount: number) { // Indexes the stones of the pit are dropped into, in order. A single stone goes to the next pit; otherwise the first
this.fireOnGameMoveStart(startIndex); // stone goes back into the played pit. The player's own bank gets a stone, the opponent's bank is skipped. The last
for (let i = startIndex; i < startIndex + stepCount; i++) { // index is where the last stone lands. Empty for a bank or an empty pit.
this.onGameMoveStep(i); public getSowingPath(
index: number,
stoneCount = this.pits[index].stoneCount
): number[] {
this.checkIndeAndMaybeThrowError(index);
if (stoneCount <= 0 || this.checkPitTypeIsBankByIndex(index)) return [];
const opponentBankIndex =
this.getPitTypeByIndex(index) === 'player1Pit'
? this.player2BankIndex()
: this.player1BankIndex();
const path: number[] = [];
let current =
stoneCount === 1 ? this.getNextPitIndexCircularly(index) : index;
while (path.length < stoneCount) {
if (current !== opponentBankIndex) path.push(current);
current = this.getNextPitIndexCircularly(current);
} }
const stopIndex = startIndex + stepCount - 1; return path;
this.fireOnGameMoveEnd(stopIndex);
} }
public onGameMoveStep(index: number) { public onGameMoveStep(index: number) {

View File

@ -103,9 +103,33 @@ describe('Board Test', () => {
expect(board.getStoneArray()).toStrictEqual(initialBoard); expect(board.getStoneArray()).toStrictEqual(initialBoard);
board.move(5); board.move(5);
expect(board.getStoneArray()).toStrictEqual([ expect(board.getStoneArray()).toStrictEqual([
5, 5, 5, 5, 5, 2, 1, 5, 5, 5, 5, 5, 5, 1 5, 5, 5, 5, 5, 2, 2, 5, 5, 5, 5, 5, 5, 0
]); ]);
}); });
test('move skips the opponent bank for both players', () => {
const board = new Board(6, 4);
board.pits[12].stoneCount = 10;
board.move(12);
expect(board.getStoneArray()).toStrictEqual([
5, 5, 5, 5, 5, 5, 0, 5, 5, 4, 4, 4, 1, 1
]);
});
test('test getSowingPath', () => {
const board = new Board(6, 4);
expect(board.getSowingPath(0)).toStrictEqual([0, 1, 2, 3]);
expect(board.getSowingPath(0, 1)).toStrictEqual([1]);
expect(board.getSowingPath(5, 2)).toStrictEqual([5, 6]);
expect(board.getSowingPath(5, 9)).toStrictEqual([
5, 6, 7, 8, 9, 10, 11, 12, 0
]);
expect(board.getSowingPath(12, 3)).toStrictEqual([12, 13, 0]);
expect(board.getSowingPath(11, 10)).toStrictEqual([
11, 12, 13, 0, 1, 2, 3, 4, 5, 7
]);
expect(board.getSowingPath(0, 0)).toStrictEqual([]);
expect(board.getSowingPath(6)).toStrictEqual([]);
expect(board.getSowingPath(13)).toStrictEqual([]);
});
test('test getOppositePitIndex', () => { test('test getOppositePitIndex', () => {
const board = new Board(6, 4); const board = new Board(6, 4);

View File

@ -33,7 +33,7 @@ describe('GRDoubleStoneInPit Test', () => {
expect(game.history[game.history.length - 1].gameSteps).toStrictEqual([ expect(game.history[game.history.length - 1].gameSteps).toStrictEqual([
new GameStep(12, GAME_STEP_GAME_MOVE), new GameStep(12, GAME_STEP_GAME_MOVE),
new GameStep(13, GAME_STEP_GAME_MOVE), new GameStep(13, GAME_STEP_GAME_MOVE),
new GameStep(14, GAME_STEP_GAME_MOVE), new GameStep(0, GAME_STEP_GAME_MOVE),
new GameStep(0, GAME_STEP_DOUBLE_STONE_IN_PIT, { new GameStep(0, GAME_STEP_DOUBLE_STONE_IN_PIT, {
pitIndex: 0, pitIndex: 0,
bankIndex: 13 bankIndex: 13