From 713890550159d9b7a357acbd9cf676521279b26c Mon Sep 17 00:00:00 2001 From: kske Date: Thu, 24 Oct 2019 19:54:59 +0200 Subject: [PATCH] Enhanced FENString class, added unit test and Board#equals() --- .classpath | 5 ++ src/dev/kske/chess/board/Board.java | 23 +++++- src/dev/kske/chess/board/FENString.java | 83 +++++++++++--------- src/dev/kske/chess/board/Log.java | 17 ++++ src/dev/kske/chess/board/Move.java | 17 ++++ src/dev/kske/chess/board/MoveNode.java | 19 +++++ src/dev/kske/chess/board/Piece.java | 15 ++++ test/dev/kske/chess/board/FENStringTest.java | 72 +++++++++++++++++ 8 files changed, 212 insertions(+), 39 deletions(-) create mode 100644 test/dev/kske/chess/board/FENStringTest.java diff --git a/.classpath b/.classpath index e4beaea..8469573 100644 --- a/.classpath +++ b/.classpath @@ -7,6 +7,11 @@ + + + + + diff --git a/src/dev/kske/chess/board/Board.java b/src/dev/kske/chess/board/Board.java index e353a35..94b481b 100644 --- a/src/dev/kske/chess/board/Board.java +++ b/src/dev/kske/chess/board/Board.java @@ -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 diff --git a/src/dev/kske/chess/board/FENString.java b/src/dev/kske/chess/board/FENString.java index 5e53511..6183015 100644 --- a/src/dev/kske/chess/board/FENString.java +++ b/src/dev/kske/chess/board/FENString.java @@ -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 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( - "^(?(?:[1-8nbrqkpNBRQKP]{1,8}\\/){7}[1-8nbrqkpNBRQKP]{1,8}) (?[wb]) (?-|[KQkq]{1,4}) (?-|[a-h][1-8]) (?\\d) (?\\d)$"); + "^(?(?:[1-8nbrqkpNBRQKP]{1,8}\\/){7}[1-8nbrqkpNBRQKP]{1,8}) (?[wb]) (?-|[KQkq]{1,4}) (?-|[a-h][1-8]) (?\\d) (?\\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); } /** diff --git a/src/dev/kske/chess/board/Log.java b/src/dev/kske/chess/board/Log.java index d5c1794..133d42a 100644 --- a/src/dev/kske/chess/board/Log.java +++ b/src/dev/kske/chess/board/Log.java @@ -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 { 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 */ diff --git a/src/dev/kske/chess/board/Move.java b/src/dev/kske/chess/board/Move.java index 74f8cb7..34cacf9 100644 --- a/src/dev/kske/chess/board/Move.java +++ b/src/dev/kske/chess/board/Move.java @@ -1,5 +1,7 @@ package dev.kske.chess.board; +import java.util.Objects; + /** * Project: Chess
* File: Move.java
@@ -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 } diff --git a/src/dev/kske/chess/board/MoveNode.java b/src/dev/kske/chess/board/MoveNode.java index 6d4afad..e71c4b8 100644 --- a/src/dev/kske/chess/board/MoveNode.java +++ b/src/dev/kske/chess/board/MoveNode.java @@ -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); + } } \ No newline at end of file diff --git a/src/dev/kske/chess/board/Piece.java b/src/dev/kske/chess/board/Piece.java index e9ca766..79cb5d6 100644 --- a/src/dev/kske/chess/board/Piece.java +++ b/src/dev/kske/chess/board/Piece.java @@ -2,6 +2,7 @@ package dev.kske.chess.board; import java.util.Iterator; import java.util.List; +import java.util.Objects; /** * Project: Chess
@@ -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; diff --git a/test/dev/kske/chess/board/FENStringTest.java b/test/dev/kske/chess/board/FENStringTest.java new file mode 100644 index 0000000..8d2810f --- /dev/null +++ b/test/dev/kske/chess/board/FENStringTest.java @@ -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: Chess
+ * File: FENStringTest.java
+ * Created: 24 Oct 2019
+ * + * @author Kai S. K. Engelbart + */ +class FENStringTest { + + List fenStrings = new ArrayList<>(); + List 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()); + } +}