diff --git a/src/dev/kske/chess/board/Board.java b/src/dev/kske/chess/board/Board.java index fed1bea..1a04150 100644 --- a/src/dev/kske/chess/board/Board.java +++ b/src/dev/kske/chess/board/Board.java @@ -4,6 +4,8 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import dev.kske.chess.board.Piece.Color; import dev.kske.chess.board.Piece.Type; @@ -182,6 +184,65 @@ public class Board { updateCastlingRights(); } + /** + * Moves a piece across the board without checking if the move is legal. + * + * @param sanMove The move to execute in SAN (Standard Algebraic Notation) + */ + public void move(String sanMove) { + Map patterns = new HashMap<>(); + patterns.put("pieceMove", + Pattern.compile( + "^(?[NBRQK])(?:(?[a-h])|(?[1-8])|(?[a-h][1-8]))?x?(?[a-h][1-8])$")); + patterns.put("pawnCapture", + Pattern + .compile("^(?[a-h])(?[1-8])?x(?[a-h][1-8])(?[NBRQK])?$")); + patterns.put("pawnPush", Pattern.compile("^(?[a-h][1-8])(?[NBRQK])?$")); + + patterns.forEach((patternName, pattern) -> { + Matcher m = pattern.matcher(sanMove); + if (m.find()) { + Position pos = null, dest = Position.fromLAN(m.group("toSquare")); + switch (patternName) { + case "pieceMove": + if (m.group("fromSquare") != null) pos = Position.fromLAN(m.group("fromSquare")); + else { + Type type = Type.fromFirstChar(m.group("pieceType").charAt(0)); + char file; + int rank; + if (m.group("fromFile") != null) { + file = m.group("fromFile").charAt(0); + rank = get(type, file); + pos = Position.fromLAN(String.format("%c%d", file, rank)); + } else if (m.group("fromRank") != null) { + rank = Integer.parseInt(m.group("fromRank").substring(0, 1)); + file = get(type, rank); + pos = Position.fromLAN(String.format("%c%d", file, rank)); + } else pos = get(type, dest); + } + break; + case "pawnCapture": + char file = m.group("fromFile").charAt(0); + int rank = m.group("fromRank") == null ? get(Type.PAWN, file) + : Integer.parseInt(m.group("fromRank")); + pos = Position.fromLAN(String.format("%c%d", file, rank)); + break; + case "pawnPush": + // TODO: Pawn promotion + int step = log.getActiveColor() == Color.WHITE ? 1 : -1; + + // One step forward + if (boardArr[dest.x][dest.y + step] != null) pos = new Position(dest.x, dest.y + step); + // Double step forward + else pos = new Position(dest.x, dest.y + 2 * step); + break; + } + move(new Move(pos, dest)); + return; + } + }); + } + /** * Reverts the last move. */ @@ -485,7 +546,7 @@ public class Board { castlingRights.put(Color.BLACK, blackCastling); // En passant availability - if (!parts[3].equals("-")) log.setEnPassant(Position.fromSAN(parts[3])); + if (!parts[3].equals("-")) log.setEnPassant(Position.fromLAN(parts[3])); // Halfmove clock log.setHalfmoveClock(Integer.parseInt(parts[4])); @@ -506,7 +567,7 @@ public class Board { for (int j = 0; j < 8; j++) { final Piece piece = boardArr[j][i]; if (piece == null) ++emptyCount; - else { + else { // TODO: rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b - - 1 2 error if (emptyCount != 0) { sb.append(emptyCount); emptyCount = 0; @@ -535,7 +596,7 @@ public class Board { final MoveNode lastMove = log.getLast(); // En passant availability - sb.append(" " + (lastMove == null || lastMove.enPassant == null ? "-" : lastMove.enPassant.toSAN())); + sb.append(" " + (lastMove == null || lastMove.enPassant == null ? "-" : lastMove.enPassant.toLAN())); // Halfmove clock sb.append(" " + String.valueOf(lastMove == null ? 0 : lastMove.halfmoveClock)); @@ -554,6 +615,58 @@ public class Board { return boardArr[pos.x][pos.y]; } + /** + * Searches for a {@link Piece} inside a file (A - H). + * + * @param type The {@link Type} of the piece to search for + * @param file The file in which to search for the piece + * @return The rank (1 - 8) of the first piece with the specified type and + * current color in the file, or {@code -1} if there isn't any + */ + public int get(Type type, char file) { + int x = file - 97; + for (int i = 0; i < 8; i++) + if (boardArr[x][i] != null && boardArr[x][i].getType() == type + && boardArr[x][i].getColor() == log.getActiveColor()) + return 8 - i; + return -1; + } + + /** + * Searches for a {@link Piece} inside a rank (1 - 8). + * + * @param type The {@link Type} of the piece to search for + * @param rank The rank in which to search for the piece + * @return The file (A - H) of the first piece with the specified type and + * current color in the file, or {@code -} if there isn't any + */ + public char get(Type type, int rank) { + int y = rank - 1; + for (int i = 0; i < 8; i++) + if (boardArr[i][y] != null && boardArr[i][y].getType() == type + && boardArr[i][y].getColor() == log.getActiveColor()) + return (char) (i + 97); + return '-'; + } + + /** + * Searches for a {@link Piece} that can move to a {@link Position}. + * + * @param type The {@link Type} of the piece to search for + * @param dest The destination that the piece is required to reach + * @return The position of a piece that can move to the specified destination + */ + public Position get(Type type, Position dest) { + for (int i = 0; i < 8; i++) + for (int j = 0; j < 8; j++) + if (boardArr[i][j] != null && boardArr[i][j].getType() == type + && boardArr[i][j].getColor() == log.getActiveColor()) { + Position pos = new Position(i, j); + if (boardArr[i][j].isValidMove(new Move(pos, dest))) return pos; + } + return null; + } + /** * Places a piece at a position. * diff --git a/src/dev/kske/chess/board/Move.java b/src/dev/kske/chess/board/Move.java index a781361..74f8cb7 100644 --- a/src/dev/kske/chess/board/Move.java +++ b/src/dev/kske/chess/board/Move.java @@ -30,13 +30,13 @@ public class Move { this(new Position(xPos, yPos), new Position(xDest, yDest)); } - public static Move fromSAN(String move) { - return new Move(Position.fromSAN(move.substring(0, 2)), - Position.fromSAN(move.substring(2))); + public static Move fromLAN(String move) { + return new Move(Position.fromLAN(move.substring(0, 2)), + Position.fromLAN(move.substring(2))); } - public String toSAN() { - return pos.toSAN() + dest.toSAN(); + public String toLAN() { + return pos.toLAN() + dest.toLAN(); } public boolean isHorizontal() { return yDist == 0; } diff --git a/src/dev/kske/chess/board/Piece.java b/src/dev/kske/chess/board/Piece.java index aa5082c..e9ca766 100644 --- a/src/dev/kske/chess/board/Piece.java +++ b/src/dev/kske/chess/board/Piece.java @@ -85,17 +85,39 @@ public abstract class Piece implements Cloneable { } public static enum Type { + KING, QUEEN, ROOK, KNIGHT, BISHOP, PAWN; - + /** - * @return The first character of this {@link Type} in algebraic notation and lower case + * @return The first character of this {@link Type} in algebraic notation and + * lower case */ public char firstChar() { return this == KNIGHT ? 'n' : Character.toLowerCase(this.toString().charAt(0)); } + + public static Type fromFirstChar(char firstChar) { + switch (Character.toLowerCase(firstChar)) { + case 'k': + return KING; + case 'q': + return QUEEN; + case 'r': + return ROOK; + case 'n': + return KNIGHT; + case 'b': + return BISHOP; + case 'p': + return PAWN; + default: + return null; + } + } } public static enum Color { + WHITE, BLACK; public static Color fromFirstChar(char c) { diff --git a/src/dev/kske/chess/board/Position.java b/src/dev/kske/chess/board/Position.java index ac9638d..5edcf7c 100644 --- a/src/dev/kske/chess/board/Position.java +++ b/src/dev/kske/chess/board/Position.java @@ -15,11 +15,11 @@ public class Position { this.y = y; } - public static Position fromSAN(String pos) { + public static Position fromLAN(String pos) { return new Position(pos.charAt(0) - 97, 8 - Character.getNumericValue(pos.charAt(1))); } - public String toSAN() { + public String toLAN() { return String.valueOf((char) (x + 97)) + String.valueOf(8 - y); } diff --git a/src/dev/kske/chess/game/UCIPlayer.java b/src/dev/kske/chess/game/UCIPlayer.java index 79d12bc..12b73d3 100644 --- a/src/dev/kske/chess/game/UCIPlayer.java +++ b/src/dev/kske/chess/game/UCIPlayer.java @@ -51,7 +51,7 @@ public class UCIPlayer extends Player implements UCIListener { @Override public void onBestMove(String move) { - Move moveObj = Move.fromSAN(move); + Move moveObj = Move.fromLAN(move); game.onMove(this, moveObj); } diff --git a/src/dev/kske/chess/uci/UCIInfo.java b/src/dev/kske/chess/uci/UCIInfo.java index 5c47f01..c68f4f4 100644 --- a/src/dev/kske/chess/uci/UCIInfo.java +++ b/src/dev/kske/chess/uci/UCIInfo.java @@ -68,7 +68,7 @@ public class UCIInfo { multipv = Integer.parseInt(tokens[++i]); break; case "currmove": - currmove = Move.fromSAN(tokens[++i]); + currmove = Move.fromLAN(tokens[++i]); break; case "currmovenumber": currmovenumber = Integer.parseInt(tokens[++i]); @@ -97,11 +97,11 @@ public class UCIInfo { break; case "pv": while (++i < tokens.length && !params.contains(tokens[i])) - pv.add(Move.fromSAN(tokens[i])); + pv.add(Move.fromLAN(tokens[i])); break; case "refutation": while (++i < tokens.length && !params.contains(tokens[i])) - refutation.add(Move.fromSAN(tokens[i])); + refutation.add(Move.fromLAN(tokens[i])); break; // TODO: currline case "currline": diff --git a/src/dev/kske/chess/uci/UCIReceiver.java b/src/dev/kske/chess/uci/UCIReceiver.java index f9d8cdc..06ba8d6 100644 --- a/src/dev/kske/chess/uci/UCIReceiver.java +++ b/src/dev/kske/chess/uci/UCIReceiver.java @@ -93,7 +93,7 @@ public class UCIReceiver implements Runnable { String move = tokens[0]; // Ponder move - if (tokens.length == 3) listeners.forEach(l -> l.onBestMove(move, Move.fromSAN(tokens[2]))); + if (tokens.length == 3) listeners.forEach(l -> l.onBestMove(move, Move.fromLAN(tokens[2]))); else listeners.forEach(l -> l.onBestMove(move)); } diff --git a/src/dev/kske/chess/ui/LogPanel.java b/src/dev/kske/chess/ui/LogPanel.java index a4b11ec..e0bff7c 100644 --- a/src/dev/kske/chess/ui/LogPanel.java +++ b/src/dev/kske/chess/ui/LogPanel.java @@ -58,8 +58,8 @@ public class LogPanel extends JPanel implements Subscribable { if (log == null || log.isEmpty()) return; final DefaultTableModel model = new DefaultTableModel(new String[] { "White", "Black" }, 0); for (Iterator iter = log.iterator(); iter.hasNext();) { - String[] row = new String[] { iter.next().move.toSAN(), "" }; - if (iter.hasNext()) row[1] = iter.next().move.toSAN(); + String[] row = new String[] { iter.next().move.toLAN(), "" }; + if (iter.hasNext()) row[1] = iter.next().move.toLAN(); model.addRow(row); } mtable.setModel(model); diff --git a/test/dev/kske/chess/board/LogTest.java b/test/dev/kske/chess/board/LogTest.java index bd688c2..4e682d4 100644 --- a/test/dev/kske/chess/board/LogTest.java +++ b/test/dev/kske/chess/board/LogTest.java @@ -43,10 +43,10 @@ class LogTest { log.setActiveColor(Color.WHITE); other.setActiveColor(Color.BLACK); assertNotEquals(log.getActiveColor(), other.getActiveColor()); - log.add(Move.fromSAN("a2a4"), null, true); - log.add(Move.fromSAN("a4a5"), null, true); - other.add(Move.fromSAN("a2a4"), null, true); - other.add(Move.fromSAN("a4a5"), null, true); + log.add(Move.fromLAN("a2a4"), null, true); + log.add(Move.fromLAN("a4a5"), null, true); + other.add(Move.fromLAN("a2a4"), null, true); + other.add(Move.fromLAN("a4a5"), null, true); assertNotEquals(log.getRoot(), other.getRoot()); assertNotEquals(log.getRoot().getVariations(), other.getRoot().getVariations()); } diff --git a/test/dev/kske/chess/board/PositionTest.java b/test/dev/kske/chess/board/PositionTest.java index 8ae1d0e..5cb4a1b 100644 --- a/test/dev/kske/chess/board/PositionTest.java +++ b/test/dev/kske/chess/board/PositionTest.java @@ -19,21 +19,21 @@ class PositionTest { /** * Test method for - * {@link dev.kske.chess.board.Position#fromSAN(java.lang.String)}. + * {@link dev.kske.chess.board.Position#fromLAN(java.lang.String)}. */ @Test void testFromSAN() { for (int i = 0; i < n; i++) - assertEquals(positions[i], Position.fromSAN(sans[i])); + assertEquals(positions[i], Position.fromLAN(sans[i])); } /** - * Test method for {@link dev.kske.chess.board.Position#toSAN()}. + * Test method for {@link dev.kske.chess.board.Position#toLAN()}. */ @Test void testToSAN() { for (int i = 0; i < n; i++) - assertEquals(sans[i], positions[i].toSAN()); + assertEquals(sans[i], positions[i].toLAN()); } /**