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

3
.gitignore vendored
View File

@@ -8,4 +8,5 @@ app/bin
.vscode .vscode
audio/*.wav audio/*.wav
app/audio/*.wav

View File

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

View File

@@ -1,5 +1,7 @@
package chess.controller; package chess.controller;
import java.util.List;
import chess.controller.Command.CommandResult; import chess.controller.Command.CommandResult;
import chess.controller.commands.UndoCommand; import chess.controller.commands.UndoCommand;
import chess.controller.event.AsyncGameDispatcher; import chess.controller.event.AsyncGameDispatcher;
@@ -42,6 +44,13 @@ public class CommandExecutor {
return result; 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) { private void processResult(Command command, CommandResult result) {
switch (result) { switch (result) {
case NotAllowed: case NotAllowed:

View File

@@ -112,21 +112,33 @@ public class ChessBoard {
} }
public Coordinate findKing(Color color) { 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()]; 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) { public boolean isKingInCheck(Color color) {
Coordinate kingPos = findKing(color); Coordinate kingPos = findKing(color);
assert kingPos.isValid() : "King position is invalid!"; assert kingPos.isValid() : "King position is invalid!";

View File

@@ -4,6 +4,7 @@ import java.util.List;
import chess.controller.CommandExecutor; import chess.controller.CommandExecutor;
import chess.controller.PlayerCommand; import chess.controller.PlayerCommand;
import chess.controller.Command.CommandResult;
import chess.controller.commands.CastlingCommand; import chess.controller.commands.CastlingCommand;
import chess.controller.commands.GetPieceAtCommand; import chess.controller.commands.GetPieceAtCommand;
import chess.controller.commands.GetPlayerMovesCommand; import chess.controller.commands.GetPlayerMovesCommand;
@@ -88,7 +89,7 @@ public class PgnExport {
Piece otherPiece = pieceAt(cmdExec, move.getStart()); Piece otherPiece = pieceAt(cmdExec, move.getStart());
// checking type of piece // checking type of piece
if (otherPiece.hashCode() != movingPiece.hashCode()) if (!otherPiece.getClass().equals(movingPiece.getClass()))
continue; continue;
String startPos = toString(pieceMove.getStart()); 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) { CommandExecutor executor) {
String result = ""; String result = "";
if (cmd instanceof MoveCommand move) { if (cmd instanceof MoveCommand move) {
@@ -177,14 +181,14 @@ public class PgnExport {
result += castling(castlingCommand); result += castling(castlingCommand);
} }
executor.executeCommand(cmd); CommandResult commandResult = executor.executeCommand(cmd);
// check or checkmate // check or checkmate
result += checkCheckMate(virtualGame); result += checkCheckMate(virtualGame);
result += " "; result += " ";
return result; return new MoveResult(result, commandResult);
} }
public static String exportGame(Game game) { public static String exportGame(Game game) {
@@ -199,17 +203,33 @@ public class PgnExport {
int tour = 1; int tour = 1;
String lastMove = null;
for (int i = 0; i < commands.size(); i++) { for (int i = 0; i < commands.size(); i++) {
PlayerCommand cmd = commands.get(i); PlayerCommand cmd = commands.get(i);
PlayerCommand nextCommand = null; PlayerCommand nextCommand = null;
if (i != commands.size() - 1) { if (i != commands.size() - 1) {
nextCommand = commands.get(i + 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 + "."; result += tour + ".";
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); 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) { private void buildButtons(JPanel bottom) {
castlingButton.addActionListener((_) -> { castlingButton.addActionListener((event) -> {
sendCommand(new CastlingCommand(false)); sendCommand(new CastlingCommand(false));
}); });
bigCastlingButton.addActionListener((_) -> { bigCastlingButton.addActionListener((event) -> {
sendCommand(new CastlingCommand(true)); sendCommand(new CastlingCommand(true));
}); });
undoButton.addActionListener((_) -> { undoButton.addActionListener((event) -> {
sendCommand(new UndoCommand()); 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 1. e4 e5 2. Nc3
Bc5 {J'aime un feu chaud.} 3. Nf3 {It's too cold in here for my liking.} 3... Bc5 3. Nf3
Qf6 4. Nd5 Qd6 5. d3 c6 {A fiery position is what I seek $1} 6. Nc3 h6 7. a3 Qg6 Qf6 4. Nd5 Qd6 5. d3 c6 6. Nc3 h6 7. a3 Qg6
8. Nxe5 {Things are beginning to heat up, non $2} 8... Qd6 9. Nc4 Qe6 10. d4 Be7 8. Nxe5 Qd6 9. Nc4 Qe6 10. d4 Be7
11. Ne3 b5 12. Nf5 d5 13. Nxg7+ {That's not very nice.} 13... Kd7 14. Nxe6 11. Ne3 b5 12. Nf5 d5 13. Nxg7+ Kd7 14. Nxe6
{Brrrrrr. It is getting cold in here.} 14... fxe6 15. exd5 cxd5 16. Bf4 Nf6 17. 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 Bxb5+ Kd8 18. Qf3 Bd7 19. Be5 a6 20. Bxf6 Re8 21. Bxe7+ Kxe7 22. Nxd5+ exd5 23. Qxd5
more firewood $1} 19... a6 20. Bxf6 Re8 21. Bxe7+ Kxe7 22. Nxd5+ exd5 23. Qxd5 Kf8+ 24. Be2 Bc6 25. Qd6+ Re7 26. Kf1 Ba4 27. b3
Kf8+ {It's getting toasty in here $1} 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 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 Kh7 34. Qg6+ Kh8 35. Ra2 Rxd4 36. Qg8# 1-0
play $1 I'll have to throw another log on the fire and try again.} 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();
}
}
}