- to make a working chess engine
- learn chess programming in the process
- use time effectively
- defeat stockfish
- clone stockfish
- waste time
- how to write chess engines?
- read some code
- which language to choose?
python
: easy, ready-made libraries, slowjs
: almost same, nice guic++
: hard, have to learnc
: hard and complex, hard to maintain, boring?
- read about chess programming
- uci protocol
- numba+python vs c/c++? what about julia?
- started coding
- c or c++?
- c arrays vs stl arrays/vectors for 2d
- printing unicode pieces or ascii?
- ditching color output for now
- reading
- more reading
- modularizing, exceptions
- started writing this blog lol
- started writing move generation function
(made a snake clone in c++ during this time, check it out!)
- still coding move generation
- moved hard-coded numbers to Enums (
Direction
andPlayer
) - refactored sliding moves (Q, B, R)
- added
pgn
output (ugly but atleast lichess can parse) - now engine generates moves from both sides, yay!
- separated
main.cpp
into several header and cpp files
- added
fen
output, generates direct url to lichess analysis!
- repaired enpassant and castling moves
- refactored move generation function
- minor
san
notation improvements - started legal move generation function
- almost completed legal move generation function
- issue: sometimes switches black and white during random play
- issue: handling castling rights and enpassant squares while undoing a move
- bugfix:
san
notation doesn't show promotion - added
perft
anddivide
function - now we can find corner cases easily!
- wasted 1 hour on why I'm getting 1 move less than expected
reason: code generates kingside
else
queenside castling moves 🥲
- bugfix: now engine checks threatened squares during castling
- improved
perft
function - still a lot of things to do (getting wrong
perft
results)
- found one corner case: castling is still allowed after rook capture!
- fixed a corner case with promotion (earlier it always promoted to white pieces)
- fixed a few corner cases with castling (pawns don't threaten blank squares)
- move count fixed
- planning to automate perft
- graphics???
- another castling bug (capturing rook with king cleared only king's castling rights)
reason:
else
- there's a lot to tell you, but firstly, i'm very happy now because i just teached my ai to capture pieces and guess what, it doesn't just look one move into the future. yes guys, i wrote the negamax algorithm!! it's much more fun to play with it now. but still it has to prioritize other things besides captures, like promotions, checks, pins etc. and obviously, mates!
- now i'm going to sleep as i got many works to do :(
- well, there's a lot of small and big changes. i'll try to explain by checking the diff.
- AI part:
- replaced minimax with alpha-beta (similar to negamax version)
- TODO: implement quiescence search
- added piece-square table scoring in eval function
- reduced branching in code
- move generation part:
- bugfix: moves like
e1c1
were always interpreted as castling, fixed - changed board representation from
string
tochar[64]
- now king positions are incrementally saved, so faster to find kings
- several optimizations in move generation
- bugfix: moves like
- general:
- now engine trys to avoid moves leading to repetitions (but it is not always optimal, so should do something better)
- fixed several gui bugs in promotion and flipping
- gui supports (some) premoves
- fixed some bugs in move hints, still some remaining ig
- faster makefile
- BIGGEST UPDATES:
- uci protocol working!
- made a lichess bot running this engine!
- bugfixes:
- fixed hard to find enpassant bug!
- AI part:
- code refactorings
- engine understands clock!!
- BIGGEST UPDATES:
- iterative deepening
- mate distance pruning
- null move pruning
- late move reduction
- others:
- simplified position hash
- engine now avoids draws!
- TODO: verify all of them, i guess i'm rushing into adding more and more features.
- i've spent around one week on this commit, now it's time for me to focus on my academics :|
- planning to make a major change in the codebase: using
enum
instead ofchar
for pieces. This is to elude branching in code? - added best move tester
- solved a small bug in move generation
- completed the change to
enum
for pieces - maybe the engine is now faster? (because we can now access piece-square tables directly using index without branching)
- better
uci
handling, usingistringstream
to parse commands - better Mate Score output
- added Mate in 2, 3, and 4 tests (and they pass! but slow)
- inlined some functions (maybe faster now?)
- added Search Type:
Infinite
,Fixed Depth
,Time per Move
,Time per Game
,Ponder
andMate
. Not all of them are working yet :')
movelist.reserve(40)
: doing this made move generation ~1.5x faster! Now reserving in all vectors.- modularized move generation
- added
get_threats
function
- wrote README
- plans:
- find a way to make
movelist
faster - make move generation faster, I think checking for checks is the bottleneck.
- find a way to make
- plans:
- handle
uci
commands on a separate thread - optional comments in input using
#
- handle
- implemented! now engine can handle `stop` command after `go` command
- BIG IMPROVEMENT:
- wrote hand-crafted
is_in_threat
function, now engine can avoid moves leading to checks! - earlier it worked by generating all opponent's pseudo-legal moves and checked if any move attacks a square.
- this made move generation faster by 3.5x!
- wrote hand-crafted
...
- cleaned up code
- replaced push_back with emplace_back
- refactored out of board checks for moves
- result: reduced movegen time by 15%!
- working
to_san
function - refactored out legal move checks
- increased max node limit in perft tests
- shocked to see test fail! new bug-hunt begins!!
- bug:
- FEN: r3k2r/p1ppPpb1/1n2pnp1/1b2N3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R b KQkq - 0 2
- castling move generation
- I didn't check the case where the squares adjacent to the king are attacked by a pawn on the 7th rank
- fixed it by using the new
is_in_threat
function
- moved
to_san
function toMove
class - fixed move sorting bug: was considering non-captures first
- added transposition table
- hash table that stores scores of recently searched positions
- replaced
endl
with "\n" (idk if it improves performance) - BUGFIX:
- mate score was displayed wrong
- TODO:
- thoroughly test TT
- incremental zobrist hashing
- TT is working, I'm happy :)
- another bugfix:
- was generating only one en passant capture
- similar to old one side castling bug
else
was the culprit- fixed
- perft test now takes maxnodes as first arg
- ran all perft tests, all passed at full depth!
- changed "\n" to endl, maybe solves the issue of not getting the bestmove detected by GUIs
- program still reports incorrect mate scores
- TODO: fix mate score
- remove unnecessary condition in search function (depth > max_depth) which cannot occur
- increased position eval score weight in endgame
- corrected 'losing king' to mean the opponent's king in endgame pos eval
ply
count now replaces depth to calculate mate score- Finally now the engine outputs correct mate scores! The bug was due to
depth
being used instead ofply
.depth
doesn't increase linearly withply
, so it was giving wrong mate scores (because quiescence search, check extensions, etc. make it unpredictable).
- Finally now the engine outputs correct mate scores! The bug was due to
debug_mode
actually works now
- added 3-fold repetition check, hopefully it works! Earlier I had tried using multiset to store hashes, but I was not sure how to implement it 'inside' search function. Now I'm using a vector to store hashes, and checking for repetitions after every move.
- added tapered eval, now the engine can play endgames better!
- I am astonished by the level at which the engine is playing now! Earlier it was making blunders in endgames, but now it is playing so well! I'm so happy :D
- NEW FEATURES:
- MADE GUI USABLE AGAIN!
- choose random out of best-scoring moves
- can connect to external engines (decoupled my own engine from the GUI, yet to test thoroughly)
- fixed infinite recursion bug in a function (although that function was never called)
- fixed usage of
to_san
function, it should be called only before making the move on the board - draw by repetition yet to be tested in GUI
- main bug was that the
zobrist_init
function was not called at the start of the game, so the hash was same for all positions. Hours of debugging and it was just a single line of code! LOL!! - removed unnecessary include in
main.cpp
and also in Makefile - separated out
get_move_if_legal
frommake_move_if_legal
, it was required elsewhere
- I let my brother play against the engine, and he won! But found a bug in the endgame.
- FEN: 5rk1/2RR1p1p/2p3p1/8/8/p3PK1P/2r3P1/8 w - - 0 28
- Rxf7?? was played, a big blunder! Gotta figure out why.
- fixed gui bug when the board is flipped
- added move manager to avoid deadlock due to recursion, i still dont know whether this is good or bad
- made many GUI improvements, proud of myself :D
- Maybe I should focus more on the engine now. Also, the interprocess communication (whatever it is called, for I hacked up a solution) is working just for my engine executable. For Stockfish, it just spits out a move as fast as it can (any legal move). I have to figure out why this is happening.
- wrote
test_mate.py
to test mate-in-x positions - checked enough positions, I'm sure that the engine is working correctly
- wow, this is a long break! I was busy with my academics and other stuff.
- pushed some pending changes:
- improved engine tester code
- There were a lot of binaries as I archived them, so that I could run matches between different versions of the engine. Some of them are broken, so I needed to delete them. Solution: wrote a bash script to delete all bad binaries.
- plans:
- read all the code again (with fresh and experienced eyes, lol)
- write a better tester, using some kind of framework
- learn about profiling and optimize the engine
- added a utility script to convert UCI moves to PGN, helpful for debugging engine output using lichess, for example
- learnt how to profile using valgrind/callgrind/kcachegrind
- steps:
- compile with
-g
flag - run
valgrind --dump-instr=yes --collect-jumps=yes --tool=callgrind ./runtest perft 4000
- open the output file (callgrind.out.xxxxx) using
kcachegrind
- compile with
- steps:
- found that
is_safe
function is taking a lot of time, tried to memoize it but it didn't help much (before: 0.75s, after: 5s) - added utility function:
movegen_speedtest
- modularized
is_in_threat
function- wrote
slide_find_end
function that returns the end square if a piece slides in a direction, and pieces on those squares are checked for threats - this intends to replace
diagonal_threats
which checked two pieces at a time for threats - this change made move generation ~14% faster!
- wrote
- made move generation 33% faster!!!
- this was a result of rewriting the whole legal move generation logic
- earlier, I was generating all pseudo-legal moves and then checking if they are legal
- now, I generate only legal moves
- so many conditions to handle: single check, double check, absolute pins, castling, en passant, x-ray attacks on king, capturing checking pieces, interposing pieces in check, etc.
- yet to simplify the code, it's become a bit complex now
- also wrote a perft-checking tool in python
- learnt to properly use
subprocess.Popen
- this tool will help me to verify that the engine is still working correctly after optimizations
- works with any UCI engine, not just mine!
- employs a Teacher-Student model: a strong engine (teacher) is used to verify the moves of a weaker engine (student). Greatly reduces the time taken to pinpoint a bug. The tool directly bisects the moves where the engines differ, showing missing and extra moves generated
- thinking of relocating this tool to a whole new repo :)
- UPDATE: https://github.com/sohamkorade/autoperft
- learnt to properly use
- add MoveGenType enum, to better split code for check evasions and non-evasions.
- try to use last move to know if the current position is in check
- just added
-std=c++20
flag to Makefile, and got ~6.6% speedup in move generation!