Elegant way to count how many pieces of each type are present in a position

Viewed 84

I have an abstract class which has 1 attributes like so:

PieceColor {
    WHITE, BLACK
}

abstract class Piece {
    PieceColor color

    abstract char getAbbr();
}

This class Piece has been extended by the classes Bishop Knight Rook King Queen and Pawn. Each of those class have a method called getAbbr() which returns the abbreviation of the Piece, KQBNRP.

Now I have a Map<Piece, Tile> piecePlacements which represents all the pieces currently on the board at a particular time.

I need to count how many pieces are there of a type say White Rook, Black Queen etc. But the normal way of doing this gets too long and is very ugly to look at. This is how I did it...

//0: White pieces, 1: Black pieces; 0-5: KQRBNP
    int[][] pieceCount = new int[2][6];

    for(Piece piece : piecePlacements.keySet()) {
        switch (piece.getAbbr()) {
            case 'K' :
                if (piece.color == PieceColor.WHITE) {
                    pieceCount[0][0]++;
                } else {
                    pieceCount[1][0]++;
                }
                break;
            case 'Q' :
                if (piece.color == PieceColor.WHITE) {
                    pieceCount[0][1]++;
                } else {
                    pieceCount[1][1]++;
                }
                break;
            case 'R' :
                if (piece.color == PieceColor.WHITE) {
                    pieceCount[0][2]++;
                } else {
                    pieceCount[1][2]++;
                }
                break;
            case 'B' :
                if (piece.color == PieceColor.WHITE) {
                    pieceCount[0][3]++;
                } else {
                    pieceCount[1][3]++;
                }
                break;
            case 'N' :
                if (piece.color == PieceColor.WHITE) {
                    pieceCount[0][4]++;
                } else {
                    pieceCount[1][4]++;
                }
                break;
            case 'P' :
                if (piece.color == PieceColor.WHITE) {
                    pieceCount[0][5]++;
                } else {
                    pieceCount[1][5]++;
                }
                break;
         }
    }

There has to be a more elegant way to this, which takes fewer lines of code. I mean what if there were more than 6 types of pieces and 2 types of colors. I cant just keep adding cases to the switch statement like this right? But I cant figure how to make this more elegant.

3 Answers

The normal way to do this would be with an enumerator (which is called enum in Java). Enumerators let you define a set of values, and they automatically do all the rest of the work, like assigning a unique ID to each value, under the hood. Here's a quick example:

public enum PieceType {
    KING,
    QUEEN,
    ROOK,
    BISHOP,
    KNIGHT,
    PAWN;
}

And then you could use this in your Piece class. I will also add proper encapsulation, which is something you should follow.

public class Piece {
    private final PieceType type;
    private final boolean isBlack;

    public Piece(PieceType type, boolean isBlack) {
        this.type = type;
        this.isBlack = isBlack;
    }

    public PieceType getType() {
        return type;
    }

    public boolean isBlack() {
        return isBlack;
    }
}

Now that we have that all set up, it becomes trivial to write your for loop:

for (Piece piece : piecePlacements) {
    pieceCount[piece.isBlack() ? 1 : 0][piece.getType().ordinal()]++;
}

For the first dimension's index, we use the ternary operator. This allows us to write a conditional expression without writing out a whole if statement. In this case, if piece.isBlack() is true, it plugs in an index of 1, else, it plugs in an index of 0.

For the second dimension's index, we use the ordinal() method. This is a special method that all enumerators implicitly have. It returns the unique ID for a given enumerator value, and the nice part is, you don't have to worry about how it comes up with that ID. It does it all automatically.

tl;dr

Define enums for both color and role of each piece.

Use the misnamed Enum#ordinal to get the index number of each enum object’s position within its definition. Use that number as the index into your arrays.

for ( Piece piece : pieces )          // `Piece` is class with two member fields: a `Color` enum object and a `Role` enum object.
{
    pieceCount                        // Your two-dimensional array. 
        [ piece.color().ordinal() ]   // Get the `Piece` object’s `Color` enum object’s definition position.
        [ piece.role().ordinal() ]    // Get the `Piece` object’s `Role` enum object’s definition position.
        ++                            // Using those two index numbers as index into your arrays, increment the count.
    ;
}

Details

Using enums makes this task much simpler.

Enums offer the method ordinal, a misnomer as it actually returns the zero-based index (not one-based ordinal) number of the order in which an enum object was defined. We can use this enum index number as the index into your arrays.

Let's define your Piece record with a pair of nested enums, Color and Role.

A record is a new feature in Java 16, for a brief way to write a class whose main purpose is transparently and immutably communicating data. The compiler implicitly creates constructor, getters, equals & hashCode, and toString. Not necessary for this solution, but ideal fit for your Piece class.

package work.basil.example.chess;

public record Piece(Color color , Role role)
{
    public enum Color
    { WHITE, BLACK }

    public enum Role
    { KING, QUEEN, ROOK, BISHOP, KNIGHT, PAWN }
}

So Piece.Color.WHITE.ordinal() returns 0, while BLACK returns 1.

We need some sample data.

List < Piece > pieces = List.of(
        new Piece( Piece.Color.BLACK , Piece.Role.QUEEN ) ,
        new Piece( Piece.Color.BLACK , Piece.Role.KNIGHT ) ,
        new Piece( Piece.Color.WHITE , Piece.Role.PAWN )
);

We can define your arrays in a soft-coded way, by asking each enum for its length.

int[][] pieceCount = new int[ Piece.Color.values().length ][ Piece.Role.values().length ]; // [2][6]

Next we loop the pieces on the board. We ask each Piece object’s Color and Role enum member field object for its index position by calling that misnamed ordinal() method.

for ( Piece piece : pieces )
{
    pieceCount[ piece.color().ordinal() ][ piece.role().ordinal() ]++;
}

Here is entire example app class.

package work.basil.example.chess;

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

public class App
{
    public static void main ( String[] args )
    {
        App app = new App();
        app.demo();
    }

    private void demo ( )
    {
        List < Piece > pieces = List.of(
                new Piece( Piece.Color.BLACK , Piece.Role.QUEEN ) ,
                new Piece( Piece.Color.BLACK , Piece.Role.KNIGHT ) ,
                new Piece( Piece.Color.WHITE , Piece.Role.PAWN )
        );
        int[][] pieceCount = new int[ Piece.Color.values().length ][ Piece.Role.values().length ]; // [2][6]
        for ( Piece piece : pieces )
        {
            pieceCount[ piece.color().ordinal() ][ piece.role().ordinal() ]++;
        }

        System.out.println( "pieces = " + pieces );
        for ( int i = 0 ; i < pieceCount.length ; i++ )
        {
            System.out.println( Arrays.toString( pieceCount[ i ] ) );
        }
    }
}

When run.

pieces = [Piece[color=BLACK, role=QUEEN], Piece[color=BLACK, role=KNIGHT], Piece[color=WHITE, role=PAWN]]
[0, 0, 0, 0, 0, 1]
[0, 1, 0, 0, 1, 0]

Besides being a clever solution to your challenge, using Java enums offers three other big wins:

Sorting things first can often speed up a count. If you know the order, you can reduce the number of tests.

Another angle is to have a prime number associated with each item. Multiply each into a total. Then divide by that prime to get a count for the corresponding item.

But truly, the right answer will probably involve enums. Also, fewer lines of code doesn't necessarily mean the calculation is done more quickly or efficiently.

Related