Computer Chess
Computer Chess
Ulysse Carion,
Junior at La Jolla High School
February 2, 2013
Abstract
The goal of this project is to create a computer program that plays
a relatively strong game of chess using programming techniques used by
the top engines in use today. The result of this project is Godot, a Java
program that plays a redoubtable game of chess.
Godot uses bitboards (64-bit numbers representing a chessboard) to
implement board representation. When searching for moves, the Godot
uses the most common methods of the day, including alpha-beta pruning,
principal variation searching, history heuristics, iterative deepening, quiescent searching, static exchange evaluation, and null move pruning. The
program evaluates positions by taking into account many factors that are
typically an indication of a strong position. Godot can evaluate tens of
thousands of positions per second.
Godot also has an opening book based on a large database of thousands
of very high-quality games. At the time of this writing, Godots opening
database has a little over 252,000 positions pre-programmed into it.
The program is based on other chess engines, especially open-source
ones such as Stockfish, Carballo, and Winglet. Despite being based on
other programs, Godot has a distinctive style of play that has been
repeatedly described as appearing creative.
Godot has achieved an Elo ranking online of above 2100 at 1-minute
chess. It has also defeated multiple FIDE1 -titled players. Though certainly not nearly as strong as commercial chess engines, Godot certainly
plays a very respectable game of chess.
1 F
ed
eration
Contents
1 Acknowledgements
2 Introduction
3 Statement of Purpose
4 Review of Literature
4
4.1 Board Representation . . . . . . . . . . . . . . . . . . . . . . . .
4
4.1.1 Square-Centric Methods . . . . . . . . . . . . . . . . . . .
4
4.1.2 Piece-Centric Methods . . . . . . . . . . . . . . . . . . . .
4
4.1.3 Hybrid Methods . . . . . . . . . . . . . . . . . . . . . . .
5
4.2 Bitboards . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5
4.2.1 Binary Operators . . . . . . . . . . . . . . . . . . . . . . .
5
4.2.2 Implementation . . . . . . . . . . . . . . . . . . . . . . . .
6
4.2.3 Zobrist Hashing . . . . . . . . . . . . . . . . . . . . . . . .
9
4.3 Searching . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
4.3.1 Claude Shannons Types A & B . . . . . . . . . . . . . . 10
4.3.2 Minimax . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
4.3.3 Alpha-Beta Pruning . . . . . . . . . . . . . . . . . . . . . 12
4.3.4 Quiescent Searching . . . . . . . . . . . . . . . . . . . . . 13
4.3.5 Iterative Deepening, Principal Variation, and History Heuristic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
4.3.6 Null Move Pruning . . . . . . . . . . . . . . . . . . . . . . 16
4.4 Evaluation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
4.4.1 Material . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
4.4.2 Pawn Structure . . . . . . . . . . . . . . . . . . . . . . . . 18
4.4.3 Piece-Square Tables . . . . . . . . . . . . . . . . . . . . . 19
4.4.4 King Safety . . . . . . . . . . . . . . . . . . . . . . . . . . 19
5 Development
5.1 Version 0.1
5.2 Version 0.2
5.3 Version 0.3
5.4 Version 0.4
5.5 Version 0.5
5.6 GodotBot .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
21
21
23
23
28
34
34
6 Results
36
7 Recommendations
37
ii
Acknowledgements
Id like to thank Mr. Greg Volger for helping me write my notebook, Dr. David
Groce for his support in the creation of this projects presentation, and Mr.
Martin Teachworth for guiding me and acting as my mentor.
In addition, Id like to thank the chess programming community for helping
me with more technical issues I faced.
Introduction
We stand at the brief corona of an eclipse - the eclipse of certain
human mastery by machines that humans have created.
Stephen Levy
Statement of Purpose
Chess has always been at the heart of computer science and artificial intelligence.
The goal of this project is to research and develop a chess-playing program that
is unique and plays a strong game.
Creating a program that can compete at the level of play of current top-level
programs is almost impossible and not the objective of this projectthe goal is
to create an original program that uses a carefully-selected combination of the
hundreds of ways to assault the challenge of teaching a computer to play chess.
Review of Literature
4.1
Board Representation
Board representation is the usage of some data structure to contain information about a chessboard. A good board representation makes move generation
(determining what moves are available in a position) and board evaluation (determining how strong a position ismore on this in Section 4.4) very fast. There
are overall three different methods of representing a board: square-centric, piececentric, and hybrid solutions.
4.1.1
Square-Centric Methods
A square-centric method is perhaps the most obvious: a program holds in memory what is on each square. Square-centric programs are optimal for quickly
figuring out what piece is on a squaregoing the other way around (viz., determining what square a piece is on) is more difficult. There are many ways
to represent a board in a square-centric method. The most obvious (and least
effective) method is to use an 8x8 array. Though this may seem simple, it is
in fact very cumbersome and requires a lot of work to make sure we do not go
outside the arrays bounds.
A more common square-centric method is the 0x88 method. This technique works by representing a chessboard as an 8 by 16 array. Though the
details of this implementation are complicated (and are not particularly useful
to know), 0x88 is quite fast because it takes little effort to determine if a square
is in boundsif a squares index ANDed2 with the hexadecimal number 0x88
returns anything else than 0, then the index is out of bounds. (Hyatt, 2004).
4.1.2
Piece-Centric Methods
Section 4.2.1
centric methods are optimized for situations where we must find out where a
given piece is, but are far from optimal if we must find out what, if anything,
stands on a given square. The very first chess programs used such a method
called piece-lists, wherein a list storing where each piece stands on the board
is maintained. These piece-lists were optimal in the early days of computing,
when memory was very valuable, because they required very little space on the
computer. (Kotok, 1962). The modern variant of piece-centric representation,
bitboards, will be explained later in section 4.2 (Hyatt, 2004).
4.1.3
Hybrid Methods
4.2
Bitboards
Binary Operators
The bitwise operations AND, OR, and XOR all take two binary numbers as
arguments, and return another binary number as a result. The bitwise AND,
for instance, goes through each bit of its arguments, setting the nth bit of the
result to be 1 if the nth bit of both of its arguments is 1. For example, if we
were to try 10110100 AND 10011000, the result would be:
10110100
& 10011000
10010000
Notice how in the bitwise operator AND, we set the nth bit to 1 if the nth
bit of the first number is 1 and the nth bit of the second number is 1 as
well. Similar logic works for the bitwise OR operator, which sets the nth bit to
1 if the nth bit of the first number or the second number is 1. Thus, taking
the same example as last time,
10110100
| 10011000
10111100
Finally, the XOR operator, or the exclusive or operator, sets the nth bit
to 1 if the nth bit of the first or second (but not both) is 1. With the same
example as before3
10110100
10011000
00101100
The left-shift and right-shift operators are much simpler than the AND, OR,
and XOR operators. These operators simply move the bits of a number left or
right (as they appear when written out). Thus, 00100 left-shifted by 2 gives
10000, and right-shifted by 2 gives 00001. Note that the left-shift and rightshift operators do not replace numbers if they are shifted out; new bits being
inserted at either end of a number being shifted are unconditionally 0. If we
work with a number that has strictly eight bits,
10001010
left-shift by 1 = 00010100
(Note how the 1 at the left-end of the number was not replaced (carried)
back into the resulta zero will unconditionally be placed at the end of the
number.)
11001010
right-shift by 1 = 01100101
(Note how 0 was inserted where there was an opening as the number was
shifted right.)
All of these operators have symbols in programming languages. In C (and
related languages, such as C++ or Java) the operations AND, OR, and XOR
are represented as &, |, and , respectively. In Java, left-shift is <<, and
right-shift is >>> 4 .
4.2.2
Implementation
Bitboards are considered the best way to represent a chessboard because they
are optimized for usage with bitwise operations. The idea is to represent the
64 squares on a chessboard with the 64 bits in a 64-bit number. In Java, these
numbers are called longs (in C, these are called unsigned long longs). We
3 The example below denotes XOR with the symbol , which is also commonly used to
represent the logical XOR operator; however, this symbol is not used in any programming
languages.
4 Many languages represent right-shifting with >>, but in Java this symbol is used for
a variant of bit-shifting that preserves the state of the leftmost bit, an operation called an
arithmetic shift.
can represent the bottom-left square of a chess board as the last (rightmost) bit
of a long, and assign the top-right square to the first (leftmost) bit, as shown in
Figure 1.
57
49
41
33
25
17
9
1
58
50
42
34
26
18
10
2
59
51
43
35
27
19
11
3
60
52
44
36
28
20
12
4
61
53
45
37
29
21
13
5
62
54
46
38
30
22
14
6
63
55
47
39
31
23
15
7
64
56
48
40
32
24
16
8
Figure 1: The position of each bit; board shown from Whites perspective.
We can represent where any type of piece is using a single number thanks
to bitboards. For instance, if we wanted to represent the position of the white
bishops in the initial setup of the board, we would use the number 0000 [...]
000100100, which would correspond to the board shown in Figure 2.
0Z0Z0Z0Z
7
Z0Z0Z0Z0
6
0Z0Z0Z0Z
5
Z0Z0Z0Z0
4
0Z0Z0Z0Z
3
Z0Z0Z0Z0
2
0Z0Z0Z0Z
1
Z0A0ZBZ0
a
b
c
d
e
f
g h
8
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
0
0
0
0
In the previous example, we saw how we could use a single bitboard to represent the white bishops. In the more general case, we have to represent 6 types of
pieces for 2 different colors, resulting in 12 total bitboards minimum. However,
most programs also use three extra bitboards, representing white pieces, black
pieces, and all pieces. Thus, we use 15 bitboards to represent a chess position.
There exist other factors as well (such as whose turn it is to move), but these
are trivial to implement.
Representing a board as a number is especially useful when we want to
evaluate a position. If we want to find all white pawns that are on blacks side
of the board, we can do this using one quick bitwise operation. Lets consider
the pawn position shown in Figure 3.
0Z0Z0Z0Z
Z0Z0Z0Z0
6
0Z0O0Z0Z
5
Z0Z0ZPZ0
4
0Z0Z0Z0Z
3
O0O0Z0O0
2
0O0Z0Z0Z
1
Z0Z0Z0Z0
a
b
c
d
e
f
g h
8
7
0
0
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
1
0
0
0
0
1
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
with our white-pawns bitboard. The result is a bitboard of all white pawns on
blacks side, as shown in Figure 5.
0
0
1
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
1
0
0
0
0
1
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
0
1
0
0
1
0
0
1
0
1
0
1
&
0
0
0
0
0
0
0
0
1
1
1
1
0
0
0
0
1
1
1
1
0
0
0
0
1
1
1
1
0
0
0
0
1
1
1
1
0
0
0
0
1
1
1
1
0
0
0
0
1
1
1
1
0
0
0
0
0
1
1
0
1
0
1
0
=
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
Zobrist Hashing
One of the more difficult-to-handle aspects of board representation is the threefold repetition rule. This rule states that if a position is visited three times
during a game, either side can declare a draw. This rule must be accounted
for whenever the program considers a move, but poses a problem: how do we
efficiently keep a record of the positions our board has been in? The obvious
technique would be to keep a record of our complete previous state (in other
words, to have the board keep an array of previous states, or perhaps create a
linked list of boards), but this is extremely inefficient for two reasons: it would
require much more effort to make a move, and determining if two positions are
the same could require a lot of comparisons. There exists an easier solution:
Zobrist hashes.
Zobrist hashes rely on representing an entire board using a single 64-bit
number as hash of a position. Each element of a position, such as where pieces
are, whose turn it is to move, etc. is represented with a 64-bit bitstring which
we XOR into/out of the Zobrist hash for our position. For example, if we are
moving a pawn from b4 to c5, and we have a Zobrist hash Z of our position,
we would do the following changes:
Z = Z P awnAt[B4]
Z = Z P awnAt[C5]
To undo this change, we could repeat the same process. Because XOR is commutative (A B B A) and associative (A (B C) (A B) C), it
doesnt matter what order XOR things in or out, and we can always undo
our actions by simply repeating the XOR we have executed.
4.3
Searching
Board representation is only the foundations for the actual point of a chess
program: searching. Searching is the process a chess program undergoes to look
for and find the best move available in its position.
4.3.1
Chess artificial intelligence was formalized and developed in a paper, Programming a Computer for Playing Chess, by Claude Shannon. In his seminal work,
Shannon described two types of programs, which he called types A and B. Type
A would be a brute force algorithmall moves are examined, and the best
one is selected; type B would only examine plausible moves. Though it may
seem that a type B approach would be more effective, all strong programs are
nowadays type Athis is due to the fact determining if a move is plausible
takes too much work to be worthwhile. In fact, because type B programs are
essentially nonexistent, I will use the word search to mean a Shannon type A
search from this point forward.
Shannons work is also notable for his estimation of the number of possible
chess positions possiblehe provided the estimated number of different chess
positions that can arise. He finds that the number of positions that can arise is
on the order of
64!
1043
32! (8!)2 (2!)6
This number is stunningly high; so large, in fact, that it dispels any hope of
creating a chess-playing program that does not perform limited-depth searching (Shannon, 1950).
4.3.2
Minimax
The basis of chess artificial intelligence was explored far before computers existed; in 1928 John von Neumann described an algorithm nowadays known as
minimax that could help computers make decisions in games including chess (von
Neumann, 1928).
Minimax works by considering two players, aptly named min and max.
Mins goal is to try minimize the amount of points Max gets. Maxs goal,
obviously, is to try to maximize his score. Maxs technique is to attribute a
5 The likelihood of a collision in a Zobrist hash is remarkably unlikelyso unlikely that
Robert Hyatt (creator of Crafty) and Anthony Cozzie (creator of Zappa) remark that the
danger of collisions isnt a problem that should worry anyone
10
score to every move, and then choose the one with the highest score. Mins
technique is precisely the same thing, except he chooses the move the lowest
score. Max finds out how good a score is by asking Min what he thinks of it,
and Min finds out how good a score is by asking Max the very same question.
Of course, there is a problem hereif Max and Min just ask each other what
the score is all day, we will never find out what Maxs move is. To fix this, we
simply say that when we reach a certain search depth, we stop searching and
just give an estimation of how good a position is using an evaluation function.
In psuedocode, minimax is shown in Figure 6.
function min(depth)
if endOfGame() depth = 0
then
return estimation()
end if
min
for all moves do
makeTheMove()
score max(depth 1)
unmakeTheMove()
if score < min then
min score
end if
end for
return min
end function
function max(depth)
if endOfGame() depth = 0
then
return estimation()
end if
max
for all moves do
makeTheMove()
score min(depth 1)
unmakeTheMove()
if score > max then
max score
end if
end for
return max
end function
credit: Wikimedia
11
Alpha-Beta Pruning
function minimax(depth)
if endOfGame() depth = 0 then
return estimation()
end if
max
for all moves do
makeTheMove()
score minimax(depth 1)
unmakeTheMove()
if score > max then
max score
end if
end for
return max
end function
Figure 8: The Negamax algorithm.
4.3.4
Quiescent Searching
0Z0Z0ZkZ
Z0Z0ZpZ0
6
pm0o0m0Z
5
M0ZpZ0o0
4
PO0OpZ0l
3
ZBsbO0LP
2
0Z0Z0ZKZ
1
Z0Z0S0Z0
a
b
c
d
e
f
g h
8
7
14
function qsearch(, )
standP at estimation()
if standP at then
return
end if
if standP at > then
standP at
end if
for all capturing moves do
makeTheMove()
score qsearch(, )
unmakeTheMove()
if score then
return
end if
if score > then
score
end if
end for
return
end function
Figure 11: The Quiescent Searching algorithm
However, this technique can still be improvedwe can limit ourselves to
only considering captures that are actually winning (this can be done using a
technique called Static Exchange Evaluation, which I do not explain here). We
can also use a clever (if perhaps a bit risky) technique called delta pruning, which
will skip quiescent-searching if no captures will allow the player to overcome
alpha.
4.3.5
15
because alpha-beta works fastest if the best moves are found first (if good moves
are found early on, more moves will be eliminated due to a narrower window
between alpha and beta), it is oftentimes worthwhile to spend a little extra
effort to identify moves that appear to have a lot of potential.
Principal Variation Searching (PVS) identifies the best move at the current
depth (the move that raises alpha) and stores it in order for it to be found again
at the next search depth. Oftentimes (in fact, most of the time), this best move
(the prinipal variation) remains the best move even as we increase our depthsearch, so after we find the principal variation at a low depth (which takes little
time), we quickly find the best move at a high depth because we begin our
search with the principal variation (Marsland and Campbell, 1982).
History heuristic is the technique of maintaining two 64x64 arrays (one for
each side) that represents from and to squares for moves that cause a value
to be returned (be it due to beta cutoff or alpha improvement). When we
progress to the next search depth, we will prioritize moves with high values
in the history heuristic arrays because these are more likely to cause a betacutoff or alpha improvement (which, either way, makes the process of searching
faster). History heuristic is a controversial methodthough simple to implement,
top-level programs avoid it because it ceases to be useful at very high search
depths (Schaeffer, 1989).
4.3.6
Null move pruning is a creative method to quickly detect positions that are
almost certainly going to cause a beta-cutoff. The technique works based on
the assumption that passing ones turn (doing a null move) is detrimental to
ones position.8 If, even despite the handicap of a null move, a player is still
winning in a position (to be more specific, if the player is still causing a betacutoff), then it is reasonable to assume that the position is ridiculous and we
can move on to search more plausible moves (Adelson-Velsky et al., 1975).
There exist many aspects to null move pruning that vary from program to
program. This includes the value of R, the value representing by how much
we reduce our search depth after doing a tentative null-move pruning. Some
programs use the value 2, others 3, and yet others chose between these two
values depending on the current search depth.
A very important aspect of null-move pruning to keep in mind is the possibility of (relatively rare) positions where there doesnt exist any moves better
than the null move. These positions, which are known as zugzwang, occur
when any move a side can make will be to the detriment of his / her position.
Figure 12 shows an example of a zugzwang position; blacks only option is the
move Kb7, whereupon white will be guaranteed to be able to promote his pawn
and win.
To avoid using null-move pruning in zugzwang positions, one simple solution
is to require a certain minimum amount of non-pawn pieces on the board before
8 Null moves are illegal in chessnonetheless, chess programs can still use the assumption
that a null move is detrimental (this is called the null move observation) to their advantage.
16
0ZkZ0Z0Z
7
Z0O0Z0Z0
6
0Z0J0Z0Z
5
Z0Z0Z0Z0
4
0Z0Z0Z0Z
3
Z0Z0Z0Z0
2
0Z0Z0Z0Z
1
Z0Z0Z0Z0
a
b
c
d
e
f
g h
8
4.4
Evaluation
Position evaluation is the third and final element to a chess engine. Search
optimizations can help a program go faster, but only a strong evaluation function
can make it stronger (in terms of its knowledge of chess). Deep Blue, the
famous IBM computer that defeated world champion Garry Kasparov in a 6game match in 1997, had an extremely complex position evaluator that had
been developed for years by multiple grandmasters.
All searching algorithms eventually reach rock bottom and stop searching
any further; instead of going to deeper search depths, they call an evaluator
that estimates how good a position is for whoevers turn it is to move. If
a position is advantageous for the side to move, the evaluator should return a
positive value; if the position is not good for the side to move, the result should
be negative. A dead draw should return 0.
Most chess players are familiar with the idea of certain pieces being worth
more than others. For example, it is relatively common knowledge that a rook
is more or less worth five pawns. However, some other elements to playing, such
as having pieces in good places, are rarely worth as much as a pawn is, even in
the most contrived of scenarios. So how do we represent things that are worth
less than a pawn? The key is to come up with an imaginary unit, the centipawn,
and use this value when evaluating. The advantage to using centipawns is that
we can easily handle things worth less than a pawn (for instance, something
17
Material
Pawn Structure
In chess, a solid position oftentimes comes from having a very strong pawn
structure. Because of the particular way in which pawns can move, these little
pieces can simultaneously be remarkably strong or hopelessly useless. It is up to
the evaluator to detect strong or weak pawn structure. Most programs penalize
three types of weak pawn structure, which are labeled in Figure 13.
In the first diagram, the pawns on a5, b3, and e4 are isolated. In the second
diagram, the pawns on the b and f files are doubled. In the third diagram, the
pawns on d5 and g7 are backward.
0Z0Z0Z0Z
Z0Z0Z0Zp
0Z0o0ZpZ
o0o0Z0Z0
0Z0ZPZPZ
ZPZ0Z0ZP
0Z0Z0Z0Z
Z0Z0Z0Z0
0Z0Z0Z0Z
Z0Z0Z0Z0
0OPZ0Z0Z
Z0ZPZPZ0
0O0Z0O0Z
Z0Z0Z0Z0
0Z0Z0Z0Z
Z0Z0Z0Z0
0Z0Z0Z0Z
Z0o0Z0o0
0ZPZ0o0Z
Z0ZPZPZ0
0Z0ZPZ0Z
Z0Z0Z0Z0
0Z0Z0Z0Z
Z0Z0Z0Z0
18
4.4.3
Piece-Square Tables
An easy way to encourage a chess program to put its pieces in good squares is
to use Piece-Square Tables. These tables are in fact arrays that give a given
bonus (or penalty) for a piece on a specific square. For example, a piece-square
table for white pawns would likely give bonuses for pawns in the center and
for those about to promote. A piece-square table for knights would probably
penalize having the knight along the edge of the board (a knight on the rim is
grim).
An example of a piece-square table for white pawns could look like the
array shown in Figure 14. This particular piece-square table encourages rapid
development of the center pawns and gives bonuses for pawns that have moved
forward, especially if they are closer to the central files. Such a piece-square
table will likely encourage good piece positioning.
0 0 0
0
0
0 0 0
5 10 15 20
20 15 10 5
4 8 12 16
16 12 8 4
3 6 9
12
12
9 6 3
2 4 6
8
8
6 4 2
1 2 3 10 10 3 2 1
0 0 0 40 40 0 0 0
0 0 0
0
0
0 0 0
Figure 14: A possible piece-square table for white pawns.
4.4.4
King Safety
19
as the current reigning champions, it will certainly represent its own unique
take at the challenge. The decisions I make when developing my program will
determine what techniques will be useful to me further down the line. Each
chess program has its own defining characteristics in representation, searching,
and evaluation that distinguish it from others and perhaps adds a very human
aspect to a deterministic process.
20
5
5.1
Development
Version 0.1
Moves
20
400
8902
197281
4865609
119060324
rmblka0s
7
opo0Z0op
6
0Z0Z0o0Z
5
Z0Z0Z0Z0
4
0ZNZ0OnZ
3
Z0Z0Z0Z0
2
POPZ0ZPO
1
SNAQJBZR
a
b
c
d
e
f
g h
8
Time (s)
.163
.854
22.018
224.256
Nodes
1206
25605
793889
8307900
Rate (node/s)
7398.773
29983.606
36056.363
37046.500
Time (s)
.167
.966
15.545
203.086
Nodes
1367
29355
506818
6898488
Rate (node/s)
8185.628
30388.199
32610.281
33968.309
22
Depth
1
2
3
4
5
6
Time (s)
.042
.114
.146
.218
.764
7.319
Nodes
43
157
1198
3829
29777
340529
Rate (node/s)
1023.810
1377.193
8205.479
17564.220
38975.130
46526.711
5.2
Version 0.2
5.3
Version 0.3
What is immediately apparent from the first game (apart from the fact that I
am awful at chess, which is perhaps a little ironic at this point) is that Godot
needs quiescent searching. In this last game, Godot sacrifices a knight with the
move . . . NXd4 due to the fact that he doesnt see through the whole exchange,
only noticing he has the last move and naively concluding this means he wins
the exchange. The final result of this type of searching is the program playing
embarrassing blunders.
My next goal, then, was to implement quiescent searching. This required
that I implement SEE, or Static Exchange Evaluation. This can be implemented
using the SWAP algorithm, which avoids the ostensibly recursive nature of a
program that looks through all possible exchanges at a square. The final result
is a program that is quite a bit slower, but makes very few blunders.
23
rZblkZ0s
opopZpop
6
0ZnZpZ0Z
5
Z0Z0O0Z0
4
0a0O0ZnZ
3
Z0M0ZNZ0
2
POPA0OPO
1
S0ZQJBZR
8
7
At this point Godot makes a flat-out blunder with the startlingly dumb move
6. . . NXd4??
I happily accept the sacrifice.
7 NXd4 Qh4
Unfortunately for me I was even more short-sighted than Godot was here;
8 Bb5?? QXf2m
rZbZkZ0s
opopZpop
6
0Z0ZpZ0Z
5
ZBZ0O0Z0
4
0a0M0ZnZ
3
Z0M0Z0Z0
2
POPA0lPO
1
S0ZQJ0ZR
8
7
rZ0ZkZ0s
opo0Zpop
6
nZ0Z0m0Z
5
ZNZpZpZ0
4
0a0O0Z0Z
3
Z0O0ONZ0
2
PO0Z0OPO
1
S0A0J0ZR
8
7
Godot 0.3 here plays a clever . . . c6!, which forces Godot 0.2 to juggle too
many balls at once ...
10. . . c6! 11 cXb4 cXb5 12 O-O NXb4
rZ0ZkZ0s
opZ0Zpop
6
0Z0Z0m0Z
5
ZpZpZpZ0
4
0m0O0Z0Z
3
Z0Z0ONZ0
2
PO0Z0OPO
1
S0A0ZRJ0
8
7
... and Black is now up a pawn and has a bishop for a knight. The game then
drifts into an uncomfortable endgame for white.
13 Bd2 Nd3 14 a4 bXa4 15 Ne5 NXe5 16 dXe5 Ne4 17 Be1 Nc5 18
Rd1 O-O-O 19 Bb4 Nb3 20 Rd3 f6 21 Rc3+ Kb8 22 eXf6 gXf6 23 Be7
Rc8 24 BXf6 Rhe8 25 Rd1 Rc6 26 RXc6 bXc6 27 Kf1 Kc7 28 Bc3 Kd6
29 Ke2 f4 30 g3 f Xe3 31 f4 Kc5 32 Bf6 Nd2 33 Bc3 Nb3 34 Bf6 Nd2 35
Bc3 Nc4 36 Bd4+ Kd6
25
0Z0ZrZ0Z
o0Z0Z0Zp
6
0Zpj0Z0Z
5
Z0ZpZ0Z0
4
pZnA0O0Z
3
Z0Z0o0O0
2
0O0ZKZ0O
1
Z0ZRZ0Z0
8
7
In the style of Cronus with Uranus and Zeus with Cronus, Godot 0.3 defeats
its predecessor with great gusto.
37 b4 aXb3 38 Bc3 c5 39 Ra1 d4 40 Be1 b2 41 Rb1 Rb8 42 Ba5 Na3
43 Rd1 Kd5 44 Bc3 b1Q 45 RXb1 RXb1 46 BXd4 cXd4 47 Kf3 Rf1+ 48
Ke2 Rf2+ 49 Kd1 Nc2 50 f5 d3 51 Kc1 e2 52 Kb2 e1Q 53 g4 Qa1+ 54
Kb3 Qa3m
0Z0Z0Z0Z
o0Z0Z0Zp
6
0Z0Z0Z0Z
5
Z0ZkZPZ0
4
0Z0Z0ZPZ
3
lKZpZ0Z0
2
0ZnZ0s0O
1
Z0Z0Z0Z0
8
7
0Z0Z0Z0Z
Zpo0s0Zp
6
pZ0Z0Z0Z
5
Z0Z0mkZ0
4
0Z0S0Z0Z
3
ZPZ0Z0o0
2
PZ0Z0o0Z
1
Z0Z0Z0AK
8
7
0Z0Z0Z0Z
Z0Z0ZkZ0
6
0Z0o0Z0Z
5
Z0Z0Z0J0
4
0Z0ZpZ0Z
3
s0Z0Z0Z0
2
0Z0Z0o0Z
1
Z0Z0Z0Z0
8
7
27
0Z0Z0Z0Z
o0Z0ZpZk
6
0Z0Z0Z0O
5
Z0Z0Z0Z0
4
0Z0Z0Z0Z
3
Z0Z0ZPZ0
2
0Z0l0ZqZ
1
Z0Z0ZKZ0
8
7
The results of online play indicated two thingsfirst, that Godot was definitely playing at an extremely high level (to the extent that I was no longer
capable of judging how well it plays purely by looking at its moves), and second,
that Godot could still be improved. 1-minute chess is highly tactical, and that
humans could defeat it meant that it was not searching deeply enough. I still
had many optimizations I could explore at this point. Also, it was becoming
quite evident that my evaluation was not very goodby analysing my games using a stronger program, it became evident that Godot was not putting enough
emphasis on things such as pawn structure or king safety.
5.4
Version 0.4
Godot 0.4 marked two large improvements over its predecessor: null move pruning and better evaluation.
Null move pruning causes a remarkable speed-up over previous implementations. In order to detect potential zugzwang, positions where null move pruning
wont work, Godot does not do null-move pruning if either side has no pieces
(pieces here meaning any chessman except pawns). The results, which are
considerably better than any previous test, are conclusively better and shown
in Figure 20.
Null-move pruning again adds a new level of achievable search depth, making
Godot even stronger. At 5-minute games, Godot can now reasonably play at
depth 6, and is incredibly fast at depth 5. For longer games, Godot can now
reach depth 7. However, likely just as important is the new evaluator, which
takes into consideration:
28
Depth
1
2
3
4
5
6
7
Time (s)
.138
.173
.249
.425
.52
3.928
13.785
Nodes
58
483
1336
6271
9424
103167
377588
Rate (node/s)
420.290
2791.908
32610.281
33968.309
5365.462
18123.077
27391.222
11 United
29
c4 b6 g3 Bb7 Nf3 e6 Bg2 d6 O-O g6 d4 Bg7 Nc3 Nf6 Bg5 O-O [...]
At this point, all we have is a long list of moves in what is called algebraic
notation 12 . My goal at this point was to create a large database of positions and
the most common move in that position. Though it may be tempting to simply
create a large table of lines of algebraic notation and replies in that position,
this would be inadequate. Such an implementation would not consider the move
sequences 1 e4 e5 2 Nc3 and 1 Nc3 e5 2 e4 to be equivalent, despite the fact
that they are13 .
For my program to work correctly with transpositions, it must enter the
moves into the actual chess-playing logic, and ask the chess program itself if two
lines are equivalent. This means I must convert SAN into computer-formatted
moves. Algebraic notation is not optimal for computers, who must try to convert
a move represented in legible format into a move usable by the program. This is
due to the fact that algebraic notation accounts for the ambiguity of a move.
Normally, a move representing a rook moving to f3 would be represented as Rf3
in algebraic notation, but if there are two rooks that can move there, then we
disambiguate by specifying the file or rank (preferably the former) of the piece
moving. This is demonstrated in Figure 21.
0Z0Z0Z0Z
Z0Z0Z0Z0
6 0Z0Z0Z0Z
5 Z0Z0Z0Z0
4 0Z0Z0Z0Z
3 ZrZ0Z0Z0
2 0Z0Z0Z0Z
1 Z0Z0Z0Z0
0Z0Z0Z0Z
Z0Z0Z0Z0
6 0Z0Z0s0Z
5 Z0Z0Z0Z0
4 0Z0Z0Z0Z
3 ZrZ0Z0Z0
2 0Z0Z0Z0Z
1 Z0Z0Z0Z0
Figure 21: With SAN, Rb3f3 is Rf3 in the first position, but Rff3 in the latter.
Once I created a simple utility to convert a SAN move into a computerusable format (using something very similar to Stockfishs method, implemented
in Stockfishs notation.cpp), I could then easily keep track of what positions had
what replies. However, even with a large table of positions and most common
replies, I still need a way to easily find the move Im looking for (assuming it is
even there).
The solution was to create a large Tree Map, a data structure that can link
keys with values. For each position, I used the positions Zobrist hash as its key,
and the move to reply with as its value. The advantage with using a Tree Map
is that its implementation is such that the time to find a move is proportional to
the logarithm of the size of the Map (in computer science terms, this is referred
to as running in O(log n)), meaning my opening database will not become
much slower even if I make my opening database larger.
12 Also
13 In
31
rZbl0skZ
7
opZ0opap
6
0Zno0mpZ
5
Z0o0Z0Z0
4
0Z0OPZ0Z
3
Z0M0ZNZP
2
POPZBOPZ
1
S0AQZRJ0
a
b
c
d
e
f
g h
8
With Godot having consumed less than half a second of thought, the psychological effects of a relatively aggressive computer with time to spare
causes a blunder on whites part.
8 d5 Ne5 9 NXe5 dXe5 10 Be3 b6 11 Bb5 Rb8 12 a4 Qd6 13
32
Nb1??
0sbZ0skZ
o0Z0opap
6
0o0l0mpZ
5
ZBoPo0Z0
4
PZ0ZPZ0Z
3
Z0Z0A0ZP
2
0OPZ0OPZ
1
SNZQZRJ0
a
b
c
d
e
f
g h
8
7
0sbZ0skZ
o0Z0Zpap
6
0o0Z0mpZ
5
OBoqo0Z0
4
0Z0Z0Z0Z
3
Z0Z0APZP
2
0O0Z0ZPZ
1
SNZQZRJ0
a
b
c
d
e
f
g h
8
7
With only a few seconds on the clock and Godot with still over two thirds
of his time remaining, white makes another final blunder.
18 Nc3 Qe6 19 Ba4 c4 20 aXb6 aXb6 21 Qe2 Nh5 22 Bb5??
33
Ng3!
0sbZ0skZ
Z0Z0Zpap
6
0o0ZqZpZ
5
ZBZ0o0Z0
4
0ZpZ0Z0Z
3
Z0M0APmP
2
0O0ZQZPZ
1
S0Z0ZRJ0
a
b
c
d
e
f
g h
8
7
5.5
Version 0.5
In order to make my program quite a bit faster, I decided to change my program from being a hybrid-representation program (containing both piece-centric
and square-centric data) to a completely bitboard-based program. My initial
program had a few dependencies that ran faster with square-centric implementations (namely evaluation), but Godot 0.4 could easily be implemented in a
completely piece-centric way. The only operation that would have to be slowed
down with the change is outputting the board, an operation that is almost never
executed.
With the new, completely bitboard-based program, Godot is quite a bit
faster. With perft, Godot can loop through 2.4 million nodes / second, making
the new change a 70% optimization on its predecessor.
5.6
GodotBot
GodotBot is the nickname gave to the new, final online version of Godot. Playing
on chess.com, GodotBot is how I evaluate exactly how good Godot really is by
putting it up against chess players online from around the world.
34
The main challenge with GodotBot is that it must communicate over the
internet. I chose to implement this by using Selenium, a tool for simulating
a web browser. Specifically, I used Selenium WebDriver with ChromeDriver,
which allows me to automate a Chrome browser in Java. The algorithm I used
is shown in Figure 22.
Set up
Wait for
game to
begin
Start new
game
Wait
for turn
Make move
yes
Game
over?
no
35
Results
The final result of the project is Godot, a chess program that is surprisingly
strong. In sifting through the many options available to me, I made Godot
into a bitboard-based program relying on a combination of null-move pruning,
iterative deepening, quiescent searching, static exchange evaluation, alpha-beta,
PVS, and history heuristics.
For the vast majority of chess players, Godot would be very challenging to
play against, especially in short time controls. However, using Godot to set up
a position and find the best move available takes only two lines of code. The
Godot source code is very user-friendly.
Though Godot is strong, it will not be able to defeat current world-class programs. Godot is not Deep Blue, and will almost certainly lose against Stockfish,
Rybka, Critter, or other top-level engines.
The program is
Fast Godot can generate in excess of 2.4 million nodes per second, and
can evaluate over 30 thousand positions per second.
Fine-Tuned Godots position evaluation takes into account material,
pawn structure, king safety, rook-pawn cooperation, and piece placement
to determine how good a position is.
Unique No single program is the basis for the projects result; Godot
is a combination of techniques inspired from world-class programs and is
original.
Maintainable Godot is written in clean code that is easy to maintain.
36
Recommendations
37
References
G. M. Adelson-Velskii, V. L. Arlazarov, A. R. Bitman, A. A. Zhivotovskii, and
A. V. Uskov. Programming a computer to play chess. Russian Mathematical
Surveys, 25(2):221, 1970.
Georgy Adelson-Velsky, Vladimir Arlazarov, and Mikhail Donskoy. Some methods of controlling the tree search in chess programs. Artificial Intelligence, 6
(4):361371, 1975.
D. J. Edwards and T. F. Hart. The alpha-beta heuristic, December 1961.
Frederic Friedel. A short history of computer chess. Chessbase, 2002.
Robert Hyatt. Chess program board representations, 2004.
Larry Kaufman. The evaluation of material imbalances. Chess Life, March
1999.
Richard E. Korf. Depth-first iterative-deepening: An optimal admissible tree
search. Artificial Intelligence, 27:97109, 1985.
Alan Kotok. A chess playing program. Technical report, MIT, December 1962.
David Levy. Bilbao: The humans strike back. Chessbase, November 2005.
T. A. Marsland and M. Campbell. Parallel search of strongly ordered game trees.
Technical report, Department of Computing Science, University of Alberta,
Edmonton, 1982.
Sren Riis. A gross miscarriage of justice in computer chess. Chessbase, February
2012.
Jonathan Schaeffer. The history heuristic and alpha-beta search enhancements
in practice. IEEE Transactions on Pattern Analysis and Machine Intelligence,
11:12031212, 1989.
Claude E. Shannon. Programming a computer for playing chess. Philosophical
Magazine, 41(314), March 1950.
John von Neumann. Zur theorie der gesellschaftsspiele. Mathematische Annalen,
100(1):295320, 1928.
Mark Allen Weiss. Data Structures & Problem Solving Using Java. Addison
Wesley, 2 edition, 2002.
Albert L. Zobrist. A new hashing method with application for game playing.
Technical Report 88, Computer Sciences Department, University of Wisconsin, April 1970.
38