A UCI-Compliant Chess Engine in Go
Libra Chess is a UCI (Universal Chess Interface) compliant chess engine written in Go. The primary goal of Libra is to achieve a balance between high performance, modern software architecture, and clarity of design. This project serves as an exploration of chess engine development leveraging Go's unique strengths in concurrency, tooling, and efficient compilation.
This engine is designed for chess enthusiasts, developers looking to understand chess engine internals, and as a demonstration of software engineering principles applied to a complex domain.
- UCI Protocol Compliant: Seamless integration with popular UCI-compatible GUIs (e.g., CuteChess, CoreChess, PyChess).
- Alpha-Beta Search: Optimized Alpha-Beta pruning forms the core of the search algorithm.
- Iterative Deepening: Allows for flexible time management and progressive deepening of the search.
- Transposition Tables: Utilizes Zobrist hashing to store and retrieve previously evaluated positions, significantly speeding up search by avoiding redundant computations.
- Piece-Square Tables (PSTs): Employs PSTs for nuanced positional evaluation, guiding the engine's understanding of piece placement.
- Material Evaluation: Core evaluation component based on standard piece values.
- Endgame Evaluation Heuristics: Includes logic to encourage king activity and mating sequences in the endgame (e.g., incentivizing the stronger side's king to approach the opponent's king).
- Move Generation: Optimized and validated pseudo-legal move generation with legality checks.
- Comprehensive Testing Suite:
- Unit tests for core logic (
go test
). - Perft testing for move generation correctness.
- Automated match play against itself and Stockfish using
cutechess-cli
for strength benchmarking and regression testing.
- Unit tests for core logic (
- Go: Version 1.23 or higher.
- Make: Standard
make
utility. golangci-lint
(Optional): For running linters (make lint
). Install via their official instructions.cutechess-cli
(Optional but Recommended): For running match tests. Download from its official repository. Ensure it's in your PATH or adjustMakefile
paths.
go build
# or
make build
This will produce the main executable libra-chess
- Directly (for UCI interaction):
The engine will start and await UCI commands.
./libra
- With a UCI GUI:
Configure your favorite UCI GUI (e.g., CuteChess, CoreChess, PyChess) to use the
./libra-chess
executable. - Using
main.go
(if it contains a simple CLI or test loop):go run main.go
- Unit Tests:
make test
- Linter:
make lint
- Match Play vs. Itself (MainLibra vs PullLibra):
To run a versus match between two versions of the engine using
cutechess-cli
, you need to have two binaries:libra-chess
andlibra-main
. If you want to test the current version against itself, simply copy thelibra-chess
binary tolibra-main
:Then run:cp libra-chess libra-main
(Note: The Makefile refers tomake test-cutechess
./libra-chess
and./libra-main
. Ensure these binaries are correctly built and named.libra-chess
might be the current development version andlibra-main
a stable baseline or a copy for self-play.) - Match Play vs. Stockfish:
(Requires Stockfish CLI to be at
make test-stockfish
./stockfish/stockfish-cli
) - Debug Match:
make test-debug
Libra Chess is architected with modularity, simplicity, and maintainability as primary considerations. The choice of Go as the implementation language was deliberate, aiming to harness its excellent concurrency primitives, straightforward syntax, and robust standard library for an extra boost in performance.
- Advantages:
- Concurrency: Go's goroutines and channels offer a powerful yet simple model for concurrent programming, which is pivotal for future enhancements like parallel search.
- Performance: While not C/C++, Go offers impressive performance, especially with its efficient garbage collector (GC) and direct compilation to machine code. Careful memory management is still crucial.
- Simplicity & Readability: Go's clean syntax and established conventions promote maintainable and understandable code.
- Tooling: Rich ecosystem including
gofmt
for automated formatting,go test
for testing, andgolangci-lint
for static analysis.
- Trade-offs & Considerations:
- Garbage Collection: While Go's GC is highly optimized, in performance-critical sections like deep search, GC pauses can be a concern. This necessitates careful memory allocation patterns (Move/UndoMove instead of cloning).
- Ecosystem for Chess Engines: The C++ ecosystem for chess engines is more mature with a larger pool of shared libraries and knowledge. Libra aims to contribute to the growing Go presence in this domain.
The engine's core logic is organized within the pkg/
directory, with separation of concerns:
board.go
: Board representation, piece management, and core game state.evaluate.go
: Static evaluation function, including material, PSTs, and endgame heuristics.generate.go
: Move generation logic.search.go
: Search algorithms (Alpha-Beta, iterative deepening).tt.go
: Transposition table implementation.zobrist.go
: Zobrist hashing for position keys.move.go
: Move/UndoMove for calculations,piece.go
: Pieces definitions,utils.go
: Utility and data structure definitions.
This modular design facilitates easier testing, debugging, and future feature development.
The current evaluation function is a classical handcrafted one, balancing speed and accuracy.
- Components:
- Material: Standard piece values (Pawn:100, Knight:300, Bishop:300, Rook:500, Queen:900).
- Piece-Square Tables (PSTs): Static tables that assign positional bonuses or penalties to pieces based on their square. These are currently simplified and offer a good baseline.
- Endgame Heuristics: A specific heuristic encourages the king with a material advantage to move towards the opponent's king when total material on the board is low (<= 14 units, excluding kings). This promotes checkmates in won endgames.
- Trade-offs:
- Speed vs. Accuracy: Handcrafted evaluation functions are generally fast. The current PSTs are relatively simple, which makes evaluation quick but potentially less nuanced than more complex schemes or ML-based models.
- Complexity of Terms: Adding more evaluation terms (e.g., detailed pawn structure, king safety, mobility beyond basic checks, passed pawns) can improve strength but increases computational cost and tuning complexity. This is a key area for future refinement.
- Minimax with Alpha-Beta Pruning: A standard and effective framework for chess search.
- Iterative Deepening: Allows the engine to search to a certain depth, then use the information from that search (e.g., principal variation) to order moves for the next, deeper iteration. This is essential for effective time management.
- Quiescence Search: (Assumed, or a high-priority addition) To mitigate the horizon effect, a quiescence search is typically implemented to evaluate "quiet" positions by extending the search for captures and other tactical moves.
- Trade-offs:
- Search Depth vs. Time: The deeper the search, generally the stronger the play, but time is a finite resource. Effective move ordering, pruning, and extensions/reductions are key to searching deeper within the allocated time.
- Selectivity: Deciding which branches of the search tree to explore deeply (extensions) and which to prune or search shallowly (reductions) is a complex balancing act.
A robust testing strategy is paramount for engine development.
- Correctness:
perft
tests validate move generation exhaustively. Unit tests cover individual functions and modules. - Strength: Regular match play against baseline versions of Libra, other engines like Stockfish (at controlled ELO levels), and itself (
test-cutechess
) provides a measure of playing strength and helps identify regressions or improvements from changes. TheMakefile
provides targets for these tests. - Debugging: The
test-debug
target in theMakefile
facilitates focused debugging sessions withcutechess-cli
.
- Bitboards: The board state is represented using 64-bit integers (bitboards) for each piece type and color (e.g.,
WhitePawns
,BlackKnights
). Each bit corresponds to a square, enabling fast piece lookup and efficient move generation. - Castling Rights: Castling availability is managed with a dedicated struct, storing four booleans for each possible castling right (white/black, king/queen side).
- Turn, En Passant, and Move Counters:
WhiteToMove
(bool): Indicates which side is to move.OnPassant
(byte): Stores the en passant target square index (0 if not available).HalfMoveClock
andFullMoveCounter
: Track the 50-move rule and move number.
- FEN Support: The board can be initialized from and exported to FEN, supporting all standard fields (piece placement, turn, castling, en passant, clocks).
- Utility Methods: Includes methods for piece lookup, move application, cloning, and board printing.
- Design Rationale: This design leverages bitboards for performance and clarity, and is extensible for future features.
- Pseudo-Legal to Legal: Moves are typically generated as pseudo-legal (ignoring checks to the king) and then validated.
- Efficiency: Techniques like pre-calculated attack tables for sliding pieces and knight moves are common. Libra's current approach is direct computation, which can be optimized further.
- Zobrist Hashing: Each position is mapped to a unique (with high probability) 64-bit hash key. Keys are updated incrementally when moves are made/unmade.
- Table Structure: Typically a hash map or a large array with a simple indexing scheme (e.g.,
hash % table_size
). - Stored Information: Each entry stores the hash key (for collision detection), depth of the search, score, score type (exact, lower bound, upper bound), and best move.
- Trade-off: The amount of information stored per entry affects TT size and the utility of hits. More info is better but costs memory.
- Parallel Move Evaluation: The core search algorithm leverages Go's concurrency by evaluating top-level moves in parallel. The
Search
function distributes each legal move at the root to a worker goroutine, allowing multiple positions to be searched simultaneously and efficiently utilizing all available CPU cores. - Worker Pool: The number of worker goroutines is determined by the number of logical CPUs (
runtime.GOMAXPROCS(0)
), ensuring optimal parallelism on the host system. - Result Aggregation: Results from all workers are collected and the best move is selected according to the search score, preserving move ordering for tie-breaking.
- Thread Safety: Each worker operates on a cloned board state, ensuring thread safety and correctness.
- UCI Communication: UCI command handling can still be performed in a separate goroutine to keep the engine responsive during search.
This approach provides significant speedup for the root search and is a foundation for further parallelism in deeper search layers in the future.
The following table shows the results of running the PerftParallel(N)
benchmark at increasing depths. The "Time per op" column shows the average time (in microseconds) to compute all legal moves at each depth, and the "Legal moves calculated" column shows the total number of legal moves (nodes) at that depth.
perft(N) | Time per op (microseconds) | Legal moves calculated |
---|---|---|
1 | 2.81 | 20 |
2 | 37.85 | 400 |
3 | 274.88 | 8,902 |
4 | 6512.77 | 197,281 |
5 | 142827.80 | 4,865,609 |
6 | 3865875.06 | 119,060,324 |
cpu: AMD Ryzen 7 6800H
This benchmark demonstrates that the engine can process approximately 30 million moves per second. This is calculated by dividing the number of legal moves at depth 5 by the time taken per operation (in seconds):
Moves per second β 4,865,609 moves / 0.1428278 seconds β 34,070,000 moves/second
Libra Chess is an actively evolving project. Key areas for future development include:
- Search Enhancements:
- Principal Variation Search (PVS): Implement PVS for more efficient search.
- Late Move Reductions (LMR): Reduce search depth for moves ordered later.
- Futility Pruning & Razoring: More aggressive pruning techniques.
- Null Move Pruning: A powerful pruning technique.
- Improved Quiescence Search: More robust handling of tactical positions.
- Evaluation Refinements:
- Advanced Positional Terms: Incorporate pawn structure analysis, king safety, mobility scores, passed pawn evaluation.
- Tapered Evaluation: Smoothly transition PSTs and other eval terms from middlegame to endgame.
- Automated Tuning: Explore techniques like CLOP (Chess ELO Optimizer) for tuning evaluation parameters.
- Time Management: More sophisticated algorithms to allocate time effectively across moves.
- Opening Book: Develop or integrate a more comprehensive internal opening book format.
- Endgame Tablebases: Integrate support for Syzygy or Gaviota tablebases for perfect play in endgames.
- Concurrency in Search: Leverage Go's goroutines to implement parallel search algorithms (e.g., Lazy SMP or ABDADA).
- UCI Options: Expose more internal parameters (e.g., Hash size, contempt factor) via UCI options.
- Continuous Integration/Delivery: Enhance CI pipeline for automated testing and releases.
We welcome contributions of all kinds! Whether you're a chess enthusiast, Go developer, or just curious, your ideas, bug reports, code improvements, and questions are valuable to us.
- How to contribute:
- Open an Issue for bugs, feature requests, or questions.
- Submit a Pull Request for code, documentation, or test improvements.
- Suggest enhancements or discuss design ideas.
- Help with testing, benchmarking, or documentation.
If you're unsure where to start, feel free to ask!
This project is licensed under the MIT License (or specify your chosen license).