### A Pluto.jl notebook ###
# v0.14.5
using Markdown
using InteractiveUtils
# ╔═╡ eb038b1a-b2af-49a2-b506-9315df701f47
using Chess
# ╔═╡ f30458ad-89ea-4928-a4e0-4ebff355c573
using PlutoUI # For `with_terminal`.
# ╔═╡ e89bfaed-22da-4f99-aec2-989595e2eff3
using Chess.PGN
# ╔═╡ b8476885-214a-4708-b933-4aac278f3b5b
using Chess.Book
# ╔═╡ f0cf3415-571f-4661-8823-2598aef00c2d
using Chess.UCI
# ╔═╡ a6d99974-5c03-4bf6-b6af-4ccf76d1b9e9
md"# Chess.jl Tutorial"
# ╔═╡ b9d53c2d-faa1-4a44-8b8c-bc62c5237797
md"""
## Table of Contents
* [Introduction](#introduction)
* [Boards](#boards)
+ [Creating Boards](#creating_boards)
+ [Making and Unmaking Moves](#making_and_unmaking_moves)
* [Pieces, Piece Colors, and Piece Types](#pieces_piece_colors_and_piece_types)
* [Squares](#squares)
* [Moves](#moves)
* [Square Sets](#square_sets)
+ [Creating Square Sets](#creating_square_sets)
+ [Extracting Square Sets From Boards](#extracting_square_sets_from_boards)
+ [Set Operations](#set_operations)
+ [Iterating Through Square Sets](#iterating_through_square_sets)
+ [Attack Square Sets](#attack_square_sets)
* [Games](#games)
+ [Creating Games and Adding Moves](#creating_games_and_adding_moves)
+ [Example: Generating Random Games](#example_generating_random_games)
+ [Opening Games in Lichess](#opening_games_in_lichess)
+ [Variations](#variations)
+ [Comments](#comments)
+ [Numeric Annotation Glyphs](#numeric_annotation_glyphs)
* [PGN Import and Export](#pgn_import_and_export)
+ [Creating a Game From a PGN String](#creating_a_game_from_a_pgn_string)
+ [Working With PGN Files](#working_with_pgn_files)
* [Opening Books](#opening_books)
+ [Finding Book Moves](#finding_book_moves)
+ [Example: Playing Random Openings](#example_playing_random_openings)
* [Interacting With UCI Chess Engines](#interacting_with_uci_chess_engines)
+ [Starting and Initializing Engines](#starting_and_initializing_engines)
+ [Searching](#searching)
+ [Parsing Search Output](#parsing_search_output)
+ [Example: Engine vs Engine Games](#example_engine_vs_engine_games)
"""
# ╔═╡ 2ef8d62b-c32f-486f-a2e5-c82ed0418170
md"""
## $(html"") Introduction
Chess.jl is a library for doing computer chess in Julia. It contains utilities for creating and manipulating chess positions and games, reading and writing chess games in the popular [PGN](https://en.wikipedia.org/wiki/Portable_Game_Notation) format (including support for comments and variations), for creating opening trees, and for interacting with [UCI chess engines](http://wbec-ridderkerk.nl/html/UCIProtocol.html).
The library should be suitable for most chess programming tasks, except perhaps for trying to write the strongest possible chess engine. Writing a strong chess engine using Chess.jl is certainly possible, and reaching super-human strength shouldn't be too hard, but for maximum performance, a lower-level language like C, C++, Rust or Nim would probably be better.
Most of the functions described in this tutorial are located in the `Chess` module:
"""
# ╔═╡ 563c93f0-721d-4d93-8530-3d219f197bb5
md"""
## $(html"") Boards
### $(html"") Creating boards
A chess board is represented by the `Board` type. A board is usually obtained in one of five ways:
1. By calling the `startboard()` function, which returns a board initialized to the standard chess opening position.
2. By using the `@startboard` macro, which allows you to provide a sequence of moves from the starting position.
3. By calling the `fromfen()` function, which takes a board string in [Forsyth-Edwards Notation](https://en.wikipedia.org/wiki/Forsyth–Edwards_Notation) and returns the corresponding board.
4. By making a move or a sequence of moves on an existing chess board, using a function like `domove()` or `domoves()`.
5. By calling the `board()` function on a `Game` or a `SimpleGame`, obtaining the current board position in a game. See the section on games later in this tutorial for a discussion of these types)
Let's begin with the most basic way of creating a chess board: The `startboard()` function.
"""
# ╔═╡ fe7e2e16-6e57-4ab0-92cb-be3465c5bcab
startboard()
# ╔═╡ 861c5b57-7f28-4d59-800b-eeb68a1223b0
md"""The "open in lichess" link will open the board in the popular chess site [lichess](https://lichess.org). Lichess is the best place to play or study chess on the Internet, and best of all, it is completely free!
Sometimes you want to set up a board position by making some moves from the starting position. You could do this by first calling `startboard()` and then calling the `domoves()` or `domoves!()` function (more about those later in this tutorial), but that quickly becomes tedious for interactive use. The `@startboard` macro can be used as a convenient shortcut:
"""
# ╔═╡ 9c7d61a6-81b9-4674-bcee-c86023fa3dc0
@startboard e4 e5 Nf3 Nc6 Bb5
# ╔═╡ f49a0b6f-4283-4e9c-9570-a37d71f653f9
md"""
Annoyingly, the minus sign in standard castling notation (`O-O` for kingside castling and `O-O-O` for queenside castling) confuses Julia's parser. For castling moves, just skip the minus sign and write `OO` or `OOO`, as in the following example.
"""
# ╔═╡ aee3fa17-44b8-41c4-ba6e-aaca43081157
@startboard e4 c5 Nf3 d6 d4 cxd4 Nxd4 Nf6 Nc3 g6 Be3 Bg7 f3 OO Qd2 Nc6 OOO
# ╔═╡ 293a3b35-3466-4d11-b116-8cc96fade2a9
md"""
Setting up an arbitrary board position without entering a move sequence can be done with the `fromfen()` function:
"""
# ╔═╡ 0c12848b-5892-425f-8ed2-fd00c5e3a4db
fromfen("5rk1/p1pb2pp/2p5/3p3q/2P3n1/1Q4BN/PP1Np1KP/R3R3 b - -")
# ╔═╡ b4cfb722-7f8c-4106-9cc3-5fcefb8d2233
md"""
FEN strings are quite easy to understand. The first component (`5rk1/p1pb2pp/2p5/3p3q/2P3n1/1Q4BN/PP1Np1KP/R3R3` in the above example) is the board setup. The ranks of the board are listed from top to bottom (beginning with rank 8), separated by the `/` character. For each rank, lowercase letters (p, n, b, r, q or k) denote black pieces, while uppercase letters (P, N, B, R, Q or K) denote white pieces. Digits represents empty squares. In the above example, the 8th rank is `5rk1`, meaning five empty squares followed by a black rook and a black king, and finally one empty square.
The second component (`b` in the above example) is the side to move. It is always one of the two characters `w` or `b`, depending on the side to move. In this case, it's black.
The third component (`-` in the example) is the current castle rights. The dash means that neither side has the right to castle. If one or both sides still have the right to castle, the letters `K`, `Q`, `k` and `q` are used. The uppercase letters mean that white can castle kingside or queenside, while the lowercase letters mean that black can castle. For instance, in a position when both sides can still castle in either direction, the third component would be `KQkq`. In a position where white can only castle queenside and black only kingside, it would be `Qk`.
The fourth coponent (`-` in the example) is the square on which an en passant capture is possible. The dash means that no en passant capture is possible in our case. If an en passant capture had been possible on e3, the fourth component would have been `e3`.
For additional examples and explanations, visit the [Wikipedia article on FEN strings](https://en.wikipedia.org/wiki/Forsyth–Edwards_Notation).
"""
# ╔═╡ 967d3055-45d9-4c77-b0c2-7d514a407278
md"""
### $(html"")Making and Unmaking Moves
Given a chess board, you will often want to modify the board by making some moves. The most straightforward way to do this is with the `domove` function, which takes two parameters: A chess board and a move. The move can be either a value of the `Move` type or a string representing a move in UCI or SAN notation.
Here's an example of using `domove` to make a move given by a string in short algebraic notation (SAN):
"""
# ╔═╡ 7dd7fa6e-717b-4ba1-a110-987a4cb53a5b
begin
local b = startboard()
domove(b, "d4")
end
# ╔═╡ be9065b3-3040-4704-a6cd-6deb7060a0c1
md"""
There is also a function `domoves` that takes a sequence of several moves and executes all of them:
"""
# ╔═╡ b5f6826e-3855-4d41-8ce0-27587dfa598b
begin
local b = startboard()
domoves(b, "Nf3", "d5", "c4")
end
# ╔═╡ 633a0f4c-6e5f-40fb-8373-6dd25d8fda6a
md"""
Note that both of these functions return new boards: The original board is left unchanged, as illustrated by this example:
"""
# ╔═╡ 43eb49ed-afba-4850-9b3d-2e36fc800eff
begin
local b = startboard()
domove(b, "c4")
b
end
# ╔═╡ 240dde91-ca7b-4078-a9da-5af8ca72b210
md"""
This is convenient when writing code in a functional style, or when using a reactive notebook environment like Pluto. Unfortunately, it also results in a lot of copying of data, and heap allocations that may have signifcant performance impacts for certain types of applications. When this is a problem, there are alternative functions `domove!` and `domoves!` that destructively modify the input board.
Here is the result of the previous example when modified to use `domove!`:
"""
# ╔═╡ d56d75c2-879f-466f-a634-8f7a19ff389a
begin
local b = startboard()
domove!(b, "c4")
b
end
# ╔═╡ ccd6feec-a51a-4a6e-ad84-95d215604e9a
md"""
`domove!` returns a value of type `UndoInfo`. This can be used to undo the move and go back to the board position before the move was made.
"""
# ╔═╡ ba204c76-7f08-47ec-b64e-9b28e16ce141
begin
local b = startboard()
local u = domove!(b, "e4")
undomove!(b, u)
b
end
# ╔═╡ 917de419-d7b0-4d61-a897-5bcb012b8e2e
md"""
There is also a function `domoves!()` that can be used to destructively update a board with a sequence of several moves. Unlike `domove!`, this operation is irreversible. No `UndoInfo` is generated, and there is no way to undo the moves and return to the original board.
"""
# ╔═╡ bb906070-43a8-4b5c-a9e5-b248f12b0b89
begin
local b = startboard()
domoves!(b, "d4", "Nf6", "c4", "g6", "Nc3", "Bg7", "e4", "d6", "Nf3", "O-O")
b
end
# ╔═╡ d70b1d38-c897-4b7c-9fa4-7c19ca06e553
md"""
Remember that there is also a macro `@startboard` that allows you to do this more conveniently. The above example could also be written like this:
"""
# ╔═╡ 528d6ca6-7b33-4dcf-8738-90260915c699
@startboard d4 Nf6 c4 g6 Nc3 Bg7 e4 d6 Nf3 OO
# ╔═╡ dd5a8f77-8af5-41ea-936f-49b36ee22f54
md"""
## $(html"")Pieces, Piece Colors, and Piece Types
Chess pieces are represented by the `Piece` type (internally, a simple wrapper around an integer). There are constants `PIECE_WP`, `PIECE_WN`, `PIECE_WB`, `PIECE_WR`, `PIECE_WQ`, `PIECE_WK`, `PIECE_BP`, `PIECE_BN`, `PIECE_BB`, `PIECE_BR`, `PIECE_BQ` and `PIECE_BK` for each of the possible white or black pieces, and a special piece value `EMPTY` for the contents of an empty square on the board.
There are also *piece colors*, represented by the `PieceColor` type (possible values `WHITE`, `BLACK` and `COLOR_NONE`), as well as *piece types*, represented by the `PieceType` type (possible values `PAWN`, `KNIGHT`, `BISHOP`, `ROOK`, `QUEEN`, `KING` and `PIECE_TYPE_NONE`).
Given a piece, you can ask for its color and type by using `pcolor` and `ptype`:
"""
# ╔═╡ 0f257004-8605-4b14-b255-ca0e231ae406
pcolor(PIECE_BN)
# ╔═╡ 4d60d4da-ae36-4784-80ca-bac8aebc1933
ptype(PIECE_BN)
# ╔═╡ a310bac6-080a-4ada-acc1-f423843a9497
md"""
Conversely, if you have a `PieceColor` and a `PieceType`, you can create a `Piece` value by calling the `Piece` constructor:
"""
# ╔═╡ a400d50d-02d5-4a52-ad76-f22d86d5e332
Piece(WHITE, ROOK)
# ╔═╡ 2ca00e38-f7b9-4c0b-be0e-7b92898944ca
md"""
The special `Piece` value `EMPTY` has piece color `COLOR_NONE` and piece type `PIECE_TYPE_NONE`:
"""
# ╔═╡ 93e07389-1624-4530-95fc-b5a51a926fc3
pcolor(EMPTY)
# ╔═╡ caea759c-a63d-43d5-a87f-70441ef77aba
ptype(EMPTY)
# ╔═╡ 4877f578-96d6-44fc-a9c5-cf431f274303
md"""
The current side to move of a board is obtained by calling `sidetomove`:
"""
# ╔═╡ fb2aed2e-aa35-4d7d-92f3-5236075be26a
sidetomove(startboard())
# ╔═╡ 60672e37-4ad8-41dc-9854-344fd40362f6
sidetomove(@startboard Nf3)
# ╔═╡ a7ba9a08-a508-4771-a411-5d564fbcad22
md"""
Use the unary minus operator or the function `coloropp` to invert a color:
"""
# ╔═╡ d06dc3a1-a8ef-4f09-8538-e4ba25efca64
-WHITE
# ╔═╡ aabdade4-3ec7-4832-8bf7-7285d0658361
coloropp(BLACK)
# ╔═╡ 9db8e449-f98d-4735-9c49-2d369aed0b6e
md"""
## $(html"")Squares
Squares are represented by the `Square` data type. Just as for pieces, piece colors, and piece types, this type is internally just a simple wrapper around an integer. There are constants `SQ_A1`, `SQ_A2`, ..., `SQ_H8` for the 64 squares of the board.
One of the common uses of `Square` values is to ask about the contents of a square on a chess board. This is done with the `pieceon` function:
"""
# ╔═╡ cfdef501-7c9e-4662-80c4-54441bd093c1
pieceon(startboard(), SQ_B1)
# ╔═╡ 29e63f8c-980a-44d3-96d8-4728ba558ac2
pieceon(startboard(), SQ_E8)
# ╔═╡ 20382e3f-6b31-4ba2-b07f-641dcc73fa71
pieceon(startboard(), SQ_A3)
# ╔═╡ 2f8050ef-6faa-4150-8221-5714e94bd2e1
md"""
There are also two types `SquareFile` and `SquareRank` for representing the files and ranks of a board. Given a square, we can get its file or rank by calling `file` or `rank`:
"""
# ╔═╡ 0d8e77f3-dd7b-40c1-b9af-18d5883009a9
file(SQ_E5)
# ╔═╡ 09674208-60c5-4fad-83af-ffb15d8f2f91
rank(SQ_E5)
# ╔═╡ 13cc3c48-9690-4b91-a253-c7a355c1a40e
md"""
Conversely, it is possible to create a `Square` from a `SquareFile` and a `SquareRank`:
"""
# ╔═╡ 47b748d5-c1a4-41ca-99f4-51fb216db311
Square(FILE_C, RANK_4)
# ╔═╡ 6e0451c3-c68a-4783-a1a0-87c69b321bd7
md"""
We can use the functions `tostring` and `squarefromstring` to convert between `Square` values and strings:
"""
# ╔═╡ 51a76d49-d8b9-46b0-8908-08e86b1640f0
tostring(SQ_D4)
# ╔═╡ 7436bddb-53e2-4044-a2df-a63d8cb5ba5d
squarefromstring("g6")
# ╔═╡ eb6edb9c-c4b3-4dc0-b1ac-9bc5817a1149
md"""
## $(html"") Moves
Moves are represented by the type `Move`. A `Move` value can be obtained by calling one of two possible constructors:
"""
# ╔═╡ 3e1544e5-1a68-419e-8039-99afc55c3836
# Normal move
Move(SQ_E2, SQ_E4)
# ╔═╡ 53ac9f47-f66a-4329-aa16-61dcbe4be365
# Promotion move
Move(SQ_A7, SQ_A8, QUEEN)
# ╔═╡ 67cdc5dd-865d-4bb2-b130-adf7c2e874a5
md"""
We can also convert a move to/from strings in UCI notation:
"""
# ╔═╡ 31cfa4be-8af4-4db5-a183-7f7828f8a15b
tostring(Move(SQ_G8, SQ_F6))
# ╔═╡ a55b94dc-a9d1-4749-b03f-294e2371e169
movefromstring("b2c1r")
# ╔═╡ c03f1d2c-544f-47eb-89d0-fbcb3143524d
md"""
Parsing move strings in short algebraic notation (SAN) requires a board. Without
a board, there is no way to know the source square of a move string like `"Nf3"`. Given a board, we can convert to/from SAN move strings using `movetosan` and `movefromsan`:
"""
# ╔═╡ aba22fd4-c0aa-4d93-863f-fc571396e7c3
movetosan(startboard(), Move(SQ_G1, SQ_F3))
# ╔═╡ e1266ab4-d5ae-4414-bd4b-0113c1875cd6
movefromsan(startboard(), "e4")
# ╔═╡ bd80c9f7-1ddb-47a7-a4f5-01bae805e8cd
md"""
One of the most common ways to obtain a move is to call the `moves` function on a board. This returns a `MoveList`, a list of all legal moves for the board:
"""
# ╔═╡ fb57388a-b5eb-4383-b2ce-38b6e10afb01
begin
b = @startboard d4 Nf6 c4 e6 Nc3 Bb4
moves(b)
end
# ╔═╡ 2730adae-23b3-41f0-856a-a46813465208
md"""
## $(html"") Square Sets
The `SquareSet` type represents a set of squares on the chess board. We can do set-theoretic operations like union, intersection and complement on square sets, and test for set membership. Internally, a `SquareSet` is represented by a 64-bit integer, with set operations performed through bitwise operations. This makes square sets very fast to manipulate.
"""
# ╔═╡ e2c56253-8745-4262-86d4-be83ab7908ca
md"""
### $(html"") Creating Square Sets
There is a `SquareSet` constructor that takes a sequence of squares as input and returns the corresponding square set:
"""
# ╔═╡ 9c8b289b-5cda-4578-8d99-6311322c0e98
SquareSet(SQ_A1, SQ_A2, SQ_A3)
# ╔═╡ 5e193e21-457c-4bb2-bd51-8e8d131891cc
md"""
There are also pre-defined constants `SS_FILE_A`, ..., `SS_FILE_H` for the eight files of the board, and `SS_RANK_1`, ..., `SS_RANK_8` for the eight ranks.
"""
# ╔═╡ ff444a68-e964-47a7-bbf4-2061f85ffb5b
SS_FILE_B
# ╔═╡ 37d27b7a-1e57-4a73-a1d3-04b54add087a
SS_RANK_6
# ╔═╡ 9e14663e-89cb-4445-9eb3-f3ac9dc84d36
md"""
### $(html"") Extracting Square Sets From Boards
Given a `Board` value, there are several functions for obtaining various square sets. The `pieces` function has several methods for extracting sets of squares occupied by various pieces.
The squares occupied by white pieces:
"""
# ╔═╡ 142b4958-449f-4983-afe6-cee5ecb17ae5
pieces(startboard(), WHITE)
# ╔═╡ 4ef7bd67-e834-422a-baa0-2f3870a976d9
md"The set of all squares occupied by pawns of either color (you can also do `pawns(startboard())` with the same effect):"
# ╔═╡ 82a738f6-e3d3-45a3-91ae-90dd3568834b
pieces(startboard(), PAWN)
# ╔═╡ a376190c-69d0-4f47-9b48-fdaa77ffb57b
md"""
The set of squares occupied by black knights (you can also do `knights(startboard(), BLACK)`:
"""
# ╔═╡ d97d771d-6d61-4dbd-b224-0093757b3c5c
pieces(startboard(), PIECE_BN)
# ╔═╡ 42ff96a4-fc48-47f8-b2c2-129a96f6821e
md"The set of all occupied squares on the board:"
# ╔═╡ 245f5586-c8dc-4d61-8d7c-d557d228a9f7
occupiedsquares(startboard())
# ╔═╡ a8df6297-1fdb-4b46-8410-d7e268c6fc1c
md"""
The set of all empty squares on the board:
"""
# ╔═╡ 42db7246-f3a1-4d1d-a6d0-4fec13c3c2c2
emptysquares(startboard())
# ╔═╡ 07f3796a-2fa9-4dc0-b32a-26c834e55716
md"""
### $(html"") Set Operations
It is possible to do various basic set theoretic operations likecomplement, union, increment, and membership tests on square sets, using standard mathematical notation. This sections gives a few examples.
Set membership tests (type `\in ` and `\notin ` for the `∈` and `∉` characters):
"""
# ╔═╡ fed01377-5252-40b6-9e0f-a6034b3ef62d
SQ_D1 ∈ SS_FILE_D
# ╔═╡ 7dbd5b17-cc63-4464-9d95-cf87ed172a0b
SQ_D1 ∈ SS_RANK_2
# ╔═╡ 1971c152-3f24-4934-bb94-d0d9567f9024
SQ_E4 ∉ SS_RANK_8
# ╔═╡ 0cc94af0-759a-4399-b21f-5e1d7e278683
md"Set complement:"
# ╔═╡ 4e00a4e7-e615-4a0d-bdd8-64a7aefa6e93
-SS_RANK_4
# ╔═╡ 512ba223-92bf-4ed6-adf1-86744ebb5712
md"""
Set union (type `\cup ` for the `∪` character):
"""
# ╔═╡ 061ac99b-8391-4a72-af14-d35dcabd063d
SS_RANK_2 ∪ SS_FILE_F
# ╔═╡ 5baa21b3-f0c3-4f63-b474-527d6820059a
md"""
Set intersection (type `\cap ` for the `∩` character):
"""
# ╔═╡ e8923543-8e52-48e0-a427-cd60494fc258
SS_FILE_D ∩ SquareSet(SQ_D4, SQ_D5, SQ_E4, SQ_E5)
# ╔═╡ 51b64306-e90b-409b-9472-f542948d10ec
md"Set subtraction:"
# ╔═╡ 243faa91-d14e-4224-9c50-054536f1fc7c
SS_FILE_G - (SS_RANK_3 ∪ SS_RANK_4)
# ╔═╡ 3431451d-9440-4918-8018-41c5a2a077d8
md"""
### $(html"") Iterating Through Square Sets
The `squares` function can be used to convert a `SquareSet` to a vector of squares:
"""
# ╔═╡ ef447813-6405-475c-ae2d-427fcc6b38a1
squares(SS_FILE_A)
# ╔═╡ e5cbc125-f314-49d1-8c4e-6db26b77ff6d
md"""
The `squares` function is not necessary for most tasks. It is possible – and much more efficient – to iterate through a `SquareSet` directly:
"""
# ╔═╡ aafe9561-3af4-43ff-a069-02832b18997d
with_terminal() do
for s ∈ SS_RANK_5
println(tostring(s))
end
end
# ╔═╡ 96f1a91e-2ba4-47ed-ba15-2490c93177a6
md"""
### $(html"") Attack Square Sets
Chess.jl contains several functions for generating attacks to/from squares on the chess board.
Attacks by knights, kings or pawns from a given square on the board are the most straightforward.
The squares attacked by a knight on e5:
"""
# ╔═╡ fd30409a-a432-4c17-8210-3782c06daa98
knightattacks(SQ_E5)
# ╔═╡ d66fac3b-431b-48fb-80fb-fc59fd4cf4bf
md"The squares attacked by a king on g2:"
# ╔═╡ ee8dceb5-45b0-43e6-80c5-608f9278fd07
kingattacks(SQ_G2)
# ╔═╡ b3ce5ebc-e40b-491d-8a76-a4dd94db671d
md"The squares attacked by a black pawn on c5 (the color is necessary here, because white and black pawns move in the opposite direction):"
# ╔═╡ f06e6a49-a160-4e83-a921-91da15586c86
pawnattacks(BLACK, SQ_C5)
# ╔═╡ 2e2f2d49-c654-4310-92b9-bed38e9c43c6
md"""
Sliding pieces (bishops, rooks and queens) are a little more complicated, because we need the set of occupied squares on the board in order to identify possible blockers before we can know what squares they attack.
The most common way of providing a set of occupied squares is to use an actual chess board. Let's first create a board position a little more interesting than the starting position.
"""
# ╔═╡ c9493ea0-6e3e-498e-99bb-bcdb8ed5ecee
attackboard = @startboard e4 e5 Nf3 Nc6 d4 exd4 Nxd4 Nf6
# ╔═╡ 74ce06b7-4874-4af7-95e5-191874897fde
md"""
The set of squares attacked by the white queen on d1:
"""
# ╔═╡ 87cbdb1d-b22d-4ebb-8d39-78e22d8288cf
queenattacks(attackboard, SQ_D1)
# ╔═╡ 98fdea63-d717-4deb-a0b3-df98beb14e7d
md"""
The set of squares a bishop on c4 would have attacked (there is no bishop on c4 at the moment, but this does not stop us from asking which squares a hypothetical bishop there would attack):
"""
# ╔═╡ a0e1bd70-57a2-4927-997c-4d34498d06a3
bishopattacks(attackboard, SQ_C4)
# ╔═╡ f70b74fb-d8f7-4442-8844-aca4f949236b
md"""
There is also an `attacksfrom` function, that returns the set of squares attacked by the piece on a given non-empty square, and an `attacksto` function, that returns all squares that contains pieces of either side that attacks a given square:
"""
# ╔═╡ 753f9583-3ccd-4003-9b9b-13b45bf94da4
attacksfrom(attackboard, SQ_H8)
# ╔═╡ 943b6011-c084-4bcd-982b-edc98c131e84
attacksto(attackboard, SQ_D4)
# ╔═╡ 0781fda3-6272-480e-b735-eb5df673d551
md"""
It is possible to identify pieces that can be captured by intersecting attack square sets with sets of pieces of a given color:
"""
# ╔═╡ b38816ba-9081-48e1-b048-5efae8ec9983
attacksfrom(attackboard, SQ_D4) ∩ pieces(attackboard, BLACK)
# ╔═╡ c7448f82-a870-4985-bc68-20de5ca22bc1
md"""
Here is a more complicated example: A function that finds all pieces of a given side that are attacked, but undefended:
"""
# ╔═╡ 46ed3c54-56ca-4055-8026-9d509acc4f8e
function attacked_but_undefended(board, color)
attacker = -color # The opposite color
# Find all attacked squares
attacked = SS_EMPTY # The empty square set
for s ∈ pieces(board, attacker)
attacked = attacked ∪ attacksfrom(board, s)
end
# Find all defended squares
defended = SS_EMPTY
for s ∈ pieces(b, color)
defended = defended ∪ attacksfrom(board, s)
end
# Return all attacked, but undefended squares containing pieces of
# the desired color:
attacked ∩ -defended ∩ pieces(board, color)
end
# ╔═╡ 1cbe2b57-c6d5-4daa-a192-9f4026f04cd7
md"""
Let's create a position for testing the above function:
"""
# ╔═╡ 35ec6c59-a647-4151-9615-d6732a96f68d
testboard = fromfen("r1b5/1kp5/1p3b1p/8/8/4B3/2R3n1/QK6 w - - 0 1")
# ╔═╡ ce18f885-8979-4139-aec4-47d74dad48ab
md"""
A total of five black pieces can be captured: The rook on a8, the pawns on b6 and c7, the bishop on f6, the knight of g2, and the pawn on h6. However, the first three of these are defended by black pieces. Let's try our `attacked_and_undefended` function:
"""
# ╔═╡ e1c6d80d-78b9-496b-9507-fe7ad72665e6
attacked_but_undefended(testboard, BLACK)
# ╔═╡ 0e921fbf-5a1e-4059-9f75-6b04a5c5d44c
md"""
## $(html"") Games
There are two types for representing chess games: `SimpleGame` and `Game`. `SimpleGame` is a basic type that contains little more than the PGN headers (player names, game result, etc.) and a sequence of moves. `Game` is a more complicated type that support annotated, tree-like games with comments and variations. If you don't need these features, `SimpleGame` is always a better choice, as manipulating a `SimpleGame` is much faster.
For the rest of this section, most of our examples use the more complicated `Game` type. With a few exceptions (that will be pointed out), methods with identical names and behavior exist for the `SimpleGame` type. Remember again that `SimpleGame` is really the preferred type in practice, unless you *really* need the extra functionality of the `Game` type.
"""
# ╔═╡ 62cdb11f-b5af-4ed4-b530-42cb5516fcc8
md"""
### $(html"") Creating Games and Adding Moves
To create an empty game from the standard chess position, use the parameterless `Game()` constructor:
"""
# ╔═╡ 8ae7347e-0d28-44ff-a69f-c28630223a32
Game()
# ╔═╡ aa8d936a-d770-4c54-b2b3-2056da300e0d
md"""
To the left, we have the current chess board. To the right, we have the move list, which is currently empty, because we haven't added any moves to the game.
Moves can be added to the game with the `domove!()` function:
"""
# ╔═╡ 445c66af-6c1e-4333-a23c-f40a054a840c
begin
local g = Game()
domove!(g, "c4")
domove!(g, "e5")
domove!(g, "Nc3")
domove!(g, "Nf6")
g
end
# ╔═╡ eaec04ab-f617-4668-a1fc-8a6b9e9985d0
md"""
Constructing games this way quickly becomes tedious. For interactive use, there is a macro `@game` (and a similar macro `@simplegame` for the `SimpleGame` type) for constructing a game from the regular starting position with a sequence of moves. The following is equivalent to the above example:
"""
# ╔═╡ e74c0896-32fc-4b62-8358-60d965ac7e1b
@game c4 e5 Nc3 Nf6
# ╔═╡ ea9e5051-f456-4ec2-91da-435b5b66c60e
md"""
Notice that there is now a list of moves in the right hand side of the output.
The "👉" symbol indicates our current position in the game. It is possible to go back to the previous position in the game with the `back!` function, and to go forward again with `forward!`. There are also functions `tobeginning!` and `toend!` for going to the beginning or end of the game.
Examples:
"""
# ╔═╡ 0988d9ab-352c-419e-8294-837f0b42ca2c
begin
local g = @game e4 c6 d4 d5 Nc3 dxe4 Nxe4 Nf6
back!(g)
back!(g)
end
# ╔═╡ cc28612f-89e4-4ceb-90c0-75451839f7b2
md"""
The "👉" is now before the move Nxe4, because we went back two moves.
"""
# ╔═╡ 4868af76-ccd5-40a6-b9bc-5e2d6031368e
begin
local g = @game d4 Nf6 c4 e6 Nc3 Bb4
tobeginning!(g)
end
# ╔═╡ 7780d253-78d1-4952-9f3e-ab25f0cb154c
md"""
We see the starting position on the left, and the "👉" at the beginning of the move list on the right, because we called `tobeginning!` to go back to the beginning of the game.
"""
# ╔═╡ 519492dc-e393-4002-aead-5c679fdb4def
md"""
To get the current board in the game, use the `board` function:
"""
# ╔═╡ 3848c2e7-1a11-4f46-b153-c4806dc2d299
begin
local g = @game d4 Nf6 c4 c5 d5 b5
board(g)
end
# ╔═╡ 66e5c7ea-85ae-41a9-994f-225e3a7299e3
md"""
### $(html"") Example: Generating Random Games
By putting together things we've learned earlier in this tutorial, we can now generate random games. This function generates a `SimpleGame` containing random moves:
"""
# ╔═╡ 5edb6907-1012-4e47-b656-daac485deb0a
function randomgame()
game = SimpleGame()
while !isterminal(game)
move = rand(moves(board(game)))
domove!(game, move)
end
game
end
# ╔═╡ 6fd5e41f-7597-499e-bb8b-668e1169a17f
md"""
The only new function in the above function is `isterminal`, which tests for a game over condition (checkmate or some type of immediate draw).
Let's generate a random game:
"""
# ╔═╡ 83cd74d2-d30c-4e35-9670-868d1558165e
randomgame()
# ╔═╡ 05d63c5c-a517-4651-897b-9c1321c9d698
md"""
Approximately how often do completely random games end in checkmate? Let's find out. The following function takes an optional number of games as input (by default, one thousand), generates the deseired number of random games, and returns the fraction of the games that (accidentally) ends in checkmate.
"""
# ╔═╡ 09153cfa-40b5-4bb7-8607-30116e67f65b
function checkmate_fraction(game_count = 1000)
checkmate_count = 0
for _ in 1:game_count
g = randomgame()
if ischeckmate(board(g))
checkmate_count += 1
end
end
checkmate_count / game_count
end
# ╔═╡ 6143279b-3467-4d16-830e-8797394f1be3
md"""
The above code introduces the new function `ischeckmate`, which tests if a board is a checkmate position.
Let's test it:
"""
# ╔═╡ 618f732c-6dff-445b-a8d6-9062f1f74ade
checkmate_fraction()
# ╔═╡ 76aba57e-0212-4c5d-881b-2c7f20346af1
md"""
It seems that about 15% of all random games end in an accidental checkmate. To me, this is a suprisingly high number.
What will happen if we make random moves, except that we always play the mating move if there is a mate in one? Let's find out. As a first step, let's write a function that checks whether a move is a mate in one.
"""
# ╔═╡ 503bb6dd-1857-494c-a9d5-573e1821d280
move_is_mate_slow(board, move) = ischeckmate(domove(board, move))
# ╔═╡ edbc60ff-1742-42f7-8ac2-5f0469e8929e
md"""
This is simple, elegant and readable. Unfortunately, as the name indicates, it is also kind of slow. The reason is that `domove` copies the board. Using the destructive `domove!` function performs much better, at the price of longer and less readable code.
The function below is functionally equivalent to the one above, but performs much better.
"""
# ╔═╡ f87b6c16-d1db-4c2d-81e0-6a13296f146e
function move_is_mate(board, move)
# Do the move
u = domove!(board, move)
# Check if the resulting board is checkmate
result = ischeckmate(board)
# Undo the move
undomove!(board, u)
# Return result
result
end
# ╔═╡ 3d6e9366-7dd2-43d6-a07c-8d506dd3880c
md"""
Using the function we just wrote, we can make a function that takes a board as input and returns a mate in 1 move if there is one, or a random move otherwise.
"""
# ╔═╡ 8828a3fe-9716-4d8b-ab3a-572c487b74a1
function mate_or_random(board)
ms = moves(board)
for move ∈ ms
if move_is_mate(board, move)
return move
end
end
rand(ms)
end
# ╔═╡ 69f6e662-8e9f-432a-b698-a6141100b931
md"""
The function below is identical to the `randomgame` function above, except that it uses `mate_or_random` instead of totally random moves:
"""
# ╔═╡ e8e047c4-186c-44bb-966a-b8b1b7c4b57f
function almost_random_game()
game = SimpleGame()
while !isterminal(game)
move = mate_or_random(board(game))
domove!(game, move)
end
game
end
# ╔═╡ 1de7ccfa-7ef9-4519-bdfe-5da16473e977
md"""
What percentage of the games end in checkmate now? Here's a function to find out:
"""
# ╔═╡ c84b0b25-054e-48be-b5c8-33c17c34747f
function checkmate_fraction_2(game_count = 1000)
checkmate_count = 0
for _ in 1:game_count
g = almost_random_game()
if ischeckmate(board(g))
checkmate_count += 1
end
end
checkmate_count / game_count
end
# ╔═╡ d08ab867-3784-4afb-b0ab-3c68a8bed5c6
checkmate_fraction_2()
# ╔═╡ 1e955883-50a5-4558-bbf9-d920615ff9a2
md"""
The result should be somewhere around 0.81. About 81% of all completely random games include at least one opportunity to deliver mate in 1!
"""
# ╔═╡ 7eacb6d9-4c19-4c6c-b047-783e0962c9be
md"""
### $(html"")Opening Games in Lichess
You may have noticed the "Open in lichess" link in some of the above examples. This is used to open and browse through the game in lichess. Unfortunately, this is not completely automatic. The link takes you to the lichess page for importing a game in PGN notation, but does not automatically paste the game PGN. However, it does copy the PGN of the game to the clipboard.
The way to use the "Open in lichess" link is this: First, click the link. Then, in the page that appears in your browser, use Cmd+V or Ctrl+V to paste the game into the "Paste the PGN text here" box, and press the "Import PGN" button. You can now easily browse through the game.
Here's an "almost random game" for you to try out:
"""
# ╔═╡ 2d8b2671-2ce3-43be-b26c-7f7f30ac4c70
almost_random_game()
# ╔═╡ 111f2a14-561f-4875-8203-3fda5526277f
md"""
### $(html"") Variations
If we create a game with some moves, go back to an earlier place in the game,
and call `domove!` again with a new move, the previous game continuation is
overwritten:
"""
# ╔═╡ fb1654f1-00cd-4a0f-b96a-4c8efebbd8dd
begin
local g = @game d4 d5 c4 e6 Nc3 Nf6 Bg5
back!(g)
back!(g)
back!(g)
domove!(g, "Nf3")
g
end
# ╔═╡ 50774dbe-6568-4bdb-a744-181da4b8a809
md"""
This is not always desirable. Sometimes we want to add an *alternative* move,
and to view the game as a *tree of variations*.
Games of type `Game` (but not `SimpleGame`!) are able to handle variations.
To add an alternative variation at some point in the game, first make the main line,
then go back to the place where you want to add the alternative move, and then do `addmove!`. The following example is identical to the one above, except that `domove!` has been replaced by `addmove!`:
"""
# ╔═╡ 4277b47f-c106-4bb1-aa6f-713023abce38
begin
local g = @game d4 d5 c4 e6 Nc3 Nf6 Bg5
back!(g)
back!(g)
back!(g)
addmove!(g, "Nf3")
g
end
# ╔═╡ d0afaa99-fabc-42ce-b0b9-4a2101ff3cbb
md"""
The board position is the same, because our current location in the game tree (indicated by the "👉" symbol) is the one after white's move 3. Nf3. However, in the move list on the right, we can see that the original main line with 3. Nc3 Nf6 4. Bg5 is still there. Alternative variations are printed in parens in the Pluto output; the "(1... Nf6 👉)" in the above example.
The function `forward!` takes an optional second argument: Which move to follow when going forward at a branching point in the tree. If this argument is ommited, the main (i.e. first) move is followed.
Here is how you would go to the point after 3. Nc3 in the above example:
"""
# ╔═╡ a1572851-4901-47c7-bda5-4901e6e493d5
begin
local g = @game d4 d5 c4 e6 Nc3 Nf6 Bg5
back!(g)
back!(g)
back!(g)
addmove!(g, "Nf3")
# Our current position in the game tree is here:
# 1. d4 d5 2. c4 e6 3. Nc3 (3. Nf3 👉) Nf6 4. Bg5
# Go one move back:
back!(g)
# New position in the game tree:
# 1. d4 d5 2. c4 e6 👉 3. Nc3 (3. Nf3) Nf6 4. Bg5
# Go forward, following the move Nc3:
forward!(g, "Nc3")
end
# ╔═╡ 7fe31d85-d100-45ef-ae0e-e3625a1dcd32
md"""
Two other functions that are useful for navigating games with variations are `tobeginningofvariation!` and `toendofvariation!`. See the documentation of these functions for details.
Of course, variations can be nested recursively:
"""
# ╔═╡ 12b8d126-fe7b-48ff-8676-582a3060741b
begin
local g = @game e4 c5 Nf3 Nc6
back!(g)
back!(g)
addmove!(g, "c3")
addmove!(g, "Nf6")
addmove!(g, "e5")
back!(g)
back!(g)
addmove!(g, "d5")
addmove!(g, "exd5")
end
# ╔═╡ e5f96c44-c248-4bd4-a2b2-603f020894d4
md"""
### $(html"") Comments
Games of type `Game` (but again, not `SimpleGame`) can also be annotated with
textual comments, by using the `addcomment!` function:
"""
# ╔═╡ bd8f9b95-bd67-4483-b9eb-a80c5070db57
begin
local g = @game d4 f5
addcomment!(g, "This opening is known as the Dutch Defense")
g
end
# ╔═╡ 2807fa4c-3b89-4d5d-bb78-4ffabf77effb
md"""
### $(html"") Numeric Annotation Glyphs
It is also possible to add *numeric annotation glyphs* (NAGs) to the game. NAGs
are a standard way of adding symbolic annotations to a chess game. All integers
in the range 0 to 139 have a pre-defined meaning, as described in [this
Wikipedia article](https://en.wikipedia.org/wiki/Numeric_Annotation_Glyphs).
Here is how to add the NAG `$4` ("very poor move or blunder") to the move 2... g4 in the game 1. f4 e5 2. g4 Qh4#:
"""
# ╔═╡ fc4cca60-ce23-4a10-8127-09bb5511deb8
begin
local g = @game f4 e5 g4 Qh4
back!(g)
addnag!(g, 4)
g
end
# ╔═╡ 56daae05-8c61-4cbe-b5ee-f0182e7e28d5
md"""
## $(html"") PGN Import and Export
This section describes import and export of chess games in the popular [PGN
format](https://www.chessclub.com/help/PGN-spec). PGN is a rather awkward and
complicated format, and a lot of the "PGN files" out there on the Internet don't
quite follow the standard, and are broken in various ways. The functions
described in this section do a fairly good job of handling correct PGNs
(although bugs are possible), but will often fail on the various not-quite-PGNs
found on the Internet.
The PGN functions are found in the submodule `Chess.PGN`:
"""
# ╔═╡ 7386b26b-4e5a-46fd-aa1a-1488c35878bb
md"""
### $(html"") Creating a Game From a PGN String
Given a PGN string, the `gamefromstring` function creates a game object from the string (throwing a `PGNException` on failure). By default, the return value is a `SimpleGame` containing only the moves of the game, without any comments, variations or numeric annotatin glyphs. If the optional named parameter `annotations` is `true`, the return value is a `Game` with all annotations included.
Here's a PGN string for us to experiment with:
"""
# ╔═╡ 75853f99-0962-4683-ad10-a3803d4aa6ad
pgnstring = """
[Event "Important Tournament"]
[Site "Somewhere"]
[Date "2021.04.29"]
[Round "42"]
[White "Sixpack, Joe"]
[Black "Public, John Q"]
[Result "0-1"]
1. f4 e5 2. fxe5 d6 3. exd6 Bxd6 4. Nc3 \$4 {A terrible blunder. White should
play} (4. Nf3 {, and Black has insufficient compensation for the pawn.}) Qh4+
5. g3 Qxg3+ {Black could also have played} (5... Bxg3+ 6. hxg3 Qxg3#) 6. hxg3
Bxg3# 0-1
""";
# ╔═╡ 952577ea-391f-406d-b026-814f48ea96cb
md"""
Trying to import this gives us:
"""
# ╔═╡ f3ef69e2-1ce4-4bef-a2de-043dffb7c2bb
gamefromstring(pgnstring) |> toend!
# ╔═╡ 396c5aa8-ba71-4302-8c24-ec76e74d5184
md"""
The result is a `SimpleGame`, without the annotations. If we want to import the annotations and create a `Game`, we must set the named parameter `annotations` to `true`, like this:
"""
# ╔═╡ 59be4352-7271-4a9b-adef-36d23d8eeb64
gamefromstring(pgnstring, annotations=true) |> toend!
# ╔═╡ b403afab-3ddf-42c5-98da-ac9991091f08
md"""
Unless you really need the annotations, importing to a `SimpleGame` is the
preferred choice. A `SimpleGame` is much faster to create and consumes less
memory.
Exporting a game to a PGN string is done by the `gametopgn` function. This works for both `SimpleGame` and `Game` values:
"""
# ╔═╡ f7a1bd72-2595-42e0-be8a-5f5763517131
gametopgn(gamefromstring(pgnstring)) |> Print
# ╔═╡ 4d067bb7-786a-4946-9380-a27d39f9d784
gametopgn(gamefromstring(pgnstring, annotations=true)) |> Print
# ╔═╡ 91e1cbcf-7ada-44d3-89e2-a2a9a164ec13
md"""
### $(html"") Working With PGN Files
Given a file with one or more PGN games, the function `gamesinfile` returns a
`Channel` of game objects, one for each game in the file. Like `gamefromstring`,
`gamesinfile` takes an optional named parameter `annotations`. If `annotations`
is `false` (the default), you get a channel of `SimpleGame`s. If it's `true`,
you get a channel of `Game`s with the annotations (comments, variations and
numeric annotation glyphs) included in the PGN games.
As an example, here's a function that scans a PGN file and returns a vector
of all games that end in checkmate:
"""
# ╔═╡ 7cf93ffd-8b5e-43c6-ac5e-13d1a9183970
function checkmategames(pgnfilename::String)
result = SimpleGame[]
for g in gamesinfile(pgnfilename)
toend!(g)
if ischeckmate(g)
push!(result, g)
end
end
result
end
# ╔═╡ 7119bdf0-070e-40d7-94e9-ab35cf17dcf5
md"""
## $(html"") Opening Books
The opening book function are located not in the main `Chess` module, but in the submodule `Chess.Book`.
"""
# ╔═╡ 35d20127-b307-4de5-850c-46d06cbf3a1f
md"""
The `Chess.Book` module contains functions for processing large PGN files and creating opening book files. There is also a small built-in opening book. The rest of the examples in this section will use the built-in opening book. For information about generating your own books, consult the documentation for the `Chess.Book` module.
"""
# ╔═╡ 768a2a20-4b63-4c5c-ae12-4b18152b412d
md"""
### $(html"") Finding Book Moves
Given a `Board`, the function `findbookentries` finds all opening book moves for that board position:
"""
# ╔═╡ 271648d8-10e8-4512-9cb3-d4907d0e0750
begin
g = @startboard e4 c5 Nf3 d6 d4 cxd4 Nxd4 Nf6 Nc3 Nc6
findbookentries(g)
end
# ╔═╡ 0114debc-032d-4d99-8d59-8c79dd20528b
md"""
The return value is a vector of `BookEntry` structs. This struct contains the
following slots:
* `move`: The book move for this book entry. For space reasons, the move is stored as an `Int32` value. To get the actual `Move` of a `BookEntry` `e`, do `Move(e.move)`.
* `wins`: The number of times the player who made this move won the game.
* `draws`: The number of times the game was drawn when this move was played.
* `losses`: The number of times the player who played this move lost the game.
* `elo`: The Elo rating of the highest rated player who played this move.
* `oppelo`: The Elo rating of the highest rated opponent against whom this move
was played.
* `firstyear`: The first year this move was played.
* `lastyear`: The last year this move was played.
* `score`: The score of the move, used to decide the probability that this move is played when picking a book move to play. The score is computed based on the move's win/loss/draw statistics and its popularity, especially in recent games and games with strong players.
As you can see in the output of the example above, book entries are printed in a way that makes it somewhat easier to read them and understand their contents. Nevertheless, it is not easy to scan the above `BookEntry` vector and compare the statistics for the various book moves. A good way to get a summary of this information is the `printbookentries` function:
"""
# ╔═╡ dd75a72c-4218-4778-b0aa-e279794bc3fd
with_terminal() do
b = @startboard e4 c5 Nf3 d6 d4 cxd4 Nxd4 Nf6 Nc3 Nc6
printbookentries(b)
end
# ╔═╡ e1b274eb-7689-43df-aa3f-8077689267af
md"""
The output columns have the following meanings:
* `move`: The move.
* `prob`: Probability that this move will be played when calling `pickbookmove`.
* `score`: Percentage score of this move in the games used to produce this
book file.
* `won`: Number of games won with this move.
* `drawn`: Number of games drawn with this move.
* `lost`: Number of games lost with this move.
* `elo`: Maximum Elo of players that played this move.
* `oelo`: Maximum Elo of opponents against which this move was played.
* `first`: The first year this move was played.
* `last`: The last year this move was played.
To pick a book move, use `pickbookmove`:
"""
# ╔═╡ 33823010-0e63-4f9b-ba29-802e9363c604
pickbookmove(@startboard d4 Nf6 c4 g6 Nc3)
# ╔═╡ cd525aa7-386f-4bc3-9814-265411e8a741
md"""
If no book moves are found for the input position, `pickbookmove` returns `nothing`.
"""
# ╔═╡ ef17fd7b-6436-4e0e-a0a7-3a0d5a019fc5
md"""
### $(html"") Example: Playing Random Openings
Here's a function that generates a game (or rather, the beginning of a game) by picking and playing book moves until it reaches a position where no book move is found:
"""
# ╔═╡ fab9da9c-b661-4081-bc72-b5acdb3c019d
function random_opening()
g = Game()
while true
move = pickbookmove(board(g))
if isnothing(move)
break
end
domove!(g, move)
end
g
end
# ╔═╡ 95a978e2-cbcf-4a0c-90f1-98ba7405f2be
random_opening()
# ╔═╡ 8e8f6cee-f6da-4cd9-bf9d-167b7ac89808
md"""
## $(html"") Interacting With UCI Engines
This section describes how to run and interact with chess engines using the
[Universal Chess Interface](http://wbec-ridderkerk.nl/html/UCIProtocol.html)
protocol. There are hundreds of UCI chess engines out there. A free, strong
and popular choice is [Stockfish](https://stockfishchess.org). Stockfish is
used as an example in this section, but any other engine should work just as
well.
For running the examples in this section, it is assumed that you have an executable `stockfish` somewhere in your `PATH` environment variable.
The code for interacting with UCI engines is found in the submodule `Chess.UCI`:
"""
# ╔═╡ 57f2184d-676e-4da9-99aa-34b0ec7798f9
md"""
### $(html"")Starting and Initializing Engines
An engine is started by calling the `runengine` function, which takes the path to the engine as a parameter:
"""
# ╔═╡ 34589d78-9009-412c-8d10-1738729b14b9
sf = runengine("stockfish")
# ╔═╡ 97abbd67-36fa-4451-9e4f-ab96cf82c82b
md"""
The first thing you want to do after starting a chess engine is probably to
set some UCI parameter values. This can be done with `setoption`:
"""
# ╔═╡ 69b5b90b-9c93-4de2-a106-e0f9e6bd10a4
setoption(sf, "Hash", 256)
# ╔═╡ 9d1447b8-2d25-4567-b9f0-4b3ead7cd873
md"""
### $(html"") Searching
You can send a game to the engine with `setboard`:
"""
# ╔═╡ ae5595c3-0238-4f00-b4ce-d88f366d4e2e
begin
local g = @simplegame f4 e5 fxe5 d6 exd6 Bxd6 Nc3
setboard(sf, g)
end
# ╔═╡ 3afc20d0-4f2f-40f5-bd8d-00fb1c838b07
md"""
The second parameter to `setboard` can also be a `Board` or a `Game`.
To ask the engine to search the position you just sent to it, use the `search` function. `search` has two required parameters: The engine and the UCI `go` command we want to send to it.
Here is the most basic example of using `search`:
"""
# ╔═╡ c1b8fcc1-cbbd-43ae-8669-de7b524186b0
search(sf, "go depth 10")
# ╔═╡ abfc5a3d-5024-44a7-af3b-70eefa26dc37
md"""
The return value is a `BestMoveInfo`, a struct containing the two slots `bestmove` (the best move returned by the engine, a `Move`) and `ponder` (the ponder move returned by the engine, a `Move` or `nothing`).
The `search` function also takes an optional named parameter `infoaction`. This parameter is a function that takes each of the engine's `info` output lines and does something to them. Here's an example where we just print the engine output with `println` as our `infoaction`:
"""
# ╔═╡ 9b2e6cb8-67c7-4d15-afde-03a4ae7595da
begin
local g = @simplegame d4 Nf6 c4 g6 Nc3 d5 cxd5 Nxd5
setboard(sf, g)
with_terminal() do
search(sf, "go depth 10", infoaction = println)
end
end
# ╔═╡ b941833a-0efa-45e8-a59f-a7007653cfc6
md"""
### $(html"") Parsing Search Output
In most cases, we want something more easily to manipulate than the raw string values sent by the engines `info` lines in our `infoaction` function. The function `parsesearchinfo` takes care of this. It takes an `info` string as input and returns a `SearchInfo` value, a struct that contains the various components of the `info` line as its slots.
Let's see how this works:
"""
# ╔═╡ 29d1ada2-75ce-4a77-a136-f761052c0637
parsesearchinfo("info depth 10 seldepth 17 multipv 1 score cp 50 nodes 16598 nps 691583 tbhits 0 time 24 pv e2e4 d5c3 b2c3 c7c5 f1b5 c8d7 b5c4 f8g7 g1f3 c5d4 c3d4 d8a5 c1d2")
# ╔═╡ e6961baa-90c2-4b5d-9bfa-f71355330509
md"""
The meaning of most of the slots in this struct should be evident if you are familiar with the UCI protocol. If you are not, the two most important slots are the `score` and the `pv`.
The `score` is a value of type `Score`. The definition of the `Score` struct looks
like this:
```julia
struct Score
value::Int
ismate::Bool
bound::BoundType
end
```
There are two types of score: *Centipawn scores* are an evaluation where advantages is measured on a scale where 100 means an advantage corresponding to the value of one pawn. *Mate scores* are scores of the type "mate in X moves". The type of score is indicated by the `ismate` slot, while the numerical value is indicated by the `value` slot.
For instance, when `value` is 50 and `ismate` is `false`, it means that the side to move has an advantage worth about half a pawn. If `value` is 5 and `ismate` is true, it means that the side to move has a forced checkmate in 5 half moves or less.
The final slot, `bound`, indicates whether the score is just an upper bound, a lower bound, or an exact score. The three possible values are `upper`, `lower` and `exact`.
When presenting scores to humans, the `scorestring` function is useful. For centipawn scores, it converts the score to a scale of pawn=1.0, and outputs the score with a single decimal:
"""
# ╔═╡ 08dc0954-02b2-49d2-b3a9-b99d8c41a125
scorestring(Score(-87, false, Chess.UCI.exact))
# ╔═╡ b8806c05-7158-4774-a5f7-80f579f44388
md"""
Mate in N scores are displayed as `#N`:
"""
# ╔═╡ 69f1f5fc-b538-45b3-8e41-3d4822d638ec
scorestring(Score(6, true, Chess.UCI.exact))
# ╔═╡ e72113f2-1608-4e16-adc1-eb359096dbdc
md"""
UCI chess engines always output scores from the point of view of the current side to move. This is not always what we want; often we want scores from white's point of view (i.e. positive scores mean that white is better, while negative scores mean that black is better). `scorestring` takes an optional named parameter `invertsign` that can be used to invert the sign:
"""
# ╔═╡ 07d590d0-3775-468b-a9a3-99d26393d91a
scorestring(Score(-140, false, Chess.UCI.exact), invertsign=true)
# ╔═╡ f0206a54-b71b-42b6-84da-2b87f348eced
md"""
The other interesting slot of `SearchInfo` is the `pv`. This is a vector of moves, what the engine considers the best line of play, assuming optimal play from both sides.
"""
# ╔═╡ 41bdb293-d905-4fda-ac48-fa5926222bc7
md"""
### $(html"") Example: Engine vs Engine Games
Using what we have learned, we can easily make a function that generates engine vs engine games. Let's use the `random_opening` function we wrote earlier to initialize the game with some opening position, and let the engine play out the game from there. We'll let the engine think 10 thousand nodes per move.
"""
# ╔═╡ 69feaef9-d4b1-4d12-86f6-0aadf030f8eb
function engine_game(engine)
g = random_opening()
while !isterminal(g)
setboard(engine, g)
move = search(engine, "go nodes 10000").bestmove
domove!(g, move)
end
g
end
# ╔═╡ 3654e4d9-8e0a-4154-805a-330fc29ae64c
md"""
An example game:
"""
# ╔═╡ 7e1db1d9-0a8b-4056-8a21-9a6c04ad3d9d
engine_game(sf)
# ╔═╡ 727ff4b9-6f52-481d-8155-792bc35803db
md"""
Let's try to build a slightly more sophisticated function for running engine vs engine matches, that also includes the engine evaluation for each move as a comment in the game.
In our improved engine vs engine function, we need to supply an `infoaction` in the call to `search`, in order to obtain the engine evaluation. It can be done like this:
"""
# ╔═╡ f16db75f-9d13-4650-a025-705ca32a9404
function engine_vs_engine_with_evals(engine)
# A variable for keeping track of the score:
score = Score(0, true, Chess.UCI.exact)
# An infoaction function that updates the score:
function infoaction(infoline)
info = parsesearchinfo(infoline)
if !isnothing(info.score)
score = info.score
end
end
g = random_opening()
while !isterminal(g)
whitetomove = sidetomove(board(g)) == WHITE
setboard(engine, g)
# Use the infoaction defined above when calling search:
move = search(engine, "go nodes 10000", infoaction=infoaction).bestmove
# Add the move to the game:
domove!(g, move)
# Add the score as a comment:
addcomment!(g, scorestring(score, invertsign = !whitetomove))
end
g
end
# ╔═╡ e753394e-ad69-47b8-8639-d60a22275c72
md"""
A test game:
"""
# ╔═╡ 5c6f4050-b7a8-4f56-aad1-f418b51e2cad
engine_vs_engine_with_evals(sf)
# ╔═╡ Cell order:
# ╟─a6d99974-5c03-4bf6-b6af-4ccf76d1b9e9
# ╟─b9d53c2d-faa1-4a44-8b8c-bc62c5237797
# ╟─2ef8d62b-c32f-486f-a2e5-c82ed0418170
# ╠═eb038b1a-b2af-49a2-b506-9315df701f47
# ╟─563c93f0-721d-4d93-8530-3d219f197bb5
# ╠═fe7e2e16-6e57-4ab0-92cb-be3465c5bcab
# ╟─861c5b57-7f28-4d59-800b-eeb68a1223b0
# ╠═9c7d61a6-81b9-4674-bcee-c86023fa3dc0
# ╟─f49a0b6f-4283-4e9c-9570-a37d71f653f9
# ╠═aee3fa17-44b8-41c4-ba6e-aaca43081157
# ╟─293a3b35-3466-4d11-b116-8cc96fade2a9
# ╠═0c12848b-5892-425f-8ed2-fd00c5e3a4db
# ╟─b4cfb722-7f8c-4106-9cc3-5fcefb8d2233
# ╟─967d3055-45d9-4c77-b0c2-7d514a407278
# ╠═7dd7fa6e-717b-4ba1-a110-987a4cb53a5b
# ╟─be9065b3-3040-4704-a6cd-6deb7060a0c1
# ╠═b5f6826e-3855-4d41-8ce0-27587dfa598b
# ╟─633a0f4c-6e5f-40fb-8373-6dd25d8fda6a
# ╠═43eb49ed-afba-4850-9b3d-2e36fc800eff
# ╟─240dde91-ca7b-4078-a9da-5af8ca72b210
# ╠═d56d75c2-879f-466f-a634-8f7a19ff389a
# ╟─ccd6feec-a51a-4a6e-ad84-95d215604e9a
# ╠═ba204c76-7f08-47ec-b64e-9b28e16ce141
# ╟─917de419-d7b0-4d61-a897-5bcb012b8e2e
# ╠═bb906070-43a8-4b5c-a9e5-b248f12b0b89
# ╟─d70b1d38-c897-4b7c-9fa4-7c19ca06e553
# ╠═528d6ca6-7b33-4dcf-8738-90260915c699
# ╟─dd5a8f77-8af5-41ea-936f-49b36ee22f54
# ╠═0f257004-8605-4b14-b255-ca0e231ae406
# ╠═4d60d4da-ae36-4784-80ca-bac8aebc1933
# ╟─a310bac6-080a-4ada-acc1-f423843a9497
# ╠═a400d50d-02d5-4a52-ad76-f22d86d5e332
# ╟─2ca00e38-f7b9-4c0b-be0e-7b92898944ca
# ╠═93e07389-1624-4530-95fc-b5a51a926fc3
# ╠═caea759c-a63d-43d5-a87f-70441ef77aba
# ╟─4877f578-96d6-44fc-a9c5-cf431f274303
# ╠═fb2aed2e-aa35-4d7d-92f3-5236075be26a
# ╠═60672e37-4ad8-41dc-9854-344fd40362f6
# ╟─a7ba9a08-a508-4771-a411-5d564fbcad22
# ╠═d06dc3a1-a8ef-4f09-8538-e4ba25efca64
# ╠═aabdade4-3ec7-4832-8bf7-7285d0658361
# ╟─9db8e449-f98d-4735-9c49-2d369aed0b6e
# ╠═cfdef501-7c9e-4662-80c4-54441bd093c1
# ╠═29e63f8c-980a-44d3-96d8-4728ba558ac2
# ╠═20382e3f-6b31-4ba2-b07f-641dcc73fa71
# ╟─2f8050ef-6faa-4150-8221-5714e94bd2e1
# ╠═0d8e77f3-dd7b-40c1-b9af-18d5883009a9
# ╠═09674208-60c5-4fad-83af-ffb15d8f2f91
# ╟─13cc3c48-9690-4b91-a253-c7a355c1a40e
# ╠═47b748d5-c1a4-41ca-99f4-51fb216db311
# ╟─6e0451c3-c68a-4783-a1a0-87c69b321bd7
# ╠═51a76d49-d8b9-46b0-8908-08e86b1640f0
# ╠═7436bddb-53e2-4044-a2df-a63d8cb5ba5d
# ╟─eb6edb9c-c4b3-4dc0-b1ac-9bc5817a1149
# ╠═3e1544e5-1a68-419e-8039-99afc55c3836
# ╠═53ac9f47-f66a-4329-aa16-61dcbe4be365
# ╟─67cdc5dd-865d-4bb2-b130-adf7c2e874a5
# ╠═31cfa4be-8af4-4db5-a183-7f7828f8a15b
# ╠═a55b94dc-a9d1-4749-b03f-294e2371e169
# ╟─c03f1d2c-544f-47eb-89d0-fbcb3143524d
# ╠═aba22fd4-c0aa-4d93-863f-fc571396e7c3
# ╠═e1266ab4-d5ae-4414-bd4b-0113c1875cd6
# ╟─bd80c9f7-1ddb-47a7-a4f5-01bae805e8cd
# ╠═fb57388a-b5eb-4383-b2ce-38b6e10afb01
# ╟─2730adae-23b3-41f0-856a-a46813465208
# ╟─e2c56253-8745-4262-86d4-be83ab7908ca
# ╠═9c8b289b-5cda-4578-8d99-6311322c0e98
# ╟─5e193e21-457c-4bb2-bd51-8e8d131891cc
# ╠═ff444a68-e964-47a7-bbf4-2061f85ffb5b
# ╠═37d27b7a-1e57-4a73-a1d3-04b54add087a
# ╟─9e14663e-89cb-4445-9eb3-f3ac9dc84d36
# ╠═142b4958-449f-4983-afe6-cee5ecb17ae5
# ╟─4ef7bd67-e834-422a-baa0-2f3870a976d9
# ╠═82a738f6-e3d3-45a3-91ae-90dd3568834b
# ╟─a376190c-69d0-4f47-9b48-fdaa77ffb57b
# ╠═d97d771d-6d61-4dbd-b224-0093757b3c5c
# ╟─42ff96a4-fc48-47f8-b2c2-129a96f6821e
# ╠═245f5586-c8dc-4d61-8d7c-d557d228a9f7
# ╟─a8df6297-1fdb-4b46-8410-d7e268c6fc1c
# ╠═42db7246-f3a1-4d1d-a6d0-4fec13c3c2c2
# ╟─07f3796a-2fa9-4dc0-b32a-26c834e55716
# ╠═fed01377-5252-40b6-9e0f-a6034b3ef62d
# ╠═7dbd5b17-cc63-4464-9d95-cf87ed172a0b
# ╠═1971c152-3f24-4934-bb94-d0d9567f9024
# ╟─0cc94af0-759a-4399-b21f-5e1d7e278683
# ╠═4e00a4e7-e615-4a0d-bdd8-64a7aefa6e93
# ╟─512ba223-92bf-4ed6-adf1-86744ebb5712
# ╠═061ac99b-8391-4a72-af14-d35dcabd063d
# ╟─5baa21b3-f0c3-4f63-b474-527d6820059a
# ╠═e8923543-8e52-48e0-a427-cd60494fc258
# ╟─51b64306-e90b-409b-9472-f542948d10ec
# ╠═243faa91-d14e-4224-9c50-054536f1fc7c
# ╟─3431451d-9440-4918-8018-41c5a2a077d8
# ╠═ef447813-6405-475c-ae2d-427fcc6b38a1
# ╟─e5cbc125-f314-49d1-8c4e-6db26b77ff6d
# ╠═f30458ad-89ea-4928-a4e0-4ebff355c573
# ╠═aafe9561-3af4-43ff-a069-02832b18997d
# ╟─96f1a91e-2ba4-47ed-ba15-2490c93177a6
# ╠═fd30409a-a432-4c17-8210-3782c06daa98
# ╟─d66fac3b-431b-48fb-80fb-fc59fd4cf4bf
# ╠═ee8dceb5-45b0-43e6-80c5-608f9278fd07
# ╟─b3ce5ebc-e40b-491d-8a76-a4dd94db671d
# ╠═f06e6a49-a160-4e83-a921-91da15586c86
# ╟─2e2f2d49-c654-4310-92b9-bed38e9c43c6
# ╠═c9493ea0-6e3e-498e-99bb-bcdb8ed5ecee
# ╟─74ce06b7-4874-4af7-95e5-191874897fde
# ╠═87cbdb1d-b22d-4ebb-8d39-78e22d8288cf
# ╟─98fdea63-d717-4deb-a0b3-df98beb14e7d
# ╠═a0e1bd70-57a2-4927-997c-4d34498d06a3
# ╟─f70b74fb-d8f7-4442-8844-aca4f949236b
# ╠═753f9583-3ccd-4003-9b9b-13b45bf94da4
# ╠═943b6011-c084-4bcd-982b-edc98c131e84
# ╟─0781fda3-6272-480e-b735-eb5df673d551
# ╠═b38816ba-9081-48e1-b048-5efae8ec9983
# ╟─c7448f82-a870-4985-bc68-20de5ca22bc1
# ╠═46ed3c54-56ca-4055-8026-9d509acc4f8e
# ╟─1cbe2b57-c6d5-4daa-a192-9f4026f04cd7
# ╠═35ec6c59-a647-4151-9615-d6732a96f68d
# ╟─ce18f885-8979-4139-aec4-47d74dad48ab
# ╠═e1c6d80d-78b9-496b-9507-fe7ad72665e6
# ╟─0e921fbf-5a1e-4059-9f75-6b04a5c5d44c
# ╟─62cdb11f-b5af-4ed4-b530-42cb5516fcc8
# ╠═8ae7347e-0d28-44ff-a69f-c28630223a32
# ╟─aa8d936a-d770-4c54-b2b3-2056da300e0d
# ╠═445c66af-6c1e-4333-a23c-f40a054a840c
# ╟─eaec04ab-f617-4668-a1fc-8a6b9e9985d0
# ╠═e74c0896-32fc-4b62-8358-60d965ac7e1b
# ╟─ea9e5051-f456-4ec2-91da-435b5b66c60e
# ╠═0988d9ab-352c-419e-8294-837f0b42ca2c
# ╟─cc28612f-89e4-4ceb-90c0-75451839f7b2
# ╠═4868af76-ccd5-40a6-b9bc-5e2d6031368e
# ╟─7780d253-78d1-4952-9f3e-ab25f0cb154c
# ╟─519492dc-e393-4002-aead-5c679fdb4def
# ╠═3848c2e7-1a11-4f46-b153-c4806dc2d299
# ╟─66e5c7ea-85ae-41a9-994f-225e3a7299e3
# ╠═5edb6907-1012-4e47-b656-daac485deb0a
# ╟─6fd5e41f-7597-499e-bb8b-668e1169a17f
# ╠═83cd74d2-d30c-4e35-9670-868d1558165e
# ╟─05d63c5c-a517-4651-897b-9c1321c9d698
# ╠═09153cfa-40b5-4bb7-8607-30116e67f65b
# ╟─6143279b-3467-4d16-830e-8797394f1be3
# ╠═618f732c-6dff-445b-a8d6-9062f1f74ade
# ╟─76aba57e-0212-4c5d-881b-2c7f20346af1
# ╠═503bb6dd-1857-494c-a9d5-573e1821d280
# ╟─edbc60ff-1742-42f7-8ac2-5f0469e8929e
# ╠═f87b6c16-d1db-4c2d-81e0-6a13296f146e
# ╟─3d6e9366-7dd2-43d6-a07c-8d506dd3880c
# ╠═8828a3fe-9716-4d8b-ab3a-572c487b74a1
# ╟─69f6e662-8e9f-432a-b698-a6141100b931
# ╠═e8e047c4-186c-44bb-966a-b8b1b7c4b57f
# ╟─1de7ccfa-7ef9-4519-bdfe-5da16473e977
# ╠═c84b0b25-054e-48be-b5c8-33c17c34747f
# ╠═d08ab867-3784-4afb-b0ab-3c68a8bed5c6
# ╟─1e955883-50a5-4558-bbf9-d920615ff9a2
# ╟─7eacb6d9-4c19-4c6c-b047-783e0962c9be
# ╠═2d8b2671-2ce3-43be-b26c-7f7f30ac4c70
# ╟─111f2a14-561f-4875-8203-3fda5526277f
# ╠═fb1654f1-00cd-4a0f-b96a-4c8efebbd8dd
# ╟─50774dbe-6568-4bdb-a744-181da4b8a809
# ╠═4277b47f-c106-4bb1-aa6f-713023abce38
# ╟─d0afaa99-fabc-42ce-b0b9-4a2101ff3cbb
# ╠═a1572851-4901-47c7-bda5-4901e6e493d5
# ╟─7fe31d85-d100-45ef-ae0e-e3625a1dcd32
# ╠═12b8d126-fe7b-48ff-8676-582a3060741b
# ╟─e5f96c44-c248-4bd4-a2b2-603f020894d4
# ╠═bd8f9b95-bd67-4483-b9eb-a80c5070db57
# ╟─2807fa4c-3b89-4d5d-bb78-4ffabf77effb
# ╠═fc4cca60-ce23-4a10-8127-09bb5511deb8
# ╟─56daae05-8c61-4cbe-b5ee-f0182e7e28d5
# ╠═e89bfaed-22da-4f99-aec2-989595e2eff3
# ╟─7386b26b-4e5a-46fd-aa1a-1488c35878bb
# ╠═75853f99-0962-4683-ad10-a3803d4aa6ad
# ╟─952577ea-391f-406d-b026-814f48ea96cb
# ╠═f3ef69e2-1ce4-4bef-a2de-043dffb7c2bb
# ╟─396c5aa8-ba71-4302-8c24-ec76e74d5184
# ╠═59be4352-7271-4a9b-adef-36d23d8eeb64
# ╟─b403afab-3ddf-42c5-98da-ac9991091f08
# ╠═f7a1bd72-2595-42e0-be8a-5f5763517131
# ╠═4d067bb7-786a-4946-9380-a27d39f9d784
# ╟─91e1cbcf-7ada-44d3-89e2-a2a9a164ec13
# ╠═7cf93ffd-8b5e-43c6-ac5e-13d1a9183970
# ╟─7119bdf0-070e-40d7-94e9-ab35cf17dcf5
# ╠═b8476885-214a-4708-b933-4aac278f3b5b
# ╠═35d20127-b307-4de5-850c-46d06cbf3a1f
# ╟─768a2a20-4b63-4c5c-ae12-4b18152b412d
# ╠═271648d8-10e8-4512-9cb3-d4907d0e0750
# ╟─0114debc-032d-4d99-8d59-8c79dd20528b
# ╠═dd75a72c-4218-4778-b0aa-e279794bc3fd
# ╟─e1b274eb-7689-43df-aa3f-8077689267af
# ╠═33823010-0e63-4f9b-ba29-802e9363c604
# ╟─cd525aa7-386f-4bc3-9814-265411e8a741
# ╟─ef17fd7b-6436-4e0e-a0a7-3a0d5a019fc5
# ╠═fab9da9c-b661-4081-bc72-b5acdb3c019d
# ╠═95a978e2-cbcf-4a0c-90f1-98ba7405f2be
# ╟─8e8f6cee-f6da-4cd9-bf9d-167b7ac89808
# ╠═f0cf3415-571f-4661-8823-2598aef00c2d
# ╟─57f2184d-676e-4da9-99aa-34b0ec7798f9
# ╠═34589d78-9009-412c-8d10-1738729b14b9
# ╟─97abbd67-36fa-4451-9e4f-ab96cf82c82b
# ╠═69b5b90b-9c93-4de2-a106-e0f9e6bd10a4
# ╟─9d1447b8-2d25-4567-b9f0-4b3ead7cd873
# ╠═ae5595c3-0238-4f00-b4ce-d88f366d4e2e
# ╟─3afc20d0-4f2f-40f5-bd8d-00fb1c838b07
# ╠═c1b8fcc1-cbbd-43ae-8669-de7b524186b0
# ╟─abfc5a3d-5024-44a7-af3b-70eefa26dc37
# ╠═9b2e6cb8-67c7-4d15-afde-03a4ae7595da
# ╟─b941833a-0efa-45e8-a59f-a7007653cfc6
# ╟─29d1ada2-75ce-4a77-a136-f761052c0637
# ╟─e6961baa-90c2-4b5d-9bfa-f71355330509
# ╠═08dc0954-02b2-49d2-b3a9-b99d8c41a125
# ╟─b8806c05-7158-4774-a5f7-80f579f44388
# ╠═69f1f5fc-b538-45b3-8e41-3d4822d638ec
# ╠═e72113f2-1608-4e16-adc1-eb359096dbdc
# ╠═07d590d0-3775-468b-a9a3-99d26393d91a
# ╟─f0206a54-b71b-42b6-84da-2b87f348eced
# ╟─41bdb293-d905-4fda-ac48-fa5926222bc7
# ╠═69feaef9-d4b1-4d12-86f6-0aadf030f8eb
# ╟─3654e4d9-8e0a-4154-805a-330fc29ae64c
# ╠═7e1db1d9-0a8b-4056-8a21-9a6c04ad3d9d
# ╟─727ff4b9-6f52-481d-8155-792bc35803db
# ╠═f16db75f-9d13-4650-a025-705ca32a9404
# ╟─e753394e-ad69-47b8-8639-d60a22275c72
# ╠═5c6f4050-b7a8-4f56-aad1-f418b51e2cad