README.adoc

= 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://travis-ci.org/DOBRO/binbo.svg?branch=master["Build Status", link="https://travis-ci.org/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 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 **bin**ary **bo**ard 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.

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}♚
* Ready for use on game servers.

== Requirements

** https://www.erlang.org/[Erlang/OTP] 20.0 or higher.
** https://www.rebar3.org/[rebar3]

== Quick start

Run `make shell` or `rebar3 shell` and then:

[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]).
----

== 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()}.
----

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().
----

[[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:
* `Pid` is the `pid` of the process where the game is to be initialized;
* `Fen` (`string()` or `binary()`) is the https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation[Forsyth–Edwards Notation] (FEN);
* `GameStatus` is the link:#game-status[game status].

It is possible to reinitilize 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 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}.
----

where:

* `Pid` is the pid of the game process;
* `Move` is of `binary()` or `string()` type;
* `GameStatus` is the link:#game-status[game status].

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.

.Examples for `binbo:move/2`:
[source,erlang]
----
%% In 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}
----

.Examples for `binbo:san_move/2`:
[source,erlang]
----
%% In 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}
----

==== Castling

Binbo recognizes https://en.wikipedia.org/wiki/Castling[castling] when:

* White king moves from `E1` to `G1` (`O-O`);
* White king moves from `E1` to `C1` (`O-O-O`);
* Black king moves from `E8` to `G8` (`O-O`);
* Black king moves from `E8` to `C8` (`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 7` to square of `rank 8`;
* Black pawn moves from square of `rank 2` to square of `rank 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:
* `Pid` is the pid of the game process;
* `PGN` is a https://en.wikipedia.org/wiki/Portable_Game_Notation[Portable Game Notation], its type is `binary()`;
* `Filename` is a path to the file from which PGN is to be loaded. Its type is `binary()` or `string()`.
* `GameStatus` is 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:
* `Pid` is the the pid of the game process;
* `GameStatus` is the game status itself;
* `Reason` is 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` - one of the sides (White or Black) 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 of `WhyDraw`.

=== List of legal moves

[source,erlang]
----
binbo:all_legal_moves(Pid) -> {ok, Movelist} | {error, Reason}.

binbo:all_legal_moves(Pid, Movetype) -> {ok, Movelist} | {error, Reason}.
----

.where:
* `Pid` is the pid of the game process;
* `Movelist` is a list of all legal moves for the current position. Each element of `Movelist` is a tuple `{From, To}` or `{From, To, Promo}`, where:
** `From` and `To` are starting and target square respectively.
** `Promo` is 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*.
* `Movetype` can take on of the values: `int`, `bin`, or `str`.

The call `binbo:all_legal_moves(Pid)` is the same as `binbo:all_legal_moves(Pid, int)`.

The values of `From` and `To` depend on `Movetype` as follows:

* `int`: the values of `From` and `To` are _integers_ in range `0..63`, namely, square indices. For example, the move from `A1` to `H8` corresponds to `{0, 63}`. Use `int` to get the *fastest* reply from the game process.
* `bin`: the values of `From` and `To` are _binaries_. For example: `{<<"e2">>, <<"e4">>}`.
* `str`: the values of `From` and `To` are _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}

> {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",[...]},
     {[...],...},
     {...}|...]}

----

[[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:
* `Pid` is the pid of the game process;
* `WhyDraw` is 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}}}}
----

=== 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().
----

== 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
----

[source,bash]
----
$ 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 aproach 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:_
* `Occupied` is the bitboard of all pieces.
* `Mask` is the attack mask of a piece for a given square.
* `MagicNumber` is the magic number, see &quot;https://www.chessprogramming.org/Looking_for_Magics[Looking for Magics]&quot;.
* `Shift = (64 - Bits)`, where `Bits` is 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 `A1` the size of `moves_from` is `4096` (2^12 = 4096, 12 bits requred for the attack mask);
* for bishop on `A1` it is `64` (2^6 = 64, 6 bits requred 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 :&#41;.

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 =&gt; 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 _atack 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.