Renamed SAN-like coordinate notation to LAN, added SAN support to Board

This commit is contained in:
Kai S. K. Engelbart 2019-10-09 21:02:22 +02:00
parent dfa3874dcf
commit c16395fe1f
Signed by: kske
GPG Key ID: 8BEB13EC5DF7EF13
10 changed files with 162 additions and 27 deletions

View File

@ -4,6 +4,8 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; 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.Color;
import dev.kske.chess.board.Piece.Type; import dev.kske.chess.board.Piece.Type;
@ -182,6 +184,65 @@ public class Board {
updateCastlingRights(); 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<String, Pattern> patterns = new HashMap<>();
patterns.put("pieceMove",
Pattern.compile(
"^(?<pieceType>[NBRQK])(?:(?<fromFile>[a-h])|(?<fromRank>[1-8])|(?<fromSquare>[a-h][1-8]))?x?(?<toSquare>[a-h][1-8])$"));
patterns.put("pawnCapture",
Pattern
.compile("^(?<fromFile>[a-h])(?<fromRank>[1-8])?x(?<toSquare>[a-h][1-8])(?<promotedTo>[NBRQK])?$"));
patterns.put("pawnPush", Pattern.compile("^(?<toSquare>[a-h][1-8])(?<promotedTo>[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. * Reverts the last move.
*/ */
@ -485,7 +546,7 @@ public class Board {
castlingRights.put(Color.BLACK, blackCastling); castlingRights.put(Color.BLACK, blackCastling);
// En passant availability // 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 // Halfmove clock
log.setHalfmoveClock(Integer.parseInt(parts[4])); log.setHalfmoveClock(Integer.parseInt(parts[4]));
@ -506,7 +567,7 @@ public class Board {
for (int j = 0; j < 8; j++) { for (int j = 0; j < 8; j++) {
final Piece piece = boardArr[j][i]; final Piece piece = boardArr[j][i];
if (piece == null) ++emptyCount; if (piece == null) ++emptyCount;
else { else { // TODO: rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b - - 1 2 error
if (emptyCount != 0) { if (emptyCount != 0) {
sb.append(emptyCount); sb.append(emptyCount);
emptyCount = 0; emptyCount = 0;
@ -535,7 +596,7 @@ public class Board {
final MoveNode lastMove = log.getLast(); final MoveNode lastMove = log.getLast();
// En passant availability // 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 // Halfmove clock
sb.append(" " + String.valueOf(lastMove == null ? 0 : lastMove.halfmoveClock)); sb.append(" " + String.valueOf(lastMove == null ? 0 : lastMove.halfmoveClock));
@ -554,6 +615,58 @@ public class Board {
return boardArr[pos.x][pos.y]; 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. * Places a piece at a position.
* *

View File

@ -30,13 +30,13 @@ public class Move {
this(new Position(xPos, yPos), new Position(xDest, yDest)); this(new Position(xPos, yPos), new Position(xDest, yDest));
} }
public static Move fromSAN(String move) { public static Move fromLAN(String move) {
return new Move(Position.fromSAN(move.substring(0, 2)), return new Move(Position.fromLAN(move.substring(0, 2)),
Position.fromSAN(move.substring(2))); Position.fromLAN(move.substring(2)));
} }
public String toSAN() { public String toLAN() {
return pos.toSAN() + dest.toSAN(); return pos.toLAN() + dest.toLAN();
} }
public boolean isHorizontal() { return yDist == 0; } public boolean isHorizontal() { return yDist == 0; }

View File

@ -85,17 +85,39 @@ public abstract class Piece implements Cloneable {
} }
public static enum Type { public static enum Type {
KING, QUEEN, ROOK, KNIGHT, BISHOP, PAWN; 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() { public char firstChar() {
return this == KNIGHT ? 'n' : Character.toLowerCase(this.toString().charAt(0)); 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 { public static enum Color {
WHITE, BLACK; WHITE, BLACK;
public static Color fromFirstChar(char c) { public static Color fromFirstChar(char c) {

View File

@ -15,11 +15,11 @@ public class Position {
this.y = y; 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))); 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); return String.valueOf((char) (x + 97)) + String.valueOf(8 - y);
} }

View File

@ -51,7 +51,7 @@ public class UCIPlayer extends Player implements UCIListener {
@Override @Override
public void onBestMove(String move) { public void onBestMove(String move) {
Move moveObj = Move.fromSAN(move); Move moveObj = Move.fromLAN(move);
game.onMove(this, moveObj); game.onMove(this, moveObj);
} }

View File

@ -68,7 +68,7 @@ public class UCIInfo {
multipv = Integer.parseInt(tokens[++i]); multipv = Integer.parseInt(tokens[++i]);
break; break;
case "currmove": case "currmove":
currmove = Move.fromSAN(tokens[++i]); currmove = Move.fromLAN(tokens[++i]);
break; break;
case "currmovenumber": case "currmovenumber":
currmovenumber = Integer.parseInt(tokens[++i]); currmovenumber = Integer.parseInt(tokens[++i]);
@ -97,11 +97,11 @@ public class UCIInfo {
break; break;
case "pv": case "pv":
while (++i < tokens.length && !params.contains(tokens[i])) while (++i < tokens.length && !params.contains(tokens[i]))
pv.add(Move.fromSAN(tokens[i])); pv.add(Move.fromLAN(tokens[i]));
break; break;
case "refutation": case "refutation":
while (++i < tokens.length && !params.contains(tokens[i])) while (++i < tokens.length && !params.contains(tokens[i]))
refutation.add(Move.fromSAN(tokens[i])); refutation.add(Move.fromLAN(tokens[i]));
break; break;
// TODO: currline // TODO: currline
case "currline": case "currline":

View File

@ -93,7 +93,7 @@ public class UCIReceiver implements Runnable {
String move = tokens[0]; String move = tokens[0];
// Ponder move // 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)); else listeners.forEach(l -> l.onBestMove(move));
} }

View File

@ -58,8 +58,8 @@ public class LogPanel extends JPanel implements Subscribable {
if (log == null || log.isEmpty()) return; if (log == null || log.isEmpty()) return;
final DefaultTableModel model = new DefaultTableModel(new String[] { "White", "Black" }, 0); final DefaultTableModel model = new DefaultTableModel(new String[] { "White", "Black" }, 0);
for (Iterator<MoveNode> iter = log.iterator(); iter.hasNext();) { for (Iterator<MoveNode> iter = log.iterator(); iter.hasNext();) {
String[] row = new String[] { iter.next().move.toSAN(), "" }; String[] row = new String[] { iter.next().move.toLAN(), "" };
if (iter.hasNext()) row[1] = iter.next().move.toSAN(); if (iter.hasNext()) row[1] = iter.next().move.toLAN();
model.addRow(row); model.addRow(row);
} }
mtable.setModel(model); mtable.setModel(model);

View File

@ -43,10 +43,10 @@ class LogTest {
log.setActiveColor(Color.WHITE); log.setActiveColor(Color.WHITE);
other.setActiveColor(Color.BLACK); other.setActiveColor(Color.BLACK);
assertNotEquals(log.getActiveColor(), other.getActiveColor()); assertNotEquals(log.getActiveColor(), other.getActiveColor());
log.add(Move.fromSAN("a2a4"), null, true); log.add(Move.fromLAN("a2a4"), null, true);
log.add(Move.fromSAN("a4a5"), null, true); log.add(Move.fromLAN("a4a5"), null, true);
other.add(Move.fromSAN("a2a4"), null, true); other.add(Move.fromLAN("a2a4"), null, true);
other.add(Move.fromSAN("a4a5"), null, true); other.add(Move.fromLAN("a4a5"), null, true);
assertNotEquals(log.getRoot(), other.getRoot()); assertNotEquals(log.getRoot(), other.getRoot());
assertNotEquals(log.getRoot().getVariations(), other.getRoot().getVariations()); assertNotEquals(log.getRoot().getVariations(), other.getRoot().getVariations());
} }

View File

@ -19,21 +19,21 @@ class PositionTest {
/** /**
* Test method for * Test method for
* {@link dev.kske.chess.board.Position#fromSAN(java.lang.String)}. * {@link dev.kske.chess.board.Position#fromLAN(java.lang.String)}.
*/ */
@Test @Test
void testFromSAN() { void testFromSAN() {
for (int i = 0; i < n; i++) 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 @Test
void testToSAN() { void testToSAN() {
for (int i = 0; i < n; i++) for (int i = 0; i < n; i++)
assertEquals(sans[i], positions[i].toSAN()); assertEquals(sans[i], positions[i].toLAN());
} }
/** /**