Add assignment
This commit is contained in:
844
README.md
Normal file
844
README.md
Normal file
@@ -0,0 +1,844 @@
|
||||
[TOC]
|
||||
|
||||
# Introduction
|
||||
|
||||
In this project, the goal is to develop a [*chess engine*][chess engine] in C++.
|
||||
A chess engine is a computer program that analyzes chess positions and generates a move that it considers best.
|
||||
It typically consists of three major parts:
|
||||
- *Board representation*: the data structure that stores all relevant information about a chess position;
|
||||
- *Move generation*: given a position, generate all valid moves from that position;
|
||||
- *Search*: use move generation to create a game tree and search it for good moves using an *evaluation heuristic*.
|
||||
|
||||
You will be implementing all three parts in this project.
|
||||
|
||||
## Goal
|
||||
|
||||
The main goal of this project is to create a *correct* implementation.
|
||||
Many [tests](#testing) are provided to help you validate the correctness of your implementation.
|
||||
*However*, performance is an important aspect of chess engines.
|
||||
The search space is simply too large for a brute-force search, even at very small depths.
|
||||
Therefore, you are expected to put some effort in optimizing your engine.
|
||||
|
||||
Some important points about our evaluation:
|
||||
- Passing all [unit tests](#unit-tests) is a good indication of a correct board representation and move generation implementation.
|
||||
However, not all corner cases are tested and we *will* run more tests;
|
||||
- Passing all [puzzle tests](#puzzles) is a good indication of a correct and adequately performing search/evaluation implementation.
|
||||
However, we *will* use more puzzles of increasing difficulty for our evaluation;
|
||||
- All given puzzles are run with a time limit of 3 minutes.
|
||||
Puzzles that take longer to solve will fail.
|
||||
|
||||
It is a good idea to add more tests yourself!
|
||||
|
||||
## Grading
|
||||
|
||||
This project counts for 5/20 points of the CPL course.
|
||||
Of those 5 points, 4 are used to grade the correctness and quality of your implementation and 1 is used to assess its performance.
|
||||
More concretely:
|
||||
|
||||
- 3/5: Correctness.
|
||||
Tested by the unit tests and "mate-in-N" puzzles.
|
||||
We *will* use more tests than the ones provided.
|
||||
- 1/5: Code quality.
|
||||
We will check some objective measures like the absence of undefined behavior and memory leaks.
|
||||
- 1/5: Performance.
|
||||
This might include puzzles in the style of the provided "crushing" ones, playing games against a reference engine,...
|
||||
|
||||
## Submitting
|
||||
|
||||
This is an **individual assignment**!
|
||||
A private Git repository will be created for you on the KU Leuven GitLab servers which you can use for development and will be used for submitting your project.
|
||||
Do not share this repository with anyone.
|
||||
|
||||
|
||||
**Submitting**:
|
||||
- Deadline: **Friday December 23, 2022 at 23:59**;
|
||||
- How: push your solution to the `main` branch of your private repository.
|
||||
|
||||
> :bulb: At the time of the deadline, your repository will be archived.
|
||||
> This means it's still available but read-only.
|
||||
|
||||
**Important guidelines**:
|
||||
- Whatever is on the `main` branch of your private repository at the time of the deadline is considered your solution, nothing else.
|
||||
Make sure *all* your code is on the *correct branch* in the *correct repository*;
|
||||
- Your **code must work on our test infrastructure**.
|
||||
If it does not, we **cannot grade it**.
|
||||
When you push to your private repository, the tests are [automatically run](#automatic-test-runs) on our infrastructure.
|
||||
Do this regularly;
|
||||
- Do not change how tests are run, do not remove any tests, and do not change the compiler options related to warnings in [CMakeLists.txt](CMakeLists.txt).
|
||||
If you do, the automatic test runs will not be representative anymore since we will restore everything when running our evaluation tests;
|
||||
- Do **not share your code** with anyone and do **not include any code not written by you** in your repository (except, of course, the initial code we provided).
|
||||
|
||||
## Allowed language features
|
||||
|
||||
You are allowed to use all features of [C++20][c++20] including everything in the standard library as long as the compiler on our test infrastructure supports it.
|
||||
However, we recommend sticking with [C++17][c++17] as this is what was used during the lecture and exercise sessions.
|
||||
See [this][c++ features] for an overview of all (library) features and in which C++ version they are available.
|
||||
|
||||
> :bulb: The compiler used on our test infrastructure is GCC 11.3.0 which [supports][gcc c++ status] (nearly) all C++20 features.
|
||||
|
||||
> :bulb: To use C++20, you will have to update the `CMAKE_CXX_STANDARD` variable in [CMakeLists.txt](CMakeLists.txt).
|
||||
|
||||
You are **not allowed to use *any* external libraries**.
|
||||
This includes header-only libraries.
|
||||
|
||||
> :warning: Including *any* source files not fully written by you in your repository will be considered plagiarism!
|
||||
|
||||
## Questions and issues
|
||||
|
||||
All code contains bugs, so I expect that some will be found in the code provided for this assignment.
|
||||
If you happen to find one, please [report][questions] it and I will try to fix it as soon as possible.
|
||||
I will then publish the fix on the [assignment repository][assignment repo] from which it can be merged into your private repository.
|
||||
|
||||
> :bulb: To make sure merging is as painless as possible, try not to commit any unnecessary changes to the code, especially in the provided implementation files like [Fen.cpp](Fen.cpp) and [Uci.cpp](Uci.cpp).
|
||||
> Always check what you are about to commit using, for example, `git diff --staged`.
|
||||
|
||||
> :bulb: The easiest way to merge changes into your private repository is to use Git:
|
||||
> ```
|
||||
> git remote add assignment https://gitlab.kuleuven.be/distrinet/education/cpl/cplusplus-project-assignment.git
|
||||
> git fetch assignment
|
||||
> git merge assignment/main
|
||||
> ```
|
||||
> The first command is only needed the first time you merge.
|
||||
|
||||
|
||||
The same issue tracker can be used to **ask questions** about the project.
|
||||
Just open [open an issue][questions] and I will try to answer as soon as possible.
|
||||
|
||||
# Setup
|
||||
|
||||
The basic setup for the project is the same as for the [exercise sessions][exercises setup].
|
||||
|
||||
From now on, we use the following variables in commands:
|
||||
- `$REPO_URL`: the Git URL of your private repository;
|
||||
- `$REPO_DIR`: the directory where you will clone your repository;
|
||||
- `$BUILD_DIR`: the directory where you will build the project.
|
||||
|
||||
[Catch2][catch2] is used for unit testing and its source code is included as a [submodule][git submodules].
|
||||
Therefore, we have to clone the project repository recursively:
|
||||
|
||||
```
|
||||
$ git clone --recurse-submodules $REPO_URL $REPO_DIR
|
||||
```
|
||||
|
||||
If you did a normal clone, don't worry, this can be fixed:
|
||||
```
|
||||
$ cd $REPO_DIR
|
||||
$ git submodule update --init
|
||||
```
|
||||
|
||||
Now the project can be built in the usual way:
|
||||
|
||||
```
|
||||
$ mkdir $BUILD_DIR
|
||||
$ cd $BUILD_DIR
|
||||
$ cmake $REPO_DIR
|
||||
$ cmake --build .
|
||||
```
|
||||
|
||||
By default, CMake will perform a *debug build*.
|
||||
This means, among others, that optimizations are disabled.
|
||||
Since performance is important for a chess engine, you might want perform a *release build*.
|
||||
This can be done by replacing the third command above by the following:
|
||||
```
|
||||
$ cmake -DCMAKE_BUILD_TYPE=Release $REPO_DIR
|
||||
```
|
||||
|
||||
> :bulb: You can have multiple build directories, for example, one with a debug build and one with a release build.
|
||||
> This is useful since debug builds are preferred if you ever need to use a debugger.
|
||||
> Debugging release builds is difficult since they do not contain debugging symbols (so you cannot put breakpoints on functions, for example) and are highly optimized.
|
||||
|
||||
> :bulb: The above procedure for creating a debug build does not work on Visual Studio since it creates multiple builds in the same directory.
|
||||
> Therefore, a release build can be created as follows (replaces the fourth command above):
|
||||
> ```
|
||||
> $ cmake --build . --config Release
|
||||
> ```
|
||||
|
||||
If all went well, there should now be an executable called `cplchess` for the engine, and one called `Tests/tests` for the unit tests.
|
||||
Try to execute the latter to verify your build worked.
|
||||
The tests will obviously fail but it should not crash.
|
||||
|
||||
If you want to add extra `.cpp` files to your project (which you will have to), you should add them to the `cplchess_lib` library in [CMakeLists.txt](CMakeLists.txt).
|
||||
This library is linked into both the engine and the unit tests executables.
|
||||
If you want to add extra unit test files, add them to the `tests` executable in [Tests/CMakeLists.txt](Tests/CMakeLists.txt)
|
||||
|
||||
> :bulb: As explained [earlier](#submitting), you are not allowed to change the compiler options related to warnings in [CMakeLists.txt](CMakeLists.txt).
|
||||
> We have enabled many [compiler warnings][gcc warnings] in the build and made sure that warnings are treated as errors.
|
||||
> This forces you to write warning-free code to help you discover many mistakes early.
|
||||
|
||||
# Problem description
|
||||
|
||||
This section describes the parts you have to implement in your chess engine.
|
||||
If you are not familiar with chess, it might be worthwhile to learn its [rules][chess rules] first.
|
||||
|
||||
Although it is entirely up to you in which order to implement the parts, following the order in this section is probably a good idea.
|
||||
The unit tests allow you to [select certain tests](#test-tags) to focus on specific parts.
|
||||
This also allows you to postpone the implementation of more difficult move types (i.e., [castling][castling], [promotion][promotion], and [en passant][en passant]).
|
||||
It could be interesting, for example, to test the full search algorithm before adding those moves.
|
||||
|
||||
> :bulb: Most code you have to implement can be added to existing source files.
|
||||
> We have provided skeleton code to ensure that all tests compile and that you adhere to the needed API.
|
||||
> All method bodies are empty but to prevent compiler warnings/errors we
|
||||
> - return some arbitrary value in non-`void` methods;
|
||||
> - pretend to use arguments by casting them to `void` (e.g., `(void)arg`).
|
||||
|
||||
## Fundamental data structures
|
||||
|
||||
> :bulb: Relevant unit test [tag](#test-tags): `[Fundamental]`
|
||||
|
||||
We start with describing the fundamental data structures used to represent the board state and moves.
|
||||
All these data structures need to follow the given API but how they are implemented is up to you.
|
||||
|
||||
### Piece
|
||||
|
||||
Pieces are the main tools at the disposal of the chess players.
|
||||
Although the different kind of pieces vary in behavior, we represent them simply by their basic properties: *color* (black or white) and *type* (pawn, knight, etc).
|
||||
Their behavior (i.e., how they move) is [handled elsewhere](#move-generation).
|
||||
|
||||
Piece representation is implemented in [Piece.hpp](Piece.hpp) and [Piece.cpp](Piece.cpp).
|
||||
Three types are defined here:
|
||||
- `PieceColor` (`enum`);
|
||||
- `PieceType` (`enum`);
|
||||
- `Piece` (`class` that stores a color and a type).
|
||||
|
||||
Besides their representation, the `Piece` class also handles conversion to and from standard piece symbols: `Piece::fromSymbol()` takes a `char` and converts it to a `Piece` (if possible) and streaming to `std::ostream` can be used for the opposite.
|
||||
The following symbols are used for chess pieces: `P` (pawn), `N` (knight), `B` (bishop), `R` (rook), `Q` (queen), and `K` (king). Uppercase letters are used for white, lowercase for black.
|
||||
|
||||
> :bulb: Throughout this project, [`std::optional`][std optional] is used to represent optional values.
|
||||
> For example, creation methods like `Piece::fromSymbol()` return an optional if they may fail.
|
||||
> Many classes that are often used as optional values define a type alias called `Optional` for brevity.
|
||||
|
||||
### Square
|
||||
|
||||
Chessboards consist of a grid of 8x8 *squares*.
|
||||
In [*algebraic notation*][san], each square is identified by a coordinate pair from white's point of view.
|
||||
Columns (called *files*) are labeled *a* through *h* while rows (called *ranks*) are labeled *1* through *8*.
|
||||
So, the lower-left square is called *a1* while the upper-right square is called *h8*.
|
||||
|
||||
The `Square` class (implemented in [Square.hpp](Square.hpp) and [Square.cpp](Square.cpp)) is used to identify squares.
|
||||
It offers two ways of identifying squares:
|
||||
- Coordinates: create using `fromCoordinates()` and get using `file()` and `rank()`.
|
||||
Note that both coordinates are numbers in the range `[0, 8)`.
|
||||
- Index: create using `fromIndex()` and get using `index()`.
|
||||
In this representation, all squares have a unique index in the range `[0, 64)` where the index of *a1* is *0*, *b1* is *1*, and that of *h8* is *63*.
|
||||
|
||||
Squares also offer conversion from (`fromName`) and to (streaming to `std::ostream`) their name.
|
||||
|
||||
Two comparison operator are needed:
|
||||
- `operator==` for comparing squares in the tests and elsewhere;
|
||||
- `operator<` for using squares as keys in associative (sorted) containers like [`std::map`][std_map].
|
||||
You can use any total ordering.
|
||||
|
||||
Constants are provided to conveniently references all squares (e.g., `Square::E4`).
|
||||
These are implemented using a private constructor that takes an index as argument.
|
||||
You don't have to keep this constructor, but if you don't, you will have to update the initialization of the constants.
|
||||
|
||||
### Move
|
||||
|
||||
Moves are how a game of chess progresses from turn to turn.
|
||||
Although the concept of a move potentially contains a lot of information (e.g., which piece is moved, if the move is valid, etc.), the `Move` class (implemented in [Move.hpp](Move.hpp) and [Move.cpp](Move.cpp)) only uses the bare minimum: the *from* and *to* squares and optionally the [promoted][promotion]-to piece type.
|
||||
Its constructor takes all three items (although the promotion type is optional and by default a move is not a promotion).
|
||||
|
||||
As a textual representation, the `Move` class uses the [UCI][uci] notation (also sometimes called the [*long algebraic notation*][long algebraic notation]).
|
||||
In this notation, moves are represented by the concatenation of the names of the from and to squares, optionally followed by the lower case symbol of the promotion piece type.
|
||||
[*Castling*][castling] is considered a king move so is represented by the from and to squares of the king (e.g., white kingside is castling is *e1g1*).
|
||||
Moves can be created from this representation using `fromUci` and the conversion to UCI is done by streaming to `std::ostream`.
|
||||
|
||||
Two comparison operator are needed:
|
||||
- `operator==` for comparing moves in the tests and elsewhere;
|
||||
- `operator<` for using moves as keys in associative (sorted) containers like [`std::map`][std_map] and [`std::set`][std_set].
|
||||
You can use any total ordering.
|
||||
|
||||
> :warning: If you don't correctly implement a total ordering on `Move`s with `operator<`, you might run into strange behavior.
|
||||
> For example, the [unit tests](#unit-tests) for pseudo-legal move generation store moves in a `std::set` and compare sets of expected- and generated moves.
|
||||
> If you ever notice that tests pass or fail depending on the order in which you generate moves, it's probably because `operator<` does not correctly implement a total ordering.
|
||||
|
||||
Note that this simple representation allows the creation of illegal moves.
|
||||
Indeed, since the `Move` class has no relation with a `Board`, it's impossible to validate the legality of moves in isolation.
|
||||
We will see how to handle illegal moves [later](#move-making).
|
||||
|
||||
> :bulb: There are some things that *can* be checked about a move's validity.
|
||||
> For example, whether a promotion is certainly invalid (e.g., *a7a8k* or *e6e8q* are *never* valid).
|
||||
> You are free to check this in `fromUci()` but this is not necessary, especially since the constructor has no way of reporting errors so creating such invalid moves is always possible.
|
||||
|
||||
### Board
|
||||
|
||||
Arguably the most important decision for chess engines is how the [board state is represented][board representation wp] as it will have a large impact on how [*move generation*](#move-generation) will be implemented and thus how efficient this will be.
|
||||
The most popular among high-rated chess engines is probably a [bitboard][bitboard] representation (e.g., [Stockfish][stockfish] uses this).
|
||||
Although very efficient for move generation, this representation can be difficult to grasp and work with.
|
||||
Square-centric, array-based representations (e.g., [8x8 boards][8x8 board]) might be easier to work with at the cost of less efficient move generation.
|
||||
|
||||
You are entirely free to choose the way the `Board` class represents its state as long as you can implement its interface:
|
||||
- Getting and setting pieces: `piece(Square)`, `setPiece(Square, Piece)`;
|
||||
- Getting and setting the turn: `turn()`, `setTurn(PieceColor)`;
|
||||
- Getting and setting the [castling rights](#castling-rights): `castlingRights()`, `setCastlingRights(CastlingRights)`;
|
||||
- Getting and setting the [en passant square](#en-passant-square): `enPassantSquare()`, `setEnPassantSquare(Square)`;
|
||||
- Default construction (`Board()`): create an _empty_ board.
|
||||
|
||||
You should also implement streaming a `Board` to `std::ostream` for debugging purposes.
|
||||
You are free to choose how a board is printed.
|
||||
I like the following format:
|
||||
|
||||
```
|
||||
r n b q k b n r
|
||||
p p p p p p p p
|
||||
. . . . . . . .
|
||||
. . . . . . . .
|
||||
. . . . . . . .
|
||||
. . . . . . . .
|
||||
P P P P P P P P
|
||||
R N B Q K B N R
|
||||
```
|
||||
|
||||
#### Castling rights
|
||||
|
||||
[*Castling*][castling] is probably the most complex move type in chess and requires extra state to be stored in `Board` (i.e., knowing which castling moves are valid does not depend only on the state of the pieces).
|
||||
For example, once a rook has moved, its king permanently loses the right to castle on that side of the board, even when the rook returns to its original square.
|
||||
|
||||
To represent *castling rights*, the `CastlingRights` enumeration is provided (see [CastlingRights.hpp](CastlingRights.hpp) and [CastlingRights.cpp](CastlingRights.cpp)).
|
||||
Overloads are provided for many bitwise operations so that this type can be used as [bit flags][bit flags].
|
||||
For example:
|
||||
|
||||
```c++
|
||||
CastlingRights rights = ...;
|
||||
|
||||
if ((rights & CastlingRights::BlackQueenside) != CastlingRights::None) {
|
||||
// Queenside castling is available for black
|
||||
}
|
||||
|
||||
// Add kingside castling rights for white
|
||||
rights |= CastlingRights::WhiteKingside;
|
||||
|
||||
// Remove all castling rights for black
|
||||
rights &= ~CastlingRights::Black;
|
||||
```
|
||||
|
||||
#### En passant square
|
||||
|
||||
[*En passant*][en passant], probably the least-known move type in chess, also requires extra state to be stored since it is only allowed for one move after a pawn made a two-square move.
|
||||
Therefore, to implement pawn captures correctly, `Board` should store the square to which an en passant capture can be made.
|
||||
|
||||
## Move generation
|
||||
|
||||
> :bulb: Relevant unit test [tag](#test-tags): `[MoveGen]`
|
||||
|
||||
[*Move generation*][move generation] is the process of generating valid moves from a board state.
|
||||
These moves will be used for [searching](#searching).
|
||||
|
||||
There are generally two ways of generating moves: legal or pseudo-legal.
|
||||
|
||||
### Legal move generation
|
||||
|
||||
Only moves that are [legal][legal move] according to the [rules of chess][chess rules] are generated.
|
||||
This is convenient for the search algorithm but might not be that easy to implement efficiently.
|
||||
The main issue is verifying that the king is not left in [check][chess rules check] after a move (which is illegal).
|
||||
The naive way of doing this might be to generate the opponent's moves and verifying that none of them captures the king.
|
||||
While this might work, it would result in doubling the amount of generated moves.
|
||||
|
||||
### Pseudo-legal move generation
|
||||
|
||||
A [*pseudo-legal move*][pseudo legal move] is one that adheres to all the rules of chess *except* that it might leave the king in check.
|
||||
From the description above it should be clear that this is easier to generate but might complicate the search algorithm.
|
||||
Indeed, we now have to make sure that we somehow reject illegal moves while searching.
|
||||
One way to tackle this could be to search until a move that captures the king and then reject the previous move.
|
||||
|
||||
> :bulb: Due to the complex rules of [castling][castling], a pseudo-legal castling move is usually also legal.
|
||||
|
||||
### Interface
|
||||
|
||||
Move generation should be implemented by the `Board` class through the following method:
|
||||
|
||||
```c++
|
||||
using MoveVec = std::vector<Move>;
|
||||
|
||||
void Board::pseudoLegalMoves(MoveVec& moves) const;
|
||||
```
|
||||
|
||||
This method should add all (pseudo) legal moves for the current player to the `moves` vector.
|
||||
|
||||
For [testing](#unit-tests) purposes, the following method should also be implemented to generate all moves from a specific square:
|
||||
|
||||
```c++
|
||||
void Board::pseudoLegalMovesFrom(const Square& from, MoveVec& moves) const;
|
||||
```
|
||||
|
||||
> :bulb: You are free to use an entirely different interface to communicate between the search algorithm and `Board` as long as you can still implement this one for testing.
|
||||
|
||||
> :bulb: Even though "pseudo-legal" is part of the method names, you are free to generate legal moves here.
|
||||
> The tests will only verify the correct generation of pseudo-legal moves, though.
|
||||
|
||||
## Move making
|
||||
|
||||
> :bulb: Relevant unit test [tag](#test-tags): `[MoveMaking]`
|
||||
|
||||
[*Move making*][move making] is the process of updating the board state to reflect a move.
|
||||
Obviously, this should include moving the moved piece from the source square to the destination square (possibly replacing a captured piece) but it may also include updating [castling rights](#castling-rights) and the [en passant square](#en-passant-square).
|
||||
|
||||
Move making is used by the search algorithm to generate child nodes after move generation.
|
||||
|
||||
It should be implemented in the `Board` class by the following method:
|
||||
|
||||
```c++
|
||||
void Board::makeMove(const Move& move);
|
||||
```
|
||||
|
||||
> :bulb: Since this method is only used by your search algorithm, it is probably not necessary to verify that the given move is legal in this method.
|
||||
> You just have to make sure that *if* a legal move is given, it is correctly executed.
|
||||
|
||||
## Searching
|
||||
|
||||
> :bulb: Relevant tests: [puzzles](#puzzles).
|
||||
|
||||
The basic question a chess engine tries to answer is what the best move to play is at a particular position.
|
||||
To answer this, a [search tree][game tree] is typically constructed with the initial position in the root node and the children at every level being positions resulting from valid moves from their parents.
|
||||
|
||||
Given this tree, a search algorithm tries to find a path that *guarantees* the player will end-up in *the most favorable* position.
|
||||
There are two words to discuss a bit here:
|
||||
- *Guaranteed* most favorable position: what we mean here is that we must assume the opponent always makes the best possible move for them.
|
||||
That is, we assume the *worst case scenario* and try to minimize our loss (or maximize our win);
|
||||
- *Favorable*: we try to win the chess game but most of the time it will not feasible to search the whole tree to find a [checkmate][checkmate].
|
||||
Therefore, we have to limit the search depth and use a [heuristic][eval] to evaluate positions that are not the [end of the game][chess rules game end].
|
||||
|
||||
[*Minimax*][minimax] is a well-known search algorithm that guarantees finding the move that ends-up in the most favorable position.
|
||||
You are free to use any correct algorithm but minimax is definitely a good choice.
|
||||
|
||||
> :bulb: Here are some tips for using minimax:
|
||||
> - Since chess is a [zero-sum game][zero-sum game], the slightly simpler [negamax][negamax] variant can be used;
|
||||
> - It is highly recommended to implement [*alpha-beta pruning*][alpha-beta pruning] to improve search performance.
|
||||
> Without it, it is probably impossible to solve most puzzles within the time limit;
|
||||
> - When using alpha-beta pruning, [*move ordering*][move ordering] becomes important: if potentially good moves are searched first, it may lead to earlier pruning reducing the size of the search tree;
|
||||
> - If you want to take time into account (see below), you might want to use [*iterative deepening*][iterative deepening] to make sure you have a reasonable move available quickly while continuing the search at higher depths to find better moves.
|
||||
|
||||
[Evaluating][eval] chess positions is very complex.
|
||||
The most naive way is to simply calculate the [value][piece value] of all remaining pieces.
|
||||
However, this is clearly not optimal so more advanced strategies will take other aspects into account (e.g., [center control][center control], [king safety][king safety], etc).
|
||||
|
||||
> :bulb: It is up to you to decide how far you want to go with improving the heuristic.
|
||||
> However, it should at least be good enough to solve the provided [puzzles](#puzzles).
|
||||
|
||||
Remember that if you use [pseudo-legal move generation](#pseudo-legal-move-generation), the search algorithm needs to somehow reject illegal moves.
|
||||
And unless you evaluation function can detect [checkmate][checkmate] and [stalemate][stalemate], it also needs to be able to handle [end of the game][chess rules game end] conditions.
|
||||
|
||||
### Principal variation
|
||||
|
||||
The result of a search is a [*principal variation*][pv] (PV) which is the sequence of moves the engine considers best.
|
||||
A PV is represented by the `PrincipalVariation` class (in [PrincipalVariation.hpp](PrincipalVariation.hpp) and [PrincipalVariation.cpp](PrincipalVariation.cpp)).
|
||||
The main interface that has to be implemented is the following:
|
||||
|
||||
```c++
|
||||
using MoveIter = /* your iterator type here */;
|
||||
|
||||
std::size_t PrincipalVariation::length() const;
|
||||
MoveIter PrincipalVariation::begin() const;
|
||||
MoveIter PrincipalVariation::end() const;
|
||||
```
|
||||
|
||||
Where `length()` returns the number of [plies][ply] in the PV and `begin()` and `end()` allows for iterating over the `Move`s.
|
||||
|
||||
> :bulb: You are free to choose any container to store the `Move`s.
|
||||
> Adjust `MoveIter` accordingly.
|
||||
|
||||
The `PrincipalVariation` class also stores information about the evaluation [score][eval score]:
|
||||
|
||||
```c++
|
||||
bool PrincipalVariation::isMate() const;
|
||||
int PrincipalVariation::score() const;
|
||||
```
|
||||
|
||||
`isMate()` should return `true` if the PV ends in checkmate.
|
||||
In this case, `score()` should return the number of plies that leads to the checkmate.
|
||||
Otherwise, `score()` returns the evaluation of the position the PV leads to.
|
||||
In both cases, the score is from the point of view of the engine: positive values are advantageous for the engine, negative ones for its opponent.
|
||||
|
||||
> :bulb: Your are free to choose the unit of the evaluation score as long as larger scores mean better evaluations.
|
||||
> Typically, [*centipawns*][centipawns] are used where 100 centipawns corresponds to the value of one pawn.
|
||||
|
||||
There is also an overload declared to stream `PrincipalVariation` to a `std::ostream`.
|
||||
You can choose how PVs are printed.
|
||||
|
||||
### Engine
|
||||
|
||||
The main interface to the search algorithm is provided by the abstract class `Engine` (see [Engine.hpp](Engine.hpp)).
|
||||
This class functions as an interface so you have to add you own derived class to implement this interface.
|
||||
You should then adapt `EngineFactory::createEngine()` (in [EngineFactory.cpp](EngineFactory.cpp)) to return an instance of your class.
|
||||
|
||||
The most important method in the `Engine` interface is the following:
|
||||
|
||||
```c++
|
||||
virtual PrincipalVariation Engine::pv(
|
||||
const Board& board,
|
||||
const TimeInfo::Optional& timeInfo = std::nullopt
|
||||
) = 0;
|
||||
```
|
||||
|
||||
This should calculate and return the PV starting from the position represented by `board`.
|
||||
|
||||
> :bulb: The second argument, `timeInfo`, provides timing information (see [TimeInfo.hpp](TimeInfo.hpp)) if it is available.
|
||||
> Most chess games are played with a [time control][time control] and for those games, `timeInfo` contains the time left on the clock of both players as well as their increment.
|
||||
> It is *completely optional* to use this information but will be especially useful if you choose to use [iterative deepening][iterative deepening].
|
||||
|
||||
The following method is called whenever a new game starts (not guaranteed to be called for the first game played by an `Engine` instance):
|
||||
|
||||
```c++
|
||||
virtual void Engine::newGame() = 0;
|
||||
```
|
||||
|
||||
> :bulb: You probably don't need this method but it could be useful if you store state inside your engine.
|
||||
|
||||
The following methods are used to identify your engine over the [UCI interface](#uci):
|
||||
|
||||
```c++
|
||||
virtual std::string Engine::name() const = 0;
|
||||
virtual std::string Engine::version() const = 0;
|
||||
virtual std::string Engine::author() const = 0;
|
||||
```
|
||||
|
||||
Note that the value of `name()` will be used to identify your engine during the
|
||||
tournament so make sure to chose something appropriate!
|
||||
|
||||
Lastly, most chess engines use [_transposition tables_][transpositiion table] as
|
||||
an optimization technique. While it is not necessary to implement this technique
|
||||
for this project, if you do chose to implement it, you _have to_ respect the
|
||||
maximum table size specified over UCI. In order to do this, you will have to
|
||||
override the following methods:
|
||||
|
||||
```c++
|
||||
virtual std::optional<HashInfo> hashInfo() const;
|
||||
virtual void setHashSize(std::size_t size);
|
||||
```
|
||||
|
||||
# Testing
|
||||
|
||||
A number of tests are provided in the [Tests](Tests/) directory that you can run locally and are also run automatically when pushing commits to GitLab.
|
||||
You are free (even encouraged) to add more tests but **do not modify any of the existing test files**.
|
||||
They will be overwritten when we run our automatic tests after the deadline.
|
||||
Also remember that the tests do not try to cover all edge cases and we will run more tests when grading the projects.
|
||||
|
||||
Unit tests are provided to verify the correctness of the [board representation](#fundamental-data-structures) and [pseudo-legal move generation](#move-generation).
|
||||
To test the search algorithm, chess puzzles are used.
|
||||
|
||||
> :bulb: It is possible to pass all the tests with a relatively shallow search depth (about 5 [plies](ply)) and a relatively simple evaluation function.
|
||||
> The most naive search, however, will probably not suffice to finish most puzzles within the time limit.
|
||||
|
||||
## Automatic test runs
|
||||
|
||||
Whenever you push commits to your private repository, a [CI/CD pipeline][gitlab pipelines] will start automatically that builds and tests your code.
|
||||
The pipelines can be viewed by going to "CI/CD -> Pipelines" in the web interface of your repository.
|
||||
|
||||
When clicking on a pipeline, you can see it consist of two stages and three jobs:
|
||||
- Build stage: runs the "build" job that compiles your code and produces the engine and unit tests executables for the next stage;
|
||||
- Test stage: runs the "unit_tests" and "puzzle_tests" jobs that perform all automatic tests.
|
||||
|
||||
> :bulb: If the build stage fails, the test stage will not run.
|
||||
> Both jobs in the test stage are independent and run in parallel, though.
|
||||
|
||||
You can click on a specific job to view its output which can be useful when debugging failures (e.g., to see compiler errors).
|
||||
|
||||
The "Tests" tab contains detailed information about the result of the test runs.
|
||||
If you want to know why a test failed, click on the job name in the "Tests" tab first (e.g., "unit_tests") and then on "View details" next to the failed test.
|
||||
|
||||
> :bulb: For those who are interested, the pipeline is defined in [.gitlab-ci.yml](.gitlab-ci.yml).
|
||||
> **Do not modify this file, though!**
|
||||
|
||||
## Unit tests
|
||||
|
||||
Unit tests are implemented using the [Catch2][catch2] framework.
|
||||
After building the project, a test executable is available at `$BUILD_DIR/Tests/tests`.
|
||||
Running this executable without any arguments runs all tests and looks something like this when everything passes:
|
||||
|
||||
```
|
||||
$ $BUILD_DIR/Tests/tests
|
||||
===============================================================================
|
||||
All tests passed (1698 assertions in 95 test cases)
|
||||
```
|
||||
|
||||
### Test tags
|
||||
|
||||
Since initially (almost) all tests will fail, test cases are tagged to be able to only run a subset of tests.
|
||||
The following tags are provided:
|
||||
- `[Fundamental]`: tests for functionality that is fundamental.
|
||||
For example, for classes or methods used by the tests themselves.
|
||||
This includes the `Piece`, `Square`, and `Move` classes as well as the state of the `Board` class.
|
||||
**This functionality should be the first thing to implement!**
|
||||
- `[ClassName`]: each class with unit tests has a corresponding tag (e.g., `[Board]`) that runs all tests of that class;
|
||||
- `[MoveGen]`: all tests for pseudo-legal move generation;
|
||||
- `[MoveMaking]`: all tests for move making;
|
||||
- `[Castling]`, `[Promotion]`, `[EnPassant]`: all tests for these "special" moves are tagged accordingly.
|
||||
This allows you to implement and test "normal" moves first;
|
||||
- `[PieceType]`: tests for a specific piece type (e.g., `[Queen]`) are tagged accordingly.
|
||||
This is mostly related to `[MoveGen]` tests.
|
||||
|
||||
Tags can be selected by providing them as an argument to the test executable.
|
||||
If one is preceded by a `~`, it is deselected.
|
||||
For example, to test the move generation for pawns except en passant:
|
||||
|
||||
```
|
||||
$ $BUILD_DIR/Tests/tests '[MoveGen][Pawn]~[EnPassant]'
|
||||
Filters: [MoveGen][Pawn]~[EnPassant]
|
||||
===============================================================================
|
||||
All tests passed (65 assertions in 11 test cases)
|
||||
```
|
||||
|
||||
See [this][catch2 filter tags] for more information.
|
||||
|
||||
## Puzzles
|
||||
|
||||
Puzzles are chess problems that have a clear, short, and unique solution.
|
||||
They are often used to train a chess player's [tactical][chess tactic] awareness.
|
||||
Simple puzzles typically present a position where a checkmate can be forced or a large material advantage can be gained.
|
||||
For more advanced puzzles, one might need to find moves that gain a [positional advantage][positional advantage].
|
||||
|
||||
We have selected a number of easy puzzles from the freely available [puzzle database][lichess puzzle db] of [Lichess][lichess].
|
||||
They can be found in [Tests/Puzzles/](Tests/Puzzles/) and contain three major categories:
|
||||
- `mateIn1`: checkmate can be forced in one move;
|
||||
- `mateIn2`: checkmate can be forced in two moves;
|
||||
- `crushing`: a significant material advantage can be gained.
|
||||
|
||||
For each category, three variants are provided:
|
||||
- `simple`: puzzles do not involve castling or en passant moves;
|
||||
- `castling`: all puzzles involve castling moves;
|
||||
- `enPassant`: all puzzles involve en passant moves;
|
||||
|
||||
The puzzle files are named according to the category and variant.
|
||||
For example, the simple mate-in-one puzzles can be found in [Tests/Puzzles/mateIn1_simple.csv](Tests/Puzzles/mateIn1_simple.csv).
|
||||
|
||||
### Running puzzles
|
||||
|
||||
> :bulb: To run the puzzles locally, you need Python 3 (at least 3.7) and the `chess` and `junit-xml` packages which can be installed through `pip`:
|
||||
> ```
|
||||
> pip3 install chess junit-xml
|
||||
> ```
|
||||
|
||||
A Python script is provided ([Tests/puzzlerunner.py](Tests/puzzlerunner.py)) to run puzzles through an engine and verify its solution. It uses the [UCI interface](#uci) to communicate with the engine.
|
||||
The script can be used as follows:
|
||||
```
|
||||
$ ./puzzlerunner.py --engine /path/to/engine/executable [csv files...]
|
||||
```
|
||||
|
||||
For example, to run the simple mate-in-one puzzles using your engine:
|
||||
|
||||
```
|
||||
$ ./puzzlerunner.py --engine $BUILD_DIR/cplchess Puzzles/mateIn1_simple.csv
|
||||
=== Running puzzles from /.../Tests/Puzzles/mateIn1_simple.csv ===
|
||||
Running puzzle jrJls ... OK (1.645s)
|
||||
Running puzzle rZoKr ... OK (0.834s)
|
||||
[snip]
|
||||
Running puzzle BEUkI ... OK (0.435s)
|
||||
Running puzzle EAnMf ... OK (0.027s)
|
||||
Total time: 25.294s
|
||||
All tests passed
|
||||
```
|
||||
|
||||
> :bulb: The timing information shown is not very accurate since it's measured in wall-clock time.
|
||||
> Because running a puzzle involves spawning a new process (the engine) and inter-process communication between the Python script and this process, the measured time will depend on the current load of the system.
|
||||
|
||||
When a puzzle fails because the engine returned a wrong move, some information is shown to help debug the issue.
|
||||
For example:
|
||||
|
||||
```
|
||||
Running puzzle 93ERa ... FAIL (5.034s)
|
||||
===
|
||||
Failure reason: unexpected move
|
||||
URL: https://lichess.org/training/93ERa
|
||||
position=4rqk1/5rp1/7R/4B3/4P1Q1/6PK/PP5P/2n5 w - - 1 39
|
||||
move=e5d6
|
||||
expected move=g4g6
|
||||
===
|
||||
```
|
||||
|
||||
It shows a link to the puzzle on Lichess, the position from which the wrong move was generated (in [FEN](#fen) notation), and the generated and expected moves.
|
||||
|
||||
### Generating puzzles
|
||||
|
||||
The full [Lichess puzzle database][lichess puzzle db] currently contains about three million puzzles.
|
||||
We have selected only a few to be included in the automatic tests but you are encouraged to use more puzzles to test your engine.
|
||||
To help you with this, we provide a script ([Tests/puzzledb.py](Tests/puzzledb.py)) that can parse and filter the database.
|
||||
|
||||
Puzzles can be filtered on a number of criteria, including their [tags][lichess puzzle themes], rating, whether they include castling or en passant moves, etc.
|
||||
For a full list of options, run `./puzzledb.py --help`.
|
||||
|
||||
As an example, the simple mate-in-one puzzles were generated like this:
|
||||
|
||||
```
|
||||
$ ./puzzledb.py --tag=mateIn1 --castling=no --en-passant=no lichess_db_puzzle.csv | sort -R | head -n 20
|
||||
```
|
||||
|
||||
> :bulb: `sort -R` randomly shuffles all matches and `head -n 20` selects the first 20.
|
||||
> If you run this locally, you will (most likely) get different puzzles than the ones in [Tests/Puzzles/mateIn1_simple.csv](Tests/Puzzles/mateIn1_simple.csv).
|
||||
> Also note that this is Bash syntax, the full command will not work in other (incompatible) shells but the invocation of `puzzledb.py` itself will.
|
||||
|
||||
> :bulb: Note that the options `--tag` and `--not-tag` can be given multiple times and it will filter on puzzles that (don't) have *all* given tags.
|
||||
|
||||
> :bulb: [Tests/puzzledb.py](Tests/puzzledb.py) is not used by our testing infrastructure so feel free to modify it to add more filter options.
|
||||
|
||||
### Puzzle rating
|
||||
|
||||
We also provide a script to determine your engine's puzzle rating. All puzzles
|
||||
in the Lichess puzzle database have a rating and we can estimate an engine's
|
||||
rating by making it "play against" a large number of puzzles and using
|
||||
[Glicko 2][glicko] to update its rating.
|
||||
|
||||
> :bulb: You need to install the `glicko2` Python package to run the script:
|
||||
> ```
|
||||
> pip3 install glicko2
|
||||
> ```
|
||||
|
||||
You can run the script as follows:
|
||||
|
||||
```
|
||||
$ ./puzzlerating.py --engine $BUILD_DIR/cplchess --puzzle-db lichess_db_puzzle.csv
|
||||
```
|
||||
|
||||
Note that the script takes quite some time to start since it parses the full
|
||||
database.
|
||||
|
||||
# Tools
|
||||
|
||||
Here we list some freely available tools that you can use while developing your chess engine.
|
||||
|
||||
## FEN
|
||||
|
||||
[*Forsyth–Edwards Notation*][fen] (FEN) is a standard notation for describing chess positions.
|
||||
It contains information about piece placement, current turn, castling rights, en passant square, and number of moves made.
|
||||
|
||||
A FEN parser is provided (see [Fen.hpp](Fen.hpp) and [Fen.cpp](Fen.cpp)) that converts a FEN string to a `Board`.
|
||||
|
||||
> :warning: The FEN parser will not work properly until all `[Fundamental]` [unit tests](#unit-tests) pass.
|
||||
|
||||
> :bulb: The FEN parser currently does not support parsing the move number information (nor does `Board` store it) because you don't need it for a simple engine.
|
||||
> [Some extensions](#draw-conditions) might need it, though, so if you want to implement those, you'll have to extend the FEN parser.
|
||||
|
||||
When running the engine executable with a FEN string as argument, the position is evaluated and the PV printed.
|
||||
For example:
|
||||
|
||||
```
|
||||
$ $BUILD_DIR/cplchess "2kr4/ppp2Q2/3Pp3/1q2P3/n1r4p/5P2/6PB/R1R3K1 b - - 0 29"
|
||||
PV: +600 [b5c5 g1h1 c4c1 a1c1 c5c1]
|
||||
```
|
||||
|
||||
> :bulb: The FEN string should be passed *as a single argument* so you have to put it in quotes.
|
||||
|
||||
> :bulb: Since you are free to choose [how a PV is printed](#principal-variation), the output may look different.
|
||||
|
||||
While developing your engine, you will most likely want to test it on specific positions.
|
||||
To manually create positions and convert them to FEN, the Lichess [board editor][lichess board editor] is a very handy tool.
|
||||
All unit tests that start from a position (which is most of them) contain a link to the board editor with that specific position in a comment.
|
||||
|
||||
> :bulb: The en passant square cannot be set from the editor so you'll have to fill that in by hand if you need it.
|
||||
|
||||
## UCI
|
||||
|
||||
The [*Universal Chess Protocol*][uci] is a protocol that is mainly used to communicate between chess engines and user interfaces.
|
||||
It allows user interfaces to send positions (and other information like timing) to an engine and for the engine to send its best move (and optionally a PV and evaluation) back.
|
||||
See [this][uci protocol] for a description of the protocol.
|
||||
|
||||
A UCI implementation is provided (see [Uci.hpp](Uci.hpp) and [Uci.cpp](Uci.cpp)) that allows you to use your engine with a GUI or to let it play against another engine (or itself).
|
||||
Running the engine executable without any arguments starts it in UCI mode.
|
||||
It will listen on stdin for commands and write replies to stdout.
|
||||
It will also log some information to a file called `uci-log.txt` in its current working directory:
|
||||
- All incoming and outgoing UCI commands;
|
||||
- After receiving a new position and after getting a move from the engine, the board is printed;
|
||||
- The PV received from the engine.
|
||||
|
||||
|
||||
There are many [UCI GUIs][uci gui] and all of them should work but the one I use is [Cute Chess][cutechess].
|
||||
|
||||
To use your engine in Cute Chess, you first have to add it to its engine list.
|
||||
Go to "Tools -> Settings" and then to the "Engines" tab to add your engine by clicking on the "+" symbol at the bottom.
|
||||
Once you added your engine, you can use it by going to "Game -> New" and then selecting "CPU" and your engine for one or both of the players.
|
||||
|
||||
> :bulb: Cute Chess also includes a command line tool (`cutechess-cli`) that you can use to make two engines play each other.
|
||||
> See [this][cutechess cli] and `cutechess-cli -help` for more info.
|
||||
|
||||
# Extensions
|
||||
|
||||
This section describes some (optional) extensions that could give your engine an edge during the [tournament](#tournament).
|
||||
|
||||
## Time control
|
||||
|
||||
As described [before](#engine), timing information is passed to the engine.
|
||||
This can be used to prevent the engine from timing out during games.
|
||||
|
||||
## Draw conditions
|
||||
|
||||
Besides [stalemate][stalemate], there are some other conditions that can draw a game.
|
||||
Most notably the [*fifty-move rule*][fifty-move rule] and [*threefold repetition*][threefold repetition].
|
||||
These rules can be used to turn a losing position into a draw, especially against an engine that does not understand these rules.
|
||||
They will be applied in the tournament.
|
||||
|
||||
> :bulb: Last year, many games during the tournament were drawn due to threefold
|
||||
> repetition.
|
||||
|
||||
# Tournament
|
||||
|
||||
We plan to organize a tournament between the engines that pass all tests.
|
||||
More details will follow.
|
||||
|
||||
[san]: https://en.wikipedia.org/wiki/Algebraic_notation_(chess)
|
||||
[long algebraic notation]: https://en.wikipedia.org/wiki/Algebraic_notation_(chess)#Long_algebraic_notation
|
||||
[std_map]: https://en.cppreference.com/w/cpp/container/map
|
||||
[std_set]: https://en.cppreference.com/w/cpp/container/set
|
||||
[promotion]: https://en.wikipedia.org/wiki/Promotion_(chess)
|
||||
[uci]: https://en.wikipedia.org/wiki/Universal_Chess_Interface
|
||||
[board representation wp]: https://en.wikipedia.org/wiki/Board_representation_(computer_chess)
|
||||
[board representation cpw]: https://www.chessprogramming.org/Board_Representation
|
||||
[bitboard]: https://en.wikipedia.org/wiki/Bitboard
|
||||
[stockfish]: https://stockfishchess.org/
|
||||
[8x8 board]: https://www.chessprogramming.org/8x8_Board
|
||||
[castling]: https://en.wikipedia.org/wiki/Castling
|
||||
[bit flags]: https://blog.podkalicki.com/bit-level-operations-bit-flags-and-bit-masks/
|
||||
[en passant]: https://en.wikipedia.org/wiki/En_passant
|
||||
[move generation]: https://www.chessprogramming.org/Move_Generation
|
||||
[pseudo legal move]: https://www.chessprogramming.org/Pseudo-Legal_Move
|
||||
[legal move]: https://www.chessprogramming.org/Legal_Move
|
||||
[chess rules]: https://en.wikipedia.org/wiki/Rules_of_chess
|
||||
[chess rules check]: https://en.wikipedia.org/wiki/Rules_of_chess#Check
|
||||
[chess rules game end]: https://en.wikipedia.org/wiki/Rules_of_chess#End_of_the_game
|
||||
[move making]: https://www.chessprogramming.org/Make_Move
|
||||
[game tree]: https://en.wikipedia.org/wiki/Game_tree
|
||||
[checkmate]: https://en.wikipedia.org/wiki/Checkmate
|
||||
[stalemate]: https://en.wikipedia.org/wiki/Stalemate
|
||||
[minimax]: https://en.wikipedia.org/wiki/Minimax
|
||||
[zero-sum game]: https://en.wikipedia.org/wiki/Zero-sum_game
|
||||
[negamax]: https://en.wikipedia.org/wiki/Negamax
|
||||
[alpha-beta pruning]: https://en.wikipedia.org/wiki/Alpha-beta_pruning
|
||||
[move ordering]: https://www.chessprogramming.org/Move_Ordering
|
||||
[iterative deepening]: https://www.chessprogramming.org/Iterative_Deepening
|
||||
[pv]: https://www.chessprogramming.org/Principal_Variation
|
||||
[eval]: https://www.chessprogramming.org/Evaluation
|
||||
[piece value]: https://en.wikipedia.org/wiki/Chess_piece_relative_value
|
||||
[center control]: https://www.chessprogramming.org/Center_Control
|
||||
[king safety]: https://www.chessprogramming.org/King_Safety
|
||||
[ply]: https://en.wikipedia.org/wiki/Ply_(game_theory)
|
||||
[eval score]: https://www.chessprogramming.org/Score
|
||||
[centipawns]: https://www.chessprogramming.org/Centipawns
|
||||
[time control]: https://en.wikipedia.org/wiki/Time_control#Chess
|
||||
[catch2]: https://github.com/catchorg/Catch2
|
||||
[catch2 filter tags]: https://github.com/catchorg/Catch2/blob/v2.x/docs/command-line.md#specifying-which-tests-to-run
|
||||
[chess tactic]: https://en.wikipedia.org/wiki/Chess_tactic
|
||||
[positional advantage]: https://www.chessstrategyonline.com/content/tutorials/introduction-to-chess-strategy-positional-advantage
|
||||
[lichess]: https://lichess.org/
|
||||
[lichess puzzle db]: https://database.lichess.org/#puzzles
|
||||
[lichess puzzle themes]: https://lichess.org/training/themes
|
||||
[fen]: https://en.wikipedia.org/wiki/Forsyth-Edwards_Notation
|
||||
[lichess board editor]: https://lichess.org/editor
|
||||
[uci]: https://en.wikipedia.org/wiki/Universal_Chess_Interface
|
||||
[uci protocol]: https://backscattering.de/chess/uci/
|
||||
[uci gui]: https://www.chessprogramming.org/UCI#GUIs
|
||||
[cutechess]: https://github.com/cutechess/cutechess
|
||||
[cutechess cli]: https://github.com/cutechess/cutechess#running
|
||||
[chess engine]: https://en.wikipedia.org/wiki/Chess_engine
|
||||
[questions]: https://gitlab.kuleuven.be/distrinet/education/cpl/cplusplus-project-assignment/-/issues
|
||||
[assignment repo]: https://gitlab.kuleuven.be/distrinet/education/cpl/cplusplus-project-assignment
|
||||
[exercises setup]: https://gitlab.kuleuven.be/distrinet/education/cpl/cplusplus-exercises-assignment#setup
|
||||
[git submodules]: https://git-scm.com/book/en/v2/Git-Tools-Submodules
|
||||
[c++17]: https://en.cppreference.com/w/cpp/17
|
||||
[c++20]: https://en.cppreference.com/w/cpp/20
|
||||
[c++ features]: https://en.cppreference.com/w/cpp
|
||||
[fifty-move rule]: https://en.wikipedia.org/wiki/Fifty-move_rule
|
||||
[threefold repetition]: https://en.wikipedia.org/wiki/Threefold_repetition
|
||||
[gcc c++ status]: https://gcc.gnu.org/projects/cxx-status.html
|
||||
[gitlab pipelines]: https://docs.gitlab.com/ee/ci/pipelines/
|
||||
[gcc warnings]: https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
|
||||
[std optional]: https://en.cppreference.com/w/cpp/utility/optional
|
||||
[transposition table]: https://www.chessprogramming.org/Transposition_Table
|
||||
[glicko]: https://en.wikipedia.org/wiki/Glicko_rating_system
|
Reference in New Issue
Block a user