binbo
binbo copied to clipboard
Chess representation written in Erlang using Bitboards, ready for use on game servers
= Binbo :toc: macro :toclevels: 4
image:https://img.shields.io/hexpm/v/binbo.svg?color=yellow["Binbo on Hex.pm", link="https://hex.pm/packages/binbo"] image:https://github.com/DOBRO/binbo/workflows/Build/badge.svg?branch=master["CI Status", link="https://github.com/DOBRO/binbo/actions?query=workflow%3ABuild+branch%3Amaster"] image:https://codecov.io/gh/DOBRO/binbo/branch/master/graph/badge.svg["Code coverage", link="https://codecov.io/gh/DOBRO/binbo"] image:https://img.shields.io/badge/erlang-%3E%3D%2020.0-0d6e8c.svg["Erlang", link="https://www.erlang.org/"] image:https://img.shields.io/badge/license-Apache%202.0-blue.svg["License", link="LICENSE"]
Binbo is a full-featured Chess representation written in pure Erlang using https://www.chessprogramming.org/Bitboards[Bitboards]. It is basically aimed to be used on game servers where people play chess online.
It's called Binbo because its ground is a binary board containing only zeros and ones (0 and 1) since this is the main meaning of Bitboards as an internal chessboard representation.
Binbo also uses the https://www.chessprogramming.org/Magic_Bitboards[Magic Bitboards] approach for a blazing fast move generation of sliding pieces (rook, bishop, and queen).
Note: it's not a chess engine but it could be a good starting point for it. It can play the role of a core (regarding move generation and validation) for multiple chess engines running on distributed Erlang nodes, since Binbo is an OTP application itself.
In addition, the application is able to communicate with https://www.chessprogramming.org/Category:UCI[chess engines that support UCI protocol] (https://www.chessprogramming.org/UCI[Universal Chess Interface]) such as Stockfish, Shredder, Houdini, etc. You can therefore write your own client-side or server-side chess bot application on top of Binbo, or just play with engine right in Erlang shell. TCP connections to remote chess engines are also supported.
Binbo is part of the https://github.com/h4cc/awesome-elixir[Awesome Elixir] list.
image::https://user-images.githubusercontent.com/296845/61208986-40792d80-a701-11e9-93c8-d2c41c5ef00d.png[Binbo sample]
'''
toc::[]
'''
== Features
- Blazing fast move generation and validation.
- No bottlenecks. Every game is an Erlang process (
gen_server) with its own game state. - Ability to create as many concurrent games as many Erlang processes allowed in VM.
- Support for PGN loading.
- All the chess rules are completely covered including: ** https://en.wikipedia.org/wiki/En_passant[En-passant move]; ** https://en.wikipedia.org/wiki/Castling[Castling]; ** https://en.wikipedia.org/wiki/Fifty-move_rule[Fifty-move rule]; ** https://en.wikipedia.org/wiki/Threefold_repetition[Threefold repetition]; ** Draw by insufficient material: *** King versus King, *** King and Bishop versus King, *** King and Knight versus King, *** King and Bishop versus King and Bishop with the bishops on the same color;
- Unicode chess symbols support for the board visualization right in Erlang shell: + ♙{nbsp}♘{nbsp}♗{nbsp}♖{nbsp}♕{nbsp}♔{nbsp}{nbsp}{nbsp}{nbsp}♟{nbsp}♞{nbsp}♝{nbsp}♜{nbsp}♛{nbsp}♚
- UCI protocol support.
- Support for TCP connections to remote UCI chess engines.
- Passes all https://www.chessprogramming.org/Perft_Results[perft] tests.
- Cross-platform application. It can run on Linux, Unix, Windows, and macOS.
- Ready for use on game servers.
== Requirements
** https://www.erlang.org/[Erlang/OTP] 20.0 or higher. ** https://www.rebar3.org/[rebar3]
== Installation
=== For Erlang projects
Add Binbo as a dependency to your rebar.config file:
[source,erlang]
{deps, [ {binbo, "4.0.3"} ]}.
=== For Elixir projects
Add Binbo as a dependency to your mix.exs file:
[source,elixir]
defp deps do [ {:binbo, "~> 4.0.2"} ] end
== Quick start
Clone repository, change directory to binbo and run rebar3 shell (or make shell):
[source,bash]
$ git clone https://github.com/DOBRO/binbo.git $ cd binbo $ rebar3 shell
=== Common example
.In the Erlang shell: [source,erlang]
%% Start Binbo application first: binbo:start().
%% Start new process for the game: {ok, Pid} = binbo:new_server().
%% Start new game in the process: binbo:new_game(Pid).
%% Or start new game with a given FEN: binbo:new_game(Pid, <<"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1">>).
%% Look at the board with ascii or unicode pieces: binbo:print_board(Pid). binbo:print_board(Pid, [unicode]).
%% Make move for White and Black: binbo:move(Pid, <<"e2e4">>). binbo:move(Pid, <<"e7e5">>).
%% Have a look at the board again: binbo:print_board(Pid). binbo:print_board(Pid, [unicode]).
[[quickstart-play-with-engine]] === Play with engine on local machine
.In the Erlang shell: [source,erlang]
%% Start Binbo application first:
binbo:start(). {ok,[compiler,syntax_tools,uef,binbo]}
%% Start new process for the game:
{ok, Pid} = binbo:new_server(). {ok,<0.157.0>}
%% Set full path to the engine's executable file:
EnginePath = "/usr/local/bin/stockfish". "/usr/local/bin/stockfish"
%% Start new game in the process:
binbo:new_uci_game(Pid, #{engine_path => EnginePath}). {ok,continue}
%% Which side is to move?
binbo:side_to_move(Pid). {ok,white}
%% Say, you want to play Black. Tell the engine to make move for White.
binbo:uci_play(Pid, #{}). {ok,continue,<<"e2e4">>}
%% Make your move for Black and get the engine's move immediately:
binbo:uci_play(Pid, #{}, <<"e7e5">>). {ok,continue,<<"g1f3">>} % the engine's move was "g1f3"
%% Make your next move for Black and, again, get the engine's move at once:
binbo:uci_play(Pid, #{}, <<"b8c6">>). {ok,continue,<<"b1c3">>} % the engine's move was "b1c3"
%% Look at the board with ascii or unicode pieces. %% Flip the board to see Black on downside: binbo:print_board(Pid, [flip]). binbo:print_board(Pid, [unicode, flip]).
%% It's your turn now. Let the engine search for the best move for you with default options. %% No move actually done, just hint:
binbo:uci_bestmove(Pid, #{}). {ok,<<"g8f6">>}
%% Tell the engine to search for the best move at depth 20:
binbo:uci_bestmove(Pid, #{depth => 20}). {ok,<<"g8f6">>}
%% To make the gameplay more convenient, introduce new function:
Play = fun(Move) -> Result = binbo:uci_play(Pid, #{}, Move), binbo:print_board(Pid, [unicode, flip]), Result end.
%% Now, with this function, go through three steps at once: %% - make move "g8f6", %% - get the engine's move, %% - see how the position was changed.
Play("g8f6").
… engine's move was "d2d4": [source]
+---+---+---+---+---+---+---+---+ 1 | ♖ | | ♗ | ♔ | ♕ | ♗ | | ♖ | +---+---+---+---+---+---+---+---+ 2 | ♙ | ♙ | ♙ | | | ♙ | ♙ | ♙ | +---+---+---+---+---+---+---+---+ 3 | | | ♘ | | | ♘ | | | +---+---+---+---+---+---+---+---+ 4 | | | | ♙ | ♙ | | | | +---+---+---+---+---+---+---+---+ 5 | | | | ♟ | | | | | +---+---+---+---+---+---+---+---+ 6 | | | ♞ | | | ♞ | | | +---+---+---+---+---+---+---+---+ 7 | ♟ | ♟ | ♟ | | ♟ | ♟ | ♟ | ♟ | +---+---+---+---+---+---+---+---+ 8 | ♜ | | ♝ | ♚ | ♛ | ♝ | | ♜ | +---+---+---+---+---+---+---+---+ H G F E D C B A
Side to move: Black Lastmove: d2-d4, WHITE_PAWN Fullmove: 4 Halfmove: 0 FEN: "r1bqkb1r/pppp1ppp/2n2n2/4p3/3PP3/2N2N2/PPP2PPP/R1BQKB1R b KQkq d3 0 4" Status: continue
{ok,continue,<<"d2d4">>}
[[quickstart-uci-over-tcp]] === Connect to remote engine over TCP
The examples below assume that Stockfish is used as the chess engine and its path is /usr/local/bin/stockfish, change it according to your environment.
TCP service starts on local machine on port 9010.
If you are on Linux, install socat and start TCP service. On macOS just use file org.stockfish.x86.plist (for Intel-based devices) or org.stockfish.arm.plist (for Apple silicon) provided in the test folder (see below).
.On Ubuntu/Debian: [source,bash]
$ apt install socat -y $ socat TCP-LISTEN:9010,reuseaddr,fork EXEC:/usr/local/bin/stockfish & $ git clone https://github.com/DOBRO/binbo.git $ cd binbo $ rebar3 shell
.On Centos/Fedora: [source,bash]
$ dnf install socat -y $ socat TCP-LISTEN:9010,reuseaddr,fork EXEC:/usr/local/bin/stockfish & $ git clone https://github.com/DOBRO/binbo.git $ cd binbo $ rebar3 shell
.On macOS (Intel x86 Architecture): [source,bash]
$ git clone https://github.com/DOBRO/binbo.git $ cd binbo $ launchctl load test/helper-files/org.stockfish.x86.plist $ rebar3 shell
.On macOS (Apple silicon): [source,bash]
$ git clone https://github.com/DOBRO/binbo.git $ cd binbo $ launchctl load test/helper-files/org.stockfish.arm.plist $ rebar3 shell
.Now in the Erlang shell: [source,erlang]
%% Start Binbo application first:
binbo:start(). {ok,[compiler,syntax_tools,uef,binbo]}
%% Start new process for the game:
{ok, Pid} = binbo:new_server(). {ok,<0.282.0>}
%% Set path to the remote engine as tuple {Host, Port, Timeout}:
EnginePath = {"localhost", 9010, 5000}. {"localhost",9010,5000}
%% Start new game in the process:
binbo:new_uci_game(Pid, #{engine_path => EnginePath}). {ok,continue}
%% UCI-over-TCP connection made, start playing:
binbo:uci_play(Pid, #{movetime => 100}, <<"e2e4">>). {ok,continue,<<"c7c5">>} % the engine's move was "c7c5"
== Interface
There are three steps to be done before making game moves:
. Start Binbo application. . Create process for the game. . Initialize game state in the process.
Note: process creation and game initialization are separated for the following reason: since Binbo is aimed to handle a number of concurrent games, the game process should be started as quick as possible leaving the http://erlang.org/doc/design_principles/sup_princ.html[supervisor] doing the same job for another game. It's important for high-load systems where game creation is a very frequent event.
=== Starting application
To start Binbo, call:
[source,erlang]
binbo:start().
=== Creating game process
[source,erlang]
binbo:new_server() -> {ok, Pid} | {error, Reason}. binbo:new_server(Options) -> {ok, Pid} | {error, Reason}.
.where:
Pid- pid of the created process;Options- options for the game process (see link:#server-options[bellow]).
.So, to start one or more game processes: [source,erlang]
{ok, Pid1} = binbo:new_server(), {ok, Pid2} = binbo:new_server(), {ok, Pid3} = binbo:new_server().
[[server-options]] ==== Options for the game process [source,erlang]
binbo:set_server_options(Pid, Options) -> ok | {error, Reason}.
Pid is the pid of the game process.
.Options:
[source,erlang]
#{ idle_timeout => timeout(), onterminate => {fun my_callback/4, Arg} }
.where:
idle_timeout- time in milliseconds with no messages received before the game process exits. Defaults toinfinity.onterminate- tuple where the first element is a callback function that performs when process exits. This function must be of arity 4 with argumnentsPid,Reason,GameState, andArgwhere: **Pid- pid of the game process; **Reason- the reason why the game process exited; **GameState- the whole link:#game-state[game state]; **Arg- the argument you want to pass to the callback function.
.Example: [source,erlang]
-module(on_terminate).
-export([run/0]).
run() -> binbo:start(), {ok, Pid} = binbo:new_server(), binbo:new_game(Pid), binbo:set_server_options(Pid, #{ idle_timeout => 1000, onterminate => {fun onterminate_callback/4, "my argument"} }), % 'onterminate_callback/4' will be called after 1000 ms ok.
onterminate_callback(GamePid, Reason, Game, Arg) -> io:format("GamePid: ~p~n", [GamePid]), io:format("Reason: ~p~n", [Reason]), io:format("Game: ~p~n", [Game]), io:format("Arg: ~p~n", [Arg]), ok.
.To reset options, call: [source,erlang]
binbo:set_server_options(Pid, #{ idle_timeout => infinity, onterminate => undefined })
[[initializing-new-game]] === Initializing new game
[source,erlang]
binbo:new_game(Pid) -> {ok, GameStatus} | {error, Reason}.
binbo:new_game(Pid, Fen) -> {ok, GameStatus} | {error, Reason}.
.where:
Pidis thepidof the process where the game is to be initialized;Fen(string()orbinary()) is the https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation[Forsyth–Edwards Notation] (FEN);GameStatusis the link:#game-status[game status].
It is possible to reinitialize game in the same process. For example:
[source,erlang]
binbo:new_game(Pid), binbo:new_game(Pid, Fen2), binbo:new_game(Pid, Fen3).
.Example: [source,erlang]
%% In the Erlang shell.
{ok, Pid} = binbo:new_server(). {ok,<0.185.0>}
% New game from the starting position:
binbo:new_game(Pid). {ok,continue}
% New game with the given FEN:
binbo:new_game(Pid, <<"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1">>). {ok,continue}
=== Making moves
==== API
[source,erlang]
binbo:move(Pid, Move) -> {ok, GameStatus} | {error, Reason}.
binbo:san_move(Pid, Move) -> {ok, GameStatus} | {error, Reason}.
binbo:index_move(Pid, FromIndex, ToIndex) -> {ok, GameStatus} | {error, Reason}.
binbo:index_move(Pid, FromIndex, ToIndex, PromotionType) -> {ok, GameStatus} | {error, Reason}.
where:
Pidis the pid of the game process;Moveis ofbinary()orstring()type;GameStatusis the link:#game-status[game status].FromIndex- index of square a piece moves from.ToIndex- index of square a piece moves to.PromotionType- piece that a pawn should be promoted to, one of the atoms:q,r,b,n(queen, rook, bishop, knight). Defaults toq(queen).
Function binbo:move/2 supports only strict square notation with respect to argument Move, for example: <<"e2e4">>, <<"e7e5">>, etc.
Function binbo:san_move/2 is intended to handle various formats of argument Move including https://en.wikipedia.org/wiki/Algebraic_notation_(chess)[standard algebraic notation] (SAN), for example: <<"e4">>, <<"Nf3">>, <<"Qxd5">>, <<"a8=Q">>, <<"Rdf8">>, <<"R1a3">>, <<"O-O">>, <<"O-O-O">>, <<"e1e8">>, etc.
Function binbo:index_move/3,4 takes only square indices for the second and third parameter. For example, binbo:index_move(Pid, 12, 28) is the same as binbo:move(Pid, <<"e2e4">>).
.Examples for binbo:move/2:
[source,erlang]
%% In the Erlang shell.
% New game from the starting position:
{ok, Pid} = binbo:new_server(). {ok,<0.190.0>} binbo:new_game(Pid). {ok,continue}
% Start making moves
binbo:move(Pid, <<"e2e4">>). % e4 {ok,continue}
binbo:move(Pid, <<"e7e5">>). % e5 {ok,continue}
binbo:move(Pid, <<"f1c4">>). % Bc4 {ok,continue}
binbo:move(Pid, <<"d7d6">>). % d6 {ok,continue}
binbo:move(Pid, <<"d1f3">>). % Qf3 {ok,continue}
binbo:move(Pid, <<"b8c6">>). % Nc6 {ok,continue}
% And here is checkmate!
binbo:move(Pid, <<"f3f7">>). % Qf7# {ok,{checkmate,white_wins}}
.Examples for binbo:san_move/2:
[source,erlang]
%% In the Erlang shell.
% New game from the starting position:
{ok, Pid} = binbo:new_server(). {ok,<0.190.0>} binbo:new_game(Pid). {ok,continue}
% Start making moves
binbo:san_move(Pid, <<"e4">>). {ok,continue}
binbo:san_move(Pid, <<"e5">>). {ok,continue}
binbo:san_move(Pid, <<"Bc4">>). {ok,continue}
binbo:san_move(Pid, <<"d6">>). {ok,continue}
binbo:san_move(Pid, <<"Qf3">>). {ok,continue}
binbo:san_move(Pid, <<"Nc6">>). {ok,continue}
% Checkmate!
binbo:san_move(Pid, <<"Qf7#">>). {ok,{checkmate,white_wins}}
.Examples for binbo:index_move/3:
[source,erlang]
%% In the Erlang shell.
% New game from the starting position:
{ok, Pid} = binbo:new_server(). {ok,<0.190.0>} binbo:new_game(Pid). {ok,continue}
% Start making moves
binbo:index_move(Pid, 12, 28). % e2-e4 {ok,continue}
binbo:index_move(Pid, 52, 36). % e7-e5 {ok,continue}
==== Castling
Binbo recognizes https://en.wikipedia.org/wiki/Castling[castling] when:
- White king moves from
E1toG1(O-O); - White king moves from
E1toC1(O-O-O); - Black king moves from
E8toG8(O-O); - Black king moves from
E8toC8(O-O-O).
Binbo also checks whether castling allowed or not acording to the chess rules.
.Castling examples: [source,erlang]
% White castling kingside binbo:move(Pid, <<"e1g1">>). binbo:san_move(Pid, <<"O-O">>).
% White castling queenside binbo:move(Pid, <<"e1c1">>). binbo:san_move(Pid, <<"O-O-O">>).
% Black castling kingside binbo:move(Pid, <<"e8g8">>). binbo:san_move(Pid, <<"O-O">>).
% Black castling queenside binbo:move(Pid, <<"e8c8">>). binbo:san_move(Pid, <<"O-O-O">>).
==== Promotion
Binbo recognizes https://en.wikipedia.org/wiki/Promotion_(chess)[promotion] when:
- White pawn moves from square of
rank 7to square ofrank 8; - Black pawn moves from square of
rank 2to square ofrank 1.
.Promotion examples: [source,erlang]
% White pawn promoted to Queen: binbo:move(Pid, <<"a7a8q">>). binbo:san_move(Pid, <<"a8=Q">>). % or just: binbo:move(Pid, <<"a7a8">>). binbo:san_move(Pid, <<"a8">>).
% White pawn promoted to Knight: binbo:move(Pid, <<"a7a8n">>). binbo:san_move(Pid, <<"a8=N">>).
% Black pawn promoted to Queen: binbo:move(Pid, <<"a2a1q">>). binbo:san_move(Pid, <<"a1=Q">>). % or just: binbo:move(Pid, <<"a2a1">>). binbo:san_move(Pid, <<"a1">>).
% Black pawn promoted to Knight: binbo:move(Pid, <<"a2a1n">>). binbo:san_move(Pid, <<"a1=N">>).
==== En passant
Binbo also recognizes the https://en.wikipedia.org/wiki/En_passant[en passant capture] in strict accordance with the chess rules.
=== Getting FEN
[source,erlang]
binbo:get_fen(Pid) -> {ok, Fen}.
.Example: [source,erlang]
binbo:get_fen(Pid). {ok, <<"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1">>}.
=== PGN loading
[source,erlang]
binbo:load_pgn(Pid, PGN) -> {ok, GameStatus} | {error, Reason}.
binbo:load_pgn_file(Pid, Filename) -> {ok, GameStatus} | {error, Reason}.
.where:
Pidis the pid of the game process;PGNis a https://en.wikipedia.org/wiki/Portable_Game_Notation[Portable Game Notation], its type isbinary();Filenameis a path to the file from which PGN is to be loaded. Its type isbinary()orstring().GameStatusis the link:#game-status[game status].
Function binbo:load_pgn/2 loads PGN itself.
If PGN is pretty large and you are able to load it from local file, to avoid sending large data between processes, use binbo:load_pgn_file/2 since it's highly optimized for reading local files.
To extract move list, Binbo takes into account various cases specific to PGN such as comments in braces, https://chess.stackexchange.com/questions/18214/valid-pgn-variations[recursive annotation variations] (RAVs) and https://en.wikipedia.org/wiki/Numeric_Annotation_Glyphs[numeric annotation glyphs] (NAGs).
.Examples: [source,erlang]
%% Binary PGN: load_pgn() -> PGN = <<"1. e4 e5 2. Nf3 Nc6 3. Bb5 a6">>, {ok, Pid} = binbo:new_server(), binbo:load_pgn(Pid, PGN).
%% From file: load_pgn_from_file() -> Filename = "/path/to/game.pgn", {ok, Pid} = binbo:new_server(), binbo:load_pgn_file(Pid, Filename).
=== Board visualization
[source,erlang]
binbo:print_board(Pid) -> ok. binbo:print_board(Pid, [unicode|ascii|flip]) -> ok.
You may want to see the current position right in Elang shell. To do it, call: [source,erlang]
% With ascii pieces: binbo:print_board(Pid).
% With unicode pieces: binbo:print_board(Pid, [unicode]).
% Flipped board: binbo:print_board(Pid, [flip]). binbo:print_board(Pid, [unicode, flip]).
[[game-status]] === Game status
[source,erlang]
binbo:game_status(Pid) -> {ok, GameStatus} | {error, Reason}.
.where:
Pidis the the pid of the game process;GameStatusis the game status itself;Reasonis the reason why the game status cannot be obtained (usually due to the fact that the game is not initialized via link:#initializing-new-game[binbo:new_game/1,2]).
.The value of GameStatus:
continue- game in progress;{checkmate, white_wins}- White wins, Black checkmated;{checkmate, black_wins}- Black wins, White checkmated;{draw, stalemate}- draw because of stalemate;{draw, rule50}- draw according to the fifty-move rule;{draw, insufficient_material}- draw because of insufficient material;{draw, threefold_repetition}- draw according to the threefold repetition rule;{draw, {manual, WhyDraw}}- draw was set link:#setting-a-draw[manually] for the reason ofWhyDraw.{winner, Winner, {manual, WinnerReason}}- winnerWinnerwas set link:#setting-game-winner[manually] for the reason ofWinnerReason.
=== List of legal moves
[source,erlang]
binbo:all_legal_moves(Pid) -> {ok, Movelist} | {error, Reason}.
binbo:all_legal_moves(Pid, Movetype) -> {ok, Movelist} | {ok, Number} | {error, Reason}.
.where:
Pidis the pid of the game process;Movelistis a list of all legal moves for the current position. Each element ofMovelistis a tuple{From, To}or{From, To, Promo}, where: **FromandToare starting and target square respectively. **Promois one of the atoms:q,r,b,n(i.e. queen, rook, bishop, and knight respectively). Three-element tuple{From, To, Promo}occurs in case of pawn promotion.Movetypecan take on of the values:int,bin,str, orcount.
The call binbo:all_legal_moves(Pid) is the same as binbo:all_legal_moves(Pid, int).
If Movetype is count, the function returns tuple {ok, Number} where Number is the number of legal moves.
The values of From and To depend on Movetype as follows:
int: the values ofFromandToare integers in range0..63, namely, square indices. For example, the move fromA1toH8corresponds to{0, 63}. Useintto get the fastest reply from the game process.bin: the values ofFromandToare binaries. For example:{<<"e2">>, <<"e4">>}.str: the values ofFromandToare strings. For example:{"e2", "e4"}.
.Example: [source,erlang]
{ok, Pid} = binbo:new_server(). {ok,<0.212.0>}
%% Start new game from FEN that corresponds to Position 5 %% from Perft Results: https://www.chessprogramming.org/Perft_Results
binbo:new_game(Pid, <<"rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8">>). {ok,continue}
%% Count legal moves
binbo:all_legal_moves(Pid, count). {ok,44}
{ok, Movelist} = binbo:all_legal_moves(Pid). {ok,[{51,58,q}, {51,58,r}, {51,58,b}, {51,58,n}, {26,53}, {26,44}, {26,40}, {26,35}, {26,33}, {26,19}, {26,17}, {15,31}, {15,23}, {14,30}, {14,22}, {12,29}, {12,27}, {12,22}, {12,18}, {12,6}, {10,18}, {9,25}, {9,17}, {8,24}, {8,16}, {7,...}, {...}|...]}
%% Count moves:
erlang:length(Movelist). 44
binbo:all_legal_moves(Pid, bin). {ok,[{<<"d7">>,<<"c8">>,q}, {<<"d7">>,<<"c8">>,r}, {<<"d7">>,<<"c8">>,b}, {<<"d7">>,<<"c8">>,n}, {<<"c4">>,<<"f7">>}, {<<"c4">>,<<"e6">>}, {<<"c4">>,<<"a6">>}, {<<"c4">>,<<"d5">>}, {<<"c4">>,<<"b5">>}, {<<"c4">>,<<"d3">>}, {<<"c4">>,<<"b3">>}, {<<"h2">>,<<"h4">>}, {<<"h2">>,<<"h3">>}, {<<"g2">>,<<"g4">>}, {<<"g2">>,<<"g3">>}, {<<"e2">>,<<"f4">>}, {<<"e2">>,<<"d4">>}, {<<"e2">>,<<"g3">>}, {<<"e2">>,<<"c3">>}, {<<"e2">>,<<"g1">>}, {<<"c2">>,<<"c3">>}, {<<"b2">>,<<"b4">>}, {<<"b2">>,<<"b3">>}, {<<"a2">>,<<"a4">>}, {<<"a2">>,<<...>>}, {<<...>>,...}, {...}|...]}
binbo:all_legal_moves(Pid, str). {ok,[{"d7","c8",q}, {"d7","c8",r}, {"d7","c8",b}, {"d7","c8",n}, {"c4","f7"}, {"c4","e6"}, {"c4","a6"}, {"c4","d5"}, {"c4","b5"}, {"c4","d3"}, {"c4","b3"}, {"h2","h4"}, {"h2","h3"}, {"g2","g4"}, {"g2","g3"}, {"e2","f4"}, {"e2","d4"}, {"e2","g3"}, {"e2","c3"}, {"e2","g1"}, {"c2","c3"}, {"b2","b4"}, {"b2","b3"}, {"a2","a4"}, {"a2",[...]}, {[...],...}, {...}|...]}
=== Side to move
[source,erlang]
binbo:side_to_move(Pid) -> {ok, white | black} | {error, Reason}.
If White is to move, it returns {ok, white}. If Black is to move, it returns {ok, black}.
.Example: [source,erlang]
{ok, Pid} = binbo:new_server(). {ok,<0.232.0>}
binbo:new_game(Pid). {ok,continue}
binbo:side_to_move(Pid). % White is to move {ok,white}
binbo:move(Pid, <<"e2e4">>). {ok,continue}
binbo:side_to_move(Pid). % Black is to move now {ok,black}
[[game-state]] === Game state
[source,erlang]
binbo:game_state(Pid) -> GameState. binbo:set_game_state(Pid, GameState) -> {ok, GameStatus} | {error, Reason}.
.where:
Pidis the pid of the game process;GameStateis the whole game state.GameStatusis the link:#game-status[game status].
binbo:game_state/1 returns a raw game state, it may be useful when you want to save it somehow (e.g. into a database) and then restore it in the future with binbo:set_game_state(Pid, GameState). It's much faster than restoring game move by move incrementally.
.Example: [source,erlang]
{ok, Pid} = binbo:new_server(). {ok,<0.194.0>}
binbo:new_game(Pid). {ok,continue}
GameState = binbo:game_state(Pid). #{12 => 1,4 => 6,38 => 0,16 => 0,53 => 17,46 => 0,28 => 0, 23 => 0,lastmovepc => 0,59 => 21,58 => 19,bbenpa => 0, 30 => 0,40 => 0,47 => 0,24 => 0,27 => 0,21 => 0, bbwp => 65280,29 => 0,22 => 0,31 => 0,61 => 19,18 => 0, 54 => 17,5 => 3,14 => 1,51 => 17,57 => 18,...}
BinGame = erlang:term_to_binary(GameState). <<131,116,0,0,0,89,97,48,97,17,100,0,4,98,98,98,98,110,8, 0,0,0,0,0,0,0,0,36,100,...>>
binbo:set_game_state(Pid, erlang:binary_to_term(BinGame)). {ok,continue}
[[setting-a-draw]] === Setting a draw
It is possible to set a draw via API:
[source,erlang]
binbo:game_draw(Pid) -> ok | {error, Reason}. binbo:game_draw(Pid, WhyDraw) -> ok | {error, Reason}.
.where:
Pidis the pid of the game process;WhyDrawis the reason why a draw is to be set.
Calling binbo:game_draw(Pid) is the same as: binbo:game_draw(Pid, undefined).
.Example: [source,erlang]
% Players agreed to a draw:
binbo:game_draw(Pid, by_agreement). ok
% Trying to set a draw for the other reason:
binbo:game_draw(Pid, other_reason). {error,{already_has_status,{draw,{manual,by_agreement}}}}
[[setting-game-winner]] === Setting game winner
[source,erlang]
binbo:set_game_winner(Pid, Winner) -> ok | {error, Reason}. binbo:set_game_winner(Pid, Winner, WinnerReason) -> ok | {error, Reason}.
.where:
Pidis the pid of the game process;Winneris the winner, it can be any Erlang term (white,black,'Bobby Fischer', etc.);WinnerReasonis the reason why winner is to be set.
Calling binbo:set_game_winner(Pid, Winner) is the same as: binbo:set_game_winner(Pid, Winner, undefined).
.Example: [source,erlang]
% Black resigned
binbo:set_game_winner(Pid, white, black_resigned). ok
% Now the status of the game is: {winner,white,{manual,black_resigned}}
binbo:game_status(Pid). {ok,{winner,white,{manual,black_resigned}}}
% Trying to set the winner right after that (impossible):
binbo:set_game_winner(Pid, white, black_lost_on_time). {error,{already_has_status,{winner,white, {manual,black_resigned}}}}
=== Stopping game process
If, for some reason, you want to stop the game process and free resources, use:
[source,erlang]
binbo:stop_server(Pid) -> ok | {error, {not_pid, Pid}}.
Function terminates the game process with pid Pid.
=== Stopping application
To stop Binbo, call:
[source,erlang]
binbo:stop().
=== Using chess engines
You can write a chess bot application or play with engine using functions described in this section.
.Please note:
- Chess engine must support UCI protocol;
- Chess engine must be installed on the same machine where Binbo runs on.
Read the https://gist.github.com/DOBRO/2592c6dad754ba67e6dcaec8c90165bf[description of the Universal Chess Interface (UCI)] with examples for details.
[[start-new-game-with-engine]] ==== Start new game with engine
[source,erlang]
binbo:new_uci_game(Pid, Options) -> {ok, GameStatus} | {error, Reason}.
.Types: [source,erlang]
Pid :: pid().
Options :: #{ engine_path := EnginePath, fen => Fen }.
EnginePath :: binary() | string() | {TCPHost, TCPPort, timeout()}. TCPHost :: inet:socket_address() | inet:hostname(). TCPPort :: inet:port_number().
Fen :: binary() | string().
.where:
Pidis thepidof the process where the game is to be initialized;EnginePathis the full path to the engine's executable file (e.g./usr/local/bin/stockfish) or tuple{Host, Port, Timeout}for TCP connection;Fenis the Forsyth–Edwards Notation (FEN), defaults to initial if omitted;GameStatusis the link:#game-status[game status].
.Example: [source,erlang]
%% In the Erlang shell.
% Start new process for the game:
{ok, Pid} = binbo:new_server(). {ok,<0.185.0>}
% New game from the starting position:
binbo:new_uci_game(Pid, #{engine_path => "/usr/local/bin/stockfish"}). {ok,continue}
% New game with the given FEN:
binbo:new_uci_game(Pid, #{engine_path => "/usr/local/bin/stockfish", fen => <<"rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR b KQkq - 0 1">>}). {ok,continue}
[[search-for-the-best-move]] ==== Search for the best move
[source,erlang]
binbo:uci_bestmove(Pid) -> {ok, BestMove} | {error, Reason}. binbo:uci_bestmove(Pid, BestMoveOptions) -> {ok, BestMove} | {error, Reason}.
.Types: [source,erlang]
Pid :: pid(). BestMove :: binary() % e.g. <<"e2e4">>, <<"a7a8q">>, ...
BestMoveOptions :: #{
depth => pos_integer(), % depth (search x plies only)
wtime => non_neg_integer(), % wtime (white has x msec left on the clock)
btime => non_neg_integer(), % btime (black has x msec left on the clock)
winc => pos_integer(), % winc (white increment per move in mseconds if x > 0)
binc => pos_integer(), % binc (black increment per move in mseconds if x > 0)
movestogo => pos_integer(), % movestogo (there are x moves to the next time control, this will only be sent if x > 0, if you don't get this and get the wtime and btime it's sudden death)
nodes => pos_integer(), % nodes (search x nodes only)
movetime => pos_integer() % movetime (search exactly x mseconds)
}.
binbo:uci_bestmove(Pid) is the same as binbo:uci_bestmove(Pid, #{movetime => 1000}), it sends command go to the engine.
binbo:uci_bestmove(Pid, BestMoveOptions) sends command go ... to the engine adding values associated with the keys of BestMoveOptions.
For example, calling binbo:uci_bestmove(Pid, #{movetime => 2000, depth => 10}) means sending command go movetime 2000 depth 10 to the engine.
Note: the very important option is movetime, it tells the engine how long (in milliseconds) to search for the best move. Defaults to 1000 milliseconds.
Functions binbo:uci_bestmove/2,3 do NOT change the position on the board, they return the bestmove as a hint. To make moves and play with engine, use functions link:#binbo-uci-play-docs[binbo:uci_play/2,3].
.Example: [source,erlang]
%% In the Erlang shell.
% Start new process for the game:
{ok, Pid} = binbo:new_server(). {ok,<0.185.0>}
% New game with the given FEN:
binbo:new_uci_game(Pid, #{engine_path => "/usr/local/bin/stockfish", fen => <<"r1bqkbnr/pp1ppp1p/2n3p1/1Bp5/4P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 0 4">>}). {ok,continue}
% Search for the best move (no options given):
binbo:uci_bestmove(Pid). {ok,<<"e1g1">>}
% Search exactly 1000 milliseconds:
binbo:uci_bestmove(Pid, #{movetime => 1000}). {ok,<<"e1g1">>}
% Search for the best move at depth 10:
binbo:uci_bestmove(Pid, #{depth => 10}). {ok,<<"b5c6">>}
% Search exactly 5000 milliseconds at depth 30:
binbo:uci_bestmove(Pid, #{depth => 30, movetime => 5000}). {ok,<<"e1g1">>}
[[binbo-uci-play-docs]] ==== Play with engine, make moves
[source,erlang]
binbo:uci_play(Pid, BestMoveOptions) -> {ok, GameStatus, EngineMove} | {error, Reason}. binbo:uci_play(Pid, BestMoveOptions, YourMove) -> {ok, GameStatus, EngineMove} | {error, Reason}.
.where:
Pid-pidof the game process;BestMoveOptions- options for the best move the engine should search for, same as options for link:#search-for-the-best-move[binbo:uci_bestmove/2];EngineMove- move that was done by the engine;YourMove- your move to send to the engine before it makes its move, e.g.<<"e2e4">>,<<"a7a8q">>, …GameStatusis the link:#game-status[game status].
Function binbo:uci_play(Pid, BestMoveOptions) goes through the following steps:
- the engine searches for the bestmove (
EngineMove) from the current position; - the engine makes this move and changes its internal position;
- tuple
{ok, GameStatus, EngineMove}is returned.
The behaviour of function binbo:uci_play(Pid, BestMoveOptions, YourMove) is slightly different. Here are the steps it goes through:
- your move
YourMoveis sent to the engine; - the engine receives
YourMoveand changes its internal position; - the engine searches for the bestmove (
EngineMove) from the changed position; - the engine makes this move and changes its internal position;
- tuple
{ok, GameStatus, EngineMove}is returned.
See how to play with engine in the link:#quickstart-play-with-engine[example] from "Quick start" section.
==== Сhange position after the game is created
[source,erlang]
binbo:uci_set_position(Pid, Fen) -> {ok, GameStatus} | {error, Reason}.
.where:
Pid-pidof the game process;Fenis the Forsyth–Edwards Notation (FEN);GameStatusis the link:#game-status[game status].
Using this function you can change the position at any time. The game MUST be link:#start-new-game-with-engine[created] before.
.Example: [source,erlang]
%% In the Erlang shell.
% Start new process for the game:
{ok, Pid} = binbo:new_server(). {ok,<0.185.0>}
% Start new game from the initial position:
binbo:new_uci_game(Pid, #{engine_path => "/usr/local/bin/stockfish"}).
% Set up new position with the given FEN:
binbo:uci_set_position(Pid, <<"r1bqk1nr/ppppppb1/2n3p1/7p/2PP4/5NPP/PP2PP2/RNBQKB1R b KQkq - 2 5">>). {ok,continue}
==== Synchronize positions
[source,erlang]
binbo:uci_sync_position(Pid) -> ok | {error, Reason}.
.where:
Pid-pidof the game process.
It can be useful to call this function when the position of the game process was changed somehow and the engine wasn't notified about that.
.Example: [source,erlang]
%% In the Erlang shell.
% Start new process for the game:
{ok, Pid} = binbo:new_server(). {ok,<0.185.0>}
% Start new game from the initial position:
binbo:new_uci_game(Pid, #{engine_path => "/usr/local/bin/stockfish"}).
% Make move (the engine knows nothing about it):
binbo:move(Pid, "e2e4"). {ok,continue}
% Now synchronize the engine's position with the position of the game process:
binbo:uci_sync_position(Pid). ok
==== Send any command to engine
[source,erlang]
binbo:uci_command_call(Pid, Command) -> ok | {error, Reason}. binbo:uci_command_cast(Pid, Command) -> ok.
.where:
Pid-pidof the game process;Command- UCI command to send to the engine.
You can send any command to the engine with functions binbo:uci_command_call/2 and binbo:uci_command_cast/2.
binbo:uci_command_call/2 is a synchronous function, it calls https://erlang.org/doc/man/gen_server.html#call-2[gen_server:call/2] inside. Returns ok if Command is sent, or tuple {error, no_uci_connection} if the engine's process is not connected to the game process.
binbo:uci_command_cast/2 is an asynchronous function, it calls https://erlang.org/doc/man/gen_server.html#cast-2[gen_server:cast/2] inside. Returns ok. It also checks if the engine's process is connected to the game process before sending message and, if not connected, returns ok anyway.
.Example: [source,erlang]
%% In the Erlang shell.
% Start new process for the game:
{ok, Pid} = binbo:new_server(). {ok,<0.185.0>}
% Start new game:
binbo:new_uci_game(Pid, #{engine_path => "/usr/local/bin/stockfish"}). {ok,continue}
% Set hash to 32 MB (synchronous):
binbo:uci_command_call(Pid, "setoption name Hash value 32"). ok
% Set hash to 32 MB (asynchronous):
binbo:uci_command_cast(Pid, "setoption name Hash value 32"). ok
==== Handling messages from engine
[source,erlang]
binbo:set_uci_handler(Pid, Handler) -> ok.
.Types: [source,erlang]
Pid :: pid(). Handler :: undefined | default | fun().
.where:
Pid-pidof the game process;Handler- what to do with the message received from the engine.
If Handler is undefined, no operations are performed (the initial behaviour).
If Handler is set to default, function binbo_uci_protocol:default_handler/1 from module link:src/binbo_uci_protocol.erl[binbo_uci_protocol] is performed. It just prints the message to the Erlang shell.
If Handler is a function of arity 1, this function is performed. The only argument the function takes is the message received from the engine.
Note: all the messages received from the engine are of binary() type.
.Example with default handler: [source,erlang]
%% In the Erlang shell.
% Start new process for the game:
{ok, Pid} = binbo:new_server(). {ok,<0.185.0>}
% Start new game (no message handler):
binbo:new_uci_game(Pid, #{engine_path => "/usr/local/bin/stockfish"}). {ok,continue}
% Set default message handler:
binbo:set_uci_handler(Pid, default). ok
% Now start new game (with default message handler):
binbo:new_uci_game(Pid, #{engine_path => "/usr/local/bin/stockfish"}). {ok,continue}
.… and get the messages from the engine: [source]
--- UCI LOG BEGIN --- Stockfish 10 64 POPCNT by T. Romstad, M. Costalba, J. Kiiski, G. Linscott --- UCI LOG END ---
--- UCI LOG BEGIN --- id name Stockfish 10 64 POPCNT id author T. Romstad, M. Costalba, J. Kiiski, G. Linscott
option name Debug Log File type string default
option name Contempt type spin default 24 min -100 max 100
option name Analysis Contempt type combo default Both var Off var White var Black var Both
option name Threads type spin default 1 min 1 max 512
option name Hash type spin default 16 min 1 max 131072
option name Clear Hash type button
option name Ponder type check default false
option name MultiPV type spin default 1 min 1 max 500
option name Skill Level type spin default 20 min 0 max 20
option name Move Overhead type spin default 30 min 0 max 5000
option name Minimum Thinking Time type spin default 20 min 0 max 5000
option name Slow Mover type spin default 84 min 10 max 1000
option name nodestime type spin default 0 min 0 max 10000
option name UCI_Chess960 type check default false
option name UCI_AnalyseMode type check default false
option name SyzygyPath type string default
option name SyzygyProbeDepth type spin default 1 min 1 max 100
option name Syzygy50MoveRule type check default true
option name SyzygyProbeLimit type spin default 7 min 0 max 7
uciok
--- UCI LOG END ---
.Example with custom message handler: [source,erlang]
%% In the Erlang shell.
% Start new process for the game:
{ok, Pid} = binbo:new_server(). {ok,<0.185.0>}
% Start new game (no message handler):
binbo:new_uci_game(Pid, #{engine_path => "/usr/local/bin/stockfish"}). {ok,continue}
% Remember pid of the calling process:
SomePid = self(). <0.411.0>
% Set custom message handler as a function that resends messages to the process with pid SomePid:
binbo:set_uci_handler(Pid, fun(Message) -> SomePid ! Message end). ok
% Tell the engine to search for the bestmove:
binbo:uci_bestmove(Pid). {ok,<<"e2e4">>}
% Get the messages received:
flush(). Shell got <<"info depth 1 seldepth 1 multipv 1 score cp 116 nodes 20 nps 20000 tbhits 0 time 1 pv e2e4\n">> Shell got <<"info depth 2 seldepth 2 multipv 1 score cp 112 nodes 54 nps 54000 tbhits 0 time 1 pv e2e4 b7b6\n">> Shell got <<"info depth 3 seldepth 3 multipv 1 score cp 148 nodes 136 nps 136000 tbhits 0 time 1 pv d2d4 d7d6 e2e4\n">> Shell got <<"info depth 4 seldepth 4 multipv 1 score cp 137 nodes 247 nps 123500 tbhits 0 time 2 pv d2d4 e7e6 e2e4 c7c6\n">> Shell got <<"info depth 5 seldepth 5 multipv 1 score cp 77 nodes 1157 nps 385666 tbhits 0 time 3 pv c2c3 d7d5 d2d4 b8c6 c1g5\n">> Shell got <<"info depth 6 seldepth 6 multipv 1 score cp 83 nodes 2250 nps 562500 tbhits 0 time 4 pv e2e4 b8c6 d2d4 d7d6 f1c4 g8f6\n">> Shell got <<"info depth 7 seldepth 7 multipv 1 score cp 67 nodes 4481 nps 746833 tbhits 0 time 6 pv e2e4 e7e5 d2d4 e5d4 d1d4 b8c6 d4d1\n">> Shell got <<"info depth 8 seldepth 8 multipv 1 score cp 60 nodes 7849 nps 981125 tbhits 0 time 8 pv e2e4 e7e5 g1f3 d7d5 d2d4 b8c6 f3e5\n">> Shell got <<"info depth 9 seldepth 11 multipv 1 score cp 115 nodes 11846 nps 1184600 tbhits 0 time 10 pv e2e4 e7e5 g1f3 g8f6 b1c3\n">> Shell got <<"info depth 10 seldepth 10 multipv 1 score cp 106 upperbound nodes 14951 nps 1245916 tbhits 0 time 12 pv e2e4 d7d5\nbestmove e2e4 ponder d7d5\n">> ok
% Now turn the message handler off:
binbo:set_uci_handler(Pid, undefined). ok
=== Other helper functions
==== binbo:get_pieces_list/2
[source,erlang]
binbo:get_pieces_list(Pid, SquareType) -> {ok, PiecesList} | {error, Reason}.
.where:
Pid-pidof the game process;SquareTypeis one of the atoms:indexornotation;PiecesList- list of tuples{Square, Color, PieceType}: **Square- square index (0 .. 63) or notation (binary:<<"a1">>, ...,<<"h8">>) depending onSquareType; **Color-white|black; **PieceType-pawn|knight|bishop|rook|queen|king.
.Example: [source,erlang]
binbo:get_pieces_list(Pid, index). {ok,[{63,black,rook}, {62,black,knight}, {61,black,bishop}, {60,black,king}, {59,black,queen}, {58,black,bishop}, {57,black,knight}, {56,black,rook}, {55,black,pawn}, {54,black,pawn}, {53,black,pawn}, {52,black,pawn}, {51,black,pawn}, {50,black,pawn}, {49,black,pawn}, {48,black,pawn}, {15,white,pawn}, {14,white,pawn}, {13,white,pawn}, {12,white,pawn}, {11,white,pawn}, {10,white,pawn}, {9,white,pawn}, {8,white,pawn}, {7,white,...}, {6,...}, {...}|...]}
binbo:get_pieces_list(Pid, notation). {ok,[{<<"h8">>,black,rook}, {<<"g8">>,black,knight}, {<<"f8">>,black,bishop}, {<<"e8">>,black,king}, {<<"d8">>,black,queen}, {<<"c8">>,black,bishop}, {<<"b8">>,black,knight}, {<<"a8">>,black,rook}, {<<"h7">>,black,pawn}, {<<"g7">>,black,pawn}, {<<"f7">>,black,pawn}, {<<"e7">>,black,pawn}, {<<"d7">>,black,pawn}, {<<"c7">>,black,pawn}, {<<"b7">>,black,pawn}, {<<"a7">>,black,pawn}, {<<"h2">>,white,pawn}, {<<"g2">>,white,pawn}, {<<"f2">>,white,pawn}, {<<"e2">>,white,pawn}, {<<"d2">>,white,pawn}, {<<"c2">>,white,pawn}, {<<"b2">>,white,pawn}, {<<"a2">>,white,pawn}, {<<"h1">>,white,...}, {<<...>>,...}, {...}|...]}
== Building and testing
Two possible ways are presented here for building and testing the application (with make and rebar3).
=== Building
[source,bash]
$ make
[source,bash]
$ rebar3 compile
=== Dialyzer
[source,bash]
$ make dialyze
[source,bash]
$ rebar3 dialyzer
=== Testing
[source,bash]
$ make test
$ export BINBO_UCI_ENGINE_PATH="/path/to/engine" $ export BINBO_UCI_ENGINE_HOST=localhost $ export BINBO_UCI_ENGINE_HOST=9010 $ make test
[source,bash]
$ rebar3 ct --verbose
$ export BINBO_UCI_ENGINE_PATH="/path/to/engine" $ export BINBO_UCI_ENGINE_HOST=localhost $ export BINBO_UCI_ENGINE_HOST=9010 $ rebar3 ct --verbose
=== Code coverage
[source,bash]
$ make cover
[source,bash]
$ rebar3 cover
=== Generating Edoc files
[source,bash]
$ make docs
[source,bash]
$ rebar3 edoc
== Binbo and Magic Bitboards
As mentioned above, Binbo uses https://www.chessprogramming.org/Magic_Bitboards[Magic Bitboards], the fastest solution for move generation of sliding pieces (rook, bishop, and queen). Good explanations of this approach can also be found https://stackoverflow.com/questions/16925204/sliding-move-generation-using-magic-bitboard/30862064#30862064[here] and http://vicki-chess.blogspot.com/2013/04/magics.html[here].
The main problem is to find the index which is then used to lookup legal moves of sliding pieces in a preinitialized move database. The formula for the index is:
.in C/C++: [source,c]
magic_index = ((occupied & mask) * magic_number) >> shift;
.in Erlang: [source,erlang]
MagicIndex = (((Occupied band Mask) * MagicNumber) bsr Shift).
.where:
Occupiedis the bitboard of all pieces.Maskis the attack mask of a piece for a given square.MagicNumberis the magic number, see "https://www.chessprogramming.org/Looking_for_Magics[Looking for Magics]".Shift = (64 - Bits), whereBitsis the number of bits corresponding to attack mask of a given square.
All values for magic numbers and shifts are precalculated before and stored in binbo_magic.hrl.
To be accurate, Binbo uses https://www.chessprogramming.org/Magic_Bitboards#Fancy[Fancy Magic Bitboards]. It means that all moves are stored in a table of its own (individual) size for each square. In C/C++ such tables are actually two-dimensional arrays and any move can be accessed by a simple lookup:
[source,c]
move = global_move_table[square][magic_index]
.If detailed: [source,c]
moves_from = global_move_table[square]; move = moves_from[magic_index];
The size of moves_from table depends on piece and square where it is placed on. For example:
- for rook on
A1the size ofmoves_fromis4096(2^12 = 4096, 12 bits required for the attack mask); - for bishop on
A1it is64(2^6 = 64, 6 bits required for the attack mask).
There are no two-dimensional arrays in Erlang, and no global variables which could help us to get the fast access to the move tables from everywhere.
So, how does Binbo beat this? Well, it's simple :).
Erlang gives us the power of tuples and maps with their blazing fast lookup of elements/values by their index/key.
Since the number of squares on the chessboard is the constant value (it's always 64, right?),
our global_move_table can be constructed as a tuple of 64 elements, and each element of this tuple
is a map containing the key-value association as MagicIndex => Moves.
.If detailed, for moves: [source,erlang]
GlobalMovesTable = { MoveMap1, ..., MoveMap64 }
.where: [source,erlang]
MoveMap1 = #{ MagicIndex_1_1 => Moves_1_1, ... MagicIndex_1_K => Moves_1_K }, MoveMap64 = #{ MagicIndex_64_1 => Moves_64_1, ... ... MagicIndex_64_N => Moves_64_N },
and then we lookup legal moves from a square, say, E4 (29th element of the tuple):
[source,erlang]
E4 = 29, MoveMapE4 = erlang:element(E4, GlobalMovesTable), MovesFromE4 = maps:get(MagicIndex, MovesMapE4).
To calculate magic index we also need the attack mask for a given square. Every attack mask generated is stored in a tuple of 64 elements:
[source,erlang]
GlobalMaskTable = {Mask1, Mask2, ..., Mask64}
where Mask1, Mask2, ..., Mask64 are bitboards (integers).
Finally, if we need to get all moves from E4:
[source,erlang]
E4 = 29, Mask = erlang:element(E4, GlobalMaskTable), MagicIndex = ((Occupied band Mask) * MagicNumber) bsr Shift, MoveMapE4 = erlang:element(E4, GlobalMovesTable), MovesFromE4 = maps:get(MagicIndex, MovesMapE4).
Next, no global variables? We make them global!
How do we get the fastest access to the move tables and to the attack masks from everywhere?
http://erlang.org/doc/man/ets.html[ETS]? No! Using ETS as a storage for static terms we get the overhead due to extra data copying during lookup.
And now we are coming to the fastest solution.
When Binbo starts up, all move tables are initialized.
Once these tables (tuples, actually) initialized, they are "injected" into dynamically generated
modules compiled at Binbo start. Then, to get the values, we just call a getter function
(binbo_global:get/1) with the argument as the name of the corresponding dynamic module.
This awesome trick is used in MochiWeb library, see module https://github.com/mochi/mochiweb/blob/master/src/mochiglobal.erl[mochiglobal].
Using http://erlang.org/doc/man/persistent_term.html[persistent_term] (since OTP 21.2) for storing static data is also a good idea.
But it doesn't seem to be a better way for the following reason with respect to dynamic modules.
When Binbo stops, it gets them unloaded as they are not necessary anymore.
It should do the similar things for persistent_term data, say, delete all unused
terms to free memory.
In this case we run into the issue regarding scanning the heaps in all processes.
So, using global dynamic modules with large static data seems to be more reasonable in spite of that fact that it significantly slows down the application startup due to the run-time compilation of these modules.
== Changelog
See link:CHANGELOG.md[CHANGELOG] for details.
== Contributing
Want to contribute? Really? Awesome!
Please refer to the link:CONTRIBUTING.md[CONTRIBUTING] file for details.
== License
This project is licensed under the terms of the Apache License, Version 2.0.
See the link:LICENSE[LICENSE] file for details.