Implemented game serialization to the PGN format #16
@ -6,8 +6,6 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import dev.kske.chess.board.Piece.Color;
|
import dev.kske.chess.board.Piece.Color;
|
||||||
|
|
||||||
@ -36,9 +34,9 @@ public class Board {
|
|||||||
* apart from the current {@link MoveNode}.
|
* apart from the current {@link MoveNode}.
|
||||||
*
|
*
|
||||||
* @param other The {@link Board} instance to copy
|
* @param other The {@link Board} instance to copy
|
||||||
|
* @param copyVariations TODO
|
||||||
*/
|
*/
|
||||||
public Board(Board other) {
|
public Board(Board other, boolean copyVariations) {
|
||||||
boardArr = new Piece[8][8];
|
|
||||||
for (int i = 0; i < 8; i++)
|
for (int i = 0; i < 8; i++)
|
||||||
for (int j = 0; j < 8; j++) {
|
for (int j = 0; j < 8; j++) {
|
||||||
if (other.boardArr[i][j] == null) continue;
|
if (other.boardArr[i][j] == null) continue;
|
||||||
@ -47,7 +45,11 @@ public class Board {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kingPos.putAll(other.kingPos);
|
kingPos.putAll(other.kingPos);
|
||||||
log = new Log(other.log, false);
|
log = new Log(other.log, copyVariations);
|
||||||
|
|
||||||
|
// Synchronize the current move node with the board
|
||||||
|
while (log.getLast().hasVariations())
|
||||||
|
log.selectNextNode(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -98,83 +100,7 @@ public class Board {
|
|||||||
* @param sanMove The move to execute in SAN (Standard Algebraic Notation)
|
* @param sanMove The move to execute in SAN (Standard Algebraic Notation)
|
||||||
*/
|
*/
|
||||||
public void move(String sanMove) {
|
public void move(String sanMove) {
|
||||||
Map<String, Pattern> patterns = new HashMap<>();
|
move(Move.fromSAN(sanMove, this));
|
||||||
patterns.put("pieceMove",
|
|
||||||
Pattern.compile(
|
|
||||||
"^(?<pieceType>[NBRQK])(?:(?<fromFile>[a-h])|(?<fromRank>[1-8])|(?<fromSquare>[a-h][1-8]))?x?(?<toSquare>[a-h][1-8])(?:\\+{0,2}|\\#)$"));
|
|
||||||
patterns.put("pawnCapture",
|
|
||||||
Pattern.compile("^(?<fromFile>[a-h])(?<fromRank>[1-8])?x(?<toSquare>[a-h][1-8])(?<promotedTo>[NBRQ])?(?:\\+{0,2}|\\#)?$"));
|
|
||||||
patterns.put("pawnPush", Pattern.compile("^(?<toSquare>[a-h][1-8])(?<promotedTo>[NBRQ])?(?:\\+{0,2}|\\#)$"));
|
|
||||||
patterns.put("castling", Pattern.compile("^(?<queenside>O-O-O)|(?<kingside>O-O)(?:\\+{0,2}|\\#)?$"));
|
|
||||||
|
|
||||||
patterns.forEach((patternName, pattern) -> {
|
|
||||||
Matcher m = pattern.matcher(sanMove);
|
|
||||||
if (m.find()) {
|
|
||||||
Position pos = null, dest = null;
|
|
||||||
Move move = null;
|
|
||||||
switch (patternName) {
|
|
||||||
case "pieceMove":
|
|
||||||
dest = Position.fromLAN(m.group("toSquare"));
|
|
||||||
if (m.group("fromSquare") != null) pos = Position.fromLAN(m.group("fromSquare"));
|
|
||||||
else {
|
|
||||||
Class<? extends Piece> pieceClass = Piece.fromFirstChar(m.group("pieceType").charAt(0));
|
|
||||||
char file;
|
|
||||||
int rank;
|
|
||||||
if (m.group("fromFile") != null) {
|
|
||||||
file = m.group("fromFile").charAt(0);
|
|
||||||
rank = get(pieceClass, 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(pieceClass, rank);
|
|
||||||
pos = Position.fromLAN(String.format("%c%d", file, rank));
|
|
||||||
} else pos = get(pieceClass, dest);
|
|
||||||
}
|
|
||||||
move = new Move(pos, dest);
|
|
||||||
break;
|
|
||||||
case "pawnCapture":
|
|
||||||
char file = m.group("fromFile").charAt(0);
|
|
||||||
int rank = m.group("fromRank") == null ? get(Pawn.class, file) : Integer.parseInt(m.group("fromRank"));
|
|
||||||
|
|
||||||
dest = Position.fromLAN(m.group("toSquare"));
|
|
||||||
pos = Position.fromLAN(String.format("%c%d", file, rank));
|
|
||||||
|
|
||||||
if (m.group("promotedTo") != null) {
|
|
||||||
try {
|
|
||||||
move = new PawnPromotion(pos, dest, Piece.fromFirstChar(m.group("promotedTo").charAt(0)));
|
|
||||||
} catch (NoSuchMethodException | SecurityException | IllegalArgumentException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
} else move = new Move(pos, dest);
|
|
||||||
break;
|
|
||||||
case "pawnPush":
|
|
||||||
dest = Position.fromLAN(m.group("toSquare"));
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (m.group("promotedTo") != null) {
|
|
||||||
try {
|
|
||||||
move = new PawnPromotion(pos, dest, Piece.fromFirstChar(m.group("promotedTo").charAt(0)));
|
|
||||||
} catch (NoSuchMethodException | SecurityException | IllegalArgumentException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
} else move = new Move(pos, dest);
|
|
||||||
break;
|
|
||||||
case "castling":
|
|
||||||
pos = new Position(4, log.getActiveColor() == Color.WHITE ? 7 : 0);
|
|
||||||
dest = new Position(m.group("kingside") != null ? 6 : 2, pos.y);
|
|
||||||
move = new Castling(pos, dest);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
move(move);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,4 +32,13 @@ public class Castling extends Move {
|
|||||||
super.revert(board, capturedPiece);
|
super.revert(board, capturedPiece);
|
||||||
rookMove.revert(board, null);
|
rookMove.revert(board, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {@code O-O-O} for a queenside castling or {@code O-O} for a kingside
|
||||||
|
* castling
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toSAN(Board board) {
|
||||||
|
return rookMove.pos.x == 0 ? "O-O-O" : "O-O";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,12 +43,17 @@ public class Log implements Iterable<MoveNode> {
|
|||||||
|
|
||||||
// The new root is the current node of the copied instance
|
// The new root is the current node of the copied instance
|
||||||
if (!other.isEmpty()) {
|
if (!other.isEmpty()) {
|
||||||
root = new MoveNode(other.current, copyVariations);
|
root = new MoveNode(other.root, copyVariations);
|
||||||
root.setParent(null);
|
root.setParent(null);
|
||||||
current = root;
|
current = root;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return an iterator over all {@link MoveNode} objects that are either the
|
||||||
|
* root node or a first variation of another node, starting from the
|
||||||
|
* root node
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Iterator<MoveNode> iterator() {
|
public Iterator<MoveNode> iterator() {
|
||||||
return new Iterator<MoveNode>() {
|
return new Iterator<MoveNode>() {
|
||||||
@ -98,7 +103,7 @@ public class Log implements Iterable<MoveNode> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removed the last move from the log and adjusts its state to the previous
|
* Removes the last move from the log and adjusts its state to the previous
|
||||||
* move.
|
* move.
|
||||||
*/
|
*/
|
||||||
public void removeLast() {
|
public void removeLast() {
|
||||||
@ -109,8 +114,14 @@ public class Log implements Iterable<MoveNode> {
|
|||||||
} else reset();
|
} else reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {@code true} if the root node exists
|
||||||
|
*/
|
||||||
public boolean isEmpty() { return root == null; }
|
public boolean isEmpty() { return root == null; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {@code true} if the current node has a parent node
|
||||||
|
*/
|
||||||
public boolean hasParent() { return !isEmpty() && current.hasParent(); }
|
public boolean hasParent() { return !isEmpty() && current.hasParent(); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -159,6 +170,10 @@ public class Log implements Iterable<MoveNode> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the active color, castling rights, en passant target square, fullmove
|
||||||
|
* number and halfmove clock to those of the current {@link MoveNode}.
|
||||||
|
*/
|
||||||
private void update() {
|
private void update() {
|
||||||
activeColor = current.activeColor;
|
activeColor = current.activeColor;
|
||||||
castlingRights = current.castlingRights.clone();
|
castlingRights = current.castlingRights.clone();
|
||||||
@ -167,6 +182,15 @@ public class Log implements Iterable<MoveNode> {
|
|||||||
halfmoveClock = current.halfmoveClock;
|
halfmoveClock = current.halfmoveClock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removed the castling rights bound to a rook or king for the rest of the game.
|
||||||
|
* This method should be called once the piece has been moved, as a castling
|
||||||
|
* move involving this piece is forbidden afterwards.
|
||||||
|
*
|
||||||
|
* @param piece the rook or king to disable the castling rights for
|
||||||
|
* @param initialPosition the initial position of the piece during the start of
|
||||||
|
* the game
|
||||||
|
*/
|
||||||
private void disableCastlingRights(Piece piece, Position initialPosition) {
|
private void disableCastlingRights(Piece piece, Position initialPosition) {
|
||||||
// Kingside
|
// Kingside
|
||||||
if (piece instanceof King || piece instanceof Rook && initialPosition.x == 7)
|
if (piece instanceof King || piece instanceof Rook && initialPosition.x == 7)
|
||||||
@ -220,7 +244,7 @@ public class Log implements Iterable<MoveNode> {
|
|||||||
|
|
||||||
public int getFullmoveNumber() { return fullmoveNumber; }
|
public int getFullmoveNumber() { return fullmoveNumber; }
|
||||||
|
|
||||||
public void setFullmoveNumber(int fullmoveCounter) { this.fullmoveNumber = fullmoveCounter; }
|
public void setFullmoveNumber(int fullmoveNumber) { this.fullmoveNumber = fullmoveNumber; }
|
||||||
|
|
||||||
public int getHalfmoveClock() { return halfmoveClock; }
|
public int getHalfmoveClock() { return halfmoveClock; }
|
||||||
|
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
package dev.kske.chess.board;
|
package dev.kske.chess.board;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import dev.kske.chess.board.Piece.Color;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project: <strong>Chess</strong><br>
|
* Project: <strong>Chess</strong><br>
|
||||||
@ -44,7 +50,7 @@ public class Move {
|
|||||||
if (move.length() == 5) {
|
if (move.length() == 5) {
|
||||||
try {
|
try {
|
||||||
return new PawnPromotion(pos, dest, Piece.fromFirstChar(move.charAt(4)));
|
return new PawnPromotion(pos, dest, Piece.fromFirstChar(move.charAt(4)));
|
||||||
} catch (NoSuchMethodException | SecurityException | IllegalArgumentException e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -53,6 +59,116 @@ public class Move {
|
|||||||
|
|
||||||
public String toLAN() { return getPos().toLAN() + getDest().toLAN(); }
|
public String toLAN() { return getPos().toLAN() + getDest().toLAN(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a move string from standard algebraic notation to a {@link Move}
|
||||||
|
* object.
|
||||||
|
*
|
||||||
|
* @param sanMove the move string to convert from
|
||||||
|
* @param board the board on which the move has to be executed
|
||||||
|
* @return the converted {@link Move} object
|
||||||
|
*/
|
||||||
|
public static Move fromSAN(String sanMove, Board board) {
|
||||||
|
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])(?:\\+{0,2}|\\#)$"));
|
||||||
|
patterns.put("pawnCapture",
|
||||||
|
Pattern.compile("^(?<fromFile>[a-h])(?<fromRank>[1-8])?x(?<toSquare>[a-h][1-8])(?<promotedTo>[NBRQ])?(?:\\+{0,2}|\\#)?$"));
|
||||||
|
patterns.put("pawnPush", Pattern.compile("^(?<toSquare>[a-h][1-8])(?<promotedTo>[NBRQ])?(?:\\+{0,2}|\\#)$"));
|
||||||
|
patterns.put("castling", Pattern.compile("^(?<queenside>O-O-O)|(?<kingside>O-O)(?:\\+{0,2}|\\#)?$"));
|
||||||
|
|
||||||
|
for (Map.Entry<String, Pattern> entry : patterns.entrySet()) {
|
||||||
|
Matcher m = entry.getValue().matcher(sanMove);
|
||||||
|
if (m.find()) {
|
||||||
|
Position pos = null, dest = null;
|
||||||
|
Move move = null;
|
||||||
|
switch (entry.getKey()) {
|
||||||
|
case "pieceMove":
|
||||||
|
dest = Position.fromLAN(m.group("toSquare"));
|
||||||
|
if (m.group("fromSquare") != null) pos = Position.fromLAN(m.group("fromSquare"));
|
||||||
|
else {
|
||||||
|
Class<? extends Piece> pieceClass = Piece.fromFirstChar(m.group("pieceType").charAt(0));
|
||||||
|
char file;
|
||||||
|
int rank;
|
||||||
|
if (m.group("fromFile") != null) {
|
||||||
|
file = m.group("fromFile").charAt(0);
|
||||||
|
rank = board.get(pieceClass, 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 = board.get(pieceClass, rank);
|
||||||
|
pos = Position.fromLAN(String.format("%c%d", file, rank));
|
||||||
|
} else pos = board.get(pieceClass, dest);
|
||||||
|
}
|
||||||
|
move = new Move(pos, dest);
|
||||||
|
break;
|
||||||
|
case "pawnCapture":
|
||||||
|
char file = m.group("fromFile").charAt(0);
|
||||||
|
int rank = m.group("fromRank") == null ? board.get(Pawn.class, file) : Integer.parseInt(m.group("fromRank"));
|
||||||
|
|
||||||
|
dest = Position.fromLAN(m.group("toSquare"));
|
||||||
|
pos = Position.fromLAN(String.format("%c%d", file, rank));
|
||||||
|
|
||||||
|
if (m.group("promotedTo") != null) {
|
||||||
|
try {
|
||||||
|
move = new PawnPromotion(pos, dest, Piece.fromFirstChar(m.group("promotedTo").charAt(0)));
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
} else move = new Move(pos, dest);
|
||||||
|
break;
|
||||||
|
case "pawnPush":
|
||||||
|
dest = Position.fromLAN(m.group("toSquare"));
|
||||||
|
int step = board.getLog().getActiveColor() == Color.WHITE ? 1 : -1;
|
||||||
|
|
||||||
|
// One step forward
|
||||||
|
if (board.getBoardArr()[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);
|
||||||
|
|
||||||
|
if (m.group("promotedTo") != null) {
|
||||||
|
try {
|
||||||
|
move = new PawnPromotion(pos, dest, Piece.fromFirstChar(m.group("promotedTo").charAt(0)));
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
} else move = new Move(pos, dest);
|
||||||
|
break;
|
||||||
|
case "castling":
|
||||||
|
pos = new Position(4, board.getLog().getActiveColor() == Color.WHITE ? 7 : 0);
|
||||||
|
dest = new Position(m.group("kingside") != null ? 6 : 2, pos.y);
|
||||||
|
move = new Castling(pos, dest);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return move;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toSAN(Board board) {
|
||||||
|
final Piece piece = board.get(pos);
|
||||||
|
StringBuilder sb = new StringBuilder(8);
|
||||||
|
|
||||||
|
// Piece symbol
|
||||||
|
if(!(piece instanceof Pawn))
|
||||||
|
sb.append(Character.toUpperCase(piece.firstChar()));
|
||||||
|
|
||||||
|
// Position
|
||||||
|
// TODO: Deconstruct position into optional file or rank
|
||||||
|
// Omit position if the move is a pawn push
|
||||||
|
if (!(piece instanceof Pawn && xDist == 0)) sb.append(pos.toLAN());
|
||||||
|
|
||||||
|
// Capture indicator
|
||||||
|
if (board.get(dest) != null) sb.append('x');
|
||||||
|
|
||||||
|
// Destination
|
||||||
|
sb.append(dest.toLAN());
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isHorizontal() { return getyDist() == 0; }
|
public boolean isHorizontal() { return getyDist() == 0; }
|
||||||
|
|
||||||
public boolean isVertical() { return getxDist() == 0; }
|
public boolean isVertical() { return getxDist() == 0; }
|
||||||
|
@ -64,11 +64,11 @@ public class MoveNode {
|
|||||||
other.fullmoveCounter, other.halfmoveClock);
|
other.fullmoveCounter, other.halfmoveClock);
|
||||||
if (copyVariations && other.variations != null) {
|
if (copyVariations && other.variations != null) {
|
||||||
if (variations == null) variations = new ArrayList<>();
|
if (variations == null) variations = new ArrayList<>();
|
||||||
other.variations.forEach(variation -> {
|
for (MoveNode variation : other.variations) {
|
||||||
MoveNode copy = new MoveNode(variation, true);
|
MoveNode copy = new MoveNode(variation, true);
|
||||||
copy.parent = this;
|
copy.parent = this;
|
||||||
variations.add(copy);
|
variations.add(copy);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ public class Pawn extends Piece {
|
|||||||
moves.add(new PawnPromotion(pos, dest, Rook.class));
|
moves.add(new PawnPromotion(pos, dest, Rook.class));
|
||||||
moves.add(new PawnPromotion(pos, dest, Knight.class));
|
moves.add(new PawnPromotion(pos, dest, Knight.class));
|
||||||
moves.add(new PawnPromotion(pos, dest, Bishop.class));
|
moves.add(new PawnPromotion(pos, dest, Bishop.class));
|
||||||
} catch (NoSuchMethodException | SecurityException | IllegalArgumentException e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
} else moves.add(move);
|
} else moves.add(move);
|
||||||
|
@ -16,21 +16,23 @@ import dev.kske.chess.board.Piece.Color;
|
|||||||
*/
|
*/
|
||||||
public class PawnPromotion extends Move {
|
public class PawnPromotion extends Move {
|
||||||
|
|
||||||
private final Class<? extends Piece> promotionPieceClass;
|
|
||||||
private final Constructor<? extends Piece> promotionPieceConstructor;
|
private final Constructor<? extends Piece> promotionPieceConstructor;
|
||||||
|
private final char promotionPieceChar;
|
||||||
|
|
||||||
public PawnPromotion(Position pos, Position dest, Class<? extends Piece> promotionPieceClass) throws NoSuchMethodException, SecurityException {
|
public PawnPromotion(Position pos, Position dest, Class<? extends Piece> promotionPieceClass)
|
||||||
|
throws ReflectiveOperationException, RuntimeException {
|
||||||
super(pos, dest);
|
super(pos, dest);
|
||||||
this.promotionPieceClass = promotionPieceClass;
|
|
||||||
|
|
||||||
// Cache piece constructor
|
// Cache piece constructor
|
||||||
promotionPieceConstructor = promotionPieceClass.getDeclaredConstructor(Color.class, Board.class);
|
promotionPieceConstructor = promotionPieceClass.getDeclaredConstructor(Color.class, Board.class);
|
||||||
promotionPieceConstructor.setAccessible(true);
|
promotionPieceConstructor.setAccessible(true);
|
||||||
|
|
||||||
|
// Get piece char
|
||||||
|
promotionPieceChar = (char) promotionPieceClass.getMethod("firstChar").invoke(promotionPieceConstructor.newInstance(null, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
public PawnPromotion(int xPos, int yPos, int xDest, int yDest, Class<? extends Piece> promotionPiece)
|
public PawnPromotion(int xPos, int yPos, int xDest, int yDest, Class<? extends Piece> promotionPiece)
|
||||||
throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException,
|
throws ReflectiveOperationException, RuntimeException {
|
||||||
InstantiationException {
|
|
||||||
this(new Position(xPos, yPos), new Position(xDest, yDest), promotionPiece);
|
this(new Position(xPos, yPos), new Position(xDest, yDest), promotionPiece);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,22 +53,19 @@ public class PawnPromotion extends Move {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toLAN() {
|
public String toLAN() { return pos.toLAN() + dest.toLAN() + promotionPieceChar; }
|
||||||
char promotionPieceChar = '-';
|
|
||||||
try {
|
@Override
|
||||||
promotionPieceChar = (char) promotionPieceClass.getMethod("firstChar").invoke(promotionPieceConstructor.newInstance(null, null));
|
public String toSAN(Board board) {
|
||||||
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException
|
String san = super.toSAN(board);
|
||||||
| InstantiationException e) {
|
return san + Character.toUpperCase(promotionPieceChar);
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return pos.toLAN() + dest.toLAN() + promotionPieceChar;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
final int prime = 31;
|
final int prime = 31;
|
||||||
int result = super.hashCode();
|
int result = super.hashCode();
|
||||||
result = prime * result + Objects.hash(promotionPieceClass);
|
result = prime * result + Objects.hash(promotionPieceChar, promotionPieceConstructor);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,8 +73,8 @@ public class PawnPromotion extends Move {
|
|||||||
public boolean equals(Object obj) {
|
public boolean equals(Object obj) {
|
||||||
if (this == obj) return true;
|
if (this == obj) return true;
|
||||||
if (!super.equals(obj)) return false;
|
if (!super.equals(obj)) return false;
|
||||||
if (getClass() != obj.getClass()) return false;
|
if (!(obj instanceof PawnPromotion)) return false;
|
||||||
PawnPromotion other = (PawnPromotion) obj;
|
PawnPromotion other = (PawnPromotion) obj;
|
||||||
return Objects.equals(promotionPieceClass, other.promotionPieceClass);
|
return promotionPieceChar == other.promotionPieceChar && Objects.equals(promotionPieceConstructor, other.promotionPieceConstructor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ public class AIPlayer extends Player {
|
|||||||
/*
|
/*
|
||||||
* Get a copy of the board and the available moves.
|
* Get a copy of the board and the available moves.
|
||||||
*/
|
*/
|
||||||
Board board = new Board(this.board);
|
Board board = new Board(this.board, false);
|
||||||
List<Move> moves = board.getMoves(color);
|
List<Move> moves = board.getMoves(color);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -66,7 +66,7 @@ public class AIPlayer extends Player {
|
|||||||
for (int i = 0; i < numThreads; i++) {
|
for (int i = 0; i < numThreads; i++) {
|
||||||
if (rem-- > 0) ++endIndex;
|
if (rem-- > 0) ++endIndex;
|
||||||
endIndex += step;
|
endIndex += step;
|
||||||
processors.add(new MoveProcessor(new Board(board), moves.subList(beginIndex, endIndex), color,
|
processors.add(new MoveProcessor(new Board(board, false), moves.subList(beginIndex, endIndex), color,
|
||||||
maxDepth, alphaBetaThreshold));
|
maxDepth, alphaBetaThreshold));
|
||||||
beginIndex = endIndex;
|
beginIndex = endIndex;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@ package dev.kske.chess.pgn;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Scanner;
|
import java.util.Scanner;
|
||||||
@ -12,6 +14,9 @@ import dev.kske.chess.exception.ChessException;
|
|||||||
* Project: <strong>Chess</strong><br>
|
* Project: <strong>Chess</strong><br>
|
||||||
* File: <strong>PGNDatabase.java</strong><br>
|
* File: <strong>PGNDatabase.java</strong><br>
|
||||||
* Created: <strong>4 Oct 2019</strong><br>
|
* Created: <strong>4 Oct 2019</strong><br>
|
||||||
|
* <br>
|
||||||
|
* Contains a series of {@link PGNGame} objects that can be stored inside a PGN
|
||||||
|
* file.
|
||||||
*
|
*
|
||||||
* @since Chess v0.5-alpha
|
* @since Chess v0.5-alpha
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
@ -20,13 +25,31 @@ public class PGNDatabase {
|
|||||||
|
|
||||||
private final List<PGNGame> games = new ArrayList<>();
|
private final List<PGNGame> games = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads PGN games from a file.
|
||||||
|
*
|
||||||
|
* @param pgnFile the file to load the games from
|
||||||
|
* @throws FileNotFoundException if the specified file is not found
|
||||||
|
* @throws ChessException if an error occurs while parsing the file
|
||||||
|
*/
|
||||||
public void load(File pgnFile) throws FileNotFoundException, ChessException {
|
public void load(File pgnFile) throws FileNotFoundException, ChessException {
|
||||||
try (Scanner sc = new Scanner(pgnFile)) {
|
Scanner sc = new Scanner(pgnFile);
|
||||||
while (sc.hasNext())
|
while (sc.hasNext())
|
||||||
games.add(PGNGame.parse(sc));
|
games.add(PGNGame.parse(sc));
|
||||||
} catch (FileNotFoundException | ChessException e) {
|
sc.close();
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves PGN games to a file.
|
||||||
|
*
|
||||||
|
* @param pgnFile the file to save the games to.
|
||||||
|
* @throws IOException if the file could not be created
|
||||||
|
*/
|
||||||
|
public void save(File pgnFile) throws IOException {
|
||||||
|
pgnFile.getParentFile().mkdirs();
|
||||||
|
PrintWriter pw = new PrintWriter(pgnFile);
|
||||||
|
games.forEach(g -> g.writePGN(pw));
|
||||||
|
pw.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<PGNGame> getGames() { return games; }
|
public List<PGNGame> getGames() { return games; }
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
package dev.kske.chess.pgn;
|
package dev.kske.chess.pgn;
|
||||||
|
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Scanner;
|
import java.util.Scanner;
|
||||||
import java.util.regex.MatchResult;
|
import java.util.regex.MatchResult;
|
||||||
@ -8,6 +12,8 @@ import java.util.regex.Pattern;
|
|||||||
|
|
||||||
import dev.kske.chess.board.Board;
|
import dev.kske.chess.board.Board;
|
||||||
import dev.kske.chess.board.FENString;
|
import dev.kske.chess.board.FENString;
|
||||||
|
import dev.kske.chess.board.Move;
|
||||||
|
import dev.kske.chess.board.Piece.Color;
|
||||||
import dev.kske.chess.exception.ChessException;
|
import dev.kske.chess.exception.ChessException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,7 +27,11 @@ import dev.kske.chess.exception.ChessException;
|
|||||||
public class PGNGame {
|
public class PGNGame {
|
||||||
|
|
||||||
private final Map<String, String> tagPairs = new HashMap<>(7);
|
private final Map<String, String> tagPairs = new HashMap<>(7);
|
||||||
private final Board board = new Board();
|
private final Board board;
|
||||||
|
|
||||||
|
public PGNGame() { board = new Board(); }
|
||||||
|
|
||||||
|
public PGNGame(Board board) { this.board = board; }
|
||||||
|
|
||||||
public static PGNGame parse(Scanner sc) throws ChessException {
|
public static PGNGame parse(Scanner sc) throws ChessException {
|
||||||
PGNGame game = new PGNGame();
|
PGNGame game = new PGNGame();
|
||||||
@ -62,6 +72,45 @@ public class PGNGame {
|
|||||||
return game;
|
return game;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void writePGN(PrintWriter pw) {
|
||||||
|
// Set the unknown result tag if no result tag is specified
|
||||||
|
tagPairs.putIfAbsent("Result", "*");
|
||||||
|
|
||||||
|
// Write tag pairs
|
||||||
|
tagPairs.forEach((k, v) -> pw.printf("[%s \"%s\"]%n", k, v));
|
||||||
|
|
||||||
|
// Insert newline if tags were printed
|
||||||
|
if (!tagPairs.isEmpty()) pw.println();
|
||||||
|
|
||||||
|
if (!board.getLog().isEmpty()) {
|
||||||
|
// Collect SAN moves
|
||||||
|
Board clone = new Board(board, true);
|
||||||
|
List<String> chunks = new ArrayList<>();
|
||||||
|
boolean flag = true;
|
||||||
|
while (flag) {
|
||||||
|
Move move = clone.getLog().getLast().move;
|
||||||
|
flag = clone.getLog().hasParent();
|
||||||
|
clone.revert();
|
||||||
|
String chunk = clone.getLog().getActiveColor() == Color.WHITE ? String.format(" %d. ", clone.getLog().getFullmoveNumber()) : " ";
|
||||||
|
chunk += move.toSAN(clone);
|
||||||
|
chunks.add(chunk);
|
||||||
|
}
|
||||||
|
Collections.reverse(chunks);
|
||||||
|
|
||||||
|
// Write movetext
|
||||||
|
String line = "";
|
||||||
|
for (String chunk : chunks)
|
||||||
|
if (line.length() + chunk.length() <= 80) line += chunk;
|
||||||
|
else {
|
||||||
|
pw.println(line);
|
||||||
|
line = chunk;
|
||||||
|
}
|
||||||
|
if (!line.isEmpty()) pw.println(line);
|
||||||
|
}
|
||||||
|
// Write game termination marker
|
||||||
|
pw.print(tagPairs.get("Result"));
|
||||||
|
}
|
||||||
|
|
||||||
public String getTag(String tagName) { return tagPairs.get(tagName); }
|
public String getTag(String tagName) { return tagPairs.get(tagName); }
|
||||||
|
|
||||||
public boolean hasTag(String tagName) { return tagPairs.containsKey(tagName); }
|
public boolean hasTag(String tagName) { return tagPairs.containsKey(tagName); }
|
||||||
|
@ -5,6 +5,7 @@ import java.awt.Font;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
@ -15,7 +16,7 @@ import javax.swing.JFileChooser;
|
|||||||
import javax.swing.JLabel;
|
import javax.swing.JLabel;
|
||||||
import javax.swing.JOptionPane;
|
import javax.swing.JOptionPane;
|
||||||
import javax.swing.JPanel;
|
import javax.swing.JPanel;
|
||||||
import javax.swing.filechooser.FileFilter;
|
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||||
|
|
||||||
import dev.kske.chess.io.EngineUtil;
|
import dev.kske.chess.io.EngineUtil;
|
||||||
|
|
||||||
@ -31,25 +32,22 @@ public class DialogUtil {
|
|||||||
|
|
||||||
private DialogUtil() {}
|
private DialogUtil() {}
|
||||||
|
|
||||||
public static void showFileSelectionDialog(Component parent, Consumer<List<File>> action) {
|
public static void showFileSelectionDialog(Component parent, Consumer<List<File>> action, Collection<FileNameExtensionFilter> filters) {
|
||||||
JFileChooser fileChooser = new JFileChooser();
|
JFileChooser fileChooser = new JFileChooser();
|
||||||
fileChooser.setCurrentDirectory(new File(System.getProperty("user.home")));
|
fileChooser.setCurrentDirectory(new File(System.getProperty("user.home")));
|
||||||
fileChooser.setAcceptAllFileFilterUsed(false);
|
fileChooser.setAcceptAllFileFilterUsed(false);
|
||||||
fileChooser.addChoosableFileFilter(new FileFilter() {
|
filters.forEach(fileChooser::addChoosableFileFilter);
|
||||||
|
if (fileChooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) action.accept(Arrays.asList(fileChooser.getSelectedFile()));
|
||||||
@Override
|
|
||||||
public boolean accept(File f) {
|
|
||||||
int dotIndex = f.getName().lastIndexOf('.');
|
|
||||||
if (dotIndex >= 0) {
|
|
||||||
String extension = f.getName().substring(dotIndex).toLowerCase();
|
|
||||||
return extension.equals(".fen") || extension.equals(".pgn");
|
|
||||||
} else return f.isDirectory();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public static void showFileSaveDialog(Component parent, Consumer<File> action, Collection<FileNameExtensionFilter> filters) {
|
||||||
public String getDescription() { return "FEN and PGN files"; }
|
JFileChooser fileChooser = new JFileChooser();
|
||||||
});
|
fileChooser.setCurrentDirectory(new File(System.getProperty("user.home")));
|
||||||
if (fileChooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) action.accept(Arrays.asList(fileChooser.getSelectedFile()));
|
fileChooser.setAcceptAllFileFilterUsed(false);
|
||||||
|
filters.forEach(fileChooser::addChoosableFileFilter);
|
||||||
|
if (fileChooser.showSaveDialog(parent) == JFileChooser.APPROVE_OPTION) action.accept(
|
||||||
|
new File(fileChooser.getSelectedFile().getAbsolutePath() + "."
|
||||||
|
+ ((FileNameExtensionFilter) fileChooser.getFileFilter()).getExtensions()[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void showGameConfigurationDialog(Component parent, BiConsumer<String, String> action) {
|
public static void showGameConfigurationDialog(Component parent, BiConsumer<String, String> action) {
|
||||||
@ -64,7 +62,7 @@ public class DialogUtil {
|
|||||||
dialogPanel.add(lblWhite);
|
dialogPanel.add(lblWhite);
|
||||||
|
|
||||||
JComboBox<Object> cbWhite = new JComboBox<>();
|
JComboBox<Object> cbWhite = new JComboBox<>();
|
||||||
cbWhite.setModel(new DefaultComboBoxModel<Object>(options.toArray()));
|
cbWhite.setModel(new DefaultComboBoxModel<>(options.toArray()));
|
||||||
cbWhite.setBounds(98, 9, 159, 22);
|
cbWhite.setBounds(98, 9, 159, 22);
|
||||||
dialogPanel.add(cbWhite);
|
dialogPanel.add(cbWhite);
|
||||||
|
|
||||||
@ -74,7 +72,7 @@ public class DialogUtil {
|
|||||||
dialogPanel.add(lblBlack);
|
dialogPanel.add(lblBlack);
|
||||||
|
|
||||||
JComboBox<Object> cbBlack = new JComboBox<>();
|
JComboBox<Object> cbBlack = new JComboBox<>();
|
||||||
cbBlack.setModel(new DefaultComboBoxModel<Object>(options.toArray()));
|
cbBlack.setModel(new DefaultComboBoxModel<>(options.toArray()));
|
||||||
cbBlack.setBounds(98, 36, 159, 22);
|
cbBlack.setBounds(98, 36, 159, 22);
|
||||||
dialogPanel.add(cbBlack);
|
dialogPanel.add(cbBlack);
|
||||||
|
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package dev.kske.chess.ui;
|
package dev.kske.chess.ui;
|
||||||
|
|
||||||
|
import java.awt.Desktop;
|
||||||
import java.awt.EventQueue;
|
import java.awt.EventQueue;
|
||||||
import java.awt.Toolkit;
|
import java.awt.Toolkit;
|
||||||
import java.awt.dnd.DropTarget;
|
import java.awt.dnd.DropTarget;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -164,4 +166,30 @@ public class MainWindow extends JFrame {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void saveFile(File file) {
|
||||||
|
final int dotIndex = file.getName().lastIndexOf('.');
|
||||||
|
final String extension = file.getName().substring(dotIndex).toLowerCase();
|
||||||
|
|
||||||
|
if (extension.equals(".pgn")) try {
|
||||||
|
PGNGame pgnGame = new PGNGame(getSelectedGamePane().getGame().getBoard());
|
||||||
|
pgnGame.setTag("Event", tabbedPane.getTitleAt(tabbedPane.getSelectedIndex()));
|
||||||
|
pgnGame.setTag("Result", "*");
|
||||||
|
PGNDatabase pgnDB = new PGNDatabase();
|
||||||
|
pgnDB.getGames().add(pgnGame);
|
||||||
|
pgnDB.save(file);
|
||||||
|
|
||||||
|
if (JOptionPane.showConfirmDialog(this,
|
||||||
|
"Game export finished. Do you want to view the created file?",
|
||||||
|
"Game export finished",
|
||||||
|
JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION)
|
||||||
|
Desktop.getDesktop().open(file);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
JOptionPane.showMessageDialog(this,
|
||||||
|
"Failed to save the file " + file.getName() + ": " + e.toString(),
|
||||||
|
"File saving error",
|
||||||
|
JOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,13 @@ package dev.kske.chess.ui;
|
|||||||
|
|
||||||
import java.awt.Toolkit;
|
import java.awt.Toolkit;
|
||||||
import java.awt.datatransfer.StringSelection;
|
import java.awt.datatransfer.StringSelection;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
import javax.swing.JMenu;
|
import javax.swing.JMenu;
|
||||||
import javax.swing.JMenuBar;
|
import javax.swing.JMenuBar;
|
||||||
import javax.swing.JMenuItem;
|
import javax.swing.JMenuItem;
|
||||||
import javax.swing.JOptionPane;
|
import javax.swing.JOptionPane;
|
||||||
|
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||||
|
|
||||||
import dev.kske.chess.board.FENString;
|
import dev.kske.chess.board.FENString;
|
||||||
import dev.kske.chess.exception.ChessException;
|
import dev.kske.chess.exception.ChessException;
|
||||||
@ -48,9 +50,18 @@ public class MenuBar extends JMenuBar {
|
|||||||
gameMenu.add(newGameMenuItem);
|
gameMenu.add(newGameMenuItem);
|
||||||
|
|
||||||
JMenuItem loadFileMenu = new JMenuItem("Load game file");
|
JMenuItem loadFileMenu = new JMenuItem("Load game file");
|
||||||
loadFileMenu.addActionListener((evt) -> DialogUtil.showFileSelectionDialog(mainWindow, mainWindow::loadFiles));
|
loadFileMenu.addActionListener((evt) -> DialogUtil
|
||||||
|
.showFileSelectionDialog(mainWindow,
|
||||||
|
mainWindow::loadFiles,
|
||||||
|
Arrays.asList(new FileNameExtensionFilter("FEN and PGN files", "fen", "pgn"))));
|
||||||
gameMenu.add(loadFileMenu);
|
gameMenu.add(loadFileMenu);
|
||||||
|
|
||||||
|
JMenuItem saveFileMenu = new JMenuItem("Save game file");
|
||||||
|
saveFileMenu
|
||||||
|
.addActionListener((evt) -> DialogUtil
|
||||||
|
.showFileSaveDialog(mainWindow, mainWindow::saveFile, Arrays.asList(new FileNameExtensionFilter("PGN file", "pgn"))));
|
||||||
|
gameMenu.add(saveFileMenu);
|
||||||
|
|
||||||
add(gameMenu);
|
add(gameMenu);
|
||||||
newGameMenuItem.doClick();
|
newGameMenuItem.doClick();
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ class BoardTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void testClone() {
|
void testClone() {
|
||||||
Board clone = new Board(board);
|
Board clone = new Board(board, false);
|
||||||
assertNotSame(clone, board);
|
assertNotSame(clone, board);
|
||||||
assertNotSame(clone.getBoardArr(), board.getBoardArr());
|
assertNotSame(clone.getBoardArr(), board.getBoardArr());
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user