Skip to content

Commit 9f63663

Browse files
committed
Add chess book
1 parent f29eb1f commit 9f63663

5 files changed

Lines changed: 427 additions & 37 deletions

File tree

experiments/chessbook/index.html

Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
<!doctype html>
2+
<html lang="en">
3+
4+
<head>
5+
<script src="/header.js"></script>
6+
<!-- Google tag (gtag.js) -->
7+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-E58GKHEECC"></script>
8+
<script> window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } gtag('js', new Date()); gtag('config', 'G-E58GKHEECC');</script>
9+
10+
<!-- Formulas -->
11+
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
12+
<script>
13+
window.MathJax = {
14+
tex: {
15+
inlineMath: { '[+]': [['$', '$']] }
16+
}
17+
};
18+
</script>
19+
20+
<!-- Code display -->
21+
<link rel="stylesheet" href="https://unpkg.com/@highlightjs/cdn-assets@11.11.1/styles/default.min.css">
22+
<script src="https://unpkg.com/@highlightjs/cdn-assets@11.11.1/highlight.min.js"></script>
23+
<script>hljs.highlightAll();</script>
24+
<link id="hljs-light" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.8.0/styles/github.min.css">
25+
<link id="hljs-dark" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.8.0/styles/atom-one-dark.min.css">
26+
27+
<!-- Post title automation -->
28+
<style>
29+
.post_title {
30+
position: relative;
31+
}
32+
33+
/* Icon in the left margin */
34+
.post_title a .link-icon {
35+
position: absolute;
36+
left: -1.5em;
37+
top: 50%;
38+
transform: translateY(-50%);
39+
opacity: 0;
40+
}
41+
42+
/* Show icon on hover */
43+
.post_title:hover a .link-icon {
44+
opacity: 1;
45+
}
46+
</style>
47+
<script>
48+
document.addEventListener("DOMContentLoaded", () => {
49+
document.querySelectorAll(".post_title").forEach(h => {
50+
if (h.querySelector("a"))
51+
return;
52+
const id = h.id;
53+
if (!id)
54+
return;
55+
56+
const text = h.textContent.trim();
57+
h.textContent = "";
58+
h.classList.add("forehead", "undertow", "charming", "hella", "anchor");
59+
60+
const a = document.createElement("a");
61+
a.className = "relax";
62+
a.href = `./#${id}`;
63+
64+
// Add Font Awesome link icon on the left
65+
const icon = document.createElement("i");
66+
icon.className = "fa-solid fa-link link-icon";
67+
a.appendChild(icon);
68+
69+
// Add the title text
70+
const span = document.createElement("span");
71+
span.textContent = text;
72+
a.appendChild(span);
73+
74+
h.appendChild(a);
75+
});
76+
});
77+
</script>
78+
79+
<script src="https://kit.fontawesome.com/ddb1b5eb41.js" crossorigin="anonymous"></script>
80+
81+
<meta charset="utf-8">
82+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
83+
<meta name="author" content="Paul-Elie Pipelin">
84+
<meta name="description" content="The Ultimate Chess Programming Book. Personal site of Paul-Elie Pipelin, engineer in digital imaging.">
85+
<meta name="keywords" content="Chess, Book, Ultimate, Engineer, Paul-Elie, Pipelin, Digital, Imaging, Computer, Graphics">
86+
<meta name="theme-color" content="#36a1b0">
87+
88+
<title>Paul-Élie Pipelin - The Ultimate Chess Programming Book</title>
89+
<link rel="preconnect" href="https://fonts.googleapis.com">
90+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
91+
<link href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,400;0,550;1,400;1,550&family=Quicksand:wght@400;550&display=swap" rel="stylesheet">
92+
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">
93+
<link rel="stylesheet" href="/styles.css">
94+
<link rel="icon" type="image/x-icon" href="/assets/Torch.png">
95+
</head>
96+
97+
<body>
98+
<a class="finger hella bae-rock +" href="/experiments/chessbook">
99+
english
100+
</a>
101+
<a class="finger hella bae-rock +" href="/experiments/chessbook">
102+
français
103+
</a>
104+
105+
<header id="header">
106+
<h1 class="forehead undertow charming hella ++++">
107+
Paul-Élie Pipelin
108+
</h1>
109+
</header>
110+
111+
<!------------------------------------------------>
112+
113+
<nav class="tight stormsurge +++">
114+
<a class="finger bae-rock hella" href="/">home</a>
115+
<a class="finger bae-rock hella" href="/experiments"><code>experiments</code></a>
116+
</nav>
117+
118+
<!------------------------------------------------>
119+
<div class="book">
120+
<h1 class="forehead undertow charming hella ++++" id="title">
121+
The Ultimate Chess Programming Book
122+
</h1>
123+
This book describes all the elements that a decent chess engine can contain and is written for beginners in <a href="https://www.chessprogramming.org/Main_Page" , target="_blank">chess programming</a>.
124+
<br>
125+
Some parts are illustrated with code written in <a href="https://ziglang.org/" target="_blank">Zig</a> often coming from <a class="finger inline bae-swat hella" href="https://github.com/ppipelin/radiance" target="_blank">Radiance</a> engine. Multiple good guides already exists alongside eponymous engines, like <a href="https://rustic-chess.org/" , target="_blank">Rustic</a> or <a href="http://mediocrechess.blogspot.com/" , target="_blank">Mediocre</a> but isn't exhaustive enough in my opinion.
126+
127+
<h2 class="post_title" id="introduction">Introduction</h2>
128+
Chess is a turn-based board game where all information are known at all time. Position is symmetrical with white color starting. Each piece type moves a certain way.
129+
<br>
130+
The king is the most important piece and cannot move to an attacked square. A game finishes when a player cannot move on its turn. At this moment, if its king is directly attacked by the opponent pieces (<em>in check</em>), the game is lost, (<em>checkmate</em>). Otherwise the game is a draw (<em>stalemate</em>).
131+
<br>
132+
<br>
133+
Each player aims at taking pieces that could defend the king, negate king space on the board to <em>checkmate</em>.
134+
<h2 class="post_title" id="board_representations">
135+
Board representations
136+
</h2>
137+
Let's first dive into the modeling of chess in engines. The board representation can take multiple forms, each having their benefits.<!-- , it can be square-centric or piece-centric. -->
138+
<br>
139+
A common implementation is through mailbox, an array with the size of the board (or bigger) that contains pieces.
140+
<pre><code class="language-rs">const board: [64]Piece = @splat(Piece.none);
141+
// or
142+
const board: [8][8]Piece = @splat(@splat(Piece.none));</code></pre>
143+
This is convenient to quickly assert which piece type is on a specific tile but has some drawbacks:
144+
<ul>
145+
<li>Memory cost</li>
146+
<li>Accession cost</li>
147+
<li>Operations are tile by tile</li>
148+
</ul>
149+
Such issues can be mitigated with bitboards. A bitboard makes the corresponding between a tile on the board and a bit of an integer. For an 8 by 8 chess board, this requires a 64 bits integer. Since only occupancy is described, a single bitboard cannot be enough for piece type nor piece color information. The board state can be described with 8 bitboards, 6 for each piece types and and 2 for each piece colors. A specific piece can be found with a bitwise and between bitboards such as:
150+
<pre><code class="language-rs">const bb_pieces: [PieceType.nb()]u64 = @splat(0);
151+
const bb_color: [Color.nb()]u64 = @splat(0);
152+
... // Fill bitboards
153+
154+
const white_knights: u64 = bb_pieces[PieceType.knight] & bb_color[Color.white];</code></pre>
155+
Extracting the position of a piece in a bitboard can be done with bit manipulation functions by computing the least bit function.
156+
<pre><code class="language-rs">pub inline fn lsb(x: u64) u7 {
157+
return @ctz(x);
158+
}
159+
160+
pub inline fn popLsb(x: *u64) Square {
161+
const l: u7 = lsb(x.*);
162+
x.* &= x.* - 1;
163+
return @enumFromInt(l);
164+
}</code></pre>
165+
<pre><code class="language-rs">while (white_knights != 0)
166+
const white_knight_position: Square = popLsb(&white_knights);</code></pre>
167+
<h2 class="post_title" id="move_representations">
168+
Move representations
169+
</h2>
170+
A minimal definition of a move is the <i>from</i> tile of the moving piece and the <i>to</i> tile. This can be extended with flags and can improve the code readability and performances. For example, knowing a move is a capture without verifying that the <i>to</i> tile is occupied provide a speedup.
171+
<br>
172+
With a maximum value of 64, <i>from</i> and <i>to</i> tiles can be stored in a 6 bits integer and the 16 types of flags can be stored with 4 bits. This is convenient as an move then requires 16 bits which can be stored into an integer, data can be access with bit shifting.
173+
<br>
174+
With Zig elegant packed struct no shifting mechanics are needed:
175+
<pre><code class="language-rs">pub const Move = packed struct {
176+
flags: u4 = MoveFlags.quiet.index(),
177+
from: u6,
178+
to: u6,
179+
};</code></pre>
180+
<h2 class="post_title" id="search_and_evaluation">
181+
Search and Evaluation
182+
</h2>
183+
A chess game is a succession of moves up to a final position. At each turn, each player evaluate every possible moves to pick the one that has the most chances to win.
184+
<br>
185+
To pick a move, the player has to visualize the future board state after doing the move and anticipate the opponent response in the list of all possible moves. This can be done multiple time and is called the <em>depth</em> of analysis.
186+
<br>
187+
<br>
188+
All these combinations of moves can be represented in a tree, the root being the starting position with the first nodes being the first possible moves. In a chess engine looking for possible moves deeper and deeper in the tree is called <em>searching</em>, choosing the best leaf is called <em>evaluating</em>.
189+
<br>
190+
In general, chess game trees have a branching factor of around 30, it is possible to compute the number of leaves for a specific depth with the formula $ leaves = branching\_factor^{depth} $. Hence at depth of 5 is 24 millions leaves while a depth of 8 is 656 billions. A standard chess game has a depth of around 80.
191+
192+
<h3 class="post_title" id="minimax">
193+
Minimax
194+
</h3>
195+
During the game, every player is looking for the best possible leaf in the tree. This behavior can be modeled with a <em>minimax algorithm</em>. At each node, a player will try to maximize the final score and the other one will try to minimize it. It is possible to trace a path in the tree that satisfies both players to eventually arrive at a leaf and evaluate the position.
196+
<br>
197+
Since the game tree can be very long, most of the time, a leaf will not be the final position of the game (win/draw/loss) but another, more advanced position. Chess engine evaluations have to use heuristics to determine <i>how likely</i> is the win from this new board state. This likelihood of win is also called <em>score</em> and often spans from -30 to +30.
198+
<h3 class="post_title" id="alpha-beta">
199+
Alpha-Beta Pruning and Iterative Deepening
200+
</h3>
201+
Minimax requires all the leaves to be evaluated but their number grows exponentially with depth. There is a way to prune some branches without reduce faithfulness and this can be done with alpha-beta pruning.
202+
<br>
203+
Once a first leaf has been evaluated it gives a boundary of the attainable score for each player. Since the players want to either maximize or minimize the score value, none of them would take a path that could result in a worse score than a previously found one.
204+
<br>
205+
A path that will give a score higher than beta would never arise because the losing player would prefer to take the path that obtain a score of beta. Conversely, current player can avoid getting a lower score than alpha by taking the path of alpha. All paths outside of the alpha-beta bounds will be avoided by each player.
206+
<br>
207+
In chess, the likelihood of win is reversible, a the opponent of a player that has a 80% win has 20% chances of lose (or draw). Alpha and beta bounds can then be switched when changing to the other player point of view and the score can be negated. This newer version of minimax is called negamax.
208+
<br>
209+
<pre><code class="language-rs">fn negamax(alpha : Value, beta : Value, depth: u8) Value {
210+
211+
// Termination, leaf evaluation
212+
if (current_depth <= 0) {
213+
return evaluation();
214+
}
215+
216+
var score: Value = -value_none;
217+
218+
// Loop over all legal moves
219+
for (legal_moves) |move| {
220+
position.movePiece(move);
221+
222+
// Transposition Table probing for early return
223+
224+
score = negamax(-beta, -alpha, depth - 1);
225+
226+
position.unMovePiece(move);
227+
228+
if (score > alpha) {
229+
// Fail-high, opponent won't take this path
230+
if (score >= beta) {
231+
break;
232+
} else {
233+
// Found better alpha
234+
alpha = score;
235+
}
236+
}
237+
}
238+
239+
// If no move stalemate or checkmate
240+
if (score == -value_none) {
241+
if (pos.state.checkers != 0)
242+
return -value_mate + ply_number;
243+
return value_stalemate;
244+
}
245+
};</code></pre>
246+
<br>
247+
The first evaluated paths give alpha-beta bounds, also called <em>windows</em>, if the best path is selected first, the narrower the window will be, and the closer it will be to the actual score of the position. With a smaller window, most of the remaining paths will be pruned and very few evaluations will be necessary. With these characterics emerges <em>iterative deepening</em>.
248+
<br>
249+
<br>
250+
A depth of 80 is not attainable with time (and memory) constraints, it is necessary to select a specific depth and avoid getting incomplete results.
251+
<br>
252+
Iterative deepening consists in first searching for depth 1, then depth 2 and so on with each new search initialized using the previously best-scoring path. With alpha-beta pruning, searching depth 1, 2, 3 and 4 successively with the best path first takes almost the same time as searching at depth 4 directly.
253+
<br>
254+
DIAGRAM?
255+
<br>
256+
With the first path as the most important one, many optimizations are possible. The search framework can be upgraded into <em>principal variation search</em> (PVS). This method assumes that only the first path (principal) has to be searched up to the newer depth with a full alpha-beta window. Secondary paths are searched with a null window which corresponds to a bound of [alpha-1, alpha]. Such method allows to quickly remove all worse paths with aggressive pruning.
257+
<br>
258+
During PVS, it can happen that a secondary path gives a result better than alpha, it is then necessary to re-do a search with a full window to improve the position. This is costly and can be avoided by improving the move ordering.
259+
<br>
260+
PVS, Late move reductions.
261+
262+
<h2 class="post_title" id="move_ordering">
263+
Move Ordering
264+
</h2>
265+
With pruning, move ordering is a key factor of a chess engine. It is possible to sort move types into two categories, capturing moves and quiet moves.
266+
<br>
267+
Capturing moves are often changing board state more thant quiet moves and needs to be studied carefully by chess engines. Since chess piece types have different movesets, some are more valuable than other. Ranking captures can be done in multiple ways.
268+
<br>
269+
The most simple way is called <i>Most Valuable Victim - Least Valuable Attacker</i> (MVV-LVA). The best capture is the piece that has the less value on the board that takes the most valuable attacked piece.
270+
<br>
271+
This method only seek for a single capture but pieces are often defended, and it is more reliable to compute captures and re-captures. This is done with the Static Exchange Evaluation (SEE).
272+
<h4 class="forehead undertow charming hella">
273+
Static Exchange Evaluation
274+
</h4>
275+
SEE computes the series of capture on a specific tile of the board. It has to take into account only legal moves. In some cases the exact value of the series of is not needed and this allows early returns by using a threshold. For example, in move ordering it is possible to use a threshold of 1 which which tells if the series of capture benefits the player. Other thresholds can be useful, in bad positions, only looking for hard winning series of exchange is needed.
276+
<br>
277+
278+
<pre><code class="language-rs">pub fn seeGreaterEqual(move: Move, threshold: Value) bool {
279+
280+
if (move.isEnPassant())
281+
return threshold >= value_zero;
282+
283+
const from: Square = move.getFrom();
284+
const to: Square = move.getTo();
285+
const from_piece: Piece = pos.board[from];
286+
const to_piece: Piece = pos.board[to];
287+
288+
var see = material_values[to_piece.pieceToPieceType()] - threshold;
289+
290+
// Cannot gain more than initial piece
291+
// Opponent would not give us more
292+
if (see < 0)
293+
return false;
294+
295+
see = tables.material[from_piece.pieceToPieceType()] - see;
296+
297+
// If we took a valuable piece from a least valuable piece
298+
// with a difference above threshold we exit
299+
// Equivalent to MVV-LVA
300+
if (see <= 0) {
301+
return true;
302+
}
303+
304+
position.movePiece(move);
305+
306+
var result: bool = true;
307+
308+
// Begin series of exchange
309+
while (true) {
310+
position.updateBlockersAndPinned();
311+
position.updateAttackersAndDefenders(); // set attackers and defenders
312+
313+
// Cannot defend piece
314+
if (position.attackers == 0)
315+
break;
316+
317+
result = !result;
318+
319+
// Search for least valuable attacker type
320+
// Remove it, add new attackers and change turn
321+
for (std.enums.values(types.PieceType)) |pt| {
322+
323+
// If capture with king but opponent still has attackers we lose
324+
if (pt == .king)
325+
return if (position.defenders > 0) !result else result;
326+
327+
// Look for PieceType in attackers
328+
const bb: Bitboard = pos.attackers & pos.bb_pieces[pt.index()];
329+
if (bb == 0)
330+
continue;
331+
332+
// Update SEE score if found
333+
see = tables.material[pt.index()] - see;
334+
335+
const new_move = computeNewMove();
336+
position.movePiece(new_move);
337+
}
338+
339+
// Early break because retaking was not worth it from current
340+
if (see < @intFromBool(result))
341+
break;
342+
}
343+
344+
return result;
345+
}</code></pre>
346+
347+
SEE, transposition table
348+
<h2 class="post_title" id="reductions">
349+
Reductions
350+
</h2>
351+
<h2 class="post_title" id="quiescence">
352+
Quiescence
353+
</h2>
354+
<h2 class="post_title" id="staged_move_generation">
355+
Staged Move Generation
356+
</h2>
357+
Update state pinned on move
358+
<h2 class="post_title" id="magic_bitboard">
359+
Magic Bitboard
360+
</h2>
361+
<h2 class="post_title" id="transposition_table">
362+
Transposition Table
363+
</h2>
364+
<h2 class="post_title" id="piece-square_table">
365+
Piece-Square Table
366+
</h2>
367+
<h2 class="post_title" id="tapered_evaluation">
368+
Tapered Evaluation
369+
</h2>
370+
</div>
371+
372+
<footer id="footer" class="forehead +++"></footer>
373+
</body>
374+
375+
</html>

experiments/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ <h1 class="forehead undertow charming hella ++++">
4545
<!------------------------------------------------>
4646

4747
<nav class="tight stormsurge +++">
48+
<a class="finger bae-rock hella" href="experiments/chessbook">The Ultimate Chess Programming Book</a>
4849
<a class="finger bae-rock hella" href="experiments/webgltracer">WEBGL Tracer</a>
4950
<a class="finger bae-rock hella" href="experiments/chesstrainer">Chess Trainer - A tool to learn chess openings</a>
5051
<a class="finger bae-rock hella" href="experiments/rvb">RVB - Game designed in 35 hours at a ESIR 2019's GameJam</a>

footer.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1-
<a class="finger hella bae-kdin" href="https://www.linkedin.com/in/paul-elie-pipelin/" rel="noreferrer" target="_blank">Linked<span style="color: white">in</span></a>
2-
<a class="finger hella bae-wish" href="mailto:paul.elie.pipelin@gmail.com">mail</a>
1+
<footer>
2+
<a class="finger hella bae-kdin" href="https://www.linkedin.com/in/paul-elie-pipelin/" rel="noreferrer" target="_blank">Linked<span style="color: white">in</span></a>
3+
<a class="finger hella bae-wish" href="mailto:paul.elie.pipelin@gmail.com">mail</a>
4+
</footer>

0 commit comments

Comments
 (0)