Skip to main content
3 of 4
added 2 characters in body
coderodde
  • 32k
  • 15
  • 78
  • 204

A chess engine in Java: generating white pawn moves

I decided to embark on implementing my own chess engine. The first (and perhaps most demanding) part of that endeavour is generating child states out of a given chess board state. Below, you can see my attempt to generate (only) movements of white pawns. The code also mentions a little bit of logic for moving black pawns, yet I don't want that part reviewed since it will eventually become sort of symmetric to moving the white pawns. Finally, the entire GitHub repo is here.

Code

com.github.coderodde.game.chess.ChessBoardState.java:

package com.github.coderodde.game.chess;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * This class implements a chess board state.
 * 
 * @version 1.0.0 (Jun 22, 2024)
 * @since 1.0.0 (Jun 22, 2024)
 */
public final class ChessBoardState {
    
    private static final int N = 8;
    
    public static final int EMPTY        = 0;
    public static final int WHITE_PAWN   = 1;
    public static final int WHITE_BISHOP = 2;
    public static final int WHITE_KNIGHT = 3;
    public static final int WHITE_ROOK   = 4;
    public static final int WHITE_QUEEN  = 5;
    public static final int WHITE_KING   = 6;
 
    public static final int BLACK_PAWN   = 9;
    public static final int BLACK_BISHOP = 10;
    public static final int BLACK_KNIGHT = 11;
    public static final int BLACK_ROOK   = 12;
    public static final int BLACK_QUEEN  = 13;
    public static final int BLACK_KING   = 14;
    
    private static final int CELL_COLOR_NONE  = 0;
    private static final int CELL_COLOR_WHITE = +1;
    private static final int CELL_COLOR_BLACK = -1;
    
    private int[][] state = new int[N][N];
    private boolean[] whiteIsPreviouslyDoubleMoved = new boolean[N];
    private boolean[] blackIsPreviouslyDoubleMoved = new boolean[N];
    
    public ChessBoardState() {
        
        // Black pieces:
        state[0][0] =
        state[0][7] = BLACK_ROOK;
  
        state[0][1] = 
        state[0][6] = BLACK_KNIGHT;
        
        state[0][2] = 
        state[0][5] = BLACK_BISHOP;
  
        state[0][3] = BLACK_QUEEN;
        state[0][4] = BLACK_KING;
        
        for (int x = 0; x < N; x++) {
            state[1][x] = BLACK_PAWN;
        }
        
        // White pieces:
        state[7][0] =
        state[7][7] = WHITE_ROOK;
  
        state[7][1] = 
        state[7][6] = WHITE_KNIGHT;
        
        state[7][2] = 
        state[7][5] = WHITE_BISHOP;
        
        state[7][3] = WHITE_QUEEN;
        state[7][4] = WHITE_KING;
        
        for (int x = 0; x < N; x++) {
            state[6][x] = WHITE_PAWN;
        }
    }
    
    public ChessBoardState(final ChessBoardState copy) {
        this.state = new int[N][N];
        
        for (int y = 0; y < N; y++) {
            System.arraycopy(copy.state[y], 0, this.state[y], 0, N);
        }
        
        this.whiteIsPreviouslyDoubleMoved = copy.whiteIsPreviouslyDoubleMoved;
        this.blackIsPreviouslyDoubleMoved = copy.blackIsPreviouslyDoubleMoved;
    }
    
    @Override
    public boolean equals(final Object o) {
        if (o == this) {
            return true;
        }
        
        if (o == null) {
            return false;
        }
        
        if (!getClass().equals(o.getClass())) {
            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 int[N][N];
    }
    
    /**
     * Returns the piece value at rank {@code y}, file {@code x}. Used in unit 
     * testing.
     * 
     * @param x the file of the requested piece.
     * @param y the rank of the requested piece.
     * 
     * @return the piece value.
     */
    public int get(final int x, final int y) {
        return state[y][x];
    }
    
    /**
     * Sets the piece {@code piece} at rank {@code y}, file {@code x}. Used in 
     * unit testing.
     * 
     * @param x the file of the requested piece.
     * @param y the rank of the requested piece.
     * 
     * @param piece the value of the desired piece.
     */
    public void set(final int x, final int y, final int piece) {
        state[y][x] = piece;
    }
    
    /**
     * Clears the position at rank {@code y} and file {@code x}. Used in unit 
     * testing.
     * 
     * @param x the file of the requested piece.
     * @param y the rank of the requested piece.
     */
    public void clear(final int x, final int y) {
        state[y][x] = EMPTY;
    }
    
    /**
     * Returns a simple textual representation of this state. Not very readable.
     * 
     * @return a textual representation of this state.
     */
    @Override
    public String toString() {
        final StringBuilder stringBuilder =
                new StringBuilder((N + 3) * (N + 2));
        
        int rankNumber = 8;
        
        for (int y = 0; y < N; y++) {
            for (int x = -1; x < N; x++) {
                if (x == -1) {
                    stringBuilder.append(rankNumber--).append(' ');
                } else {
                    stringBuilder.append(
                            convertPieceCodeToUnicodeCharacter(x, y));
                }
            }
            
            stringBuilder.append('\n');
        }
        
        stringBuilder.append("\n  abcdefgh");
        return stringBuilder.toString();
    }
    
    public List<ChessBoardState> expand(final PlayerTurn playerTurn) {
        
        final List<ChessBoardState> children = new ArrayList<>();
        
        if (playerTurn == PlayerTurn.WHITE) {
            for (int y = 0; y < N; y++) {
                for (int x = 0; x < N; x++) {
                    final int cellColor = getCellColor(x, y);

                    if (cellColor != CELL_COLOR_WHITE) {
                        continue;
                    }

                    expandWhiteMovesImpl(children, x, y);
                }
            }
        } else { // playerTurn == PlayerTurn.BLACK
            for (int y = 0; y < N; y++) {
                for (int x = 0; x < N; x++) {
                    final int cellColor = getCellColor(x, y);

                    if (cellColor != CELL_COLOR_BLACK) {
                        continue;
                    }

                    expandBlackMovesImpl(children, x, y);
                }
            }    
        }
        
        return children;
    }
    
    /**
     * Marks that the white pawn at file {@code x} made an initial double move.
     * Used for unit testing.
     * 
     * @param x the file number of the target white pawn. 
     */
    public void markWhitePawnInitialDoubleMove(final int x) {
        this.whiteIsPreviouslyDoubleMoved[x] = true;
    }
    
    /**
     * Marks that the black pawn at file {@code x} made an initial double move.
     * Used for unit testing.
     * 
     * @param x the file number of the target white pawn. 
     */
    public void markBlackPawnInitialDoubleMove(final int x) {
        this.blackIsPreviouslyDoubleMoved[x] = true;
    }
    
    private void expandWhiteMovesImpl(final List<ChessBoardState> children,
                                      final int x,
                                      final int y) {
        
        unmarkAllInitialWhiteDoubleMoveFlags();
        
        final int cell = state[y][x];
        
        switch (cell) {
            case WHITE_PAWN:
                expandImplWhitePawn(children, x, y);
                break;
                
            case WHITE_ROOK:
                break;
                
            case WHITE_BISHOP:
                break;
                
            case WHITE_KNIGHT:
                break;
                
            case WHITE_QUEEN:
                break;
                
            case WHITE_KING:
                break;
                
            default:
                throw new IllegalStateException("Should not get here.");
        }
    }
    
    private void expandBlackMovesImpl(final List<ChessBoardState> children,
                                      final int x,
                                      final int y) {
        throw new UnsupportedOperationException();
    }
    
    private void expandImplWhitePawn(final List<ChessBoardState> children,
                                     final int x,
                                     final int y) {
        
        if (y == 6 && state[5][x] == EMPTY 
                   && state[4][x] == EMPTY) {
           
            // Once here, can move the white pawn two moves forward:
            final ChessBoardState child = new ChessBoardState(this);

            child.markWhitePawnInitialDoubleMove(x);
            
            child.state[6][x] = EMPTY; 
            child.state[4][x] = WHITE_PAWN;
            
            children.add(child);
            
            this.markWhitePawnInitialDoubleMove(x);
        }
        
        if (y == 3) {
            // Try en passant, white pawn can capture a black onen?
            if (x > 0) {
                // Try en passant to the left:
                enPassantWhitePawnToLeft(x, children);
            }
            
            if (x < N - 1) {
                // Try en passant to the right:
                enPassantWhitePawnToRight(x, children);
            }
        }
        
        if (state[y - 1][x] == EMPTY && y == 1) {
            // Once here, can do promotion:
            addWhitePromotion(children,
                              this,
                              x);
            return;
        }
        
        // Move forward:
        if (y > 0 && getCellColor(x, y - 1) == CELL_COLOR_NONE) {
            // Once here, can move forward:
            ChessBoardState child = new ChessBoardState(this);

            child.state[y][x] = EMPTY;
            child.state[y - 1][x] = WHITE_PAWN;
            children.add(child);
        }
        
        if (x > 0 && y > 0 && getCellColor(x - 1, y - 1) == CELL_COLOR_BLACK) {
            // Once here, can capture to the left:
            final ChessBoardState child = new ChessBoardState(this);
            
            child.state[y][x] = EMPTY;
            child.state[y - 1][x - 1] = WHITE_PAWN;
            
            children.add(child);
        }
        
        if (x < N - 1 && y > 0 
                      && getCellColor(x + 1, y - 1) == CELL_COLOR_BLACK) {
            // Once here, can capture to the right:
            final ChessBoardState child = new ChessBoardState(this);
            
            child.state[y][x] = EMPTY;
            child.state[y - 1][x + 1] = WHITE_PAWN;
            
            children.add(child);
        }
    }
    
    /**
     * Tries to perform an en passant by the white pawn at the file {@code x} 
     * to a black pawn at the file {@code x - 1}.
     * 
     * @param x        the file of the capturing white pawn.
     * @param children the list of child n
     */
    private void enPassantWhitePawnToLeft(
            final int x, 
            final List<ChessBoardState> children) {
        
        if (!blackIsPreviouslyDoubleMoved[x - 1]) {
            return;
        }
        
        final ChessBoardState child = new ChessBoardState(this);
        
        child.clear(x, 3);
        child.clear(x - 1, 3);
        child.set(x - 1, 2, WHITE_PAWN);
        
        children.add(child);
    }
    
    private void enPassantWhitePawnToRight(
            final int x,
            final List<ChessBoardState> children) {
        
        if (!blackIsPreviouslyDoubleMoved[x + 1]) {
            return;
        }
        
        final ChessBoardState child = new ChessBoardState(this);
        
        child.clear(x, 3);
        child.clear(x + 1, 3);
        child.set(x + 1, 2, WHITE_PAWN);
        
        children.add(child);
    }
    
    private void unmarkAllInitialWhiteDoubleMoveFlags() {
        for (int i = 0; i < N; i++) {
            this.whiteIsPreviouslyDoubleMoved[i] = false;
        }
    }
    
    private void expandImplBlackPawn(final List<ChessBoardState> children,
                                     final int x,
                                     final int y) {
        
        if (y == 6 && state[2][x] == EMPTY 
                   && state[3][x] == EMPTY) {
            
            // Once here, can move the black pawn two moves forward:
            final ChessBoardState child = new ChessBoardState(this);
//            
//            child.unsetBlackInitialMovePawn(x);
            child.state[2][x] = EMPTY;
            child.state[4][x] = BLACK_PAWN;
        }
        
    }
    
    private void addWhitePromotion(
            final List<ChessBoardState> children,
            final ChessBoardState state,
            final int x) {
        
        ChessBoardState child = new ChessBoardState(state);
        child.state[0][x] = WHITE_QUEEN;
        child.state[1][x] = EMPTY;
        children.add(child);
        
        if (x > 0 && getCellColor(x - 1, 0) == CELL_COLOR_BLACK) {
            // Can capture/promote to the left:
            child = new ChessBoardState(state);
            child.state[0][x - 1] = WHITE_QUEEN;
            child.state[1][x] = EMPTY;
            children.add(child);
        }
        
        if (x < N - 1 && getCellColor(x + 1, 0) == CELL_COLOR_BLACK) {
            // Can capture/promote to the right:
            child = new ChessBoardState(state);
            child.state[0][x + 1] = WHITE_QUEEN;
            child.state[1][x] = EMPTY;
            children.add(child);
        }
    }
    
    private void addBlackPromotion(final List<ChessBoardState> children,
                                   final ChessBoardState state,
                                   final int x) {
        
        final ChessBoardState child = new ChessBoardState(state);
        child.state[7][x] = BLACK_QUEEN;
        children.add(child);
    }
    
    /**
     * Returns the color of the cell at file {@code (x + 1)} and rank 
     * {@code 8 - y}.
     * 
     * @param x the file index.
     * @param y the rank index.
     * 
     * @return {@link #CELL_COLOR_NONE} if the requested cell is empty,
     *         {@link #CELL_COLOR_WHITE} if the requested cell is white, and,
     *         {@link #CELL_COLOR_BLACK} if the requested cell is black.
     */
    private int getCellColor(final int x, final int y) {
        final int cell = state[y][x];
        
        if (cell == EMPTY) {
            return CELL_COLOR_NONE;
        }
        
        return 0 < cell && cell < 7 ? CELL_COLOR_WHITE : 
                                      CELL_COLOR_BLACK;
    }
    
    private char convertPieceCodeToUnicodeCharacter(final int x, final int y) {
        final int pieceCode = state[y][x];
        
        switch (pieceCode) {
            case EMPTY -> {
                return (x + y) % 2 == 0 ? '.' : '#';
            }
                
            case WHITE_PAWN -> {
                return 'P';
            }
                
            case WHITE_KNIGHT -> {
                return 'K';
            }
                
            case WHITE_BISHOP -> {
                return 'B';
            }
                
            case WHITE_ROOK -> {
                return 'R';
            }
                
            case WHITE_QUEEN -> {
                return 'Q';
            }
                
            case WHITE_KING -> {
                return 'X';
            }
                
            case BLACK_PAWN -> {
                return 'p';
            }
                
            case BLACK_KNIGHT -> {
                return 'k';
            }
                
            case BLACK_BISHOP -> {
                return 'b';
            }
                
            case BLACK_ROOK -> {
                return 'r';
            }
                
            case BLACK_QUEEN -> {
                return 'q';
            }
                
            case BLACK_KING -> {
                return 'x';
            }
                
            default -> throw new IllegalStateException("Should not get here.");
        }
    }
}

The unit test class follows.

com.github.coderodde.game.chess.ChessBoardStateTest.java:

package com.github.coderodde.game.chess;

import static com.github.coderodde.game.chess.ChessBoardState.BLACK_BISHOP;
import static com.github.coderodde.game.chess.ChessBoardState.BLACK_KNIGHT;
import static com.github.coderodde.game.chess.ChessBoardState.BLACK_PAWN;
import static com.github.coderodde.game.chess.ChessBoardState.BLACK_ROOK;
import static com.github.coderodde.game.chess.ChessBoardState.WHITE_BISHOP;
import static com.github.coderodde.game.chess.ChessBoardState.WHITE_PAWN;
import static com.github.coderodde.game.chess.ChessBoardState.WHITE_QUEEN;
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 ChessBoardStateTest {
    
    private ChessBoardState state;
    
    @Before
    public void before() {
        state = new ChessBoardState();
        state.clear();
    }
    
    @Test
    public void moveWhitePawnInitialDoubleMove() {
        state.set(0, 6, WHITE_PAWN);
        
        final List<ChessBoardState> children = state.expand(PlayerTurn.WHITE);
        
        assertEquals(2, children.size());
        
        final ChessBoardState child1 = children.get(0);
        final ChessBoardState child2 = children.get(1);
        
        assertTrue(children.contains(child1));
        assertTrue(children.contains(child2));
        
        final Set<Integer> indexSet = new HashSet<>();
        
        indexSet.add(children.indexOf(child1));
        indexSet.add(children.indexOf(child2));
        
        assertEquals(2, indexSet.size());
        
        final ChessBoardState move1 = new ChessBoardState();
        final ChessBoardState move2 = new ChessBoardState();
        
        move1.clear();
        move2.clear();
        
        move1.set(0, 5, WHITE_PAWN);
        move2.set(0, 4, WHITE_PAWN);
        
        assertTrue(children.contains(move1));
        assertTrue(children.contains(move2));
    }
    
    @Test
    public void whitePawnCannotMoveForward() {
        state.set(4, 5, WHITE_PAWN);
        state.set(4, 4, BLACK_ROOK);
        
        final List<ChessBoardState> children = state.expand(PlayerTurn.WHITE);
        
        assertTrue(children.isEmpty());
    }
    
    @Test
    public void whitePawnCanEatBothDirectionsAndMoveForward() {
        state.set(5, 4, WHITE_PAWN);
        state.set(4, 3, BLACK_KNIGHT);
        state.set(6, 3, BLACK_ROOK);
        
        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();
        
        // Eat to the left:
        move1.set(4, 3, WHITE_PAWN);
        move1.set(6, 3, BLACK_ROOK);
        
        // Move forward:
        move2.set(5, 3, WHITE_PAWN);
        move2.set(4, 3, BLACK_KNIGHT);
        move2.set(6, 3, BLACK_ROOK);
        
        // Eat to the right:
        move3.set(6, 3, WHITE_PAWN);
        move3.set(4, 3, BLACK_KNIGHT);
        
        assertTrue(children.contains(move1));
        assertTrue(children.contains(move2));
        assertTrue(children.contains(move3));
    }
    
    @Test
    public void whitePawnCannotMakeFirstDoubleMoveDueToObstruction() {
        state.set(6, 6, WHITE_PAWN);
        state.set(6, 5, WHITE_BISHOP);
        
        assertTrue(state.expand(PlayerTurn.WHITE).isEmpty());
        
        state.clear();
        state.set(4, 6, WHITE_PAWN);
        state.set(4, 5, BLACK_ROOK);
        
        assertTrue(state.expand(PlayerTurn.WHITE).isEmpty());
    }
    
    @Test
    public void whitePawnPromotion() {
        state.set(3, 1, WHITE_PAWN);
        final List<ChessBoardState> children = state.expand(PlayerTurn.WHITE);
        
        assertEquals(1, children.size());
        
        final ChessBoardState move = new ChessBoardState();
        
        move.clear();
        move.set(3, 0, WHITE_QUEEN);
        
        assertEquals(move, children.get(0));
    }
    
    @Test
    public void whitePawnPromotionCaptureBoth() {
        state.set(5, 1, WHITE_PAWN);
        state.set(4, 0, BLACK_BISHOP);
        state.set(6, 0, BLACK_PAWN);
        
        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();
        
        // Queen forward:
        move1.set(4, 0, BLACK_BISHOP);
        move1.set(5, 0, WHITE_QUEEN);
        move1.set(6, 0, BLACK_PAWN);
        
        // Queen left:
        move2.set(4, 0, WHITE_QUEEN);
        move2.set(6, 0, BLACK_PAWN);
        
        // Queen right:
        move3.set(6, 0, WHITE_QUEEN);
        move3.set(4, 0, BLACK_BISHOP);
        
        assertTrue(children.contains(move1));
        assertTrue(children.contains(move2));
        assertTrue(children.contains(move3));
        
        final Set<Integer> indexSet = new HashSet<>();
        
        indexSet.add(children.indexOf(move1));
        indexSet.add(children.indexOf(move2));
        indexSet.add(children.indexOf(move3));
        
        assertEquals(3, indexSet.size());
    }
    
    @Test
    public void whitePawnEnPassantToLeft() {
        state.set(0, 3, BLACK_PAWN);
        state.set(1, 2, BLACK_ROOK);
        state.set(1, 3, WHITE_PAWN);
        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, 2, BLACK_ROOK);
        move.set(0, 2, WHITE_PAWN);
        
        assertTrue(children.contains(move));
    }
    
    @Test
    public void whitePawnEnPassantToRight() {
        state.set(7, 3, BLACK_PAWN);
        state.set(6, 2, BLACK_ROOK);
        state.set(6, 3, WHITE_PAWN);
        state.markBlackPawnInitialDoubleMove(7);
        
        final List<ChessBoardState> children = state.expand(PlayerTurn.WHITE);
        
        assertEquals(1, children.size());
        
        final ChessBoardState move = new ChessBoardState();
        
        move.clear();
        move.set(6, 2, BLACK_ROOK);
        move.set(7, 2, WHITE_PAWN);
        
        assertTrue(children.contains(move));
    }
}

Critique request

First of all, do I break the rules? Also, I would like whatever comes to mind.

coderodde
  • 32k
  • 15
  • 78
  • 204