Enhanced FENString class, added unit test and Board#equals()

This commit is contained in:
Kai S. K. Engelbart 2019-10-24 19:54:59 +02:00
parent 3603153254
commit 7138905501
Signed by: kske
GPG Key ID: 8BEB13EC5DF7EF13
8 changed files with 212 additions and 39 deletions

View File

@ -7,6 +7,11 @@
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin_test" path="test_res">
<attributes>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/5"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
<classpathentry kind="output" path="bin"/>

View File

@ -1,9 +1,11 @@
package dev.kske.chess.board;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -576,7 +578,7 @@ public class Board {
for (int j = 0; j < 8; j++) {
final Piece piece = boardArr[j][i];
if (piece == null) ++emptyCount;
else { // TODO: rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b - - 1 2 error
else {
if (emptyCount != 0) {
sb.append(emptyCount);
emptyCount = 0;
@ -616,6 +618,25 @@ public class Board {
return sb.toString();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.deepHashCode(boardArr);
result = prime * result + Objects.hash(castlingRights, kingPos, log);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
Board other = (Board) obj;
return Arrays.deepEquals(boardArr, other.boardArr) && Objects.equals(castlingRights, other.castlingRights)
&& Objects.equals(kingPos, other.kingPos) && Objects.equals(log, other.log);
}
/**
* @param pos The position from which to return a piece
* @return The piece at the position

View File

@ -1,7 +1,5 @@
package dev.kske.chess.board;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -21,25 +19,23 @@ import dev.kske.chess.exception.ChessException;
*/
public class FENString {
private Board board;
private Map<FENField, String> fields = new LinkedHashMap<>();
public static enum FENField {
PIECE_PLACEMENT, ACTIVE_COLOR, CASTLING_AVAILABILITY, EN_PASSANT_TARGET_SQUARE, HALFMOVE_CLOCK, FULLMOVE_NUMBER
}
private Board board;
private String piecePlacement, castlingAvailability;
private int halfmoveClock, fullmoveNumber;
private Color activeColor;
private Position enPassantTargetSquare;
/**
* Constructs a {@link FENString} representing the starting position
* {@code rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1}.
*/
public FENString() {
board = new Board();
fields.put(FENField.PIECE_PLACEMENT, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR");
fields.put(FENField.ACTIVE_COLOR, "w");
fields.put(FENField.CASTLING_AVAILABILITY, "KQkq");
fields.put(FENField.EN_PASSANT_TARGET_SQUARE, "-");
fields.put(FENField.HALFMOVE_CLOCK, "0");
fields.put(FENField.FULLMOVE_NUMBER, "1");
board = new Board();
piecePlacement = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR";
activeColor = Color.WHITE;
castlingAvailability = "KQkq";
halfmoveClock = 0;
fullmoveNumber = 1;
}
/**
@ -51,11 +47,18 @@ public class FENString {
public FENString(String fen) throws ChessException {
// Check fen string against regex
Pattern fenPattern = Pattern.compile(
"^(?<PIECE_PLACEMENT>(?:[1-8nbrqkpNBRQKP]{1,8}\\/){7}[1-8nbrqkpNBRQKP]{1,8}) (?<ACTIVE_COLOR>[wb]) (?<CASTLING_AVAILABILITY>-|[KQkq]{1,4}) (?<EN_PASSANT_TARGET_SQUARE>-|[a-h][1-8]) (?<HALFMOVE_CLOCK>\\d) (?<FULLMOVE_NUMBER>\\d)$");
"^(?<piecePlacement>(?:[1-8nbrqkpNBRQKP]{1,8}\\/){7}[1-8nbrqkpNBRQKP]{1,8}) (?<activeColor>[wb]) (?<castlingAvailability>-|[KQkq]{1,4}) (?<enPassantTargetSquare>-|[a-h][1-8]) (?<halfmoveClock>\\d) (?<fullmoveNumber>\\d)$");
Matcher matcher = fenPattern.matcher(fen);
if (!matcher.find()) throw new ChessException("FEN string does not match pattern " + fenPattern.pattern());
for (FENField field : FENField.values())
fields.put(field, matcher.group(field.toString()));
// Initialize data fields
piecePlacement = matcher.group("piecePlacement");
activeColor = Color.fromFirstChar(matcher.group("activeColor").charAt(0));
castlingAvailability = matcher.group("castlingAvailability");
if (!matcher.group("enPassantTargetSquare").equals("-"))
enPassantTargetSquare = Position.fromLAN(matcher.group("enPassantTargetSquare"));
halfmoveClock = Integer.parseInt(matcher.group("halfmoveClock"));
fullmoveNumber = Integer.parseInt(matcher.group("fullmoveNumber"));
// Initialize and clean board
board = new Board();
@ -66,7 +69,7 @@ public class FENString {
// Parse individual fields
// Piece placement
final String[] rows = fields.get(FENField.PIECE_PLACEMENT).split("/");
final String[] rows = piecePlacement.split("/");
if (rows.length != 8) throw new ChessException("FEN string contains invalid piece placement");
for (int i = 0; i < 8; i++) {
final char[] cols = rows[i].toCharArray();
@ -80,22 +83,22 @@ public class FENString {
Color color = Character.isUpperCase(c) ? Color.WHITE : Color.BLACK;
switch (Character.toUpperCase(c)) {
case 'K':
board.getBoardArr()[i][j] = new King(color, board);
board.getBoardArr()[j][i] = new King(color, board);
break;
case 'Q':
board.getBoardArr()[i][j] = new Queen(color, board);
board.getBoardArr()[j][i] = new Queen(color, board);
break;
case 'R':
board.getBoardArr()[i][j] = new Rook(color, board);
board.getBoardArr()[j][i] = new Rook(color, board);
break;
case 'N':
board.getBoardArr()[i][j] = new Knight(color, board);
board.getBoardArr()[j][i] = new Knight(color, board);
break;
case 'B':
board.getBoardArr()[i][j] = new Bishop(color, board);
board.getBoardArr()[j][i] = new Bishop(color, board);
break;
case 'P':
board.getBoardArr()[i][j] = new Pawn(color, board);
board.getBoardArr()[j][i] = new Pawn(color, board);
break;
}
++j;
@ -104,19 +107,18 @@ public class FENString {
}
// Active color
board.getLog().setActiveColor(Color.fromFirstChar(fields.get(FENField.ACTIVE_COLOR).charAt(0)));
board.getLog().setActiveColor(activeColor);
// TODO: Castling availability
// En passant square
if (!fields.get(FENField.EN_PASSANT_TARGET_SQUARE).equals("-"))
board.getLog().setEnPassant(Position.fromLAN(fields.get(FENField.EN_PASSANT_TARGET_SQUARE)));
board.getLog().setEnPassant(enPassantTargetSquare);
// Halfmove clock
board.getLog().setHalfmoveClock(Integer.parseInt(fields.get(FENField.HALFMOVE_CLOCK)));
board.getLog().setHalfmoveClock(halfmoveClock);
// Fullmove number
board.getLog().setFullmoveNumber(Integer.parseInt(fields.get(FENField.FULLMOVE_NUMBER)));
board.getLog().setFullmoveNumber(fullmoveNumber);
}
/**
@ -134,7 +136,7 @@ public class FENString {
for (int i = 0; i < 8; i++) {
int empty = 0;
for (int j = 0; j < 8; j++) {
final Piece piece = board.getBoardArr()[i][j];
final Piece piece = board.getBoardArr()[j][i];
if (piece == null) ++empty;
else {
@ -159,22 +161,21 @@ public class FENString {
if (i < 7) sb.append('/');
}
fields.put(FENField.PIECE_PLACEMENT, sb.toString());
piecePlacement = sb.toString();
// Active color
fields.put(FENField.ACTIVE_COLOR, String.valueOf(board.getLog().getActiveColor().firstChar()));
activeColor = board.getLog().getActiveColor();
// TODO: Castling availability
// En passant availability
final Position enPassantPosition = board.getLog().getEnPassant();
fields.put(FENField.EN_PASSANT_TARGET_SQUARE, enPassantPosition == null ? "-" : enPassantPosition.toLAN());
enPassantTargetSquare = board.getLog().getEnPassant();
// Halfmove clock
fields.put(FENField.HALFMOVE_CLOCK, String.valueOf(board.getLog().getHalfmoveClock()));
halfmoveClock = board.getLog().getHalfmoveClock();
// Fullmove counter
fields.put(FENField.FULLMOVE_NUMBER, String.valueOf(board.getLog().getFullmoveNumber()));
fullmoveNumber = board.getLog().getFullmoveNumber();
}
/**
@ -184,7 +185,13 @@ public class FENString {
*/
@Override
public String toString() {
return String.join(" ", fields.values());
return String.format("%s %c %s %s %d %d",
piecePlacement,
activeColor.firstChar(),
castlingAvailability,
enPassantTargetSquare == null ? "-" : enPassantTargetSquare.toLAN(),
halfmoveClock,
fullmoveNumber);
}
/**

View File

@ -1,6 +1,7 @@
package dev.kske.chess.board;
import java.util.Iterator;
import java.util.Objects;
import dev.kske.chess.board.Piece.Color;
@ -160,6 +161,22 @@ public class Log implements Iterable<MoveNode> {
halfmoveClock = current.halfmoveClock;
}
@Override
public int hashCode() {
return Objects.hash(activeColor, current, enPassant, fullmoveNumber, halfmoveClock, root);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
Log other = (Log) obj;
return activeColor == other.activeColor && Objects.equals(current, other.current)
&& Objects.equals(enPassant, other.enPassant) && fullmoveNumber == other.fullmoveNumber
&& halfmoveClock == other.halfmoveClock && Objects.equals(root, other.root);
}
/**
* @return The first logged move, or {@code null} if there is none
*/

View File

@ -1,5 +1,7 @@
package dev.kske.chess.board;
import java.util.Objects;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Move.java</strong><br>
@ -50,6 +52,21 @@ public class Move {
return String.format("%s -> %s", pos, dest);
}
@Override
public int hashCode() {
return Objects.hash(dest, pos, type, xDist, xSign, yDist, ySign);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
Move other = (Move) obj;
return Objects.equals(dest, other.dest) && Objects.equals(pos, other.pos) && type == other.type
&& xDist == other.xDist && xSign == other.xSign && yDist == other.yDist && ySign == other.ySign;
}
public static enum Type {
NORMAL, PAWN_PROMOTION, CASTLING, EN_PASSANT, UNKNOWN
}

View File

@ -2,6 +2,7 @@ package dev.kske.chess.board;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import dev.kske.chess.board.Piece.Color;
@ -93,4 +94,22 @@ public class MoveNode {
public boolean hasParent() {
return parent != null;
}
@Override
public int hashCode() {
return Objects
.hash(activeColor, capturedPiece, enPassant, fullmoveCounter, halfmoveClock, move, parent, variations);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
MoveNode other = (MoveNode) obj;
return activeColor == other.activeColor && Objects.equals(capturedPiece, other.capturedPiece)
&& Objects.equals(enPassant, other.enPassant) && fullmoveCounter == other.fullmoveCounter
&& halfmoveClock == other.halfmoveClock && Objects.equals(move, other.move)
&& Objects.equals(parent, other.parent) && Objects.equals(variations, other.variations);
}
}

View File

@ -2,6 +2,7 @@ package dev.kske.chess.board;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
/**
* Project: <strong>Chess</strong><br>
@ -84,6 +85,20 @@ public abstract class Piece implements Cloneable {
--moveCounter;
}
@Override
public int hashCode() {
return Objects.hash(color, moveCounter);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
Piece other = (Piece) obj;
return color == other.color && moveCounter == other.moveCounter;
}
public static enum Type {
KING, QUEEN, ROOK, KNIGHT, BISHOP, PAWN;

View File

@ -0,0 +1,72 @@
package dev.kske.chess.board;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.exception.ChessException;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>FENStringTest.java</strong><br>
* Created: <strong>24 Oct 2019</strong><br>
*
* @author Kai S. K. Engelbart
*/
class FENStringTest {
List<String> fenStrings = new ArrayList<>();
List<Board> boards = new ArrayList<>();
void cleanBoard(Board board) {
for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++)
board.getBoardArr()[i][j] = null;
}
/**
* @throws java.lang.Exception
*/
@BeforeEach
void setUp() throws Exception {
fenStrings.addAll(Arrays.asList("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b - - 1 2"));
Board board = new Board();
board.set(Position.fromLAN("c7"), null);
board.set(Position.fromLAN("c5"), new Pawn(Color.BLACK, board));
board.set(Position.fromLAN("e4"), new Pawn(Color.WHITE, board));
board.set(Position.fromLAN("f3"), new Knight(Color.WHITE, board));
board.set(Position.fromLAN("e2"), null);
board.set(Position.fromLAN("g1"), null);
board.getLog().setActiveColor(Color.BLACK);
board.getLog().setHalfmoveClock(1);
board.getLog().setFullmoveNumber(2);
boards.add(board);
}
/**
* Test method for {@link dev.kske.chess.board.FENString#toString()}.
*/
@Test
void testToString() {
for (int i = 0; i < fenStrings.size(); i++)
assertEquals(fenStrings.get(i), new FENString(boards.get(i)).toString());
}
/**
* Test method for {@link dev.kske.chess.board.FENString#getBoard()}.
*
* @throws ChessException
*/
@Test
void testGetBoard() throws ChessException {
for (int i = 0; i < boards.size(); i++)
assertEquals(boards.get(i), new FENString(fenStrings.get(i)).getBoard());
}
}