feat: pgn parser

This commit is contained in:
2025-05-03 20:46:20 +02:00
parent b2a6b23681
commit b18b53f195
21 changed files with 384 additions and 219 deletions

View File

@@ -43,16 +43,20 @@ run {
standardInput = System.in
}
// Apply a specific Java toolchain to ease working on different environments.
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
jar {
manifest {
attributes 'Main-Class': application.mainClass
}
}
run {
standardInput = System.in
}
tasks.named('test') {
// Use JUnit Platform for unit tests.
useJUnitPlatform()

View File

@@ -1,5 +1,7 @@
package chess.controller;
import java.util.List;
import chess.controller.Command.CommandResult;
import chess.controller.commands.UndoCommand;
import chess.controller.event.AsyncGameDispatcher;
@@ -42,6 +44,13 @@ public class CommandExecutor {
return result;
}
public void executeCommands(List<? extends Command> commands) {
for (Command command : commands) {
CommandResult result = executeCommand(command);
assert result != CommandResult.NotAllowed;
}
}
private void processResult(Command command, CommandResult result) {
switch (result) {
case NotAllowed:

View File

@@ -112,21 +112,33 @@ public class ChessBoard {
}
public Coordinate findKing(Color color) {
// KingIdentifier kingIdentifier = new KingIdentifier(color);
// for (int i = 0; i < Coordinate.VALUE_MAX; i++) {
// for (int j = 0; j < Coordinate.VALUE_MAX; j++) {
// Coordinate coordinate = new Coordinate(i, j);
// Piece piece = pieceAt(coordinate);
// if (kingIdentifier.isKing(piece)) {
// return coordinate;
// }
// }
// }
// assert false : "No king found ?!";
// return null;
return kingPos[color.ordinal()];
}
public List<Coordinate> getAllowedStarts(Coordinate finish, Color color) {
List<Coordinate> starts = new ArrayList<>();
for (int i = 0; i < Coordinate.VALUE_MAX; i++) {
for (int j = 0; j < Coordinate.VALUE_MAX; j++) {
Coordinate attackCoords = new Coordinate(i, j);
Piece attackPiece = pieceAt(attackCoords);
if (attackPiece == null || attackPiece.getColor() != color)
continue;
Move move = new Move(attackCoords, finish);
PiecePathChecker piecePathChecker = new PiecePathChecker(this,
move);
if (!piecePathChecker.isValid())
continue;
applyMove(move);
if (!isKingInCheck(color))
starts.add(attackCoords);
undoLastMove();
}
}
return starts;
}
public boolean isKingInCheck(Color color) {
Coordinate kingPos = findKing(color);
assert kingPos.isValid() : "King position is invalid!";

View File

@@ -4,6 +4,7 @@ import java.util.List;
import chess.controller.CommandExecutor;
import chess.controller.PlayerCommand;
import chess.controller.Command.CommandResult;
import chess.controller.commands.CastlingCommand;
import chess.controller.commands.GetPieceAtCommand;
import chess.controller.commands.GetPlayerMovesCommand;
@@ -88,7 +89,7 @@ public class PgnExport {
Piece otherPiece = pieceAt(cmdExec, move.getStart());
// checking type of piece
if (otherPiece.hashCode() != movingPiece.hashCode())
if (!otherPiece.getClass().equals(movingPiece.getClass()))
continue;
String startPos = toString(pieceMove.getStart());
@@ -149,7 +150,10 @@ public class PgnExport {
}
}
private static String printMove(PlayerCommand cmd, PlayerCommand nextCommand, Game virtualGame,
private record MoveResult(String move, CommandResult commandResult) {
}
private static MoveResult printMove(PlayerCommand cmd, PlayerCommand nextCommand, Game virtualGame,
CommandExecutor executor) {
String result = "";
if (cmd instanceof MoveCommand move) {
@@ -177,14 +181,14 @@ public class PgnExport {
result += castling(castlingCommand);
}
executor.executeCommand(cmd);
CommandResult commandResult = executor.executeCommand(cmd);
// check or checkmate
result += checkCheckMate(virtualGame);
result += " ";
return result;
return new MoveResult(result, commandResult);
}
public static String exportGame(Game game) {
@@ -199,17 +203,33 @@ public class PgnExport {
int tour = 1;
String lastMove = null;
for (int i = 0; i < commands.size(); i++) {
PlayerCommand cmd = commands.get(i);
PlayerCommand nextCommand = null;
if (i != commands.size() - 1) {
nextCommand = commands.get(i + 1);
}
if (virtualGame.getPlayerTurn() == Color.White) {
MoveResult moveResult = printMove(cmd, nextCommand, virtualGame, executor);
if (moveResult.commandResult() == CommandResult.Moved && virtualGame.getPlayerTurn() == Color.Black) {
result += tour + ".";
tour++;
}
result += printMove(cmd, nextCommand, virtualGame, executor);
if (moveResult.commandResult() == CommandResult.ActionNeeded) {
lastMove = moveResult.move();
continue;
}
if (lastMove != null && moveResult.commandResult() == CommandResult.Moved){
result += lastMove;
lastMove = null;
continue;
}
result += moveResult.move();
}
return result + " " + gameEnd(virtualGame);
}

View File

@@ -0,0 +1,22 @@
package chess.pgn;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import chess.controller.CommandExecutor;
import chess.view.AssetManager;
public class PgnFileSimulator extends PgnSimulator{
private static String readResource(String path) {
StringBuilder builder = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(AssetManager.getResource(path)));
reader.lines().forEach((line) -> builder.append(line + "\n"));
return builder.toString();
}
public PgnFileSimulator(CommandExecutor commandExecutor, String fileName) {
super(commandExecutor, readResource(fileName));
}
}

View File

@@ -0,0 +1,172 @@
package chess.pgn;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import chess.controller.CommandExecutor;
import chess.controller.PlayerCommand;
import chess.controller.commands.CastlingCommand;
import chess.controller.commands.MoveCommand;
import chess.controller.commands.NewGameCommand;
import chess.controller.commands.PromoteCommand;
import chess.controller.commands.PromoteCommand.PromoteType;
import chess.controller.event.EmptyGameDispatcher;
import chess.model.ChessBoard;
import chess.model.Coordinate;
import chess.model.Game;
import chess.model.Move;
import chess.model.Piece;
import chess.model.pieces.Bishop;
import chess.model.pieces.King;
import chess.model.pieces.Knight;
import chess.model.pieces.Pawn;
import chess.model.pieces.Queen;
import chess.model.pieces.Rook;
public class PgnImport {
private static final Map<String, Class<? extends Piece>> pieceMap = Map.of(
"K", King.class,
"Q", Queen.class,
"R", Rook.class,
"B", Bishop.class,
"N", Knight.class);
private static final Map<String, PromoteType> promoteMap = Map.of(
"Q", PromoteType.Queen,
"R", PromoteType.Rook,
"B", PromoteType.Bishop,
"N", PromoteType.Knight);
public static List<PlayerCommand> importGame(String pgnContent) {
String[] parts = pgnContent.split("\n\n");
// we just ignore headers
return getMoves(parts[parts.length - 1]);
}
private static List<PlayerCommand> getMoves(String unparsedMoves) {
String[] moves = unparsedMoves.replaceAll("\\{.*?\\}", "") // Remove comments
.replaceAll("\\n", " ") // Remove new lines
.split("[\\s.]+"); // Split by whitespace and dots (trimming it also)
Game virtualGame = new Game();
CommandExecutor commandExecutor = new CommandExecutor(virtualGame, new EmptyGameDispatcher());
List<PlayerCommand> instructions = new ArrayList<>();
commandExecutor.executeCommand(new NewGameCommand());
for (int i = 0; i < moves.length; i++) {
if (i % 3 == 0)
continue;
String move = moves[i];
if (move.equals("1-0") || move.equals("0-1") || move.equals("1/2-1/2")) {
break; // End of the game
}
List<PlayerCommand> cmds = parseMove(move, virtualGame);
commandExecutor.executeCommands(cmds);
instructions.addAll(cmds);
}
return instructions;
}
private static List<PlayerCommand> parseMove(String move, Game game) {
if (move.equals("O-O-O"))
return Arrays.asList(new CastlingCommand(true));
if (move.equals("O-O"))
return Arrays.asList(new CastlingCommand(false));
move = move.replaceAll("[x|#|\\+]", "");
PromoteCommand promoteCommand = null;
if (move.contains("=")) {
String promoteString = move.substring(move.length() - 1);
promoteCommand = new PromoteCommand(promoteMap.get(promoteString));
move = move.substring(0, move.length() - 2);
}
Class<? extends Piece> pieceType = pieceMap.get(move.substring(0, 1));
if (pieceType == null)
pieceType = Pawn.class;
else
move = move.substring(1);
assert move.length() == 3 || move.length() == 2;
Coordinate ambiguity = new Coordinate(-1, -1);
// ambiguity
if (move.length() == 3) {
ambiguity = getAmbiguityPattern(move.charAt(0));
move = move.substring(1);
}
Coordinate dest = stringToCoordinate(move);
Coordinate start = getStartCoord(dest, pieceType, ambiguity, game);
List<PlayerCommand> cmds = new ArrayList<>();
cmds.add(new MoveCommand(new Move(start, dest)));
if (promoteCommand != null)
cmds.add(promoteCommand);
return cmds;
}
private static Coordinate getStartCoord(Coordinate dest, Class<? extends Piece> pieceType, Coordinate ambiguity,
Game game) {
final ChessBoard board = game.getBoard();
List<Coordinate> starts = board.getAllowedStarts(dest, game.getPlayerTurn());
assert !starts.isEmpty() : "No moves allowed!";
for (Coordinate start : starts) {
Piece piece = board.pieceAt(start);
if (piece.getClass().equals(pieceType) && coordPatternMatch(start, ambiguity))
return start;
}
assert false : "There is a small problem ...";
return null;
}
private static int getXCoord(char xPos) {
return xPos - 'a';
}
private static int getYCoord(char yPos) {
return Coordinate.VALUE_MAX - 1 - (yPos - '1');
}
private static boolean coordPatternMatch(Coordinate coord, Coordinate pattern) {
if (pattern.getX() != -1 && coord.getX() != pattern.getX())
return false;
if (pattern.getY() != -1 && coord.getY() != pattern.getY())
return false;
return true;
}
private static Coordinate getAmbiguityPattern(char amb) {
if (Character.isDigit(amb))
return new Coordinate(-1, getYCoord(amb));
return new Coordinate(getXCoord(amb), -1);
}
private static Coordinate stringToCoordinate(String coordinates) {
char xPos = coordinates.charAt(0);
char yPos = coordinates.charAt(1);
return new Coordinate(getXCoord(xPos), getYCoord(yPos));
}
}

View File

@@ -0,0 +1,28 @@
package chess.pgn;
import java.util.List;
import chess.controller.CommandExecutor;
import chess.controller.PlayerCommand;
import chess.controller.commands.NewGameCommand;
import chess.controller.event.GameAdaptator;
import chess.model.Game;
public class PgnSimulator extends GameAdaptator {
private final CommandExecutor commandExecutor;
private final String pgn;
public PgnSimulator(CommandExecutor commandExecutor, String pgn) {
this.commandExecutor = commandExecutor;
this.pgn = pgn;
}
@Override
public void onGameStart() {
List<PlayerCommand> cmds = PgnImport.importGame(this.pgn);
this.commandExecutor.executeCommands(cmds);
}
}

View File

@@ -1,31 +0,0 @@
package chess.simulator;
import chess.controller.CommandExecutor;
import chess.model.Coordinate;
import chess.model.Move;
import java.util.Arrays;
import java.util.List;
public class CastlingTest extends Simulator {
public CastlingTest(CommandExecutor commandExecutor) {
super(commandExecutor);
}
@Override
protected List<Move> getMoves() {
return Arrays.asList(
// white pawn
new Move(new Coordinate(6, 6), new Coordinate(6, 4)),
// black knight
new Move(new Coordinate(1, 0), new Coordinate(0, 2)),
// white bishop
new Move(new Coordinate(5, 7), new Coordinate(7, 5)),
// black pawn
new Move(new Coordinate(1, 1), new Coordinate(1, 2)),
// white knight
new Move(new Coordinate(6, 7), new Coordinate(5, 5)),
// black pawn, bis
new Move(new Coordinate(2, 1), new Coordinate(2, 2)));
}
}

View File

@@ -1,29 +0,0 @@
package chess.simulator;
import chess.controller.CommandExecutor;
import chess.model.Coordinate;
import chess.model.Move;
import java.util.Arrays;
import java.util.List;
public class EnPassantTest extends Simulator{
public EnPassantTest(CommandExecutor commandExecutor) {
super(commandExecutor);
}
@Override
protected List<Move> getMoves() {
return Arrays.asList(
// white pawn
new Move(new Coordinate(4, 6), new Coordinate(4, 4)),
// black pawn 1
new Move(new Coordinate(4, 1), new Coordinate(4, 2)),
// white pawn
new Move(new Coordinate(4, 4), new Coordinate(4, 3)),
// black pawn #2
new Move(new Coordinate(3, 1), new Coordinate(3, 3)));
}
}

View File

@@ -1,28 +0,0 @@
package chess.simulator;
import java.util.Arrays;
import java.util.List;
import chess.controller.CommandExecutor;
import chess.model.Coordinate;
import chess.model.Move;
public class FoolCheckMate extends Simulator {
public FoolCheckMate(CommandExecutor commandExecutor) {
super(commandExecutor);
}
@Override
public List<Move> getMoves() {
return Arrays.asList(
// white pawn
new Move(new Coordinate(5, 6), new Coordinate(5, 5)),
// black pawn
new Move(new Coordinate(4, 1), new Coordinate(4, 3)),
// 2nd white pawn
new Move(new Coordinate(6, 6), new Coordinate(6, 4)),
// black queen
new Move(new Coordinate(3, 0), new Coordinate(7, 4)));
}
}

View File

@@ -1,40 +0,0 @@
package chess.simulator;
import java.util.Arrays;
import java.util.List;
import chess.controller.CommandExecutor;
import chess.model.Coordinate;
import chess.model.Move;
public class PromoteTest extends Simulator{
public PromoteTest(CommandExecutor commandExecutor) {
super(commandExecutor);
}
@Override
protected List<Move> getMoves() {
return Arrays.asList(
// white pawn
new Move(new Coordinate(5, 6), new Coordinate(5, 4)),
// black pawn
new Move(new Coordinate(4, 1), new Coordinate(4, 3)),
// white pawn capture
new Move(new Coordinate(5, 4), new Coordinate(4, 3)),
// black king
new Move(new Coordinate(4, 0), new Coordinate(4, 1)),
// white pawn moves
new Move(new Coordinate(4, 3), new Coordinate(4, 2)),
// black king
new Move(new Coordinate(4, 1), new Coordinate(5, 2)),
// white pawn moves
new Move(new Coordinate(4, 2), new Coordinate(4, 1)),
// black king
new Move(new Coordinate(5, 2), new Coordinate(6, 2))
// white pawn moves
// new Move(new Coordinate(4, 1), new Coordinate(4, 0))
);
}
}

View File

@@ -1,39 +0,0 @@
package chess.simulator;
import java.util.List;
import chess.controller.CommandExecutor;
import chess.controller.commands.MoveCommand;
import chess.controller.event.GameAdaptator;
import chess.model.Move;
import common.Signal0;
public abstract class Simulator extends GameAdaptator {
protected final CommandExecutor commandExecutor;
public final Signal0 onComplete = new Signal0();
private int currentMove = 0;
public Simulator(CommandExecutor commandExecutor) {
this.commandExecutor = commandExecutor;
}
@Override
public void onGameStart() {
for (Move move : getMoves()) {
this.commandExecutor.executeCommand(new MoveCommand(move));
}
}
@Override
public void onBoardUpdate() {
currentMove++;
if (currentMove == getMoves().size()) {
onComplete.emit();
}
}
protected abstract List<Move> getMoves();
}

View File

@@ -74,15 +74,15 @@ public class Window extends JFrame implements GameListener {
}
private void buildButtons(JPanel bottom) {
castlingButton.addActionListener((_) -> {
castlingButton.addActionListener((event) -> {
sendCommand(new CastlingCommand(false));
});
bigCastlingButton.addActionListener((_) -> {
bigCastlingButton.addActionListener((event) -> {
sendCommand(new CastlingCommand(true));
});
undoButton.addActionListener((_) -> {
undoButton.addActionListener((event) -> {
sendCommand(new UndoCommand());
});

View File

@@ -0,0 +1 @@
1.g4 Na6 2.Bh3 b6 3.Nf3 c6

View File

@@ -0,0 +1 @@
1.e4 e6 2.e5 d5

View File

@@ -0,0 +1 @@
1.f3 e5 2.g4 Qh4# 0-1

View File

@@ -0,0 +1 @@
1.f4 e5 2.fxe5 Ke7 3.e6 Kf6 4.e7 Kg6

View File

@@ -1,15 +0,0 @@
[Event "URS-chT"]
[Site "Moscow"]
[Date "1963.??.??"]
[Round "?"]
[White "Listergarten, Leonid B"]
[Black "Akopian, Vladimir"]
[Result "1-0"]
[WhiteElo ""]
[BlackElo ""]
[ECO "B48"]
1.e4 c5 2.Nf3 e6 3.d4 cxd4 4.Nxd4 a6 5.Nc3 Qc7 6.Bd3 Nc6 7.Be3 b5 8.a3 Bb7
9.O-O Rc8 10.Nxc6 Qxc6 11.Qg4 Nf6 12.Qg3 h5 13.e5 Nd5 14.Ne4 h4 15.Qh3 Qc7
16.f4 Nxe3 17.Qxe3 h3 18.gxh3 f5 19.exf6 d5 20.Nf2 Kf7 21.Rae1 Re8 22.Qg3 g5
23.fxg5 Qxg3+ 24.hxg3 e5 25.g6+ Kxf6 26.Ng4+ Kg5 27.Rf5+ 1-0

View File

@@ -1,12 +1,10 @@
1. e4 {1.e4 $1 A fiery start $1} 1... e5 {I like how this game is starting $1} 2. Nc3
Bc5 {J'aime un feu chaud.} 3. Nf3 {It's too cold in here for my liking.} 3...
Qf6 4. Nd5 Qd6 5. d3 c6 {A fiery position is what I seek $1} 6. Nc3 h6 7. a3 Qg6
8. Nxe5 {Things are beginning to heat up, non $2} 8... Qd6 9. Nc4 Qe6 10. d4 Be7
11. Ne3 b5 12. Nf5 d5 13. Nxg7+ {That's not very nice.} 13... Kd7 14. Nxe6
{Brrrrrr. It is getting cold in here.} 14... fxe6 15. exd5 cxd5 16. Bf4 Nf6 17.
Bxb5+ Kd8 18. Qf3 Bd7 19. Be5 {My attack is getting cold, I need to go get some
more firewood $1} 19... a6 20. Bxf6 Re8 21. Bxe7+ Kxe7 22. Nxd5+ exd5 23. Qxd5
Kf8+ {It's getting toasty in here $1} 24. Be2 Bc6 25. Qd6+ Re7 26. Kf1 Ba4 27. b3
1. e4 e5 2. Nc3
Bc5 3. Nf3
Qf6 4. Nd5 Qd6 5. d3 c6 6. Nc3 h6 7. a3 Qg6
8. Nxe5 Qd6 9. Nc4 Qe6 10. d4 Be7
11. Ne3 b5 12. Nf5 d5 13. Nxg7+ Kd7 14. Nxe6
fxe6 15. exd5 cxd5 16. Bf4 Nf6 17.
Bxb5+ Kd8 18. Qf3 Bd7 19. Be5 a6 20. Bxf6 Re8 21. Bxe7+ Kxe7 22. Nxd5+ exd5 23. Qxd5
Kf8+ 24. Be2 Bc6 25. Qd6+ Re7 26. Kf1 Ba4 27. b3
Nc6 28. bxa4 a5 29. Qxc6 Rd8 30. Qxh6+ Kg8 31. Bc4+ Rf7 32. Qg5+ Kh8 33. Bxf7
{C'est très, très mauvais $1} 33... Kh7 34. Qg6+ Kh8 35. Ra2 Rxd4 36. Qg8# {Good
play $1 I'll have to throw another log on the fire and try again.} 1-0
Kh7 34. Qg6+ Kh8 35. Ra2 Rxd4 36. Qg8# 1-0

View File

@@ -0,0 +1,77 @@
package chess;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import org.junit.jupiter.api.Test;
import chess.ai.DumbAI;
import chess.controller.CommandExecutor;
import chess.controller.PlayerCommand;
import chess.controller.commands.NewGameCommand;
import chess.controller.event.GameAdaptator;
import chess.model.Color;
import chess.model.Game;
import chess.pgn.PgnExport;
import chess.pgn.PgnImport;
public class PgnTest {
private Game getRandomGame() {
Game game = new Game();
CommandExecutor commandExecutor = new CommandExecutor(game);
commandExecutor.addListener(new DumbAI(commandExecutor, Color.White));
commandExecutor.addListener(new DumbAI(commandExecutor, Color.Black));
commandExecutor.addListener(new GameAdaptator() {
@Override
public void onGameEnd() {
synchronized (game) {
game.notifyAll();
}
}
});
commandExecutor.executeCommand(new NewGameCommand());
synchronized (game) {
try {
game.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
commandExecutor.close();
return game;
}
private void importExport() {
Game game = getRandomGame();
String pgnContent = PgnExport.exportGame(game);
List<PlayerCommand> moves = PgnImport.importGame(pgnContent);
Game game2 = new Game();
CommandExecutor commandExecutor = new CommandExecutor(game2);
commandExecutor.executeCommand(new NewGameCommand());
commandExecutor.executeCommands(moves);
String pgnContent2 = PgnExport.exportGame(game2);
commandExecutor.close();
assertEquals(pgnContent, pgnContent2);
}
@Test void importExports() {
for (int i = 0; i < 50; i++) {
importExport();
}
}
}