A dependency-free chess engine library built to run anywhere.
Above all, this video by Tom7 is my inspiration for this project. He’s absolutely brilliant and I implore you to watch his content.
I love chess a lot. It’s definitely one of my favorite games ever. However, I’ve always been disappointed when trying to write programs that play chess digitally (particularly in a compiled language). Although several amazing engines exist, it’s near impossible to find a neat library for chess-related-programming that runs on everything.
chess-engine is a solution to my problem. If you want a chess engine that runs on embedded devices, the terminal, the desktop (with a gui), and the web, this is probably your best bet.
This particular AI (along with most other chess AIs) works using the Minimax algorithm, along with Alpha-Beta pruning for optimization.
Now, let’s unpack that.
The Minimax algorithm essentially iterates through all possible moves recursively, and evaluates all of the boards after the moves are played. If the board is more favorable, it will encourage playing its parent move, but if a board is less favorable, then it will select against playing a given move.
Additionally, when the AI attempts to see past just the current board, it will assume the human always responds with the best moves. As a result, the computer almost never blunders. This allows the computer to almost always play objectively better moves than the player.
Because it has zero dependencies, it’s extremely simple to embed in the web browser using wasm. Try playing it yourself!
The Board
structure has a few different methods that allow users to generate moves from a given position, including get_best_next_move
, get_worst_next_move
, and get_legal_moves
. These are particularly handy for writing chess AIs to play against.
fn main() {
let board = Board::default();
// Get the best move with 4 moves of lookahead
let best_move = board.get_best_next_move(4);
// Get the worst move with 3 moves of lookahead
let worst_move = board.get_worst_next_move(3);
// Get all of the possible legal moves for the given player
let legal_moves = board.get_legal_moves();
// Print the board
println!("{}", board);
print!("CPU chose to ");
match best_move {
Move::Piece(from, to) => println!("move {} to {}", from, to),
Move::KingSideCastle => println!("castle kingside"),
Move::QueenSideCastle => println!("castle queenside"),
Move::Resign => println!("resign")
}
}
To add some variation or more advanced play, consider writing an AI that plays known openings that build better positions before using the get_best_next_move
method!
Additionally, users can create their own custom Board
objects other than the default one. This is done using the BoardBuilder
structure. The BoardBuilder
structure supports enabling and disabling castling, placing rows and columns of pieces, and placing individual pieces.
Keep in mind when using a BoardBuilder
that castling is disabled by default!
fn main() {
// `BoardBuilder::new()` returns an empty board
// with castling disabled.
// Creating a board builder from another board
// structure will preserve
// all settings from the board (such as castling
// and the last en-passant move).
// This BoardBuilder constructs the "Horde" chess variant!
let board = BoardBuilder::from(Board::default())
.row(Piece::Pawn(WHITE, A1))
.row(Piece::Pawn(WHITE, A2))
.row(Piece::Pawn(WHITE, A3))
.row(Piece::Pawn(WHITE, A4))
.piece(Piece::Pawn(WHITE, F5))
.piece(Piece::Pawn(WHITE, G5))
.piece(Piece::Pawn(WHITE, B5))
.piece(Piece::Pawn(WHITE, C5))
.build();
// The CPU can also play variants!
let cpu_move = board.get_best_next_move(3);
match board.play_move(cpu_move) {
GameResult::Continuing(next_board) => {
println!("{}", next_board);
}
GameResult::Victory(winner) => {
// You can use the ! operator on a player's
// color to invert.
println!("{} loses. {} is victorious.",
!winner, winner
);
}
GameResult::IllegalMove(x) => {
eprintln!("{} is an illegal move.", x);
}
GameResult::Stalemate => {
println!("Drawn game.");
}
}
}