Skip to content

Commit 920c825

Browse files
Copilotmgierschdev
andcommitted
Add comprehensive unit tests for AI components (BoardEvaluator and ChessAI)
Co-authored-by: mgierschdev <62764972+mgierschdev@users.noreply.github.com>
1 parent 5f43d6e commit 920c825

File tree

3 files changed

+474
-7
lines changed

3 files changed

+474
-7
lines changed

backend/src/main/java/com/backend/ai/ChessAI.java

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,11 @@ public static AIMove findBestMove(ChessGame game, int depth) {
6666
// Save current state
6767
String fenBeforeMove = game.exportToFEN();
6868

69-
// Make the move
70-
ChessPiece result = game.MoveController(move.from, move.to);
69+
// Make the move (create new positions to avoid modification)
70+
ChessPiece result = game.MoveController(
71+
new Position(move.from.row, move.from.col),
72+
new Position(move.to.row, move.to.col)
73+
);
7174

7275
if (result.type() != ChessPieceType.Invalid) {
7376
// Evaluate this position
@@ -79,7 +82,11 @@ public static AIMove findBestMove(ChessGame game, int depth) {
7982
// Update best move if this is better
8083
if (score > bestScore || (score == bestScore && random.nextBoolean())) {
8184
bestScore = score;
82-
bestMove = new AIMove(move.from, move.to, score);
85+
bestMove = new AIMove(
86+
new Position(move.from.row, move.from.col),
87+
new Position(move.to.row, move.to.col),
88+
score
89+
);
8390
}
8491

8592
alpha = Math.max(alpha, score);
@@ -128,7 +135,10 @@ private static int minimax(ChessGame game, int depth, int alpha, int beta,
128135
if (isMaximizing) {
129136
int maxEval = Integer.MIN_VALUE;
130137
for (AIMove move : legalMoves) {
131-
ChessPiece result = game.MoveController(move.from, move.to);
138+
ChessPiece result = game.MoveController(
139+
new Position(move.from.row, move.from.col),
140+
new Position(move.to.row, move.to.col)
141+
);
132142
if (result.type() != ChessPieceType.Invalid) {
133143
int eval = minimax(game, depth - 1, alpha, beta, false, aiColor);
134144
game.undo();
@@ -144,7 +154,10 @@ private static int minimax(ChessGame game, int depth, int alpha, int beta,
144154
} else {
145155
int minEval = Integer.MAX_VALUE;
146156
for (AIMove move : legalMoves) {
147-
ChessPiece result = game.MoveController(move.from, move.to);
157+
ChessPiece result = game.MoveController(
158+
new Position(move.from.row, move.from.col),
159+
new Position(move.to.row, move.to.col)
160+
);
148161
if (result.type() != ChessPieceType.Invalid) {
149162
int eval = minimax(game, depth - 1, alpha, beta, true, aiColor);
150163
game.undo();
@@ -172,10 +185,11 @@ private static List<AIMove> getAllLegalMoves(ChessGame game, Color color) {
172185
ChessPiece piece = board[row][col];
173186
if (piece.color() == color) {
174187
Position from = new Position(row + 1, col + 1); // Add offset for controller
175-
Position[] validMoves = game.getValidMovesController(from);
188+
Position[] validMoves = game.getValidMovesController(new Position(row + 1, col + 1));
176189

177190
for (Position to : validMoves) {
178-
moves.add(new AIMove(from, to, 0));
191+
// Create new Position for from to avoid modification issues
192+
moves.add(new AIMove(new Position(row + 1, col + 1), to, 0));
179193
}
180194
}
181195
}
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
package com.backend.ai;
2+
3+
import com.backend.models.ChessPiece;
4+
import com.backend.models.ChessPieceType;
5+
import com.backend.models.Color;
6+
import com.backend.models.GameState;
7+
import org.junit.jupiter.api.Test;
8+
9+
import static org.junit.jupiter.api.Assertions.*;
10+
11+
class BoardEvaluatorTest {
12+
13+
@Test
14+
void testEvaluateStartingPosition() {
15+
ChessPiece[][] board = createStartingBoard();
16+
17+
// Starting position should be equal for both sides
18+
int whiteEval = BoardEvaluator.evaluate(board, Color.White);
19+
int blackEval = BoardEvaluator.evaluate(board, Color.Black);
20+
21+
// Both should have same material
22+
assertEquals(whiteEval, blackEval, "Starting position should be equal");
23+
}
24+
25+
@Test
26+
void testEvaluateMaterialAdvantage() {
27+
// Create a board where white has an extra pawn
28+
ChessPiece[][] board = createEmptyBoard();
29+
30+
// White king and pawn
31+
board[0][4] = new ChessPiece(ChessPieceType.King, Color.White);
32+
board[1][0] = new ChessPiece(ChessPieceType.Pawn, Color.White);
33+
board[1][1] = new ChessPiece(ChessPieceType.Pawn, Color.White);
34+
35+
// Black king and one pawn
36+
board[7][4] = new ChessPiece(ChessPieceType.King, Color.Black);
37+
board[6][0] = new ChessPiece(ChessPieceType.Pawn, Color.Black);
38+
39+
int whiteEval = BoardEvaluator.evaluate(board, Color.White);
40+
41+
// White should have positive evaluation (extra pawn = ~100 centipawns)
42+
assertTrue(whiteEval > 50, "White should have material advantage");
43+
}
44+
45+
@Test
46+
void testEvaluateQueenVsRook() {
47+
ChessPiece[][] board = createEmptyBoard();
48+
49+
// White: King + Queen
50+
board[0][4] = new ChessPiece(ChessPieceType.King, Color.White);
51+
board[0][3] = new ChessPiece(ChessPieceType.Queen, Color.White);
52+
53+
// Black: King + Rook
54+
board[7][4] = new ChessPiece(ChessPieceType.King, Color.Black);
55+
board[7][0] = new ChessPiece(ChessPieceType.Rock, Color.Black);
56+
57+
int whiteEval = BoardEvaluator.evaluate(board, Color.White);
58+
59+
// Queen (900) vs Rook (500) = 400 centipawn advantage
60+
assertTrue(whiteEval > 300 && whiteEval < 500,
61+
"White should have ~400 centipawn advantage (Queen vs Rook)");
62+
}
63+
64+
@Test
65+
void testEvaluatePawnPositionBonus() {
66+
ChessPiece[][] board = createEmptyBoard();
67+
68+
// White king and advanced pawn (row 6 = near promotion)
69+
board[0][4] = new ChessPiece(ChessPieceType.King, Color.White);
70+
board[6][4] = new ChessPiece(ChessPieceType.Pawn, Color.White); // Row 6 for white
71+
72+
// Black king and starting pawn (row 6 from black's perspective = row 1 from white)
73+
board[7][4] = new ChessPiece(ChessPieceType.King, Color.Black);
74+
board[6][3] = new ChessPiece(ChessPieceType.Pawn, Color.Black); // Row 6 from white = row 1 from black's view
75+
76+
int whiteEval = BoardEvaluator.evaluate(board, Color.White);
77+
78+
// Both pawns should have similar material value, evaluation should be close
79+
// This test just verifies the evaluation works without error
80+
assertTrue(whiteEval >= -200 && whiteEval <= 200,
81+
"Evaluation should be in reasonable range");
82+
}
83+
84+
@Test
85+
void testEvaluateKnightCentralization() {
86+
ChessPiece[][] board = createEmptyBoard();
87+
88+
// White king and centralized knight
89+
board[0][4] = new ChessPiece(ChessPieceType.King, Color.White);
90+
board[3][3] = new ChessPiece(ChessPieceType.Knight, Color.White); // Center
91+
92+
// Black king and edge knight
93+
board[7][4] = new ChessPiece(ChessPieceType.King, Color.Black);
94+
board[0][0] = new ChessPiece(ChessPieceType.Knight, Color.Black); // Corner
95+
96+
int whiteEval = BoardEvaluator.evaluate(board, Color.White);
97+
98+
// Centralized knight should be better
99+
assertTrue(whiteEval > 0, "Centralized knight should have better evaluation");
100+
}
101+
102+
@Test
103+
void testEvaluateTerminalCheckmate() {
104+
ChessPiece[][] board = createEmptyBoard();
105+
board[0][4] = new ChessPiece(ChessPieceType.King, Color.White);
106+
board[7][4] = new ChessPiece(ChessPieceType.King, Color.Black);
107+
108+
// Test checkmate for white (white is checkmated, it's white's turn)
109+
int eval = BoardEvaluator.evaluateTerminal(board, GameState.Checkmate, Color.White, Color.White);
110+
assertEquals(-100000, eval, "Checkmate should return large negative value");
111+
112+
// Test checkmate for black (black is checkmated, it's black's turn)
113+
eval = BoardEvaluator.evaluateTerminal(board, GameState.Checkmate, Color.Black, Color.White);
114+
assertEquals(100000, eval, "Opponent checkmate should return large positive value");
115+
}
116+
117+
@Test
118+
void testEvaluateTerminalStalemate() {
119+
ChessPiece[][] board = createEmptyBoard();
120+
board[0][4] = new ChessPiece(ChessPieceType.King, Color.White);
121+
board[7][4] = new ChessPiece(ChessPieceType.King, Color.Black);
122+
123+
int eval = BoardEvaluator.evaluateTerminal(board, GameState.DrawByStalemate, Color.White, Color.White);
124+
assertEquals(0, eval, "Stalemate should return 0");
125+
}
126+
127+
@Test
128+
void testEvaluateTerminalDraw() {
129+
ChessPiece[][] board = createEmptyBoard();
130+
board[0][4] = new ChessPiece(ChessPieceType.King, Color.White);
131+
board[7][4] = new ChessPiece(ChessPieceType.King, Color.Black);
132+
133+
int eval = BoardEvaluator.evaluateTerminal(board, GameState.DrawByFiftyMove, Color.White, Color.White);
134+
assertEquals(0, eval, "50-move draw should return 0");
135+
136+
eval = BoardEvaluator.evaluateTerminal(board, GameState.DrawByRepetition, Color.White, Color.White);
137+
assertEquals(0, eval, "Repetition draw should return 0");
138+
}
139+
140+
@Test
141+
void testEvaluateEmptySquares() {
142+
ChessPiece[][] board = createEmptyBoard();
143+
board[0][4] = new ChessPiece(ChessPieceType.King, Color.White);
144+
board[7][4] = new ChessPiece(ChessPieceType.King, Color.Black);
145+
146+
int whiteEval = BoardEvaluator.evaluate(board, Color.White);
147+
int blackEval = BoardEvaluator.evaluate(board, Color.Black);
148+
149+
// Only kings, should be equal
150+
assertEquals(whiteEval, blackEval, "King-only position should be equal");
151+
}
152+
153+
@Test
154+
void testEvaluateComplexPosition() {
155+
ChessPiece[][] board = createEmptyBoard();
156+
157+
// White: King, Queen, Rook, Bishop, 2 Pawns
158+
board[0][4] = new ChessPiece(ChessPieceType.King, Color.White);
159+
board[0][3] = new ChessPiece(ChessPieceType.Queen, Color.White);
160+
board[0][0] = new ChessPiece(ChessPieceType.Rock, Color.White);
161+
board[0][2] = new ChessPiece(ChessPieceType.Bishop, Color.White);
162+
board[1][0] = new ChessPiece(ChessPieceType.Pawn, Color.White);
163+
board[1][1] = new ChessPiece(ChessPieceType.Pawn, Color.White);
164+
165+
// Black: King, Rook, Bishop, Knight, 2 Pawns
166+
board[7][4] = new ChessPiece(ChessPieceType.King, Color.Black);
167+
board[7][0] = new ChessPiece(ChessPieceType.Rock, Color.Black);
168+
board[7][2] = new ChessPiece(ChessPieceType.Bishop, Color.Black);
169+
board[7][1] = new ChessPiece(ChessPieceType.Knight, Color.Black);
170+
board[6][0] = new ChessPiece(ChessPieceType.Pawn, Color.Black);
171+
board[6][1] = new ChessPiece(ChessPieceType.Pawn, Color.Black);
172+
173+
int whiteEval = BoardEvaluator.evaluate(board, Color.White);
174+
175+
// White has Queen (900) vs Knight (320) = ~580 advantage
176+
assertTrue(whiteEval > 400, "White should have significant material advantage");
177+
}
178+
179+
// Helper methods
180+
private ChessPiece[][] createEmptyBoard() {
181+
ChessPiece[][] board = new ChessPiece[8][8];
182+
ChessPiece empty = new ChessPiece(ChessPieceType.Empty, Color.None);
183+
184+
for (int row = 0; row < 8; row++) {
185+
for (int col = 0; col < 8; col++) {
186+
board[row][col] = empty;
187+
}
188+
}
189+
190+
return board;
191+
}
192+
193+
private ChessPiece[][] createStartingBoard() {
194+
ChessPiece[][] board = new ChessPiece[8][8];
195+
ChessPiece empty = new ChessPiece(ChessPieceType.Empty, Color.None);
196+
197+
// Empty squares
198+
for (int row = 2; row <= 5; row++) {
199+
for (int col = 0; col < 8; col++) {
200+
board[row][col] = empty;
201+
}
202+
}
203+
204+
// White pawns
205+
for (int col = 0; col < 8; col++) {
206+
board[1][col] = new ChessPiece(ChessPieceType.Pawn, Color.White);
207+
}
208+
209+
// Black pawns
210+
for (int col = 0; col < 8; col++) {
211+
board[6][col] = new ChessPiece(ChessPieceType.Pawn, Color.Black);
212+
}
213+
214+
// White pieces
215+
board[0][0] = new ChessPiece(ChessPieceType.Rock, Color.White);
216+
board[0][1] = new ChessPiece(ChessPieceType.Knight, Color.White);
217+
board[0][2] = new ChessPiece(ChessPieceType.Bishop, Color.White);
218+
board[0][3] = new ChessPiece(ChessPieceType.Queen, Color.White);
219+
board[0][4] = new ChessPiece(ChessPieceType.King, Color.White);
220+
board[0][5] = new ChessPiece(ChessPieceType.Bishop, Color.White);
221+
board[0][6] = new ChessPiece(ChessPieceType.Knight, Color.White);
222+
board[0][7] = new ChessPiece(ChessPieceType.Rock, Color.White);
223+
224+
// Black pieces
225+
board[7][0] = new ChessPiece(ChessPieceType.Rock, Color.Black);
226+
board[7][1] = new ChessPiece(ChessPieceType.Knight, Color.Black);
227+
board[7][2] = new ChessPiece(ChessPieceType.Bishop, Color.Black);
228+
board[7][3] = new ChessPiece(ChessPieceType.Queen, Color.Black);
229+
board[7][4] = new ChessPiece(ChessPieceType.King, Color.Black);
230+
board[7][5] = new ChessPiece(ChessPieceType.Bishop, Color.Black);
231+
board[7][6] = new ChessPiece(ChessPieceType.Knight, Color.Black);
232+
board[7][7] = new ChessPiece(ChessPieceType.Rock, Color.Black);
233+
234+
return board;
235+
}
236+
}

0 commit comments

Comments
 (0)