Intro
This post continues the A chess engine in Java: generating white pawn moves.
I was advised to choose between efficiency and type safety. Since this is my first attempt at a chess engine, I have decided that I will stick to type safety this time.
In this post, I essentially extracted the move generating logic to a dedicated class implementing an expansion interface. That way, my code will stay well modularized.
(The entire repository is here.)
Code
com.github.coderodde.game.chess.ChessBoardState.java:
package com.github.coderodde.game.chess;
import com.github.coderodde.game.chess.impl.WhitePawnExpander;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* This class implements a chess board state.
*
* @version 1.0.1 (Jun 26, 2024)
* @since 1.0.0 (Jun 22, 2024)
*/
public final class ChessBoardState {
public static final int N = 8;
private Piece[][] state;
private boolean[] whiteIsPreviouslyDoubleMoved = new boolean[N];
private boolean[] blackIsPreviouslyDoubleMoved = new boolean[N];
private byte enPassantFlags;
public ChessBoardState() {
state = new Piece[N][N];
// Black pieces:
state[0][0] = new Piece(PieceColor.BLACK, PieceType.ROOK, null);
state[0][7] = new Piece(PieceColor.BLACK, PieceType.ROOK, null);
state[0][1] = new Piece(PieceColor.BLACK, PieceType.KNIGHT, null);
state[0][6] = new Piece(PieceColor.BLACK, PieceType.KNIGHT, null);
state[0][2] = new Piece(PieceColor.BLACK, PieceType.BISHOP, null);
state[0][5] = new Piece(PieceColor.BLACK, PieceType.BISHOP, null);
state[0][3] = new Piece(PieceColor.BLACK, PieceType.QUEEN, null);
state[0][4] = new Piece(PieceColor.BLACK, PieceType.KING, null);
for (int file = 0; file < N; file++) {
state[1][file] = new Piece(PieceColor.BLACK,
PieceType.PAWN,
new WhitePawnExpander());
}
// White pieces:
state[7][0] = new Piece(PieceColor.WHITE, PieceType.ROOK, null);
state[7][7] = new Piece(PieceColor.WHITE, PieceType.ROOK, null);
state[7][1] = new Piece(PieceColor.WHITE, PieceType.KNIGHT, null);
state[7][6] = new Piece(PieceColor.WHITE, PieceType.KNIGHT, null);
state[7][2] = new Piece(PieceColor.WHITE, PieceType.BISHOP, null);
state[7][5] = new Piece(PieceColor.WHITE, PieceType.BISHOP, null);
state[7][3] = new Piece(PieceColor.WHITE, PieceType.QUEEN, null);
state[7][4] = new Piece(PieceColor.WHITE, PieceType.KING, null);
for (int file = 0; file < N; file++) {
state[6][file] = new Piece(PieceColor.WHITE,
PieceType.PAWN,
null);
}
}
public ChessBoardState(final ChessBoardState copy) {
this.state = new Piece[N][N];
for (int rank = 0; rank < N; rank++) {
for (int file = 0; file < N; file++) {
if (copy.state[rank][file] == null) {
continue;
}
this.state[rank][file] = new Piece(copy.state[rank][file]);
}
}
// TODO: Just set?
System.arraycopy(this.whiteIsPreviouslyDoubleMoved,
0,
copy.whiteIsPreviouslyDoubleMoved,
0,
N);
System.arraycopy(this.blackIsPreviouslyDoubleMoved,
0,
copy.blackIsPreviouslyDoubleMoved,
0,
N);
}
@Override
public boolean equals(final Object o) {
if (!(o instanceof ChessBoardState)) {
return false;
}
final ChessBoardState other = (ChessBoardState) o;
return Arrays.deepEquals(state, other.state);
}
@Override
public int hashCode() {
return Arrays.deepHashCode(state);
}
/**
* Clears the entire board. Used in unit testing.
*/
public void clear() {
this.state = new Piece[N][N];
}
/**
* Returns the piece value at rank {@code rank}, file {@code file}. Used in
* unit testing.
*
* @param file the file of the requested piece.
* @param rank the rank of the requested piece.
*
* @return the piece.
*/
public Piece get(final int file, final int rank) {
return state[rank][file];
}
/**
* Sets the piece {@code piece} at rank {@code rank}, file {@code file}. Used in
* unit testing.
*
* @param file the file of the requested piece.
* @param rank the rank of the requested piece.
* @param piece the piece to set.
*/
public void set(final int file,
final int rank,
final Piece piece) {
state[rank][file] = piece;
}
/**
* Clears the position at rank {@code rank} and file {@code file}. Used in
* unit testing.
*
* @param file the file of the requested piece.
* @param rank the rank of the requested piece.
*/
public void clear(final int file, final int rank) {
state[rank][file] = null;
}
/**
* Returns the array of previous double moves flags. Used in unit testing.
*
* @return the array of previous double moves flags for the white player.
*/
public boolean[] getWhiteIsPreviouslyDoubleMoved() {
return whiteIsPreviouslyDoubleMoved;
}
/**
* Returns the array of previous double moves flags. Used in unit testing.
*
* @return the array of previous double moves flags for the black player.
*/
public boolean[] getBlackIsPreviouslyDoubleMoved() {
return blackIsPreviouslyDoubleMoved;
}
/**
* Returns a simple tefiletual representation of this state. Not verrank readable.
*
* @return a tefiletual representation of this state.
*/
@Override
public String toString() {
final StringBuilder stringBuilder =
new StringBuilder((N + 3) * (N + 2));
int rankNumber = 8;
for (int rank = 0; rank < N; rank++) {
for (int file = -1; file < N; file++) {
if (file == -1) {
stringBuilder.append(rankNumber--).append(' ');
} else {
final Piece piece = state[rank][file];
stringBuilder.append(
(piece == null ?
((file + rank) % 2 == 0 ? "." : "#") :
piece));
}
}
stringBuilder.append('\n');
}
stringBuilder.append("\n abcdefgh");
return stringBuilder.toString();
}
/**
* Marks that the white pawn at file {@code file} made an initial double move.
* Used for unit testing.
*
* @param file the file number of the target white pawn.
*/
public void markWhitePawnInitialDoubleMove(final int file) {
this.whiteIsPreviouslyDoubleMoved[file] = true;
}
/**
* Marks that the black pawn at file {@code file} made an initial double move.
* Used for unit testing.
*
* @param file the file number of the target white pawn.
*/
public void markBlackPawnInitialDoubleMove(final int file) {
this.blackIsPreviouslyDoubleMoved[file] = true;
}
public CellType getCellColor(final int file, final int rank) {
final Piece piece = state[rank][file];
if (piece == null) {
return CellType.EMPTY;
}
if ((piece.getPieceCodeBits() & Piece.WHITE_COLOR) != 0) {
return CellType.WHITE;
}
if ((piece.getPieceCodeBits() & Piece.BLACK_COLOR) != 0) {
return CellType.BLACK;
}
throw new IllegalStateException("Unknown cell color: " + piece);
}
public List<ChessBoardState> expand(final PlayerTurn playerTurn) {
final List<ChessBoardState> children = new ArrayList<>();
if (null == playerTurn) {
throw new NullPointerException("playerTurn is null.");
} else switch (playerTurn) {
case WHITE -> {
for (int rank = 0; rank < N; rank++) {
for (int file = 0; file < N; file++) {
final CellType cellType = getCellColor(file, rank);
if (cellType == CellType.WHITE) {
children.addAll(
state[rank]
[file].expand(this, file, rank));
}
}
}
}
case BLACK -> throw new UnsupportedOperationException();
default -> throw new EnumConstantNotPresentException(
PlayerTurn.class,
playerTurn.name());
}
return children;
}
}
com.github.coderodde.game.chess.WhitePawnExpander.java:
package com.github.coderodde.game.chess.impl;
import com.github.coderodde.game.chess.CellType;
import com.github.coderodde.game.chess.ChessBoardState;
import static com.github.coderodde.game.chess.ChessBoardState.N;
import com.github.coderodde.game.chess.AbstractChessBoardStateExpander;
import com.github.coderodde.game.chess.Piece;
import com.github.coderodde.game.chess.PieceColor;
import com.github.coderodde.game.chess.PieceType;
import java.util.List;
/**
* This class implements an expander for generating all white pawn moves.
*
* @version 1.0.0 (Jun 26, 2024)
* @since 1.0.0 (Jun 26, 2024)
*/
public final class WhitePawnExpander extends AbstractChessBoardStateExpander {
public static final int INITIAL_WHITE_PAWN_RANK = 6;
public static final int INITIAL_WHITE_PAWN_MOVE_1_RANK = 5;
public static final int INITIAL_WHITE_PAWN_MOVE_2_RANK = 4;
public static final int EN_PASSANT_SOURCE_RANK = 3;
public static final int EN_PASSANT_TARGET_RANK = 2;
public static final int PROMOTION_SOURCE_RANK = 1;
public static final int PROMOTION_TARGET_RANK = 0;
@Override
public void expand(final ChessBoardState root,
final Piece piece,
final int file,
final int rank,
final List<ChessBoardState> children) {
if (rank == INITIAL_WHITE_PAWN_RANK
&& root.get(file, INITIAL_WHITE_PAWN_MOVE_1_RANK) == null
&& root.get(file, INITIAL_WHITE_PAWN_MOVE_2_RANK) == null) {
// Once here, we can move a white pawn two moves forward:
final ChessBoardState child = new ChessBoardState(root);
child.markWhitePawnInitialDoubleMove(file);
child.clear(file, INITIAL_WHITE_PAWN_RANK);
child.set(file, INITIAL_WHITE_PAWN_MOVE_2_RANK, piece);
children.add(child);
tryBasicMoveForward(root,
children,
file,
rank,
piece);
return;
} else if (rank == EN_PASSANT_SOURCE_RANK) {
if (file > 0) {
// Try en passant to the left:
tryEnPassantToLeft(root,
piece,
file,
children);
}
if (file < N - 1) {
// Try en passant to the right:
tryEnPassantToRight(root,
piece,
file,
children);
}
tryBasicMoveForward(root,
children,
file,
rank,
piece);
return;
} else if (rank == PROMOTION_SOURCE_RANK) {
if (file > 0 &&
root.getCellColor(file - 1,
PROMOTION_TARGET_RANK) == CellType.BLACK) {
// Once here, can capture to the left and promote:
for (final PieceType pieceType : PROMOTION_PIECE_TYPES) {
final Piece newPiece =
new Piece(
PieceColor.WHITE,
pieceType,
this);
final ChessBoardState child = new ChessBoardState(root);
child.set(file - 1, PROMOTION_TARGET_RANK, newPiece);
child.clear(file, PROMOTION_SOURCE_RANK);
children.add(child);
}
}
if (file < N - 1 &&
root.getCellColor(file + 1,
PROMOTION_TARGET_RANK) == CellType.BLACK) {
// Once here, can capture to the right and promote:
for (final PieceType pieceType : PROMOTION_PIECE_TYPES) {
final Piece newPiece =
new Piece(
PieceColor.WHITE,
pieceType,
this);
final ChessBoardState child = new ChessBoardState(root);
child.set(file + 1, PROMOTION_TARGET_RANK, newPiece);
child.clear(file, PROMOTION_SOURCE_RANK);
children.add(child);
}
}
if (root.getCellColor(file,
PROMOTION_TARGET_RANK) == CellType.EMPTY) {
// Once here, can move forward an promote:
for (final PieceType pieceType : PROMOTION_PIECE_TYPES) {
final Piece newPiece =
new Piece(
PieceColor.WHITE,
pieceType,
this);
final ChessBoardState child = new ChessBoardState(root);
child.set(file, PROMOTION_TARGET_RANK, newPiece);
child.clear(file, PROMOTION_SOURCE_RANK);
children.add(child);
}
}
return;
}
// Try move forward:
tryBasicMoveForward(root,
children,
file,
rank,
piece);
// Try capture to left:
if (file > 0
&& rank > 1
&& root.getCellColor(file - 1, rank - 1)
== CellType.BLACK) {
final ChessBoardState child = new ChessBoardState(root);
child.set(file, rank, null);
child.set(file - 1, rank - 1, piece);
children.add(child);
}
// Try capture to right:
if (file < N - 1
&& rank > 1
&& root.getCellColor(file + 1, rank - 1)
== CellType.BLACK) {
final ChessBoardState child = new ChessBoardState(root);
child.set(file, rank, null);
child.set(file + 1, rank - 1, piece);
children.add(child);
}
}
private void tryBasicMoveForward(final ChessBoardState root,
final List<ChessBoardState> children,
final int file,
final int rank,
final Piece piece) {
if (rank > 1 &&
root.getCellColor(file, rank - 1) == CellType.EMPTY) {
final ChessBoardState child = new ChessBoardState(root);
child.set(file, rank - 1, piece);
child.set(file, rank, null);
children.add(child);
}
}
private void tryEnPassantToLeft(final ChessBoardState root,
final Piece piece,
final int file,
final List<ChessBoardState> children) {
if (!root.getBlackIsPreviouslyDoubleMoved()[file - 1]) {
return;
}
final ChessBoardState child = new ChessBoardState(root);
child.clear(file, EN_PASSANT_SOURCE_RANK);
child.clear(file - 1, EN_PASSANT_SOURCE_RANK);
child.set(file - 1, EN_PASSANT_TARGET_RANK, piece);
children.add(child);
}
private void tryEnPassantToRight(final ChessBoardState root,
final Piece piece,
final int file,
final List<ChessBoardState> children) {
if (!root.getBlackIsPreviouslyDoubleMoved()[file + 1]) {
return;
}
final ChessBoardState child = new ChessBoardState(root);
child.clear(file, EN_PASSANT_SOURCE_RANK);
child.clear(file + 1, EN_PASSANT_SOURCE_RANK);
child.set(file + 1, EN_PASSANT_TARGET_RANK, piece);
children.add(child);
}
}
com.github.coderodde.game.chess.WhitePawnExpanderTest.java:
package com.github.coderodde.game.chess.impl;
import com.github.coderodde.game.chess.AbstractChessBoardStateExpander;
import com.github.coderodde.game.chess.ChessBoardState;
import com.github.coderodde.game.chess.Piece;
import static com.github.coderodde.game.chess.PieceColor.BLACK;
import static com.github.coderodde.game.chess.PieceColor.WHITE;
import com.github.coderodde.game.chess.PieceType;
import static com.github.coderodde.game.chess.PieceType.BISHOP;
import static com.github.coderodde.game.chess.PieceType.KNIGHT;
import static com.github.coderodde.game.chess.PieceType.PAWN;
import static com.github.coderodde.game.chess.PieceType.QUEEN;
import static com.github.coderodde.game.chess.PieceType.ROOK;
import com.github.coderodde.game.chess.PlayerTurn;
import static com.github.coderodde.game.chess.impl.WhitePawnExpander.EN_PASSANT_SOURCE_RANK;
import static com.github.coderodde.game.chess.impl.WhitePawnExpander.EN_PASSANT_TARGET_RANK;
import static com.github.coderodde.game.chess.impl.WhitePawnExpander.INITIAL_WHITE_PAWN_MOVE_1_RANK;
import static com.github.coderodde.game.chess.impl.WhitePawnExpander.INITIAL_WHITE_PAWN_MOVE_2_RANK;
import static com.github.coderodde.game.chess.impl.WhitePawnExpander.INITIAL_WHITE_PAWN_RANK;
import static com.github.coderodde.game.chess.impl.WhitePawnExpander.PROMOTION_SOURCE_RANK;
import static com.github.coderodde.game.chess.impl.WhitePawnExpander.PROMOTION_TARGET_RANK;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
public final class WhitePawnExpanderTest {
private ChessBoardState state;
private final AbstractChessBoardStateExpander expander =
new WhitePawnExpander();
@Before
public void before() {
state = new ChessBoardState();
state.clear();
}
@Test
public void moveWhitePawnInitialDoubleMove() {
state.set(0, INITIAL_WHITE_PAWN_RANK, new Piece(WHITE, PAWN, expander));
final List<ChessBoardState> children = state.expand(PlayerTurn.WHITE);
assertEquals(2, children.size());
final ChessBoardState move1 = new ChessBoardState();
final ChessBoardState move2 = new ChessBoardState();
move1.clear();
move2.clear();
move1.set(0,
INITIAL_WHITE_PAWN_MOVE_1_RANK,
new Piece(WHITE, PAWN, expander));
move2.set(0,
INITIAL_WHITE_PAWN_MOVE_2_RANK,
new Piece(WHITE, PAWN, expander));
assertTrue(children.contains(move1));
assertTrue(children.contains(move2));
}
@Test
public void whitePawnCannotMoveForward() {
state.set(4, 5, new Piece(WHITE, PAWN, expander));
state.set(4, 4, new Piece(BLACK, ROOK, expander));
final List<ChessBoardState> children = state.expand(PlayerTurn.WHITE);
assertTrue(children.isEmpty());
}
@Test
public void whitePawnCanEatBothDirectionsAndMoveForward() {
state.set(5, 4, new Piece(WHITE, PAWN, expander));
state.set(4, 3, new Piece(BLACK, KNIGHT, expander));
state.set(6, 3, new Piece(BLACK, ROOK, expander));
final List<ChessBoardState> children = state.expand(PlayerTurn.WHITE);
assertEquals(3, children.size());
final ChessBoardState move1 = new ChessBoardState();
final ChessBoardState move2 = new ChessBoardState();
final ChessBoardState move3 = new ChessBoardState();
move1.clear();
move2.clear();
move3.clear();
// Capture to the left:
move1.set(4, 3, new Piece(WHITE, PAWN, expander));
move1.set(6, 3, new Piece(BLACK, ROOK, expander));
// Move forward:
move2.set(5, 3, new Piece(WHITE, PAWN, expander));
move2.set(4, 3, new Piece(BLACK, KNIGHT, expander));
move2.set(6, 3, new Piece(BLACK, ROOK, expander));
// Caupture to the right:
move3.set(6, 3, new Piece(WHITE, PAWN, expander));
move3.set(4, 3, new Piece(BLACK, KNIGHT, expander));
assertTrue(children.contains(move1));
assertTrue(children.contains(move2));
assertTrue(children.contains(move3));
}
@Test
public void whitePawnCannotMakeFirstDoubleMoveDueToObstruction() {
state.set(6, 6, new Piece(WHITE, PAWN, expander));
state.set(6, 5, new Piece(BLACK, BISHOP, expander));
assertTrue(state.expand(PlayerTurn.WHITE).isEmpty());
state.clear();
state.set(4, 6, new Piece(WHITE, PAWN, expander));
state.set(4, 5, new Piece(BLACK, ROOK, expander));
assertTrue(state.expand(PlayerTurn.WHITE).isEmpty());
}
@Test
public void whitePawnPromotion() {
state.set(3, PROMOTION_SOURCE_RANK, new Piece(WHITE, PAWN, expander));
final List<ChessBoardState> children = state.expand(PlayerTurn.WHITE);
assertEquals(4, children.size());
final ChessBoardState move1 = new ChessBoardState();
final ChessBoardState move2 = new ChessBoardState();
final ChessBoardState move3 = new ChessBoardState();
final ChessBoardState move4 = new ChessBoardState();
move1.clear();
move2.clear();
move3.clear();
move4.clear();
move1.set(3, PROMOTION_TARGET_RANK, new Piece(WHITE, QUEEN));
move2.set(3, PROMOTION_TARGET_RANK, new Piece(WHITE, ROOK));
move3.set(3, PROMOTION_TARGET_RANK, new Piece(WHITE, KNIGHT));
move4.set(3, PROMOTION_TARGET_RANK, new Piece(WHITE, BISHOP));
assertTrue(children.contains(move1));
assertTrue(children.contains(move1));
assertTrue(children.contains(move1));
assertTrue(children.contains(move1));
final Set<ChessBoardState> stateSet = new HashSet<>();
stateSet.addAll(Arrays.asList(move1, move2, move3, move4));
assertEquals(4, stateSet.size());
}
@Test
public void whitePawnPromotionCaptureBoth() {
state.set(5, PROMOTION_SOURCE_RANK, new Piece(WHITE, PAWN, expander));
state.set(4, PROMOTION_TARGET_RANK, new Piece(BLACK, BISHOP));
state.set(6, PROMOTION_TARGET_RANK, new Piece(BLACK, PAWN));
final List<ChessBoardState> children = state.expand(PlayerTurn.WHITE);
assertEquals(12, children.size());
final ChessBoardState move1 = new ChessBoardState();
final ChessBoardState move2 = new ChessBoardState();
final ChessBoardState move3 = new ChessBoardState();
move1.clear();
move2.clear();
move3.clear();
// Promote forward:
move1.set(4, PROMOTION_TARGET_RANK, new Piece(BLACK, BISHOP));
move1.set(6, PROMOTION_TARGET_RANK, new Piece(BLACK, PAWN));
for (final PieceType pieceType :
AbstractChessBoardStateExpander.PROMOTION_PIECE_TYPES) {
move1.set(5, PROMOTION_TARGET_RANK, new Piece(WHITE, pieceType));
assertTrue(children.contains(move1));
}
// Promote left:
move2.set(6, 0, new Piece(BLACK, PAWN));
for (final PieceType pieceType :
AbstractChessBoardStateExpander.PROMOTION_PIECE_TYPES) {
move2.set(4, PROMOTION_TARGET_RANK, new Piece(WHITE, pieceType));
assertTrue(children.contains(move2));
}
// Promote right:
move3.set(4, 0, new Piece(BLACK, BISHOP));
for (final PieceType pieceType :
AbstractChessBoardStateExpander.PROMOTION_PIECE_TYPES) {
move3.set(6, PROMOTION_TARGET_RANK , new Piece(WHITE, pieceType));
assertTrue(children.contains(move3));
}
}
@Test
public void whitePawnEnPassantToLeft() {
state.set(0, EN_PASSANT_SOURCE_RANK, new Piece(BLACK, PAWN));
state.set(1, EN_PASSANT_TARGET_RANK, new Piece(BLACK, ROOK));
state.set(1, EN_PASSANT_SOURCE_RANK, new Piece(WHITE, PAWN, expander));
state.markBlackPawnInitialDoubleMove(0);
final List<ChessBoardState> children = state.expand(PlayerTurn.WHITE);
assertEquals(1, children.size());
final ChessBoardState move = new ChessBoardState();
move.clear();
move.set(1, EN_PASSANT_TARGET_RANK, new Piece(BLACK, ROOK));
move.set(0, EN_PASSANT_TARGET_RANK, new Piece(WHITE, PAWN));
assertTrue(children.contains(move));
}
@Test
public void whitePawnEnPassantToRight() {
state.set(3, EN_PASSANT_SOURCE_RANK, new Piece(WHITE, PAWN, expander));
state.set(4, EN_PASSANT_SOURCE_RANK, new Piece(BLACK, PAWN));
state.set(3, EN_PASSANT_TARGET_RANK, new Piece(BLACK, ROOK));
state.markBlackPawnInitialDoubleMove(4);
final List<ChessBoardState> children = state.expand(PlayerTurn.WHITE);
assertEquals(1, children.size());
final ChessBoardState move = new ChessBoardState();
move.clear();
move.set(3, EN_PASSANT_TARGET_RANK, new Piece(BLACK, ROOK));
move.set(4, EN_PASSANT_TARGET_RANK, new Piece(WHITE, PAWN));
boolean pass = children.contains(move);
assertTrue(pass);
}
}