Merge pull request #16 from CyB3RC0nN0R/f/pgn_save
Implemented game serialization to the PGN format
This commit is contained in:
		| @@ -6,8 +6,6 @@ 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; | ||||
|  | ||||
| import dev.kske.chess.board.Piece.Color; | ||||
|  | ||||
| @@ -15,7 +13,7 @@ import dev.kske.chess.board.Piece.Color; | ||||
|  * Project: <strong>Chess</strong><br> | ||||
|  * File: <strong>Board.java</strong><br> | ||||
|  * Created: <strong>01.07.2019</strong><br> | ||||
|  *  | ||||
|  * | ||||
|  * @since Chess v0.1-alpha | ||||
|  * @author Kai S. K. Engelbart | ||||
|  */ | ||||
| @@ -34,11 +32,11 @@ public class Board { | ||||
| 	 * Creates a copy of another {@link Board} instance.<br> | ||||
| 	 * The created object is a deep copy, but does not contain any move history | ||||
| 	 * apart from the current {@link MoveNode}. | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @param other The {@link Board} instance to copy | ||||
| 	 * @param copyVariations TODO | ||||
| 	 */ | ||||
| 	public Board(Board other) { | ||||
| 		boardArr = new Piece[8][8]; | ||||
| 	public Board(Board other, boolean copyVariations) { | ||||
| 		for (int i = 0; i < 8; i++) | ||||
| 			for (int j = 0; j < 8; j++) { | ||||
| 				if (other.boardArr[i][j] == null) continue; | ||||
| @@ -47,12 +45,16 @@ public class Board { | ||||
| 			} | ||||
|  | ||||
| 		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); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Moves a piece across the board if the move is legal. | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @param move The move to execute | ||||
| 	 * @return {@code true}, if the attempted move was legal and thus executed | ||||
| 	 */ | ||||
| @@ -75,7 +77,7 @@ public class Board { | ||||
|  | ||||
| 	/** | ||||
| 	 * Moves a piece across the board without checking if the move is legal. | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @param move The move to execute | ||||
| 	 */ | ||||
| 	public void move(Move move) { | ||||
| @@ -94,87 +96,11 @@ public class Board { | ||||
|  | ||||
| 	/** | ||||
| 	 * 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])(?:\\+{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; | ||||
| 			} | ||||
| 		}); | ||||
| 		move(Move.fromSAN(sanMove, this)); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| @@ -196,7 +122,7 @@ public class Board { | ||||
|  | ||||
| 	/** | ||||
| 	 * Generated every legal move for one color | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @param color The color to generate the moves for | ||||
| 	 * @return A list of all legal moves | ||||
| 	 */ | ||||
| @@ -212,7 +138,7 @@ public class Board { | ||||
|  | ||||
| 	/** | ||||
| 	 * Checks, if the king is in check. | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @param color The color of the king to check | ||||
| 	 * @return {@code true}, if the king is in check | ||||
| 	 */ | ||||
| @@ -220,7 +146,7 @@ public class Board { | ||||
|  | ||||
| 	/** | ||||
| 	 * Checks, if a field can be attacked by pieces of a certain color. | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @param dest  the field to check | ||||
| 	 * @param color the color of a potential attacker piece | ||||
| 	 * @return {@code true} if a move with the destination {@code dest} | ||||
| @@ -237,7 +163,7 @@ public class Board { | ||||
| 	/** | ||||
| 	 * Checks, if the king is in checkmate. | ||||
| 	 * This requires the king to already be in check! | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @param color The color of the king to check | ||||
| 	 * @return {@code true}, if the king is in checkmate | ||||
| 	 */ | ||||
| @@ -334,7 +260,7 @@ public class Board { | ||||
|  | ||||
| 	/** | ||||
| 	 * Searches for a {@link Piece} inside a file (A - H). | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @param pieceClass The class 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 | ||||
| @@ -349,7 +275,7 @@ public class Board { | ||||
|  | ||||
| 	/** | ||||
| 	 * Searches for a {@link Piece} inside a rank (1 - 8). | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @param pieceClass The class 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 | ||||
| @@ -365,7 +291,7 @@ public class Board { | ||||
|  | ||||
| 	/** | ||||
| 	 * Searches for a {@link Piece} that can move to a {@link Position}. | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @param pieceClass The class 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 | ||||
| @@ -382,7 +308,7 @@ public class Board { | ||||
|  | ||||
| 	/** | ||||
| 	 * Places a piece at a position. | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @param pos   The position to place the piece at | ||||
| 	 * @param piece The piece to place | ||||
| 	 */ | ||||
| @@ -402,7 +328,7 @@ public class Board { | ||||
|  | ||||
| 	/** | ||||
| 	 * Places a piece at the position of a move. | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @param move  The move at which position to place the piece | ||||
| 	 * @param piece The piece to place | ||||
| 	 */ | ||||
| @@ -410,7 +336,7 @@ public class Board { | ||||
|  | ||||
| 	/** | ||||
| 	 * Places a piece at the destination of a move. | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @param move  The move at which destination to place the piece | ||||
| 	 * @param piece The piece to place | ||||
| 	 */ | ||||
|   | ||||
| @@ -4,7 +4,7 @@ package dev.kske.chess.board; | ||||
|  * Project: <strong>Chess</strong><br> | ||||
|  * File: <strong>Castling.java</strong><br> | ||||
|  * Created: <strong>2 Nov 2019</strong><br> | ||||
|  *  | ||||
|  * | ||||
|  * @since Chess v0.5-alpha | ||||
|  * @author Kai S. K. Engelbart | ||||
|  */ | ||||
| @@ -32,4 +32,13 @@ public class Castling extends Move { | ||||
| 		super.revert(board, capturedPiece); | ||||
| 		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"; | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import dev.kske.chess.board.Piece.Color; | ||||
|  * Project: <strong>Chess</strong><br> | ||||
|  * File: <strong>Log.java</strong><br> | ||||
|  * Created: <strong>09.07.2019</strong><br> | ||||
|  *  | ||||
|  * | ||||
|  * @since Chess v0.1-alpha | ||||
|  * @author Kai S. K. Engelbart | ||||
|  */ | ||||
| @@ -28,7 +28,7 @@ public class Log implements Iterable<MoveNode> { | ||||
| 	/** | ||||
| 	 * Creates a (partially deep) copy of another {@link Log} instance which begins | ||||
| 	 * with the current {@link MoveNode}. | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @param other          The {@link Log} instance to copy | ||||
| 	 * @param copyVariations If set to {@code true}, subsequent variations of the | ||||
| 	 *                       current {@link MoveNode} are copied with the | ||||
| @@ -43,12 +43,17 @@ public class Log implements Iterable<MoveNode> { | ||||
|  | ||||
| 		// The new root is the current node of the copied instance | ||||
| 		if (!other.isEmpty()) { | ||||
| 			root = new MoveNode(other.current, copyVariations); | ||||
| 			root = new MoveNode(other.root, copyVariations); | ||||
| 			root.setParent(null); | ||||
| 			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 | ||||
| 	public Iterator<MoveNode> iterator() { | ||||
| 		return new Iterator<MoveNode>() { | ||||
| @@ -71,7 +76,7 @@ public class Log implements Iterable<MoveNode> { | ||||
|  | ||||
| 	/** | ||||
| 	 * Adds a move to the move history and adjusts the log to the new position. | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @param move          The move to log | ||||
| 	 * @param piece         The piece that performed the move | ||||
| 	 * @param capturedPiece The piece captured with the move | ||||
| @@ -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. | ||||
| 	 */ | ||||
| 	public void removeLast() { | ||||
| @@ -109,8 +114,14 @@ public class Log implements Iterable<MoveNode> { | ||||
| 		} else reset(); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * @return {@code true} if the root node exists | ||||
| 	 */ | ||||
| 	public boolean isEmpty() { return root == null; } | ||||
|  | ||||
| 	/** | ||||
| 	 * @return {@code true} if the current node has a parent node | ||||
| 	 */ | ||||
| 	public boolean hasParent() { return !isEmpty() && current.hasParent(); } | ||||
|  | ||||
| 	/** | ||||
| @@ -129,7 +140,7 @@ public class Log implements Iterable<MoveNode> { | ||||
|  | ||||
| 	/** | ||||
| 	 * Changes the current node to one of its children (variations). | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @param index the index of the variation to select | ||||
| 	 */ | ||||
| 	public void selectNextNode(int index) { | ||||
| @@ -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() { | ||||
| 		activeColor		= current.activeColor; | ||||
| 		castlingRights	= current.castlingRights.clone(); | ||||
| @@ -167,6 +182,15 @@ public class Log implements Iterable<MoveNode> { | ||||
| 		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) { | ||||
| 		// Kingside | ||||
| 		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 void setFullmoveNumber(int fullmoveCounter) { this.fullmoveNumber = fullmoveCounter; } | ||||
| 	public void setFullmoveNumber(int fullmoveNumber) { this.fullmoveNumber = fullmoveNumber; } | ||||
|  | ||||
| 	public int getHalfmoveClock() { return halfmoveClock; } | ||||
|  | ||||
|   | ||||
| @@ -1,12 +1,18 @@ | ||||
| package dev.kske.chess.board; | ||||
|  | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| 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> | ||||
|  * File: <strong>Move.java</strong><br> | ||||
|  * Created: <strong>02.07.2019</strong><br> | ||||
|  *  | ||||
|  * | ||||
|  * @since Chess v0.1-alpha | ||||
|  * @author Kai S. K. Engelbart | ||||
|  */ | ||||
| @@ -44,7 +50,7 @@ public class Move { | ||||
| 		if (move.length() == 5) { | ||||
| 			try { | ||||
| 				return new PawnPromotion(pos, dest, Piece.fromFirstChar(move.charAt(4))); | ||||
| 			} catch (NoSuchMethodException | SecurityException | IllegalArgumentException e) { | ||||
| 			} catch (Exception e) { | ||||
| 				e.printStackTrace(); | ||||
| 				return null; | ||||
| 			} | ||||
| @@ -53,6 +59,116 @@ public class Move { | ||||
|  | ||||
| 	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 isVertical() { return getxDist() == 0; } | ||||
|   | ||||
| @@ -11,7 +11,7 @@ import dev.kske.chess.board.Piece.Color; | ||||
|  * Project: <strong>Chess</strong><br> | ||||
|  * File: <strong>MoveNode.java</strong><br> | ||||
|  * Created: <strong>02.10.2019</strong><br> | ||||
|  *  | ||||
|  * | ||||
|  * @since Chess v0.5-alpha | ||||
|  * @author Kai S. K. Engelbart | ||||
|  */ | ||||
| @@ -31,7 +31,7 @@ public class MoveNode { | ||||
|  | ||||
| 	/** | ||||
| 	 * Creates a new {@link MoveNode}. | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @param move            The logged {@link Move} | ||||
| 	 * @param capturedPiece   The {@link Piece} captures by the logged {@link Move} | ||||
| 	 * @param enPassant       The en passant {@link Position} valid after the logged | ||||
| @@ -53,7 +53,7 @@ public class MoveNode { | ||||
|  | ||||
| 	/** | ||||
| 	 * Creates a (deep) copy of another {@link MoveNode}. | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @param other          The {@link MoveNode} to copy | ||||
| 	 * @param copyVariations When this is set to {@code true} a deep copy is | ||||
| 	 *                       created, which | ||||
| @@ -64,17 +64,17 @@ public class MoveNode { | ||||
| 				other.fullmoveCounter, other.halfmoveClock); | ||||
| 		if (copyVariations && other.variations != null) { | ||||
| 			if (variations == null) variations = new ArrayList<>(); | ||||
| 			other.variations.forEach(variation -> { | ||||
| 			for (MoveNode variation : other.variations) { | ||||
| 				MoveNode copy = new MoveNode(variation, true); | ||||
| 				copy.parent = this; | ||||
| 				variations.add(copy); | ||||
| 			}); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Adds another {@link MoveNode} as a child node. | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @param variation The {@link MoveNode} to append to this {@link MoveNode} | ||||
| 	 */ | ||||
| 	public void addVariation(MoveNode variation) { | ||||
|   | ||||
| @@ -7,7 +7,7 @@ import java.util.List; | ||||
|  * Project: <strong>Chess</strong><br> | ||||
|  * File: <strong>Pawn.java</strong><br> | ||||
|  * Created: <strong>01.07.2019</strong><br> | ||||
|  *  | ||||
|  * | ||||
|  * @since Chess v0.1-alpha | ||||
|  * @author Kai S. K. Engelbart | ||||
|  */ | ||||
| @@ -74,7 +74,7 @@ public class Pawn extends Piece { | ||||
| 					moves.add(new PawnPromotion(pos, dest, Rook.class)); | ||||
| 					moves.add(new PawnPromotion(pos, dest, Knight.class)); | ||||
| 					moves.add(new PawnPromotion(pos, dest, Bishop.class)); | ||||
| 				} catch (NoSuchMethodException | SecurityException | IllegalArgumentException e) { | ||||
| 				} catch (Exception e) { | ||||
| 					e.printStackTrace(); | ||||
| 				} | ||||
| 			} else moves.add(move); | ||||
|   | ||||
| @@ -10,27 +10,29 @@ import dev.kske.chess.board.Piece.Color; | ||||
|  * Project: <strong>Chess</strong><br> | ||||
|  * File: <strong>PawnPromotion.java</strong><br> | ||||
|  * Created: <strong>2 Nov 2019</strong><br> | ||||
|  *  | ||||
|  * | ||||
|  * @since Chess v0.5-alpha | ||||
|  * @author Kai S. K. Engelbart | ||||
|  */ | ||||
| public class PawnPromotion extends Move { | ||||
|  | ||||
| 	private final Class<? extends Piece>		promotionPieceClass; | ||||
| 	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); | ||||
| 		this.promotionPieceClass = promotionPieceClass; | ||||
|  | ||||
| 		// Cache piece constructor | ||||
| 		promotionPieceConstructor = promotionPieceClass.getDeclaredConstructor(Color.class, Board.class); | ||||
| 		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) | ||||
| 			throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, | ||||
| 			InstantiationException { | ||||
| 			throws ReflectiveOperationException, RuntimeException { | ||||
| 		this(new Position(xPos, yPos), new Position(xDest, yDest), promotionPiece); | ||||
| 	} | ||||
|  | ||||
| @@ -51,22 +53,19 @@ public class PawnPromotion extends Move { | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public String toLAN() { | ||||
| 		char promotionPieceChar = '-'; | ||||
| 		try { | ||||
| 			promotionPieceChar = (char) promotionPieceClass.getMethod("firstChar").invoke(promotionPieceConstructor.newInstance(null, null)); | ||||
| 		} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException | ||||
| 				| InstantiationException e) { | ||||
| 			e.printStackTrace(); | ||||
| 		} | ||||
| 		return pos.toLAN() + dest.toLAN() + promotionPieceChar; | ||||
| 	public String toLAN() { return pos.toLAN() + dest.toLAN() + promotionPieceChar; } | ||||
|  | ||||
| 	@Override | ||||
| 	public String toSAN(Board board) { | ||||
| 		String san = super.toSAN(board); | ||||
| 		return san + Character.toUpperCase(promotionPieceChar); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public int hashCode() { | ||||
| 		final int	prime	= 31; | ||||
| 		int			result	= super.hashCode(); | ||||
| 		result = prime * result + Objects.hash(promotionPieceClass); | ||||
| 		result = prime * result + Objects.hash(promotionPieceChar, promotionPieceConstructor); | ||||
| 		return result; | ||||
| 	} | ||||
|  | ||||
| @@ -74,8 +73,8 @@ public class PawnPromotion extends Move { | ||||
| 	public boolean equals(Object obj) { | ||||
| 		if (this == obj) return true; | ||||
| 		if (!super.equals(obj)) return false; | ||||
| 		if (getClass() != obj.getClass()) return false; | ||||
| 		if (!(obj instanceof PawnPromotion)) return false; | ||||
| 		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. | ||||
| 			 */ | ||||
| 			Board		board	= new Board(this.board); | ||||
| 			Board		board	= new Board(this.board, false); | ||||
| 			List<Move>	moves	= board.getMoves(color); | ||||
|  | ||||
| 			/* | ||||
| @@ -66,7 +66,7 @@ public class AIPlayer extends Player { | ||||
| 			for (int i = 0; i < numThreads; i++) { | ||||
| 				if (rem-- > 0) ++endIndex; | ||||
| 				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)); | ||||
| 				beginIndex = endIndex; | ||||
| 			} | ||||
|   | ||||
| @@ -2,6 +2,8 @@ package dev.kske.chess.pgn; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.FileNotFoundException; | ||||
| import java.io.IOException; | ||||
| import java.io.PrintWriter; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import java.util.Scanner; | ||||
| @@ -12,21 +14,42 @@ import dev.kske.chess.exception.ChessException; | ||||
|  * Project: <strong>Chess</strong><br> | ||||
|  * File: <strong>PGNDatabase.java</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 | ||||
|  * @author Kai S. K. Engelbart | ||||
|  */ | ||||
| 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 { | ||||
| 		try (Scanner sc = new Scanner(pgnFile)) { | ||||
| 			while (sc.hasNext()) | ||||
| 				games.add(PGNGame.parse(sc)); | ||||
| 		} catch (FileNotFoundException | ChessException e) { | ||||
| 			throw e; | ||||
| 		} | ||||
| 		Scanner sc = new Scanner(pgnFile); | ||||
| 		while (sc.hasNext()) | ||||
| 			games.add(PGNGame.parse(sc)); | ||||
| 		sc.close(); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * 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; } | ||||
|   | ||||
| @@ -1,6 +1,10 @@ | ||||
| package dev.kske.chess.pgn; | ||||
|  | ||||
| import java.io.PrintWriter; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.Scanner; | ||||
| import java.util.regex.MatchResult; | ||||
| @@ -8,20 +12,26 @@ import java.util.regex.Pattern; | ||||
|  | ||||
| import dev.kske.chess.board.Board; | ||||
| 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; | ||||
|  | ||||
| /** | ||||
|  * Project: <strong>Chess</strong><br> | ||||
|  * File: <strong>PGNGame.java</strong><br> | ||||
|  * Created: <strong>22 Sep 2019</strong><br> | ||||
|  *  | ||||
|  * | ||||
|  * @since Chess v0.5-alpha | ||||
|  * @author Kai S. K. Engelbart | ||||
|  */ | ||||
| public class PGNGame { | ||||
|  | ||||
| 	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 { | ||||
| 		PGNGame game = new PGNGame(); | ||||
| @@ -62,6 +72,45 @@ public class PGNGame { | ||||
| 		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 boolean hasTag(String tagName) { return tagPairs.containsKey(tagName); } | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import java.awt.Font; | ||||
| import java.io.File; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.Collection; | ||||
| import java.util.List; | ||||
| import java.util.function.BiConsumer; | ||||
| import java.util.function.Consumer; | ||||
| @@ -15,7 +16,7 @@ import javax.swing.JFileChooser; | ||||
| import javax.swing.JLabel; | ||||
| import javax.swing.JOptionPane; | ||||
| import javax.swing.JPanel; | ||||
| import javax.swing.filechooser.FileFilter; | ||||
| import javax.swing.filechooser.FileNameExtensionFilter; | ||||
|  | ||||
| import dev.kske.chess.io.EngineUtil; | ||||
|  | ||||
| @@ -23,7 +24,7 @@ import dev.kske.chess.io.EngineUtil; | ||||
|  * Project: <strong>Chess</strong><br> | ||||
|  * File: <strong>DialogUtil.java</strong><br> | ||||
|  * Created: <strong>24.07.2019</strong><br> | ||||
|  *  | ||||
|  * | ||||
|  * @since Chess v0.3-alpha | ||||
|  * @author Kai S. K. Engelbart | ||||
|  */ | ||||
| @@ -31,27 +32,24 @@ public class 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(); | ||||
| 		fileChooser.setCurrentDirectory(new File(System.getProperty("user.home"))); | ||||
| 		fileChooser.setAcceptAllFileFilterUsed(false); | ||||
| 		fileChooser.addChoosableFileFilter(new FileFilter() { | ||||
|  | ||||
| 			@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 String getDescription() { return "FEN and PGN files"; } | ||||
| 		}); | ||||
| 		filters.forEach(fileChooser::addChoosableFileFilter); | ||||
| 		if (fileChooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) action.accept(Arrays.asList(fileChooser.getSelectedFile())); | ||||
| 	} | ||||
|  | ||||
| 	public static void showFileSaveDialog(Component parent, Consumer<File> action, Collection<FileNameExtensionFilter> filters) { | ||||
| 		JFileChooser fileChooser = new JFileChooser(); | ||||
| 		fileChooser.setCurrentDirectory(new File(System.getProperty("user.home"))); | ||||
| 		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) { | ||||
| 		JPanel dialogPanel = new JPanel(); | ||||
|  | ||||
| @@ -64,7 +62,7 @@ public class DialogUtil { | ||||
| 		dialogPanel.add(lblWhite); | ||||
|  | ||||
| 		JComboBox<Object> cbWhite = new JComboBox<>(); | ||||
| 		cbWhite.setModel(new DefaultComboBoxModel<Object>(options.toArray())); | ||||
| 		cbWhite.setModel(new DefaultComboBoxModel<>(options.toArray())); | ||||
| 		cbWhite.setBounds(98, 9, 159, 22); | ||||
| 		dialogPanel.add(cbWhite); | ||||
|  | ||||
| @@ -74,7 +72,7 @@ public class DialogUtil { | ||||
| 		dialogPanel.add(lblBlack); | ||||
|  | ||||
| 		JComboBox<Object> cbBlack = new JComboBox<>(); | ||||
| 		cbBlack.setModel(new DefaultComboBoxModel<Object>(options.toArray())); | ||||
| 		cbBlack.setModel(new DefaultComboBoxModel<>(options.toArray())); | ||||
| 		cbBlack.setBounds(98, 36, 159, 22); | ||||
| 		dialogPanel.add(cbBlack); | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,11 @@ | ||||
| package dev.kske.chess.ui; | ||||
|  | ||||
| import java.awt.Desktop; | ||||
| import java.awt.EventQueue; | ||||
| import java.awt.Toolkit; | ||||
| import java.awt.dnd.DropTarget; | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| import java.nio.file.Files; | ||||
| 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.datatransfer.StringSelection; | ||||
| import java.util.Arrays; | ||||
|  | ||||
| import javax.swing.JMenu; | ||||
| import javax.swing.JMenuBar; | ||||
| import javax.swing.JMenuItem; | ||||
| import javax.swing.JOptionPane; | ||||
| import javax.swing.filechooser.FileNameExtensionFilter; | ||||
|  | ||||
| import dev.kske.chess.board.FENString; | ||||
| import dev.kske.chess.exception.ChessException; | ||||
| @@ -48,9 +50,18 @@ public class MenuBar extends JMenuBar { | ||||
| 		gameMenu.add(newGameMenuItem); | ||||
|  | ||||
| 		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); | ||||
|  | ||||
| 		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); | ||||
| 		newGameMenuItem.doClick(); | ||||
| 	} | ||||
|   | ||||
| @@ -31,7 +31,7 @@ class BoardTest { | ||||
| 	 */ | ||||
| 	@Test | ||||
| 	void testClone() { | ||||
| 		Board clone = new Board(board); | ||||
| 		Board clone = new Board(board, false); | ||||
| 		assertNotSame(clone, board); | ||||
| 		assertNotSame(clone.getBoardArr(), board.getBoardArr()); | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 GitHub
						GitHub