Skip to content

Commit 88a2dd7

Browse files
feat: Implement backgammon move validation logic
I've implemented traditional backgammon rules for validating your moves based on dice rolls for online matches. Key changes include: - A new `isValidMove` function in `src/GameLogic/validation.ts` that checks: - Standard piece movement (correct distance, direction, ownership). - Valid landing spots (empty, own pieces, opponent blots, blocked points). - Rules for re-entering pieces from the bar. - Rules for bearing off pieces, including when all pieces are in the home board and using higher die rolls. - A new `calculateNewMove` function in `src/Utils.ts` that: - Uses `isValidMove` to authorize a move. - Updates the game board, prison, and home counts based on the valid move. - Consumes the used die value from your dice. - Generates a standard backgammon move notation label. - Integration of `calculateNewMove` into the main move handling logic in `src/index.tsx`: - The `move` function now iterates through available dice to find a valid die for your intended move (`from` and `to` points). - The `onDrop` handler for bearing off was updated to use the correct `'off'` parameter. - I've added comprehensive unit tests for `isValidMove` and `calculateNewMove` to ensure correctness of the new logic, covering a wide range of game scenarios. This change enhances the game by enforcing core backgammon rules, preventing you from making invalid moves.
1 parent 717c61e commit 88a2dd7

File tree

5 files changed

+712
-25
lines changed

5 files changed

+712
-25
lines changed

src/GameLogic/validation.test.ts

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import { isValidMove } from './validation';
2+
import { type GameType } from '../Types';
3+
import { DEFAULT_BOARD, newGame } from '../Utils'; // Assuming newGame and DEFAULT_BOARD are exported from Utils
4+
5+
// Helper function to create game states
6+
const createGameState = (
7+
boardSetup: number[] | null,
8+
dice: [number, number],
9+
color: 'white' | 'black',
10+
whitePrison: number = 0,
11+
blackPrison: number = 0,
12+
whiteHome: number = 0,
13+
blackHome: number = 0
14+
): GameType => {
15+
const game = newGame();
16+
if (boardSetup) {
17+
game.board = [...boardSetup];
18+
} else {
19+
game.board = [...DEFAULT_BOARD];
20+
}
21+
game.dice = dice;
22+
game.color = color;
23+
game.turn = 'player1'; // Dummy turn ID
24+
game.prison = { white: whitePrison, black: blackPrison };
25+
game.home = { white: whiteHome, black: blackHome };
26+
game.status = 'rolled'; // Assume dice have been rolled
27+
return game;
28+
};
29+
30+
describe('isValidMove - Standard Moves', () => {
31+
it('should allow White to move to an empty point with correct die', () => {
32+
const board = [...DEFAULT_BOARD];
33+
board[11] = 1; // White piece on point 12 (0-indexed: 11)
34+
board[5] = 0; // Point 6 (0-indexed: 5) is empty
35+
const game = createGameState(board, [6, 1], 'white');
36+
expect(isValidMove(game, 11, 5, 6)).toBe(true);
37+
});
38+
39+
it('should allow Black to move to their own piece with correct die', () => {
40+
const board = [...DEFAULT_BOARD];
41+
board[12] = -1; // Black piece on point 13 (0-indexed: 12)
42+
board[17] = -1; // Black piece on point 18 (0-indexed: 17)
43+
const game = createGameState(board, [5, 2], 'black');
44+
expect(isValidMove(game, 12, 17, 5)).toBe(true);
45+
});
46+
47+
it('should allow White to hit a Black blot', () => {
48+
const board = [...DEFAULT_BOARD];
49+
board[20] = 1; // White piece on point 21 (0-indexed: 20)
50+
board[17] = -1; // Black blot on point 18 (0-indexed: 17)
51+
const game = createGameState(board, [3, 4], 'white');
52+
expect(isValidMove(game, 20, 17, 3)).toBe(true);
53+
});
54+
55+
it('should not allow Black to land on White\'s blocked point', () => {
56+
const board = [...DEFAULT_BOARD];
57+
board[5] = -1; // Black piece on point 6 (0-indexed: 5)
58+
board[8] = 2; // White blocked point at 9 (0-indexed: 8)
59+
const game = createGameState(board, [3, 1], 'black');
60+
expect(isValidMove(game, 5, 8, 3)).toBe(false);
61+
});
62+
63+
it('should not allow White to move in the wrong direction', () => {
64+
const board = [...DEFAULT_BOARD];
65+
board[5] = 1; // White piece on point 6 (0-indexed: 5)
66+
const game = createGameState(board, [3, 1], 'white');
67+
expect(isValidMove(game, 5, 8, 3)).toBe(false); // Attempting to move 5 to 8 (black's direction)
68+
});
69+
70+
it('should not allow Black to move if distance does not match die', () => {
71+
const board = [...DEFAULT_BOARD];
72+
board[12] = -1;
73+
const game = createGameState(board, [5, 2], 'black');
74+
expect(isValidMove(game, 12, 18, 5)).toBe(false); // Should be 12 to 17 for die 5
75+
});
76+
77+
it('should not allow moving opponent\'s piece', () => {
78+
const board = [...DEFAULT_BOARD];
79+
board[11] = -1; // Black piece on point 12
80+
const game = createGameState(board, [6, 1], 'white'); // White's turn
81+
expect(isValidMove(game, 11, 5, 6)).toBe(false);
82+
});
83+
84+
it('should not allow moving from an empty point', () => {
85+
const board = [...DEFAULT_BOARD];
86+
board[11] = 0; // Point 12 is empty
87+
const game = createGameState(board, [6, 1], 'white');
88+
expect(isValidMove(game, 11, 5, 6)).toBe(false);
89+
});
90+
});
91+
92+
describe('isValidMove - Re-entry from Bar', () => {
93+
it('should allow White to re-enter from bar to an empty point with die 3', () => {
94+
// Point 3 (0-indexed: 2)
95+
const game = createGameState([...DEFAULT_BOARD], [3, 1], 'white', 1);
96+
game.board[2] = 0; // Ensure point 3 is empty
97+
expect(isValidMove(game, 'white', 2, 3)).toBe(true);
98+
});
99+
100+
it('should allow Black to re-enter from bar and hit a White blot with die 5', () => {
101+
// Target point 20 (0-indexed: 19, since 24 - 5 = 19)
102+
const board = [...DEFAULT_BOARD];
103+
board[19] = 1; // White blot on point 20
104+
const game = createGameState(board, [5, 2], 'black', 0, 1);
105+
expect(isValidMove(game, 'black', 19, 5)).toBe(true);
106+
});
107+
108+
it('should not allow White to re-enter if target point is blocked by Black', () => {
109+
const board = [...DEFAULT_BOARD];
110+
board[3] = -2; // Black blocks point 4 (0-indexed: 3)
111+
const game = createGameState(board, [4, 1], 'white', 1);
112+
expect(isValidMove(game, 'white', 3, 4)).toBe(false);
113+
});
114+
115+
it('should not allow Black to re-enter if "from" is a board point', () => {
116+
const game = createGameState([...DEFAULT_BOARD], [5, 2], 'black', 0, 1);
117+
expect(isValidMove(game, 0, 19, 5)).toBe(false); // from is 0, not 'black'
118+
});
119+
120+
it('should not allow White to re-enter if "to" point does not match die', () => {
121+
const game = createGameState([...DEFAULT_BOARD], [3, 1], 'white', 1);
122+
expect(isValidMove(game, 'white', 1, 3)).toBe(false); // Die 3, target should be 2, not 1
123+
});
124+
125+
it('should not allow re-entry attempt if no pieces are on the bar', () => {
126+
const game = createGameState([...DEFAULT_BOARD], [3, 1], 'white', 0); // No pieces on bar
127+
expect(isValidMove(game, 'white', 2, 3)).toBe(false);
128+
});
129+
130+
it('should not allow a board move if player has pieces on the bar', () => {
131+
const game = createGameState([...DEFAULT_BOARD], [3,1], 'white', 1); // White has piece on bar
132+
game.board[11] = 1; // White piece on board
133+
expect(isValidMove(game, 11, 8, 3)).toBe(false); // Attempting board move
134+
});
135+
});
136+
137+
describe('isValidMove - Bearing Off', () => {
138+
const allWhiteHomeBoard = () => {
139+
const board = Array(24).fill(0);
140+
board[0] = 2; board[1] = 2; board[2] = 2; board[3] = 2; board[4] = 2; board[5] = 5; // 15 pieces
141+
return board;
142+
};
143+
const allBlackHomeBoard = () => {
144+
const board = Array(24).fill(0);
145+
board[18] = -2; board[19] = -2; board[20] = -2; board[21] = -2; board[22] = -2; board[23] = -5; // 15 pieces
146+
return board;
147+
};
148+
149+
it('should allow White to bear off with exact die when all pieces are home', () => {
150+
const game = createGameState(allWhiteHomeBoard(), [3, 1], 'white');
151+
// Bearing off from point 3 (0-indexed: 2) with die 3
152+
expect(isValidMove(game, 2, 'off', 3)).toBe(true);
153+
});
154+
155+
it('should allow Black to bear off with exact die when all pieces are home', () => {
156+
const game = createGameState(allBlackHomeBoard(), [4, 2], 'black');
157+
// Bearing off from point 21 (0-indexed: 20) with die 4 (24-4 = 20)
158+
expect(isValidMove(game, 20, 'off', 4)).toBe(true);
159+
});
160+
161+
it('should allow White to bear off with higher die when all pieces are home and it\'s the furthest piece', () => {
162+
const board = allWhiteHomeBoard();
163+
board[5] = 1; // Furthest piece is on point 6 (0-indexed: 5)
164+
board[4] = 0; board[3] = 0; // Clear higher points for this test
165+
const game = createGameState(board, [6, 1], 'white');
166+
// Try to bear off piece from point 6 (idx 5) with die 6 (exact) - This is the setup
167+
// Now, if piece was at idx 2 (point 3), die 5 should take it off if 5,4 are empty
168+
board[5]=0; board[4]=0; board[3]=0; board[2]=1; // piece at point 3 (idx 2)
169+
const game2 = createGameState(board, [5,1], 'white');
170+
expect(isValidMove(game2, 2, 'off', 5)).toBe(true);
171+
});
172+
173+
it('should allow Black to bear off with higher die when all pieces are home and it\'s the furthest piece', () => {
174+
const board = allBlackHomeBoard();
175+
board[18] = -1; // Furthest piece is on point 19 (0-indexed: 18)
176+
board[19] = 0; board[20] = 0; // Clear higher points
177+
const game = createGameState(board, [6, 1], 'black');
178+
// Try to bear off piece from point 19 (idx 18) with die 6 (exact)
179+
// Now, if piece was at idx 21 (point 22), die 5 (target 24-5=19) should take it off if 19,20 are empty
180+
board[18]=0; board[19]=0; board[20]=0; board[21]=-1; // piece at point 22 (idx 21)
181+
const game2 = createGameState(board, [5,1], 'black');
182+
expect(isValidMove(game2, 21, 'off', 5)).toBe(true);
183+
});
184+
185+
it('should NOT allow White to bear off with higher die if there is a piece on a higher point', () => {
186+
const board = allWhiteHomeBoard();
187+
board[2] = 1; // Piece on point 3 (idx 2)
188+
board[4] = 1; // Piece on point 5 (idx 4) - this should be moved first
189+
const game = createGameState(board, [5,1], 'white'); // Die 5
190+
expect(isValidMove(game, 2, 'off', 5)).toBe(false); // Cannot bear off from point 3 if point 5 has a piece
191+
});
192+
193+
194+
it('should not allow White to bear off if pieces are outside home board', () => {
195+
const board = allWhiteHomeBoard();
196+
board[6] = 1; // Piece on point 7 (outside home)
197+
const game = createGameState(board, [3, 1], 'white');
198+
expect(isValidMove(game, 2, 'off', 3)).toBe(false);
199+
});
200+
201+
it('should not allow Black to bear off if piece is on the bar', () => {
202+
const game = createGameState(allBlackHomeBoard(), [4, 2], 'black', 0, 1); // Black piece on bar
203+
expect(isValidMove(game, 20, 'off', 4)).toBe(false);
204+
});
205+
206+
it('should not allow White to bear off if die does not match point and not covered by higher die rule', () => {
207+
const game = createGameState(allWhiteHomeBoard(), [3, 1], 'white');
208+
// Attempt to bear off from point 5 (0-indexed: 4) with die 3. Point 3 (idx 2) is not empty.
209+
expect(isValidMove(game, 4, 'off', 3)).toBe(false);
210+
});
211+
212+
it('should not allow White to bear off from a point not in the home board (e.g. point 7)', () => {
213+
const game = createGameState(allWhiteHomeBoard(), [1,1], 'white');
214+
// Technically, all pieces are home, but 'from' is invalid.
215+
// isValidMove should catch from < 0 || from > 5 for white bearoff.
216+
// This specific test is slightly redundant with the "pieces outside home" one if that one is general,
217+
// but good for explicitness on `from` point.
218+
// Let's ensure a piece is actually at point 7 for this test to be meaningful beyond just "all home"
219+
const board = allWhiteHomeBoard(); // All home
220+
board[6] = 1; // Add a white piece at point 7 (idx 6)
221+
// Now, the "all pieces home" check in isValidMove should fail.
222+
// If we manually bypass that and just test the from point:
223+
// The `isValidMove` check `if (from < 0 || from > 5) return false;` for white bearoff should catch this.
224+
// This test might be better framed as "cannot select 'from' outside home board for bearing off"
225+
// But isValidMove's internal logic for bearoff (is all home? then is 'from' valid?) covers this.
226+
// The existing "pieces outside home" test is more robust.
227+
// For this test, let's make sure "all pieces are home" but from is wrong.
228+
const gameAllHome = createGameState(allWhiteHomeBoard(), [1,1], 'white');
229+
expect(isValidMove(gameAllHome, 6, 'off', 1)).toBe(false); // from=6 is point 7
230+
});
231+
});

0 commit comments

Comments
 (0)