Java шахматы

Load a chess game collection from a PGN file

    PgnHolder pgn = new PgnHolder("/opt/games/linares_2002.pgn");
    pgn.loadPgn();
    for (Game game: pgn.getGames()) {
        game.loadMoveText();
        MoveList moves = game.getHalfMoves();
        Board board = new Board();
        //Replay all the moves from the game and print the final position in FEN format
        for (Move move: moves) {
            board.doMove(move);
        }
        System.out.println("FEN: " + board.getFen());
    }

You could achieve the same by loading the move list final FEN position:

    ...
    board.loadFromFen(moves.getFen());

Iterating over a PGN file games using the :

    PgnIterator games = new PgnIterator("/opt/games/linares_2002.pgn");
    for (Game game: games) {
        System.out.println("Game: " + game);
    }

Note: The iterator is highly recommended for processing large PGN files as it is not retaining in the memory
intermediate objects loaded during the process of each iteration.

Capturing the comments from each move:

    PgnIterator games = new PgnIterator("src/test/resources/rav_alternative.pgn");
    for (Game game: games) {
        String[] moves = game.getHalfMoves().toSanArray();
        Map<Integer, String> comments = game.getComments();
        for (int i = ; i < moves.length; i++) {
            String ply = ((i + 2) / 2) + (i % 2 !=  ? ".." : " ");
            String move = moves;
            String comment = comments.get(i + 1) + "";
            System.out.println(ply + move + " " + comment.trim());
        }
    }

The output should be something like:

Capturing and reacting to events

Actions occurring in the chessboard or when loading a PGN file are emitted as events by the library so that it can
be captured by a GUI, for example:

Listening to PGN loading progress

Create your listener:

class MyListener implements PgnLoadListener {
    
    private int games = ;

     @Override
    public void notifyProgress(int games) {
        System.out.println("Loaded " + games + " games...");
    }
}

Add the listener to object and load the games:

    PgnHolder pgn = new PgnHolder(".../games.pgn");
    // add your listener
    pgn.getListener().add(myListener);
    pgn.loadPgn();

Example implementing a to update a Swing with PGN loading status:

private final ProgressBarDialog progress = new ProgressBarDialog("PGN Loader", frame);
...
private void init() {
    ...
    LoadPGNWorker loadPGNWorker = new LoadPGNWorker();
    loadPGNWorker.addPropertyChangeListener(new PropertyChangeListener() {
        @Override
        public void propertyChange(PropertyChangeEvent e) {                
            progress.getProgressBar().setIndeterminate(true);
            progress.getProgressBar().setValue((Integer) e.getNewValue());
        }
    });
    loadPGNWorker.execute();

}
...
private class LoadPGNWorker extends SwingWorker<Integer, Integer> implements PgnLoadListener {

    @Override
    protected Integer doInBackground() throws Exception {
        try {
            getPgnHolder().getListener().add(this);
            loadPGN();
        } catch (Exception e) {
            JOptionPane.showMessageDialog(owner, errorMessageFromBundle + e.getMessage(), JOptionPane.ERROR_MESSAGE);
            log.error("Error loading pgn", e);
        } finally {
            progress.dispose();
        }
        return getPgnHolder().getSize();
    }
    
    @Override
    protected void done() {
        setProgress(100);
    }
    
    @Override
    public void notifyProgress(int games) {
        setProgress(Math.min(90, games));
        progress.getLabel().setText("Loading games...");
    }
}

Listening to chessboard events

Moves played and game statuses are emitted by the whenever these actions happen.

Implement your listener:

class MyBoardListener implements BoardEventListener {

    @Override
    public void onEvent(BoardEvent event) {

        if (event.getType() == BoardEventType.ON_MOVE) {
            Move move = (Move) event;
            System.out.println("Move " + move + " was played");
        }
    }
}

Add your listener to and listen to played moves events:

    Board board = new Board();
    board.addEventListener(BoardEventType.ON_MOVE, new MyBoardListener());    

    handToGui(board);
    ...

Beware that listeners are executed using the calling thread that updated the Board and
depending on your listener processing requirements you’d want to hand the execution off
to a separate thread like in a threadpool:

    public void onEvent(BoardEvent event) {

        executors.submit(myListenerRunnable);
    }

4 — The UI ♜

To create the UI, I used the Swing package.

Printing the Interface

The interface is mainly made through the superposition of JPanels. To create the chess board, I created a 2D array of JButtons, each of alternating colours. I used a nested for loop to print the whole chess board. I also used this to assign coordinates to each JButton square, which allowed me to display a picture representing a piece on top of the square. Then, with each move, I executed the for loop again, but with pieces updated to reflect the current game. I also created an area to the right of the board that contained a section to see the moves played, alongside a button for saving a game, and an area displaying the outcome of a game (a draw, stalemate or win).

Handling movements

To make a move, the user needs to select a piece, and then select a destination square. I created a flag that would allow me to check whether the user has clicked twice, as this would represent a move. When the user clicks a square (JButton), I looped through the array of JButtons until I found the JButton that had been clicked. I then turned this information into a Coordinate, which then allowed me to find the Piece occupying the square. This then made it so the squares corresponding to the potential moves of the piece got illuminated. It also set the flag to true. Once there was a second click the program checked to see if the selected square corresponded to one of the potential moves of the piece. If so, the move was executed and the board was updated, resetting the flag. Otherwise, the potential moves of the selected piece would be shown.

1 — Basic Logic ♟

The logic for the terminal based game and the UI is essentially the same:

  1. Instantiate a pieces class. This is the board of the game, and contains all of the pieces.
  2. Select a piece and select a coordinate. If the destination coordinate is a valid coordinate for your given piece, and its the piece’s colour turn to move, then make a move.
  • this is represented by changing pieces.
  • the piece’s key changes from its origin to its destination coordinate
  • if a piece from the opposite colour was occupying the coordiante, that piece is eliminated
  1. Check if its check mate. If so, the game ends. Otherwise, it is the turn of the other colour.
Популярные статьи  Тактика в защите Грюнфельда

Memory Usage

Normally you wouldn’t need to worry about memory usage, but if you want to tweak chess4j here is some important information.

chess4j currently employs two transposition tables. One is used in the main search and one in the pawn evaluation. The default size for each table is 128 MB. (If you run with the -native option, the default size may be different.)

You can specify the maximum memory allocated to each table via command line parameters, but you would really only want to do this if you were running the program directly from the command line, and not using a Winboard compatible GUI or test harness.
(I do this when running test suites but that’s about it.)

The above arguments would allocate 256 MB to each table.

Winboard / XBoard has an option to specify the maximum memory usage, and chess4j does respect that. chess4j will divide the memory equally between the two tables.

2 — Enums ♞

Enums were an integral part of this project, as I used them to represent important constants for the game. The enums I created were ID, COLOUR and BOARD.

ID

Used as an identifier for a piece. The types of pieces are:

  • KING
  • QUEEN
  • ROOK
  • BISHOP
  • KNIGHT
  • PAWN

This enum contained 2 methods. One () is used to print the piece’s letter for describing moves. For example, if a bishop moves to e6, such move would be described as Be6. The other one () is mainly used for testing and «human» printing purposes. It returns the full name of the String. For example, would return .

COLOUR

Used as a colour identifier for a piece. These are:

  • B (a black piece)
  • W (a white piece)

This enum also contains 2 methods, albeit these are barely used (mainly only for tests). The most important method is the method. It is used to return the opposite colour to the argument it takes. Hence, would return . This is extremely useful, for example when handling the turn of play, or calculating when a move leads to check.

BOARD

Used to contain the dimensions of the board. These are determined by 4 constants:

  • FIRST_FILE(‘a’)
  • LAST_FILE(‘h’)
  • FIRST_RANK(1)
  • LAST_RANK(8)

A file is used to represent a column, and is represented by a character from a to h. A rank represents a row, and is represented by an integer from 1 to 8. BOARD contains methods to access the values associated with these constants.

Building from Source

chess4j can be built with or without Prophet bundled as a static library. The benefit of bundling Prophet is simply speed. When Prophet is bundled and activated (using the ‘-native’ command line argument), chess4j will leverage Prophet when «thinking.» Since Prophet is written in C and is native, it will execute about 2-3 times faster. It doesn’t move any faster, but it will «see further» and therefore play stronger moves. Otherwise, the algorithms are the same. Just keep in mind that native code is platform dependent. Currently the only platform supported for bundling Prophet is Linux.

Whether you want to bundle Prophet or not, you will need a Java 11 (or better) SDK and Maven. You will probably also need to ensure the JAVA_HOME environment variable is properly set.

Without the Prophet Engine

Clone the repository and go into the chess4j/chess4j-java directory.

Once this process is complete you should see the build artifact in the target directory, e.g. chess4j-java-5.0-uber.jar. Verify everything is working:

You should see the program search for about 10 seconds and display the result.

With the Prophet Engine

This option is slighly more complex. In addition to the other prerequisites, you’ll also need a working C/C++ toolchain. I always use gcc / g++. Others may work but have not been tested. You’ll also need ‘make’ and a copy of Google Test from https://github.com/google/googletest . Finally, set an environment variable GTEST_DIR to point to the ‘googletest’ project (we don’t need the googlemock stuff).

Once you have the prerequisites, clone the chess4j repository. Since Prophet4 is a separate project, you’ll need to do a recursive clone, e.g.

If that worked you should see the contents of chess4j/lib/prophet populated. Now, just go into the top level ‘chess4j’ directory and execute:

That will kick off the build process, first building Prophet, then the JNI code that is the «bridge» between the Java and C layer, and finally chess4j itself. The final build artifact will be in the chess4j-java/target directory.

You now have the option to run with or without the native (Prophet) code enabled by using the ‘-native’ command line argument. If you omit that argument, the native code will not be enabled.

Verify everything is working:

(Note the ‘-native’ argument.)

You should see the program search for about 10 seconds and display the result.

About the game

Before you start the game

Before the game starts, you will be able to adjust some parameters:

  • Color choice: pieces of chosen color will be initialized at the bottom
  • Rule choice: as there are some not-sure-if-I-like-this-rules, specific rules can be deactivated
  • Mode choice: choose how players are controlled (by you or AI)
  • Difficulty choice: the AI player difficulty can be controlled here. the highest setting is around the difficulty level 5-6 of Chess.com
  • Load: here, you can load existing games from FEN or PGN code

The game

The game can be played by clicking or by using drag&drop. For more information about how to handle the application see the manual. If you are not very familiar with the chess game itself, there is an additional file which quickly explains the rules.

The AI opponent

The AI opponent was created by using a minimax algorithm with alpha-beta-pruning. This means, that the opponent will simulate a few moves ahead and then choose the move which seems to avoid the worst situation while maximizing the chance to win.

The algorithm takes following to account:

  • Recursion depth (steps of simulating moves ahead)
  • Heuristics to add recursion depth for the endgame
  • Piece values (e.g. a queen is valued higher than a pawn)
  • Piece position values (e.g. a knight is valued higher when positioned at the center of the board, as it has more move options there)
  • So called ‘spasm-parameter’, which will generate random moves occasionally to appear more human
  • Library of known chess openings to obtain a good starting position
  • Heuristics to avoid a a draw by stalemate
  • Heuristics to avoid a draw by threefold repetition
  • Heuristics to avoid a draw by 50 ‘moves of no value’

Depending on the rule and difficulty settings, the behavior of the AI opponent will change accordingly.
Example: On the highest difficulty setting we have a recursion depth of 4, while on the lowest difficulty setting we have a no recursion at all.

As the recursion depth has a huge impact on the speed of the caluclations (which increases exponentially), the maximum depth of 4 seemed a suitable compromise to get a more-or-less smart opponent, without having to wait 100 years for the next move.

Chess notations

During the game, the moves are logged to the console and can be exported as html file. Here, the long algebraic notation (LAN) is used.
Example for LAN:

For importing and exporting games, the FEN and PGN notations are used. These notations are widely used for chess games.

The FEN notation is a very compact one-line-code for a current board situation. It is useful if a board state should be quickly copied to be recreated later and/or in another chess program.
Example for FEN:

The PGN notation is more complex. It contains metadata about the game (players, location, round, etc.) and the complete list of moves. This means, a whole game can be recreated step by step, which is useful if you want to do analyzes.
Example for PGN:

Editing a game

I really hated to implement this feature, as in a real chess game, steps should not be undone. Still, it’s cool for whait-what-happened-moments or analyzing already played games.

Thoughts about this project

I did this project for fun and curiosity. The main motivation I was driven by was ‘how do I get a nice chess game with a really smart AI’ (and so far, I was not able to beat the AI myself).
As you will see in the code, many of the features (e.g. exports, edit mode) grew in in a quite organic way. If I was more serious about it, the architecture should have been restructured accordingly.
The main issue of the AI opponent is the speed of the calculations, which limits the recursion depth to 4. Recursion depth could be increased if the move generation handling would be closer to machine code (i.e. bit shifting methods). After all, Java might not be the best programming language for that purpose.
Another issue is testability. As the coupling is not loose enough, I was not going too far with unit tests.
Some other time, I will do further work here or start again from scratch.

Sanity checking of chesslib move generation with Perft

Perft, (performance test, move path enumeration) is a debugging function to walk the
move generation tree of strictly legal moves to count all the leaf nodes of a certain depth.
Example of a perft function using chesslib:

    private long perft(Board board, int depth, int ply) throws MoveGeneratorException {

        if (depth == ) {
            return 1;
        }
        long nodes = ;      
        List<Move> moves = board.legalMoves();
        for (Move move : moves) {
            board.doMove(move);
            nodes += perft(board, depth - 1, ply + 1);
            board.undoMove();
        }
        return nodes;
    }

There are plenty of known results for Perft tests on a given set of chess positions.
It can be tested against the library to check if it’s reliably generating moves and while
keeping the in a consistent state, e.g.:

    @Test
    public void testPerftInitialPosition() throws MoveGeneratorException {

        Board board = new Board();
        board.setEnableEvents(false);
        board.loadFromFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");

        long nodes = perft(board, 5, 1);
        assertEquals(4865609, nodes);
    }

It’s known that from the initial standard chess position, there should have exactly 4865609 positions
for depth 5. Deviation from this number would imply a bug in move generation or keeping the board state.

Using the API

Declaring a board model

Example of a ChessBoardModel object initialized and displayed in the standard output.

public static void main(String... args) {
    ChessBoardModel board = new ChessBoardModel();

    board.setInitialPosition();

    System.out.println("Current position is:");
    System.out.println(board);
}

Will output:

The position is displayed as multi-line ascii data:

  • Which player has to play
  • Four flags indicating if castling is still available at the four corners
  • The board data:
    • Uppercase for white
    • Lowercase for black

This example introduces the ChessRules interface.

Here the board model is accessed through the Position interface which is read-only. While the board model is internally mutable, it is recommended to avoid updating it in-place.

The following code is:

  • Initializing a ChessRules instance
  • Getting a read-only board in an initial position
  • Iterating over all available moves
  • Displaying the move and generating a new board model that will hold the new position after the move is played

The source code:

public static void main(String... args) throws IllegalMoveException {
    ChessRules rules = new ChessRulesImpl();
    ChessPosition position = rules.getInitialPosition();

    System.out.println("Current position:");
    System.out.println(position);

    System.out.println("Available moves:");

    for (ChessMovePath movePath : rules.getAvailableMoves(position)) {
        System.out.println("--> " + movePath);
        List<ChessBoardUpdate> updates = rules.getUpdatesForMove(position, movePath);

        ChessBoardModel afterMove = new ChessBoardModel();
        afterMove.setPosition(position);
        for ( ChessBoardUpdate update: updates) {
            update.apply(afterMove);
        }
        afterMove.nextPlayerTurn();

        System.out.println(afterMove);
    }
}

Note that the move is split in a collection of board updates that will breakdown the move in atomic board piece move/add/remove. This is handy to animate the board between moves.

Популярные статьи  Мат в 3 хода, автор задачи Сэмуэль Лойд

Output:

Replaying moves in PGN format

In the following example, we will be demonstrating the use of the PgnMarshaller. Since it has to be connected to the chess rules by autowiring, we will include our components in a Spring context.
PGN notation is a worldwide chess standard, here the moves are fetched from a static array. In a real life situation, these moves can be loaded from a file or a database.

private static final String[] pgnMoves = {"e4", "e5", "Nf3", "Nc6", "Bb5"};

public static void main(String... args) throws IllegalMoveException, PgnMoveException {
    ApplicationContext appContext = new AnnotationConfigApplicationContext(
            ChessRulesImpl.class, PgnMarshallerImpl.class);

    ChessRules rules = appContext.getBean(ChessRules.class);
    PgnMarshaller marshaller = appContext.getBean(PgnMarshaller.class);
    ChessPosition position = rules.getInitialPosition();

    for (String pgn : pgnMoves) {
        ChessMovePath movePath = marshaller.convertPgnToMove(position, pgn);
        List<ChessBoardUpdate> updates = rules.getUpdatesForMove(position, movePath);

        ChessBoardModel afterMove = new ChessBoardModel();
        afterMove.setPosition(position);
        for (ChessBoardUpdate update : updates) {
            update.apply(afterMove);
        }
        afterMove.nextPlayerTurn();

        position = afterMove;
    }

    System.out.println(position);
}

Output:

Concise code using ChessHelper

The ChessHelper class provides methods allowing to directly apply a move on a given position, returning the destination position. This is used to avoid having to apply all board updates in a nested loop.

@Autowired
private ChessRules chessRules;

@Autowired
private PgnMarshaller pgnMarshaller;

@Test
public void testGame() throws PgnMoveException, IllegalMoveException {
    String[] history = {"f3", "e5", "g4", "Qh4"};
    ChessPosition position = chessRules.getInitialPosition();

    for ( String pgnMove: history) {
        ChessMovePath path = pgnMarshaller.convertPgnToMove(position, pgnMove);
        position = ChessHelper.applyMove(chessRules, position, path);
    }

    System.out.println(position);
}

This will output:

3 — Key Classes ♝

There are 4 key classes that sustain this project: Coordinate, Piece, Pieces and Move. The first 3 are used to create objects to represent the chess board and its pieces. They all contain getters, setters, alongside functionality to create deep copies of its instances. This is paramount, as will be explained later. The methods , and have all been overridden. The last class, Move simply contains methods that are essential for the correct functioining of the project.

Coordinate

Uses a char (file) and an int (rank) to determine a square within a board, according to Chess nomenclature. Includes functionality to ensure that the arguments provided represent a valid coordinate within the board.

Piece

A class identifying the pieces of the game. A piece is initialised with an ID (type of piece), a COLOUR (black or white) and its initial Coordinate within the board. It acts as a super class to the more specific pieces: King, Queen, Rook, Bishop, Knight, and Pawn. The most important method in Piece have to do with the creation, updating and validation of the moves that a piece can move. We define raw moves as those moves that a piece can make independently of whether the King is in check of not. Potential moves are the actual moves that a piece can make, taking checks into accounts. Piece contains abstract methods that are then individually defined within the children classes. For example, is used to obtain the raw moves that are available to an individual piece. Since each piece moves differently, the details of are defined individually.

Perhaps the most important of all its methods is . This method is used to take in the raw moves available to a piece, and then filter out all of the moves that are impossible; namely those that would either:

  • lead to check
  • not stop a check (i.e if a piece moves away from the King, leading to a check by the opposition)

In order to do this, we must create a deep copy of the board. From the raw moves of the piece, we make the piece execute the move within the copied board. We then check if that has lead to situation of check by the opposition. If it has, said move is deleted. Otherwise, it is maintained. This is a crucial process, as it allows the pieces to determine all of their moves, so checking whether the move provided by the user is legal becomes trivial. Moreover, for the UI, it allows us to display all the moves avaialbale to the given piece.

Pieces

Contains a HashMap with Coordinate-Piece key-value pairs. It contains all the methods used to handling the positioning of the pieces throughout the game. For example, we can use it to find the King of a certain colour, determine which pieces lie on the same file or whether it is the end of the game (via check mate or a draw/stalemate). Pieces also contains the method that executes the moves provided by the user. It is a particularly long method, which must check for all moves that constitute special cases, such as a King castling or a pawn queening/capturing in diagonal/en passant.

Move

This class contains all the classes pertinent to the movement of the pieces. It contains functionality to, given a board (Pieces) and a piece determine which range of movement it has. We can determine available moves in vertical, diagonal and horizontal direction, alongside the moves available to a Knight. It is these methods that are used within a Piece to determine the raw moves available to them. It must be noted that there are pieces, such as the King or the Pawn that have a special range of moves available to them. The handling of these moves is made directly within their classes.

Оцените статью
Павел Алексеев
Добавить комментарии

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

Java шахматы
Мат в 1 ход для тех, кто снова на карантине