Mon 17 March 2025
Chess Programming - Bitboards
The "Chess Programming" series:
-
1: (here) Chess Programming - Bitboards
While exploring an interesting game engine which compiles python code to WASM; I thought, there's no better way to have fun than to write a chess engine. This was the start of a one month long project which led me to create my own chess AI.
Chess programming is a topic that has been written about extensively, it has been a proving ground for some of the first AI researchers and even today it is still used as a benchmark for techniques in applied research.
This article is the first part in a series of three in which I attempt to distill some insight into the application of algorithms in software, namely chess programming. In the hopes that it may inspire how you approach solving similar problems in the future.
Bit Operations
If you've ever done some leetcoding or any other live algorithm style interview prep in software engineering you might have come across the odd question on bit operations and wondered if you'll ever need to actually do these in your day to day job. I'm not here to answer that, but I am here to show you how it applies to chess programming.
Firstly what are bit operations:
a bitwise operation operates on a bit string, a bit array or a binary numeral at the level of its individual bits. It is a fast and simple action, basic to the higher-level arithmetic operations and directly supported by the processor.
Essentially bitwise operations are physical operations that are embedded into the processor, they consist of your logical AND, OR, XOR and bit shifts. The mathematical operations that you're familiar with (such as Add, Subtract etc.) are all built upon bitwise operations.
Here's an example of a NOT operation:
NOT 0111 (decimal 7)
= 1000 (decimal 8)
And an example of an AND operation:
1010 (decimal 5)
AND 1100 (decimal 12)
= 1000 (decimal 8)
We can see these operations supported in python:
>>> 0b1100 & 0b1010
8
>>> bin(0b1100 & 0b1010)
'0b1000'
There's two ways we can shift bits. (Left and right) Here's an example in python:
>>> # Left
>>> bin(0b0010 << 1)
'0b100'
>>> # Right
>>> bin(0b0010 >> 1)
'0b1'
Explaining how the processor does addition is out of scope of this blog post, but put together some of these bit operators and you've got yourself a function that can do addition.
Bitboards
Back to the chess:
We are doing all of this essentially to get as close to the processor as possible, since we want the most performance out of a computer as we can by avoiding any unnecessary operations.
In chess programming we will use a single number to keep
track of all the positions of pieces on the board. This is
what we call a "bitboard". We will have a few bitboards
to maintain the game's state. The first one will be 'board'
which tracks all the pieces. At the start of the game
this number will be: 0xFFFF00000000FFFF. Which is
hexadecimal for 18446462598732906495. If we print the
binary form of this number and format it over 8 rows we get
the following:
Bitboard:
8 1 1 1 1 1 1 1 1
7 1 1 1 1 1 1 1 1
6 0 0 0 0 0 0 0 0
5 0 0 0 0 0 0 0 0
4 0 0 0 0 0 0 0 0
3 0 0 0 0 0 0 0 0
2 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1
a b c d e f g h
There are a few other bitboards to track during a game of chess. There will be 2 boards for each colour (white and black). And there will be a bitboard for each type (6). Which gives us a total of 9 bitboards, or 9 numbers to cover the entire game state of chess. Here's two more examples of bitboards, one for pawns and one for white pieces:
>>> pawns = 0x00FF00000000FF00
>>> white = 0x000000000000FFFF
Now we can uses these three numbers to give us the answer to some questions for example; Where are all the white pawns?
>>> board & pawns & white
65280
Bitboard (65280):
8 0 0 0 0 0 0 0 0
7 0 0 0 0 0 0 0 0
6 0 0 0 0 0 0 0 0
5 0 0 0 0 0 0 0 0
4 0 0 0 0 0 0 0 0
3 0 0 0 0 0 0 0 0
2 1 1 1 1 1 1 1 1
1 0 0 0 0 0 0 0 0
a b c d e f g h
During a game of chess you'll use these to calculate which
pieces are under attack. (e.g. given:
knightsmove & theirpieces the result will be 0 if
there's no piece under threat). We can also use these
bitboards workout where the valid moves are with
moves & ~board. (Moves and not board).
A chess AI that's looking 4 moves ahead will have ~5mil positions to consider, so having these operations done on a low level can help boost the performance of the software. The more you squeeze out of performance the more accurate your chess AI will be, because this improves it's ability to look into the future.
Beyond Chess
In summary learning about how bitboards are applied to chess programming has allowed me to understand how bit operations can be applied practically in software. Before I dived into learning how chess is programmed I had only imagined keeping track of the board state using several objects and an array, however I now see there's more than one way to crack an egg; the board state egg.
I hope the article helps to providing insight into practical applications of bit operators and leaves them to be considered as valuable for solving technical problems or at least reminds software engineers that we have them in our toolbelt.
Further Reading
Mon 10 March 2025
Flash Cards
A while back I found out that Jnr. Doctors are keen on using flash cards to help them revise for exams. It occurred to me that I could also use flash cards to drill some of the concepts that would come up in discussions about software systems and system design.
At the time I was developing on a raspberry-pi with a 7-inch screen and I was limited to how much I could install on the little SD card. At first I tried to use a browser based flash card tool, but I was in the mood for doing some deep dives to understanding how these actually work under the hood.
This led me to develop Kanki a bespoke flash card tool for the command line. In this blog post I break down the process I typically use to undertake a software project of this scale (i.e tiny scale).
Requirements
Like all good software projects we start with the high level expectations. What do we want this system to do? (Some of these are quoted straight out of my notebook.
- I want to have 1000+ flashcards based on software and system design
- I should be able to hide the answer, I should be able to reveal the answer.
- Can I input "correct", "close", "wrong" etc and update an SR algorithm to handle recall.
- Nice to have, filtering by topic.
In order to understand how flash cards operate you'd need to dig into figuring how the SR works:
Spaced Repetition: Newly introduced and more difficult flashcards are shown more frequently, while older and less difficult flashcards are shown less frequently in order to exploit the psychological spacing effect. The use of spaced repetition has been proven to increase the rate of learning. - Wikipedia
Entities
We need to know what things we will be working with:
| Card |
|---|
| - Question (string) |
| - Answer (string) |
| - Topic (string) |
| - DeckID (int) |
The card is the question and answer entity which I might have 1000 of (In the end I think I had about 60).
| Deck |
|---|
| - Id (int) |
| - Name (string |
The deck is the entity which groups cards, so a card belongs to a deck and this allows me to have cards around separate topics.
Interface
This isn't far off from what I drew in the notebook but clearly I had a vision and the following sort of captures it at the time:
I want to have a view which allows me to load a specified deck. Create a new deck or add cards to an existing deck.
Load: -> deck_a, deck_b
New: (deck)
Add: (cards)
I want to display the different inputs I can provide as answers to each card.
Again, Hard, Good, Easy
< 1min < 6min < 10min 4 days
These are some of the settings I'd like to fine tune as I get used to playing with the flashcard system.
config:
new cards / day
review cards / day
Spaced Repetition Algorithm
This is where things get slightly more involved since I had to understand how Anki and other flash card apps applied repetition. It was mostly finding the one that suited me best and felt the most natural to play with.
This is the algorithm I went for:
step 1: Cards have a queue field (set to 1 for 'learning') and a
due field (in minutes) determining when a card should be shown again.
step 2: review cards, already studied but are now due to
be relearnt. queue field set to (2) and due less than or
equal to today.
step 3: 'new', queue is set to (0), these new cards are capped.
Essentially this determines how we populate the flash cards during a session. Using the three queue types:
- 0:
new - 1:
learning - 2:
review - 3:
relearning(This is essentially a card deemed as "learnt" but you've forgotten it and now it needs to be relearnt).
Properties of a card.
- due: the timestamp for when the card should be shown again.
- left: how many learning steps before the card graduates to review.
- reps: how many times the card has been shown.
- ivl: tracking the card interval (time to add when recalculating due).
Input
When answering the flash cards there's four inputs you can provide, each of which will cause a different effect, these decide the cards position in one of two or three queues. The result of these inputs also depends on which queue the card is currently in. You can see this here
Again: Reset number of training repsHard: +1 rep, show again in 1 minGood: +1 rep, show again in 5 minEasy: +1 rep, show again in 1 day
Development
Up until this point I hadn't done a single line of code. The research done above was undertaken before I started writing any code. This helps me have a clear understanding of what I'm actually trying to do. It's far easier to scribble away in my notebook that it is to change code.
There are a few things I needed, which I learnt after I'd written the tool. Things such as being able to edit a card, but these were not major features.
The other thing that I find interesting (or obvious) is solution for the queue. During a learning session you need to have the card queues be dynamic since you're popping the earliest due card and then you're sorting them by when they are next due so the implementation fits using a heap quite well. You can see me initialising the heap here.
Conclusion
I really enjoyed reading up on some of the complexities involved in getting the spaced repetition working in a way that allows new topics to be slowly introduced while working on cementing topics that were in the progress of being learnt. It feels a little exciting when you encounter something new during the learning session. I used kanki to prepare on some topics and every so often add new cards to system design.
One thing I found quite useful is getting someone else to ask you the questions verbally and they make a judgement on how close you are to the answer. This can get you more comfortable with talking about these topics and the person reading the questions can help keep you honest about how well you know a topic.
Lastly it's also interesting that learning is effectively broken down into three separate speeds. 1. This is new. 2. I'm actively learning this. 3. I should know this.
Mon 24 January 2022
Configuring Traefik with Redis
Over a weekend I had a quick play around with the
config provider to traefik, experimenting with reverse
proxies. I'm fairly familiar with the redis-cli
and it's fairly easy to build a redis client in any language
so getting off the grounds is a 10min job at most.
I've always got redis container running locally, so I'm sticking with that, to get this running I do:
docker run --name some-redis -p 6379:6379 -d redis
Since I am running traefik on docker-compose and I'll need it to connect to redis, I have to create a network and redis should be connected to this network:
docker network create mynet
docker network connect mynet some-redis
The next step is the compose file for traefik:
# docker-compose.yml
version: '3'
networks:
maunet:
external: true
services:
reverse-proxy:
image: traefik:v2.5
command:
- "--api.insecure=true"
- "--providers.redis"
- "--providers.redis.endpoints=some-redis:6379"
ports:
# HTTP
- "80:80"
# Web UI
- "8080:8080"
networks:
- maunet
And up:
docker-compose up -d
The traefik frontend should be accessible at 127.0.0.1:8080.
Configuration
A few of the traefik concepts didn't make sense at first, but it didn't take long to understand what they're talking about.
-
I have another container running a simple rest server, it's serving on port 3000 which is exposed at port 53782 this container is called
'some-server'. -
A hurdle that took me a while to figure out was to also connect this container to the network we have traefik and redis running on:
docker network connect mynet some-server
- I need to tell traefik a service (server) exists:
set traefik/http/services/<service-name>/loadbalancer/servers/0/url "http://some-server:3000"
- Now assign this service to a router:
set traefik/http/routers/newroute/service some-server
- Lastly give the router a rule: (Note those are backticks around the forward slash.)
set traefik/http/routers/newroute/rule "Path(`/`)"
Now hitting 127.0.0.1:80 will forward the request to your server. You can also do all the other interesting things that traefik provides like loadbalancing and stripping path prefixes.
References
- https://doc.traefik.io/traefik/routing/providers/kv/
Mon 31 August 2020
Python Deque
This is now my third article on lists. As someone that uses the built-in python list on a fairly regular basis, I might have built up a false sense of security. I'm pretty familiar with these listy-boys. However, recently I found out that I was not thinking about them correctly. Readers might smack themselves if they're familiar with data-structures but don't know how lists are implemented internally. The built-in lists are dynamic arrays.
How else could they optimise a sweet O(1) lookup time
on indexing: mylist[4]. Especially when analysts are
trying to avoid the built-in iterator and cursing their code
with: for i in len(mylist): mylist[i].
Another trait an established data-structurer
with be familiar with when it comes to dynamic arrays
is that the append and pop methods are an amortised
O(1). Amortised; because occasionally you have to suffer
a cost of realloc(ating) memory across larger arrays.
Where the list starts to suffer is from poping and
inserting at arbitrary positions.
Linked-List
The data-structurer will have had the linked-list
slammed into their head often enough that it will
pain them to hear about it again. So theory aside,
I'll give you that sweet O(1) append and last item pop
that you expect from a performant Stack.
Python deque provides a comparatively larger
performance hit on initialisation to list and
has poor O(n) performance when you want any arbitrary
item somewhere in the middle. It does, however, have
O(1); popleft, pop, append and appendleft. Due
to being a doubly-linked list (or double-ended queue to
get the abbreviation deque)
Deque in the wild
I saw a nice little quote from an enginneer on Quora:
In 8 years of getting paid to write computer programs, this post is the only time I’ve typed ‘deque.’
There are many places deque is used in the stdlib, most
commonly whenever someone needs a queue or stack such as
constructing a traceback, parsing python's sytax tree and
keeping track of context scope.
My little run-in with deque was using it instead of a
recursive function to avoid python's
maximum recursion depth exceeded
This limit happens to be set to 10^4. The solution was
to add child nodes to a deque and when you were done with
analysing the current node, popleft the next node.
Python Queue
You might be tempted to ask, well if deque is for queues.
What on earth is from queue import Queue.
These queues are different (although, still using deque under
the hood). They are optimised for communication across threads,
which need to involve locking mechanisms and support methods like
put_nowait() and join(). These are not intended to be used
as a collective data-structure, hence the lack of support for
the in operator.
More information
There is some neat documentation in the cpython repo which
contains more data-structures and other alternatives to
the standard built-in list. Tools for working with
lists
References
- How are lists implemented:
- https://stackoverflow.com/a/15121933/3407256
- https://stackoverflow.com/a/23487658/3407256
Thu 16 April 2020
Module not found Heroku
There are some bugs and problems that give a thrill once they're
solved. The best bugs are the ones that teach you something,
the worst bugs are the ones that indicate that you've not
improved your spelling and the difference between an l and a 1
is large.
Figuring out why I was facing the following traceback in heroku provided a time consuming learning experience, but probably one that I won't forget.
heroku[web.1]: State changed from crashed to starting
heroku[web.1]: State changed from starting to crashed
heroku[web.1]: State changed from crashed to starting
app[web.1]: Traceback (most recent call last):
app[web.1]: File "/home/app/server/bin/server", line 3, in <module>
app[web.1]: from api.server import deploy
app[web.1]: ModuleNotFoundError: No module named 'api'
Why can't the module be found 🤔!
Worst of all, the dockerfile builds locally, and when I run it.
It's executing /home/app/server/bin/server and ready to
receive traffic...
FROM python:3.8.2
ENV USER appuser
ENV HOME /home/${USER}
RUN mkdir -p ${HOME}/server
WORKDIR ${HOME}/server
ENV PATH ${HOME}/server/bin:${HOME}/.local/bin:$PATH
USER appuser
CMD ["server"]
I won't go into how long I spent on thinking it had something to do with permission. Looking back, it's quite clear that a permission has nothing to do with it, since the logs would say so.
Take a step back
The difference between the image on heroku and the image running locally. What probably made me assume it was a permission error was the quote from their docs:
containers are not run with root privileges in Heroku
So there was some funky business they're doing to the user I provided.
Get closer to the problem
Finding out that I could get inside the heroku container certainly helped me figure out the problem:
heroku run bash
I could now recreate the Module not found error. I tried using
pipenv to install the module that was missing, however that
didn't work either. hmm.. Where are these modules installed??
Show me the site-packages:
python -m site
Heroku
sys.path = [
'/',
'/usr/local/lib/python38.zip',
'/usr/local/lib/python3.8',
'/usr/local/lib/python3.8/lib-dynload',
'/home/appuser/server/.local/lib/python3.8/site-packages',
'/home/appuser/server',
'/usr/local/lib/python3.8/site-packages',
]
USER_BASE: '/home/appuser/server/.local' (exists)
USER_SITE: '/home/appuser/server/.local/lib/python3.8/site-packages' (exists)
ENABLE_USER_SITE: True
Local Docker Container
sys.path = [
'/home/appuser/server',
'/usr/local/lib/python38.zip',
'/usr/local/lib/python3.8',
'/usr/local/lib/python3.8/lib-dynload',
'/home/hints/.local/lib/python3.8/site-packages',
'/usr/local/lib/python3.8/site-packages',
]
USER_BASE: '/home/appuser/.local' (exists)
USER_SITE: '/home/appuser/.local/lib/python3.8/site-packages' (exists)
ENABLE_USER_SITE: True
Right so they are not referencing the same site-packages. Are the site packages even installed in the heroku container?!?
$ ls /home/appuser/.local/lib/python3.8/site-packages
Flask-1.1.2.dist-info mccabe-0.6.1.dist-info
Flask_Alembic-2.0.1.dist-info mccabe.py
Flask_Cors-3.0.8.dist-info more_itertools
Flask_JWT_Extended-3.24.1.dist-info more_itertools-8.2.0.dist-info
Flask_SQLAlchemy-2.4.1.dist-info oauthlib
🎉 so they aren't missing...
Locally
echo $HOME
/home/hints
Heroku
$ echo $HOME
/home/hints/server
Ah.. So $HOME has something to do with it.
Well I have $HOME=/home/appuser and WORKDIR=/home/appuser/server
perhaps heroku is setting the work directory to the home
directory. 🤷♂️
ENV USER hints
ENV HOME /home/${USER}
RUN mkdir -p ${HOME}
WORKDIR ${HOME}
And sure enough fixing my deployment.