<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
	<title>DogeyStamp</title>
	<link href="https://www.dogeystamp.com/atom.xml" rel="self" />
	<updated>2026-04-04T17:37:37Z</updated>
	<author>
		<name>dogeystamp</name>
	</author>
	<id>https://www.dogeystamp.com,2023-05-14:default-atom-feed/</id>
	<entry>
		<title>Contributing to Zathura as a novice</title>
		<content type="html">&lt;h1 id=&quot;contributing-to-zathura-as-a-novice&quot;&gt;Contributing to Zathura as a novice&lt;/h1&gt;
&lt;div class=&quot;creation-date&quot;&gt;
2023-10-24
&lt;/div&gt;
&lt;p&gt;Earlier this year I made a &lt;a href=&quot;https://github.com/pwmt/zathura/commit/c11ba01cfbdc57f9d3a61585520ae1ac8c9c4fd3&quot;&gt;patch&lt;/a&gt;
for Zathura, the open-source PDF reader.
In this post, I&amp;#8217;ll document the journey of how I diagnosed the root cause of a bug in Zathura,
then wrote a bugfix for it.
As this is my first &amp;#8220;real&amp;#8221; patch that got accepted, the
most important take-away for me is that it&amp;#8217;s actually not that hard to contribute to open-source.&lt;/p&gt;
&lt;h2 id=&quot;the-bug&quot;&gt;The bug&lt;/h2&gt;
&lt;p&gt;First of all, as seen in &lt;a href=&quot;../typst-notes/&quot;&gt;my last blog post&lt;/a&gt;, I make use of Zathura extensively in my note-taking setup.
On one half of my window, I write markup in Neovim, and in the other half Zathura displays the rendered result.
Every time I save the markup, it is automatically re-compiled, then Zathura reloads the document.
Normally, this works really well, but if you&amp;#8217;re reloading documents often, you&amp;#8217;ll notice that there&amp;#8217;s flicker every time:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/public/img/zathura/flicker.mp4&quot;&gt; &lt;img src=&quot;/public/img/zathura/flicker-thumb.gif&quot; alt=&quot;&quot; /&gt; &lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Of course, this is not a deal-breaking issue.
However, it is definitely annoying if you save often, and even more so if your editor automatically saves on change.&lt;/p&gt;
&lt;p&gt;Upon finding this bug, I did what any normal person does and searched online to see if there was any fixes for it.
I stumbled upon this GitLab issue: &lt;a href=&quot;http://web.archive.org/web/20240616121410/https://git.pwmt.org/pwmt/zathura/-/issues/268&quot;&gt;&lt;em&gt;Prevent flickering when reloading document&lt;/em&gt;&lt;/a&gt;.
Unfortunately for me, the thread was nearly empty, and the last comment was from another user seeking a solution.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://superuser.com/questions/1459927/zathura-flicker-when-updating-pdf&quot;&gt;Another thread&lt;/a&gt;
on Stack Exchange was also a dead end, with no real answer to the problem.&lt;/p&gt;
&lt;p&gt;At this point, there was clearly no easy solution to remove flicker from Zathura.
Since this software is open source, I decided to take a look at the source code in an attempt to diagnose the issue.&lt;/p&gt;
&lt;h2 id=&quot;investigating-source-code&quot;&gt;Investigating source code&lt;/h2&gt;
&lt;p&gt;Zathura&amp;#8217;s source code is available at their git repository.
I cloned it to my projects folder:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https:&amp;#47;&amp;#47;github.com&amp;#47;pwmt&amp;#47;zathura
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At the time, the latest commit was &lt;code&gt;c7baa2f6&lt;/code&gt; (&amp;#8220;Make icon visible on dark backgrounds&amp;#8221;).
If you want to follow along with the exact same code as I saw, run this command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout c7baa2f6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At first glance, it definitely can seem daunting to navigate an unfamiliar codebase.
However, these days, we have language servers, which makes it much faster.
Essentially, language servers are plugins for text editors that give language &amp;#8220;intelligence&amp;#8221;.
With them, you can, for example, jump to the definition of a variable, or get completions when writing code.
I find that language servers are useful especially in situations like these, where you might have no idea where a symbol comes from.
For C, which Zathura is written in, I use &lt;a href=&quot;https://github.com/clangd/clangd&quot;&gt;clangd&lt;/a&gt;, which integrates easily with Neovim.&lt;/p&gt;
&lt;p&gt;A necessary step to use clangd is to have a &lt;code&gt;compile_commands.json&lt;/code&gt; file.
Thankfully, with Zathura&amp;#8217;s build system, Meson, this is automatically generated during the build.
I followed the build instructions at the bottom of the README:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;meson build
cd build
ninja
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, I copied the resulting file back into the root directory.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cp compile_commands.json ..&amp;#47;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, Neovim will automatically have language intelligence.&lt;/p&gt;
&lt;p&gt;Still, we have no idea where the code responsible for the bug lies.
Considering the bug happens upon reload, it would make sense to search for code related to reloading documents.
To do this, I searched for the term &amp;#8220;reload&amp;#8221; using ripgrep (you could use &lt;code&gt;git grep&lt;/code&gt; too):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rg &quot;reload&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From here, I saw an interesting function signature in &lt;code&gt;zathura&amp;#47;shortcuts.c&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;sc_reload(girara_session_t* session, girara_argument_t* UNUSED(argument)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This looks like it could be the code responsible for reloading.
Looking at it, this is the function body (heavily abridged):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;bool
sc_reload()
{
  &amp;#47;* close current document *&amp;#47;
  document_close(zathura, true);

  &amp;#47;* reopen document *&amp;#47;
  document_open();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Surprisingly, reloading a document is implemented by closing and reopening the document,
rather than a separate in-place refresh feature.
This would explain why the screen flickers during reloads:
the document disappears entirely before the new one is loaded in its place.
It seems like there might be a moment between closing and reopening where there is nothing on screen,
which is what causes jarring flicker.&lt;/p&gt;
&lt;h2 id=&quot;fixing-the-bug&quot;&gt;Fixing the bug&lt;/h2&gt;
&lt;p&gt;So now, we&amp;#8217;ve successfully diagnosed the root cause of the flickering.
However, this provides no clear path towards solving it.&lt;/p&gt;
&lt;p&gt;At this point, I decided to check out the &lt;code&gt;document_close&lt;/code&gt; and &lt;code&gt;document_open&lt;/code&gt; functions mentioned above.
After studying these two functions, something caught my eye:
during opening, we create &lt;code&gt;zathura-&amp;#62;pages&lt;/code&gt;, and during closing, we free it.
Looking at the definition, &lt;code&gt;zathura-&amp;#62;pages&lt;/code&gt; is an array of pointers to GtkWidgets.&lt;/p&gt;
&lt;p&gt;As an experiment, I commented out a bunch of code from &lt;code&gt;document_close&lt;/code&gt; to see what happens if the pages are not freed.
In my head, I thought it might leave the old pages on screen and let the new ones replace it.
Obviously, that didn&amp;#8217;t happen, and I got multiple segmentation faults, crashes and memory leaks (womp womp).&lt;/p&gt;
&lt;p&gt;Now, I was practically at a dead end again.
However, I remembered something the author of the Stack Exchange thread tried regarding this issue:
they attempted to set &lt;code&gt;render-loading&lt;/code&gt; to false to fix the flicker.
On default settings, when the document is reloaded, a &amp;#8220;Loading&amp;#8230;&amp;#8221; prompt flickers on screen:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/public/img/zathura/loading.jpg&quot; alt=&quot;Zathura loading prompt&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Turning it off just makes the flickering a blank screen.
I decided to look for code related to this prompt with &lt;code&gt;rg &quot;loading&quot;&lt;/code&gt;.
Doing that, I found an interesting function in &lt;code&gt;zathura&amp;#47;page-widget.c&lt;/code&gt; called &lt;code&gt;zathura_page_widget_draw&lt;/code&gt;.
Skimming the code, it looks like it handles drawing pages to screen.
This was a promising new direction to look into.&lt;/p&gt;
&lt;p&gt;Near the end, there is a section that takes care of drawing the loading screen.
Reading above, we see that the loading screen only renders if the page has no Cairo surface.
Essentially, if the page hasn&amp;#8217;t fully loaded, put up a loading screen.&lt;/p&gt;
&lt;p&gt;Here, I had an idea: what if we replace the loading screen with the original page from before the reload?
That way, it would be a seamless transition from the original document to the new document.&lt;/p&gt;
&lt;p&gt;To implement this, I added two extra pointers to the &lt;code&gt;zathura&lt;/code&gt; struct:
the &lt;code&gt;predecessor_document&lt;/code&gt;, and the &lt;code&gt;predecessor_pages&lt;/code&gt;.
When Zathura closes the document for a reload, it preserves the current document and pages as &amp;#8220;predecessors&amp;#8221;.
Zathura will not free the predecessor document and pages immediately.
Then, in &lt;code&gt;zathura_page_widget_draw&lt;/code&gt;, instead of drawing a loading screen, it will draw the predecessor pages.
Since having an extra buffer also uses more memory, I added a toggle option &lt;code&gt;smooth-reload&lt;/code&gt; that switches this feature on and off.&lt;/p&gt;
&lt;p&gt;Of course, I&amp;#8217;m skimming over many specific details here.
To see the exact code of my patch, you can look at the &lt;a href=&quot;https://github.com/pwmt/zathura/commit/c11ba01cfbdc57f9d3a61585520ae1ac8c9c4fd3&quot;&gt;commit&lt;/a&gt; on Zathura&amp;#8217;s git repository.&lt;/p&gt;
&lt;p&gt;Anyways, here is the end result of the bug-fix, where the bottom window is patched and the top one isn&amp;#8217;t:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/public/img/zathura/before-after.mp4&quot;&gt; &lt;img src=&quot;/public/img/zathura/before-after-thumb.gif&quot; alt=&quot;Before and after the bugfix&quot; /&gt; &lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I was really ecstatic when Zathura first smoothly reloaded a document.
It finally worked!
After this initial success, I collected all these changes into a &lt;a href=&quot;https://github.com/pwmt/zathura/issues/542&quot;&gt;merge request&lt;/a&gt; on GitLab.&lt;sup id=&quot;fnref1&quot;&gt;&lt;a href=&quot;#fn1&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;
Finally, after a month of waiting, I got the maintainer to merge my patch.
All in all, it took a weekend in total to get familiar with the codebase and create this patch.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;&lt;del&gt;At the time of writing, this patch is still only available in the bleeding-edge&amp;#47;git build of Zathura.
To check that my code has made it into your version of Zathura, you can check in the man page &lt;code&gt;zathurarc(5)&lt;/code&gt; for the option &lt;code&gt;smooth-reload&lt;/code&gt;.&lt;/del&gt;
(Since 0.5.5, &lt;code&gt;smooth-reload&lt;/code&gt; &lt;a href=&quot;https://github.com/pwmt/zathura/commit/ef6e7e295c9c046368a3202e8c82efd8b9d24a92&quot;&gt;is no longer an option,&lt;/a&gt;
and it is the default behaviour.)&lt;/p&gt;
&lt;p&gt;As of version 0.5.3, my code for flicker-free reload is enabled by default for all users of Zathura.
Hopefully, this small contribution will improve the experience of future users of Zathura.
Again, what I want to show in this blog post is that contributing to open-source is actually not &lt;em&gt;that&lt;/em&gt; daunting.
When you encounter odd behaviour in your favourite software, or you want improve a feature, do not be afraid to just open it up and start tinkering.&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
&lt;hr/&gt;
&lt;ol&gt;

&lt;li id=&quot;fn1&quot;&gt;
&lt;p&gt;As of today the PWMT GitLab has since moved to GitHub (2025-07-19).&amp;#160;&lt;a href=&quot;#fnref1&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;
&lt;/div&gt;</content>
		<link href="https://www.dogeystamp.com/zathura"/>
		<id>https://www.dogeystamp.com/zathura</id>
		<updated>2023-10-24T10:00:00Z</updated>
		<published>2023-10-24T10:00:00Z</published>
	</entry>
	<entry>
		<title>WireGuard VPN server setup guide for Linux</title>
		<content type="html">&lt;h1 id=&quot;wireguard-vpn-server-setup-guide-for-linux&quot;&gt;WireGuard VPN server setup guide for Linux&lt;/h1&gt;
&lt;div class=&quot;creation-date&quot;&gt;
2024-06-17
&lt;/div&gt;
&lt;p&gt;Recently, I decided to set up a WireGuard VPN server for personal use.
I found that existing guides about this subject are all lacking in some way.
The server setup tutorials I found all go through the commands and configurations needed very throughly,
but don&amp;#8217;t explain &lt;em&gt;why&lt;/em&gt; things work.
Meanwhile, WireGuard&amp;#8217;s &lt;a href=&quot;https://www.wireguard.com/#conceptual-overview&quot;&gt;&lt;em&gt;Conceptual Overview&lt;/em&gt;&lt;/a&gt;
focuses on the protocol itself and how it is different from others.&lt;/p&gt;
&lt;p&gt;So, this post is a summary of my mental model of how WireGuard works,
plus a tutorial for setting up the server.
I assume that you have some knowledge of networking,
but aren&amp;#8217;t familiar with WireGuard.
I also assume Linux knowledge.
This guide should work generally, but I tested it on Arch Linux.&lt;/p&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In this post, if you see an &lt;code&gt;&amp;#47;24&lt;/code&gt; at the end of an IP address, that&amp;#8217;s &lt;a href=&quot;https://en.m.wikipedia.org/wiki/Classless_Inter-Domain_Routing&quot;&gt;CIDR notation&lt;/a&gt;.
&lt;code&gt;192.168.0.0&amp;#47;24&lt;/code&gt; could be written as &amp;#8220;any address that fits the pattern
&lt;code&gt;192.168.0.*&lt;/code&gt;&amp;#8221;. Because there are 32 bits in an IPv4 address, the &lt;code&gt;&amp;#47;24&lt;/code&gt; at
the end means that the first 24 bits (the first three bytes &lt;code&gt;192.168.0&lt;/code&gt;) are
fixed, and the remaining 8 bits can be anything. Also, a &lt;code&gt;&amp;#47;32&lt;/code&gt; pattern
matches only one address.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;what-is-a-vpn&quot;&gt;What is a VPN?&lt;/h2&gt;
&lt;p&gt;A VPN (virtual private network) is often advertised to the average user as an
encrypted tunnel for their Internet connection.
Mostly, this is useful for changing your IP address.&lt;/p&gt;
&lt;p&gt;However, this is not the full picture of what a VPN is.
In corporate networks, there are many important services that can&amp;#8217;t be exposed publicly,
which is why they&amp;#8217;re only available on an internal, private network.
Some off-site users, especially remote workers, need to access the internal network,
but from the outside Internet.
This is where a VPN can be useful: users can &lt;em&gt;virtually&lt;/em&gt; be on the private network,
as if they were on site.
Using a VPN is more secure than publicly exposing services,
as authentication allows fine-grained control over who can access the network.&lt;/p&gt;
&lt;p&gt;For someone like me, VPNs are useful because I self-host private services.
Normally, I would need to expose them to the public Internet to access them.
Instead, I can access them solely through the local network or the VPN,
preventing strangers from even seeing the login pages of my services.
This reduces the attack surface and makes things more secure.&lt;/p&gt;
&lt;h2 id=&quot;wireguard-setup&quot;&gt;WireGuard setup&lt;/h2&gt;
&lt;p&gt;WireGuard is a relatively recent protocol for implementing VPNs.
It&amp;#8217;s shiny and new, and also has a slimmer codebase than older, more established protocols.&lt;/p&gt;
&lt;p&gt;Let&amp;#8217;s go through the process of setting up a VPN to access an internal network.
The setup for tunneling all of a device&amp;#8217;s Internet traffic is similar, and I will explain it too later.
We will have a &lt;em&gt;Client&lt;/em&gt; device, and a &lt;em&gt;Server&lt;/em&gt; device.
The Server is in the internal network (let&amp;#8217;s say in the &lt;code&gt;192.168.0.0&amp;#47;24&lt;/code&gt; subnet),
and the Client is outside of it.
There is also a publicly accessible domain &lt;code&gt;vpn.example.com&lt;/code&gt; that resolves to the Server.&lt;/p&gt;
&lt;p&gt;To recap, here is a diagram of what we&amp;#8217;re trying to do:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 
   ╭────────────╮
   │ VPN Client │
   ╰─────┬──────╯
         │                  ╭────────────────────────────────────────────────╮
         │                  │ the private network (192.168.0.0&amp;#47;24)           │
         │                  │      ╭───────────────────╮                     │
 ╭───────┴─────────╮        │      │ VPN Server        │                     │
 │ public internet ├────────┼──────┤ (vpn.example.com) ├────── ⋯ other hosts │
 ╰─────────────────╯        │      ╰─┬───────────────┬─╯                     │
                            │ ╭──────┴──────╮   ╭────┴────────╮              │
                            │ │ machine 1   │   │ machine 2   │              │
                            │ │ 192.168.0.4 │   │ 192.168.0.7 │              │
                            │ ╰─────────────╯   ╰─────────────╯              │
                            │                                                │
                            ╰────────────────────────────────────────────────╯
                           
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On Linux, one of the quicker ways to set up WireGuard is through &lt;code&gt;wg-quick&lt;/code&gt;,
which is an utility that takes a config file and sets up WireGuard with it.
In Arch Linux, this comes in the &lt;code&gt;wireguard-tools&lt;/code&gt; package:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# pacman -S wireguard-tools
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;wg-quick&lt;/code&gt; stores config files under &lt;code&gt;&amp;#47;etc&amp;#47;wireguard&amp;#47;&lt;/code&gt;.
Each config file has the name &lt;code&gt;[name].conf&lt;/code&gt;,
where the file name is used as the &lt;em&gt;interface&lt;/em&gt; name.
These are the same network interfaces as &lt;code&gt;wlan0&lt;/code&gt; or &lt;code&gt;eth0&lt;/code&gt;;
WireGuard typically uses names like &lt;code&gt;wg0&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;server-configuration&quot;&gt;Server configuration&lt;/h3&gt;
&lt;p&gt;An important thing to understand about WireGuard is that it makes no distinction of server or client:
every device is a &lt;em&gt;peer&lt;/em&gt;.
To authenticate with each other, each peer has a private key,
and a list of public keys of the peers it talks to.
This is similar to how SSH&amp;#8217;s &lt;code&gt;authorized_keys&lt;/code&gt; works.
To generate both the private key and the public key at once, use this command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ wg genkey | tee &amp;#47;dev&amp;#47;tty | wg pubkey
WDax5fhKcKwdeuiYjgi4w&amp;#47;34ig2aZuDjmLHYnWUtfGc=
pJApgMHuIvMMsApTNXA3MMq+82nQ30XuVbAk9jsBNRs=
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The private key will be on the first line, and the public key on the second.
Keep these keys around, as they will be useful later.&lt;/p&gt;
&lt;p&gt;Let&amp;#8217;s now see the config file for the Server:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;# SERVER config
# &amp;#47;etc&amp;#47;wireguard&amp;#47;wg0.conf

[Interface]
Address = 10.0.0.1&amp;#47;32
# REPLACE THIS!
PrivateKey = [private key]
ListenPort = 51820

# make sure to replace eth0 by the appropriate interface
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

[Peer]
PublicKey = xZGlY8HIJt+rhGfbDK&amp;#47;T2xq0LQoR3kL6tGGVijaRBDI=
AllowedIPs = 10.0.0.2&amp;#47;32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, let&amp;#8217;s look at the &lt;code&gt;[Interface]&lt;/code&gt; section, which contains information about the Server.
It has the private key, and also the UDP port that WireGuard listens on
(remember to open this in the firewall).
The address marked here is the Server&amp;#8217;s address.
Its subnet is completely different from the LAN the Server is actually on (&lt;code&gt;192.168.0.0&amp;#47;24&lt;/code&gt;).
This is because we&amp;#8217;re creating a brand new private network (&lt;code&gt;10.0.0.0&amp;#47;24&lt;/code&gt;) inside the VPN connection,
where our Client and Server will coexist.&lt;/p&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;
&lt;code&gt;10.0.0.0&amp;#47;8&lt;/code&gt;, &lt;code&gt;172.16.0.0&amp;#47;12&lt;/code&gt; and &lt;code&gt;192.168.0.0&amp;#47;24&lt;/code&gt; are all entirely reserved for private subnets, and you will not see them on the open Internet.
To avoid collisions, the new VPN subnet should be different from the real LAN subnets for all peers.
OpenVPN &lt;a href=&quot;https://openvpn.net/community-resources/numbering-private-subnets/&quot;&gt;recommends&lt;/a&gt; obscure subnets like &lt;code&gt;10.66.77.0&amp;#47;24&lt;/code&gt;,
which are equivalent to the middle of nowhere in IP address space.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;There is also the &lt;code&gt;PostUp&lt;/code&gt; and &lt;code&gt;PostDown&lt;/code&gt; fields.
These are commands run in &lt;code&gt;bash&lt;/code&gt; after starting and stopping the VPN Server.
I&amp;#8217;m not going to go into the details, but I&amp;#8217;ll explain in general what they mean:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;iptables -A&lt;/code&gt; means to add a firewall rule when starting the VPN, and &lt;code&gt;-D&lt;/code&gt; is to delete the rule when the VPN stops.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;%i&lt;/code&gt; variable is part of &lt;code&gt;wg-quick&lt;/code&gt;, and it expands to the VPN interface name (e.g. &lt;code&gt;wg0&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;These rules in general allow the Server to &lt;em&gt;forward&lt;/em&gt; packets, while &lt;a href=&quot;https://askubuntu.com/a/1295626&quot;&gt;&lt;em&gt;masquerading&lt;/em&gt;&lt;/a&gt;.
Essentially, just as a device can access the Internet through a router, the Client accesses the internal network through the Server.
To do this, the Server will act like a router and perform &lt;a href=&quot;https://en.wikipedia.org/wiki/Network_address_translation&quot;&gt;NAT&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-o eth0&lt;/code&gt; means the internal network is accessed over the &lt;code&gt;eth0&lt;/code&gt; interface.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Meanwhile, in the &lt;code&gt;[Peer]&lt;/code&gt; section, we write the Client&amp;#8217;s public key,
which allows it to talk to the Server.
Our Client has an address of &lt;code&gt;10.0.0.2&lt;/code&gt;, but
instead of an &lt;code&gt;Address&lt;/code&gt; field, we use &lt;code&gt;AllowedIPs&lt;/code&gt;.
These are the IP addresses that &lt;em&gt;can be routed to and from this peer&lt;/em&gt;.
Here are some examples to clarify what that means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Let&amp;#8217;s say the Server wants to send a packet to &lt;code&gt;10.0.0.2&lt;/code&gt;.
WireGuard sees that peer &lt;code&gt;xZGlY...&lt;/code&gt; (the Client) has this IP in its allowlist,
so the Server sends the packet to the Client.&lt;/li&gt;
&lt;li&gt;The Server wants to send a packet to &lt;code&gt;archlinux.org (95.217.163.246)&lt;/code&gt;.
This IP is not in the peer&amp;#8217;s allowlist, so it will not route the packet to the Client.&lt;/li&gt;
&lt;li&gt;The Client sends a packet to the Server, with the source being its IP address &lt;code&gt;10.0.0.2&lt;/code&gt;.
The Server sees that this IP is in the allowlist, so it accepts the packet from the Client.&lt;/li&gt;
&lt;li&gt;The Client, who is now evil, decides to send a packet to the Server but impersonating another IP address: &lt;code&gt;10.0.0.69&lt;/code&gt;.
This IP not being in the Server&amp;#8217;s allowlist, it rejects the packet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In general, you can see that the &lt;code&gt;AllowedIPs&lt;/code&gt; field is what determines where packets can and can&amp;#8217;t go.
By setting &lt;code&gt;AllowedIPs = 10.0.0.2&lt;/code&gt;, the server knows that the Client only has control over packets directed to or from that address.
It is not allowed packets to or from any other IP.
This concept of pairing the allowlist with public keys to manage packet routing is called &lt;a href=&quot;https://www.wireguard.com/#cryptokey-routing&quot;&gt;&lt;em&gt;cryptokey routing&lt;/em&gt;&lt;/a&gt; in WireGuard.&lt;/p&gt;
&lt;p&gt;Also, 
by default, Linux disables IP forwarding. To enable it, edit &lt;code&gt;&amp;#47;etc&amp;#47;sysctl.conf&lt;/code&gt; and add&amp;#47;uncomment the line&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;net.ipv4.ip_forward = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and run&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;# sysctl -p
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to load the new configuration.
If your VPN server is on the public internet,
be sure to have sane firewall rules before doing this.&lt;/p&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you use &lt;a href=&quot;https://wiki.archlinux.org/title/Uncomplicated_Firewall&quot;&gt;UFW&lt;/a&gt; as a firewall like me, note that it has its own &lt;code&gt;sysctl.conf&lt;/code&gt;, which lives at &lt;code&gt;&amp;#47;etc&amp;#47;ufw&amp;#47;sysctl.conf&lt;/code&gt;.
This will override the regular &lt;code&gt;sysctl&lt;/code&gt; if you follow the instructions above.
To prevent it from erasing your changes, uncomment the relevant line:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;# &amp;#47;etc&amp;#47;ufw&amp;#47;sysctl.conf

# Uncomment this to allow this host to route packets between interfaces
net&amp;#47;ipv4&amp;#47;ip_forward=1
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 id=&quot;client-configuration&quot;&gt;Client configuration&lt;/h3&gt;
&lt;p&gt;Let&amp;#8217;s now examine the Client&amp;#8217;s configuration file.
If the client is running Linux, it will also be in &lt;code&gt;&amp;#47;etc&amp;#47;wireguard&amp;#47;*.conf&lt;/code&gt;,
However, the &lt;code&gt;wg-quick&lt;/code&gt; configuration format can be read by many other clients,
like the WireGuard Android app.&lt;/p&gt;
&lt;p&gt;Anyways, here is the configuration file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;# CLIENT config
# wg0.conf

[Interface]
Address = 10.0.0.2&amp;#47;32
# REPLACE THIS!
PrivateKey = [private key]

# be careful not to use CIDR notation here
DNS = 10.0.0.1

[Peer]
PublicKey = M&amp;#47;HD4qJYi1RlMH&amp;#47;K9xQ12yW6Cu62LuGasyZhfnVsbUE=

# do not forget the port here
Endpoint = vpn.example.com:51820

AllowedIPs = 192.168.0.0&amp;#47;24
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&amp;#8217;ll see this configuration is very similar to the Server&amp;#8217;s,
which is natural as WireGuard devices are all peers.
There are a few differences, though.&lt;/p&gt;
&lt;p&gt;First, there is a &lt;code&gt;DNS&lt;/code&gt; field, which can be used to prevent &lt;a href=&quot;https://en.m.wikipedia.org/wiki/DNS_leak&quot;&gt;DNS leaks&lt;/a&gt;.
This only works if the VPN server provides a DNS server too.
Otherwise, set it to some other DNS server, or remove the line.&lt;/p&gt;
&lt;p&gt;There is also the &lt;code&gt;Endpoint&lt;/code&gt; field.
This marks the Server&amp;#8217;s public address on the open Internet.
The &lt;code&gt;Endpoint&lt;/code&gt; field is omitted in the Server configuration,
as it is implicit:
the Server will find out the endpoint IP when the Client reaches out to it for the first time.&lt;/p&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Technically, the &lt;code&gt;Endpoint&lt;/code&gt; field not always strictly necessary.
If &lt;code&gt;SaveConfig&lt;/code&gt; is enabled in the &lt;code&gt;[Interface]&lt;/code&gt; section, and either the Server or Client changes IP address
while connected, &lt;a href=&quot;https://www.wireguard.com/#built-in-roaming&quot;&gt;roaming&lt;/a&gt; allows WireGuard to keep working
and update &lt;code&gt;Endpoint&lt;/code&gt; to the new address in the config file.
I haven&amp;#8217;t tested this, though.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Most importantly, we also have the &lt;code&gt;AllowedIPs&lt;/code&gt; for the server.
Here, it&amp;#8217;s an entire subnet, and not just a single address.
What this means is that if the Client wants to send a packet to a device on the internal network,
say &lt;code&gt;192.168.0.4&lt;/code&gt;, WireGuard matches the IP to the subnet &lt;code&gt;192.168.0.0&amp;#47;24&lt;/code&gt;, takes the packet and routes it through to the server,
which then forwards these packets to the machine at &lt;code&gt;192.168.0.4&lt;/code&gt; in the internal network.&lt;/p&gt;
&lt;p&gt;If the machine then replies with a packet, WireGuard sees it is from the &lt;code&gt;192.168.0.0&amp;#47;24&lt;/code&gt; subnet, then routes it back to the Client.
Using the VPN, the Client will be able to communicate with machines in the internal network as if it were there.&lt;/p&gt;
&lt;h2 id=&quot;running-the-vpn&quot;&gt;Running the VPN&lt;/h2&gt;
&lt;h3 id=&quot;wg-quick&quot;&gt;wg-quick&lt;/h3&gt;
&lt;p&gt;To start&amp;#47;stop the VPN for both Client and Server (if they run Linux),
you can use&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;# wg-quick up wg0
# wg-quick down wg0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;where &lt;code&gt;wg0&lt;/code&gt; should be the name of your configuration file.
Alternatively, there is a SystemD service you can use:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;# systemctl start wg-quick@wg0
# systemctl stop wg-quick@wg0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or, to have WireGuard start at boot:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;# systemctl enable wg-quick@wg0
# systemctl disable wg-quick@wg0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To diagnose issues with WireGuard, running &lt;code&gt;wg show&lt;/code&gt; can be useful:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;# wg show
interface: wg0
  public key: [snip]
  private key: (hidden)
  listening port: 49595

peer: [snip]
  endpoint: [snip]:51820
  allowed ips: 0.0.0.0&amp;#47;0
  latest handshake: 57 seconds ago
  transfer: 4.83 GiB received, 261.16 MiB sent
  persistent keepalive: every 25 seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This can show that there is no handshake at all between peers,
and thus that there is no connection.&lt;/p&gt;
&lt;h3 id=&quot;networkmanager&quot;&gt;NetworkManager&lt;/h3&gt;
&lt;p&gt;There is also the option of using &lt;a href=&quot;https://wiki.archlinux.org/title/NetworkManager&quot;&gt;NetworkManager&lt;/a&gt;,
which is neat if you already use it to manage your existing network connections.
To do this, save your configuration file, and then import it through &lt;code&gt;nmcli&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;$ nmcli con import type wireguard file wg0.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, you can manage the connection as usual through &lt;code&gt;nmcli&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;$ nmcli con down wg0
$ nmcli con up wg0
$ nmcli con edit wg0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;mobile-devices&quot;&gt;Mobile devices&lt;/h3&gt;
&lt;p&gt;On mobile devices, WireGuard apps can also use these configuration files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;F-Droid: &lt;a href=&quot;https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/&quot;&gt;WG Tunnel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Google Play: &lt;a href=&quot;https://play.google.com/store/apps/details?id=com.wireguard.android&quot;&gt;WireGuard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;App Store: &lt;a href=&quot;https://apps.apple.com/us/app/wireguard/id1441195209&quot;&gt;WireGuard&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can securely transfer the config files in a normal way,
but I recommend using QR codes because it&amp;#8217;s way simpler.
Install the &lt;code&gt;qrencode&lt;/code&gt; package,
and do this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;$ cat wg0.conf | qrencode -t ansiutf8

█████████████████████████████████████
█████████████████████████████████████
████ ▄▄▄▄▄ █▄▄▄ ▀▀▀▄▀▄█▀██ ▄▄▄▄▄ ████
████ █   █ ██▄▀ █ ██▀▀▄▄▀█ █   █ ████
████ █▄▄▄█ ██▀▄ ▄▀▄█▄▄ ▄▀█ █▄▄▄█ ████
████▄▄▄▄▄▄▄█ ▀▄█ ▀▄▀ █▄█▄█▄▄▄▄▄▄▄████
████  ▀  ▀▄▀█▄▀█▄▄▄▄█ ▀ ▄▀▄▀▀█▀▀▄████
████▀█▄ ▀█▄  ▄██▄█ ▄   ▀▀▀▀▄ ▀▄█▀████
████▄█▄▄█▄▄█▀▄ █▀▄ ▀█▀▄█ █ ▀▀▀▀▀ ████
████▀ ▀█ █▄▄▄█▀█▀▀  █▄█▄▄▄▄  █ █ ████
████▀▄▀▀▄▀▄██   ▄██ ▄▀█  ███▀▄▀▀█████
████  ██▀▄▄█▀▀█▀▄ ▄▄ █▀▄█▀▄▀█ ▀█▄████
████▄▄▄▄█▄▄█▀ ▀▄▀ █ ▀ ▄█ ▄▄▄  ▄▄█████
████ ▄▄▄▄▄ ██▀▄ ▀ █ ▄ █  █▄█ ▄█▀█████
████ █   █ █ ▄ ▄▄ ▀ ▀█▀▄▄▄▄▄▄▄██ ████
████ █▄▄▄█ █▀   ▄▄█▄▀█▀▄█ █  ▀ ▄ ████
████▄▄▄▄▄▄▄█▄▄████▄▄▄▄█▄▄▄▄▄█▄███████
█████████████████████████████████████
█████████████████████████████████████
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and the QR code will display in your terminal.
On your phone, select the option to add a connection via QR code,
and scan it.
You can pipe any data to &lt;code&gt;qrencode&lt;/code&gt; this way to generate a code,
and it also creates PNG files if you select that format.&lt;/p&gt;
&lt;h2 id=&quot;encrypted-internet-tunnel&quot;&gt;Encrypted Internet tunnel&lt;/h2&gt;
&lt;p&gt;I mentioned earlier that to the average user, a VPN is an encrypted tunnel they can run their Internet traffic through.
The topology is&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Client -&amp;#62; VPN Server -&amp;#62; public internet -&amp;#62; website
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;which makes it look to the website that the traffic originates from the VPN server.
This is actually very similar to our internal network setup from before, which boils down to&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Client -&amp;#62; VPN Server -&amp;#62; internal network -&amp;#62; machine
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To achieve the Internet tunnel topology, we just need to modify the client configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt;[Peer]
PublicKey = M&amp;#47;HD4qJYi1RlMH&amp;#47;K9xQ12yW6Cu62LuGasyZhfnVsbUE=
Endpoint = vpn.example.com:51820

- AllowedIPs = 192.168.0.0&amp;#47;24
+ AllowedIPs = 0.0.0.0&amp;#47;0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This &lt;code&gt;[Peer]&lt;/code&gt; section refers to the Server.
We modify &lt;code&gt;AllowedIPs&lt;/code&gt; so that it includes all possible IPs
(&lt;code&gt;0.0.0.0&amp;#47;0&lt;/code&gt;),
rather than just the internal subnet&amp;#8217;s IPs.&lt;/p&gt;
&lt;p&gt;With this, every time the Client tries to communicate on the Internet,
for instance it requests &lt;code&gt;archlinux.org (95.217.163.246)&lt;/code&gt;,
WireGuard will match that IP to the &lt;code&gt;0.0.0.0&amp;#47;0&lt;/code&gt; (all IPs) mask,
and route the packet to the Server.
Then, the Server will forward that packet to &lt;code&gt;archlinux.org&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Thus, we&amp;#8217;ve made a VPN that works just like NordVPN or ProtonVPN, and all the others.&lt;/p&gt;
&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;/h2&gt;
&lt;p&gt;This guide distills my knowledge with the network topologies I use,
i.e. connecting to an internal network, and connecting to the Internet via WireGuard.
However, that&amp;#8217;s of course not the only way you can use a VPN.
Here are some resources that were useful to me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;man pages &lt;code&gt;wg-quick(8)&lt;/code&gt; and &lt;code&gt;wg(8)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.procustodibus.com/guide/wireguard/&quot;&gt;Pro Custodibus: WireGuard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wireguard.com/&quot;&gt;WireGuard website&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thank you for reading this article!&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;Edit: the &amp;#8220;our internal setup&amp;#8221; diagram has been corrected (2025-03-02)&lt;/p&gt;</content>
		<link href="https://www.dogeystamp.com/wireguard"/>
		<id>https://www.dogeystamp.com/wireguard</id>
		<updated>2024-06-17T10:00:00Z</updated>
		<published>2024-06-17T10:00:00Z</published>
	</entry>
	<entry>
		<title>Note-taking with Typst and Neovim</title>
		<content type="html">&lt;h1 id=&quot;note-taking-with-typst-and-neovim&quot;&gt;Note-taking with Typst and Neovim&lt;/h1&gt;
&lt;div class=&quot;creation-date&quot;&gt;
2023-08-14
&lt;/div&gt;
&lt;div class=&quot;notecard warning markdown-alert markdown-alert-warning&quot;&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt;
&lt;strong&gt;This post is outdated!&lt;/strong&gt; Please see the &lt;a href=&quot;/typst-notes2&quot;&gt;updated 2026 version&lt;/a&gt;,
which is a complete rewrite of this article with updated information.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;A while back, I saw Gilles Castel&amp;#8217;s &lt;a href=&quot;https://castel.dev/post/lecture-notes-1/&quot;&gt;series&lt;/a&gt;
of blog posts about his note-taking setup with LaTeX and Vim.
I was quite inspired by the idea, and I even tried it for a while,
but gave up in the end.
It was less effort to start writing in a plain text file than to write in LaTeX.
So, these were my goals for a note-taking system:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Taking notes should be convenient.
I should be able to to open a new notes page and start writing in a short amount of time.&lt;/li&gt;
&lt;li&gt;The syntax for the notes should be easy to remember.
If I need to look up syntax for common things, I&amp;#8217;m wasting time.&lt;/li&gt;
&lt;li&gt;I should be able to put basic math, figures, etc. in my notes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Eventually, I found a typesetting system called &lt;a href=&quot;https://github.com/typst/typst&quot;&gt;Typst&lt;/a&gt;.
Typst is kind of a mix between LaTeX and other formats like Markdown.
On one hand, it&amp;#8217;s designed for working with math and other technical subjects, like LaTeX.
On the other hand, it has a relatively simple syntax reminiscent of Markdown.
Compare these two snippets:&lt;/p&gt;
&lt;p&gt;LaTeX:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;Then, starting from 2:
\begin{enumerate}
    \item If the current number is composite, we skip to the next number.
    \item Mark all factors of the current number as composite.
    \item Move to the next number.
\end{enumerate}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Typst:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typst&quot;&gt;Then, starting from 2:
+ If the current number is composite, we skip to the next number.
+ Mark all factors of the current number as composite.
+ Move to the next number.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because of the above advantages, I decided to start writing things down in Typst.
In this blog post, I&amp;#8217;ll be documenting my process and tools for Typst note-taking at the time of writing.&lt;/p&gt;
&lt;h2 id=&quot;neovim-setup&quot;&gt;Neovim setup&lt;/h2&gt;
&lt;p&gt;Currently, I&amp;#8217;m using the &lt;a href=&quot;https://github.com/kaarmu/typst.vim&quot;&gt;typst.vim&lt;/a&gt; plugin to integrate Typst with Neovim.
It provides basic syntax highlighting, and recognizes &lt;code&gt;.typ&lt;/code&gt; files as Typst files.
The plugin also has a &amp;#8220;watch&amp;#8221; command that compiles your Typst file on change to a PDF.
However, I haven&amp;#8217;t figured out how to have it display compiler errors, so I have this in my Neovim config:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vim&quot;&gt;function TypstWatch()
    vsp
    vertical resize 20
    exec &amp;#39;terminal &amp;#39; .. &amp;#39;typst watch &amp;#39; .. expand(&quot;%:&quot;)
    exec &quot;norm \&amp;#60;c-w&amp;#62;h&quot;
endfunc
nnoremap &amp;#60;silent&amp;#62;&amp;#60;leader&amp;#62;fc :call TypstWatch()&amp;#60;cr&amp;#62;
nnoremap &amp;#60;silent&amp;#62;&amp;#60;leader&amp;#62;fr :silent exec &quot;!zathura --fork &quot; . expand(&quot;%:p:r&quot;) . &quot;.pdf &amp;#38;&quot;&amp;#60;cr&amp;#62;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This sets up a bind to open &lt;code&gt;typst watch&lt;/code&gt; in a terminal pane on the right,
with another bind to open the PDF in Zathura.
Every time I save the content in my text editor, it updates the PDF seamlessly.
It looks like this on screen:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/public/img/typst-notes/nvim.jpg&quot;&gt; &lt;img src=&quot;/public/img/typst-notes/nvim-thumb.jpg&quot; alt=&quot;&quot; /&gt; &lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;templates&quot;&gt;Templates&lt;/h3&gt;
&lt;p&gt;Users of LaTeX or Typst often use templates in order to configure things like document size, layout, fonts, style, and so on.
Instead of redefining these settings every time, we can just import them from a file.
I won&amp;#8217;t go into detail here; read the relvant Typst &lt;a href=&quot;https://typst.app/docs/tutorial/making-a-template/&quot;&gt;documentation&lt;/a&gt; for information.
For reference, you can find my own &lt;a href=&quot;https://github.com/dogeystamp/typst-templates&quot;&gt;templates&lt;/a&gt; at GitHub along with instructions for using them.
The developers of Typst are working on better ways of packaging templates at the moment, so beware that they&amp;#8217;re not the best example of how to write templates.&lt;/p&gt;
&lt;p&gt;Anyways, you always need to import the templates for every new document you make,
and you also need to pass in parameters like the title of the document.
Typing all this boilerplate manually would be tedious, if done for every document.
This is where &lt;a href=&quot;https://github.com/SirVer/ultisnips&quot;&gt;UltiSnips&lt;/a&gt; comes in handy.
UltiSnip provides snippets, which are templates of text that you can rapidly paste in the editor.
For example, I&amp;#8217;ve set it up so typing &lt;code&gt;today&lt;/code&gt; and pressing Tab prints out the current date:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/public/img/typst-notes/today.gif&quot;&gt; &lt;img src=&quot;/public/img/typst-notes/today-thumb.gif&quot; alt=&quot;&quot; /&gt; &lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Snippets also support slots where you can type your own text.
Every time Tab is pressed again, it moves to the next slot instantly.
Here, you can see my snippet for new documents in action:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/public/img/typst-notes/general.gif&quot;&gt; &lt;img src=&quot;/public/img/typst-notes/general-thumb.gif&quot; alt=&quot;&quot; /&gt; &lt;/a&gt;&lt;/p&gt;
&lt;p&gt;With this setup for Neovim, I have achieved the goal of being able to start writing new notes rapidly.
It only takes around twenty seconds to open a new file in the editor, then fill out the template information,
after which I can start writing.&lt;/p&gt;
&lt;h3 id=&quot;version-control&quot;&gt;Version control&lt;/h3&gt;
&lt;p&gt;When dealing with any textual content, a good practice is to put it under version control like Git.
This is the norm for code, but it&amp;#8217;s also viable for LaTeX&amp;#47;Typst content.
The main advantages of this are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You&amp;#8217;re sure you won&amp;#8217;t accidentally delete any data,&lt;/li&gt;
&lt;li&gt;You can roll back to older versions, and&lt;/li&gt;
&lt;li&gt;You can easily view what changed between versions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For that reason, I set up a private GitHub repo for storing all my notes in a single folder.
Besides that, I also set up the templates as a submodule.
Like this, I can clone the notes repository and also get my templates set up at the same time.&lt;/p&gt;
&lt;h2 id=&quot;math-code&quot;&gt;Math &amp;#38; code&lt;/h2&gt;
&lt;p&gt;In the notes I&amp;#8217;m writing, I often have to deal with mathematical expressions and code.
You can see &lt;a href=&quot;https://castel.dev/post/lecture-notes-1/#latex-snippets&quot;&gt;here&lt;/a&gt;
that Castel gets a lot of use out of UltiSnips for rapidly typing math in LaTeX.
However, in Typst, I find that these snippets are mostly unnecessary,
since it has cleaner, simpler syntax for math in general.&lt;/p&gt;
&lt;p&gt;For example, this is the LaTeX syntax for a fraction:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;\frac{3}{2}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Typst has a much more minimal syntax:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;3&amp;#47;2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Typst also has a simpler math &amp;#8220;environment&amp;#8221; syntax.
Anything between dollar signs &lt;code&gt;$&quot;like this&quot;$&lt;/code&gt; is inline math.
To use display math, add any spacing &lt;code&gt;$ &quot;like this&quot; $&lt;/code&gt;.
With &lt;a href=&quot;https://github.com/jiangmiao/auto-pairs&quot;&gt;auto-pairs&lt;/a&gt; for Neovim,
it only takes a single keystroke to start a math expression,
and another to end one.
See it here in action:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/public/img/typst-notes/math.mp4&quot;&gt; &lt;img src=&quot;/public/img/typst-notes/math-thumb.gif&quot; alt=&quot;&quot; /&gt; &lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Another bit of syntax sugar that&amp;#8217;s built-in to Typst is support for code blocks.
Inline code is introduced by single backticks,
while multi-line code is introduced by triple backticks.
This is &lt;em&gt;exactly&lt;/em&gt; the same syntax as in Markdown, which is much more familiar than &lt;code&gt;\begin{verbatim}&lt;/code&gt;.
It also provides syntax highlighting out of the box, without needing to import lstlisting:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/public/img/typst-notes/code.mp4&quot;&gt; &lt;img src=&quot;/public/img/typst-notes/code-thumb.gif&quot; alt=&quot;&quot; /&gt; &lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;figures&quot;&gt;Figures&lt;/h2&gt;
&lt;p&gt;Being able to draw diagrams in the notes is also a necessary feature.
This part of my setup is mostly unaltered from Castel&amp;#8217;s own method of drawing figures.
Read his own &lt;a href=&quot;https://castel.dev/post/lecture-notes-2/&quot;&gt;blog post&lt;/a&gt; regarding drawing figures for the best explanation of how it works.
To briefly summarize his post, Castel uses Inkscape to draw figures,
with a shortcut script that accelerates the process.
He says that he can draw almost as fast as his lecturers draw on the blackboard,
although I&amp;#8217;m not nearly skilled enough to do the same.
Here&amp;#8217;s an example from his blog post:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/public/img/typst-notes/castel-fig.jpg&quot;&gt; &lt;img src=&quot;/public/img/typst-notes/castel-fig-thumb.jpg&quot; alt=&quot;&quot; /&gt; &lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Again, read Castel&amp;#8217;s blog post about drawing figures.
Here, I will only explain the modifications I made to his process,
assuming you already know how it works.&lt;/p&gt;
&lt;p&gt;So, first of all, Castel&amp;#8217;s shortcut manager (&lt;a href=&quot;https://github.com/gillescastel/inkscape-shortcut-manager&quot;&gt;GitHub&lt;/a&gt;)
is quite useful, and does indeed speed up drawing considerably.
However, it&amp;#8217;s built to deal with LaTeX only,
which is not compatible with my notes.
Therefore, I patched his scripts so that they hook into Typst rather than latexmk.
The results of this are available &lt;a href=&quot;https://github.com/dogeystamp/inkscape-shortcut-manager&quot;&gt;on GitHub&lt;/a&gt;.
Besides that, I also have a snippet for figures in Typst.
It has slots for inputting the filename&amp;#47;ID of the figure, and also a caption for it.
Once the snippet is pasted, I can hover over the filename and press &lt;code&gt;&amp;#60;leader&amp;#62;ff&lt;/code&gt;,
which opens that figure in Inkscape, or creates it if it doesn&amp;#8217;t exist.
Then, I can draw things, and have it appear in my notes.
Here&amp;#8217;s a full demo of drawing a figure (click for a non-gif version):&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/public/img/typst-notes/figure.mp4&quot;&gt; &lt;img src=&quot;/public/img/typst-notes/figure-thumb.gif&quot; alt=&quot;&quot; /&gt; &lt;/a&gt;&lt;/p&gt;
&lt;p&gt;For now, there&amp;#8217;s the limitation that there is no equivalent of PDF+LaTeX for Typst.
This means that the font for the document is not synced to the font in the figures themselves.&lt;/p&gt;
&lt;h2 id=&quot;linkingreferences&quot;&gt;Linking&amp;#47;references&lt;/h2&gt;
&lt;p&gt;Often, when I&amp;#8217;m writing notes for a certain topic, I might want to reference another.
Now, it is possible to use a relative path link to another PDF,
but that doesn&amp;#8217;t allow for referencing specific pages or sections.
Castel himself solved this problem with &lt;a href=&quot;https://github.com/gillescastel/instant-reference&quot;&gt;Instant Reference&lt;/a&gt;,
which creates links with the &lt;code&gt;phd:&amp;#47;&amp;#47;&lt;/code&gt; protocol, which is then handled specially by a script.
However, I decided to make my own &lt;a href=&quot;https://github.com/dogeystamp/pyinstantref&quot;&gt;rewrite&lt;/a&gt; of the script in Python, in contrast to the original written in JavaScript.
I prefer having my packages managed by the system&amp;#8217;s package manager, rather than by &lt;code&gt;npm&lt;/code&gt;.
Python is better in this regard, because Arch Linux packages essential Python packages under &lt;code&gt;extra&amp;#47;python-.*&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I have a bind set up to instantly copy a link to the currently viewed page in the PDF.
For example, here I link to my algorithm notes in a document:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/public/img/typst-notes/reference.mp4&quot;&gt; &lt;img src=&quot;/public/img/typst-notes/reference-thumb.gif&quot; alt=&quot;&quot; /&gt; &lt;/a&gt;&lt;/p&gt;
&lt;p&gt;An extra feature I have compared to Castel&amp;#8217;s version is referencing specific sections.
This is helpful when you modify notes afterwards and a page number points to different content.
A key bind in my PDF reader triggers a rofi menu where you select one of the sections visible on the current page,
after which you can paste the link to that section seamlessly in your notes.&lt;/p&gt;
&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;I&amp;#8217;ve been using the above system for taking notes on competitive programming problems
and computer science for a few months.
I think that my goals of having a system for notes that was convenient, easy, and featureful has been accomplished.
Indeed, I haven&amp;#8217;t had to open up a plain text document for taking notes in quite a while;
everything is in Typst now.&lt;/p&gt;
&lt;p&gt;If you want to see the real configuration files behind all of it, check out my dotfiles &lt;a href=&quot;https://github.com/dogeystamp/dots&quot;&gt;on GitHub&lt;/a&gt;.
The important files are &lt;code&gt;src&amp;#47;.local&amp;#47;bin&amp;#47;typst-figure&lt;/code&gt;, and &lt;code&gt;src&amp;#47;.config&amp;#47;nvim&amp;#47;typst.vim&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Maybe this post will sway you to check out Typst.
But if it hasn&amp;#8217;t, look at the example on their &lt;a href=&quot;https://github.com/typst/typst&quot;&gt;GitHub page&lt;/a&gt;.
Hopefully, one day Typst will get wider usage, because it&amp;#8217;s a great typesetting system.&lt;/p&gt;</content>
		<link href="https://www.dogeystamp.com/typst-notes"/>
		<id>https://www.dogeystamp.com/typst-notes</id>
		<updated>2023-08-14T10:00:00Z</updated>
		<published>2023-08-14T10:00:00Z</published>
	</entry>
	<entry>
		<title>Note-taking with Typst and Neovim in 2026</title>
		<content type="html">&lt;h1 id=&quot;note-taking-with-typst-and-neovim-in-2026&quot;&gt;Note-taking with Typst and Neovim in 2026&lt;/h1&gt;
&lt;div class=&quot;creation-date&quot;&gt;
2026-01-01
&lt;/div&gt;
&lt;p&gt;More than two years ago, I wrote an article about the process of &lt;a href=&quot;/typst-notes&quot;&gt;taking notes
with Typst in Neovim&lt;/a&gt;; soon after, it became the most popular
post on this blog.&lt;sup id=&quot;fnref1&quot;&gt;&lt;a href=&quot;#fn1&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; Since then, however, the contents of that article have
become outdated, so I&amp;#8217;m writing this as an updated version of that post.
The goal of this article is to again document how I write lecture notes in Typst,
but now with the tooling of 2026.&lt;/p&gt;
&lt;p&gt;Now that I&amp;#8217;ve spent two years using Typst for taking notes, I can say that I&amp;#8217;m
quite happy with it. I&amp;#8217;m at the level where I can effortlessly type notes as
fast (sometimes faster) than instructors can write on the board, including
mathematical notation. I also use Typst when typing up homework.&lt;/p&gt;
&lt;h2 id=&quot;recap&quot;&gt;Recap&lt;/h2&gt;
&lt;p&gt;First, let&amp;#8217;s recall the goals for my note-taking.&lt;/p&gt;
&lt;p&gt;During lectures, I take notes in real time on my laptop,
and I do not spend time after class to clean up my notes.
This is a real example of how my notes look like (click for more detail):&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/public/img/typst-notes2/pages.png&quot;&gt;&lt;img src=&quot;/public/img/typst-notes2/pages.png&quot; alt=&quot;Image of two pages from my typed notes.&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Because of the way I write notes, it has to be fast and efficient,
otherwise I can&amp;#8217;t keep up with the lecture.
More precisely, the following are my criteria for a good note-taking setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Taking notes should be convenient.
That is, I can open my notes and start writing almost immediately.
The system should be fast and responsive.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The syntax for writing notes should be easy to remember and to write.
If the syntax for common things like fractions are painful to type, I&amp;#8217;d rather use plain text or handwritten notes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I need my notes to support math, figures, and related features for my courses.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Originally, I was quite inspired by &lt;a href=&quot;https://castel.dev/post/lecture-notes-1/&quot;&gt;Gilles Castel&amp;#8217;s
system&lt;/a&gt; which uses LaTeX for
note-taking. Unfortunately, LaTeX fails some of the criteria above; it compiles
slowly, which is inconvenient, and it has cryptic, weird syntax from decades
ago.&lt;sup id=&quot;fnref2&quot;&gt;&lt;a href=&quot;#fn2&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; So when I tried to write LaTeX, I ended up just giving up and writing
plain text documents, because they are simply more convenient to write.&lt;/p&gt;
&lt;p&gt;Then, I found &lt;a href=&quot;https://github.com/typst/typst&quot;&gt;Typst&lt;/a&gt;,
which is sort of a mix between LaTeX and Markdown.
On one hand, just like LaTeX, it&amp;#8217;s designed for writing about math and other technical subjects,
so it supports equations and figures.
On the other hand, like Markdown, Typst has simple syntax.
For instance, compare the notation for lists:&lt;/p&gt;
&lt;p&gt;LaTeX:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;\begin{enumerate}
    \item This is the first list item.
    \item Another entry.
    \item A last entry.
\end{enumerate}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Typst:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typst&quot;&gt;+ This is the first list item.
+ Another entry.
+ A last entry.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Typst notation requires much less typing than the LaTeX notation. This is
just one example; I would say that in general writing things in Typst is less
wordy than writing it in LaTeX.&lt;/p&gt;
&lt;p&gt;So, according to my criteria for a good note-taking system, Typst is the best,
which is why I use it.&lt;/p&gt;
&lt;h2 id=&quot;compiling-typst-in-neovim&quot;&gt;Compiling Typst in Neovim&lt;/h2&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This post focuses on local compilation, integrated with an editor (Neovim). For
those who don&amp;#8217;t live in the terminal, Typst has a closed-source &lt;a href=&quot;https://typst.app&quot;&gt;web
app&lt;/a&gt;, which is equivalent to Overleaf for LaTeX.
The web app does have basic Vim binds, if that is what you need.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Let&amp;#8217;s get started writing Typst.&lt;/p&gt;
&lt;p&gt;Once you have Typst installed,
you can already start writing in a &lt;code&gt;.typ&lt;/code&gt; document with your favourite text editor.
Run &lt;code&gt;typst watch document.typ&lt;/code&gt;, and Typst will compile to &lt;code&gt;document.pdf&lt;/code&gt;.
There is no boilerplate required; just start writing text.&lt;/p&gt;
&lt;p&gt;This process is fine, but navigating directories and opening a text editor, then a PDF reader
can be slow.
I wrote above that I want to be able to &lt;em&gt;immediately&lt;/em&gt; write,
and that is indeed possible with the workflow I use.&lt;/p&gt;
&lt;p&gt;In this video, I demonstrate how I can start writing notes in less than 10 seconds:&lt;/p&gt;
&lt;p&gt;&lt;video width=&quot;640&quot; height=&quot;360&quot; controls&gt;
  &lt;source src=&quot;/public/img/typst-notes2/open.mp4&quot; type=&quot;video/mp4&quot;&gt;
Your browser does not support the video tag.
&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;In this section, I&amp;#8217;ll show how you can also integrate Typst to your editor like this.&lt;/p&gt;
&lt;h3 id=&quot;typst-watch&quot;&gt;Typst watch&lt;/h3&gt;
&lt;p&gt;I still use the same setup as I did in 2023: I have VimScript binds that open
&lt;code&gt;typst watch&lt;/code&gt; in a tiny side pane, and open a PDF reader (Zathura) to view the
output besides the editor.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vim&quot;&gt;function GitRoot()
    return fnamemodify(finddir(&amp;#39;.git&amp;#39;, &quot;;&quot;), &quot;:h&quot;)
endfunc

&quot; compile typst doc on write
function TypstWatch()
       vsp
       vertical resize 20
       exec &amp;#39;terminal &amp;#39; .. &amp;#39;typst watch --root &amp;#39; .. GitRoot() .. &quot; &quot; .. expand(&quot;%:&quot;)
       exec &quot;norm \&amp;#60;c-w&amp;#62;h&quot;
endfunc

nnoremap &amp;#60;silent&amp;#62; &amp;#60;leader&amp;#62;fc :call TypstWatch()&amp;#60;cr&amp;#62;

nnoremap &amp;#60;silent&amp;#62;&amp;#60;leader&amp;#62;fr :silent exec &quot;!zathura --fork &quot; . expand(&quot;%:p:r&quot;) . &quot;.pdf &amp;#38;&quot;&amp;#60;cr&amp;#62;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every time I save the Typst source file, it compiles
and shows the result in the Zathura window:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/public/img/typst-notes2/zathura.png&quot; alt=&quot;Screenshot with the Neovim window on the left showing an error from typst output, and Zathura on the right.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As you can see, errors are also displayed in the side pane.&lt;/p&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;
A difference from the original code is that we now have to set a compilation root for Typst.
If your file accesses external data (e.g. images on disk), the data needs to be within the compilation root.
(This feature prevents Typst files from reading your SSH private keys.)
For my setup, I set the root of my notes Git repository as the compilation root.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 id=&quot;terminal-navigation&quot;&gt;Terminal Navigation&lt;/h3&gt;
&lt;p&gt;To take notes, you first need to be able to navigate to the folder where your notes are, and open Neovim there.
I&amp;#8217;ve optimized this part of the workflow using zoxide and fish.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ajeetdsouza/zoxide&quot;&gt;Zoxide&lt;/a&gt; is a &amp;#8220;smarter cd command&amp;#8221;,
which lets you cd to directories without typing their full name.
For instance, my notes are all in folders like &lt;code&gt;~&amp;#47;nt&amp;#47;university&amp;#47;25&amp;#47;term1&amp;#47;coursename&lt;/code&gt;,
which would be tedious to type every time.
But with Zoxide, I can write &lt;code&gt;z nt course&lt;/code&gt; and it will jump to the correct directory.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/fish-shell/fish-shell&quot;&gt;Fish&lt;/a&gt; is a shell which is much much much better than bash by default.
One of Fish&amp;#8217;s great features is autosuggestions:
when you type the start of a command, it will suggest commands from your history, which you can accept with &lt;code&gt;Ctrl-F&lt;/code&gt;.
As you can see here, I can type just &lt;code&gt;z&lt;/code&gt; and it will complete the entire command:&lt;/p&gt;
&lt;p&gt;&lt;video width=&quot;640&quot; height=&quot;360&quot; controls&gt;
  &lt;source src=&quot;/public/img/typst-notes2/fish.mp4&quot; type=&quot;video/mp4&quot;&gt;
Your browser does not support the video tag.
&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;Fish autosuggestions save &lt;em&gt;lots&lt;/em&gt; of typing, and the best part is that fish includes this feature
right out of the box. There&amp;#8217;s zero configuration&lt;sup id=&quot;fnref3&quot;&gt;&lt;a href=&quot;#fn3&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; for all good features in fish;
just install it, and it&amp;#8217;s an amazing shell right out of the box.
Both Fish and Zoxide are cool because they save me immense amounts of time in any context
in the terminal, not just taking notes.&lt;/p&gt;
&lt;p&gt;Now that you understand the tools, here&amp;#8217;s the process for opening notes at lightning speed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Open a terminal using a shortcut in my tiling window manager.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;z nt [course code]&lt;/code&gt; to get to a course&amp;#8217;s notes folder.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;nvim main.typ&lt;/code&gt; (I just have to type &lt;code&gt;nv&lt;/code&gt; and fish completes the rest).&lt;/li&gt;
&lt;li&gt;Press &lt;code&gt;,fc&lt;/code&gt; (start compiling Typst) and &lt;code&gt;,fr&lt;/code&gt; (open Zathura on this file).&lt;/li&gt;
&lt;li&gt;Start writing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;video width=&quot;640&quot; height=&quot;360&quot; controls&gt;
  &lt;source src=&quot;/public/img/typst-notes2/open.mp4&quot; type=&quot;video/mp4&quot;&gt;
Your browser does not support the video tag.
&lt;/video&gt;&lt;/p&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;
(2026-01-18) Thanks to the reader who wrote to me saying navigating
directories added lots of friction to writing Typst with Neovim. I realized I
didn&amp;#8217;t mention anything about zoxide and fish, which are tools I take for
granted, but make the terminal experience so much more fun;
I&amp;#8217;ve now added this section to explain how fish and zoxide improve my workflow.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 id=&quot;tinymist&quot;&gt;Tinymist&lt;/h3&gt;
&lt;p&gt;Since 2023, many things have changed in the Typst ecosystem. We now have proper
Language Server Protocol (LSP) server support for Typst, which helps editors
understand and manipulate Typst code. You can install the
&lt;a href=&quot;https://github.com/Myriad-Dreamin/tinymist&quot;&gt;Tinymist&lt;/a&gt; language server, which
has many features, like autocomplete, error detection, code formatting, and
notably instant preview in a browser window. Every time you edit the document,
Tinymist automatically compiles it and shows you a preview. You can even click
in the preview, and have it sync position with the editor window:&lt;/p&gt;
&lt;p&gt;&lt;video width=&quot;640&quot; height=&quot;360&quot; controls&gt;
  &lt;source src=&quot;/public/img/typst-notes2/preview.mp4&quot; type=&quot;video/mp4&quot;&gt;
Your browser does not support the video tag.
&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;The one issue I have with Tinymist is that using it drains battery on my laptop (based on personal experience)
which is a big disadvantage when typing notes in class.
I assume this is caused by Tinymist recompiling at every keystroke rather than every time the file is saved.
Because it consumes more power, I actually don&amp;#8217;t use Tinymist that often,
except for writing libraries in Typst.&lt;/p&gt;
&lt;p&gt;To keep this post brief, I&amp;#8217;ll avoid specifying too many details about
how to set up language servers in Neovim; there is plenty of information out
there already, and the configuration depends on how your specific setup works.
One thing I will add here is that once you have Tinymist running, you can
start the preview with the following Lua command (which you should bind to a key):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-lua&quot;&gt;vim.lsp.get_clients({ name = &quot;tinymist&quot; })[1]:exec_cmd({ command = &quot;tinymist.startDefaultPreview&quot;, title = &quot;Preview&quot; })
&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;
If you hate having to configure everything but still like Vim-style editing,
try the &lt;a href=&quot;https://github.com/helix-editor/helix&quot;&gt;Helix&lt;/a&gt; editor, which automatically
configures language servers (and many other features) for you. Once you have
Tinymist running, use this command to start the preview:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:lsp-workspace-command tinymist.startDefaultPreview
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I am looking to switch to Helix, but as of now the snippet support isn&amp;#8217;t good enough.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;math-code&quot;&gt;Math &amp;#38; code&lt;/h2&gt;
&lt;p&gt;I often need to include math, and sometimes code in my notes.
Typst is good at handling either of these.&lt;/p&gt;
&lt;h3 id=&quot;math&quot;&gt;Math&lt;/h3&gt;
&lt;p&gt;To write inline math, write &lt;code&gt;$1 + 1 = 2$&lt;/code&gt; (no spaces),
and to write display math, write&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typst&quot;&gt;$
1 + 1 = 2.
$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(Configure your auto-pairs plugin to recognize &lt;code&gt;$&lt;/code&gt; in Typst files.)
You can write multiline equations with &lt;code&gt;\&lt;/code&gt;, and align them with &lt;code&gt;&amp;#38;&lt;/code&gt;, for instance&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typst&quot;&gt;$
2 &amp;#38;= 1 + 1 \
  &amp;#38;= 1 + 1 - 1 + 1
$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;gives this output:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/public/img/typst-notes2/multiline.svg&quot; alt=&quot;The output of the above Typst code.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As I mentioned above, the best part of Typst is how concise it is compared to LaTeX.
Instead of &lt;code&gt;\frac{3}{2}&lt;/code&gt;, you can write &lt;code&gt;3&amp;#47;2&lt;/code&gt;,
and instead of &lt;code&gt;\mathbb{R}&lt;/code&gt;, it&amp;#8217;s just &lt;code&gt;RR&lt;/code&gt;.
This conciseness is especially good for people like me who are typing lecture notes in real time in class.
At this point, I can sometimes type math faster than teachers can write it on the board.&lt;/p&gt;
&lt;p&gt;See &lt;a href=&quot;https://github.com/johanvx/typst-undergradmath/releases/download/v1.5.1/undergradmath.pdf&quot;&gt;typst-undergradmath&lt;/a&gt;
for a long list of examples of typesetting math in Typst.&lt;/p&gt;
&lt;p&gt;Some things are still a bit tedious to write, though.
For instance, to type a sigma sum, you need to type &lt;code&gt;sum_(i=1)^n&lt;/code&gt;.
While typing this expression, I mainly don&amp;#8217;t like reaching my fingers off the home-row to reach the &lt;code&gt;^&lt;/code&gt; key.
A similar issue occurs with definite integrals, which are written &lt;code&gt;integral_a^b&lt;/code&gt;.
To optimize typing math, I use snippets in Neovim, specifically using the &lt;a href=&quot;https://github.com/L3MON4D3/LuaSnip&quot;&gt;LuaSnip&lt;/a&gt; plugin.&lt;sup id=&quot;fnref4&quot;&gt;&lt;a href=&quot;#fn4&quot; rel=&quot;footnote&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;
Snippets are like textual templates that let you fill in the blanks.
When I type &lt;code&gt;sum&lt;/code&gt; and then press Tab, it autocompletes the sum notation &lt;code&gt;sum_()^()&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;video width=&quot;640&quot; height=&quot;360&quot; controls&gt;
  &lt;source src=&quot;/public/img/typst-notes2/snippet.mp4&quot; type=&quot;video/mp4&quot;&gt;
Your browser does not support the video tag.
&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;Unlike in LaTeX though, snippets are more a convenience rather than a necessity
for typing fast, since Typst syntax is already short by itself. I would say
that even without snippets, as long as you type fast, you can write Typst
markup as fast as a lecturer writes on the board.&lt;/p&gt;
&lt;p&gt;Snippets are also nice to use in other contexts,
like completing &lt;code&gt;today&lt;/code&gt; to &lt;code&gt;2026-01-01&lt;/code&gt; in a journal file.&lt;/p&gt;
&lt;h3 id=&quot;code&quot;&gt;Code&lt;/h3&gt;
&lt;p&gt;Other than math, you might also want to write code blocks in Typst.
Doing this is easy, and it uses the exact same notation as in Markdown, i.e. single ticks for inline code,
and three backticks for code blocks:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typst&quot;&gt;This is `code`.

```python
print(&quot;this is code&quot;)
```&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/public/img/typst-notes2/code.svg&quot; alt=&quot;The output of the above Typst code.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;It&amp;#8217;s just like typing code on Discord,
and it even has syntax highlighting out of the box.&lt;/p&gt;
&lt;h2 id=&quot;figures&quot;&gt;Figures&lt;/h2&gt;
&lt;p&gt;Another essential feature for writing lecture notes is drawing figures.
The system I use is still generally the same as Gilles Castel&amp;#8217;s method:
draw figures in Inkscape, and include them in the Typst document.
However, compared to 2023, my setup is a bit different.
This is what the workflow looks like:&lt;/p&gt;
&lt;p&gt;&lt;video width=&quot;640&quot; height=&quot;360&quot; controls&gt;
  &lt;source src=&quot;/public/img/typst-notes2/figure.mp4&quot; type=&quot;video/mp4&quot;&gt;
Your browser does not support the video tag.
&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;When writing notes, whenever I want to insert a figure, I use a &lt;code&gt;fig&lt;/code&gt; snippet,
which inserts code like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typst&quot;&gt;#figure(
  image(&quot;fig&amp;#47;test2&amp;#47;label.svg&quot;),
  caption: [Caption],
) &amp;#60;label&amp;#62;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(The &lt;code&gt;label&lt;/code&gt; and &lt;code&gt;Caption&lt;/code&gt; bits are blanks in the snippet that I fill in, and
&lt;code&gt;test2&lt;/code&gt; is the filename of the document.) Then, after naming the figure, I
hover over the filename and press a keybind to edit the figure. The keybind triggers a &lt;a href=&quot;https://github.com/dogeystamp/dots/blob/8299e0b2c0db4d287640e588d075985385fba81e/src/dot_local/bin/executable_typst-figure&quot;&gt;script&lt;/a&gt; (&lt;code&gt;typst-figure&lt;/code&gt;)
which creates the file if it doesn&amp;#8217;t exist, and then opens Inkscape.&lt;/p&gt;
&lt;p&gt;One important difference from my old setup is that I now use the
&lt;a href=&quot;https://textext.github.io/textext/&quot;&gt;TexText&lt;/a&gt; plugin to insert Typst math expressions
in my figures. I edited the source code in my version of TexText so that the &amp;#8220;save text&amp;#8221;
bind is &lt;code&gt;Ctrl-s&lt;/code&gt;, which can be pressed by the left hand while my right hand
draws with the mouse.
Another difference is that I no longer use Gilles Castel&amp;#8217;s Inkscape Shortcut Manager,
and instead just use the default Inkscape binds (which aren&amp;#8217;t too bad actually).&lt;sup id=&quot;fnref5&quot;&gt;&lt;a href=&quot;#fn5&quot; rel=&quot;footnote&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Something I did keep from Castel&amp;#8217;s setup is the quick style changer. He had
&amp;#8220;chorded&amp;#8221; style keybinds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Space + A: arrow&lt;/li&gt;
&lt;li&gt;Space + A + G: bold&lt;sup id=&quot;fnref6&quot;&gt;&lt;a href=&quot;#fn6&quot; rel=&quot;footnote&quot;&gt;6&lt;/a&gt;&lt;/sup&gt; arrow&lt;/li&gt;
&lt;li&gt;Space + S: stroke&lt;/li&gt;
&lt;li&gt;Space + S + G: bold stroke&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;and so on for a bunch of different styles. This lets you switch styles much
faster than Inkscape&amp;#8217;s default binds. One issue I ran into using the style
switcher is that it needs a hacky X11 key event interceptor to make the binds
work. Since I switched to Wayland, that doesn&amp;#8217;t work anymore. Now, I have
extracted Gilles&amp;#8217; style code to a &lt;a href=&quot;https://github.com/dogeystamp/dots/blob/8299e0b2c0db4d287640e588d075985385fba81e/src/dot_local/bin/executable_stylinator.py&quot;&gt;Python
script&lt;/a&gt;,
and I connect it to &lt;code&gt;fuzzel&lt;/code&gt; (which is like &lt;code&gt;rofi&lt;/code&gt; for Wayland) in a &lt;a href=&quot;https://github.com/dogeystamp/dots/blob/8299e0b2c0db4d287640e588d075985385fba81e/src/dot_local/bin/executable_style.sh&quot;&gt;style
switcher shell
script&lt;/a&gt;.
Then, I use my window manager&amp;#8217;s binds to trigger this style script. So the
process in Inkscape to switch styles becomes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Press Mod+A (calls style switcher).&lt;/li&gt;
&lt;li&gt;Type the bind (e.g. &lt;code&gt;ag&lt;/code&gt; for bold arrow) and press Enter&lt;sup id=&quot;fnref7&quot;&gt;&lt;a href=&quot;#fn7&quot; rel=&quot;footnote&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;; this copies the style to clipboard.&lt;/li&gt;
&lt;li&gt;Select an object in Inkscape and Ctrl+Shift+V to paste the style on it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Admittedly, this method is slower, but it is more robust and will not break the moment I switch window managers.
It even has the potential to work on Windows, provided you have a replacement for &lt;code&gt;fuzzel&lt;/code&gt; and the clipboard tools.&lt;/p&gt;
&lt;p&gt;Castel claimed to be able to draw figures with his tools as fast as the
instructors could draw them on the board, but I still don&amp;#8217;t have enough
experience after two years to do that yet. Perhaps I just don&amp;#8217;t draw that many
figures in my lecture notes. Or maybe, my tooling is just not on his level.&lt;/p&gt;
&lt;h2 id=&quot;scripting&quot;&gt;Scripting&lt;/h2&gt;
&lt;p&gt;Typst is great not only because it&amp;#8217;s nice to typeset with,
but also because it has a sane scripting language built in.
Typst is divided into a markup mode (i.e. normal, formatted text),
and code mode, where you can write scripts.
You enter code mode by typing &lt;code&gt;#&lt;/code&gt;, and in code mode you can introduce markup with &lt;code&gt;[]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here are a few examples.
If you need a small function that makes text bold and italic and smallcaps, Typst has got you:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typst&quot;&gt;This is *markup* mode text

#let very-emph = (content) =&amp;#62; strong(emph(smallcaps(content)))

#very-emph[
  This is highly emphasized _markup._
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/public/img/typst-notes2/script.svg&quot; alt=&quot;The output of the above Typst code.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;(&lt;code&gt;#very-emph[...]&lt;/code&gt; is shorthand for &lt;code&gt;#very-emph([...])&lt;/code&gt; which is calling the function &lt;code&gt;very-emph&lt;/code&gt; on content.)
If you want to make something render conditionally, Typst can also do that:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typst&quot;&gt;#if 1 + 1 == 2 [
    We have $1 + 1 = 2$.
] else [
    We have $1 + 1 != 2$??
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/public/img/typst-notes2/script2.svg&quot; alt=&quot;The output of the above Typst code.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Generally, I&amp;#8217;d say that if you are used to regular programming languages like
JavaScript, Typst&amp;#8217;s scripting syntax will feel intuitive for you. See the later
chapters of the &lt;a href=&quot;https://typst.app/docs/tutorial/&quot;&gt;Typst tutorial&lt;/a&gt; for more
details.&lt;/p&gt;
&lt;p&gt;I often use Typst&amp;#8217;s scripting system for little things like
making my own shorthands in math mode.
For instance, I shorten the common real coordinate spaces like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#let Rn = $RR^n$
#let R2 = $RR^2$
#let R3 = $RR^3$
#let R4 = $RR^4$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So anywhere in math where I need to type ℝ², I can type &lt;code&gt;R2&lt;/code&gt;, and so on for the others.&lt;/p&gt;
&lt;p&gt;Another example is in a resume, where I make a template to format a section:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typst&quot;&gt;#let entry = (title, body) =&amp;#62; {
  v(1.25em, weak: true)
  box(
    heading(title)
      + v(weak: true, 0.25em)
      + line(length: 100%, stroke: 0.7pt + black)
      + v(weak: true, 0.75em)
      + box(inset: (left: 0.75em), body),
  )
}

#entry[Education][
    *University of Ipsum* \
    _Bachelor of Something_
]
#entry[Skills][
    - Writing long-winded blog posts
    - Rust
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/public/img/typst-notes2/script4.svg&quot; alt=&quot;The output of the above Typst code.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;These are simple examples, and Typst&amp;#8217;s scripting language can do much more than that.
For instance, I made a &lt;a href=&quot;https://github.com/dogeystamp/kindle_schedule&quot;&gt;&amp;#8220;smart&amp;#8221; calendar&lt;/a&gt;
powered by Typst&amp;#8217;s scripting system.&lt;/p&gt;
&lt;h2 id=&quot;templates&quot;&gt;Templates&lt;/h2&gt;
&lt;p&gt;Templates in Typst are like that resume &lt;code&gt;entry&lt;/code&gt; function I wrote in the above section, but more featureful.
Authors write packages which expose a template function;
you call that function with the content of your document,
and the template configures the paper size, font, layout, and so on.
Templates separate content from style, and ensure you can focus on just writing.&lt;/p&gt;
&lt;p&gt;In 2023, I rolled my own system for templates; I included all my
template code in a &lt;code&gt;.typ&lt;/code&gt; file and imported it in each new document I made.
This was painful to use because there was no standard file path to import
templates from; I had to place documents next to the template, and import
by relative path, like &lt;code&gt;..&amp;#47;..&amp;#47;template.typ&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Soon after I wrote that article, Typst made their own official packaging system
to easily distribute files like templates. Now, template authors publish
packages to the &lt;a href=&quot;https://typst.app/universe&quot;&gt;Typst Universe&lt;/a&gt;
registry.&lt;sup id=&quot;fnref8&quot;&gt;&lt;a href=&quot;#fn8&quot; rel=&quot;footnote&quot;&gt;8&lt;/a&gt;&lt;/sup&gt; This way, documents are self-contained: rather than
importing &lt;code&gt;..&amp;#47;..&amp;#47;template.typ&lt;/code&gt;, which might only exist on your computer, you
import the standard package &lt;code&gt;@preview&amp;#47;my-great-template:1.2.3&lt;/code&gt;. You can send a
single &lt;code&gt;.typ&lt;/code&gt; document to other people, or edit it on the Typst web app, and it
will work just fine.&lt;/p&gt;
&lt;p&gt;Now, whenever I want to start a new document, I can create a new &lt;code&gt;.typ&lt;/code&gt; file
anywhere, and import my template with this code snippet:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typst&quot;&gt;#import &quot;@preview&amp;#47;mousse-notes:1.0.0&quot;: *
#set page(paper: &quot;us-letter&quot;)
#show: book.with(
  title: [WUNK 101],
  subtitle: [Introduction to Wunkematics],
  subsubtitle: [Lecture notes, Fall 2023],
  subsubsubtitle: [Professor #smallcaps[Jonathan Bingus], University of Ipsum.],
  author: &quot;John S. Student&quot;,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;book&lt;/code&gt; is just a function I wrote which takes content and transforms it,
and &lt;code&gt;show&lt;/code&gt; is syntax that says &amp;#8220;call this function on the entire document.&amp;#8221;
It&amp;#8217;s cleaner than wrapping the entire document&amp;#8217;s content with a function call.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Or, even easier, I can paste the above boilerplate code with this single command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typst init @preview&amp;#47;mousse-notes dir
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;which creates a notes file at &lt;code&gt;dir&amp;#47;main.typ&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/public/img/typst-notes2/template.png&quot; alt=&quot;The output of the above Typst code.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;My template is available at its &lt;a href=&quot;https://github.com/dogeystamp/mousse-notes&quot;&gt;GitHub
repo&lt;/a&gt;. And if you want to use my
template, you can run &lt;code&gt;typst init @preview&amp;#47;mousse-notes&lt;/code&gt; on your device too.&lt;/p&gt;
&lt;p&gt;Another thing I do differently than in 2023 is the organization of my notes.
In the past, I would make a new document for each chapter of a course;
now, each course&amp;#8217;s notes are contained in a &amp;#8220;book&amp;#8221; compiled from a single Typst document.
Practically speaking, this means I only need to write the document boilerplate once at the start of each term,
when I&amp;#8217;m creating each course&amp;#8217;s notes document.&lt;/p&gt;
&lt;h2 id=&quot;plugins&quot;&gt;Plugins&lt;/h2&gt;
&lt;p&gt;Typst Universe doesn&amp;#8217;t just host templates; it also hosts plugins, which are also &lt;code&gt;.typ&lt;/code&gt; scripts.
A lot of these are useful, for example &lt;a href=&quot;https://typst.app/universe/package/whalogen/&quot;&gt;whalogen&lt;/a&gt;
provides shortcuts to type chemical equations,
and &lt;a href=&quot;https://typst.app/universe/package/metro&quot;&gt;metro&lt;/a&gt; lets you type units.
It is even possible to &lt;a href=&quot;https://typst.app/universe/package/pyrunner/&quot;&gt;run Python in Typst&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For many tasks though, plugins aren&amp;#8217;t strictly necessary.
With &lt;code&gt;whalogen&lt;/code&gt;, you can type &lt;code&gt;#ce(&quot;HCl + H2O -&amp;#62; H3O+ + Cl-&quot;)&lt;/code&gt;,
and without the plugin, it&amp;#8217;s just longer: &lt;code&gt;$&quot;HCl&quot; + &quot;H&quot;_2&quot;O&quot; -&amp;#62; &quot;H&quot;_3&quot;O&quot;^+ + &quot;Cl&quot;^-$&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I personally dislike having extra dependencies, because it creates more
maintenance burden. Oftentimes, the Typst compiler will have a breaking change
(a downside of using software that is in its growth phase), and one
of the plugins I use will break, or have annoying deprecation warnings.&lt;/p&gt;
&lt;p&gt;Nowadays, I don&amp;#8217;t use external plugins anymore: my template includes my own
implementations of &lt;code&gt;#theorem&lt;/code&gt;, &lt;code&gt;#definition&lt;/code&gt;, &lt;code&gt;#example&lt;/code&gt;, and so on. If it
breaks, it&amp;#8217;s my fault solely, which is good because then I can fix the issue
myself.&lt;/p&gt;
&lt;h2 id=&quot;synchronizing-notes&quot;&gt;Synchronizing notes&lt;/h2&gt;
&lt;p&gt;You may want to synchronize your Typst files so you can write on multiple
devices. There are a few ways to do this.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Personally, I use &lt;a href=&quot;https://git-scm.com/&quot;&gt;Git&lt;/a&gt; to manage my notes. The main
advantage of Git is that it is the industry standard for working with source
code, and it is a highly reliable tool. The disadvantage is that you need to
commit and push changes manually, which you might forget to do.
(I mainly write on a single device, so this is fine.)&lt;/li&gt;
&lt;li&gt;You could use &lt;a href=&quot;https://syncthing.net/&quot;&gt;Syncthing&lt;/a&gt;, which automatically syncs
files across your devices. It just works. One annoying thing is that when
there are sync conflicts, Syncthing will not notify you and it will instead
create a &amp;#8220;sync-conflict&amp;#8221; file.&lt;/li&gt;
&lt;li&gt;The most practical option (but one that goes against the idea of using
local open-source tools) is to use the official closed-source &lt;a href=&quot;https://typst.app&quot;&gt;web
app&lt;/a&gt; for Typst. Unlike the two other options, you need to
be online to access the web app. However, the web app synchronizes documents
perfectly and lets you collaborate in real time with other people.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the first two options, make sure that you exclude &lt;code&gt;.pdf&lt;/code&gt; outputs in &lt;code&gt;.gitignore&lt;/code&gt; or &lt;code&gt;.stignore&lt;/code&gt;
to save storage and bandwidth.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;That&amp;#8217;s all for how my current note-taking setup works. A general theme
comparing the 2026 setup to the 2023 setup is that I&amp;#8217;ve aimed for more
minimalism and simplicity: a system with less moving parts is more reliable,
and easier to maintain.&lt;/p&gt;
&lt;p&gt;Notably, all my documents are now fully self-contained: they import my template
from the standardized package registry rather than from a relative path, and
they do not have external links. If you&amp;#8217;ve read the 2023 version of this post,
you&amp;#8217;ll see that I created a convoluted setup to link between different PDF
files. I spent much effort building the link system, only to never use it. Now,
my documents are formatted as books that comprehensively cover one
subject, so that I only need to link within documents (which is a
feature Typst and PDF readers have good support for).&lt;sup id=&quot;fnref9&quot;&gt;&lt;a href=&quot;#fn9&quot; rel=&quot;footnote&quot;&gt;9&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Another way I&amp;#8217;ve simplified the note-taking system is to trim out a lot of
brittle external code and dependencies. I&amp;#8217;ve trimmed down Castel&amp;#8217;s Inkscape tools to
the bare minimum, leaving only the style code itself, and as mentioned above,
I&amp;#8217;ve stopped using external Typst plugins. The projects I do rely on are
generally well-maintained, like the Neovim editor, the Typst compiler itself,
and TexText.&lt;/p&gt;
&lt;p&gt;Looking forward to the future, I mainly wish I could reduce the complexity in
my Neovim configuration. That is, I want to stop using as many third party
plugins. The &lt;a href=&quot;https://github.com/helix-editor/helix&quot;&gt;Helix&lt;/a&gt; editor (mentioned
above) is quite promising in that a lot of the functionality in Neovim plugins
(surround, Git gutter, telescope, lspconfig, tree-sitter, &amp;#8230;) is available out
of the box in Helix and maintained by their team. Once they have solid snippet
support, I&amp;#8217;ll switch fully to Helix and I&amp;#8217;ll never again have to think about my
huge Neovim configuration file, and the headache of configuring a dozen
plugins.&lt;/p&gt;
&lt;p&gt;Thanks for reading this far. If you have any questions or comments regarding
this post, please do write to me, and I&amp;#8217;ll be happy to reply to you.&lt;/p&gt;
&lt;h3 id=&quot;appendix-configuration-files&quot;&gt;Appendix: Configuration files&lt;/h3&gt;
&lt;p&gt;To keep this post brief, I did not include most of my configuration code.
If you want to replicate my setup, here are the relevant configuration files on GitHub:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/dogeystamp/dots/blob/8299e0b2c0db4d287640e588d075985385fba81e/src/dot_config/nvim/ftplugin/typst.vim&quot;&gt;&lt;code&gt;ftplugin&amp;#47;typst.vim&lt;/code&gt;&lt;/a&gt; (bind for &lt;code&gt;typst watch&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/dogeystamp/dots/blob/8299e0b2c0db4d287640e588d075985385fba81e/src/dot_config/nvim/ftplugin/typst.lua&quot;&gt;&lt;code&gt;ftplugin&amp;#47;typst.lua&lt;/code&gt;&lt;/a&gt; (tinymist)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/dogeystamp/dots/blob/8299e0b2c0db4d287640e588d075985385fba81e/src/dot_config/nvim/snippets/typst.lua&quot;&gt;&lt;code&gt;snippets&amp;#47;typst.lua&lt;/code&gt;&lt;/a&gt; (snippets)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/dogeystamp/dots/blob/8299e0b2c0db4d287640e588d075985385fba81e/src/dot_local/bin/executable_typst-figure&quot;&gt;&lt;code&gt;typst-figure&lt;/code&gt;&lt;/a&gt; (shell script to create figures)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/dogeystamp/dots/blob/8299e0b2c0db4d287640e588d075985385fba81e/src/dot_local/bin/executable_stylinator.py&quot;&gt;&lt;code&gt;stylinator.py&lt;/code&gt;&lt;/a&gt; (Inkscape quick style script)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/dogeystamp/dots/blob/8299e0b2c0db4d287640e588d075985385fba81e/src/dot_local/bin/executable_style.sh&quot;&gt;&lt;code&gt;style.sh&lt;/code&gt;&lt;/a&gt; (frontend to &lt;code&gt;stylinator.py&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr/&gt;
&lt;p&gt;Edits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(2026-01-18) Added section about zoxide and fish, made article reflect that mousse-notes is now on Typst Universe, changed examples of scripting&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;footnotes&quot;&gt;
&lt;hr/&gt;
&lt;ol&gt;

&lt;li id=&quot;fn1&quot;&gt;
&lt;p&gt;In fact, I moved to a new school a while ago,
and some students I met there had already read my post.
Hi :)&amp;#160;&lt;a href=&quot;#fnref1&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn2&quot;&gt;
&lt;p&gt;This is, of course, just my personal opinion.&amp;#160;&lt;a href=&quot;#fnref2&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn3&quot;&gt;
&lt;p&gt;Looking at you, zsh.&amp;#160;&lt;a href=&quot;#fnref3&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn4&quot;&gt;
&lt;p&gt;In 2023, I used UltiSnips, but I got tired of it complaining about pynvim being missing on all my devices.&amp;#160;&lt;a href=&quot;#fnref4&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn5&quot;&gt;
&lt;p&gt;Some of the binds I use are &amp;#8220;toggle snap to grid&amp;#8221; (%),
&amp;#8220;toggle grid&amp;#8221; (#), the curve drawing tool (b), rectangle and circle (F4, F5).&amp;#160;&lt;a href=&quot;#fnref5&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn6&quot;&gt;
&lt;p&gt;I think G stands for &lt;a href=&quot;https://en.wiktionary.org/wiki/gras#French&quot;&gt;gras&lt;/a&gt;, &amp;#8220;bold&amp;#8221;?&amp;#160;&lt;a href=&quot;#fnref6&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn7&quot;&gt;
&lt;p&gt;I bound Shift + V as Enter in fuzzel, so that my left hand can reach better.&amp;#160;&lt;a href=&quot;#fnref7&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn8&quot;&gt;
&lt;p&gt;Or you can &amp;#8220;publish&amp;#8221; packages to a standard local folder on your computer.&amp;#160;&lt;a href=&quot;#fnref8&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn9&quot;&gt;
&lt;p&gt;Removing inter-document links and enforcing a rule of &amp;#8220;one subject,
one document&amp;#8221; is perhaps not ideal for those who want a personal knowledge
management system, but it is good for writing course notes.&amp;#160;&lt;a href=&quot;#fnref9&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;
&lt;/div&gt;</content>
		<link href="https://www.dogeystamp.com/typst-notes2"/>
		<id>https://www.dogeystamp.com/typst-notes2</id>
		<updated>2026-01-01T10:00:00Z</updated>
		<published>2026-01-01T10:00:00Z</published>
	</entry>
	<entry>
		<title>Reviving a digital piano with new brains</title>
		<content type="html">&lt;h1 id=&quot;reviving-a-digital-piano-with-new-brains&quot;&gt;Reviving a digital piano with new brains&lt;/h1&gt;
&lt;div class=&quot;creation-date&quot;&gt;
2024-05-17
&lt;/div&gt;
&lt;p&gt;One day, I was playing my Roland HP-1500 digital piano,
which is an incredibly old model.
It suddenly started making weird electrical noises, and then it died.
I opened the piano up, and looked at the circuit board,
but my efforts to figure out what went wrong were ultimately futile.&lt;/p&gt;
&lt;p&gt;At this point, I had a thought: maybe I could build a brand new circuit for the piano,
replacing the broken original board.
After all, how hard could it be?
I had just learned the basics of electronics, and this definitely seemed like a good learning experience.&lt;/p&gt;
&lt;p&gt;That was a few months ago.
Recently, I finished implementing this project, which I named geode-piano.
Here is a quick demo of it (excuse the poor microphone quality):&lt;/p&gt;
&lt;p&gt;&lt;video width=&quot;640&quot; height=&quot;360&quot; controls&gt;
  &lt;source src=&quot;/public/img/piano/demo.mp4&quot; type=&quot;video/mp4&quot;&gt;
Your browser does not support the video tag.
&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;This project is powered by a single &lt;a href=&quot;https://www.raspberrypi.com/products/raspberry-pi-pico/&quot;&gt;Raspberry Pi Pico&lt;/a&gt;, which runs firmware written in Rust.
Source code and build instructions are available on the &lt;a href=&quot;https://github.com/dogeystamp/geode-piano&quot;&gt;project repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It took quite a while to get to this point, and so this blog post will document the process of designing and implementing geode-piano.&lt;/p&gt;
&lt;h2 id=&quot;how-a-digital-piano-works&quot;&gt;How a digital piano works&lt;/h2&gt;
&lt;p&gt;First, before even designing anything, I did a bit of research on what was going on inside a digital piano.
This helps understand how feasible the project is and how complicated it will be.&lt;/p&gt;
&lt;p&gt;As it turns out, digital pianos are, electrically, pretty simple.
The switches that detect key-presses aren&amp;#8217;t that different from a regular push-button:
when pressed, they let power through, which we can detect.&lt;/p&gt;
&lt;p&gt;However, there&amp;#8217;s 88 keys on a typical piano,
and that&amp;#8217;s a lot of switches to deal with.
The microcontroller (processor chip) inside the piano usually can&amp;#8217;t handle that many inputs.&lt;/p&gt;
&lt;p&gt;This can be solved with a &lt;a href=&quot;https://en.m.wikipedia.org/wiki/Keyboard_matrix_circuit&quot;&gt;&lt;em&gt;key matrix&lt;/em&gt;&lt;/a&gt;, a specific wiring design.
Essentially, a key matrix helps cram all those key switches onto a microcontroller with way less input pins.
For example, look at this key matrix:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     column
     1 2 3 4
row  
     │ │ │ │
  1 ─┼─┼─┼─┼
  2 ─┼─┼─┼─┼
  3 ─┼─┼─┼─┼

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Columns are a power source,
and rows are inputs.
We hook up all of these wires to the microcontroller.&lt;/p&gt;
&lt;p&gt;Each intersection in this grid has a switch.
When a switch is on, power can flow (in only one way) from the column into the row.&lt;/p&gt;
&lt;p&gt;The key matrix works by scanning each column sequentially.
By detecting which rows are powered, we can deduce which switches were pressed.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     column                    column      
     1 2 3 4                   1 2 3 4     
row  ↓                    row    ↓         
     ┃ │ │ │                   │ ┃ │ │     
  1 ━╋━┿━┿━┿                1 ─┼─╂─┼─┼     
  2 ─╂─┼─┼─┼                2 ━┿━╋━┿━┿           and so on...
  3 ━╋━┿━┿━┿                3 ─┼─╂─┼─┼     
                                           
switches pressed:         switches pressed:
- C1R1                    - C2R2           
- C1R3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This scan is quite fast, usually taking less than a few milliseconds.
Using this matrix, we need 8 pins, while an equivalent non-matrix circuit would need 12 pins.
However, we sacrifice a bit of speed because we scan column by column rather than all switches at once.&lt;/p&gt;
&lt;p&gt;In the digital piano, these switches are hooked up to the piano keys,
allowing key-presses to be detected.
On my piano, we have 176 key-switches (for reasons which I will explain later), which can be scanned using only 40 pins thanks to the matrix.&lt;/p&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This diagram and explanation are both simplified, so &lt;a href=&quot;http://www.openmusiclabs.com/learning/digital/input-matrix-scanning/&quot;&gt;click here&lt;/a&gt; for a more detailled explanation.
In practice, diodes are used to ensure power doesn&amp;#8217;t flow the wrong way.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;So that&amp;#8217;s how a digital piano works, theoretically.
What does that look like, under the hood?
As it turns out, the matrix is accessible through ribbon cables (or &lt;a href=&quot;https://en.m.wikipedia.org/wiki/Flexible_flat_cable&quot;&gt;&lt;em&gt;flat flexible cable&lt;/em&gt;&lt;/a&gt;, or FFC).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/public/img/piano/ffc-test.jpg&quot; alt=&quot;A flat flexible cable with alligator clips on the contacts.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The metallic contacts on these cables correspond to the columns and rows of the key matrix.
Usually, you&amp;#8217;ll find one or multiple ribbon cables with one end plugged into the main board of the digital piano,
and the other ends leading inside the piano key mechanism.&lt;/p&gt;
&lt;h2 id=&quot;project-architecture&quot;&gt;Project architecture&lt;/h2&gt;
&lt;p&gt;geode-piano works by disconnecting the ribbon cables from the original circuit board,
then reconnecting them into my own circuit.
Effectively, I&amp;#8217;m taking over the piano key circuitry.&lt;/p&gt;
&lt;p&gt;Designing this, I tried to make things as easy as possible for me.
Therefore, this project only exposes the piano as a MIDI controller.
This means that we will only be transmitting data about what note was pressed when.
Meanwhile, on a computer, we can make use of existing software to synthesize the actual piano sound from this data.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; ╭─────────────╮  ribbon cables  ╭──────────────────╮
 │ geode-piano ├─────────────────┤ piano key matrix │
 ╰──────┬──────╯                 ╰──────────────────╯
        │ 
        │
        │  midi over usb
        │
        │
╭───────┴──────────╮
│ software sampler │
│ (in a laptop)    │
╰───────┬──────────╯
        │
        │  3.3mm or usb or whatever
        │
╭───────┴───────────╮
│ speaker&amp;#47;headphone │
╰───────────────────╯
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is in contrast to actually generating the sound in my circuit and also playing it through a speaker,
like the original board did.&lt;/p&gt;
&lt;p&gt;I personally think that this architecture is the fastest way to get to a working product.
After all, convincingly synthesizing a piano sound is difficult,
so reinventing this wheel would be unwise.&lt;/p&gt;
&lt;h2 id=&quot;hardware&quot;&gt;Hardware&lt;/h2&gt;
&lt;p&gt;Now, physically, what does that &lt;code&gt;[geode-piano]&lt;/code&gt; box in the architecture diagram above look like?
The answer is that it looks like a mess.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/public/img/piano/doodad2.jpg&quot; alt=&quot;My circuit, on a breadboard with many jumper wires&quot; /&gt;&lt;/p&gt;
&lt;h3 id=&quot;microcontroller&quot;&gt;Microcontroller&lt;/h3&gt;
&lt;p&gt;First of all, the heart of geode-piano is the Raspberry Pi Pico microcontroller,
which is the green chip in the image above.
I had a few laying around, so it was the obvious choice for me to use.
This part actually runs the firmware, does all the processing, and also connects back to a computer via a micro-USB port.&lt;/p&gt;
&lt;h3 id=&quot;sockets&quot;&gt;Sockets&lt;/h3&gt;
&lt;p&gt;Then, there are the sockets above.
Those are actually FFC sockets, which the ribbon cables can be plugged into.
This is definitely one of the cursed parts of this project,
because these sockets are designed to be soldered, and not to be used with jumper cables.
In fact, I had to slice off the tips of a bunch of female-to-male jumper cables to get them to connect to the pins.
I am still quite surprised that the pins snap perfectly in the female ends.&lt;/p&gt;
&lt;p&gt;This arrangement of many jumper cables in parallel going up to the sockets was also a bad idea,
as it caused crosstalk.
In tests, it showed up as ghost signals being detected with no visible source.
Twisting some wires together and attempting to space them out fixed this issue.&lt;/p&gt;
&lt;p&gt;As an aside, I originally bought the wrong size of socket due to carelessness.
I put up a ruler to the contacts and eyeballed the pin pitch (distance between each contacts&amp;#8217; centers),
and decided it was 1.0mm.
This was a big mistake on my part, as I found out later that it was 1.25mm.&lt;/p&gt;
&lt;p&gt;After this, I discovered that the socket specsheets had measurements of the distance between the first and last contacts,
which is easier and less error-prone to measure with a typical ruler.
Actually reading these documents should help me avoid these kinds of mistakes.&lt;/p&gt;
&lt;h3 id=&quot;pin-extenders&quot;&gt;Pin extenders&lt;/h3&gt;
&lt;p&gt;The astute among you might have noticed that a Pico microcontroller does not have enough input pins for this project.
To remedy this issue, I used two &lt;a href=&quot;https://www.microchip.com/en-us/product/mcp23017&quot;&gt;MCP23017&lt;/a&gt; chips, which are pin extenders.
Each has 16 GPIO pins, and they communicate over &lt;a href=&quot;https://en.m.wikipedia.org/wiki/I%C2%B2C&quot;&gt;I²C&lt;/a&gt; to the Pico,
which requires only 2 pins on that end.
For these 14 extra pins we get, we sacrifice a bit of convenience and efficiency.&lt;/p&gt;
&lt;p&gt;One of the features of these chips is their capacity for both input and output.
This is important because I don&amp;#8217;t actually know which contact on the ribbon cable corresponds to which row and column.
Instead of reverse-engineering the circuitry with a multimeter,
I made a &lt;a href=&quot;https://github.com/dogeystamp/geode-piano/blob/main/src/bin/pin_scanner.rs&quot;&gt;scanner&lt;/a&gt; that will try every row&amp;#47;column combination possible for each key until it finds a valid one.
With this information, we can reconstruct the key matrix pinout.&lt;/p&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A few important tips I would tell past me about this chip:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You need &lt;a href=&quot;https://www.joshmcguigan.com/blog/internal-pull-up-resistor-i2c/&quot;&gt;pull-up resistors&lt;/a&gt;
for I²C. I won&amp;#8217;t go into detail about it because the linked blog post sums up my experience with this.&lt;/li&gt;
&lt;li&gt;Multiple I²C peripherals can live on the same bus.&lt;/li&gt;
&lt;li&gt;Plug the &lt;code&gt;RESET&lt;/code&gt; pin into the positive power rail. I was stuck for an entire afternoon because no documentation said this clearly.
In the datasheet, &amp;#8220;must be externally biased&amp;#8221; means &amp;#8220;do not leave this pin floating under any circumstances&amp;#8221;.
Also, the overbar on the pin name in the datasheet means that pulling the pin low will cause a reset.&lt;/li&gt;
&lt;li&gt;MCP23017 chips are known to have weird behaviour on pins GPA7 and GPB7. (Look at the most recent &lt;a href=&quot;https://ww1.microchip.com/downloads/aemDocuments/documents/APID/ProductDocuments/DataSheets/MCP23017-Data-Sheet-DS20001952.pdf&quot;&gt;datasheet&lt;/a&gt;,
not the old one!)&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id=&quot;firmware&quot;&gt;Firmware&lt;/h2&gt;
&lt;p&gt;If you&amp;#8217;ve used microcontrollers before,
you probably know that they&amp;#8217;re programmed using C++, C, or MicroPython,
or some similar language.
The Raspberry Pi Pico is no different,
as the most common ways to write firmware for it are the &lt;a href=&quot;https://www.raspberrypi.com/documentation/microcontrollers/c_sdk.html&quot;&gt;Pico C SDK&lt;/a&gt;,
and MicroPython.&lt;/p&gt;
&lt;p&gt;I had tried C before, but the tooling was painful to deal with.
My language server &lt;a href=&quot;https://clangd.llvm.org/&quot;&gt;clangd&lt;/a&gt; would display unfixable errors
about missing imports and unknown functions.
This was fine, but it was really annoying.
MicroPython does seem quite user-friendly,
but for scanning the key matrix, it could be problematic due to performance concerns.&lt;/p&gt;
&lt;p&gt;In the end, I settled on using Rust.
This option seems relatively obscure and less well documented,
however it ended up working well for me.&lt;/p&gt;
&lt;p&gt;The main advantage of Rust for me is that it is a modern, yet quite performant language.
Even in a &lt;code&gt;no_std&lt;/code&gt; embedded environment, you have a full package manager to easily install libraries.
The &lt;a href=&quot;https://docs.rs/mcp23017/latest/mcp23017/&quot;&gt;MCP23017 library&lt;/a&gt;, for example,
let me develop that part of the code faster.
Also, &lt;a href=&quot;https://rust-analyzer.github.io/&quot;&gt;rust-analyzer&lt;/a&gt; works perfectly well, and
gives the most detailled and helpful messages out of all language servers I&amp;#8217;ve used before.&lt;/p&gt;
&lt;p&gt;Specifically for this project, I used the &lt;a href=&quot;https://embassy.dev/&quot;&gt;embassy-rs&lt;/a&gt; framework.
This library makes embedded development in Rust really easy.
It offers drivers for a bunch of useful features,
like &lt;a href=&quot;https://docs.embassy.dev/embassy-usb/git/default/index.html&quot;&gt;USB MIDI&lt;/a&gt;,
&lt;a href=&quot;https://docs.embassy.dev/embassy-usb-logger/git/default/index.html&quot;&gt;USB logger output&lt;/a&gt;,
I²C and many others.
Embassy also works using async&amp;#47;await,
which makes multitasking simple and elegant.
I&amp;#8217;m not a Rust expert, though, so consult their website for more information about this.&lt;/p&gt;
&lt;p&gt;Even though Rust is great, it does have an infamously steep learning curve.
As you might know, Rust is memory-safe by using a strict &lt;a href=&quot;https://doc.rust-lang.org/1.8.0/book/references-and-borrowing.html&quot;&gt;borrow checker&lt;/a&gt;.
If you follow its rules, you can eliminate many types of memory bugs.
In this project, though, I spent many hours fighting Rust&amp;#8217;s borrow checker.
What I learned from this experience is that, when possible,
you should follow Rust&amp;#8217;s idiomatic ways of solving problems.
This means that you should avoid long-lived references,
and keep lifetimes short.
Essentially, don&amp;#8217;t overcomplicate the program logic.&lt;/p&gt;
&lt;p&gt;Anyways, the source code for geode-piano&amp;#8217;s firmware is available on the &lt;a href=&quot;https://github.com/dogeystamp/geode-piano&quot;&gt;project repository&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;other-features&quot;&gt;Other features&lt;/h2&gt;
&lt;p&gt;That was the general overview of the project.
These are a few miscellaneous details that I could not fit well elsewhere.&lt;/p&gt;
&lt;h3 id=&quot;velocity-detection&quot;&gt;Velocity detection&lt;/h3&gt;
&lt;p&gt;A digital piano is, electronically, just a bunch of buttons that trigger sound when pressed.
There is, however, a slight nuance to this.
A button switch has only two states, on and off.
On a piano, hitting a key really hard makes a loud note, and softly pressing it makes a soft note.
From the perspective of our hardware, a button press is just a button press;
there is no information about intensity.&lt;/p&gt;
&lt;p&gt;To measure the intensity of key-presses, some engineer decided that instead of every key having one switch,
they should have two switches.
These switches are placed so that they trip one after the other during a keypress.
By measuring the time between the switches&amp;#8217; activations, the digital piano can estimate the intensity of a press.
A fast press is a hard press, and a slow press is a soft press.
This system works well, and is present in most digital pianos.&lt;/p&gt;
&lt;p&gt;geode-piano does have velocity detection too,
but it is not very precise.
I think this is because it takes too long for the key matrix scan (around 7ms),
which is not fine-grained enough to accurately detect velocity.
Possibly, it is because of the MCP23017 being too slow,
but it could also be my code.
At this point though, the piano works well enough that I do not feel it is worth it to optimise this.&lt;/p&gt;
&lt;h3 id=&quot;sustain-pedal&quot;&gt;Sustain pedal&lt;/h3&gt;
&lt;p&gt;Pianos have pedals that control the sound.
The code for handling this is not quite different from handling regular keys.
However, connecting the pedal to the microcontroller is more difficult.
Typically, the pedals are connected to the piano via a &lt;a href=&quot;https://en.m.wikipedia.org/wiki/Phone_connector_(audio)&quot;&gt;TRS jack&lt;/a&gt; (not dissimilar to a headphone jack).
However, I had no socket component for this type of plug.
Therefore, I made the most cursed part of the circuit:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/public/img/piano/jack.jpg&quot; alt=&quot;TRS jack wrapped in wires&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The brown wire is stripped on the part where it wraps around the plug,
and the yellow and pink parts are stripped paperclips.
This may seem like a fire hazard, but the wire connected to an input pin,
so unless the microcontroller uses the wrong pins (in which case we have bigger problems)
there should be no short-circuit risk.&lt;/p&gt;
&lt;p&gt;In my experience so far, this connection actually works remarkably well.
Another win for terrible wiring.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This project was pretty fun to do.
Before starting it, I thought that it was pretty ambitious for my skill level;
at the time, I&amp;#8217;d only played with wiring LEDs and buttons up to my microcontroller.
Who knew that I could implement the circuitry for an entire digital piano?&lt;/p&gt;
&lt;p&gt;I did learn a lot about electronics through this project,
as well as a bit of Rust.
I don&amp;#8217;t remember where I heard it anymore,
but I agree with the notion that you should try projects like this that are just barely within your capacities to accomplish.
This kind of hands-on learning is one of the better ways to develop problem-solving skills.&lt;/p&gt;
&lt;p&gt;Anyways, I now have a working piano again!&lt;/p&gt;</content>
		<link href="https://www.dogeystamp.com/piano"/>
		<id>https://www.dogeystamp.com/piano</id>
		<updated>2024-05-17T10:00:00Z</updated>
		<published>2024-05-17T10:00:00Z</published>
	</entry>
	<entry>
		<title>MinRSS, a lightweight feed reader</title>
		<content type="html">&lt;h1 id=&quot;minrss-a-lightweight-feed-reader&quot;&gt;MinRSS, a lightweight feed reader&lt;/h1&gt;
&lt;div class=&quot;creation-date&quot;&gt;
2023-05-14
&lt;/div&gt;
&lt;p&gt;&lt;img src=&quot;/public/img/minrss-mrss.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;purpose&quot;&gt;Purpose&lt;/h2&gt;
&lt;p&gt;If you want to read content online from many different websites, the best way is to subscribe to RSS or Atom feeds.
It&amp;#8217;s a simple, universal format for getting content onto your screen.&lt;/p&gt;
&lt;p&gt;I personally enjoy living in the terminal.
One of the more popular RSS readers for this environment is &lt;a href=&quot;https://newsboat.org/&quot;&gt;Newsboat&lt;/a&gt;.
Newsboat has tons of useful features and a pretty TUI, and I did use it for a while.&lt;/p&gt;
&lt;p&gt;However, I thought that it was too complex for what I needed in an RSS reader.
Therefore, I decided to write a new one: MinRSS.&lt;/p&gt;
&lt;h2 id=&quot;concept&quot;&gt;Concept&lt;/h2&gt;
&lt;p&gt;MinRSS is a small binary that &amp;#8220;does one thing and does it well&amp;#8221;:
it downloads RSS articles to disk.&lt;/p&gt;
&lt;p&gt;Essentially, every feed is represented as a
folder, and individual articles are files in these folders.&lt;/p&gt;
&lt;p&gt;Every time the binary is run, it creates a structure like this in the current working
directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rss
|--news
|  |--article1
|  `--article2
`--blog
  |--post
  `--other_post
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If an article is new (it wasn&amp;#8217;t on disk already), its filename is printed to standard output.&lt;/p&gt;
&lt;p&gt;The goal of doing things this way is to make writing scripts as easy as possible.
If you&amp;#8217;re familiar with shell scripting, all you need is &lt;code&gt;jq&lt;/code&gt;
and you can now parse RSS and implement any custom feature you want.&lt;/p&gt;
&lt;p&gt;If you felt masochistic, you could even read RSS feeds using only your shell, &lt;code&gt;minrss&lt;/code&gt;, &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;head&lt;/code&gt; and &lt;code&gt;w3m&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This sort of structure is inspired by suckless.org&amp;#8217;s
&lt;a href=&quot;http://tools.suckless.org/ii/&quot;&gt;ii&lt;/a&gt; and &lt;a href=&quot;http://tools.suckless.org/sic/&quot;&gt;sic&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;wrapper-script&quot;&gt;Wrapper script&lt;/h3&gt;
&lt;p&gt;I wrote my own wrapper script around MinRSS, called &lt;code&gt;mrss&lt;/code&gt;.
It has the following features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Update feeds using MinRSS&lt;/li&gt;
&lt;li&gt;Show all new articles using &lt;code&gt;fzf&lt;/code&gt; as an interface (as seen in the screenshot above)&lt;/li&gt;
&lt;li&gt;Mark articles as read&lt;/li&gt;
&lt;li&gt;Mark articles as &amp;#8220;watch later&amp;#8221;&lt;/li&gt;
&lt;li&gt;Custom handler for opening videos and podcasts in &lt;code&gt;mpv&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;installation&quot;&gt;Installation&lt;/h2&gt;
&lt;p&gt;First, ensure you have the requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;libcurl&lt;/li&gt;
&lt;li&gt;libxml2&lt;/li&gt;
&lt;li&gt;json-c&lt;/li&gt;
&lt;li&gt;xdg-open&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Clone the repo:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https:&amp;#47;&amp;#47;github.com&amp;#47;dogeystamp&amp;#47;minrss
cd minrss
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Edit the config file.
The comments in &lt;code&gt;config.h&lt;/code&gt; should guide you:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cp config.def.h config.h
vim config.h
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MinRSS outputs human-readable output by default.
The wrapper script will only work with these options set:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;static const enum outputFormats outputFormat = OUTPUT_JSON;
static const enum summaryFormats summaryFormat = SUMMARY_FILES;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, build and install MinRSS:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;make install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install the wrapper script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cp contrib&amp;#47;mrss.sh ~&amp;#47;.local&amp;#47;bin&amp;#47;mrss
chmod 755 ~&amp;#47;.local&amp;#47;bin&amp;#47;mrss
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;
&lt;p&gt;For complete help, run &lt;code&gt;mrss -h&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To get started, all you need is &lt;code&gt;mrss update&lt;/code&gt; to update feeds, then &lt;code&gt;mrss fzf&lt;/code&gt; to view articles.
Articles are, by default, saved to &lt;code&gt;~&amp;#47;rss&lt;/code&gt;, but you can set &lt;code&gt;$MRSS_DIR&lt;/code&gt; to change this.&lt;/p&gt;
&lt;h4 id=&quot;fzf-shortcuts&quot;&gt;fzf shortcuts&lt;/h4&gt;
&lt;p&gt;In &lt;code&gt;mrss fzf&lt;/code&gt;&amp;#8217;s interface, the following commands are available:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Shortcut&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;#47;read&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Enter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Opens link in the browser or &lt;code&gt;mpv&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;#47;purge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl-D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mark article as read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;#47;purge-all&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl-Alt-D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mark all articles as read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;#47;watch-later&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl-W&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send article to the watch-later folder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;#47;queue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl-E&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Queues link to be opened after leaving &lt;code&gt;fzf&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;You can also use &lt;code&gt;Tab&lt;/code&gt; and &lt;code&gt;Shift-Tab&lt;/code&gt; to select multiple articles to be acted upon.&lt;/p&gt;
&lt;h4 id=&quot;viewing-specific-folders&quot;&gt;Viewing specific folders&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;mrss fzf&lt;/code&gt; command can be used to view a specific folder&amp;#8217;s contents.&lt;/p&gt;
&lt;p&gt;To read all null-program articles (regardless of if they are marked read or not):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mrss fzf null-program
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To view new null-program articles:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mrss fzf new&amp;#47;null-program
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To see articles you&amp;#8217;ve marked as &amp;#8220;watch later&amp;#8221;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mrss fzf watch-later
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;creating-meta-feeds&quot;&gt;Creating meta-feeds&lt;/h4&gt;
&lt;p&gt;In the latest version of mrss, you can create tags to categorize your feeds.
First, create a directory for your tag under &lt;code&gt;$MRSS_DIR&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir ~&amp;#47;rss&amp;#47;tag
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To include new articles for a given feed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ln -sr ~&amp;#47;rss&amp;#47;new&amp;#47;feed ~&amp;#47;rss&amp;#47;tag&amp;#47;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To include all articles:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ln -sr ~&amp;#47;rss&amp;#47;feed ~&amp;#47;rss&amp;#47;tag&amp;#47;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To view this tag:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mrss fzf tag
&lt;/code&gt;&lt;/pre&gt;</content>
		<link href="https://www.dogeystamp.com/minrss"/>
		<id>https://www.dogeystamp.com/minrss</id>
		<updated>2023-05-14T10:00:00Z</updated>
		<published>2023-05-14T10:00:00Z</published>
	</entry>
	<entry>
		<title>Making a virtual machine from scratch (in Rust)</title>
		<content type="html">&lt;h1 id=&quot;making-a-virtual-machine-from-scratch-in-rust&quot;&gt;Making a virtual machine from scratch (in Rust)&lt;/h1&gt;
&lt;div class=&quot;creation-date&quot;&gt;
2024-01-20
&lt;/div&gt;
&lt;p&gt;Computers are wonderful machines that can do many things.
However, even though they are very complex, at their core they are surprisingly simple.
In fact, with only basic programming knowledge,
it is possible to simulate a fully-functioning computer akin to modern ones.&lt;/p&gt;
&lt;p&gt;In this post, I document how I built a virtual machine for Little Computer 3 (LC-3),
an educational computer model.
LC-3 may be simple, but it works the same way any modern computer does:
through implementing it, you get a glimpse of how real architectures like x86 or ARM work.
Besides that, once you&amp;#8217;re finished, you can play 2048 in the VM you created from scratch.
Is there anything more gratifying?&lt;/p&gt;
&lt;p&gt;Writing a virtual machine is also a great test of your programming ability.
I decided to do this project in Rust, in order to learn how the language works hands-on.
I&amp;#8217;ve mostly had experience with C and Python before this, and this was my first Rust project.
However, this article will focus more on the virtual machine aspect, rather than the Rust.&lt;/p&gt;
&lt;p&gt;If you&amp;#8217;re just looking for the source code, check it out on GitHub: &lt;a href=&quot;https://github.com/dogeystamp/lc3-vm&quot;&gt;dogeystamp&amp;#47;lc3-vm&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;prerequisite-reading&quot;&gt;Prerequisite reading&lt;/h2&gt;
&lt;p&gt;I assume you have a certain amount of fundamental knowledge before reading this article:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Programming knowledge&lt;/strong&gt; (preferably in Rust or C).
I assume you know programming decently well, but are not familiar with virtual machines.
I&amp;#8217;ll give code analogies in C, and I assume you can figure out what the Rust means with prior knowledge in other languages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Binary arithmetic&lt;/strong&gt;.
Computers, at their fundamental level, deal with exclusively binary.
You should know &lt;a href=&quot;https://www.hackerearth.com/practice/basic-programming/bit-manipulation/basics-of-bit-manipulation/tutorial/&quot;&gt;bitwise operations&lt;/a&gt;,
like left&amp;#47;right shift, AND, NOT, OR, etc. to see how binary is manipulated.
You should also be familiar with hexadecimal and binary number representation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Besides that, I aim for this article to be shorter and more of an overview of topics that were interesting to me.
For all the context and implementation details, see Justin Meiners&amp;#8217; and Ryan Pendleton&amp;#8217;s &lt;a href=&quot;https://www.jmeiners.com/lc3-vm/&quot;&gt;blog post&lt;/a&gt;
which meticulously explains LC-3,
as well as the &lt;a href=&quot;https://www.jmeiners.com/lc3-vm/supplies/lc3-isa.pdf&quot;&gt;LC-3 ISA specification&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;whats-inside-a-vm&quot;&gt;What&amp;#8217;s inside a VM?&lt;/h2&gt;
&lt;p&gt;Starting off, here is some theory to get you in context.
Skim this if you are familiar with how everything works.&lt;/p&gt;
&lt;p&gt;LC-3 can be modeled with three simple components: the processor and registers (CPU), and memory (RAM).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
    registers
    [ ][ ][ ][ ][ ]
    [ ][ ][ ][ ][ ]
    +-------------+     +------------------------+
    |             |     |                        |
    |  processor  |-----|         memory         |
    |             |     |                        |
    +-------------+     |                        |
                        +------------------------+
    
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;memory&quot;&gt;Memory&lt;/h3&gt;
&lt;p&gt;First, &lt;em&gt;memory&lt;/em&gt; is where most temporary data lives in the computer.
The easiest way for me to visualize it is a huge C-style array:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;uint16_t mem[65536];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of an index, we use an &amp;#8220;address&amp;#8221; for each element.
Just like an array, we can read and write (get or set) the data at each address.
When the computer shuts down, all data in memory is lost.
(For data to persist, we use storage like hard drives, but LC-3 doesn&amp;#8217;t have this.)&lt;/p&gt;
&lt;p&gt;In LC-3, each element in memory is 16 bits, or 2 bytes.
At this level, memory does not have any types as they do in languages like C:
it&amp;#8217;s all raw binary.
Typed variables, malloc and the stack are all abstractions on top of memory.&lt;/p&gt;
&lt;h3 id=&quot;registers&quot;&gt;Registers&lt;/h3&gt;
&lt;p&gt;Second, &lt;em&gt;registers&lt;/em&gt; are where data that is immediately useful is stored.
They are also 16-bit just like memory elements.
Think of it as having fixed variables.
LC-3 has 10 registers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the general purpose R0 to R7,&lt;/li&gt;
&lt;li&gt;Program Counter (PC),&lt;/li&gt;
&lt;li&gt;and the Processor Status Register (PSR).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&amp;#8217;ll explain the last two later.
Just like variables, you can assign values to the general purpose registers, and read from them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;uint16_t r0 = 0;
...
uint16_t r7 = 0;

r6 = r0 + r7;
r3 -= 3;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The reason registers exist when we already have memory is that it&amp;#8217;s way more convenient to use.
Physically, registers are closer to the processor than memory is,
and are therefore much faster.
Besides that, reading&amp;#47;writing from memory usually takes more instructions than just using registers.
However, in exchange for convenience and speed, you have to deal with a limited amount of registers.&lt;/p&gt;
&lt;h3 id=&quot;processor&quot;&gt;Processor&lt;/h3&gt;
&lt;p&gt;Finally, the &lt;em&gt;processor&lt;/em&gt; is where the real computing happens.
The processor reads &lt;em&gt;instructions&lt;/em&gt; one by one, and executes them.
Instructions are like statements in higher level code, but much simpler.&lt;/p&gt;
&lt;p&gt;LC-3 only has 15 different types of instructions,
that do things like &amp;#8220;read memory at this address and load the value into R4&amp;#8221;.
Instructions are just 16-bit binary data.
Again, read the &lt;a href=&quot;https://www.jmeiners.com/lc3-vm/supplies/lc3-isa.pdf&quot;&gt;ISA specification&lt;/a&gt;
for detailled information about this.
For example, the spec says this is how to ADD two registers and store the result in a third register:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ADD
| 0 0 0 1 | DR | SR1 | 0 0 0 | SR2 |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DR (Destination Register), SR1 (Source Register) and SR2 are all placeholders for general registers.
For example, if we wanted to do &lt;code&gt;r1 = r2 + r3&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ADD         r1      r2      (...)   r3
| 0 0 0 1 | 0 0 1 | 0 1 0 | 0 0 0 | 0 1 1 |

or in hex:
0x1283
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, this is pure binary data,
and as such can be read by the processor.
When you compile a C program, machine code like this is what comes out.&lt;/p&gt;
&lt;p&gt;There exists &lt;em&gt;assembly&lt;/em&gt; which is a human-readable version of this binary.
Unlike regular code, it is a 1-to-1 correspondence to the binary.
For example, the above ADD instruction would be&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;ADD R1, R2, R3     ; you can put comments using semicolon
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using an assembler, a programmer can convert the assembly code into the pure binary program that LC-3 can read.&lt;/p&gt;
&lt;h3 id=&quot;fetch-execute-loop&quot;&gt;Fetch-execute loop&lt;/h3&gt;
&lt;p&gt;Now that the individual parts are explained, we move on to how it all works together.
So far, I haven&amp;#8217;t explained where the instructions actually come from.
Since instructions are just binary data, we actually just place a series of them (a program) in memory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;address  value (hex)  equivalent assembly code
0x3000:  [e002]       (LEA R0, HELLO_WORLD)
0x3001:  [f022]       (PUTS)
0x3002:  [f025]       (HALT)
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Program Counter (PC) register we talked about earlier is a pointer to the instructions inside a program in memory.
In LC-3, as seen above, PC starts at the address &lt;code&gt;0x3000&lt;/code&gt;.
The processor will perform a &lt;em&gt;fetch-execute&lt;/em&gt; loop:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fetch the instruction in memory using the PC (&lt;code&gt;instr = mem[PC]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Increment PC (&lt;code&gt;PC += 1&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Execute the instruction&lt;/li&gt;
&lt;li&gt;Repeat on the next instruction&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Until it reaches an instruction to stop (HALT), the processor will continue this loop.
Remember that &lt;strong&gt;PC points to the next instruction&lt;/strong&gt;, not the current one!
This often tripped me up implementing the virtual machine.
The reason it doesn&amp;#8217;t point to the current instruction is that it makes the mechanisms in the next section easier to understand.&lt;/p&gt;
&lt;h3 id=&quot;control-flow&quot;&gt;Control flow&lt;/h3&gt;
&lt;p&gt;If statements, switches, and loops are all implemented using two instructions, &lt;code&gt;JMP&lt;/code&gt; and &lt;code&gt;BR&lt;/code&gt;,
which alter PC.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;JMP&lt;/code&gt; (jump) directly sets the value of PC,
which means on the next fetch-execute loop, the processor will not execute the next instruction,
but rather the instruction at the new PC.
This is what &lt;code&gt;goto&lt;/code&gt; in C does under the hood.&lt;/p&gt;
&lt;p&gt;Meanwhile, &lt;code&gt;BR&lt;/code&gt; (branch) conditionally sets the value of PC.
Remember the PSR register mentioned earlier?
The bottom few bits in PSR contain &lt;em&gt;condition flags&lt;/em&gt;.
Condition flags essentially represent the sign of the result of an operation like AND or ADD.
Respectively, they&amp;#8217;re P (Positive), Z, (Zero), N (Negative).
&lt;code&gt;BR&lt;/code&gt; works with these flags:
for example &lt;code&gt;BRnz&lt;/code&gt; is &amp;#8220;jump if result is negative or zero&amp;#8221;.&lt;/p&gt;
&lt;p&gt;Let&amp;#8217;s see how a for loop would be implemented this way:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;AND R1, R1, #0    ; set R1 to 0 (anything bitwise and 0 is 0)
ADD R1, #5        ; set R1 = R1 + 5

LOOP_START        ; this is a label

... (for loop body)

ADD R1, #-1       ; R1 = R1 + (-1) (decrement)
BRp LOOP_START    ; loop back. once assembled, the label becomes a numerical address offset

HALT              ; we&amp;#39;re done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The equivalent in C:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;for (int i = 5; i &amp;#62; 0; i--) {
    &amp;#47;&amp;#47; for loop body
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&amp;#8217;s also common practice to AND a register with itself to check if it is positive&amp;#47;negative&amp;#47;zero.
The the register AND itself is the same value, which allows testing without altering the register&amp;#8217;s contents.&lt;/p&gt;
&lt;h3 id=&quot;subroutine-calls&quot;&gt;Subroutine calls&lt;/h3&gt;
&lt;p&gt;LC-3 has support for &amp;#8220;subroutines&amp;#8221;, which are like functions but less convenient.
Practically, LC-3 can jump into a subroutine, then at the end of the subroutine, jump back to the place where the subroutine was called.
This works using instructions like JSR, and RET.&lt;/p&gt;
&lt;p&gt;JSR means &amp;#8220;jump subroutine&amp;#8221;, and it is essentially the same as JMP, however it also saves the current PC into the register R7.
At the end of the subroutine, we put the instruction RET, which is actually a disguised JMP.
RET means &amp;#8220;JMP to the address given in R7&amp;#8221;, which means return to the place where we originally used JSR.&lt;/p&gt;
&lt;p&gt;An example usage:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;    AND R1, R1, #0      ; random code
    JSR   SOME_ROUTINE  ; call subroutine
    HALT

SOME_ROUTINE
    ADD, R1, #1         ; random subroutine code
    RET                 ; return
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code will first set R1 to 0, jump into the subroutine, increment R1, then return to the main part and stop the program.&lt;/p&gt;
&lt;h3 id=&quot;memory-mapped-io&quot;&gt;Memory mapped I&amp;#47;O&lt;/h3&gt;
&lt;p&gt;Earlier, when I described how the LC-3 virtual machine works, I omitted a pretty significant component:
input and output.
I&amp;#47;O is the sole method that the virtual machine can communicate with the outside world, whether it&amp;#8217;s receiving user input,
or sending output.&lt;/p&gt;
&lt;p&gt;LC-3 uses a terminal for I&amp;#47;O, which means it can work with standard input (stdin) and standard output (stdout).
Input is done via keyboard, and output via display.
The way this works is &lt;em&gt;memory-mapped I&amp;#47;O&lt;/em&gt;.
In LC-3&amp;#8217;s memory, there are specific addresses that connect to external I&amp;#47;O devices.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;             +----------------+     +----------+
    ··· -----|     memory     |-----| terminal |
             +----------------+     +----------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is useful because LC-3 can reuse the existing instructions for loading&amp;#47;storing from memory to talk to the terminal.
When it manipulates the special memory-mapped locations (device registers), it doesn&amp;#8217;t actually store or load data from memory,
but it communicates with the peripherals like the display or keyboard.&lt;/p&gt;
&lt;p&gt;Here is a full list of memory-mapped addresses in LC-3:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;addr      name                              short description

0xFE00    keyboard status register (KBSR)   has a key been pressed?
0xFE02    keyboard data register   (KBDR)   what key was pressed?
0xFE04    display status register  (DSR)    can the display receive a character?
0xFE06    display data register    (DDR)    character to send to the display
0xFFFE    machine control register (MCR)    power button
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example, to read keyboard input, a program would first poll bit 15 (the ready bit) of KBSR to wait for the user to press a key.
If the bit is &lt;code&gt;0&lt;/code&gt;, then the program keeps waiting.
Otherwise, the bit is &lt;code&gt;1&lt;/code&gt;, and that means the user pressed a key.
Then, the program reads bits [7:0] from KBDR into its registers.
This contains the key that was pressed, encoded as ASCII.
To read more characters, it would continue this loop.&lt;/p&gt;
&lt;p&gt;As an aside, remember that by convention, the least significant bit (the right-most units bit) is bit 0,
and the other bits are numbered right-to-left as 1, 2, 3, and so on.
Not realizing this has caused me issues while implementing this VM,
as the program was reading from bit 15, while my VM was providing information on bit 0 (which if numbered left to right, is bit 15).&lt;/p&gt;
&lt;p&gt;Displaying data is similar: the program first polls bit 15 of DSR (the ready bit) until the display is ready to receive a character.
Then, the program stores a character in DDR encoded as ASCII.
This character is finally sent to the display.&lt;/p&gt;
&lt;p&gt;The program can also halt the computer (shut it down) by setting MCR to all zeroes.&lt;/p&gt;
&lt;p&gt;For detailled information, again, consult the &lt;a href=&quot;https://www.jmeiners.com/lc3-vm/supplies/lc3-isa.pdf&quot;&gt;LC-3 ISA specification&lt;/a&gt;,
specifically the Device Register Assigments.&lt;/p&gt;
&lt;h2 id=&quot;assorted-implementation-details&quot;&gt;Assorted implementation details&lt;/h2&gt;
&lt;p&gt;Now that we&amp;#8217;ve gone through the basics of how LC-3 works, I&amp;#8217;ll go through some interesting details that I encountered during implementation.
If you came here to follow along implementing yourself, I recommend you read &lt;a href=&quot;https://www.jmeiners.com/lc3-vm/&quot;&gt;Meiners&amp;#8217; and Pendleton&amp;#8217;s&lt;/a&gt; LC-3 blog post,
which is actually a tutorial.
For a Rust version, see &lt;a href=&quot;https://www.rodrigoaraujo.me/posts/lets-build-an-lc-3-virtual-machine/&quot;&gt;Rodrigo Araujo&amp;#8217;s implementation&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;endianness&quot;&gt;Endianness&lt;/h3&gt;
&lt;p&gt;Endianness is the order bytes are stored in memory within a word (a single piece of data).
There&amp;#8217;s big endian, and little endian.
By definition, big endian starts with the most significant byte,
and little endian starts with the least significant byte.
By &amp;#8220;most significant&amp;#8221;, it means in numbers like decimal 123, the hundreds position is &amp;#8220;more significant&amp;#8221; than the units position.&lt;/p&gt;
&lt;p&gt;However, I find it more comprehensible to think that big endian is the &amp;#8220;natural&amp;#8221; order,
while little endian is the &amp;#8220;reversed&amp;#8221; order. 
For example, take the number &lt;code&gt;0x12345678&lt;/code&gt;.
On a big endian system, it would be stored in memory like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;address  value (hex)
0x0001:  12
0x0002:  34
0x0003:  56
0x0004:  78
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, on a little endian system, it would be like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;address  value (hex)
0x0001:  78
0x0002:  56
0x0003:  34
0x0004:  12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://softwareengineering.stackexchange.com/questions/95556&quot;&gt;Supposedly, &lt;/a&gt;
it is easier to deal with little endian on processors,
which is why it is used in many popular CPU architectures.
However, LC-3 uses big endian.
This is an issue to consider during implementation.&lt;/p&gt;
&lt;p&gt;For example, if you use &lt;code&gt;hexdump&lt;/code&gt; on a program file, you may see this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hexdump&quot;&gt;0000000 0030 02e0 22f0 25f0 7900 6f00 7500 2000
0000010 6c00 6900 6b00 6500 2000 7600 6900 7200
0000020 7400 7500 6100 6c00 6900 7a00 6900 6e00
0000030 6700 2000 6200 6f00 7900 7300 2000 6400
0000040 6f00 6e00 7400 2000 7900 6f00 7500 0000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is actually incorrect output!
&lt;code&gt;hexdump&lt;/code&gt; assumes groups of two bytes are a single little-endian word,
so it flips it to make it the proper order.
However, LC-3 data is in big endian order.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;hexdump -C&lt;/code&gt; prints bytes as they are on disk, which produces the proper ordering:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hexdump&quot;&gt;00000000  30 00 e0 02 f0 22 f0 25  00 79 00 6f 00 75 00 20
00000010  00 6c 00 69 00 6b 00 65  00 20 00 76 00 69 00 72
00000020  00 74 00 75 00 61 00 6c  00 69 00 7a 00 69 00 6e
00000030  00 67 00 20 00 62 00 6f  00 79 00 73 00 20 00 64
00000040  00 6f 00 6e 00 74 00 20  00 79 00 6f 00 75 00 00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is important to flip bytes or specify that the data is big-endian when reading programs into memory from a file.&lt;/p&gt;
&lt;h3 id=&quot;integer-overflow&quot;&gt;Integer overflow&lt;/h3&gt;
&lt;p&gt;You may know that because integers are represented by a finite amount of bits,
it is possible for them to overflow when they get too big.
For LC-3, we usually implement registers and memory using unsigned 16-bit integers,
which gives us a range of 0-65535.
This is also the limit of our memory&amp;#8217;s size, since we can not represent an address bigger than that.
The same issue makes 32-bit computers unable to have more than around 4GB of memory (&lt;code&gt;(1 &amp;#60;&amp;#60; 32) - 1&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;When integers overflow, they often wrap around back to 0 or the lowest number possible.
This is necessary behaviour on the LC-3, as it makes it possible to use signed numbers in 2&amp;#8217;s complement.
There is no subtract operation, we just add negative numbers, and it magically wraps around to the correct value.
However, we usually do not want integer overflow, so Rust complains when it happens:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error: this arithmetic operation will overflow
 --&amp;#62; test.rs:2:20
  |
2 |     println!(&quot;{}&quot;, 65535u16+1u16)
  |                    ^^^^^^^^^^^^^ attempt to compute `u16::MAX + 1_u16`, which would overflow
  |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Earlier, I mentioned &lt;a href=&quot;https://www.rodrigoaraujo.me/posts/lets-build-an-lc-3-virtual-machine/&quot;&gt;Rodrigo Araujo&amp;#8217;s VM&lt;/a&gt;,
which was also written in Rust.
This implementation served as a Rust reference for me.
In his instruction implementations, he uses casts to perform wrapping arithmetic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;let addr = (vm.registers.get_reg(base_r) as u32 + offset as u32) as u16;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, he adds parameters as 32-bit unsigned ints, then casts it back to 16-bit unsigned.
I personally did not know what behaviour this cast would have,
but upon further experimentation it turned out that it does a modulo operation in the cast.
This means that if the value exceeds the u16 limit, it wraps back to 0.&lt;/p&gt;
&lt;p&gt;Personally, I found this to be a very janky, implicit way to perform wrapping arithmetic.
After all, it took me multiple Google searches and a bit of testing to be sure of what the code was doing.
In my own code, I use explicit syntax for a wrapping addition:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;let addr = vm.registers.get_reg(base_r).wrapping_add(offset);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code is much clearer, and, quoting the Zen of Python, &lt;code&gt;Explicit is better than implicit.&lt;/code&gt;&lt;/p&gt;
&lt;h3 id=&quot;traps&quot;&gt;Traps&lt;/h3&gt;
&lt;p&gt;Earlier, in the memory-mapped I&amp;#47;O section, we saw how LC-3 can use memory-mapped I&amp;#47;O to talk to external peripherals.
You may have noticed that all of this is a very tedious process just to get some user input.
To simplify things, LC-3 implements &lt;em&gt;traps&lt;/em&gt;.
Traps are essentially utility subroutines that make life easier.
These can be accessed with the TRAP instruction, along with a code to specify which subroutine the program wants (the trap vector).&lt;/p&gt;
&lt;p&gt;However, instead of the programmer writing these subroutines, traps are part of an operating system on the LC-3.
The operating system is also a program (it is also comprised of a bunch of instructions in memory), however it runs with higher privileges than the user program.
The OS is stored in a special location in memory, earlier than the user program memory.&lt;/p&gt;
&lt;p&gt;When a TRAP instruction is called, LC-3 takes the trap vector, looks up a corresponding address in the trap vector table (a section in memory before the operating system),
then calls that address as if using the JSR instruction on a subroutine.
These addresses all lead to subroutines within the OS.
For a C analogy, it&amp;#8217;s like the trap vector table is an array of function pointers,
where the functions are part of the operating system.&lt;/p&gt;
&lt;p&gt;Here is a list of trap subroutines in LC-3.
Consult the &lt;a href=&quot;https://www.jmeiners.com/lc3-vm/supplies/lc3-isa.pdf&quot;&gt;LC-3 ISA specification&lt;/a&gt; for a detailled explanation.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;trap vector    name    description

0x20           GETC    get a single character from keyboard (like C&amp;#39;s getchar())
0x21           OUT     put a single character to terminal
0x22           PUTS    put a string to terminal
0x23           IN      get a single character with echo (show the character typed)
0x24           PUTSP   put a string to terminal (two characters packed per memory address)
0x25           HALT    shut down the computer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, these are high level wrappers for the memory-mapped I&amp;#47;O seen in the last section,
and are also much friendlier to work with in general.
Importantly, these routines can all be implemented in LC-3 code.&lt;/p&gt;
&lt;p&gt;For my own virtual machine though, and Justin Meiner&amp;#8217;s VM that inspired it, we do not actually write assembly code for trap routines.
Instead, in the VM itself, we intercept these trap calls, and perform the tasks in high-level C or Rust code, instead of LC-3 assembly.
This is generally simpler, although less faithful to the specification.
Because of this, it is also not necessary to implement some of the memory-mapped registers, like the display registers, and the machine control register.&lt;/p&gt;
&lt;p&gt;For example, here is my code that performs GETC:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;fn trap_getc(vm: &amp;#38;mut VM) {
    while vm.mem.get_mem(0xFE00) &amp;#38; (1 &amp;#60;&amp;#60; 15) == 0 {}
    vm.registers.r0 = vm.mem.get_mem(0xFE02) &amp;#38; 0xFF;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, we poll the Keyboard Ready bit, then we load the keypress into the VM&amp;#8217;s registers.
This type of implementation is much more convenient than writing raw assembly.
My own GETC is not really efficient, but using standard library &lt;code&gt;getchar()&lt;/code&gt; or an alternative would avoid polling the ready bit constantly.
Right now, with the polling loop, we use up a lot of CPU on the host machine running the VM,
when we are doing nothing but waiting.
However, this is the only choice if you actually implement the trap routines in assembly.&lt;/p&gt;
&lt;h3 id=&quot;terminal-inputoutput&quot;&gt;Terminal input&amp;#47;output&lt;/h3&gt;
&lt;p&gt;We&amp;#8217;ve seen in the last section the interface LC-3 provides for I&amp;#47;O, but in this section, I&amp;#8217;ll explain concretely &lt;em&gt;how&lt;/em&gt; the terminal interface works in my own implementation.&lt;/p&gt;
&lt;p&gt;In my LC-3 VM, only the keyboard device registers and the output-related trap routines are directly implemented.
The input-related trap routines are based on the keyboard device registers.&lt;/p&gt;
&lt;h4 id=&quot;output&quot;&gt;Output&lt;/h4&gt;
&lt;p&gt;First, output is relatively simple : we just use the built-in print functions.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;fn trap_puts(vm: &amp;#38;mut VM) {
    let mut idx = vm.registers.r0;
    loop {
        let c = vm.mem.get_mem(idx) as u8 as char;
        if c == &amp;#39;\0&amp;#39; {
            break;
        }

        print!(&quot;{}&quot;, c);
        idx += 1;
    }
    let _ = io::stdout().flush();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example, to output a null-terminated string, we loop through it and print each character, breaking when we see a null.
Importantly, &lt;em&gt;remember to flush stdout&lt;/em&gt;.
This makes sure the output actually appears when needed, and fixes some visual glitches.&lt;/p&gt;
&lt;h4 id=&quot;input&quot;&gt;Input&lt;/h4&gt;
&lt;p&gt;Input is more difficult.
There are a few problems we need to fix:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Blocking input&lt;/strong&gt;: Normal standard library input functions block,
which means that your code will stop and wait until the user types their input.
LC-3 requires that the CPU be able to keep running and periodically check if input comes in,
instead of pausing everything to wait for input.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Buffered input&lt;/strong&gt;: In a terminal, input is buffered, which means that input is only sent to the program when you press Enter.
This behaviour is called &amp;#8220;canonical mode&amp;#8221;.
This is not what we want: we want to get raw keypresses.
It would not be fun to have to press Enter after each keypress for it to register.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Echo&lt;/strong&gt;: In a terminal, when you type, the letters you type show up.
This behaviour is called echo.
We do not want this: we want the program to silently read user input to avoid visual clutter.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We&amp;#8217;ll first get rid of canonical mode and echo.
This can be done on Linux using termios:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;fn setup_termios() {
    let mut term: Termios = Termios::from_fd(STDIN_FILENO).unwrap();
    term.c_lflag &amp;#38;= !(ICANON | ECHO);
    &amp;#47;&amp;#47; TCSANOW: &quot;the change occurs immediately&quot;
    tcsetattr(STDIN_FILENO, TCSANOW, &amp;#38;term).unwrap();

    &amp;#47;&amp;#47; when leaving the program we want to be polite and undo the above changes
    ctrlc::set_handler(|| {
        restore_terminal();
        &amp;#47;&amp;#47; typical CTRL-C exit code
        std::process::exit(130);
    })
    .expect(&quot;Failed to set CTRL-C handler&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, we disable the &lt;code&gt;ICANON&lt;/code&gt; and &lt;code&gt;ECHO&lt;/code&gt; bit-flags.
We also set a Ctrl-C handler:
if we exit the LC-3 VM unexpectedly,
we don&amp;#8217;t want to be stuck with weird terminal settings.
All &lt;code&gt;restore_terminal&lt;/code&gt; does is flip on the flags we disabled.&lt;/p&gt;
&lt;p&gt;Now, we have instant, silent input.
However, we still block on input.
This means that with code that deals with user input,
the program freezes up between keypresses.&lt;/p&gt;
&lt;p&gt;To fix this, we need &lt;em&gt;non-blocking input&lt;/em&gt;.
There are libraries to do this, however I decided to use standard Rust features to do it instead.&lt;/p&gt;
&lt;p&gt;We first create a thread dedicated to managing stdin.
This thread will block until the user presses a key, however it does not block the main thread.
There is a &amp;#8220;channel&amp;#8221; between this thread and the main thread that allows one-way communication.
This channel is like a queue data structure : the input thread can send information about key-presses,
and when the main thread is ready, it can receive this information when it wants to.&lt;/p&gt;
&lt;p&gt;In Rust, I use a &lt;code&gt;TerminalIO&lt;/code&gt; struct to implement this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;impl TerminalIO {
    pub fn new() -&amp;#62; TerminalIO {
        setup_termios();
        TerminalIO {
            stdin_channel: Self::spawn_stdin_channel(),
            char: None,
        }
    }

    fn spawn_stdin_channel() -&amp;#62; Receiver&amp;#60;u8&amp;#62; {
        &amp;#47;&amp;#47; https:&amp;#47;&amp;#47;stackoverflow.com&amp;#47;questions&amp;#47;30012995
        let (tx, rx) = mpsc::channel::&amp;#60;u8&amp;#62;();
        let mut buffer: [u8; 1] = [0];
        thread::spawn(move || loop {
            let _ = io::stdin().lock().read_exact(&amp;#38;mut buffer);
            let _ = tx.send(buffer[0]);
        });
        rx
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, we use a closure (the &lt;code&gt;move&lt;/code&gt; here means that the function acquires the variables in the outside scope)
that runs in a new thread.
It is an infinite loop that waits for a single byte of input from the user,
then transmits it over the channel back to the main thread.&lt;/p&gt;
&lt;p&gt;Back in the main thread, I then implement the KBSR and KBDR registers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;impl KeyboardIO for TerminalIO {
    fn get_key(&amp;#38;mut self) -&amp;#62; Option&amp;#60;u8&amp;#62; {
        let c = self.char;
        self.char = None;
        c
    }

    fn check_key(&amp;#38;mut self) -&amp;#62; bool {
        match self.char {
            Some(c) =&amp;#62; true,
            None =&amp;#62; match self.stdin_channel.try_recv() {
                Ok(key) =&amp;#62; {
                    self.char = Some(key);
                    true
                }
                Err(mpsc::TryRecvError::Empty) =&amp;#62; false,
                Err(mpsc::TryRecvError::Disconnected) =&amp;#62; panic!(&quot;terminal keyboard stream broke&quot;),
            },
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the main thread, when the VM checks if there is a keypress ready through the Keyboard Ready bit,
we attempt to receive a keystroke over the channel from the input thread.
If the channel is empty, return that there is no keypress ready.
Otherwise, store the character we received.
Then, when the VM gets a key through the Keyboard Data register, we give it this character.&lt;/p&gt;
&lt;p&gt;I personally find that this solution is elegant:
it allows for the VM to keep working while waiting on user input,
and it also doesn&amp;#8217;t take a bunch of boilerplate and working with obscure options like file descriptors.
The input thread just uses normal input functions, and passes it over to the main thread to be read later.&lt;/p&gt;
&lt;h2 id=&quot;debugging&quot;&gt;Debugging&lt;/h2&gt;
&lt;p&gt;Implementing a virtual machine can often introduce hard-to-find bugs.
Indeed, there&amp;#8217;s no such thing as a syntax error or a type error when you&amp;#8217;re dealing with assembly.
When something goes wrong, you&amp;#8217;ll have absolutely no indication of where the issue stems from:
you&amp;#8217;ll just see weird behaviour.
With LC-3, though, you can be reasonably sure that the programs you&amp;#8217;re running (like 2048, Rogue),
can be trusted to be bug-free, given that they&amp;#8217;ve existed for years.
Therefore, any bug most certainly stems from you, the virtual machine author.&lt;/p&gt;
&lt;p&gt;To find the source of these bugs in your virtual machine implementation,
I recommend that you read over the code implementing all the instructions,
and compare it to the ISA specification.
I find that this is in general great advice for programming anything that involves logic.
It may not seem like reading will do much,
but you will be able to catch many, many, dumb mistakes with this method.&lt;/p&gt;
&lt;p&gt;Let&amp;#8217;s talk about my own experience debugging LC-3.
Rogue worked perfectly,
but when running the 2048 program, the game started to an empty grid.
(Usually, 2048 has tiles in the grid.)
Pressing keys would not do anything either.
To fix this, I tried applying my earlier advice about re-reading your code.
I read 70% of the instructions implementation file,
then decided that it was probably not worth the effort to continue.
(We will see later this was a bad decision.)&lt;/p&gt;
&lt;p&gt;I then did run-time debugging of what was happening in the VM.
First, I wrote some debug print statements (these are still available with the &lt;code&gt;--debug&lt;/code&gt; flag of the VM.)
These had the following format:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PC: 0x3312, op: ADD, params: 0x261
R0: 0x0
R1: 0x29a
R2: 0x0
R3: 0x0
R4: 0x0
R5: 0x3017
R6: 0x3ffc
R7: 0x32db
COND: 0x2 (Z)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All the registers&amp;#8217; contents are displayed, as well as information about the current instruction.
Every cycle, this information is printed to stderr, which allows the debug stream to be piped into a log file separate from regular output.
The log is useful, but is too fast to be read while the VM is running, and doesn&amp;#8217;t show any information about memory.
Most importantly though, it doesn&amp;#8217;t have one of the best creature comforts that you&amp;#8217;d expect from a debugger: breakpoints.&lt;/p&gt;
&lt;p&gt;At this point, I figured it was probably best to use a real debugger.
For those who have used C or C++ on Linux before, you probably have experience with using GDB to debug.
GDB is a venerable debugger with a command-line interface.
The user experience is quite unfriendly, but it&amp;#8217;s efficient, and fast.
It turns out that GDB also works in Rust, with some slight modifications.
This debugger, &lt;code&gt;rust-gdb&lt;/code&gt;, comes packaged with the Rust compiler.&lt;/p&gt;
&lt;p&gt;We can&amp;#8217;t just directly use &lt;code&gt;rust-gdb&lt;/code&gt; to debug our virtual machine software though.
The debugger doesn&amp;#8217;t understand LC-3 assembly;
we can&amp;#8217;t just tell it to, for example, break on a given line in the LC-3 code.
First, I set up a breakpoint in the fetch-execute loop.
This means that entering &lt;code&gt;c&lt;/code&gt; (continue) in GDB will step through a single LC-3 instruction.
With the assembly source code in a separate window,
it is possible to step through the execution of the program
and examine the instructions and how they affect the registers.&lt;/p&gt;
&lt;p&gt;However, stepping through instructions individually gets tedious eventually.
Monitoring the PC register, I wrote down all the addresses of a few instructions as comments in the assembly source.
It&amp;#8217;s also possible to get addresses by counting how many instructions there are in the source code.
I then used GDB&amp;#8217;s conditional breakpoints to break in the VM only when PC reaches that address.
In essence, this is a breakpoint within the LC-3 code.
To make this process faster, I made a GDB macro to automate it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;define vmb
    # set a breakpoint at VM addr $0
    break lc3::vm::instruction::execute_instruction if vm.registers.pc == $arg0 + 1
    set $vmb_break = $bpnum
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a new command that can be used to set breakpoints within LC-3.&lt;/p&gt;
&lt;p&gt;Using this, I narrowed down the source of the bug in 2048 to the &lt;code&gt;RAND_MOD&lt;/code&gt; subroutine,
which is supposed to provide a random number within a given range.
This is used to determine where a new tile is placed in the grid.
When tested, it was giving a garbage number entirely outside the range argument.
Then, I further narrowed the issue down to the division&amp;#47;modulo subroutine, &lt;code&gt;MOD_DIV&lt;/code&gt;,
giving the wrong answer.
Stepping through this function,
I finally found a single instruction that was behaving oddly: &lt;code&gt;NOT&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;As it turns out, I had made a very subtle typo in the implementation of this instruction:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt;     let res = !vm.registers.get_reg(sr);
-    vm.registers.set_reg_with_cond(sr, res);
+    vm.registers.set_reg_with_cond(dr, res);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of performing the operation on the source register (&lt;code&gt;sr&lt;/code&gt;) and storing the result in the destination register (&lt;code&gt;dr&lt;/code&gt;),
I had stored the result back in the source.
In Rogue, this had not caused any issues, since that program only uses this instruction &amp;#8220;in-place&amp;#8221; (&lt;code&gt;NOT R0, R0&lt;/code&gt;).
However, this behaviour is obviously incorrect when the source and destination are different (&lt;code&gt;NOT R2, R1&lt;/code&gt;), like in 2048.
This spread corrupted data everywhere, and was hard to diagnose.&lt;/p&gt;
&lt;p&gt;In the end, had I followed my earlier advice about re-reading the code, and I hadn&amp;#8217;t given up midway,
I would&amp;#8217;ve noticed this much quicker.
Indeed, this will serve as a lesson for me to properly check over code I write in the future,
and make sure that it is not sloppy.&lt;/p&gt;
&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;While LC-3 is still a relatively simple program,
implementing it as a virtual machine is still quite educational.
I learned about the core functionality of computers,
as well as how assembly works.
Through LC-3, I also learned the basics of Rust
and got more experience finding subtle bugs too.
If you&amp;#8217;re looking to sharpen your skills in a language,
consider the idea of writing a simple virtual machine.
Or, maybe, a disassembler or assembler for LC-3.&lt;/p&gt;
&lt;p&gt;Either way, the fact that just this small, contrived system can be so interesting is surprising.
This is just a microcosm of computing,
and there is so, so much more to discover.&lt;/p&gt;
&lt;p&gt;Again, if you&amp;#8217;re interested in further reading about LC-3,
read the &lt;a href=&quot;https://www.jmeiners.com/lc3-vm/&quot;&gt;original blog post&lt;/a&gt; that inspired this one.&lt;/p&gt;
&lt;p&gt;Also, thank you for bearing with me this far.
This post is longer than any that I&amp;#8217;ve written up to now.
I hope you enjoyed my journey with LC-3 as much as I did.&lt;/p&gt;</content>
		<link href="https://www.dogeystamp.com/lc3"/>
		<id>https://www.dogeystamp.com/lc3</id>
		<updated>2024-01-20T10:00:00Z</updated>
		<published>2024-01-20T10:00:00Z</published>
	</entry>
	<entry>
		<title>Hello World (from barf.sh)</title>
		<content type="html">&lt;h1 id=&quot;hello-world-from-barf.sh&quot;&gt;Hello World (from barf.sh)&lt;/h1&gt;
&lt;div class=&quot;creation-date&quot;&gt;
2023-05-14
&lt;/div&gt;
&lt;p&gt;Another year, another static site generator.&lt;/p&gt;
&lt;p&gt;This time, for the &lt;a href=&quot;https://git.dogeystamp.com/dogeystamp/wb5&quot;&gt;5th iteration&lt;/a&gt; of the website,
I&amp;#8217;m using &lt;a href=&quot;https://barf.bt.ht&quot;&gt;barf&lt;/a&gt; to generate it.&lt;/p&gt;
&lt;p&gt;Personally, I think the old script I was using, &lt;a href=&quot;https://romanzolotarev.com/ssg.html&quot;&gt;ssg&lt;/a&gt;,
was really hacky in comparison to barf.
One of my biggest grudges is how there was no post list on the front page by default.
Most of the people using ssg (including me) had to write their own messy &lt;code&gt;sed&lt;/code&gt; contraptions to implement it.&lt;/p&gt;
&lt;p&gt;Thankfully, barf implements not only the post list, but also Atom feed generation, which is very convenient.&lt;/p&gt;
&lt;p&gt;Other than that, barf excels by being tiny.
Unlike ssg, the barf script fits in 3 screenfuls of text for me.
In my opinion, it&amp;#8217;s much more readable.&lt;/p&gt;
&lt;p&gt;The great thing about these two scripts is that migrating my website from one to the other is really simple.
They both work similarly, so I just needed minor changes to port over my (admittedly few) pages.&lt;/p&gt;
&lt;p&gt;Anyways, I&amp;#8217;ll hopefully write some blog posts soon and fix the remaining dead links on the website.&lt;/p&gt;</content>
		<link href="https://www.dogeystamp.com/hello_world"/>
		<id>https://www.dogeystamp.com/hello_world</id>
		<updated>2023-05-14T10:00:00Z</updated>
		<published>2023-05-14T10:00:00Z</published>
	</entry>
	<entry>
		<title>Using the shell as a file picker for Qutebrowser</title>
		<content type="html">&lt;h1 id=&quot;using-the-shell-as-a-file-picker-for-qutebrowser&quot;&gt;Using the shell as a file picker for Qutebrowser&lt;/h1&gt;
&lt;div class=&quot;creation-date&quot;&gt;
2023-06-28
&lt;/div&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/qutebrowser/qutebrowser&quot;&gt;Qutebrowser&lt;/a&gt; is pretty great (thanks The-Compiler &amp;#60;3).
For those who don&amp;#8217;t know about it, it&amp;#8217;s essentially a Vim-like browser:
there&amp;#8217;s a bunch of cryptic shortcuts, but they maximize your efficiency.
The best part of Vim, which is replicated in Qutebrowser,
is the ability to do everything without taking your hands off the keyboard.&lt;/p&gt;
&lt;p&gt;Anyways, an issue I had was that when uploading a file in Qutebrowser is that it calls on a normal GUI file picker.
This is probably a sane default, but the default file picker does not have Vim shortcuts (absolutely unusable!).
Now, you &lt;em&gt;could&lt;/em&gt; use &lt;a href=&quot;https://github.com/ranger/ranger&quot;&gt;ranger&lt;/a&gt;, or &lt;a href=&quot;https://github.com/vifm/vifm&quot;&gt;vifm&lt;/a&gt;
in the way described &lt;a href=&quot;https://old.reddit.com/r/qutebrowser/comments/r9igqe/need_help_using_ranger_as_file_chooser/&quot;&gt;here&lt;/a&gt;,
but that&amp;#8217;s boring.
Instead, as a masochist power user, I manage all my files exclusively in the shell,
and I wanted to have that experience in Qutebrowser.
So, I made a 25-line script &lt;code&gt;fish-fm&lt;/code&gt; that does just that.&lt;/p&gt;
&lt;h2 id=&quot;demonstration&quot;&gt;Demonstration&lt;/h2&gt;
&lt;p&gt;Here&amp;#8217;s an example usage of fish-fm.&lt;/p&gt;
&lt;p&gt;First, when a website asks you to upload a file, qutebrowser opens a terminal window with fish-fm.
Then, you have a full fish shell in which
you can run &lt;code&gt;sxiv&lt;/code&gt;, &lt;code&gt;mpv&lt;/code&gt;, or any other command to inspect your files first.
This also includes all its features like history and autosuggestions.
Once you figure out what you want to upload, you then run &lt;code&gt;sel [file paths]&lt;/code&gt;.
The great thing about this is that you can use any glob or even xargs with this command.
As a demo:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/fish-fm/term-thumb.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;../public/img/fish-fm/discord-thumb.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3 id=&quot;sxiv-selection&quot;&gt;sxiv selection&lt;/h3&gt;
&lt;p&gt;Optionally, you can run &lt;code&gt;ssel [directory or file paths]&lt;/code&gt;,
which uses sxiv to view your images.
Then, you can mark the images (see sxiv&amp;#8217;s man page), and those will be uploaded.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/fish-fm/sxiv.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;installation&quot;&gt;Installation&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Install the dependencies: qutebrowser, fish&lt;/li&gt;
&lt;li&gt;Copy &lt;a href=&quot;https://github.com/dogeystamp/dots/blob/main/src/.local/bin/fish-fm&quot;&gt;fish-fm&lt;/a&gt; somewhere in your PATH.&lt;/li&gt;
&lt;li&gt;Create a qutebrowser &lt;a href=&quot;https://github.com/qutebrowser/qutebrowser/blob/master/doc/help/configuring.asciidoc#configuring-qutebrowser-via-configpy&quot;&gt;config.py&lt;/a&gt;.
Then, add these lines:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;c.fileselect.handler = &quot;external&quot;
c.fileselect.multiple_files.command = [&quot;st&quot;, &quot;-e&quot;, &quot;fish&quot;, &quot;-C&quot;, &quot;set -x OUTPUT {}; source ~&amp;#47;.local&amp;#47;bin&amp;#47;fish-fm&quot;]
c.fileselect.single_file.command = [&quot;st&quot;, &quot;-e&quot;, &quot;fish&quot;, &quot;-C&quot;, &quot;set -x OUTPUT {}; source ~&amp;#47;.local&amp;#47;bin&amp;#47;fish-fm&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Depending on your terminal, you might have to edit these commands.
I personally use &lt;code&gt;st&lt;/code&gt;; yours may be different.
The &lt;code&gt;{}&lt;/code&gt; here is a placeholder for a temporary file where fish-fm writes its output.
This is necessary because terminals don&amp;#8217;t forward the stdout of their commands,
so fish-fm can&amp;#8217;t just print the files selected.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your file picker is now a fish shell.&lt;/li&gt;
&lt;/ul&gt;</content>
		<link href="https://www.dogeystamp.com/fish-fm"/>
		<id>https://www.dogeystamp.com/fish-fm</id>
		<updated>2023-06-28T10:00:00Z</updated>
		<published>2023-06-28T10:00:00Z</published>
	</entry>
	<entry>
		<title>Getting into crochet</title>
		<content type="html">&lt;h1 id=&quot;getting-into-crochet&quot;&gt;Getting into crochet&lt;/h1&gt;
&lt;div class=&quot;creation-date&quot;&gt;
2026-04-01
&lt;/div&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/crochet/ribbon.jpg&quot; alt=&quot;A crochet ribbon&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As of today, we&amp;#8217;re completely rebranding dogeystamp.com as an arts and crafts website!&lt;/p&gt;
&lt;p&gt;A few months ago, I remembered that joke about how senior software engineers invariably
retire to tend to crops and a chicken farm,
or to do woodworking.
I wanted be ahead of the curve, so I too decided to get a hobby that involved tangible things:
I learned &lt;a href=&quot;https://en.wikipedia.org/wiki/Crochet&quot;&gt;crochet&lt;/a&gt;,
which a way of making things (like that ribbon in the picture above) using yarn.&lt;/p&gt;
&lt;p&gt;I&amp;#8217;m not skilled or anything, so as the title suggests, this article mostly describes the beginner experience,
and what I wish I&amp;#8217;d known earlier.
If you had to retain one thing from reading this, I would say that crochet is quite fun and accessible,
as long as you have patience.
Notably, I won&amp;#8217;t actually explain &lt;em&gt;how to&lt;/em&gt; crochet; many people have made much better online resources for that.&lt;/p&gt;
&lt;h2 id=&quot;materials&quot;&gt;Materials&lt;/h2&gt;
&lt;p&gt;You only really need two things to crochet, the yarn and a crochet hook,&lt;sup id=&quot;fnref1&quot;&gt;&lt;a href=&quot;#fn1&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;
and at the end you&amp;#8217;ll need scissors to cut away the yarn you haven&amp;#8217;t used.
Usually, people have scissors already, so yarn and the hook are the only thing you need to get.
Hooks and yarn both come in different sizes, and you do want the sizes to match.
There is some margin of error on the sizes, though, so a small mismatch is fine.&lt;/p&gt;
&lt;p&gt;I have actually never personally bought a hook or yarn as of writing, because
some very generous people I know gifted me hooks and yarn.&lt;sup id=&quot;fnref2&quot;&gt;&lt;a href=&quot;#fn2&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;
It&amp;#8217;s probably not hard to buy yarn and a hook of matching sizes, but if you really want to be safe,
they sell kits for beginners that should work fine out of the box.&lt;/p&gt;
&lt;h2 id=&quot;getting-started&quot;&gt;Getting started&lt;/h2&gt;
&lt;p&gt;Crochet in particular
is nice (compared to something like knitting)
at the beginner stage, because every single stitch you make can be effortlessly undone
by unravelling the yarn.&lt;/p&gt;
&lt;p&gt;So to figure out how to crochet, I repeatedly made then unmade
yarn squares, following tutorials online.
(Pretty much any tutorial for complete beginners seems to be about making these swatches.)
Here&amp;#8217;s a work-in-progress picture of one of my first squares:
&lt;img src=&quot;../public/img/crochet/guh.jpg&quot; alt=&quot;A square crochet swatch&quot; /&gt;
It turns out that the techniques for making squares
are actually sufficient for creating many interesting items.
In fact, all of the creations pictured in this article
are entirely composed of rectangles stitched together in different ways.&lt;/p&gt;
&lt;p&gt;The process for making a square swatch starts with making a &lt;em&gt;chain&lt;/em&gt;
of yarn,
then repeatedly building rows of &lt;em&gt;single crochet&lt;/em&gt; stitches on top of that foundation,
as if laying bricks to build a wall.&lt;sup id=&quot;fnref3&quot;&gt;&lt;a href=&quot;#fn3&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Most of the difficulty in learning crochet
comes from figuring out which hole to insert the hook into.
On every row,
there will be V-shapes,
which you are supposed to pass the hook through to create stitches.
I&amp;#8217;d follow tutorials step by step,
and end up confused because I couldn&amp;#8217;t recognize
the shapes from the tutorial when looking at the yarn in my hands.
For me, it helped to fiddle and twist the row
until the V-shapes were clearly visible,
after which everything looked exactly like the tutorials.&lt;/p&gt;
&lt;p&gt;Other than figuring out how to put the hook in the right hole, an
important aspect of crochet is making your stitches nice and regular.
There are two things that helped me with this.
First, I learned that the &lt;em&gt;thickest part&lt;/em&gt; of the hook is a measurement tool;
by always wrapping your yarn around the thickest part when making stitches,
your stitches will have a regular size.
If you wrap yarn around a thinner part, the holes you make will be too tight to pass the hook through.
Second, I watched &lt;a href=&quot;https://youtube.com/watch?v=-n_iZFkye8o&quot;&gt;this video&lt;/a&gt;
about the &amp;#8220;golden loop,&amp;#8221; which contributed to making my stitches more consistent.
The video itself is much better at explaining than I am,
so please watch that for further information.&lt;/p&gt;
&lt;h2 id=&quot;making-things&quot;&gt;Making things&lt;/h2&gt;
&lt;p&gt;After becoming half-decent at making square swatches, I was able to make more interesting things.&lt;/p&gt;
&lt;h3 id=&quot;ribbons&quot;&gt;Ribbons&lt;/h3&gt;
&lt;p&gt;I followed &lt;a href=&quot;https://youtube.com/watch?v=GaTBLmiAFq0&quot;&gt;this tutorial&lt;/a&gt; for making a bow tie &amp;#47; ribbon
just like the one pictured at the top of this page.&lt;/p&gt;
&lt;p&gt;The ribbon is actually composed of two rectangles; one is for the main body part,
and one for the two dangly bits below. The tutorial tells you the sizes (number
of chains and number of rows) to make both rectangles, and once you make them,
you attach them together by threading yarn through the stitches.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/crochet/ribbon3.jpg&quot; alt=&quot;Two parts of a crochet ribbon.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Ribbons were fun, since they don&amp;#8217;t take a lot of time or material to make,
are prettier than square swatches,
and use the single crochet technique used for the beginner squares.
The method I used for &amp;#8220;sewing&amp;#8221; together the rectangles was also pretty much just single crochet stitches,&lt;sup id=&quot;fnref4&quot;&gt;&lt;a href=&quot;#fn4&quot; rel=&quot;footnote&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;
so once you figure out how to make a square, a ribbon isn&amp;#8217;t much of a big step up.&lt;/p&gt;
&lt;h3 id=&quot;beanie&quot;&gt;Beanie&lt;/h3&gt;
&lt;p&gt;Once I made a few ribbons, I got bored and started looking for a new project.
Taking some inspiration from the friend that helped me get into crochet, I made a beanie with cutesy ears,&lt;sup id=&quot;fnref5&quot;&gt;&lt;a href=&quot;#fn5&quot; rel=&quot;footnote&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;
following &lt;a href=&quot;https://vivcrochets.com/cat-ear-beanie/&quot;&gt;this tutorial&lt;/a&gt;.
Here&amp;#8217;s me wearing the beanie:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/crochet/beanie.jpg&quot; alt=&quot;A crochet beanie with dog ears, worn by a cartoon face&quot; /&gt;&lt;/p&gt;
&lt;p&gt;(It&amp;#8217;s hard to demonstrate a beanie without showing my face.)
Despite the seemingly curved ears, the beanie is formed from a single rectangle;
when worn, it naturally folds in a way that gives ears.&lt;sup id=&quot;fnref6&quot;&gt;&lt;a href=&quot;#fn6&quot; rel=&quot;footnote&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;
Thus, making a beanie is really not that different from the square swatches I started off with.
However, with this project, I did encounter some challenges.&lt;/p&gt;
&lt;p&gt;First, the beanie does not just use single crochet stiches; it uses &lt;em&gt;half-double crochet&lt;/em&gt; stitches, and &lt;em&gt;slip stitches&lt;/em&gt;.
This sounds quite daunting, but these stitches are actually quite similar to single crochet.
For me, learning the new stitches took much less time than figuring out single crochet for the first time,
because most of the muscle memory was the same.&lt;/p&gt;
&lt;p&gt;Second, getting the dimensions of the beanie right was challenging.
Making the beanie, I had to pick its width and length so that it could fit on my head.
After two weeks of crocheting,
I realized that I was running out of yarn,
and, even worse than that, my beanie was comically tall.
Unfortunately, I had picked completely incorrect dimensions,
so I had to unravel everything and start over again.
Thankfully, the second time crocheting the beanie, I was much faster because I got better at crochet.&lt;/p&gt;
&lt;p&gt;Another thing to watch out for in terms of dimensions
is that putting your crochet work in the washing machine might shrink it.
Apparently &lt;a href=&quot;https://sarahmaker.com/block-crochet/&quot;&gt;blocking&lt;/a&gt;
helps with shrinkage; I don&amp;#8217;t know much about blocking though.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Learning crochet was fun; it&amp;#8217;s easy to get into, and very forgiving of mistakes.
Best of all, I get to keep the tangible fruits of my labor,
and put cool photographs of them in this article.&lt;/p&gt;
&lt;p&gt;There&amp;#8217;s much much more to crochet than what I&amp;#8217;ve outlined here;
notably, &lt;a href=&quot;https://en.wikipedia.org/wiki/Amigurumi&quot;&gt;amigurumi&lt;/a&gt; techniques let you make things that aren&amp;#8217;t just rectangles.
Hopefully though, this is enough to convey how getting into crochet is really accessible.&lt;/p&gt;
&lt;p&gt;That concludes my thoughts on crochet.
Happy April Fool&amp;#8217;s Day!&lt;sup id=&quot;fnref7&quot;&gt;&lt;a href=&quot;#fn7&quot; rel=&quot;footnote&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
&lt;hr/&gt;
&lt;ol&gt;

&lt;li id=&quot;fn1&quot;&gt;
&lt;p&gt;&amp;#8220;Crochet hook&amp;#8221; means &amp;#8220;hook hook&amp;#8221; but it is what it is.&amp;#160;&lt;a href=&quot;#fnref1&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn2&quot;&gt;
&lt;p&gt;On the off-chance you are reading this, thanks a lot for the materials and your guidance :)&amp;#160;&lt;a href=&quot;#fnref2&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn3&quot;&gt;
&lt;p&gt;For a niche 3D modeling metaphor for crochet, see &lt;a href=&quot;https://youtube.com/watch?v=xtlDND7NVp8&quot;&gt;this CodeParade video&lt;/a&gt;.&amp;#160;&lt;a href=&quot;#fnref3&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn4&quot;&gt;
&lt;p&gt;Often, tutorials recommend that you use a proper sewing needle to sew together different pieces.
So far, I have gotten away with just stitching pieces together.&amp;#160;&lt;a href=&quot;#fnref4&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn5&quot;&gt;
&lt;p&gt;Everyone says these are cat ears, but I assure you these are shiba ears because I&amp;#8217;m &lt;em&gt;doge&lt;/em&gt;ystamp.&amp;#160;&lt;a href=&quot;#fnref5&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn6&quot;&gt;
&lt;p&gt;The normal not-eared beanie is not a rectangle, and is arguably harder to make.&amp;#160;&lt;a href=&quot;#fnref6&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn7&quot;&gt;
&lt;p&gt;To the astute readers who noticed that I posted this a few days late and backdated it to April 1st:
shush you didn&amp;#8217;t notice anything.&amp;#160;&lt;a href=&quot;#fnref7&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;
&lt;/div&gt;</content>
		<link href="https://www.dogeystamp.com/crochet"/>
		<id>https://www.dogeystamp.com/crochet</id>
		<updated>2026-04-01T10:00:00Z</updated>
		<published>2026-04-01T10:00:00Z</published>
	</entry>
	<entry>
		<title>Lower bound, upper bound in C++, visually explained</title>
		<content type="html">&lt;h1 id=&quot;lower-bound-upper-bound-in-c-visually-explained&quot;&gt;Lower bound, upper bound in C++, visually explained&lt;/h1&gt;
&lt;div class=&quot;creation-date&quot;&gt;
2024-04-02
&lt;/div&gt;
&lt;p&gt;One of the most common problems programmers have to solve is retrieving specific data — finding the needle in the haystack.
To make this simpler, languages provide standard tools to perform this task:
in particular, this post focuses on C++&amp;#8217;s &lt;code&gt;lower_bound&lt;/code&gt; and &lt;code&gt;upper_bound&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Personally, I find that the documentation for these functions is quite unclear and verbose.
For example, &lt;a href=&quot;https://en.cppreference.com/w/cpp/algorithm/lower_bound&quot;&gt;cppreference.com&lt;/a&gt;
describes &lt;code&gt;lower_bound&lt;/code&gt; like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Searches for the first element in the partitioned range [first, last) which is
**not** ordered before value.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That sounds like gibberish, and is too technical to quickly understand.
For that reason, I&amp;#8217;m making this blog post to explain my own mental model of these functions.&lt;/p&gt;
&lt;h2 id=&quot;refresher-on-binary-search&quot;&gt;Refresher on binary search&lt;/h2&gt;
&lt;p&gt;First, it&amp;#8217;s important to understand how &lt;code&gt;lower_bound&lt;/code&gt; and &lt;code&gt;upper_bound&lt;/code&gt; work under the hood.&lt;/p&gt;
&lt;p&gt;As you know, finding words in a dictionary is relatively fast.
This is possible because the words are in alphabetical order.
If the words weren&amp;#8217;t ordered, you&amp;#8217;d have to look through every single word in the dictionary, one by one.
That would be an excruciating, and much slower process.
Because of the ordering, you can rapidly narrow down the word you want.&lt;/p&gt;
&lt;p&gt;Computers can do the same with ordered data: this is called &lt;em&gt;binary search&lt;/em&gt;,
and is what powers &lt;code&gt;lower_bound&lt;/code&gt; and &lt;code&gt;upper_bound&lt;/code&gt;.
For example, say our dictionary is 1000 pages, and the computer wants to look for the word &amp;#8220;rabbit&amp;#8221;.
These are the steps it takes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Start at exactly page 500.&lt;/li&gt;
&lt;li&gt;See the word &amp;#8220;murmur&amp;#8221;, so go forwards to page 750.&lt;/li&gt;
&lt;li&gt;See the word &amp;#8220;sunny&amp;#8221;, so go backwards to page 625.&lt;/li&gt;
&lt;li&gt;And so on.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is called &amp;#8220;binary search&amp;#8221; because we halve the region we are looking in every time (we pick either the left half, or the right half.)
For step 1, the computer is halving the range &lt;code&gt;1-1000&lt;/code&gt;.
In step 2, &lt;code&gt;500-1000&lt;/code&gt;. Then for step 3, &lt;code&gt;500-750&lt;/code&gt;.
This is like the way humans look at dictionaries, but more structured.&lt;/p&gt;
&lt;p&gt;Anyways, this is not intended to be a full explanation of binary search: refer to &lt;a href=&quot;https://youtube.com/watch?v=KXJSjte_OAI&quot;&gt;Tom Scott&amp;#8217;s video&lt;/a&gt; about it for more information.&lt;/p&gt;
&lt;h2 id=&quot;lower-bound-and-upper-bound&quot;&gt;Lower bound and upper bound&lt;/h2&gt;
&lt;p&gt;Back to the real subject of this post: &lt;code&gt;lower_bound&lt;/code&gt; and &lt;code&gt;upper_bound&lt;/code&gt; in C++.
What I used to understand of these functions is that they use binary search to find elements in a sorted container.
However, I didn&amp;#8217;t get what differentiated them.
Again, if you read solely the documentation about these functions, it&amp;#8217;s not easily comprehensible.&lt;/p&gt;
&lt;p&gt;First of all, say we wish to search for the integer &lt;code&gt;k&lt;/code&gt; (k for key) in a sorted vector (array) of integers &lt;code&gt;v&lt;/code&gt;.
We can find the lower and upper bounds with these function calls:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;&amp;#47;&amp;#47; (you could use auto here instead of the verbose type)
std::vector&amp;#60;int&amp;#62;::iterator lb = std::lower_bound(v.begin(), v.end(), k);
std::vector&amp;#60;int&amp;#62;::iterator ub = std::upper_bound(v.begin(), v.end(), k);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Based on the documentation, we know
the first two arguments specify the region of &lt;code&gt;v&lt;/code&gt; we&amp;#8217;re looking in.
Here, it&amp;#8217;s the entire vector (from the beginning to the end).
Also, put simply, the functions return by default:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;lower_bound&lt;/code&gt;: the first element &lt;code&gt;e&lt;/code&gt; where &lt;code&gt;k &amp;#60;= e&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;upper_bound&lt;/code&gt;: the first element &lt;code&gt;e&lt;/code&gt; where &lt;code&gt;k &amp;#60; e&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Both functions return &lt;code&gt;v.end()&lt;/code&gt; if no valid element is found.
This iterator points just &lt;strong&gt;after&lt;/strong&gt; the last element of &lt;code&gt;v&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;This is the technical definition; it doesn&amp;#8217;t mean much by itself.
However, with a concrete example with real numbers, it clicked in my mind.
For example, let &lt;code&gt;k = 3&lt;/code&gt;.
Here is an example sorted array &lt;code&gt;v&lt;/code&gt;, with upper and lower bounds marked:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    lower   upper
      ↓       ↓
1 2 2 3 3 3 3 4 5 6
      ───────
         ↑
 matching interval
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first &lt;code&gt;3&lt;/code&gt; is the lower bound: it&amp;#8217;s the first element bigger or equal to our key.
The &lt;code&gt;4&lt;/code&gt; is the upper bound, the first element strictly bigger than our key.&lt;/p&gt;
&lt;p&gt;Here, when it&amp;#8217;s laid out visually, it&amp;#8217;s now clear what the lower and upper bounds mean:
it&amp;#8217;s the &lt;em&gt;bounds of the interval&lt;/em&gt; that matches our search key.
This is mostly useful if the array has duplicate elements.&lt;/p&gt;
&lt;p&gt;Notice how the upper bound is one past the end of the interval,
just like how &lt;code&gt;v.end()&lt;/code&gt; is one past the last element of the vector.
This is usually how C++ iterators work, and makes some tasks more convenient.
Take this regular for loop:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for (int i = 0; i &amp;#60; 10; i++) { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This loop will iterate over the numbers &lt;code&gt;0&lt;/code&gt; to &lt;code&gt;9&lt;/code&gt;,
excluding the upper bound &lt;code&gt;10&lt;/code&gt;.
The same logic applies to C++ iterators.
If we want to iterate over all elements of a vector, we&amp;#8217;d use:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for (auto it = v.begin(); it != v.end(); it++) { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, we use &lt;code&gt;!=&lt;/code&gt; instead of &lt;code&gt;&amp;#60;&lt;/code&gt; for iterators, but it does practically the same thing.
When the iterator goes past the end of the vector, it&amp;#8217;ll hit &lt;code&gt;v.end()&lt;/code&gt; (which is one past the last element),
and as such the loop stops.&lt;/p&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Usually, you&amp;#8217;d do &lt;code&gt;for (auto number : v)&lt;/code&gt; to iterate over the entire array.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;So, having the upper bound be right past the end of the interval makes this possible:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for (auto it = lb; it != ub; it++) {
    &amp;#47;&amp;#47; *it is like pointer dereference:
    &amp;#47;&amp;#47; it gets the number pointed to by the iterator
    std::cout &amp;#60;&amp;#60; *it &amp;#60;&amp;#60; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Anyways, I&amp;#8217;ll repeat it again: &lt;code&gt;lower_bound&lt;/code&gt; and &lt;code&gt;upper_bound&lt;/code&gt; represent the &lt;em&gt;interval&lt;/em&gt; that matches what you&amp;#8217;re looking for.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;So, that is my &amp;#8220;visual&amp;#8221; explanation how lower and upper bound works in C++.
In hindsight, this seems obvious, but back when I was first told about these functions,
I could not understand it because of the confusing descriptions.
Having this intuition for concepts is pretty helpful for truly understanding them:
you don&amp;#8217;t want to be stuck memorizing things that don&amp;#8217;t make sense.&lt;/p&gt;</content>
		<link href="https://www.dogeystamp.com/cpp-binary-search"/>
		<id>https://www.dogeystamp.com/cpp-binary-search</id>
		<updated>2024-04-02T10:00:00Z</updated>
		<published>2024-04-02T10:00:00Z</published>
	</entry>
	<entry>
		<title>Chess engine, pt. 6: Neural-net evaluation</title>
		<content type="html">&lt;h1 id=&quot;chess-engine-pt.-6-neural-net-evaluation&quot;&gt;Chess engine, pt. 6: Neural-net evaluation&lt;/h1&gt;
&lt;div class=&quot;creation-date&quot;&gt;
2025-05-28
&lt;/div&gt;
&lt;link rel=&quot;contents&quot; href=&quot;/chess0&quot; /&gt;
&lt;link rel=&quot;prev&quot; href=&quot;/chess5&quot; /&gt;
&lt;div class=&quot;callout markdown-alert markdown-alert-callout&quot;&gt;
&lt;p&gt;This post is part of a series about building a chess-playing engine.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/chess0&quot;&gt;Introduction (0)&lt;/a&gt;
| &lt;a href=&quot;/chess5&quot;&gt;← Last part (5)&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;The engine we built in this blog post series is now capable of playing chess,
and it understands the game decently on a tactical basis.
However, a common comment from human players was that the engine plays &amp;#8220;weird&amp;#8221;.
This unnatural-ness is a symptom of the engine having no knowledge of chess other than tactics.&lt;/p&gt;
&lt;p&gt;In this post, I&amp;#8217;ll cover some of the main methods, both traditional and modern, used to
give the engine some positional knowledge.
To do this, I will continue building on our engine&amp;#8217;s evaluation function.
Right now, it&amp;#8217;s based on material counting,
but by the end of this post, it will be replaced by a neural network.&lt;/p&gt;
&lt;p&gt;First, though, a disclaimer.
The purpose of this post is slightly different from the rest of the series,
because I am not an expert on chess neural networks,
or machine learning in general.
The point of this post is not to describe best practice,
but to show how to build a neural network, from the perspective of a beginner in ML.
If I can do it, you can too!&lt;/p&gt;
&lt;h2 id=&quot;material-counting&quot;&gt;Material counting&lt;/h2&gt;
&lt;p&gt;Let&amp;#8217;s revisit our engine&amp;#8217;s current evaluation function.
Primarily, it is based on material counting;
that is, each piece is assigned a point value, like a pawn is 1 point (or 100 centipawns),
and the queen is 9 points.
Then, whichever player (White or Black) has more points has more advantage.&lt;/p&gt;
&lt;p&gt;The main reason material counting is useful is that it&amp;#8217;s really effective at all levels of chess,
and is low effort to implement.
As I mentioned before, even the best chess players in the world would struggle if they had a huge material disadvantage.&lt;/p&gt;
&lt;p&gt;However, there is of course more to chess than counting material.
Take for example this opening, the Bongcloud Attack:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess6/bongcloud.gif&quot; alt=&quot;A chess position diagram. From the start position, e4, e5, Ke2.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If we just counted material, for all we know,
this position is perfectly equal for both sides.
In reality, the Bongcloud is so bad for White that it has become a meme.
Why is it bad? &lt;a href=&quot;https://en.m.wikibooks.org/wiki/Chess_Opening_Theory/1._e4/1...e5/2._Ke2&quot;&gt;Wikibooks&lt;/a&gt; says that this opening &amp;#8220;prevents castling to protect the King, endangers the King, ignores development and the center, and blocks the Queen and Bishop&amp;#8221;.&lt;/p&gt;
&lt;p&gt;Our engine can&amp;#8217;t understand any of that,
because it only sees that a side &lt;em&gt;has&lt;/em&gt; a given piece,
and it doesn&amp;#8217;t know &lt;em&gt;where&lt;/em&gt; that piece is.&lt;/p&gt;
&lt;h2 id=&quot;piece-square-tables&quot;&gt;Piece-square tables&lt;/h2&gt;
&lt;p&gt;To make the engine understand basic positional aspects of chess,
we can use &lt;strong&gt;piece-square tables&lt;/strong&gt;.
These encode a score not just for having a piece, like &amp;#8220;&lt;em&gt;pawn&lt;/em&gt; is 100 centipawns&amp;#8221;,
but also their position, like &amp;#8220;&lt;em&gt;pawn on a6&lt;/em&gt; is 180 centipawns&amp;#8221;.
This score includes the inherent material value of the piece (1 point),
and a bonus for its advanced position on the board (0.8 points).&lt;/p&gt;
&lt;p&gt;Piece-square tables can encode a lot of conventional wisdom about chess.
For example, &amp;#8220;knights on the rim are dim&amp;#8221;,
that is, knights on the edge of the board,
and especially the corners,
are less valuable because their mobility is limited.
In practice, the piece square table is an array of numbers that represent the score for each square a knight could be on.
For &amp;#8220;knights on the rim are dim&amp;#8221;, this could be the PST you use:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;pst_knight_white = [
    -70, -20, -20, -20, -20, -20, -20, -70,
    -20,   0,   0,   0,   0,   0,   0, -20,
    -20,   0,   0,   0,   0,   0,   0, -20,
    -20,   0,   0,   0,   0,   0,   0, -20,
    -20,   0,   0,   0,   0,   0,   0, -20,
    -20,   0,   0,   0,   0,   0,   0, -20,
    -20,   0,   0,   0,   0,   0,   0, -20,
    -70, -20, -20, -20, -20, -20, -20, -70,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each number represents a square, so the PST assigns scores for the entire chessboard.
This PST specifically would penalize the knights for being on the edge, and doubly so for being in the corners.&lt;/p&gt;
&lt;p&gt;The array is one-dimensional, because engines typically don&amp;#8217;t use row&amp;#47;column notation (a1 is row 0, column 0);
engines typically use a single number for every square, so a1 is 0, and h8 is 63.
(Here is a &lt;a href=&quot;https://pages.cs.wisc.edu/~psilord/blog/data/chess-pages/rep.html#:~:text=typedef%20uint64_t%20Bitboard%3B-,Chess%20Board%20Mapping,-So%20how%20do&quot;&gt;neat visualization&lt;/a&gt;
of which numbers correspond to which squares.)&lt;/p&gt;
&lt;p&gt;Developers will often bake the material values of the pieces into the table.
In pseudo-Python, it could look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;pst_knight_white = [score + 300 for score in pst_knight_white]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In other words, we can directly add the knight&amp;#8217;s 3 point material value into the piece-square table,
instead of it being a separate score we have to add.&lt;/p&gt;
&lt;p&gt;Also, piece-square tables are usually given from White&amp;#8217;s view of the board.
To get Black&amp;#8217;s scores for each square, engines commonly do a vertical flip of White&amp;#8217;s scores.
Using the standard square numbering convention, if a certain square is &lt;code&gt;x&lt;/code&gt;, the vertically
mirrored equivalent square is &lt;code&gt;x ^ 56&lt;/code&gt; (the square&amp;#8217;s index bitwise XOR 56).
Then to generate Black&amp;#8217;s piece-square table, we can use this code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;pst_knight_black = [pst_knight_white[i ^ 56] for i in range(64)]
&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; In this post specifically, &lt;em&gt;scores will be from White&amp;#8217;s perspective&lt;/em&gt;,
so positive is good for White, and negative is good for Black.
For negamax, if we want Black&amp;#8217;s perspective, we can negate the score.
Typically, NNUE engines do not operate this way; they use perspective networks.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;To keep with the sign convention, whenever we evaluate a position with the PST, we add
scores for White, and subtract scores for Black.&lt;/p&gt;
&lt;p&gt;So far, this is just the knight PST; you should also make a table for all the other pieces.
For instance, pawns that are close to the end of the board are very valuable because they can promote.&lt;/p&gt;
&lt;p&gt;Then, here is some pseudo-code for how your evaluation function could look like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def eval(position):
    score = 0
    for square in range(64):
        piece = position.get_piece_on(square)
        if piece.color == white:
            score += get_piece_square_value(piece, square)
        else:
            score -= get_piece_square_value(piece, square)
    return score
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once you have piece-square tables in your engine, your evaluation function can become quite smart,
depending on what heuristics you can encode in it.&lt;/p&gt;
&lt;h3 id=&quot;tapered-evaluation&quot;&gt;Tapered evaluation&lt;/h3&gt;
&lt;p&gt;The game of chess is often divided into the opening, midgame and endgame phases.
In the midgame and endgame phases, the PST is useful for guiding the engine&amp;#8217;s moves.
However, the best strategy for the midgame is probably not the best strategy for the endgame.
Therefore, engines often have two piece-square tables, one for the midgame and one for the endgame.&lt;/p&gt;
&lt;p&gt;As I mentioned in the last post,
chess engines commonly determine whether it is the midgame or endgame
based on how many pieces are still left on the board.
Often, there will be a &amp;#8220;phase&amp;#8221; number that you calculate that will determine if you are in midgame or endgame.
Based on this number, the engine will interpolate between using the midgame and endgame piece-square tables.&lt;/p&gt;
&lt;p&gt;Interpolating between the tables is important (so I&amp;#8217;ve read).
If you use a fixed threshold and switch abruptly between the tables,
the engine might avoid a good capture because it has a better score with the midgame table than the endgame table.
To smooth out the difference, you &amp;#8220;taper&amp;#8221; the evaluation between midgame and endgame.&lt;/p&gt;
&lt;h3 id=&quot;summary&quot;&gt;Summary&lt;/h3&gt;
&lt;p&gt;To briefly conclude this section, piece-square tables
help your engine understand the value of &lt;em&gt;where&lt;/em&gt; the pieces are on the board.
To do this, each piece-square combination is assigned a bonus or penalty.&lt;/p&gt;
&lt;p&gt;PSTs can be quite effective as an evaluation function.
As of writing, the &lt;a href=&quot;https://rofchade.nl/?p=307&quot;&gt;PeSTO engine&lt;/a&gt;, an engine that operates with &lt;a href=&quot;https://www.chessprogramming.org/PeSTO%27s_Evaluation_Function&quot;&gt;piece-square tables&lt;/a&gt; only,
is &lt;a href=&quot;http://computerchess.org.uk/ccrl/404/cgi/engine_details.cgi?print=Details&amp;amp;each_game=1&amp;amp;eng=PeSTO%202.210%2064-bit%204CPU#PeSTO_2_210_64-bit_4CPU&quot;&gt;rated 3125 Elo&lt;/a&gt; on CCRL Blitz (for comparison, Stockfish is 3818).
PeSTO&amp;#8217;s evaluation function is the product of lots of effort and fine-tuning,
but it goes to show that PSTs are pretty powerful, despite their relative simplicity.&lt;/p&gt;
&lt;h2 id=&quot;neural-network-evaluation&quot;&gt;Neural network evaluation&lt;/h2&gt;
&lt;p&gt;One of the main reasons I did this chess engine project was to get experience with training and running neural networks.&lt;/p&gt;
&lt;p&gt;I saw DeepMind&amp;#8217;s work with &lt;a href=&quot;https://en.m.wikipedia.org/wiki/AlphaZero&quot;&gt;AlphaZero&lt;/a&gt;,
a neural-network based chess engine with &lt;em&gt;zero&lt;/em&gt; knowledge of chess that learned to play at a superhuman level within a few hours.
The idea behind AlphaZero is quite elegant;
by having the network play games against itself,
then training from these games,
it managed to learn chess with no external help or initial knowledge of chess except for its basic rules.&lt;/p&gt;
&lt;p&gt;Before machine learning became commonplace,
hand-crafted evaluation (HCE) techniques like piece-square tables were the norm
in chess engines.
At the time, engine developers had to be good enough at chess to codify the winning strategies of the game.&lt;/p&gt;
&lt;p&gt;My idea was, if I can&amp;#8217;t play half-decent chess, maybe my computer could learn
about chess by itself like AlphaZero did.
(I can say in hindsight that my engine was definitely far from AlphaZero&amp;#8217;s success,
but it did succeed at learning chess at a higher level than my own understanding of it.)&lt;/p&gt;
&lt;p&gt;Now, since this was my first significant neural network project,
I made a lot of mistakes doing it.
Generally, the advice I give here is hopefully decent,
but in the real project my methods were flawed.
The purpose of this part is to document what I did and what I did wrong,
but also to show you that getting into chess neural nets isn&amp;#8217;t super difficult.
Chess-inator is really bad relative to the best engines out there,
but it&amp;#8217;s much better than anything I thought I could ever make on my own.&lt;/p&gt;
&lt;p&gt;Also, before I continue, you should probably have some knowledge of neural networks,
like what is a neuron and
activation function,
or how the typical neural network&amp;#8217;s forward pass works.&lt;/p&gt;
&lt;h3 id=&quot;nnue&quot;&gt;NNUE&lt;/h3&gt;
&lt;p&gt;Neural networks and machine learning can be a bit scary,
because they commonly need lots of costly hardware to run.
AlphaZero was trained using &lt;a href=&quot;https://en.wikipedia.org/w/index.php?title=AlphaZero&amp;amp;oldid=1289278179#:~:text=self%2Dplay%20using-,5%2C000%20first%2Dgeneration%20TPUs,-to%20generate%20the&quot;&gt;thousands of TPUs&lt;/a&gt; specialized for neural nets,
and many other ML projects require GPUs at inference time.&lt;/p&gt;
&lt;p&gt;With many modern chess engines, though, you don&amp;#8217;t need a GPU to run the neural network anymore.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NNUE&lt;/strong&gt;, or Efficiently Updatable Neural Network,
is a neural network design that is specifically CPU-friendly.
Unlike deep learning, where there are many layers of neurons,
I like to call NNUE &amp;#8220;shallow learning&amp;#8221;, because it often uses a very small amount of layers.
Because of this small layer count, and the nature of games like chess,
this type of network allows for extreme speed optimizations.
Other neural networks may be more intelligent,
but NNUE makes up for it in speed.&lt;/p&gt;
&lt;p&gt;The goal of NNUE is to replace the evaluation function in our engine
(which might be material counting, or piece-square table, or any other HCE logic).
It takes in a chess position, and gives us a score for it.&lt;/p&gt;
&lt;p&gt;Before I get into the details,
I want to say that at a beginner level, you can definitely wrap your head around NNUE.
The folks making Stockfish and other advanced engines have really complicated neural networks,
which makes it seem like NNUE is hard to implement.
However, basic NNUE designs are actually quite simple,
but also very effective.&lt;/p&gt;
&lt;h3 id=&quot;pst-again&quot;&gt;PST, again&lt;/h3&gt;
&lt;p&gt;To ease you into the idea of NNUE, let&amp;#8217;s take the piece-square tables we had earlier,
and turn them into a neural network.&lt;/p&gt;
&lt;p&gt;Recall that in the piece-square table, each combination of &lt;em&gt;piece&lt;/em&gt; and &lt;em&gt;square&lt;/em&gt;
is assigned a score.
The &lt;em&gt;color&lt;/em&gt; of the piece is also important for the score&amp;#8217;s sign.
For instance, a White Pawn on a1 could be worth 100 centipawns,
while a Black Rook on h8 would be worth -500 centipawns.&lt;/p&gt;
&lt;p&gt;There are 6 pieces (rook, knight, bishop, queen, pawn, king), 2 colors (White, Black), and 64 squares on the chessboard.
These &lt;strong&gt;features&lt;/strong&gt; make up 768 inputs to the piece-square table.
Based on these inputs, the table returns a single value, the score of the position.&lt;/p&gt;
&lt;p&gt;We can therefore represent a piece-square table as a neural network with no activation functions,
and no biases:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess6/neural_net.svg&quot; alt=&quot;A diagram of a neural network. There is one input layer, connected to one output node.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Each input is either a 1 or a 0.
For instance, if the first input is 1, that means &amp;#8220;there is a White pawn on a1&amp;#8221;.
Otherwise, &amp;#8220;there is not a White pawn on a1&amp;#8221;.&lt;sup id=&quot;fnref1&quot;&gt;&lt;a href=&quot;#fn1&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;These inputs almost fully represent a chess position.
Notably, castling rights and en-passant aren&amp;#8217;t accounted for.
However, for our purposes, the evaluation function doesn&amp;#8217;t have to be perfect,
and our engine&amp;#8217;s search will take care of those elements.
(I like to think that the evaluation function is the intuition of the chess engine,
while search is the logical brain,
so that evaluation doesn&amp;#8217;t have to be 100% accurate in its calculations.)&lt;/p&gt;
&lt;p&gt;We then multiply each input by a weight. (In the example diagram above, I picked some arbitrary numbers like 100 and -500,
which roughly correspond to the material values of the White pawn and Black rook, in centipawns.)
Then, we take the sum of the products, and the resulting output is the score of the position in centipawns.
The score is from White&amp;#8217;s perspective; a positive score is good for White, and a negative score is good for Black.&lt;/p&gt;
&lt;p&gt;In other words, we add the values of all White pieces, and subtract the values of all Black pieces.
But now, instead of explicitly doing that calculation, we use a neural network to do it.&lt;/p&gt;
&lt;p&gt;Here is some pseudo-code for evaluating the neural network&amp;#8217;s output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;WEIGHTS = [100, 100, 100, ..., -500, -550, -500]

def forward_pass(inputs):
    output = 0

    for i in range(768):
        output += WEIGHTS[i] * inputs[i]

    return output
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But since the inputs are binary zeroes and ones, this code is equivalent:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def forward_pass(inputs):
    output = 0

    for i in range(768):
        if inputs[i] == 1:
            output += WEIGHTS[i]

    return output
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, we have a &amp;#8220;neural network&amp;#8221; that implements a piece-square table.&lt;/p&gt;
&lt;h3 id=&quot;incremental-update&quot;&gt;Incremental update&lt;/h3&gt;
&lt;p&gt;In this section, I&amp;#8217;m going to continue using the simple PST neural network in the diagram above
to explain the core idea behind NNUE.
Remember that NNUE stands for &lt;em&gt;efficiently updatable&lt;/em&gt; neural network.
What does that mean?&lt;/p&gt;
&lt;p&gt;In the evaluation function code I showed above,
there is a for loop that iterates over all 768 inputs.
In the PST, this isn&amp;#8217;t that bad; for computers, a few hundred operations is nothing in terms of time.
However, for more complicated neural networks, we start getting nested for loops,
and in that case doing the same thing 768 times becomes computationally expensive.&lt;/p&gt;
&lt;p&gt;To avoid this all looping and repetition, engines use &lt;strong&gt;incremental updates&lt;/strong&gt;.
The engine always keeps track of the score output of the current position.
Whenever we make a move on the position,
we only need to calculate the difference in score between the old and the new position.
Since individual moves don&amp;#8217;t change the position that much,
this calculation is really simple.&lt;/p&gt;
&lt;p&gt;As an example, suppose our engine is using material counting,
and we are trying to calculate the score after exd5.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess6/scandi.svg&quot; alt=&quot;Two chess position diagrams. The first is after 1. e4 d5, and the second is after the move 2. exd5.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If I tell you that the material score of the first position is 0, what is the material score of the second position?
It&amp;#8217;s a 1 point advantage for White, because
the only difference between the two positions is that Black&amp;#8217;s pawn was removed.
With material counting, when a piece is removed, its score is removed from the evaluation.
When a piece is added, its score is added.&lt;/p&gt;
&lt;p&gt;If we apply this same idea to the PST neural network above, whenever an input bit is changed to a 1,
we add its weight, and whenever a bit is changed to 0, we subtract its weight from the output.&lt;/p&gt;
&lt;p&gt;For instance, if we move the Black rook from h8 to h7 (assuming there is no piece on h7), we turn on &lt;code&gt;Rook, Black, h7&lt;/code&gt; (bit 766),
and we turn off &lt;code&gt;Rook, Black, h8&lt;/code&gt; (bit 767):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# this is the score before we move the rook.
original_score = forward_pass(inputs)

# this is the score after moving the rook.
new_score = original_score + WEIGHTS[766] - WEIGHTS[767]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess6/neural_net2.svg&quot; alt=&quot;
The same neural network diagram as above, but the penultimate bit (Rook, Black, h7) is highlighted green, and the last bit (Rook, Black, h8) is highlighted red.
Near the output node, there is a green &amp;quot;+(-550)&amp;quot;, and a red &amp;quot;-(-500)&amp;quot;, and a black &amp;quot;difference: -50&amp;quot;.
&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Similar incremental updates can be done for captures too;
a capture turns two bits off, and one bit on.&lt;/p&gt;
&lt;p&gt;We also need to implement a way to &amp;#8220;undo&amp;#8221; the score, since we can unmake moves.
Whenever we unmake moves, we do the inverse operations to restore the original state.
For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;original_score = ...

# make move (Rh7)
score += WEIGHTS[766]
score -= WEIGHTS[767]

# unmake move (Rh7)
score -= WEIGHTS[766]
score += WEIGHTS[767]

# score is now back to the original state.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now that we have incremental updates,
we can re-evaluate our PST neural network in only a few operations,
rather than re-doing 768 operations every time.&lt;/p&gt;
&lt;p&gt;In the engine&amp;#8217;s function for applying a move to a position,
and undoing a move,
you can add calls to the incremental update functions.
Then, whenever you make moves on the board,
the evaluation updates accordingly;
whenever you unmake&amp;#47;undo moves, the evaluation returns to its original state.&lt;/p&gt;
&lt;h3 id=&quot;nnue-one-hidden-layer&quot;&gt;NNUE, one hidden layer&lt;/h3&gt;
&lt;p&gt;Now that we&amp;#8217;ve covered a contrived piece-square table neural network,
I introduce a basic NNUE design:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess6/neural_net3.svg&quot; alt=&quot;
A neural network diagram.
There is one input layer, one hidden layer with 3 nodes (labelled &amp;quot;n = 3&amp;quot;), and one output node.
Layers are fully connected.
&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This neural network takes the same chessboard input as before,
and outputs a score for it.
However, the network is now more complex.&lt;/p&gt;
&lt;p&gt;Notably, we introduce a hidden layer of neurons.
There are also biases on the hidden layer nodes and the output node.
I chose to have 3 hidden layer nodes in this example, but there&amp;#8217;s usually more than that (i.e. dozens to a few thousand).&lt;/p&gt;
&lt;p&gt;In the hidden layer, I&amp;#8217;m using the &lt;strong&gt;clipped ReLU&lt;/strong&gt; (sometimes called CReLU) activation function.
The original ReLU (rectified linear unit) is a common activation function defined as &lt;code&gt;ReLU(x) = max(0, x)&lt;/code&gt;.
Clipped ReLU is similar, but it completely clamps the output between 0 and 1.
We can define the function as &lt;code&gt;CReLU(x) = max(0, min(x, 1))&lt;/code&gt;, or equivalently, &lt;code&gt;CReLU(x) = clamp(x, 0, 1)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here is the graph for CReLU (&lt;a href=&quot;https://www.desmos.com/calculator/lyxowmvi3r&quot;&gt;Desmos&lt;/a&gt;):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess6/crelu_graph.svg&quot; alt=&quot;The graph for the clipped ReLU function&quot; /&gt;&lt;/p&gt;
&lt;p&gt;CReLU is (from what I&amp;#8217;ve read) not an ideal choice,
but it&amp;#8217;s simple and easy for beginners to deal with.
The hidden layer&amp;#8217;s activation function will be very often used compared to the output node&amp;#8217;s activation function,
so we ideally pick a function can be optimized for speed.
CReLU&amp;#8217;s limited range is good for optimization (we&amp;#8217;ll see later), which is why it is preferred over ReLU.&lt;/p&gt;
&lt;p&gt;One major difference between this neural network and our PST is that the output is no longer a score in centipawns (known as &lt;strong&gt;CP-space&lt;/strong&gt;).
Now, we express the position&amp;#8217;s score as a win probability in &lt;strong&gt;WDL-space&lt;/strong&gt;:
a 0.0 is a loss, a 0.5 is a draw, and a 1.0 is a win.
When I talk about training the network in the next section, you&amp;#8217;ll see that using WDL is more natural than centipawn scores.
Centipawns are the traditional unit for scores, though, so we need to find a way to convert between CP-space and WDL-space.
Engine developers have figured out that a &lt;a href=&quot;https://en.m.wikipedia.org/wiki/Sigmoid_function&quot;&gt;sigmoid function&lt;/a&gt; is pretty good for that task.
Sigmoid functions look like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess6/sigmoid.svg&quot; alt=&quot;The equation for a sigmoid function. sigma(x) = 1/(1 + e^(-x/k))&quot; /&gt;&lt;/p&gt;
&lt;p&gt;where k is a scaling constant.
For our purposes of converting between centipawns and WDL, a value of &lt;code&gt;k = 400&lt;/code&gt; is commonly used to scale the sigmoid properly.
Here&amp;#8217;s a graph (&lt;a href=&quot;https://www.desmos.com/calculator/xugqjb6vbo&quot;&gt;Desmos&lt;/a&gt;):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess6/sigmoid_graph.svg&quot; alt=&quot;The graph for the sigmoid function&quot; /&gt;&lt;/p&gt;
&lt;p&gt;On the X-axis, we have centipawns, and on the Y-axis, we have win probability.
The output of a sigmoid function is between zero and one,
and its domain is all real numbers.
This makes it ideal for our purposes of creating a WDL score.
You can see that for negative centipawn scores, the game is probably a loss (near 0),
and for high centipawn scores, the game is probably a win (near 1).&lt;/p&gt;
&lt;p&gt;Since we want the neural network to provide us with a WDL-space score,
the output neuron&amp;#8217;s activation function is also a sigmoid function.
However, we use a &lt;code&gt;k = 1&lt;/code&gt; curve, not the &lt;code&gt;k = 400&lt;/code&gt; centipawn-WDL curve.
I&amp;#8217;ll explain why we do this later.&lt;/p&gt;
&lt;h3 id=&quot;training-process&quot;&gt;Training process&lt;/h3&gt;
&lt;p&gt;For now, let&amp;#8217;s focus on the way the network is trained.
The NNUE is supposed to take a board position, and return an accurate WDL score.
To accomplish this task, we need to figure out good values for all the weights and biases.
&lt;a href=&quot;https://youtube.com/watch?v=IHZwWFHWa-w&quot;&gt;Gradient descent&lt;/a&gt; is the tool used to do this.&lt;/p&gt;
&lt;p&gt;By providing examples pairs composed of a chess position (input) and a correct evaluation of the position (output),
gradient descent works backwards to tune the weights so that the network is as accurate as possible.&lt;/p&gt;
&lt;p&gt;The problem is, how do we get these examples?
One supervised learning approach would be to have Stockfish evaluate a bunch of positions,
and use those position-score pairs as examples.
That&amp;#8217;s cheating, though; in a way, we&amp;#8217;re stealing Stockfish&amp;#8217;s existing knowledge of chess.&lt;/p&gt;
&lt;p&gt;Instead, the more fun solution is to generate training data ourselves.
We have the engine play against itself for lots of games, and then extract all the positions from these games.
We also track the engine&amp;#8217;s evaluation of the positions, and the result of the game.&lt;/p&gt;
&lt;p&gt;Then, for every position, the &amp;#8220;correct&amp;#8221; evaluation is a &lt;a href=&quot;https://mattdesl.svbtle.com/linear-interpolation&quot;&gt;linear interpolation&lt;/a&gt; between
the engine&amp;#8217;s evaluation, and the real final result (win, draw, loss) of the game.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First, the engine&amp;#8217;s centipawn evaluation for the position is converted to WDL space using the sigmoid curve (&lt;code&gt;k = 400&lt;/code&gt;) we saw earlier.&lt;/li&gt;
&lt;li&gt;Then, we convert the game&amp;#8217;s result into WDL, either 1.0, 0.5 or 0.0.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now, we have two scores for the position, both in WDL space (i.e. between 0 and 1), that we mix together.
This way, the engine learns from the outcomes of its games.
Eventually, with enough positions, the neural network will learn what wins and what loses in positions.&lt;/p&gt;
&lt;p&gt;The positions in the training dataset &lt;em&gt;must&lt;/em&gt; be quiescent positions.
That is, there must not be any pending captures or imminent checkmates.
Remember how material counting &lt;a href=&quot;/chess5/index.html#horizon-effect&quot;&gt;breaks in non-quiescent positions&lt;/a&gt;?
If we train the neural net on non-quiescent positions, it will
also get confused.&lt;sup id=&quot;fnref2&quot;&gt;&lt;a href=&quot;#fn2&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Typically, NNUEs are trained on the order of
&lt;a href=&quot;https://github.com/fairy-stockfish/variant-nnue-pytorch/wiki/Training-data-generation#:~:text=at%20least%20100M%20positions&quot;&gt;hundreds of millions of positions&lt;/a&gt;.
According to an Engine Programming Discord member kind enough to answer me, the training games
can be run with a &amp;#8220;5000 node soft limit&amp;#8221; on the search, for example.
I assume this means that after an iterative deepening iteration, if the count of game tree nodes visited in negamax
exceeds 5000, break the loop.
Also, I assume this node limit helps generate more positions in a small amount of time.&lt;/p&gt;
&lt;p&gt;Since training operates on a huge amount of chess position data,
it is essential that you encode it in an efficient way.
FEN, or worse, just storing the input features as a boolean array,
are horrible formats;
instead, you should use binary packed formats.
In the worst case scenario, this is the difference between your training requiring 128GB of RAM,
or 3GB of RAM to run.
See this
&lt;a href=&quot;https://lichess.org/@/revoof/blog/adapting-nnue-pytorchs-binary-position-format-for-lichess/cpeeAMeY&quot;&gt;blog post&lt;/a&gt;
from a Lichess developer about Stockfish&amp;#8217;s NNUE position format.
On average, their format requires 18.7 bytes per position.&lt;/p&gt;
&lt;div id=&quot;backref-having-no-training-data&quot;&gt;&lt;/div&gt;
&lt;p&gt;At the time I wrote the engine, I had no idea of how any of this worked,
so I actually trained chess-inator on only a few million positions,
which is &amp;#8220;&lt;em&gt;drastically&lt;/em&gt; little&amp;#8221; for the network I was using.
I believe, as a consequence of this lack of training data,
my NNUE was severely restricted in its performance.
Also, I was limited by my terrible position encoding which consumed a large amount of RAM.
&lt;a href=&quot;#appendix-having-no-training-data&quot;&gt;At the end of this article,&lt;/a&gt; I&amp;#8217;ll talk about these issues more and
mention some of the things I did to cope with these problems.&lt;/p&gt;
&lt;h3 id=&quot;an-unoptimized-implementation&quot;&gt;An unoptimized implementation&lt;/h3&gt;
&lt;p&gt;Now, here&amp;#8217;s the forward pass of the neural net in pseudo-code,
and I hope it answers any of your questions about how it works:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def crelu(x):
    return clamp(x, 0, 1)

def sigmoid(x):
    # k = 1
    return 1 &amp;#47; (1 + exp(-x))

# hidden layer size
L1_SIZE = 3
# number of input bits
INPUT_SIZE = 768

WEIGHTS_L1 = [
    [...3 weights],
    [...3 weights],
    [...3 weights],
    ... 768 times total
]
BIASES_L1 = [...3 biases]

WEIGHTS_OUTPUT = [...3 weights]
BIAS_OUTPUT = 1 bias

def forward_pass(inputs):
    # pre-activation state of the hidden layer
    hidden = [0, 0, 0]

    for j in range(L1_SIZE):
        for i in range(INPUT_SIZE):
            if inputs[i] == 1:
                hidden[j] += WEIGHTS_L1[i][j]
        hidden[j] += BIASES_L1[j]

    # weighted sum of hidden layer&amp;#39;s activations, plus bias
    output_preactivation = 0
    for j in range(L1_SIZE):
        output_preactivation += WEIGHTS_OUTPUT[j] * crelu(hidden[j])
    output_preactivation += BIAS_OUTPUT

    output = sigmoid(output_preactivation)
    return output
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I wrote this forward pass function here so you can understand exactly how the
neural network operates without any optimizations.
Practically, this is exactly how it operates in my Python neural network training code.
In the following sections though, I&amp;#8217;ll be improving this code until it runs fast.
This is necessary in the engine side of things (the Rust side in my engine).&lt;/p&gt;
&lt;h3 id=&quot;sigmoid-at-runtime&quot;&gt;Sigmoid at runtime&lt;/h3&gt;
&lt;p&gt;The neat thing about the sigmoid activation function we used for the output neuron
is that we don&amp;#8217;t need it at runtime (i.e. when the engine is playing chess.)
Because the curve that converts between CP-space and WDL-space is also a sigmoid,
we&amp;#8217;re actually training the neural network to think in terms of centipawns.
Here&amp;#8217;s the &lt;code&gt;k = 400&lt;/code&gt; curve that converts CP-space to WDL-space, but with clearer variable names.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess6/sigmoid2.svg&quot; alt=&quot;The equation for a sigmoid function. WDL = 1/(1 + e^(-CP/k))&quot; /&gt;&lt;/p&gt;
&lt;p&gt;And here is the sigmoid &lt;em&gt;activation function&lt;/em&gt; that we actually use in our neural net:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess6/sigmoid3.svg&quot; alt=&quot;The equation for a sigmoid function. sigma(x) = 1/(1 + e^(-x))&quot; /&gt;&lt;/p&gt;
&lt;p&gt;where &lt;code&gt;x&lt;/code&gt; is the pre-activation of the output neuron, and &lt;code&gt;σ(x)&lt;/code&gt; is the neuron&amp;#8217;s output.
Since we&amp;#8217;re training so that &lt;code&gt;σ(x)&lt;/code&gt; is equal to &lt;code&gt;WDL&lt;/code&gt;, that implies that&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess6/eq.svg&quot; alt=&quot;Math equation. CP/400 = x&quot; /&gt;&lt;/p&gt;
&lt;p&gt;and therefore,&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess6/eq2.svg&quot; alt=&quot;Math equation. CP = 400x&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If your eyes glazed over the moment you saw math equations, here&amp;#8217;s the key point of this section:
 &lt;em&gt;we can multiply the neural net output&amp;#8217;s pre-activation by 400,
and interpret that value as being in centipawns,&lt;/em&gt;
rather than using a WDL output.&lt;/p&gt;
&lt;p&gt;Thus, we can change our evaluation function to be like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;SCALE_K = 400

def forward_pass(inputs):
    for _ in range(L1_SIZE):
        for _ in range(INPUT_SIZE):
            ... # see last section; I removed this code for brevity

    output_preactivation = 0
    for j in range(L1_SIZE):
        output_preactivation += WEIGHTS_OUTPUT[j] * crelu(hidden[j])
    output_preactivation += BIAS_OUTPUT

    # HERE: notice how the sigmoid is missing
    return output_preactivation * SCALE_K
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and instead of returning a weird 0.0 to 1.0 win probability value from the neural network,
the evaluation function will return a centipawn score, just the way we&amp;#8217;re used to.
Also, we avoid computing &lt;code&gt;sigmoid&lt;/code&gt;, which saves time.&lt;/p&gt;
&lt;h3 id=&quot;incremental-update-for-nnue&quot;&gt;Incremental update for NNUE&lt;/h3&gt;
&lt;p&gt;Notice that the forward pass has an expensive nested for loop,
which does &lt;code&gt;768 * L1_SIZE&lt;/code&gt; iterations.
That&amp;#8217;s bad; with a sufficiently big hidden layer, this could become a million iterations.&lt;/p&gt;
&lt;p&gt;The key observation that defines NNUE is that we can also apply incremental updates to this neural network,
just like I demonstrated earlier with the PST.
Each time we need an evaluation, we don&amp;#8217;t have to re-compute the entire forward pass;
we only need to calculate the difference caused by a few input bits changing.&lt;/p&gt;
&lt;p&gt;Here is code that turns on or turns off bits in the input layer,
then incrementally updates the NNUE evaluation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# hidden layer size
L1_SIZE = 3
# number of input bits
INPUT_SIZE = 768

# sigmoid parameter
SCALE_K = 400

WEIGHTS_L1 = [
    [...3 weights],
    [...3 weights],
    [...3 weights],
    ... 768 times total
]
BIASES_L1 = [...3 biases]

WEIGHTS_OUTPUT = [...3 weights]
BIAS_OUTPUT = 1 bias


# current hidden layer state (pre-activations)
hidden = [...3 elements]

def bit_set(i, on):
    &quot;&quot;&quot;Set bit `i` on or off.&quot;&quot;&quot;
    if on:
        inputs[i] = 1
        for j in range(L1_SIZE):
            hidden[j] += WEIGHTS_L1[i][j]
    else:
        inputs[i] = 0
        for j in range(L1_SIZE):
            hidden[j] -= WEIGHTS_L1[i][j]

def evaluation():
    &quot;&quot;&quot;Get NNUE output (centipawns).&quot;&quot;&quot;
    output = 0
    for j in range(L1_SIZE):
        output += WEIGHTS_OUTPUT[j] * crelu(hidden[j])
    output += BIAS_OUTPUT

    output *= SCALE_K
    return output

# example: move rook from h8 to h7
bit_set(767, False)
bit_set(766, True)
score = evaluation()

# then unmake (undo) the above move
bit_set(767, True)
bit_set(766, False)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each bit-set operation now only requires &lt;code&gt;L1_SIZE&lt;/code&gt; operations to update the hidden layer pre-activations.
Then, once we want the NNUE&amp;#8217;s output, we use another &lt;code&gt;L1_SIZE&lt;/code&gt; operations to run the hidden layer&amp;#8217;s activation functions and sum them to get a final score.
This is much better than the earlier &lt;code&gt;768 * L1_SIZE&lt;/code&gt; number of operations.&lt;/p&gt;
&lt;p&gt;Commonly, the term &lt;strong&gt;accumulator&lt;/strong&gt; is used to describe the hidden layer pre-activations (&lt;code&gt;hidden&lt;/code&gt; in the above code).
The accumulator is the only state that you maintain for the NNUE;
the output (and any further layers there might be) are computed from the accumulator state.
Using incremental updates, it&amp;#8217;s highly efficient to update the accumulator,
so adding more hidden layer nodes is cheap.
The accumulator is the &lt;em&gt;efficiently updatable&lt;/em&gt; part of NNUE.
If we had more layers after the accumulator (some engines do), these
would not be efficiently updatable, and would have to be entirely recomputed every
time.&lt;/p&gt;
&lt;h3 id=&quot;simd-assembly-level-optimization&quot;&gt;SIMD, assembly-level optimization&lt;/h3&gt;
&lt;p&gt;I want to stress the importance of a few implementation details in &lt;code&gt;bit_set&lt;/code&gt; above.
First of all, &lt;code&gt;WEIGHTS_L1&lt;/code&gt; is indexed first by the input node,
then the hidden layer node.
This makes it so that in the loop,
array elements that are next to each other in memory
are accessed one after the other.
Secondly, in &lt;code&gt;bit_set&lt;/code&gt;, I use &lt;code&gt;if&lt;/code&gt; outside, and the &lt;code&gt;for&lt;/code&gt; inside, even if this causes a bit of code duplication.
This is the commonly advised way to write the loop,
since this way we avoid repeatedly testing a condition that won&amp;#8217;t change between iterations.&lt;/p&gt;
&lt;p&gt;In my original code, I implemented both of these things the wrong way.
After a long time fiddling in &lt;a href=&quot;https://godbolt.org/&quot;&gt;Godbolt&amp;#8217;s Compiler Explorer&lt;/a&gt; and looking at the generated assembly,
I figured out what was wrong, fixed the code and the compiler suddenly started generating &lt;a href=&quot;https://en.m.wikipedia.org/wiki/Single_instruction,_multiple_data&quot;&gt;SIMD&lt;/a&gt;
instructions.
&lt;strong&gt;SIMD&lt;/strong&gt; (single instruction, multiple data), or &lt;strong&gt;vectorized&lt;/strong&gt; code
means that instead of adding the weights one at a time to the accumulator,
it would add multiple elements at a time, speeding up the process.&lt;/p&gt;
&lt;p&gt;The new vectorized code was amazingly snappy; I was actually overjoyed seeing
the engine evaluate positions multiple times faster.
This was the first project where I actually had to go read the compiled assembly
to deeply optimize my program.
Usually, in competitive programming (and a lot of other domains),
we only think about asymptotic time complexity.
For my chess engine, I had to optimize the constant factor,
which I had ignored all these years.&lt;/p&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; In my engine, since its logic is simple, the compiler (rustc, powered by LLVM) managed to auto-vectorize the code.
I often see that other engine developers manually vectorize their code, which is too complex to auto-vectorize.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 id=&quot;quantization&quot;&gt;Quantization&lt;/h3&gt;
&lt;p&gt;Other than heavy use of SIMD, the other major optimization in NNUE is quantization.
Floating point numbers &lt;a href=&quot;https://youtube.com/watch?v=5TFDG-y-EHs&quot;&gt;are&lt;/a&gt; &lt;a href=&quot;https://en.m.wikipedia.org/wiki/Fast_inverse_square_root#:~:text=evil%20floating%20point%20bit%20level%20hacking&quot;&gt;evil&lt;/a&gt;  (*personal opinion),
so engine developers prefer to use integers rather than floating point numbers to represent the NNUE&amp;#8217;s accumulator state.
One reason is that integers are faster than floating point.&lt;/p&gt;
&lt;p&gt;The other reason to use integers is to avoid floating point error.
Infamously, &lt;code&gt;0.1 + 0.2 = 0.30000000000000004&lt;/code&gt;.
In a typical neural network, this infinitesimal error could be acceptable.
However, remember how our chess engine works.
It tries a move, evaluates, undoes the move.
Tries another move, evaluates, undoes.
Each time the engine makes or unmakes a move,
it is also incrementally updating the evaluation.
The engine relies on the undo operation to get back to the original, clean, NNUE accumulator state.
With floating point, the undo will return to a slightly different &amp;#8220;original&amp;#8221; state than before.
Eventually, because of errors accumulating, repeated undos will return to an accumulator state that is completely different from the original.&lt;/p&gt;
&lt;p&gt;Here&amp;#8217;s an analogy:
imagine you&amp;#8217;re using a text editor,
and you type:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Hello World!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then you undo, and get this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Helln
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then you type some other word, like &amp;#8220;Everyone!&amp;#8221;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Helln Everyone!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then you undo, and repeat this over and over again a thousand times, and you end up with this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Hexza
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is of course not the behaviour you want; you would expect the text to still be &lt;code&gt;Hello&lt;/code&gt;.
In this analogy, &lt;code&gt;Hello&lt;/code&gt; represents the accumulator&amp;#8217;s original state,
which, while making and unmaking moves repeatedly,
gets corrupted by floating point errors and becomes &lt;code&gt;Hexza&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; In my experience,
this sort of catastrophic floating point error only happened with
16-bit (half precision) floats,
and is not nearly as bad on &lt;code&gt;f32&lt;/code&gt; (single) and &lt;code&gt;f64&lt;/code&gt; (double) types.
I&amp;#8217;m sure there&amp;#8217;s still some drift of the accumulator state, though.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;This issue &lt;em&gt;can&amp;#8217;t&lt;/em&gt; happen if we used integers to represent the accumulator state,
because in integers, &lt;code&gt;1 + 2 = 3&lt;/code&gt; exactly,
with no error whatsoever.&lt;/p&gt;
&lt;p&gt;Now, the problem is that
neural networks don&amp;#8217;t use integers, they use floating point numbers.
We need to &lt;strong&gt;quantize&lt;/strong&gt; the network&amp;#8217;s parameters, i.e. convert the floating point numbers to integers.
Naively, we could round all the numbers to the nearest integer.
However, this causes inacceptable error.
For example, let&amp;#8217;s say one of the weights in our NNUE is &lt;code&gt;0.51&lt;/code&gt;.
If you round &lt;code&gt;0.51&lt;/code&gt; to &lt;code&gt;1&lt;/code&gt;, that&amp;#8217;s a nearly 96% relative error on the original value.&lt;/p&gt;
&lt;p&gt;To lessen this rounding error, we can scale all the values by some constant.
For example, let&amp;#8217;s multiply the values by 10.
Then we round &lt;code&gt;5.1&lt;/code&gt; to &lt;code&gt;5&lt;/code&gt;, which is much better (~2% error) than before.
Once we&amp;#8217;re done with the calculations, we can divide out the scaling constant to get a real answer.&lt;/p&gt;
&lt;p&gt;Here&amp;#8217;s a simple example with a linear function.
This is our original function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def f(x):
    WEIGHT = 0.33
    BIAS = 0.51
    return WEIGHT * x + BIAS
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we rounded the weight and bias naively, we get this function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def f_round_naive(x):
    WEIGHT = 0
    BIAS = 1
    return WEIGHT * x + BIAS
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and if we apply a scaling constant of 100:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def f_quantized(x):
    WEIGHT = 33
    BIAS = 51

    # `&amp;#47;&amp;#47;` is integer division
    return (WEIGHT * x + BIAS) &amp;#47;&amp;#47; 100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, let&amp;#8217;s compare their outputs for &lt;code&gt;x = 20&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;#62;&amp;#62;&amp;#62; f(20)
7.11

&amp;#62;&amp;#62;&amp;#62; f_round_naive(20)
1

&amp;#62;&amp;#62;&amp;#62; f_quantized(20)
7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can see that the properly quantized function has almost the right answer,
and that the naively rounded function is completely wrong.&lt;/p&gt;
&lt;p&gt;Let&amp;#8217;s apply quantization to our basic NNUE network now.
We can keep our training process completely the same
(i.e., with floating points),
and only quantize the weights after.
That is, once we have a fully trained neural network,
we take its floating-point parameters,
then convert them to integers
before using the network in the engine.&lt;/p&gt;
&lt;div id=&quot;backref-quantization&quot;&gt;&lt;/div&gt;
&lt;p&gt;Before quantizing, we scale the neural net parameters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Multiply the &lt;em&gt;accumulator&amp;#8217;s&lt;/em&gt; weights and biases by &lt;code&gt;SCALE_L1&lt;/code&gt;, which is defined to be &lt;code&gt;255&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Multiply the &lt;em&gt;output&amp;#8217;s weight&lt;/em&gt; by &lt;code&gt;SCALE_OUT&lt;/code&gt;, which is defined to be &lt;code&gt;64&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Multiply the &lt;em&gt;output&amp;#8217;s bias&lt;/em&gt; by &lt;code&gt;SCALE_OUT * SCALE_L1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(I just took the values straight from the &lt;a href=&quot;https://www.chessprogramming.org/NNUE#Basic_NNUE&quot;&gt;Wiki article&lt;/a&gt;, which says that these are commonly used.)
Then, we round the model parameters to the nearest integer.
Once the model parameters have been quantized,
we need to modify the inference code in the actual engine.
This happens in two places:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We re-define CReLU to be &lt;code&gt;clamp(x, 0, SCALE_L1)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;At the end of the neural net forward pass, we divide
by &lt;code&gt;SCALE_L1 * SCALE_OUT&lt;/code&gt;, i.e. &lt;code&gt;255 * 64&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After the division, the result is exactly the same as before scaling (if we exclude rounding error).
For an explanation of why, please see this &lt;a href=&quot;#appendix-crelu-quantization&quot;&gt;appendix&lt;/a&gt; to this post.&lt;/p&gt;
&lt;p&gt;The model now being quantized, everything in the engine code operates with integers,
so the neural network inference is faster, and most importantly, does not have drift caused by floating point error.
The trade-off for this is that the result is less accurate, because rounding the parameters
creates a different kind of error (but this error does not build up over time).&lt;/p&gt;
&lt;p&gt;It is important to carefully consider the data-types you use in your engine code,
because it could make SIMD more effective.
Recall that SIMD means you are executing a &lt;em&gt;single instruction&lt;/em&gt;,
on &lt;em&gt;multiple data&lt;/em&gt;.
The amount of data a single instruction can process is limited to a certain amount of bits;
typically this is something like 256 bits.
That means if your accumulator uses &lt;code&gt;i32&lt;/code&gt; (32-bit signed integer),
you can operate on 8 numbers at once.
However, if you use &lt;code&gt;i16&lt;/code&gt; (16-bit signed integer),
you can operate on 16 numbers at once, which could be faster.&lt;/p&gt;
&lt;p&gt;A caveat: if you pick a data-type that is too small,
there is a risk that you get integer overflows or underflows.
This is why the clipping in CReLU is important; it has a very specific and known output range,
which lets you ensure that the integer limits are respected.
Recall also how the centipawn-WDL sigmoid curve has a parameter &lt;code&gt;K = 400&lt;/code&gt;,
but our output node&amp;#8217;s activation function is a sigmoid with &lt;code&gt;K = 1&lt;/code&gt;.
Our neural network &lt;code&gt;K&lt;/code&gt; parameter is smaller so that the neural network is trained to output relatively small values.
This way, the network&amp;#8217;s weights will be small and won&amp;#8217;t cause overflows.&lt;/p&gt;
&lt;p&gt;For an example of how a good engine works with different data-types in NNUE code,
see &lt;a href=&quot;https://official-stockfish.github.io/docs/nnue-pytorch-wiki/docs/nnue.html#quantization&quot;&gt;Stockfish&amp;#8217;s docs&lt;/a&gt; about quantization.
They use a mix of &lt;code&gt;i8&lt;/code&gt; and &lt;code&gt;i32&lt;/code&gt; depending on the layer of the network,
in order to push the CPU to the maximum.&lt;/p&gt;
&lt;p&gt;For my engine, optimizing every single instruction is not my priority,
so I just picked &lt;code&gt;i16&lt;/code&gt; for the parameters, and &lt;code&gt;i32&lt;/code&gt; for the accumulator.
(Having &lt;code&gt;i32&lt;/code&gt; gives a large margin for avoiding integer overflows.)&lt;/p&gt;
&lt;p&gt;Here&amp;#8217;s an example of pseudo-code for quantized NNUE evaluation.
Note that it only contains the inference code for running CReLU and then computing the result at the output node;
the accumulator nodes are updated with practically the same code as before (but with quantized weights now).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# quantization scales
L1_SCALE = 255
OUTPUT_SCALE = 64

DE_SCALE = L1_SCALE * OUTPUT_SCALE

# sigmoid K parameter
SCALE_K = 400

def crelu(x):
    return clamp(x, 0, L1_SCALE)

def output(hidden):
    &quot;&quot;&quot;Compute the neural network evaluation in centipawns.&quot;&quot;&quot;

    # hidden is the hidden layer&amp;#39;s pre-activation states (i.e. the accumulator).

    out = 0
    for j in range(L1_SIZE):
        out += WEIGHTS_OUTPUT[j] * crelu(hidden[j])
    out += BIAS_OUTPUT

    out *= SCALE_K

    out &amp;#47;= DE_SCALE

    return out
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Recall that the weights themselves aren&amp;#8217;t quantized in the engine code (for my engine, the Rust code),
they&amp;#8217;re already quantized in the neural network training code (Python for me).
In the engine, the main thing we need to do is de-scale the output at the very end (i.e. divide by &lt;code&gt;DE_SCALE&lt;/code&gt;).
The de-scaling is the last step, because integer division loses precision;
in other words, we round only at the end.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I&amp;#8217;m done explaining how my engine&amp;#8217;s NNUE works.
Now, here&amp;#8217;s the fun part: pitting the smarter engine against the old engine, which has material counting evaluation.
When I did this, the NNUE engine crushed the material counting engine,
with &lt;strong&gt;667 wins, 7 losses, and 32 draws,&lt;/strong&gt;
which represents a few hundred Elo gain.
Not bad!&lt;/p&gt;
&lt;p&gt;Here is an example game from this tournament.
Note that this tournament ran at a time control of 8 seconds + 0.08 second increment,
and an opening book was used for the first few moves.&lt;/p&gt;
&lt;iframe src=&quot;https://lichess.org/study/embed/86CNdSfx/0qVu4T3v&quot; frameborder=0&gt;&lt;/iframe&gt;
&lt;p&gt;I&amp;#8217;m not good at chess, so I can&amp;#8217;t analyze the game for you.
However, notice how White (the material-counting engine)
shuffles its rook over and over,
while Black (NNUE) develops its pieces.
You can definitely tell that the NNUE has some positional intelligence.&lt;/p&gt;
&lt;p&gt;If you want to see more, I also uploaded two other games from this tournament to the Lichess study,
which you can access through the &amp;#8220;View on Lichess&amp;#8221; option.&lt;/p&gt;
&lt;p&gt;There are of course still many issues with my engine&amp;#8217;s NNUE in its current state.
First of all, its chess strategy is still far from being good.
One of my smarter human friends who played against it said &amp;#8220;that&amp;#8217;s not how you play chess&amp;#8221;.
(I personally don&amp;#8217;t understand chess enough to discern this.)
I think the NNUE&amp;#8217;s inability to play great chess is probably a limitation of its training,
which I talk about more in this &lt;a href=&quot;#appendix-having-no-training-data&quot;&gt;appendix&lt;/a&gt; to this post.&lt;/p&gt;
&lt;p&gt;However, in general, I&amp;#8217;m quite proud of my chess engine;
it plays chess at a level way above my own comprehension of the game,
and I&amp;#8217;m happy with that.&lt;/p&gt;
&lt;h3 id=&quot;further-reading&quot;&gt;Further reading&lt;/h3&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;
If you&amp;#8217;re trying to follow along writing an NNUE using this blog post, my apologies for the pseudo-code snippets
being disorganized; the code serves mostly to illustrate my points rather than to be part of a real engine.
If you want a full, working, implementation of this architecture (and can read Rust code), please see the &lt;a href=&quot;https://github.com/dogeystamp/chess_inator&quot;&gt;source code&lt;/a&gt;
for my engine chess-inator, specifically &lt;code&gt;src&amp;#47;nnue.rs&lt;/code&gt;, &lt;code&gt;nnue&amp;#47;s3_train_neural_net.py&lt;/code&gt; and &lt;code&gt;nnue&amp;#47;s4_weights_to_bin.py&lt;/code&gt;.
Be warned that this code may not reflect how other, better, engines do things.
Otherwise, the &lt;a href=&quot;https://www.chessprogramming.org/NNUE&quot;&gt;Chess Programming Wiki&amp;#8217;s page&lt;/a&gt; on NNUE
is a good source for implementing this architecture.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;I&amp;#8217;m sure that if you&amp;#8217;ve read this far through the more than six thousand words of this post,
you&amp;#8217;re probably interested in neural networks for chess.
So, for you, here are some interesting links to read through,
written by people more competent than I am:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/jw1912/bullet/blob/main/docs/1-basics.md&quot;&gt;Bullet docs:&lt;/a&gt; Bullet is a machine learning library specifically designed for chess engines. The documentation I linked is specifically for beginners, and starts from a similar level as this blog post.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://www.chessprogramming.org/NNUE&quot;&gt;Chess Programming Wiki &amp;#8211; NNUE&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stockfish NNUE.&lt;/strong&gt; Earlier, I mentioned that NNUE seems like a difficult topic, because Stockfish and other engines
have really complex neural networks.
The neural network I covered in this post is relatively simple, if not trivial;
engines like Stockfish have features like Half-KP inputs&lt;sup id=&quot;fnref3&quot;&gt;&lt;a href=&quot;#fn3&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;, multiple layers, perspective-based neural networks, and so on.
The core principles are the same, but advanced networks can learn more information.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://official-stockfish.github.io/docs/nnue-pytorch-wiki/docs/nnue.html#quantization&quot;&gt;Stockfish NNUE document:&lt;/a&gt;
an amazingly detailled and comprehensive overview of the NNUE used in Stockfish. It may be outdated though.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.chessprogramming.org/Stockfish_NNUE&quot;&gt;Chess Programming Wiki &amp;#8211; Stockfish NNUE&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/cosmobobak/viridithas&quot;&gt;Viridithas&lt;/a&gt;: Viridithas (by cosmobobak) is one of the world&amp;#8217;s best chess engines,
and it also happens to be written in decently readable Rust.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://cosmo.tardis.ac/files/2024-06-01-nnue.html&quot;&gt;Cosmobobak &amp;#8211; NNUE Performance Improvements&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Leela Chess Zero&lt;/strong&gt;. Leela is a powerful chess engine based on a neural network,
but not the NNUE kind.
Lc0 takes the opposite approach of having a neural network that is more intelligent,
but more expensive and time-consuming to compute.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.chessprogramming.org/Leela_Chess_Zero&quot;&gt;Chess Programming Wiki &amp;#8212; Leela Chess Zero&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;appendix-crelu-quantization&quot;&gt;Appendix: CReLU quantization&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;#backref-quantization&quot;&gt;Above&lt;/a&gt;, I outlined the procedure for applying quantization on the basic NNUE.
Recall that during the quantization process, we scale up all the neural network&amp;#8217;s parameters.
Then, at the end of inference,
we scale the result down so that it has the same value as a network without quantization.
We use scaling because rounding big parameters causes less relative rounding error than with small parameters.&lt;/p&gt;
&lt;p&gt;To scale the parameters correctly, we first follow these steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Multiply the &lt;em&gt;accumulator&amp;#8217;s&lt;/em&gt; weights and biases by &lt;code&gt;SCALE_L1&lt;/code&gt;, which is defined to be &lt;code&gt;255&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Multiply the &lt;em&gt;output&amp;#8217;s&lt;/em&gt; weight by &lt;code&gt;SCALE_OUT&lt;/code&gt;, which is defined to be &lt;code&gt;64&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Multiply the &lt;em&gt;output&amp;#8217;s&lt;/em&gt; bias by &lt;code&gt;SCALE_OUT * SCALE_L1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then in the inference code, we make the following adjustments:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redefine the CReLU activation function as &lt;code&gt;clamp(x, 0, SCALE_L1)&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Divide the neural net&amp;#8217;s output node pre-activation by &lt;code&gt;SCALE_L1 * SCALE_OUT&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I will now show why this procedure works while preserving the neural network&amp;#8217;s output.&lt;/p&gt;
&lt;p&gt;Let&amp;#8217;s define the original neural network&amp;#8217;s output as a mathematical expression,
using &lt;code&gt;i&lt;/code&gt; to index inputs, and &lt;code&gt;j&lt;/code&gt; to index hidden layer nodes:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess6/eq3.svg&quot; alt=&quot;Math equation. z_&amp;quot;out&amp;quot; = b_&amp;quot;out&amp;quot; + sum_(j=1)^(n) [w_(&amp;quot;out&amp;quot;,j) dot &amp;quot;CReLU&amp;quot;(sum_(i=1)^(768) (w_(i j) x_i) + b_(j))]&quot; /&gt;
where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;z_out&lt;/code&gt; is the pre-activation of the output node (&lt;code&gt;output_preactivation&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;&lt;code&gt;b_out&lt;/code&gt; is the bias of the output node (&lt;code&gt;BIAS_OUTPUT&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;&lt;code&gt;w_(out, j)&lt;/code&gt; is the weight of the connection between the &lt;code&gt;j&lt;/code&gt;-th hidden layer node and the output node (&lt;code&gt;WEIGHTS_OUT[j]&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;&lt;code&gt;w_ij&lt;/code&gt; is the weight of the connection between the &lt;code&gt;i&lt;/code&gt;-th input and &lt;code&gt;j&lt;/code&gt;-th hidden layer node (&lt;code&gt;WEIGHTS_L1[i][j]&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x_i&lt;/code&gt; is the value of the &lt;code&gt;i&lt;/code&gt;-th input;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;b_j&lt;/code&gt; is the &lt;code&gt;j&lt;/code&gt;-th hidden layer node&amp;#8217;s bias.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;During the quantization process, we multiply all weights and biases by some constants, &lt;code&gt;SCALE_L1&lt;/code&gt; (which I will denote &lt;code&gt;S_L1&lt;/code&gt;)
and &lt;code&gt;SCALE_OUT&lt;/code&gt; (denoted &lt;code&gt;S_out&lt;/code&gt;).
We also change the definition of CReLU to &lt;code&gt;clamp(x, 0, SCALE_L1)&lt;/code&gt; (instead of clamping from 0 to 1).
I&amp;#8217;ll denote this new CReLU as &lt;code&gt;CReLU_L1&lt;/code&gt;.
This gives us a scaled neural network output,&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess6/eq4.svg&quot; alt=&quot;Math equation. z_&amp;quot;scaled&amp;quot; = (S_&amp;quot;L1&amp;quot; S_&amp;quot;out&amp;quot;) b_&amp;quot;out&amp;quot; + sum_(j=1)^(n) [S_&amp;quot;out&amp;quot; w_(&amp;quot;out&amp;quot;,j) dot &amp;quot;CReLU&amp;quot;_&amp;quot;L1&amp;quot; (sum_(i=1)^(768) (S_&amp;quot;L1&amp;quot; w_(i j) x_i) + S_&amp;quot;L1&amp;quot; b_(j))].&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Let&amp;#8217;s first focus on the CReLU, which is this part of the expression:
&lt;img src=&quot;../public/img/chess6/eq5.svg&quot; alt=&quot;Math equation. &amp;quot;CReLU&amp;quot;_&amp;quot;L1&amp;quot; (sum_(i=1)^(768) (S_&amp;quot;L1&amp;quot; w_(i j) x_i) + S_&amp;quot;L1&amp;quot; b_(j)).&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If we factor out &lt;code&gt;S_L1&lt;/code&gt;, we get
&lt;img src=&quot;../public/img/chess6/eq6.svg&quot; alt=&quot;Math equation. &amp;quot;CReLU&amp;quot;_&amp;quot;L1&amp;quot; (S_&amp;quot;L1&amp;quot; [sum_(i=1)^(768) (w_(i j) x_i) + b_(j)]).&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As you can see in the graph below,
we can pull out the scaling constant from the scaled &lt;code&gt;CReLU_L1&lt;/code&gt; to get a normal &lt;code&gt;CReLU&lt;/code&gt;:
&lt;img src=&quot;../public/img/chess6/eq8.svg&quot; alt=&quot;Math equation. S_&amp;quot;L1&amp;quot; dot &amp;quot;CReLU&amp;quot; (sum_(i=1)^(768) (w_(i j) x_i) + b_(j)).&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess6/crelu_graph_scaled.svg&quot; alt=&quot;Graph which compares two lines, CReLU_L1(S_L1 x) and CReLU(x).&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now, the scaled output of our neural network is
&lt;img src=&quot;../public/img/chess6/eq7.svg&quot; alt=&quot;Math equation. z_&amp;quot;scaled&amp;quot; = (S_&amp;quot;L1&amp;quot; S_&amp;quot;out&amp;quot;) b_&amp;quot;out&amp;quot; + sum_(j=1)^(n) [S_&amp;quot;out&amp;quot; w_(&amp;quot;out&amp;quot;,j) dot S_&amp;quot;L1&amp;quot; dot &amp;quot;CReLU&amp;quot; (sum_(i=1)^(768) (w_(i j) x_i) + b_(j))],&quot; /&gt;
which we can factor &lt;code&gt;(S_L1 * S_out)&lt;/code&gt; from to get
&lt;img src=&quot;../public/img/chess6/eq9.svg&quot; alt=&quot;Math equation. z_&amp;quot;scaled&amp;quot; = (S_&amp;quot;L1&amp;quot; S_&amp;quot;out&amp;quot;) (b_&amp;quot;out&amp;quot; + sum_(j=1)^(n) [w_(&amp;quot;out&amp;quot;,j) dot &amp;quot;CReLU&amp;quot; (sum_(i=1)^(768) (w_(i j) x_i) + b_(j))]).&quot; /&gt;
Note that inside the bracket is the original, unscaled output of the neural network:
&lt;img src=&quot;../public/img/chess6/eq10.svg&quot; alt=&quot;Math equation. z_&amp;quot;scaled&amp;quot; = (S_&amp;quot;L1&amp;quot; S_&amp;quot;out&amp;quot;) (z_&amp;quot;out&amp;quot;)&quot; /&gt;
from which we get
&lt;img src=&quot;../public/img/chess6/eq11.svg&quot; alt=&quot;Math equation. z_&amp;quot;scaled&amp;quot;/(S_&amp;quot;L1&amp;quot; S_&amp;quot;out&amp;quot;) = z_&amp;quot;out&amp;quot;.&quot; /&gt;
To conclude, at inference time, after scaling the weights, biases, and the CReLU function,
we can recover the unscaled output by dividing the scaled output by &lt;code&gt;SCALE_L1 * SCALE_OUT&lt;/code&gt;.
&lt;a href=&quot;#backref-quantization&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;appendix-having-no-training-data&quot;&gt;Appendix: Having no training data&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;#backref-having-no-training-data&quot;&gt;In the post&lt;/a&gt;, I mentioned a few issues with the chess engine that I actually built, &lt;a href=&quot;https://github.com/dogeystamp/chess_inator&quot;&gt;chess-inator&lt;/a&gt;.
Most importantly, the NNUE is trained on a few orders of magnitude less data than it should have been trained on.
Below, I document how that affected my project.
Note that &lt;strong&gt;nothing in this section reflects best practice:&lt;/strong&gt;
I&amp;#8217;m writing this purely to document what happened during the project.&lt;/p&gt;
&lt;p&gt;There are two reasons I could not train the NNUE on a sufficiently large dataset;
first, I was using data from SPRT tests (instead of generating them specifically for NNUE training),
and also, my training code was not able to handle large datasets.
As I mentioned in the post, someone in the Engine Programming Discord recommended
using a 5000 node limit to generate training data.
In my engine&amp;#8217;s training, there is no such limit,
so searches take longer to finish.
This already reduces the number of positions that can be generated as training data.
My training pipeline also heavily limited the datasets I could train on, as I
I encoded positions in compressed text files as zeroes and ones corresponding to the input features.
In hindsight, this is one of the most inefficient options there is,
considering that in chess, most of the inputs are 0 at all times.
The compression also didn&amp;#8217;t help much because once you load the data in memory,
it must be decompressed to allow for random access.
Aggravating the memory usage problems, the Python library I used to deal with the training data, pandas,
isn&amp;#8217;t great at dealing with huge datasets.&lt;/p&gt;
&lt;p&gt;When I first implemented NNUE in chess-inator,
I trained it on roughly a thousand games from my own testing.
This was, in hindsight, woefully too little training data.
During games, this network would would make egregious tactical blunders,
demonstrating a total lack of material knowledge.
Later models were trained on barely more data, around tens of thousands of games;
this was not enough of an improvement.&lt;/p&gt;
&lt;p&gt;At the time, I could not fix the neural network&amp;#8217;s tactical blunders,
so I decided to use a hacky solution.
I hardcoded the material knowledge in the neural network
by directly writing piece values into the neurons before training.
After this modification, the neural network would
at least match the performance of material counting evaluation.&lt;/p&gt;
&lt;p&gt;Generally, I would say this strategy worked in my situation;
all of my NNUE models are trained with hardcoded material knowledge,
and they&amp;#8217;re definitely superior at chess than the non-NNUE evaluation functions.&lt;/p&gt;
&lt;p&gt;Still, training with really low amounts of training data has its issues.
Notably, the engine isn&amp;#8217;t realizing the potential of its current neural network architecture;
lots of performance could still be achieved just by increasing the amount of data.
Also, there are weird artifacts in the evaluation function:
sometimes, the score for a specific position will be thousands of centipawns off.&lt;/p&gt;
&lt;p&gt;Unfortunately, this is the point where I left off on chess-inator;
I never fixed the neural network&amp;#8217;s lack of training data.
This was my first real project in machine learning,
and ultimately I learned that throwing more data at an ML problem is usually a good idea.
It would be really fun to come back&lt;sup id=&quot;fnref4&quot;&gt;&lt;a href=&quot;#fn4&quot; rel=&quot;footnote&quot;&gt;4&lt;/a&gt;&lt;/sup&gt; to chess-inator and properly train the neural network
with lots of data,
but the reason I stopped working on the project was that chess engine development is highly addicting,
and I&amp;#8217;m scared of getting sucked back in.
&lt;a href=&quot;#backref-having-no-training-data&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
&lt;hr/&gt;
&lt;ol&gt;

&lt;li id=&quot;fn1&quot;&gt;
&lt;p&gt;Because of how chess works, there will never be a White pawn on a1,
so this bit is useless.
However, such redundancy helps simplify our code, so it&amp;#8217;s fine to leave it there.&amp;#160;&lt;a href=&quot;#fnref1&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn2&quot;&gt;
&lt;p&gt;See this &lt;a href=&quot;https://arxiv.org/html/2412.17948v1#:~:text=noisy%20or%20tactically%20unstable%20positions%20drastically%20ruins%20the%20training%20of%20the%20neural%20network.&quot;&gt;paper&lt;/a&gt;
that mentions this issue.&amp;#160;&lt;a href=&quot;#fnref2&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn3&quot;&gt;
&lt;p&gt;As I understand it, Stockfish no longer uses HalfKP as of today; they use &lt;a href=&quot;https://www.chessprogramming.org/Stockfish_NNUE#HalfKA&quot;&gt;HalfKA&lt;/a&gt;.&amp;#160;&lt;a href=&quot;#fnref3&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn4&quot;&gt;
&lt;p&gt;As of writing, it has been a few months since the last commit of chess-inator.&amp;#160;&lt;a href=&quot;#fnref4&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;
&lt;/div&gt;</content>
		<link href="https://www.dogeystamp.com/chess6"/>
		<id>https://www.dogeystamp.com/chess6</id>
		<updated>2025-05-28T10:00:00Z</updated>
		<published>2025-05-28T10:00:00Z</published>
	</entry>
	<entry>
		<title>Chess engine, pt. 5: Quiescence search, endgames, repetition avoidance</title>
		<content type="html">&lt;h1 id=&quot;chess-engine-pt.-5-quiescence-search-endgames-repetition-avoidance&quot;&gt;Chess engine, pt. 5: Quiescence search, endgames, repetition avoidance&lt;/h1&gt;
&lt;div class=&quot;creation-date&quot;&gt;
2025-04-26
&lt;/div&gt;
&lt;link rel=&quot;contents&quot; href=&quot;/chess0&quot; /&gt;
&lt;link rel=&quot;prev&quot; href=&quot;/chess4&quot; /&gt;
&lt;link rel=&quot;next&quot; href=&quot;/chess6&quot; /&gt;
&lt;div class=&quot;callout markdown-alert markdown-alert-callout&quot;&gt;
&lt;p&gt;This post is part of a series about building a chess-playing engine.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/chess0&quot;&gt;Introduction (0)&lt;/a&gt;
| &lt;a href=&quot;/chess4&quot;&gt;← Last part (4)&lt;/a&gt;
| &lt;a href=&quot;/chess6&quot;&gt;Next part (6) →&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;At this point in the series,
our DIY chess engine has an okay skill level,
but it actually has a few major flaws.&lt;/p&gt;
&lt;p&gt;If you observe the games it plays,
it occasionally makes really obvious (to a human) mistakes,
it can&amp;#8217;t checkmate with just a king and a queen,
and it will draw by threefold repetition instead of easily winning the game.&lt;/p&gt;
&lt;p&gt;These are really annoying issues
if you don&amp;#8217;t know what causes them,
or how to fix them.
The obvious blunders are especially annoying,
because it seems like a logic error in your code,
but it is actually a fundamental problem in our heuristic negamax algorithm.&lt;/p&gt;
&lt;p&gt;In this post, I&amp;#8217;ll therefore be examining each of these problems,
what causes them, and how they can be fixed.
In other words, this part of the series is a &amp;#8220;fixing annoying issues&amp;#8221; post.&lt;/p&gt;
&lt;h2 id=&quot;horizon-effect&quot;&gt;Horizon effect&lt;/h2&gt;
&lt;p&gt;The &lt;strong&gt;horizon effect&lt;/strong&gt; (&lt;a href=&quot;https://en.m.wikipedia.org/wiki/Horizon_effect&quot;&gt;Wikipedia&lt;/a&gt;)
is a problem that affects most programs that deal with a game tree,
like our chess engine.
It stems from the fact that our negamax thinks ahead by some exact depth, like 4 half-moves deep,
and it immediately stops without considering what happens at the 5th half-move.&lt;/p&gt;
&lt;h3 id=&quot;an-example-position&quot;&gt;An example position&lt;/h3&gt;
&lt;p&gt;An example is the best way to demonstrate the horizon effect.
Consider this chess position:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess5/blunder1.gif&quot; alt=&quot;A chess position diagram. The FEN for this position is rn1qkb1r/pp2ppp1/2pp1n1p/1B6/3PP1b1/2N2Q2/PPP2PPP/R1B1K1NR w KQkq - 0 6&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In earlier versions of my chess engine,
it would play Bb5+:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess5/blunder2.gif&quot; alt=&quot;A chess position diagram. The FEN for this position is rn1qkb1r/pp2ppp1/2Bp1n1p/8/3PP1b1/2N2Q2/PPP2PPP/R1B1K1NR b KQkq - 0 6&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is a bad move:
White is leaving their queen under attack,
eventually losing the bishop.
At first, it seems really weird that a program
that is based entirely on counting material
would make a move that loses material.&lt;/p&gt;
&lt;p&gt;To debug this kind of mistake,
we can construct the &lt;a href=&quot;https://www.chessprogramming.org/Principal_Variation&quot;&gt;Principal Variation&lt;/a&gt; (PV) line
of the negamax search.
The PV is the series of best moves from a position, according to the chess engine.
The first PV move is White&amp;#8217;s best move,
then the second PV move is Black&amp;#8217;s best reply,
then White&amp;#8217;s best reply, and so on.&lt;/p&gt;
&lt;p&gt;In other words, the PV is the &amp;#8220;thought process&amp;#8221; of the chess engine
that shows what it expects to happen in the future.&lt;/p&gt;
&lt;p&gt;To construct the PV, an easy (but inefficient) method is to return a list of moves
instead of just the best move from the negamax function.
A more efficient way is to use the transposition table (from &lt;a href=&quot;/chess4&quot;&gt;last post&lt;/a&gt;)
repeatedly to get the best moves,
but this is not guaranteed to get the full PV.&lt;/p&gt;
&lt;p&gt;Now, here&amp;#8217;s the PV line of the above position, annotated by me:&lt;/p&gt;
&lt;iframe src=&quot;https://lichess.org/study/embed/86CNdSfx/6o65Tt6s&quot; frameborder=0&gt;&lt;/iframe&gt;
&lt;p&gt;The engine&amp;#8217;s PV is &lt;code&gt;Bb5+ c6 Bxc6+ Nxc6 Qxf6&lt;/code&gt;.
White takes a pawn with a bishop,
then takes a knight with the queen.
With a depth of 5 half-moves, White thus gains a pawn (+100 centipawns) in material value.&lt;/p&gt;
&lt;p&gt;However, at a depth of 6 half-moves, Black can take the queen (gxf6),
and White thus has a -800 centipawn score from this exchange,
i.e. it is a huge mistake.&lt;/p&gt;
&lt;p&gt;Presumably, the chess engine searched at a depth of 5 half-moves and found that
this exchange was advantageous, not seeing the big loss that happens at 6 half-moves.&lt;/p&gt;
&lt;h3 id=&quot;quiescence-search&quot;&gt;Quiescence search&lt;/h3&gt;
&lt;p&gt;Let&amp;#8217;s now try to mitigate this horizon effect.&lt;/p&gt;
&lt;p&gt;Our chess engine evaluates this position as a +100cp advantage for White:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess5/blunder3.gif&quot; alt=&quot;A chess position diagram. The FEN for this position is r2qkb1r/pp2ppp1/2np1Q1p/8/3PP1b1/2N5/PPP2PPP/R1B1K1NR b KQkq - 0 7&quot; /&gt;&lt;/p&gt;
&lt;p&gt;However, a human player, seeing this, would understand that White&amp;#8217;s queen will immediately be captured on the next move,
so this is actually a -800cp position.
Therefore, if there are any pending captures in a position, our evaluation function can be misleading.&lt;/p&gt;
&lt;p&gt;Let&amp;#8217;s call any position &lt;em&gt;without&lt;/em&gt; such pending captures a &lt;strong&gt;quiescent position&lt;/strong&gt;, or quiet position.&lt;sup id=&quot;fnref1&quot;&gt;&lt;a href=&quot;#fn1&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;
(The above position where the queen is about to be taken is a non-quiescent position.)
Then, notice how our engine&amp;#8217;s evaluation function breaks on most non-quiet positions.&lt;/p&gt;
&lt;p&gt;To avoid the horizon effect, all we need to do is to only run our evaluation function on quiescent positions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Quiescence search&lt;/strong&gt;
(&lt;a href=&quot;https://en.m.wikipedia.org/wiki/Quiescence_search&quot;&gt;Wikipedia&lt;/a&gt;) accomplishes
this by always searching more moves into the future until a quiescent position
is found, and only then running the evaluation function.&lt;/p&gt;
&lt;p&gt;In other words, we only evaluate positions after all the captures are done;
we never use the evaluation function on positions where pieces are hanging (i.e. non-quiet positions).&lt;/p&gt;
&lt;p&gt;Our negamax search will thus be divided into two parts: regular search, and quiescence search (&amp;#8220;q-search&amp;#8221;).
For example, for a depth 5 search, negamax would search 5 half-moves ahead,
then, if the leaf node is not a quiet position, quiescence search might look an
extra 2 half-moves further until the position is quiet.
Theoretically, there should be no depth limit on quiescence search;
it runs until it finds a quiet position.&lt;sup id=&quot;fnref2&quot;&gt;&lt;a href=&quot;#fn2&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Quiescence search is different from regular search in that it does not search
all possible moves. While regular search will search every move, quiescence search will
only examine obvious captures, like pawn takes queen in the above example.&lt;/p&gt;
&lt;p&gt;What makes a capture &amp;#8220;obvious&amp;#8221;, though?
Chess players know when a capture is good because they run a quick mental
calculation of the exchange.
They look at which pieces are attacking, and which pieces are defending,
and then they see if they gain material by capturing.&lt;/p&gt;
&lt;h3 id=&quot;static-exchange-evaluation&quot;&gt;Static exchange evaluation&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Static exchange evaluation&lt;/strong&gt; (SEE) is a heuristic that performs this quick
calculation, that is it predicts how much material a player will gain or lose
from performing a certain exchange.&lt;/p&gt;
&lt;p&gt;The key idea of SEE is that
in any exchange, there are two choices for the player to move.
They can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;capture&lt;/em&gt; and keep the exchange going; or&lt;/li&gt;
&lt;li&gt;&lt;em&gt;stop&lt;/em&gt; the exchange if it&amp;#8217;s not worth it to continue.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SEE is based on this dichotomy;
both players always pick the choice that ends up giving the maximal score for them.&lt;/p&gt;
&lt;p&gt;Take this position as an example:&lt;/p&gt;
&lt;iframe src=&quot;https://lichess.org/study/embed/86CNdSfx/cwswEJeh&quot; frameborder=0&gt;&lt;/iframe&gt;
&lt;ol&gt;
&lt;li&gt;White first &lt;em&gt;captures&lt;/em&gt; with Rxd5, giving them a 900cp score.&lt;/li&gt;
&lt;li&gt;Black &lt;em&gt;captures&lt;/em&gt; with Bxd5, giving a 400cp (for White) score.&lt;/li&gt;
&lt;li&gt;White then &lt;em&gt;stops&lt;/em&gt; and plays a non-capture move Qh6, ending with a final score of 400cp.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In each step, the moves either side plays are advantageous for them
(that is, any other move would be worse).
It is worthwhile for White to trade their rook for Black&amp;#8217;s queen,
and it is worthwhile for Black to take back with a bishop.
However, White does not want to trade their queen for the bishop.&lt;/p&gt;
&lt;p&gt;Here is an example implementation of SEE in pseudo-code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def eval_see(board, dest_square, our_color) -&amp;#62; int:
    &quot;&quot;&quot;Simulate an exchange on a given square, and return its material gain for a given side.&quot;&quot;&quot;

    # attacker piece of our color with the minimal material value
    least_attacker = board.get_least_attacker(dest_square, our_color)
    defender = board.get_piece_on(dest_square)

    eval = 0

    if defender is not None:
        least_attacker.capture(dest_square)

        opponent_gain: int = eval_see(board, dest_square, our_color.flip())

        # this represents the two choices of _stop_ and _capture_.
        # if capture has a negative eval, stop the exchange and get 0 material gain.
        eval = max(0, defender.value - opponent_gain)

        least_attacker.undo_capture(dest_square)

    return eval
&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; In the above code, I used a &lt;code&gt;get_least_attacker(dest_square, our_color)&lt;/code&gt; method without explaining how to implement it.
Recall the
&lt;a href=&quot;/chess1/#:~:text=A%20common%20method%20to%20determine%20if%20the%20king%20is%20in%20check%20is%20the%20%E2%80%9CI%20see%20you%2C%20you%20see%20me%E2%80%9D%20method.%20Let%E2%80%99s%20say%20we%20want%20to%20detect%20checks%20from%20rooks.%20Pretend%20the%20king%20can%20move%20like%20a%20rook.%20If%20the%20king%2C%20with%20rook%20movement%2C%20could%20capture%20an%20enemy%20rook%2C%20then%20that%20means%20the%20enemy%20rook%20can%20capture%20the%20king.&quot;&gt;&amp;#8220;I see you, you see me&amp;#8221; check detection&lt;/a&gt;
method from the first chapter.
Suppose the king can move like a rook, and that the king-rook can capture an enemy rook.
Then, we know that this enemy rook is attacking the king
(and a similar process is used for other piece types).
We can generalize this method to any square to detect attackers for SEE.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Now that we have an SEE routine,
whenever we want to score a capture move,
we can call SEE and check if it seems like a good capture or a bad capture.&lt;sup id=&quot;fnref3&quot;&gt;&lt;a href=&quot;#fn3&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;
This is of course only a heuristic, because
our static evaluation doesn&amp;#8217;t take into account pins, promotions
and other edge cases.
SEE works most of the time, though.
Remember that we mostly use SEE in quiescence search;
regular search does take into account pins and promotions.&lt;/p&gt;
&lt;h3 id=&quot;quiescence-search-continued&quot;&gt;Quiescence search, continued&lt;/h3&gt;
&lt;p&gt;Having discussed SEE, let&amp;#8217;s return to quiescence search. Our goal is, before
calling the evaluation function, to get to a quiet position. That is, as long
as there are &lt;em&gt;good capture moves&lt;/em&gt;, or in other words pending captures, we
should not evaluate the position.&lt;/p&gt;
&lt;p&gt;Quiescence search is just like normal negamax search, except we only examine
the good capture moves. When we run out of good capture moves, we can finally
evaluate the position.&lt;/p&gt;
&lt;p&gt;To find good capture moves, we first generate all captures, then narrow down
the list by discarding captures with negative SEE score.
If there are no captures, that means the position is quiet.&lt;/p&gt;
&lt;p&gt;However, if the player is in check,
we can&amp;#8217;t just generate captures;
we should generate all possible moves
that could evade the check.
A position with check can not be quiet,
since there is a risk of checkmate.&lt;sup id=&quot;fnref4&quot;&gt;&lt;a href=&quot;#fn4&quot; rel=&quot;footnote&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;An important detail is that in quiescence search, no player is &lt;em&gt;forced&lt;/em&gt; to make
a move as in normal chess. That is, if all the captures with positive SEE
worsen the current score of the position (given by the evaluation function), you are allowed to use this current score,
known as the &lt;strong&gt;stand pat&lt;/strong&gt; evaluation.
When the stand pat evaluation is better than any capture move&amp;#8217;s score, it&amp;#8217;s a
sign that the best move in that position is a non-capture move, which we don&amp;#8217;t
care about in quiescence search.
If the player is in check though, we are not allowed to use stand-pat,
since we might be ignoring checkmate.&lt;/p&gt;
&lt;p&gt;Another note about implementing quiescence search is that many people
write it as a separate function that is called in leaf nodes of negamax,
while I integrated it into negamax to reuse some logic.&lt;/p&gt;
&lt;p&gt;Now, here is my pseudo-code for quiescence search within negamax. I have
removed the alpha-beta portions to make the function simpler; for a further
explanation of the code this is based on, see the end of &lt;a href=&quot;/chess2#heuristic-negamax-pseudocode&quot;&gt;part 2&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;QSEARCH_DEPTH_LIMIT = 8

def negamax(position, depth: int, is_qsearch: bool) -&amp;#62; (Move, int):
    &quot;&quot;&quot;
    Return the best move and evaluation of a chess position.

    Will only think `depth` half-moves ahead.
    &quot;&quot;&quot;

    if depth == 0:
        if is_qsearch:
            # hit depth limit (in quiescence search).
            # note that eval is score from our perspective
            return None, eval(position)
        else:
            # enter quiescence search
            return negamax(position, QSEARCH_DEPTH_LIMIT, True)

    best_score = -infinity

    if is_qsearch and not position.is_check():
        # stand pat score
        best_score = eval(position)

        # keep only captures with positive (good) SEE
        possible_moves = [move for move in generate_captures(position) if eval_see(position, move.destination, position.turn) &amp;#62;= 0]
    else:
        possible_moves = generate_moves(position)
        if possible_moves.empty():
            # checkmate or stalemate
            return None, eval(position)

    best_move = None

    for each move in possible_moves:
        updated_position = apply_move_to_position(position, move)
        opponent_move, opponent_score = negamax(updated_position, depth - 1, is_qsearch)
        our_score = negate_score(opponent_score)

        if our_score &amp;#62; best_score:
            best_score = our_score
            best_move = move

    return best_move, best_score
&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Make sure that you have a move generator that &lt;em&gt;specifically generates captures&lt;/em&gt;,
instead of taking a regular move generator and filtering its output.
Move generation is an expensive operation because it checks the legality of moves,
and you don&amp;#8217;t want to check the legality of moves that you immediately discard.
My engine&amp;#8217;s quiescence search took a long while to work because I made this mistake.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;checkmate-heuristics&quot;&gt;Checkmate heuristics&lt;/h2&gt;
&lt;p&gt;An essential skill in chess is delivering checkmate with few pieces.
In this section, I&amp;#8217;ll focus on the KQ vs. K (king + queen vs. king)
and KR vs. K (king + rook vs. king) endgames.&lt;/p&gt;
&lt;p&gt;Here&amp;#8217;s an example of how an early version of my engine performs in a K-Q vs. K endgame:&lt;/p&gt;
&lt;iframe src=&quot;https://lichess.org/study/embed/86CNdSfx/fhXPa7vP&quot; frameborder=0&gt;&lt;/iframe&gt;
&lt;p&gt;The engine draws by repetition,
even though KQ vs. K is supposed to be an easy win for the player with the queen.&lt;/p&gt;
&lt;p&gt;The typical strategy in the KQ vs K endgame and the KR vs K endgame is as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Bring the enemy king to the edge of the board.&lt;/li&gt;
&lt;li&gt;Have your own king near the enemy king.&lt;/li&gt;
&lt;li&gt;Checkmate with your king supporting the queen or rook.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The first two steps are essentially the preparation required to give checkmate;
they set up all the pieces so they&amp;#8217;re in the right place.
Once we get to step 3, the engine should be able to see checkmate.
Therefore, our goal is to help the engine do steps 1 and 2.&lt;/p&gt;
&lt;h3 id=&quot;cornering-the-enemy&quot;&gt;Cornering the enemy&lt;/h3&gt;
&lt;p&gt;Let&amp;#8217;s first do step 1, bringing the enemy king to the edge of the board.
In the code, we can do this by using &lt;em&gt;piece-square tables&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;To keep this article short, I&amp;#8217;m only going to fully explain piece square tables &lt;a href=&quot;/chess6#piece-square-tables&quot;&gt;in the next post&lt;/a&gt;.
Essentially, in the evaluation function, a bonus or penalty is given to the
score of a position depending on which square the king is on.
In the endgame, we want the king to be at the center, so that can be a slight bonus.
Meanwhile, a king at the edge of the board is vulnerable,
so when the king is on the edge squares it gives a significant penalty.&lt;/p&gt;
&lt;p&gt;Once we have this penalty, the engine will seek to gain advantage by cornering the enemy king,
because it now knows that a king on the edge of the board is vulnerable.&lt;/p&gt;
&lt;p&gt;We only want this piece-square table bonus&amp;#47;penalty to apply during the endgame,
since in the opening of the game, we actually do want the king to stay at the edge.
To concretely determine when it&amp;#8217;s the endgame, we can count the pieces still remaining on the board,
and if it&amp;#8217;s under a certain threshold we say it&amp;#8217;s the endgame.&lt;/p&gt;
&lt;p&gt;Again, this is just a brief summary; see the next post for details about piece-square tables and
determining when is the endgame.&lt;/p&gt;
&lt;p&gt;Here is an example implementation in pseudo-code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def eval_with_endgame_pst():
    score = count_material()  # this returns a score from our side&amp;#39;s perspective.
                              # see prior posts for more information.

    if is_endgame:
        # 64 elements, one score per square the king could be on.
        # positive is a bonus, negative is a penalty.
        square_scores = {a1: -100, b1: -100, ..., e4: 10, ..., h8: -100}

        score += square_scores[board.get_square_of(our_king)]
        score -= square_scores[board.get_square_of(their_king)]

    return score
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that in a real engine you would not implement a piece-square table just for the king:
this code is specifically for demonstrating this section&amp;#8217;s key ideas.
Also, don&amp;#8217;t use a dictionary for a piece square table.&lt;/p&gt;
&lt;h3 id=&quot;king-distance&quot;&gt;King distance&lt;/h3&gt;
&lt;p&gt;Now, after we bring the enemy king to the edge of the board (first step),
we continue with the second step: bringing our own king over to help with the checkmate.&lt;/p&gt;
&lt;p&gt;In the above example KQ-K endgame,
White&amp;#8217;s king was on the wrong side of the board.
It would take multiple steps to move the king across the board,
but the engine&amp;#8217;s search depth limit prevents it from seeing that.
Thus, the engine could not deliver checkmate,
and kept shuffling the queen until it got a threefold repetition draw.&lt;/p&gt;
&lt;p&gt;To fix this issue, we tweak the evaluation function so that, in king + piece
vs. king endgames, there is a bonus to a position&amp;#8217;s score if the kings are
closer.
Or, equivalently, there is a penalty if the kings are far away.&lt;/p&gt;
&lt;p&gt;Here is pseudo-code implementing this penalty:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def king_dist_eval():
    score = eval_with_endgame_pst()

    if (number of non-king pieces == 1) and (the piece is not a pawn):
        distance = (manhattan distance between kings)

        # this factor specifies which side is winning (i.e. doing the checkmate)
        advantage = 1 if (our side has the extra piece) else -1

        # this might have to be tweaked
        KING_PENALTY_FACTOR = 70

        king_penalty = -distance * advantage * KING_PENALTY_FACTOR

        score += king_penalty

    return score
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The king distance penalty kicks in when the engine detects that it&amp;#8217;s in a KR-K or KQ-K endgame.
Notably, the code excludes a pawn endgame, which has a different strategy.&lt;/p&gt;
&lt;p&gt;Then, we calculate the &lt;a href=&quot;https://simple.m.wikipedia.org/wiki/Manhattan_Distance&quot;&gt;Manhattan distance&lt;/a&gt;
between the kings.
I used this instead of the usual &lt;a href=&quot;https://en.m.wikipedia.org/wiki/Euclidean_distance#Two_dimensions&quot;&gt;Euclidean distance&lt;/a&gt;
because it gives an integer result and is easier to calculate.
Essentially, Manhattan distance is the distance to get from one point to another, if you can only move up, down, left, right (four directions, no diagonals).
(In retrospect, it might have been a good decision to use &lt;a href=&quot;https://en.m.wikipedia.org/wiki/Chebyshev_distance&quot;&gt;Chebyshev distance&lt;/a&gt;
instead, since it corresponds exactly to the number of moves it takes for a king to move between two points.)&lt;/p&gt;
&lt;p&gt;Next, a negative sign is added to the distance;
recall that a larger distance between kings is worse (negative) for the player trying to do checkmate.&lt;/p&gt;
&lt;p&gt;An &amp;#8220;advantage&amp;#8221; factor is also used to make the sign of the penalty correct (since
negative evaluation is good for the opponent, and positive is good for our side).&lt;/p&gt;
&lt;p&gt;Before we get a final number, we multiply the penalty by &lt;code&gt;KING_PENALTY_FACTOR&lt;/code&gt;.
Once there are other bonuses or penalties in the evaluation function,
this factor sets the weight of the &amp;#8220;king distance&amp;#8221; penalty compared to others.&lt;/p&gt;
&lt;p&gt;Once we have these two heuristics, 
bringing the enemy king to the edge,
and bringing our own king over,
the chess engine is fully able to win in a KQ-K endgame:&lt;/p&gt;
&lt;iframe src=&quot;https://lichess.org/study/embed/86CNdSfx/4PNoVKj8&quot; frameborder=0&gt;&lt;/iframe&gt;
&lt;p&gt;The engine can also win a KR-K endgame:&lt;/p&gt;
&lt;iframe src=&quot;https://lichess.org/study/embed/86CNdSfx/xlJtuHJY&quot; frameborder=0&gt;&lt;/iframe&gt;
&lt;p&gt;With the heuristics, the engine took 30 half-moves to mate,
while under optimal conditions (according to Lichess&amp;#8217;s tablebase)
this position is mate in 27 half-moves.&lt;/p&gt;
&lt;p&gt;We&amp;#8217;ve thus successfully taught the engine how to mate with just a rook, or just a queen.&lt;/p&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;a href=&quot;/chess4/#iterative-deepening&quot;&gt;Iterative deepening&lt;/a&gt; is really helpful in endgame situations like this;
since there are less pieces on the board, the engine can usually search deeper in the same amount of time.
Using a fixed search depth, the engine might complete a depth 6 search in 0.2 seconds and stop right there;
with iterative deepening, the engine might attempt a depth 7 or higher search with the extra time it has.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;threefold-repetition-draws&quot;&gt;Threefold repetition draws&lt;/h2&gt;
&lt;p&gt;In chess, when a position is repeated three times (not necessarily three times consecutively),
this is called a &lt;a href=&quot;https://en.m.wikipedia.org/wiki/Threefold_repetition&quot;&gt;threefold repetition&lt;/a&gt;,
and the players may claim a draw.&lt;/p&gt;
&lt;p&gt;In computer chess, threefold repetition is usually automatically declared a draw by the UCI server,
so you technically don&amp;#8217;t have to handle it.
However, threefold repetition leads to really annoying outcomes, like in this real game:&lt;/p&gt;
&lt;iframe src=&quot;https://lichess.org/study/embed/86CNdSfx/radOq8gd#29&quot; frameborder=0&gt;&lt;/iframe&gt;
&lt;p&gt;Though Black (my engine) has the advantage, it fails to win because it enters a loop and draws by repetition.&lt;/p&gt;
&lt;p&gt;I&amp;#8217;m not exactly sure what the engine&amp;#8217;s thought process is in this situation;
I assume it&amp;#8217;s thinking &amp;#8220;I can always capture the queen one move later&amp;#8221;
before playing Ne4+.&lt;/p&gt;
&lt;p&gt;Regardless, we don&amp;#8217;t want the engine to draw by threefold repetition when it&amp;#8217;s obviously winning.
We need to implement a system that detects threefold repetition ahead of time,
and assigns that position a draw (zero) score.
Then, the engine will prioritize winning over drawing, and avoid repetitions.&lt;/p&gt;
&lt;p&gt;First, we need to precisely define what a position being &amp;#8220;repeated&amp;#8221; means.
Wikipedia says (emphasis mine):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Two positions are by definition &amp;#8216;the same&amp;#8217; if pieces of the same &lt;strong&gt;type&lt;/strong&gt; and &lt;strong&gt;color&lt;/strong&gt; occupy the same &lt;strong&gt;squares&lt;/strong&gt;, the same &lt;strong&gt;player has the move,&lt;/strong&gt; the remaining &lt;strong&gt;castling rights&lt;/strong&gt; are the same and the possibility to capture &lt;strong&gt;en passant&lt;/strong&gt; is the same.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Recall from last post that we already implemented a system that detects repeated positions:
the &lt;a href=&quot;/chess4/#hashing&quot;&gt;Zobrist hash&lt;/a&gt; is a unique number assigned to each position such that
two identical positions will have matching hashes.
Our Zobrist hash incorporates all the elements of this definition: pieces with a given type, color, and square;
the castling rights; the en-passant rights; and the current turn.&lt;/p&gt;
&lt;p&gt;Therefore, the strategy for detecting threefold repetition is to
keep a stack (a list) of the hashes of recent positions,
and compare the hash of the each new position to the ones in the list.
If there is a match, that means there is probably a repetition,
so we can assign a draw score to the position.
Every time we make a move in negamax, add the new position to the top of the stack,
and every time we undo a move, remove the position from the stack.&lt;/p&gt;
&lt;p&gt;You may think that comparing the hash of new positions to every single hash in the list is computationally expensive.
One thing that makes it less inefficient is that we don&amp;#8217;t actually have to look at the entire list.
Moves like captures and pawn moves are irreversible, so once they happen,
it&amp;#8217;s impossible to see the previous position again.
The engine already keeps a counter of &amp;#8220;how many half-moves has it been since the last irreversible move,&amp;#8221;
since that&amp;#8217;s &lt;a href=&quot;https://en.m.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation#:~:text=%5B8%5D-,Halfmove%20clock,-%3A%20The%20number%20of&quot;&gt;part of the Forsyth-Edwards Notation (FEN) string&lt;/a&gt;.
Therefore, we only need to search that amount of positions backwards in the list.
Usually, this is some small number.&lt;/p&gt;
&lt;p&gt;Note that some chess engines don&amp;#8217;t actually implement &lt;em&gt;threefold&lt;/em&gt; repetition specifically.
Instead, twofold repetitions are already considered a draw,
since it&amp;#8217;s a waste of time to re-run negamax on a position the engine has already seen.
There are some exceptions, though.
Specifically, this is the strategy my own engine uses:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Three-fold repetition (i.e. the position is repeated twice) is an automatic draw.&lt;/strong&gt; This follows the rule in chess.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two-fold repetition (position repeated once) is a draw if the position being repeated is part of the negamax search tree,&lt;/strong&gt; and hasn&amp;#8217;t actually happened on the real board yet. This means that we&amp;#8217;re already running negamax on this position, so we avoid re-doing the computation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two-fold repetition is not a draw if the position being repeated is in the history of the game,&lt;/strong&gt; and actually already happened on the board. Two-fold repetition is not legally a draw in chess, and we still need to run negamax on the position for the first time.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If this strategy is too complicated, just ignore it and always score two-fold repetitions as draws.
(The &lt;a href=&quot;https://www.chessprogramming.org/Repetitions#Assigning_Draw_Score&quot;&gt;Chess Programming Wiki page&lt;/a&gt; has a differently-worded explanation of this concept,
if it helps.)&lt;/p&gt;
&lt;p&gt;Once repetition detection is correctly implemented,
the chess engine will only go for repetition draws when the alternative is losing.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Once all the above features have been implemented, here is an example game between the upgraded engine and itself:&lt;/p&gt;
&lt;iframe src=&quot;https://lichess.org/study/embed/86CNdSfx/jYNVh615&quot; frameborder=0&gt;&lt;/iframe&gt;
&lt;p&gt;As you can see, the game does not end in a repetition draw,
and quiescence search makes it so there are no horrible tactical blunders being played.
This is definitely an improvement over prior versions of the engine.&lt;/p&gt;
&lt;p&gt;At this point, a major issue still remains:
human chess players often say my engine plays &amp;#8220;weird&amp;#8221;.
I think this is because the engine has basic tactical (short-term) knowledge,
but zero positional (long-term strategy) knowledge in chess.
You may notice that in the game above, pawn promotion was the decisive factor in White&amp;#8217;s win.
Since pawns need many moves to get to the top of the board,
the engine can&amp;#8217;t see pawn promotion in negamax;
pawn promotion is more positional than tactical.
Clearly, positional chess skills can make or break a game.&lt;/p&gt;
&lt;p&gt;In the next post, I&amp;#8217;ll be explaining how the evaluation function of a chess engine can be improved
(this component of the engine is where positional knowledge is encoded.)
By the end of it, I&amp;#8217;ll introduce neural networks to the chess engine,
so that it plays better (and hopefully less &amp;#8220;weird&amp;#8221;).&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/chess6&quot;&gt;Next part →&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
&lt;hr/&gt;
&lt;ol&gt;

&lt;li id=&quot;fn1&quot;&gt;
&lt;p&gt;More precisely, we can define a quiescent position is a position
where there are no good moves that significantly change the evaluation
function&amp;#8217;s result; the position is thus stable, or &amp;#8220;quiet&amp;#8221;.&amp;#160;&lt;a href=&quot;#fnref1&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn2&quot;&gt;
&lt;p&gt;Empirically, I found that limiting quiescence to an amount like 8 half-moves produces performs well
(but I do not know why).&amp;#160;&lt;a href=&quot;#fnref2&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn3&quot;&gt;
&lt;p&gt;See &lt;a href=&quot;https://www.chessprogramming.org/Static_Exchange_Evaluation#Seeing_a_Capture&quot;&gt;this section&lt;/a&gt; of the relevant Chess Programming Wiki article.&amp;#160;&lt;a href=&quot;#fnref3&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn4&quot;&gt;
&lt;p&gt;Recall that a quiet position can be defined as a position where the static evaluation
is stable. A checkmate instantly destabilizes the static evaluation, so positions where that
might happen are not quiet.&amp;#160;&lt;a href=&quot;#fnref4&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;
&lt;/div&gt;</content>
		<link href="https://www.dogeystamp.com/chess5"/>
		<id>https://www.dogeystamp.com/chess5</id>
		<updated>2025-04-26T10:00:00Z</updated>
		<published>2025-04-26T10:00:00Z</published>
	</entry>
	<entry>
		<title>Chess engine, pt. 4: α-β pruning and better search</title>
		<content type="html">&lt;h1 id=&quot;chess-engine-pt.-4---pruning-and-better-search&quot;&gt;Chess engine, pt. 4: α-β pruning and better search&lt;/h1&gt;
&lt;div class=&quot;creation-date&quot;&gt;
2025-04-18
&lt;/div&gt;
&lt;link rel=&quot;contents&quot; href=&quot;/chess0&quot; /&gt;
&lt;link rel=&quot;prev&quot; href=&quot;/chess3&quot; /&gt;
&lt;link rel=&quot;next&quot; href=&quot;/chess5&quot; /&gt;
&lt;div class=&quot;callout markdown-alert markdown-alert-callout&quot;&gt;
&lt;p&gt;This post is part of a series about building a chess-playing engine.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/chess0&quot;&gt;Introduction (0)&lt;/a&gt;
| &lt;a href=&quot;/chess3&quot;&gt;← Last part (3)&lt;/a&gt;
| &lt;a href=&quot;/chess5&quot;&gt;Next part (5) →&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;In the &lt;a href=&quot;/chess2&quot;&gt;part 2&lt;/a&gt; of this series, I discussed how the negamax
algorithm can be used to construct a chess-playing program.
We saw that as negamax thinks more and more moves ahead,
it increases exponentially in runtime.
Because of that, we used a heuristic approach that limits how deep the algorithm calculates.
The advantage of this approach is that negamax can run in a reasonable timeframe, but the disadvantage is that it now no longer plays perfect chess.&lt;/p&gt;
&lt;p&gt;In the current state of the engine, it can think around 4 half-moves ahead;
anything deeper and the runtime grows exponentially to the point where it freezes.
However, many human chess players can calculate much further into the future than that,
so they can easily defeat the chess engine.
Meanwhile, modern, good chess engines can calculate lines dozens of moves deep.
Many of them actually use the exact same negamax algorithm shown earlier;
the difference between our bad engine and a good engine is how the negamax is optimized.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Search&lt;/strong&gt; is the process of traversing the gametree, and negamax is one way to do it.
Optimizing this part of the engine is how we can go from thinking 4 half-moves ahead, to thinking 30 half-moves ahead.
Search is one main component of chess engines; &lt;em&gt;evaluation&lt;/em&gt; is the other, and I&amp;#8217;ll discuss that in a future post.&lt;/p&gt;
&lt;p&gt;In this post, I&amp;#8217;ll explore some of the main optimizations that can make our engine&amp;#8217;s search more efficient,
and therefore make it play better chess.
Specifically, I&amp;#8217;ll be examining &lt;a href=&quot;https://www.chessprogramming.org/Alpha-Beta&quot;&gt;alpha-beta pruning&lt;/a&gt;,
&lt;a href=&quot;https://www.chessprogramming.org/Transposition_Table&quot;&gt;transposition tables&lt;/a&gt;,
&lt;a href=&quot;https://www.chessprogramming.org/Move_Ordering&quot;&gt;move ordering&lt;/a&gt;, and
&lt;a href=&quot;https://www.chessprogramming.org/Iterative_Deepening&quot;&gt;iterative deepening&lt;/a&gt;.
These are the optimizations that I think are the easiest to implement, but that give the biggest gains in performance.&lt;/p&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you&amp;#8217;re following along by writing your own chess engine, remember to &lt;em&gt;SPRT your changes,&lt;/em&gt; as I discuss &lt;a href=&quot;/chess3&quot;&gt;in the last post&lt;/a&gt;.
This will ensure that your optimizations actually improve the engine.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;alpha-beta-pruning&quot;&gt;Alpha-beta pruning&lt;/h2&gt;
&lt;p&gt;The main issue with our current negamax search is that it examines every possible move in every position,
which takes a long time.
What if we could reduce the number of nodes that it examines?
This is known as &lt;em&gt;pruning&lt;/em&gt; the game tree;
we are cutting branches, i.e. not examining some moves in some positions.&lt;/p&gt;
&lt;p&gt;Alpha-beta pruning is one strategy for pruning.
Its core idea is that &lt;strong&gt;once you realize a move is a mistake, you stop analyzing this line,&lt;/strong&gt;
and you think about other moves instead.&lt;/p&gt;
&lt;h3 id=&quot;an-example-situation&quot;&gt;An example situation&lt;/h3&gt;
&lt;p&gt;Here&amp;#8217;s a concrete example position, White to move:&lt;/p&gt;
&lt;iframe src=&quot;https://lichess.org/study/embed/86CNdSfx/MjqySCXG#last&quot; frameborder=0&gt;&lt;/iframe&gt;
&lt;p&gt;&lt;em&gt;(If you can&amp;#8217;t see the interactive Lichess study widget,
a picture of the position is provided below.)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Suppose our chess engine generates moves in this order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Rf1&lt;/li&gt;
&lt;li&gt;Rd2&lt;/li&gt;
&lt;li&gt;Rd8#&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I know that a human player would immediately see the checkmate,
but bear with me, because the computer has a different thought process.&lt;/p&gt;
&lt;p&gt;We can draw a game tree as follows:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess4/example_gametree.svg&quot; alt=&quot;A tree diagram representing a chess game.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This game tree is limited to two half-moves.
I&amp;#8217;ve marked the evaluations for each end position,
which are either material evaluations (0 because equal material)
or checkmate evaluations (M0).&lt;/p&gt;
&lt;p&gt;Here is how the engine evaluates the three moves:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The first move evaluated, &lt;strong&gt;Rf1&lt;/strong&gt;, leads to a drawish position
where both sides are equal in material.
Therefore, this move has an evaluation of 0.
So far, this is the best move.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Then, the engine will look at &lt;strong&gt;Rd2&lt;/strong&gt;.
This is a severe mistake, as it lets Black checkmate in one (Re1#).
Without alpha-beta pruning, the engine, once it sees the checkmate, will keep examining Black&amp;#8217;s other responses that don&amp;#8217;t checkmate,
like Re2 and a6.
In other words, the engine thinks, &amp;#8220;well, what if Black &lt;em&gt;doesn&amp;#8217;t&lt;/em&gt; do mate in one?&amp;#8221;
This is a waste of time;
a human in the same situation, knowing that Rd2 is a mistake,
would simply reject this candidate move, and think about other possibilities.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Finally, the engine sees that it can checkmate with &lt;strong&gt;Rd8#&lt;/strong&gt;;
this is the best move, and the final move it picks.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So, in this example, what we&amp;#8217;d want to prune are the moves Re2, a6, and so on that
we know aren&amp;#8217;t worth considering.
In a non-contrived example where the depth is higher,
these moves would all have their own subtrees, which the engine would fully examine.
Thus, pruning these moves creates big time savings;
alpha-beta is the mechanism used to perform this pruning.
(From my own experience, alpha-beta pruning noticeably improves the performance of the engine,
shortening the search time from a few seconds to less than a second.)&lt;/p&gt;
&lt;h3 id=&quot;alpha-and-beta&quot;&gt;Alpha and beta&lt;/h3&gt;
&lt;p&gt;To implement alpha-beta pruning,
we introduce two arguments to negamax&amp;#8217;s function signature:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;negamax(position, depth: int, alpha: int, beta: int)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function takes a node in the game tree,
and returns the score of the node, along with the best move.
Alpha and beta represent scores, in centipawns.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Alpha&lt;/em&gt; represents the &lt;strong&gt;best score obtained by the current player (us) in the current search.&lt;/strong&gt;
Meanwhile, &lt;em&gt;beta&lt;/em&gt; is the best score obtained by the &lt;strong&gt;other player (them)&lt;/strong&gt; in the current search.
The current player is the one whose turn it is in the current node.&lt;/p&gt;
&lt;p&gt;Every time negamax computes a score for a node,
alpha is updated to the score (if it is better).
Then, when negamax recurses, alpha and beta are switched and negated, because it&amp;#8217;s now the other player&amp;#8217;s turn.&lt;/p&gt;
&lt;p&gt;Pruning happens when the current player finds a move from this node that&amp;#8217;s &amp;#8220;too good&amp;#8221;,
therefore the opponent will avoid letting this situation happen in the first place.
Because of that, after finding that good move, we no longer examine other moves from this position.&lt;/p&gt;
&lt;p&gt;Specifically, a move that is &amp;#8220;too good&amp;#8221; is a move that gives a score better than beta.
This means that in some ancestor node above in the tree,
the opponent found a candidate move that had a score of beta.
Then, they examined another candidate move (which is a mistake) that leads to the current node.
We got a score that&amp;#8217;s better than beta (i.e. worse for the opponent).
Since the current node is only reached by a bad move, we do not need to analyze it any further.&lt;/p&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; When alpha-beta causes pruning, this is known as the node &lt;strong&gt;failing high&lt;/strong&gt;.
This means that the position will never happen, because it has a too &lt;em&gt;high&lt;/em&gt; score for us.
Since the score is better than beta, the pruning is also known as a &lt;strong&gt;beta cutoff&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Let&amp;#8217;s now quickly examine how negamax with alpha-beta pruning works
on the example position (see annotated game tree below).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rf1&lt;/strong&gt; gives a score of 0 for White:
no matter what both players do after Rf1, nobody has a material advantage.
Then, alpha (White&amp;#8217;s best score) is set to this score, α = 0.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Next, negamax tries &lt;strong&gt;Rd2&lt;/strong&gt;,
and now it is Black&amp;#8217;s turn.
Alpha now becomes beta, β = 0.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Black now responds with Re1#, giving a mate score (positive infinity for Black).
This score is above beta (+∞ ≥ 0), which means we have a cutoff.
Therefore, the rest of the moves, Re2, a6, and so on, are pruned and not considered at all.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words, from White&amp;#8217;s perspective, Rf1 results in a score of 0, and Rd2 results in a score of -∞.&lt;sup id=&quot;fnref1&quot;&gt;&lt;a href=&quot;#fn1&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;
The move Rd2 is clearly a mistake for White, so they will never play that move.
Therefore, we don&amp;#8217;t need to consider Re2, a6 and all the other replies Black can play to Rd2.&lt;/p&gt;
&lt;p&gt;Below, I&amp;#8217;ve annotated the game tree diagram.
The pruned moves have been grayed out and crossed with a red line.
Dotted arrows also show where the 0 value comes from for alpha then beta.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess4/example_gametree_pruned.svg&quot; alt=&quot;A tree diagram representing a chess game.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I intentionally did not go through every step of negamax here, like some other sites do;
I only wrote down the important values that caused the pruning.
Personally, I think stepping through the algorithm for each node is useful,
but it clutters a post like this.
Do remember that each node has its own alpha and beta, which is transmitted down the tree.&lt;/p&gt;
&lt;p&gt;Now, an implementation of alpha-beta in pseudo-code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def negamax(position, depth: int, alpha: int, beta: int) -&amp;#62; (Move, int):
    &quot;&quot;&quot;
    Return the best move and evaluation of a chess position.

    Will only think `depth` half-moves ahead.
    &quot;&quot;&quot;

    if depth == 0:
        return eval(position)

    possible_moves = generate_moves(position)

    if possible_moves.empty():
        # checkmate or stalemate
        return eval(position), None

    best_score = -infinity
    best_move = None

    for each move in possible_moves:
        updated_position = apply_move_to_position(position, move)
        opponent_move, opponent_score = negamax(updated_position, depth - 1, -beta, -alpha)
        our_score = negate_score(opponent_score)

        if our_score &amp;#62; best_score:
            best_score = our_score
            best_move = move

        if our_score &amp;#62;= beta:
            # Beta cut-off
            break

        alpha = max(alpha, our_score)

    return best_move, best_score
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Initially, alpha and beta should be set to negative infinity and positive infinity
(the worst scores for us and them respectively, from our perspective.).&lt;/p&gt;
&lt;h2 id=&quot;transposition-table&quot;&gt;Transposition table&lt;/h2&gt;
&lt;p&gt;A &lt;em&gt;transposition&lt;/em&gt; in chess is when you get to the same position
in a different way.
For example, look at this game tree from the starting position (&lt;a href=&quot;https://lichess.org/study/86CNdSfx/lpn4n0HN&quot;&gt;Lichess study&lt;/a&gt;):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess4/transposition.svg&quot; alt=&quot;A tree diagram representing a chess game.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Both branches have different moves,
but in the end, both reach the same outcome.
Currently, each time our engine gets to a node that is a transposition,
it does the entire negamax computation again.
This is wasteful, so we want to avoid re-computing the same values.&lt;/p&gt;
&lt;h3 id=&quot;hashing&quot;&gt;Hashing&lt;/h3&gt;
&lt;p&gt;Generally, the solution to avoid costly recomputation is caching, or memoization.
Essentially, once we compute the score for a position, we store it in a hash table.
(In our case, the hash table is usually a big array.)
Every time we encounter the same position in our search, we can reuse this cached score.&lt;/p&gt;
&lt;p&gt;Importantly though, how do we hash a chess position?
We want to be able to take a chess position and turn it into an integer array index,
so that every time we encounter the same position, we can reuse the score in the array.&lt;/p&gt;
&lt;p&gt;The common method to hash chess positions is &lt;a href=&quot;https://www.chessprogramming.org/Zobrist_Hashing&quot;&gt;Zobrist hashing&lt;/a&gt;.
I&amp;#8217;m not good enough at math to explain &lt;em&gt;why&lt;/em&gt; this is a good hashing method with low collision probability,
but I can explain how it works.&lt;/p&gt;
&lt;p&gt;First, randomly generate a bunch of 64-bit integers, known as keys. Generate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;one key for every combination of square (64 squares), piece type (6 types) and colors (2 colors),
for a total of 768 keys;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2**4&lt;/code&gt;, so 16 keys for every combination of castling rights. There are two colors, and each can have either kingside castling, queenside castling, both, or neither;&lt;/li&gt;
&lt;li&gt;8 keys for the current en-passant file;&lt;/li&gt;
&lt;li&gt;one key for every color, to mark the current turn.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Essentially, these keys all represent individual attributes of a chess position.
These are most of the fields in a FEN, except for the move counters.
Using these attributes (what pieces are on which squares, what castling rights does everyone have, if and where we can en-passant, whose turn it is),
we can uniquely identify a chess position.&lt;/p&gt;
&lt;p&gt;Then, to hash a chess position,
take all the keys that represent it,
then &lt;a href=&quot;https://en.m.wikipedia.org/wiki/Exclusive_or#Bitwise_operation&quot;&gt;bitwise XOR&lt;/a&gt; the keys.
This will produce a numeric, 64-bit integer hash.&lt;/p&gt;
&lt;p&gt;If two positions have a matching Zobrist hash, then chances are they&amp;#8217;re the same position.&lt;/p&gt;
&lt;h3 id=&quot;incremental-hashing&quot;&gt;Incremental hashing&lt;/h3&gt;
&lt;p&gt;The main advantage of Zobrist hashing
is that it can be incrementally updated.&lt;/p&gt;
&lt;p&gt;Let&amp;#8217;s say we move a White pawn from e2 to e4.
Then, to get the Zobrist hash for the new position,
we take the old hash, XOR the &lt;code&gt;(pawn, e2, White)&lt;/code&gt; key,
and XOR the &lt;code&gt;(pawn, e4, White)&lt;/code&gt; key.
This works because if you XOR twice by the same thing,
it cancels out and undoes the first operation.
Then, do the same XOR operations for the current turn &amp;#47; color keys.&lt;/p&gt;
&lt;p&gt;So, computing the new hash is only four operations,
which is good for performance.
Every single negamax call will have to compute a hash,
which is why we want to optimize this step as much as possible.&lt;/p&gt;
&lt;h3 id=&quot;indexing&quot;&gt;Indexing&lt;/h3&gt;
&lt;p&gt;Our hash table &amp;#47; cache can be implemented as a big array.
In C-style pseudo-code, it looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;TTableEntry hashTable[N];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;where N is the number of entries in our cache.
Ideally, we would have &lt;code&gt;2**64&lt;/code&gt; entries,
that is, for every possible Zobrist hash,
we store an entry.
However, if we had &lt;code&gt;2**64&lt;/code&gt; entries, each a byte,
that would be in total 18.4 exabytes in memory usage,
which is infeasible.&lt;/p&gt;
&lt;p&gt;Typically, hash tables are around the scale of 100 megabytes,
so &lt;code&gt;N = 10**8&lt;/code&gt; entries.&lt;/p&gt;
&lt;p&gt;Therefore, we need to find a way to convert a hash
into an index that is smaller than &lt;code&gt;10**8&lt;/code&gt;, or some reasonable number like that.&lt;/p&gt;
&lt;p&gt;To do this, we take the hash modulo N.
For example, if the hash was 223, but the array only has &lt;code&gt;N = 100&lt;/code&gt; entries,
then I would use the index &lt;code&gt;223 % 100&lt;/code&gt;, i.e. the index &lt;code&gt;23&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Better engines that strongly optimize performance don&amp;#8217;t explicitly use the modulo,
because this operation is relatively slow.
Instead, they set their hash table&amp;#8217;s size to some power of two.
Then, truncating the high bits off the hash is equivalent to the modulo operation.
For example, say &lt;code&gt;N = 8&lt;/code&gt;, and the hash is &lt;code&gt;0b110110&lt;/code&gt;.
Then, the top two bits would be truncated to give an index &lt;code&gt;0b0110&lt;/code&gt;.
This is equivalent to &lt;code&gt;0b110110 % 8&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Even better engines, like Stockfish, use arcane math magic to use fast CPU instructions,
while also allowing any N, not just powers of two. See &lt;a href=&quot;https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/&quot;&gt;Daniel Lemire&amp;#8217;s post&lt;/a&gt; about this technique.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 id=&quot;using-the-transposition-table&quot;&gt;Using the transposition table&lt;/h3&gt;
&lt;p&gt;Once we have the indexing scheme,
we can use it to cache our negamax results.&lt;/p&gt;
&lt;p&gt;Earlier, I used the type &lt;code&gt;TTableEntry&lt;/code&gt; for the individual array elements of the transposition table.
This could be a struct, and the important fields stored in the transposition table are usually:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the Zobrist hash of the position;&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;depth&lt;/code&gt; parameter of negamax;&lt;/li&gt;
&lt;li&gt;the score returned;&lt;/li&gt;
&lt;li&gt;the best move returned.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then, at the start of the negamax function, we attempt to use a cached result.
If there is a matching table entry, we check the depth of the result.
If the cached result&amp;#8217;s depth is greater or equal to the depth we want
(i.e. the cached result is accurate enough),
we may use the cached result directly.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;entry = hashTable[indexFrom(position.hash)]
if entry is not None and entry.hash == position.hash:
    if entry.depth &amp;#62;= depth:
        return entry.score, entry.best_move
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice how we compare the transposition table entry&amp;#8217;s hash to the position&amp;#8217;s hash.
Since we use modulo to get the index in the transposition table,
two hashes will often map to the same index.
Therefore, we need to compare the full 64-bit hash to be certain the
cache entry matches our current position.&lt;/p&gt;
&lt;p&gt;At the end of the negamax function, we save the result to the transposition table:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;hashTable[indexFrom(position.hash)] = (position.hash, depth, best_score, best_move)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, we&amp;#8217;re overwriting the hash entry at that index.
Possibly, there might already be an entry with this index,
which we would erase by doing this.
This is known as an &lt;em&gt;always replace&lt;/em&gt; replacement strategy.
There are &lt;a href=&quot;https://www.chessprogramming.org/Transposition_Table#Replacement_Strategies&quot;&gt;other replacement strategies&lt;/a&gt;,
but empirically, always replace is a simple and decent one.&lt;/p&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; When testing the transposition table, it may be useful to implement the &lt;a href=&quot;https://backscattering.de/chess/uci/#engine-info-hashfull&quot;&gt;&lt;code&gt;info hashfull&lt;/code&gt;&lt;/a&gt; statistic
in UCI. This represents, in permille (0 permille is 0%, and 1000 permille is 100%), how many entries in the hash table are used.
When &lt;code&gt;hashfull&lt;/code&gt; is high, that means entries are often being replaced.
Ideally, this number would be low, so that you aren&amp;#8217;t throwing away entries.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;move-ordering&quot;&gt;Move ordering&lt;/h2&gt;
&lt;p&gt;Move ordering is the order in which moves are evaluated by the engine in each position.
Optimizing move ordering is an easy, yet very effective approach for improving the performance of alpha-beta pruning.
We saw earlier how alpha beta pruning can easily cut out dozens of moves from the game tree,
like with Re1# in this diagram:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess4/prune.svg&quot; alt=&quot;A tree diagram representing a chess game.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;However, this relies on Re1# being the first move examined.
If Re1# was the last move, then the beta cutoff would only be triggered at the end,
and that would save no computation at all.&lt;/p&gt;
&lt;p&gt;Because of how alpha-beta pruning works,
finding a good move first can either immediately cause a beta cutoff,
or, otherwise, raise alpha so that later there is a beta cutoff.
Therefore, we want to examine good, or at least promising moves first,
and then examine bad moves.
This way, we maximize the chances of pruning the game tree and saving time.&lt;/p&gt;
&lt;p&gt;The problem with getting good move ordering is that computers have zero intuition about chess.
A hypothetical chess engine could evaluate a3 first from the starting position,
simply &lt;a href=&quot;https://xkcd.com/3045/&quot;&gt;because of alphabetical order&lt;/a&gt;,
even though it&amp;#8217;s not a great move.&lt;/p&gt;
&lt;p&gt;Generally, the strategy to teach the computer about move ordering
is to assign a score to each move based on some heuristics,
then sort the list of possible moves based on that.&lt;/p&gt;
&lt;h3 id=&quot;mvv-lva&quot;&gt;MVV-LVA&lt;/h3&gt;
&lt;p&gt;A good heuristic to order moves is &lt;a href=&quot;https://www.chessprogramming.org/MVV-LVA&quot;&gt;MVV-LVA&lt;/a&gt;, &lt;em&gt;most valuable victim, least valuable attacker&lt;/em&gt;.
Essentially, this heuristic assigns a score to each capture move, which is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(material value of captured piece) - (value of capturer piece)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example, capturing a queen with a pawn has a score of &lt;code&gt;9 - 1 = 8&lt;/code&gt;, so pretty good.
But capturing a pawn with a queen is &lt;code&gt;1 - 9 = -8&lt;/code&gt;, so not a good idea.
Thus, MVV-LVA provides some decent intuition to the chess engine about what makes a good capture.&lt;/p&gt;
&lt;h3 id=&quot;recapture-heuristic&quot;&gt;Recapture heuristic&lt;/h3&gt;
&lt;p&gt;In chess, trading pieces is an important part of the game.
For instance, in this position, White can trade its pieces on the d5 square to gain 700 centipawns of material:&lt;/p&gt;
&lt;iframe src=&quot;https://lichess.org/study/embed/86CNdSfx/6IIlb9uI&quot; frameborder=0&gt;&lt;/iframe&gt;
&lt;p&gt;When exchanging pieces, most of the time the best move is very obvious:
it&amp;#8217;s to recapture the piece on the square you are trading on.
The computer does not understand this, and it might generate completely irrelevant moves that lose material,
like Nd7?? in the above study.&lt;/p&gt;
&lt;p&gt;Therefore, the recapture heuristic says the engine should &lt;em&gt;recapture with the least valuable attacker&lt;/em&gt;.
This is practically always the best move in an exchange of pieces.
Since we have MVV-LVA, we already have a bonus for the least valuable attacker, so we just keep track of the square where a capture last happened,
then add a bonus to the move&amp;#8217;s score if it&amp;#8217;s a recapture on that square.&lt;/p&gt;
&lt;h2 id=&quot;iterative-deepening&quot;&gt;Iterative deepening&lt;/h2&gt;
&lt;p&gt;Iterative deepening is another strategy that helps with move ordering.
The key idea is that the best move you get running negamax at depth N
is probably the same move you&amp;#8217;ll get running negamax at depth N + 1.&lt;/p&gt;
&lt;p&gt;Because of that, we can run negamax at a lower depth,
then use the results from that search to help with move ordering
at a higher depth.
Practically speaking,
that means we run negamax at depth 1 to get a rough idea of what the best move is,
then run it again at depth 2 to get a better idea,
then depth 3, 4, 5, and so on.
Each iteration improves on the accuracy of the best move returned.&lt;/p&gt;
&lt;p&gt;In pseudo-code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;i = 1
while True:
    negamax(depth = i)
    i += 1

    if good enough:
        break
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Iterative deepening might seem like a waste of time because it re-runs negamax over and over again,
but in combination with the transposition table and alpha-beta pruning,
it is actually a big improvement.&lt;/p&gt;
&lt;p&gt;We can use the transposition table as a move ordering heuristic:
always consider the transposition table&amp;#8217;s best move (the &amp;#8220;hash move&amp;#8221;) first in negamax,
even when the entry&amp;#8217;s depth is lower than the search&amp;#8217;s depth.
When using iterative deepening, we&amp;#8217;ll re-run negamax on the same nodes multiple times,
and it will be able to use the hash move from prior iterations to guide the move ordering.&lt;/p&gt;
&lt;p&gt;Better move ordering creates more beta cutoffs,
and this completely offsets the time spent repeating negamax at lower depths.&lt;/p&gt;
&lt;h3 id=&quot;time-management&quot;&gt;Time management&lt;/h3&gt;
&lt;p&gt;Iterative deepening is good for move ordering,
but as a side note, it is also necessary for clock management.
When playing bullet (~1 minute) chess,
the chess engine doesn&amp;#8217;t have as much time to think as in classical (~1 hour) chess.
Therefore, depending on how much time the engine has left,
it must manage how deep it thinks.&lt;/p&gt;
&lt;p&gt;Typically, engines will use iterative deepening to
first perform low depth searches,
and if it has more time,
start higher depth searches.&lt;/p&gt;
&lt;p&gt;The way this is usually implemented is with two deadlines:
one soft, one hard.
If iterative deepening finishes a negamax search
and the time is past the soft deadline,
it breaks out of the loop.
Meanwhile, if the hard deadline is passed,
negamax immediately aborts,
even in the middle of the search.&lt;sup id=&quot;fnref2&quot;&gt;&lt;a href=&quot;#fn2&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In this post, I covered some of my favourite optimizations for chess engine search.
Unfortunately, because I didn&amp;#8217;t implement all these features in this order
in my own chess engine, I can&amp;#8217;t show an example game between the improved and original version.
Trust me though, they do make the engine play much better chess.&lt;/p&gt;
&lt;p&gt;Again, these are just some of the most simple to implement techniques;
if you wish to read more, there are a bunch of links in this post,
and you can also consult &lt;a href=&quot;https://www.chessprogramming.org/Search&quot;&gt;Search&lt;/a&gt; on the Chess Programming Wiki
for more inspiration.
Note that the wiki documents a wide variety of search strategies and optimizations,
many of which are rare and not necessarily worth implementing.&lt;/p&gt;
&lt;p&gt;At this point in the series, there are still some major issues with our chess engine.
Some of the most annoying ones are that it constantly does repetition draws,
makes obvious blunders,
and, worst of all,
according to my friend, it &amp;#8220;plays weird&amp;#8221;.
In the coming posts, I&amp;#8217;ll discuss strategies to fix all of these issues.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/chess5&quot;&gt;Next part →&lt;/a&gt;&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;&lt;em&gt;(Thanks to Lichess for the interactive studies and chessboard diagrams.)&lt;/em&gt;&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
&lt;hr/&gt;
&lt;ol&gt;

&lt;li id=&quot;fn1&quot;&gt;
&lt;p&gt;When there is a beta cutoff,
the score that causes the cutoff (+∞ for Black in the example) is a lower bound on the true score.
We never actually examine the pruned moves,
which may have a higher score.
From the opponent&amp;#8217;s perspective, the score (-∞ for White in the example) is an upper bound.&amp;#160;&lt;a href=&quot;#fnref1&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn2&quot;&gt;
&lt;p&gt;The hard deadline can be implemented with a check in negamax
that runs occasionally, e.g. every 65536 nodes searched,
to avoid a performance hit.
If the hard deadline is passed, the search is aborted.&amp;#160;&lt;a href=&quot;#fnref2&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;
&lt;/div&gt;</content>
		<link href="https://www.dogeystamp.com/chess4"/>
		<id>https://www.dogeystamp.com/chess4</id>
		<updated>2025-04-18T10:00:00Z</updated>
		<published>2025-04-18T10:00:00Z</published>
	</entry>
	<entry>
		<title>Chess engine, pt. 3: Elo, and rigorous SPRT testing</title>
		<content type="html">&lt;h1 id=&quot;chess-engine-pt.-3-elo-and-rigorous-sprt-testing&quot;&gt;Chess engine, pt. 3: Elo, and rigorous SPRT testing&lt;/h1&gt;
&lt;div class=&quot;creation-date&quot;&gt;
2025-03-15
&lt;/div&gt;
&lt;link rel=&quot;contents&quot; href=&quot;/chess0&quot; /&gt;
&lt;link rel=&quot;prev&quot; href=&quot;/chess2&quot; /&gt;
&lt;link rel=&quot;next&quot; href=&quot;/chess4&quot; /&gt;
&lt;div class=&quot;callout markdown-alert markdown-alert-callout&quot;&gt;
&lt;p&gt;This post is part of a series about building a chess-playing engine.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/chess0&quot;&gt;Introduction (0)&lt;/a&gt;
| &lt;a href=&quot;/chess2&quot;&gt;← Last part (2)&lt;/a&gt;
| &lt;a href=&quot;/chess4&quot;&gt;Next part (4) →&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;In the last part,
we went over the negamax algorithm,
which is the core of how a chess engine
can play good chess.&lt;/p&gt;
&lt;p&gt;Honestly though, I think the content in this post is the most important part of the series,
in terms of how essential it is to writing a good chess engine.
Minmax and negamax are really well-covered, even trite topics at this point.
But proper testing methodology is something many other websites won&amp;#8217;t teach you.
Despite the relative obscurity of this topic, you &lt;em&gt;need&lt;/em&gt; to test, and you should start as soon as possible.&lt;/p&gt;
&lt;p&gt;This post will talk about the &lt;strong&gt;Sequential Probability Ratio Test&lt;/strong&gt;&lt;sup id=&quot;fnref1&quot;&gt;&lt;a href=&quot;#fn1&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;, or &lt;strong&gt;SPRT&lt;/strong&gt;.
What is this test?
Essentially, it is a statistical technique that can help you determine if an improved version of your engine
is actually better than the old version.&lt;/p&gt;
&lt;p&gt;By always running an SPRT when you create an improved version,
you are certain that your engine is better,
and it&amp;#8217;s not a fluke.
Otherwise, you might be introducing changes that weaken your engine,
but because of bad testing methods, you&amp;#8217;ll think it&amp;#8217;s actually better.
And then, in the worst case scenario,
you&amp;#8217;ll have to tear apart then re-test every single piece of your code.&lt;/p&gt;
&lt;p&gt;So, learn good habits; do SPRT.&lt;/p&gt;
&lt;p&gt;In this post, I&amp;#8217;m not going to thoroughly explain all the details of the mathematics of SPRT
(since I don&amp;#8217;t have a solid background in statistics).
For our purposes, we just need to understand what it&amp;#8217;s doing, and its main results.&lt;/p&gt;
&lt;h2 id=&quot;elo-ratings&quot;&gt;Elo ratings&lt;/h2&gt;
&lt;p&gt;First, some prerequisite context.
How do we determine that a chess engine (or a version of the engine) is better than another?
We measure the performance of the engines by having them play matches against each other.
Usually, wins are assigned a score of 1 point, draws are 0.5 points, and a loss is 0 points.&lt;/p&gt;
&lt;p&gt;For example, if Engine A wins 3, draws 1, and loses 1 game against Engine B,
then Engine A has 3.5 points, and Engine B has 1.5 points.
In total in this match, 5 games were played, and these 5 points were distributed between the players.&lt;/p&gt;
&lt;p&gt;However, comparing performance based on win&amp;#47;draw&amp;#47;loss counts or points is a bit unwieldy.
For example, if Engine A and Engine B play each other and their score is 12.5 to 10.5,
while Engine B and Engine C get a score of 8.5 to 9.5,
which engine is the best?
Are you confused by all these numbers?&lt;/p&gt;
&lt;p&gt;If so, you should be happy to know that a certain chess player, Arpad Elo,
invented the &lt;a href=&quot;https://en.m.wikipedia.org/wiki/Elo_rating_system#cite_note-3&quot;&gt;&lt;strong&gt;Elo rating system&lt;/strong&gt;&lt;/a&gt; to make our life easier.
Using the point scores,
Elo&amp;#8217;s system lets you calculate a single rating number that quantifies a chess player&amp;#8217;s performance,
whether the player is human or machine.
(If you want to see these calculations, please consult &lt;a href=&quot;https://en.m.wikipedia.org/wiki/Elo_rating_system#Theory&quot;&gt;the relevant section&lt;/a&gt; on Wikipedia.)&lt;/p&gt;
&lt;p&gt;Elo rating lets you easily compare the performance of players.
If a player A has a higher rating than player B,
player A is expected, &lt;em&gt;on average&lt;/em&gt;, to perform better, i.e. score more points
against player B.
A bigger rating difference also predicts that there is a bigger skill difference,
i.e. one player will score a lot more points than the other.&lt;/p&gt;
&lt;p&gt;For instance, as of writing,
Magnus Carlsen has a FIDE rating of 2837.1,
and Hikaru Nakamura has a rating of 2799.4.
Generally, Magnus Carlsen is considered to be a better chess player.&lt;/p&gt;
&lt;p&gt;Note that Elo&amp;#8217;s performance predictions say what should &amp;#8220;probably&amp;#8221; happen;
it&amp;#8217;s always possible, but improbable, that a bad player wins against a better player.
Elo provides a framework that assigns probabilities for these outcomes.&lt;/p&gt;
&lt;p&gt;FIDE and many other chess organizations use an Elo rating system.&lt;sup id=&quot;fnref2&quot;&gt;&lt;a href=&quot;#fn2&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;
Generally, even between different player pools that both use the same Elo system,
ratings are not comparable.
Machines also have their own Elo ratings.
At the &lt;a href=&quot;http://www.computerchess.org/&quot;&gt;CCRL&lt;/a&gt; website,
you can see a leaderboard of some of the best chess engines,
as well as their rating.
As I said,
this is not really comparable to FIDE rating,
or any other rating.&lt;/p&gt;
&lt;h2 id=&quot;the-sprt&quot;&gt;The SPRT&lt;/h2&gt;
&lt;p&gt;Now that we know what Elo is,
let&amp;#8217;s return to the main topic of the &lt;a href=&quot;https://en.m.wikipedia.org/wiki/Sequential_probability_ratio_test&quot;&gt;sequential probabilility ratio test&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;what-is-an-sprt&quot;&gt;What is an SPRT?&lt;/h3&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This section is more mathematical in nature, and is not absolutely essential to using SPRT in engine development.
However, I wrote this because having a base level understanding of what you&amp;#8217;re doing is helpful.
Skip this if you&amp;#8217;re allergic to maths.
Practically, this process is handled entirely by existing tools, so you don&amp;#8217;t &lt;em&gt;need&lt;/em&gt; to understand it.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;The goal of an SPRT in chess is to accurately estimate the performance of an engine
versus another engine.
When testing, we run a series of games between the engines we want to test.
Let&amp;#8217;s say we want to test that engine A is better than engine B by some amount.
We have two possible hypotheses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Engine A is 0 Elo points better than engine B (no improvement).&lt;/li&gt;
&lt;li&gt;Engine A is 10 Elo points better than engine B (improvement).&lt;sup id=&quot;fnref3&quot;&gt;&lt;a href=&quot;#fn3&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These can be considered as &amp;#8220;models&amp;#8221; for the data.
The models give the probability of certain outcomes.
For example, let&amp;#8217;s say we ran 1000 games, and the outcomes of these games for Engine A are &lt;code&gt;WDLWWDWLLWLLWWWDD...&lt;/code&gt;.
That is, Engine A won, then drew, then lost, then won, won, and so on, for a total of 1000 games.&lt;sup id=&quot;fnref4&quot;&gt;&lt;a href=&quot;#fn4&quot; rel=&quot;footnote&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;
Here are some invented probabilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &amp;#8220;non improvement&amp;#8221; hypothesis could say &amp;#8220;there is a 1% chance of these outcomes happening.&amp;#8221;&lt;/li&gt;
&lt;li&gt;The &amp;#8220;improvement&amp;#8221; hypothesis could say &amp;#8220;there is a 3% chance of these outcomes happening.&amp;#8221;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These percentages are called the &lt;strong&gt;likelihood&lt;/strong&gt; of this outcome, under a given hypothesis.
If the likelihood for a given hypothesis is higher,
that means the hypothesis is better at explaining the results we got,
thus it seems like a better model.&lt;/p&gt;
&lt;p&gt;Let&amp;#8217;s define the &lt;strong&gt;likelihood ratio&lt;/strong&gt; &lt;code&gt;LR&lt;/code&gt; of our hypotheses as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LR = (likelihood with &quot;improvement&quot; model) &amp;#47; (likelihood with &quot;no improvement&quot; model)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, &lt;code&gt;LR&lt;/code&gt; is a value between &lt;code&gt;0&lt;/code&gt; and &lt;code&gt;infinity&lt;/code&gt;.
When it is lower than 1, that means our hypothesis of &amp;#8220;no improvement&amp;#8221; is a better explanation of our data.
When it is higher than 1, our hypothesis of &amp;#8220;improvement&amp;#8221; is a better explanation.&lt;/p&gt;
&lt;p&gt;Often, chess SPRT tools will convert this into a neat number using a logarithm.
We can define the &lt;strong&gt;log-likelihood ratio&lt;/strong&gt; as&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;log-likelihood-ratio = log(LR)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;so that if the improvement hypothesis is more likely, it will be positive,
and otherwise it will be negative.
This is the number you&amp;#8217;ll be seeing when you run an SPRT in practice.&lt;/p&gt;
&lt;p&gt;We set some bounds, &lt;code&gt;A&lt;/code&gt; and &lt;code&gt;B&lt;/code&gt;.
We set &lt;code&gt;A&lt;/code&gt; to be a negative number, and &lt;code&gt;B&lt;/code&gt; to a positive number.
If &lt;code&gt;log(LR) &amp;#60;= A&lt;/code&gt;, then we are mostly certain that the &amp;#8220;no improvement&amp;#8221; hypothesis is true.
If &lt;code&gt;log(LR) &amp;#62;= B&lt;/code&gt;, then we think &amp;#8220;improvement&amp;#8221; is the correct hypothesis.&lt;/p&gt;
&lt;p&gt;But what happens if &lt;code&gt;A &amp;#60; log(LR) &amp;#60; B&lt;/code&gt;?
That means we don&amp;#8217;t have enough data to convincingly pick either hypothesis over the other.
Remember: at this point we are running chess games between engine A and engine B,
and collecting the outcomes of the game.
So, in SPRT, if we don&amp;#8217;t have enough data,
we keep running more games, until we get enough data to prove either hypothesis.&lt;/p&gt;
&lt;p&gt;Once SPRT is confident of the hypothesis,
we stop playing the chess games.
Thus, we collect exactly enough samples to prove that one of our hypotheses is more likely than the other.&lt;/p&gt;
&lt;p&gt;Now, where do the parameters &lt;code&gt;A&lt;/code&gt; and &lt;code&gt;B&lt;/code&gt; come from?
There&amp;#8217;s a formula that takes two parameters, &lt;code&gt;α&lt;/code&gt; and &lt;code&gt;β&lt;/code&gt;,
and gives us values for &lt;code&gt;A&lt;/code&gt; and &lt;code&gt;B&lt;/code&gt;.
&lt;code&gt;α&lt;/code&gt; represents the probability of a false positive (i.e. a type I error),
and &lt;code&gt;β&lt;/code&gt; represents the probability of a false negative (i.e. a type II error).&lt;/p&gt;
&lt;p&gt;So if you configure it so that &lt;code&gt;α = 0.05&lt;/code&gt;,
there is a 5% chance that the SPRT will wrongly determine that the &amp;#8220;improvement&amp;#8221; hypothesis is true,
when it is actually false.&lt;/p&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; For more information about the mathematics of SPRT, please see these links:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://mattlapa.com/sprt/&quot;&gt;Matt Lapa &amp;#8211; The Sequential Probability Ratio Test&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.m.wikipedia.org/wiki/Sequential_probability_ratio_test&quot;&gt;Wikipedia &amp;#8211; Sequential probability ratio test&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A lot of the information in this section comes from these pages.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 id=&quot;practical-use-fastchess&quot;&gt;Practical use: fastchess&lt;/h3&gt;
&lt;p&gt;Alright, enough math.
How do we use the SPRT to test chess engines?&lt;/p&gt;
&lt;p&gt;As it turns out, other people have already developed tools that perform SPRT tests for us.
The one I use is &lt;a href=&quot;https://github.com/Disservin/fastchess&quot;&gt;fastchess&lt;/a&gt;.
Once you have an engine that supports UCI (i.e. it implements the essential UCI commands from &lt;a href=&quot;../chess1/&quot;&gt;part 1&lt;/a&gt; of this series),
you can plug it into fastchess, and it will take care of everything.&lt;/p&gt;
&lt;p&gt;First, you need to install fastchess and compile it using &lt;code&gt;make&lt;/code&gt; from source.
It is written in C++.&lt;/p&gt;
&lt;p&gt;Next, you need to download an opening book.
This is because if you play the same engine against the same other engine
1000 times,
the outcome will probably be the exact same every time.
Adding an opening book introduces some variation
by playing a few unique moves in the opening before the engine starts playing.
For my project, I used &lt;a href=&quot;https://github.com/official-stockfish/books/raw/refs/heads/master/8moves_v3.pgn.zip&quot;&gt;8moves_v3.pgn&lt;/a&gt; from stockfish.&lt;/p&gt;
&lt;p&gt;Then, you can start an engine chess tournament using fastchess:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fastchess \
    -engine cmd=engine_new name=&quot;The new engine&quot; \
    -engine cmd=engine_old name=&quot;The old engine&quot; \
    -pgnout file=&quot;games.pgn&quot; \
    -openings file=8moves_v3.pgn format=pgn order=random \
    -each tc=8+0.08 \
    -rounds 5000 -repeat \
    -concurrency 8 \
    -recover \
    -sprt elo0=0 elo1=10 alpha=0.05 beta=0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Flag by flag, this is what this command means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-engine&lt;/code&gt;: This is the path to the executables of the engines you want to test.
You can assign a human-readable name to each of them.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-pgnout&lt;/code&gt;: This is a &lt;a href=&quot;https://en.wikipedia.org/wiki/Portable_Game_Notation&quot;&gt;Portable Game Notation&lt;/a&gt; (PGN) output file,
where fastchess will export all the games played in the tournament.
&lt;ul&gt;
&lt;li&gt;Notably, the Lichess &lt;a href=&quot;https://lichess.org/analysis/&quot;&gt;analysis board&lt;/a&gt; lets you import these games for you to review them.&lt;/li&gt;
&lt;li&gt;You may also create a &lt;a href=&quot;https://lichess.org/study&quot;&gt;Lichess study&lt;/a&gt; and import multiple games at once.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-openings&lt;/code&gt;: This specifies the opening file mentioned above.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-each tc&lt;/code&gt;: The &amp;#8220;each&amp;#8221; sets an option for both engines. The &amp;#8220;tc&amp;#8221; is the time control.
This specifically means &amp;#8220;8 seconds, plus an increment of 0.08 seconds per move played.&amp;#8221;&lt;sup id=&quot;fnref5&quot;&gt;&lt;a href=&quot;#fn5&quot; rel=&quot;footnote&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-rounds&lt;/code&gt;: Play 5000 rounds at most. Each round uses one single opening.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-repeat&lt;/code&gt;: Each round, play two games (swap the players&amp;#8217; colors for the second game).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-concurrency&lt;/code&gt;: Play 8 games at once. The more cores you have on your computer, the faster you can perform an SPRT.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-recover&lt;/code&gt;: If an engine crashes, restart it.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-sprt&lt;/code&gt;: These set the parameters for SPRT. Alpha and beta are the false positive and false negative rates,
and elo0 and elo1 are the two hypotheses we want to test.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Fastchess has a decent man page and &lt;code&gt;--help&lt;/code&gt; page, so consult those for more information.&lt;/p&gt;
&lt;p&gt;One of the main pain points of using fastchess
is that you have to compile your engines repeatedly and deal with all the executable files.
Seemingly, many people (including me) accelerate this process by writing a shell script to automate it.
Here is &lt;a href=&quot;https://github.com/dogeystamp/chess_inator/blob/main/contrib/fast-chess-tag.sh&quot;&gt;my shell script&lt;/a&gt;.
Essentially, in my code repository, I can use &lt;code&gt;git tag&lt;/code&gt; to mark the old and new version with descriptive names.
Then, the script can take the tags, compile the respective versions, then run fastchess on them.&lt;/p&gt;
&lt;p&gt;Another important point is the time control.
Humans typically don&amp;#8217;t play 8+0.08,
because they can&amp;#8217;t really think that fast.
But in SPRT, you typically need thousands of games to achieve a statistically significant result.
Therefore, developers often use a short time control (called STC),
so that we can run these games in a practical time frame.
The opposite is a long time control (LTC), which is closer to human blitz chess.&lt;/p&gt;
&lt;p&gt;At this point in the series,
I still haven&amp;#8217;t covered time management yet,
so if you&amp;#8217;re writing an engine and you don&amp;#8217;t have that feature,
set its depth searched
to some low number such that it&amp;#8217;s fast enough to play a game in 8+0.08 time.&lt;/p&gt;
&lt;p&gt;The other parameter you might want to tune are the SPRT hypotheses.
&lt;code&gt;[0, 10]&lt;/code&gt; works for testing a relatively big improvement.
But if you are doing a non-regression test,
i.e. testing that engine A is &lt;em&gt;just as good as&lt;/em&gt; than engine B,
you should use &lt;code&gt;[-10, 0]&lt;/code&gt; instead.
For example, this could be useful if you do a refactor
and you want to make sure you did not accidentally weaken the engine&amp;#8217;s code.&lt;/p&gt;
&lt;p&gt;Also, if you&amp;#8217;re testing really minuscule changes that only slightly affect engine performance,
you might want to consider using a smaller value, like &lt;code&gt;[0, 5]&lt;/code&gt;.
See &lt;a href=&quot;https://www.chessprogramming.org/Sequential_Probability_Ratio_Test&quot;&gt;the Chess Programming Wiki article&lt;/a&gt; about SPRT for more examples.&lt;/p&gt;
&lt;h3 id=&quot;interpreting-the-output&quot;&gt;Interpreting the output&lt;/h3&gt;
&lt;p&gt;Once you&amp;#8217;re running fastchess,
you should start seeing the outcomes of the chess games.
Periodically, fastchess will also give a status report that looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--------------------------------------------------
Results of c_i nnue08a-512 (920bea1) vs c_i eval-saturating (9b3074e) (8+0.08, NULL, 500MB, 8moves_v3.pgn):
Elo: 107.54 +&amp;#47;- 127.62, nElo: 144.54 +&amp;#47;- 152.27
LOS: 96.86 %, DrawRatio: 50.00 %, PairsRatio: 4.00
Games: 20, Wins: 12, Losses: 6, Draws: 2, Points: 13.0 (65.00 %)
Ptnml(0-2): [0, 1, 5, 1, 3], WL&amp;#47;DD Ratio: inf
LLR: 0.17 (-2.25, 2.89) [0.00, 10.00]
--------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The main statistic you want to look at in this status report is the LLR (here, &lt;code&gt;0.17&lt;/code&gt;),
which is the log-likelihood ratio of SPRT,
as discussed above.
The &lt;code&gt;(-2.25, 2.89)&lt;/code&gt; represent the &lt;code&gt;A&lt;/code&gt; and &lt;code&gt;B&lt;/code&gt; bounds,
and &lt;code&gt;[0.00, 10.00]&lt;/code&gt; are the Elo hypotheses.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Never ever stop the SPRT prematurely.&lt;/strong&gt;
In this example, you want to wait for the LLR to either be above &lt;code&gt;2.89&lt;/code&gt;, or below &lt;code&gt;-2.25&lt;/code&gt;.
If you see something like 30 wins and 50 losses, and you think &amp;#8220;wow, this version must be worse, so I will stop the test&amp;#8221;,
that is wrong!
For all we know, the engine might just be having a bad day and isn&amp;#8217;t performing well.
The whole point of SPRT is that the log-likelihood ratio will tell you if the
engine is better or not, statistically speaking.&lt;/p&gt;
&lt;p&gt;Once the SPRT is confident about a hypothesis,
fastchess will automatically stop itself.
Here, for example, is the final outcome of a passed SPRT:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--------------------------------------------------
Results of c_i nnue08a-512 (920bea1) vs c_i eval-saturating (9b3074e) (8+0.08, NULL, 500MB, 8moves_v3.pgn):
Elo: 19.57 +&amp;#47;- 13.22, nElo: 22.46 +&amp;#47;- 15.13
LOS: 99.82 %, DrawRatio: 37.81 %, PairsRatio: 1.27
Games: 2026, Wins: 867, Losses: 753, Draws: 406, Points: 1070.0 (52.81 %)
Ptnml(0-2): [134, 143, 383, 181, 172], WL&amp;#47;DD Ratio: 8.34
LLR: 2.91 (-2.25, 2.89) [0.00, 10.00]
--------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, &lt;code&gt;2.91 &amp;#62; 2.89&lt;/code&gt;, so we are confident
that the version &lt;code&gt;nnue08a-512&lt;/code&gt; is more likely to be 10 Elo better than &lt;code&gt;eval-saturating&lt;/code&gt;, rather than 0 Elo.
A negative LLR would be the opposite.&lt;/p&gt;
&lt;p&gt;However, if your LLR is &lt;em&gt;between&lt;/em&gt; the bounds &lt;code&gt;(-2.25, 2.89)&lt;/code&gt;,
then that is &lt;em&gt;not a valid result!&lt;/em&gt;
You need to run the test for longer to get enough data to prove your hypothesis.&lt;/p&gt;
&lt;p&gt;If you do accidentally stop fastchess,
it automatically saves its state to a file called &lt;code&gt;config.json&lt;/code&gt;.
You can resume the SPRT by running&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ fastchess -config file=config.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and it will restore your existing data.&lt;/p&gt;
&lt;h3 id=&quot;game-pairs-pentanomial-results&quot;&gt;Game pairs &amp;#38; pentanomial results&lt;/h3&gt;
&lt;p&gt;Remember when I said that fastchess
will run two games per round?
These are called &lt;em&gt;game pairs&lt;/em&gt;,
and the two games use the same opening.
We use game pairs because every opening will give an advantage to a certain player.
Even the starting position in chess is unfair, because White moves first.
Therefore, for fairness reasons,
we need players to play both White and Black in the round.&lt;/p&gt;
&lt;p&gt;If we have engine A, and engine B,
then on the first game, A is White, B is Black.
Then on the second game, we swap it so B is White, A is Black.
Because of this symmetric nature,
we can say that the outcomes of individual games may be biased by the opening,
but the outcomes of game pairs are unbiased in that regard.&lt;/p&gt;
&lt;p&gt;These are the outcomes possible for the two games in a game pair:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Points&lt;/th&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;loss, loss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;loss, draw&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;WL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;win, loss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;draw, draw&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;WD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;win, draw&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;WW&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;win, win&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Since both &lt;code&gt;DD&lt;/code&gt; and &lt;code&gt;WL&lt;/code&gt; are 1 point, they are considered the same outcome.
Thus, we have 5 possible outcomes, giving a &lt;strong&gt;pentanomial model&lt;/strong&gt;.
In fastchess, the &lt;code&gt;Ptnml(0-2)&lt;/code&gt; statistic
counts the game pairs that had each outcome.
For example, if you see
&lt;code&gt;
Ptnml(0-2): [134, 143, 383, 181, 172]
&lt;/code&gt;
that means there were 134 &lt;code&gt;LL&lt;/code&gt;, 143 &lt;code&gt;LD&lt;/code&gt;, 383 &lt;code&gt;WL&lt;/code&gt; or &lt;code&gt;DD&lt;/code&gt;, and so on.&lt;/p&gt;
&lt;p&gt;Fastchess is faster than some other SPRT tools,
because the pentanomial model &lt;a href=&quot;https://www.chessprogramming.org/Sequential_Probability_Ratio_Test#:~:text=fast%2Dchess%20is%20recommended%20as%20it%20supports%20pentanomial%20statistics%2C%20which%20can%20make%20tests%20finish%20faster.&quot;&gt;will pick a hypothesis
faster&lt;/a&gt;
than using the trinomial (win, draw, loss only) model,
using the same data.&lt;/p&gt;
&lt;h2 id=&quot;making-your-own-elo-leaderboard&quot;&gt;Making your own Elo leaderboard&lt;/h2&gt;
&lt;p&gt;SPRT is the right tool for saying,
&amp;#8220;I am confident this version is better than that version.&amp;#8221;
However, if you want to rank a bunch of engines on a leaderboard,
the better tool is &lt;a href=&quot;https://github.com/michiguel/Ordo&quot;&gt;Ordo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Ordo takes the outcomes of chess games between any amount of players (human or engine),
and computes Elo ratings based on it.&lt;/p&gt;
&lt;p&gt;Once you build it from source,
here is an example usage:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ordo -o ratings.txt -- file1.pgn file2.pgn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-o ratings.txt&lt;/code&gt; means output to ratings.txt,
and &lt;code&gt;file1.pgn&lt;/code&gt;, &lt;code&gt;file2.pgn&lt;/code&gt; are the games
it should read from.
You can use a glob like &lt;code&gt;games&amp;#47;*.pgn&lt;/code&gt; to match all PGN files in a directory &lt;code&gt;games&amp;#47;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here are some ratings of different versions of my engine:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   # PLAYER                                :  RATING   POINTS  PLAYED   (%)
   1 c_i ttable-smaller4 (3060140)         :  1719.0   5897.0   11770    50
   2 c_i nnue08a-512 (920bea1)             :  1718.3   9428.0   18760    50
   3 c_i duplicate (920bea1)               :  1717.8   2478.0    4963    50
   4 c_i eval-saturating (9b3074e)         :  1698.8   3358.0    6804    49
   5 c_i ttable-smaller3 (a40e3b7)         :  1696.9   4636.5    9159    51
   6 c_i ttable-smaller2 (7123e9e)         :  1688.5   1510.0    2997    50
   7 c_i ttable-beta-cutoff (3baeda6)      :  1685.8   7544.0   15104    50
   8 c_i ttable-alpha-cutoff (a3649e4)     :  1685.5   2609.5    5223    50
   9 c_i ttable-smaller (bd477ba)          :  1674.2    689.0    1425    48
  10 c_i recap-heuristic2 (de55bab)        :  1654.8   1448.5    2839    51

  25 c_i lmr-new-new3 (cf52e13)            :  1501.8   4811.0    9626    50
  26 c_i lmr-new-new2 (3b748a1)            :  1499.3   4814.5    9614    50
  27 c_i no-frac (c0e36f5)                 :  1495.5  10259.5   19983    51
  28 c_i null-window-more2 (f5a17d8)       :  1484.4    749.5    1548    48
  29 c_i history-heuristic (dcff015)       :  1481.5    638.5    1330    48

 150 c_i nnue3-512 (4503401)               :  1012.4     11.5      28    41
 151 c_i contempt-disabled (caa3bc4)       :  1008.7     28.5      58    49
 152 c_i promote-extension2 (9b711bd)      :  1006.4     28.5      60    48
 153 c_i hash-opt-fix (79cfbef v)          :  1002.3     83.5     183    46
 154 c_i short-time2 (290a69a)             :  1000.5     28.0      60    47
 155 c_i promote-extension (359dd5a)       :  1000.0     41.0      88    47
 156 c_i contempt (3fe4b8c)                :   995.0     11.5      26    44
 157 c_i engine2 (97db55b)                 :   990.7    139.5     950    15
 158 c_i avoid-rep7 (e27e18e)              :   990.6    110.5     258    43
 159 c_i contempt3 (68d3d7e)               :   976.4     10.5      26    40
 160 c_i avoid-rep (db365c1)               :   972.4     13.5      32    42
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(Since there are many versions, I&amp;#8217;ve cut out many rows.)&lt;/p&gt;
&lt;p&gt;Ordo provides nice leaderboard-style
ratings for your engines.
So, once you test a lot,
you&amp;#8217;ll be able to see the progression.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If you are writing a chess engine,
run an SPRT on every change you make, with no exceptions.
If you don&amp;#8217;t, it&amp;#8217;s quite possible that you&amp;#8217;ll regret it.&lt;/p&gt;
&lt;p&gt;Before I move on to the next topic, here is some further reading about engine testing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://dannyhammer.github.io/engine-testing-guide/strength-testing.html&quot;&gt;Danny Hammer &amp;#8211; Proper Chess Engine Testing&lt;/a&gt;: This guide is more thorough, and outlines how to use &lt;a href=&quot;https://github.com/AndyGrant/OpenBench&quot;&gt;OpenBench&lt;/a&gt;, which allows you to runs tests on multiple computers.
It also goes over a way you can &lt;a href=&quot;https://dannyhammer.github.io/engine-testing-guide/determining-strength.html&quot;&gt;estimate your engine&amp;#8217;s CCRL rating&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Stockfish Docs &amp;#8211; &lt;a href=&quot;https://official-stockfish.github.io/docs/fishtest-wiki/Fishtest-Mathematics.html&quot;&gt;Fishtest Mathematics&lt;/a&gt; &amp;#38; &lt;a href=&quot;https://official-stockfish.github.io/docs/fishtest-wiki/Fishtest-FAQ.html&quot;&gt;Fishtest FAQ&lt;/a&gt;: Stockfish is the #1 engine in the world; these pages go over some details of how their advanced testing framework functions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the coming parts of this series,
I&amp;#8217;ll be going over the main improvements that you can make to a chess engine.
If you&amp;#8217;re following along by writing your own engine,
you should be able to implement these changes and,
using SPRT,
be confident that your engine is improving.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/chess4&quot;&gt;Next part →&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
&lt;hr/&gt;
&lt;ol&gt;

&lt;li id=&quot;fn1&quot;&gt;
&lt;p&gt;My apologies for writing &amp;#8220;SPR Test testing&amp;#8221; in the title.&amp;#160;&lt;a href=&quot;#fnref1&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn2&quot;&gt;
&lt;p&gt;A notable exception is Lichess, which &lt;a href=&quot;https://lichess.org/page/rating-systems&quot;&gt;uses the Glicko 2 rating system.&lt;/a&gt;&amp;#160;&lt;a href=&quot;#fnref2&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn3&quot;&gt;
&lt;p&gt;Internally, tools first convert Elo values to scores before performing the SPRT. See &lt;a href=&quot;https://github.com/Disservin/fastchess/blob/master/app/src/matchmaking/sprt/sprt.cpp&quot;&gt;fastchess&amp;#8217;s source code&lt;/a&gt; for an example.&amp;#160;&lt;a href=&quot;#fnref3&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn4&quot;&gt;
&lt;p&gt;In practice, SPRT isn&amp;#8217;t performed on the win&amp;#47;draw&amp;#47;loss outcomes of individual games in computer chess; we use the outcomes of game pairs to reduce bias.
See &lt;a href=&quot;#game-pairs-pentanomial-results&quot;&gt;this section&lt;/a&gt; of this post.&amp;#160;&lt;a href=&quot;#fnref4&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn5&quot;&gt;
&lt;p&gt;I personally had to talk to the Engine Programming Discord members
to discover the 8+0.08 time control,
since it&amp;#8217;s not really documented in many places online.
Less than this, and your engine
could forfeit games on time, and more time takes too long for the SPRT to finish.&amp;#160;&lt;a href=&quot;#fnref5&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;
&lt;/div&gt;</content>
		<link href="https://www.dogeystamp.com/chess3"/>
		<id>https://www.dogeystamp.com/chess3</id>
		<updated>2025-03-15T10:00:00Z</updated>
		<published>2025-03-15T10:00:00Z</published>
	</entry>
	<entry>
		<title>Chess engine, pt. 2: Negamax search</title>
		<content type="html">&lt;h1 id=&quot;chess-engine-pt.-2-negamax-search&quot;&gt;Chess engine, pt. 2: Negamax search&lt;/h1&gt;
&lt;div class=&quot;creation-date&quot;&gt;
2025-03-07
&lt;/div&gt;
&lt;link rel=&quot;contents&quot; href=&quot;/chess0&quot; /&gt;
&lt;link rel=&quot;prev&quot; href=&quot;/chess1&quot; /&gt;
&lt;link rel=&quot;next&quot; href=&quot;/chess3&quot; /&gt;
&lt;div class=&quot;callout markdown-alert markdown-alert-callout&quot;&gt;
&lt;p&gt;This post is part of a series about building a chess-playing engine.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/chess0&quot;&gt;Introduction (0)&lt;/a&gt;
| &lt;a href=&quot;/chess1&quot;&gt;← Last part (1)&lt;/a&gt;
| &lt;a href=&quot;/chess3&quot;&gt;Next part (3) →&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;In the last post, I covered the UCI protocol,
and move generation.
Implementing these, it&amp;#8217;s possible to create a chess &amp;#8220;playing&amp;#8221;
engine that plays random legal moves.
Though it does play chess, this engine is very bad,
and it loses rapidly against players with any skill.&lt;/p&gt;
&lt;p&gt;So, how do we make our program actually play chess?
The classic answer is to use the minmax algorithm,
but in my engine specifically, I use negamax,
a common variation of minmax.&lt;/p&gt;
&lt;p&gt;Negamax is the core of many modern chess engines,
so in this post, I&amp;#8217;ll attempt to explain the algorithm
in a way that builds strong intuition
and understanding for how it works.&lt;/p&gt;
&lt;h2 id=&quot;the-negamax-algorithm&quot;&gt;The negamax algorithm&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Negamax&lt;/strong&gt; is an algorithm that finds the best move to play in any chess position.
In other words, it is the brain of the chess engine, and it is what lets it play good chess.&lt;/p&gt;
&lt;p&gt;What is the &amp;#8220;best move&amp;#8221; in chess, though?
Our engine&amp;#8217;s ultimate goal while playing chess
is to win, or failing that, get a draw.&lt;/p&gt;
&lt;h3 id=&quot;scoring-outcomes&quot;&gt;Scoring outcomes&lt;/h3&gt;
&lt;p&gt;Let&amp;#8217;s now specifically define what the outcomes &amp;#8220;win, lose, draw&amp;#8221; mean.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;We lose&lt;/strong&gt; when it is our turn and we are are in checkmate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;We win&lt;/strong&gt; when our opponent loses.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;draw&lt;/strong&gt; is when the fifty-move rule or threefold repetition rule are invoked, or there is a stalemate.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For our purposes, there are no other outcomes to the chess game.&lt;/p&gt;
&lt;p&gt;Notice how I don&amp;#8217;t mention White or Black at all above.
Usually, chess engine think in terms of &lt;strong&gt;Us versus Them&lt;/strong&gt;.
&amp;#8220;We&amp;#8221; are the player to move, which can be either Black or White.&lt;/p&gt;
&lt;p&gt;Also, the outcome of a game depends on your perspective.
If we win, they lose, and vice versa.
The specific terminology is that chess is a &lt;strong&gt;zero-sum game&lt;/strong&gt;.
First, we assign numerical scores to win (&lt;code&gt;1&lt;/code&gt;), draw (&lt;code&gt;0&lt;/code&gt;) and loss (&lt;code&gt;-1&lt;/code&gt;).
These scores are positive when the outcome is good, and negative if the outcome is bad.
Then, the scores for both players literally sum to zero:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;our score + their score = 0

W + L =  1  + -1  = 0
L + W = -1  +  1  = 0
D + D =  0  +  0  = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By doing some algebra on this formula, we find out that&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;our score = -(their score).
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In other words, we can negate a player&amp;#8217;s score to get the other player&amp;#8217;s score.
This will be important later, and is the &amp;#8220;nega&amp;#8221; of negamax.&lt;/p&gt;
&lt;h3 id=&quot;the-game-tree&quot;&gt;The game tree&lt;/h3&gt;
&lt;p&gt;We may imagine the game of chess as a tree structure, like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess2/negamax1.svg&quot; alt=&quot;A tree diagram representing a chess game.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This picture represents all possible games of chess that can be played.
The nodes (the circles) represent &lt;strong&gt;positions&lt;/strong&gt;,
and the edges (the arrows between circles) represent &lt;strong&gt;moves&lt;/strong&gt;, which bring the game
from one position to another.&lt;/p&gt;
&lt;p&gt;The starting position is the node at the very top of the tree,
and all possible games branch out from it.&lt;/p&gt;
&lt;p&gt;The leaves (the nodes at the bottom) of the tree,
each represent the end of a game of chess.
There are practically infinite leaf nodes, but I only drew three of them.&lt;/p&gt;
&lt;p&gt;In the leaf nodes, I also wrote the score of the game, from the perspective of the side whose turn it is.
Notice how there are no &lt;code&gt;1&lt;/code&gt; scores,
since games can only end in either draw, or checkmate (which is a loss for the current player).&lt;/p&gt;
&lt;h3 id=&quot;trivial-cases&quot;&gt;Trivial cases&lt;/h3&gt;
&lt;p&gt;Recall that the goal of the engine is to find the best move in each position.
Let&amp;#8217;s now examine some simple situations to see what engine should do in each case.
From these examples, we&amp;#8217;ll be able to build an algorithm that works generally in all situations.&lt;/p&gt;
&lt;p&gt;First, say we&amp;#8217;re White, and we have to choose between two moves in the diagram below:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the move on the left, which delivers checkmate (i.e. we win);&lt;/li&gt;
&lt;li&gt;the move on the right, which lets Black checkmate us (i.e. we lose).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&amp;#8217;s call this position &lt;code&gt;A&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess2/negamax2.svg&quot; alt=&quot;A tree diagram representing a chess game.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;What&amp;#8217;s the best move?
Of course, the move on the left that makes us win, and not the one that makes us lose.
Then, practically speaking, if we get to position &lt;code&gt;A&lt;/code&gt;, that is a guaranteed win for White.&lt;/p&gt;
&lt;p&gt;Next, say we&amp;#8217;re White, and we have two moves to choose from. Both moves let Black checkmate us.
Let&amp;#8217;s call this position &lt;code&gt;B&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess2/negamax3.svg&quot; alt=&quot;A tree diagram representing a chess game.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In this situation, both moves are the best,
since they both have the same outcome (i.e. we lose).
Therefore, if we get to position &lt;code&gt;B&lt;/code&gt;, White is guaranteed to lose.&lt;/p&gt;
&lt;h3 id=&quot;more-complex-cases&quot;&gt;More complex cases&lt;/h3&gt;
&lt;p&gt;So far, the positions we&amp;#8217;ve looked at have been pretty simple.
Let&amp;#8217;s invent a more complicated position.&lt;/p&gt;
&lt;p&gt;Say we&amp;#8217;re Black, and we have the choice between two moves:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;one leads to position &lt;code&gt;A&lt;/code&gt; (a guaranteed win for White);&lt;/li&gt;
&lt;li&gt;one leads to position &lt;code&gt;B&lt;/code&gt; (a guaranteed loss for White).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&amp;#8217;s call this position &lt;code&gt;C&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess2/negamax4.svg&quot; alt=&quot;A tree diagram representing a chess game.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now, what is our best move?
Earlier, we saw that for White, &lt;code&gt;A&lt;/code&gt; is a win, and &lt;code&gt;B&lt;/code&gt; is a loss.
But for Black, it&amp;#8217;s the opposite: &lt;code&gt;A&lt;/code&gt; is a loss, and &lt;code&gt;B&lt;/code&gt; is a win.
Because of that, Black&amp;#8217;s best move is the one that goes to &lt;code&gt;B&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Therefore, if we get to position &lt;code&gt;C&lt;/code&gt;, Black is guaranteed to win.&lt;/p&gt;
&lt;p&gt;Eventually, we have the general case.
Say we have the position &lt;code&gt;X&lt;/code&gt;, where we have to choose between multiple moves,
and they lead to positions &lt;code&gt;Y1&lt;/code&gt;, &lt;code&gt;Y2&lt;/code&gt;, and so on.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess2/negamax5.svg&quot; alt=&quot;A tree diagram representing a chess game.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;What is the best move?
&lt;strong&gt;The best move is the one that leads to the position with the best outcome for us&lt;/strong&gt; (i.e., the best outcome for the player to move).
For example, if the position &lt;code&gt;Y1&lt;/code&gt; is a guaranteed win for us, we pick the move that leads to &lt;code&gt;Y1&lt;/code&gt;.
If there is no next position that is a guaranteed win,
we pick the guaranteed draw.&lt;/p&gt;
&lt;p&gt;Now, how do we know the outcome of the &lt;code&gt;Y&lt;/code&gt; positions?
Using a similar process for determining that position &lt;code&gt;C&lt;/code&gt; above was a win for Black,
we can build the game tree up to determine the outcome of any position in chess,
including the &lt;code&gt;Y&lt;/code&gt; positions.&lt;/p&gt;
&lt;h3 id=&quot;the-actual-algorithm&quot;&gt;The actual algorithm&lt;/h3&gt;
&lt;p&gt;Let&amp;#8217;s now build concrete code (in pseudo-Python) for negamax, based on the above method for finding the best move.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;negamax&lt;/code&gt; function can be called on a node,
and it will return both the best move for us (the player to move),
and the numerical evaluation (i.e. the guaranteed outcome) for the position.&lt;/p&gt;
&lt;p&gt;The notable difference between this code and the examples above
is that this algorithm starts from a position, and thinks a few moves ahead,
while in the examples we started from the end of the game,
and worked backwards.
(I think working backwards is easier to understand,
as it&amp;#8217;s more obvious why the best moves are the best.)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def negamax(position) -&amp;#62; (Move, int):
    &quot;&quot;&quot;Return the best move and evaluation of a chess position.&quot;&quot;&quot;

    if position.is_checkmate():
        return None, -1
    else if position.is_drawn():
        return None, 0

    possible_moves = generate_moves(position)

    best_score = -infinity
    best_move = None

    for each move in possible_moves:
        # note: in real code, consider using make&amp;#47;unmake instead
        updated_position = apply_move_to_position(position, move)

        opponent_move, opponent_score = negamax(updated_position)
        our_score = -opponent_score

        if our_score &amp;#62; best_score:
            best_score = our_score
            best_move = move

    return best_move, best_score
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here are the rough steps of the code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Look at all the moves we can make.&lt;/li&gt;
&lt;li&gt;Find the outcome of each resulting position by recursively calling the negamax function.&lt;/li&gt;
&lt;li&gt;This outcome is from &lt;em&gt;their&lt;/em&gt; perspective, so negate it to get the score from &lt;em&gt;our&lt;/em&gt; perspective.&lt;/li&gt;
&lt;li&gt;Pick the move that resulted in the best score.&lt;/li&gt;
&lt;li&gt;The best score is the guaranteed outcome of this position.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;optimal-play&quot;&gt;Optimal play&lt;/h3&gt;
&lt;p&gt;With just these lines of code, we&amp;#8217;ve (technically) solved the game of chess.
Why?
All games of chess are finite because of the fifty-move rule,
so the function must terminate in finite time.
We can therefore call our &lt;code&gt;negamax&lt;/code&gt; function on the starting position,
and it &lt;em&gt;must&lt;/em&gt; tell us that either:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;White has a guaranteed win;&lt;/li&gt;
&lt;li&gt;Black has a guaranteed win; or&lt;/li&gt;
&lt;li&gt;White has a guaranteed draw.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Not only that, it will also tell us the best opening move for White.&lt;/p&gt;
&lt;p&gt;The consequence of this is that if two geniuses played chess optimally,
the outcome would be the same every time.
Therefore, as &lt;a href=&quot;http://archive.computerhistory.org/projects/chess/related_materials/text/2-0%20and%202-1.Programming_a_computer_for_playing_chess.shannon/2-0%20and%202-1.Programming_a_computer_for_playing_chess.shannon.062303002.pdf&quot;&gt;Claude Shannon wrote&lt;/a&gt; in 1949,
if such &amp;#8220;mental giants&amp;#8221; were to start a game,
one of them would immediately resign,
or they would agree to a draw.&lt;/p&gt;
&lt;p&gt;In this (unfortunately fake) video, grandmaster &lt;a href=&quot;https://en.m.wikipedia.org/wiki/Maxime_Vachier-Lagrave&quot;&gt;Maxime Vachier-Lagrave&lt;/a&gt; shows that in this scenario, the starting position in chess simply becomes a chess puzzle.&lt;/p&gt;
&lt;p&gt;&lt;video width=&quot;426&quot; height=&quot;240&quot; controls&gt;
  &lt;source src=&quot;/public/img/chess2/puzzle.mp4&quot; type=&quot;video/mp4&quot;&gt;
Your browser does not support the video tag.
&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;Now, we know that in real life, no human chess player can play optimally.
In fact, not even chess engines play at the level of our negamax algorithm.
Theoretically, this code would play perfect chess.
But practically, it would take forever to run.&lt;/p&gt;
&lt;p&gt;If we use really generous numbers,
let&amp;#8217;s pretend that in any chess position there are two legal moves,
and that there are invariably 80 half-moves in a chess game.
Also pretend that the above algorithm requires one picosecond
to analyze one unique game.&lt;/p&gt;
&lt;p&gt;Then in that case, it would take &lt;code&gt;1e-12 * 2**80&lt;/code&gt; seconds, which is around 38000 years to run.
Of course, this is infeasible, so we must take some shortcuts with our algorithm.&lt;/p&gt;
&lt;h2 id=&quot;a-heuristic-approach&quot;&gt;A heuristic approach&lt;/h2&gt;
&lt;p&gt;The main issue with using the theoretical negamax algorithm
is that as it looks more and more moves deeper down the game tree, it takes exponentially more time to run.
An obvious way to make the algorithm practically useful
is to limit the depth of recursion.
That is, instead of examining the entire game tree down to the leaves,
only examine a few layers of it.&lt;/p&gt;
&lt;p&gt;Once we recurse up to a limit of &lt;code&gt;n&lt;/code&gt; times,
we&amp;#8217;ll stop recursing.
Then, negamax will &lt;em&gt;guess&lt;/em&gt; the outcome (&lt;code&gt;1&lt;/code&gt;, &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;-1&lt;/code&gt;) of a position,
instead of properly calculating it.
The disadvantage is that we&amp;#8217;re no longer guaranteed the outcome,
but now our negamax will run in a practical timeframe.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;evaluation function&lt;/strong&gt; is the part that does this guessing.
A chess engine&amp;#8217;s evaluation function takes a chess position, and returns a numerical score for it.
Traditionally, this score will be a single number.
A bigger (positive) number means the position is probably a &lt;code&gt;1&lt;/code&gt;,
while a lower (negative) number means the position is probably &lt;code&gt;-1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In this heuristic approach, we are replacing the theoretical scores &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;-1&lt;/code&gt; by the score given by the evaluation function.
If our evaluation function is accurate at guessing the outcome of a game,
negamax will find moves leading to positions with higher score, thus positions where the engine will probably win the game.&lt;/p&gt;
&lt;h3 id=&quot;material-counting&quot;&gt;Material counting&lt;/h3&gt;
&lt;p&gt;Now, how do we make an evaluation function?
How do we guess the outcome of a chess game?&lt;/p&gt;
&lt;p&gt;It turns out that most chess players already have a decent sense of how to evaluate a position.
Take a look at this position. Who do you think will win?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess2/winning.gif&quot; alt=&quot;A chess position diagram. White has an extra queen and knight. The FEN for this position is 1R6/2Q3bk/3rN1p1/3Bp2p/4Pp1P/5P2/6PK/8 b - - 1 76&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The answer is that White is winning.
Why?
One obvious hint is that White has an extra queen,
and an extra knight.
Having more pieces, or &lt;em&gt;material&lt;/em&gt;, in chess is a big advantage,
and will help you win.
Let&amp;#8217;s quantify this advantage now.
When learning chess, many players are taught these &lt;a href=&quot;https://en.m.wikipedia.org/wiki/Chess_piece_relative_value#Standard_valuations&quot;&gt;relative numerical values for each piece&lt;/a&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pawn&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bishop&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knight&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rook&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queen&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This rule of thumb helps players determine whether or not to sacrifice their pieces.
For instance, you should sacrifice your rook, if it allows you to take the opponent&amp;#8217;s queen.
This is because the rook is less valuable (&lt;code&gt;5&lt;/code&gt;) than the opponent&amp;#8217;s queen (&lt;code&gt;9&lt;/code&gt;),
so in the exchange, you would gain &lt;code&gt;9 - 5 = 4&lt;/code&gt; points of material.&lt;/p&gt;
&lt;p&gt;Most of the time, counting the material value in a chess position is a really decent indicator of who will win,
regardless of what level of chess you are at.
Even the best players are less likely to win if they are at a material disadvantage.
Thus, let&amp;#8217;s define our evaluation function based on material value:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;eval(position) = (sum of our piece values) - (sum of opponent&amp;#39;s piece values)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On the board in the picture above,
this would be&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;White material value:
5 + 9 + 3 + 3 + 1 + 1 + 1 + 1 = 24

Black material value:
5 + 3 + 1 + 1 + 1 + 1 = 12

Difference:
12 - 24 = -12 (score for Black)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since it&amp;#8217;s Black&amp;#8217;s turn, we calculate that Black has a 12 point disadvantage.&lt;/p&gt;
&lt;p&gt;Instead of using &amp;#8220;material points&amp;#8221; as our unit though,
the traditional unit is the &lt;strong&gt;centipawn&lt;/strong&gt;.
As seen above, pieces are defined in terms of the value of a pawn (set to &lt;code&gt;1&lt;/code&gt;).
Our scores can be more precise if we set the pawn&amp;#8217;s value to &lt;code&gt;100&lt;/code&gt; centipawns instead.
A knight is &lt;code&gt;300&lt;/code&gt; centipawns, a rook &lt;code&gt;500&lt;/code&gt; centipawns, and so on.
In the above example, Black thus has a &lt;code&gt;-1200&lt;/code&gt; centipawn score.&lt;/p&gt;
&lt;p&gt;Note that usually, chess websites and programs will always &lt;em&gt;display&lt;/em&gt; scores from White&amp;#8217;s perspective,
i.e. positive for White, negative for Black.
So, on a Lichess analysis board, you would have a positive score here.
But in negamax, and engine development generally, we usually deal with scores that are positive for &amp;#8220;us&amp;#8221; (the side to move),
and negative for &amp;#8220;them&amp;#8221; (the opponent).&lt;/p&gt;
&lt;p&gt;Regardless, this is the centipawn scale that engine evaluations and eval bars operate with.
Typically, scores will be displayed as pawns.
For instance, a &lt;code&gt;-1200&lt;/code&gt; centipawn score for Black will be displayed as &lt;code&gt;+12.0&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;checkmate-scores&quot;&gt;Checkmate scores&lt;/h3&gt;
&lt;p&gt;Our evaluation function now works using the material advantage on the board.&lt;/p&gt;
&lt;p&gt;At a basic level of chess,
players want to earn material.
However, the true end goal is always to deliver checkmate to the opponent.
Therefore, our evaluation function scores also needs to take that into account.&lt;/p&gt;
&lt;p&gt;To do this, we introduce &lt;em&gt;mate scores&lt;/em&gt;.
If the player to move can deliver checkmate in one half-move,
then that is &lt;em&gt;mate in one&lt;/em&gt;, or &lt;code&gt;mate 1&lt;/code&gt;.
For the opposite player that is about to get mated,
this score is negative, so &lt;code&gt;mate -1&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; other engines, chess websites, and the UCI protocol sometimes use full moves for mate scores.
I think it&amp;#8217;s slightly easier to code with half-moves instead, so that is what I used in my project.
Divide by two to get a full move count.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;For example, in this position, the score is &lt;code&gt;mate 1&lt;/code&gt;, since
White can win with &lt;em&gt;Qh7#&lt;/em&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess2/winning2.gif&quot; alt=&quot;A chess position diagram. White can do checkmate in one move. The FEN for this position is 8/1RQ5/4Bbpk/4p2p/4Pp1P/5P2/6PK/8 w - - 3 79&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Let&amp;#8217;s also define, as a base case, that the score for a position in checkmate is &lt;code&gt;Mate(0)&lt;/code&gt;.
That is, if the player to move is in check and has no legal moves, that position&amp;#8217;s score is &lt;code&gt;Mate(0)&lt;/code&gt;.
If a player is about to be checkmated in one move (two half moves), that is instead &lt;code&gt;Mate(-2)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Quantitatively, we can imagine mate scores as big numbers.
This is because checkmate is literally a game-winning advantage,
so it is much more important than any material advantage.&lt;/p&gt;
&lt;p&gt;For example, we can assign&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Mate(1) = 1000000
Mate(-1) = -1000000.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&amp;#8217;s also make it so that mate in more half-moves is a worse score.
If all mate scores are the same,
the engine might start procrastinating the checkmate.
It&amp;#8217;ll always think &amp;#8220;well this move lets me checkmate in 2 moves anyways, why should I do checkmate in 1?&amp;#8221;,
and then before you know it the engine starts shuffling pieces back and forth and draws by repetition.
To avoid that, we could assign the values like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Mate(2) = 999999
Mate(-2) = -999999
Mate(3) = 999998
Mate(-3) = -999998

and so on...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using these values, our engine will:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;always prioritize avoiding checkmate or delivering checkmate over material advantage;&lt;/li&gt;
&lt;li&gt;make moves that get closer and closer to winning.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now, let&amp;#8217;s redefine our evaluation function, this time in pseudocode:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def eval(position):
    if position.is_checkmate():
        return Mate(0)
    else if position.is_stalemate():
        return 0
    else:
        return position.our_material_value() - position.their_material_value()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We also need to write logic to increment the &amp;#8220;mate in n&amp;#8221; counter in the negamax algorithm.
At every level of recursion, the &lt;code&gt;Mate(0)&lt;/code&gt; will become a &lt;code&gt;Mate(1)&lt;/code&gt;, then &lt;code&gt;Mate(-2)&lt;/code&gt;, &lt;code&gt;Mate(3)&lt;/code&gt; and so on.
I&amp;#8217;ll do this in the next section, with the final pseudocode for negamax in this post.&lt;/p&gt;
&lt;h3 id=&quot;heuristic-negamax-pseudocode&quot;&gt;Heuristic negamax pseudocode&lt;/h3&gt;
&lt;p&gt;Let&amp;#8217;s now rewrite our negamax algorithm to work with heuristic evaluation scores,
instead of game-theoretical scores.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def eval(position) -&amp;#62; Score:
    &quot;&quot;&quot;Evaluate a position.&quot;&quot;&quot;
    if position.is_checkmate():
        return Mate(0)
    else if position.is_stalemate():
        return 0
    else:
        return position.our_material_value() - position.their_material_value()


def negate_score(score) -&amp;#62; Score:
    &quot;&quot;&quot;Negate score, and for checkmate scores, increment mate in `n` counter.&quot;&quot;&quot;
    if score is Mate(n):
        if n &amp;#62; 0:
            return Mate(-(n + 1))
        else:
            return Mate(-n + 1)
    else:
        return -score


def negamax(position, depth: int) -&amp;#62; (Move, int):
    &quot;&quot;&quot;
    Return the best move and evaluation of a chess position.

    Will only think `depth` half-moves ahead.
    &quot;&quot;&quot;

    if depth == 0:
        return None, eval(position)

    possible_moves = generate_moves(position)

    if possible_moves.is_empty():
        return None, eval(position)

    best_score = -infinity
    best_move = None

    for each move in possible_moves:
        updated_position = apply_move_to_position(position, move)
        opponent_move, opponent_score = negamax(updated_position, depth - 1)
        our_score = negate_score(opponent_score)

        if our_score &amp;#62; best_score:
            best_score = our_score
            best_move = move

    return best_move, best_score
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When calling &lt;code&gt;negamax&lt;/code&gt; initially,
you should set some depth limit like &lt;code&gt;4&lt;/code&gt;.
This is not a lot,
but remember that negamax takes exponentially more time
for every extra depth level searched.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Now, here&amp;#8217;s a game between last part&amp;#8217;s version
(the one that plays the first move generated, without thinking)
and a version using the negamax algorithm from
this post.
White is the negamax, and Black is the random move.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess2/game.gif&quot; alt=&quot;Animated GIF of the chess game.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As you can see, negamax successfully captures a pawn,
then delivers checkmate.
Embarassingly,
even though this version only looks 5 half moves ahead,
and it has literally zero knowledge of chess other than the rules
and the value of material,
it can reliably beat me.&lt;/p&gt;
&lt;p&gt;In this post, we&amp;#8217;ve thus constructed a chess engine that
can convincingly play chess.
In the grand scheme of things, it&amp;#8217;s not that good;
any competent chess player would easily defeat this engine.&lt;/p&gt;
&lt;p&gt;There&amp;#8217;s lots of room for improvement right now.
However, in the next part, before I explain some of the main optimizations for negamax,
I&amp;#8217;ll go over the testing methodology used
to ensure that improvements actually improve the engine.
This may seem boring,
but if you are actually writing a chess engine,
testing rigorously is a really essential good habit.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/chess3&quot;&gt;Next part →&lt;/a&gt;&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;(Credit to Lichess for the chessboard diagrams.)&lt;/p&gt;</content>
		<link href="https://www.dogeystamp.com/chess2"/>
		<id>https://www.dogeystamp.com/chess2</id>
		<updated>2025-03-07T10:00:00Z</updated>
		<published>2025-03-07T10:00:00Z</published>
	</entry>
	<entry>
		<title>Chess engine, pt. 1: Getting started</title>
		<content type="html">&lt;h1 id=&quot;chess-engine-pt.-1-getting-started&quot;&gt;Chess engine, pt. 1: Getting started&lt;/h1&gt;
&lt;div class=&quot;creation-date&quot;&gt;
2025-03-06
&lt;/div&gt;
&lt;link rel=&quot;contents&quot; href=&quot;/chess0&quot; /&gt;
&lt;link rel=&quot;prev&quot; href=&quot;/chess0&quot; /&gt;
&lt;link rel=&quot;next&quot; href=&quot;/chess2&quot; /&gt;
&lt;div class=&quot;callout markdown-alert markdown-alert-callout&quot;&gt;
&lt;p&gt;This post is part of a series about building a chess-playing engine.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/chess0&quot;&gt;Introduction (0)&lt;/a&gt;
| &lt;a href=&quot;/chess2&quot;&gt;Next part (2) →&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;In this part of the series,
I go over all the information you need to
write a basic chess engine,
one that plays random moves.
Such an engine is the &amp;#8220;hello world&amp;#8221; of chess;
once you get it to run, adding more features is simple.
If you follow along through this entire post,
you will be able to play games of chess against your engine.
You could even connect your bot to Lichess, where it can play chess online.&lt;/p&gt;
&lt;h2 id=&quot;the-uci-protocol&quot;&gt;The UCI protocol&lt;/h2&gt;
&lt;p&gt;I&amp;#8217;ll start off by explaining the &lt;strong&gt;Universal Chess Interface&lt;/strong&gt;,
or &lt;strong&gt;UCI&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In computer science,
protocols are quite powerful.
Once your program implements an interface,
or a simple subset of it,
it suddenly has a common language to talk to other devices that also implement the protocol.
For example, take HTTP; you can implement a server using any language, any hardware.
Once you build something that talks HTTP, you can effortlessly expose it to
billions of devices on the Web.&lt;/p&gt;
&lt;p&gt;The equivalent standard protocol for chess engine programming is UCI.
In UCI, there are two parties involved:
the GUI, and the engine.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The GUI keeps track of the chessboard, takes move input from human players, and determines who wins the game.&lt;/li&gt;
&lt;li&gt;The engine plays chess, i.e. given a chess position, it finds the best move.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because the GUI handles a lot of tasks in UCI, your engine only needs to implement logic to play good moves.
You can use GUIs built by other people, like &lt;a href=&quot;https://github.com/cutechess/cutechess&quot;&gt;CuteChess&lt;/a&gt;
or &lt;a href=&quot;http://www.playwitharena.de/&quot;&gt;Arena&lt;/a&gt;,
which saves time and lets you focus on actually playing chess.
The most exciting part is that once you have UCI,
you can hook your engine up to &lt;a href=&quot;https://github.com/lichess-bot-devs/lichess-bot&quot;&gt;lichess-bot&lt;/a&gt;,
which lets it play rated chess online against &lt;a href=&quot;https://lichess.org/player/bots&quot;&gt;other bots&lt;/a&gt;,
and even human players on &lt;a href=&quot;https://lichess.org&quot;&gt;Lichess&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So now, let&amp;#8217;s go over the actual protocol.
Of course, I won&amp;#8217;t go over every single detail in UCI;
please see the &lt;a href=&quot;https://backscattering.de/chess/uci/&quot;&gt;UCI specification&lt;/a&gt; for more details.
We&amp;#8217;ll only look at a subset of UCI, but once an engine implements this, it can play chess through any compatible GUI.&lt;/p&gt;
&lt;p&gt;First off, UCI communication happens over a command-line through &lt;code&gt;stdin&lt;/code&gt; &amp;#47; &lt;code&gt;stdout&lt;/code&gt;.
For example, in Python, you could use &lt;code&gt;input()&lt;/code&gt;, and &lt;code&gt;print()&lt;/code&gt; in the engine to receive and send messages to the GUI.&lt;/p&gt;
&lt;p&gt;Here is an example UCI communication.
I&amp;#8217;ll prefix the engine&amp;#8217;s messages with &lt;code&gt;&amp;#62;&amp;#62;&amp;#62;&lt;/code&gt;, and the GUI&amp;#8217;s messages with &lt;code&gt;&amp;#60;&amp;#60;&amp;#60;&lt;/code&gt;
for demonstration purposes,
but remember that these aren&amp;#8217;t actually part of UCI.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#60;&amp;#60;&amp;#60; uci

&amp;#62;&amp;#62;&amp;#62; id name chess_inator
&amp;#62;&amp;#62;&amp;#62; id author dogeystamp
&amp;#62;&amp;#62;&amp;#62; option name Hash type spin default 16 min 1 max 6200
&amp;#62;&amp;#62;&amp;#62; uciok

&amp;#60;&amp;#60;&amp;#60; isready
&amp;#62;&amp;#62;&amp;#62; readyok

&amp;#60;&amp;#60;&amp;#60; ucinewgame

&amp;#60;&amp;#60;&amp;#60; position startpos moves e2e4
&amp;#60;&amp;#60;&amp;#60; go wtime 59780 winc 0 btime 60000 binc 0

&amp;#62;&amp;#62;&amp;#62; info score cp 43
&amp;#62;&amp;#62;&amp;#62; bestmove g8f6

&amp;#60;&amp;#60;&amp;#60; position startpos moves e2e4 g8f6 b1c3
&amp;#60;&amp;#60;&amp;#60; go wtime 59323 winc 0 btime 59123 binc 0

&amp;#62;&amp;#62;&amp;#62; info score cp 28
&amp;#62;&amp;#62;&amp;#62; bestmove b8c6

&amp;#60;&amp;#60;&amp;#60; quit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&amp;#8217;s break this down.&lt;/p&gt;
&lt;h3 id=&quot;uci&quot;&gt;&lt;code&gt;uci&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;First, right after the engine is started,
the GUI sends a &lt;code&gt;uci&lt;/code&gt;.
This command means &lt;em&gt;&amp;#8220;let&amp;#8217;s talk using UCI&amp;#8221;&lt;/em&gt;.
The engine must reply with an &lt;code&gt;uciok&lt;/code&gt;, which means &lt;em&gt;&amp;#8220;yup, I support UCI.&amp;#8221;&lt;/em&gt;
These messages exist because UCI isn&amp;#8217;t the only protocol out there;
others exist, like the &lt;a href=&quot;https://www.chessprogramming.org/Chess_Engine_Communication_Protocol&quot;&gt;Chess Engine Communication Protocol (CECP)&lt;/a&gt;.
Between &lt;code&gt;uci&lt;/code&gt; and &lt;code&gt;uciok&lt;/code&gt;, the engine may also send the &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;option&lt;/code&gt; messages,
which specify metadata about the engine, like its name, or the settings you can tweak.
Typically, in the GUI, options will be displayed as buttons, or sliders, or checkboxes, depending on the &lt;code&gt;type&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;isready&quot;&gt;&lt;code&gt;isready&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Then, there&amp;#8217;s an &lt;code&gt;isready&lt;/code&gt; from the GUI, following by a &lt;code&gt;readyok&lt;/code&gt; from the engine.
This command is sort of an &amp;#8220;are you still alive?&amp;#8221; check from the GUI.
If the chess engine is busy with initialization, it should send &lt;code&gt;readyok&lt;/code&gt; after finishing up,
i.e. when it is ready to play.&lt;/p&gt;
&lt;h3 id=&quot;ucinewgame&quot;&gt;&lt;code&gt;ucinewgame&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Next, the &lt;code&gt;ucinewgame&lt;/code&gt; from the GUI tells the engine to reset its state.
This makes it possible to play multiple games without completely rebooting the engine every time.&lt;/p&gt;
&lt;h3 id=&quot;position&quot;&gt;&lt;code&gt;position&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Now for the actual game.
In this example, we could assume White is a human player making moves on a chessboard GUI,
while the engine plays Black.
Here, the human plays the move &lt;em&gt;e4&lt;/em&gt;
(this is &lt;a href=&quot;https://en.m.wikipedia.org/wiki/Algebraic_notation_(chess)&quot;&gt;Standard Algebraic Notation&lt;/a&gt;, or &lt;strong&gt;SAN&lt;/strong&gt; for moves).
The GUI therefore sends this command to the engine:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#60;&amp;#60;&amp;#60; position startpos moves e2e4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, the GUI is giving the state of the chessboard,
i.e. &amp;#8220;from the starting position, White made the move &lt;em&gt;e4&lt;/em&gt;&amp;#8221;.
In UCI though, we don&amp;#8217;t use standard algebraic notation like &lt;em&gt;e4&lt;/em&gt; to denote moves.&lt;/p&gt;
&lt;p&gt;Instead, we use &lt;strong&gt;UCI long algebraic notation&lt;/strong&gt;, like &lt;code&gt;e2e4&lt;/code&gt; to denote moves.
This means &amp;#8220;the piece on &lt;em&gt;e2&lt;/em&gt; moved to &lt;em&gt;e4&lt;/em&gt;&amp;#8221;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For promotions, the piece being promoted to is added to the end,
for example &lt;code&gt;e7e8q&lt;/code&gt; is &amp;#8220;promote a pawn on &lt;em&gt;e7&lt;/em&gt; to queen&amp;#8221;, or &lt;code&gt;e7e8r&lt;/code&gt; is &amp;#8220;promote to rook&amp;#8221;.&lt;/li&gt;
&lt;li&gt;Castling is denoted as the king moving two squares in either direction,
for instance &lt;code&gt;e1g1&lt;/code&gt; is kingside castling, while &lt;code&gt;e1c1&lt;/code&gt; is queenside castling for White.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Long algebraic notation is easier for programs to deal with,
since it&amp;#8217;s more explicit about what piece is moving.&lt;/p&gt;
&lt;h3 id=&quot;go&quot;&gt;&lt;code&gt;go&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;go&lt;/code&gt; command is the most important one in UCI.
It tells the engine to start searching the for the best move in the current position (e.g. after White played &lt;em&gt;e4&lt;/em&gt;, the engine thinks of Black&amp;#8217;s best response).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#60;&amp;#60;&amp;#60; go wtime 59780 winc 0 btime 60000 binc 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;wtime&lt;/code&gt;, &lt;code&gt;winc&lt;/code&gt;, &lt;code&gt;btime&lt;/code&gt;, &lt;code&gt;binc&lt;/code&gt; parameters give information about how much time the engine has left to think.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wtime&lt;/code&gt; and &lt;code&gt;btime&lt;/code&gt; are the clocks for White and Black, given in milliseconds.
In the example, White has 59.78 seconds on the clock, while Black has 60 seconds.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;winc&lt;/code&gt;, &lt;code&gt;binc&lt;/code&gt; are the increments for the timer. In a &lt;code&gt;10+10&lt;/code&gt; time format game, each side would have a &lt;code&gt;10000&lt;/code&gt; increment,
meaning after every move they gain 10 seconds on the clock.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once the &lt;code&gt;go&lt;/code&gt; command is received, the engine must reply with a &lt;code&gt;bestmove&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#62;&amp;#62;&amp;#62; info score cp 43
&amp;#62;&amp;#62;&amp;#62; bestmove g8f6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is essentially the engine saying &amp;#8220;I play &lt;em&gt;Nf6&lt;/em&gt;.&amp;#8221;&lt;/p&gt;
&lt;p&gt;Your engine may also print &lt;code&gt;info&lt;/code&gt; lines.
Here, &lt;code&gt;score cp 43&lt;/code&gt; means the engine thinks this position is a &lt;code&gt;0.43&lt;/code&gt; pawn advantage for itself,
i.e. a slight advantage for Black.
The &lt;code&gt;cp&lt;/code&gt; stands for &amp;#8220;centipawns&amp;#8221;.
There are fields other than &lt;code&gt;score&lt;/code&gt; (see the &lt;a href=&quot;https://backscattering.de/chess/uci/&quot;&gt;UCI spec&lt;/a&gt;), and these would be printed on the same &lt;code&gt;info&lt;/code&gt; line.
Ideally, you would print multiple &lt;code&gt;info&lt;/code&gt; lines while the engine thinks
in order to get a sense of the engine&amp;#8217;s thought process,
but this is not necessary.&lt;/p&gt;
&lt;h3 id=&quot;quit&quot;&gt;&lt;code&gt;quit&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;quit&lt;/code&gt; command is self-explanatory;
it tells your engine to terminate the program.
(In C&amp;#47;C++ for example, this would be returning from &lt;code&gt;main&lt;/code&gt;.)
This is not to be confused with &lt;code&gt;stop&lt;/code&gt;,
that only stops an individual search.&lt;/p&gt;
&lt;h3 id=&quot;position-fen&quot;&gt;&lt;code&gt;position fen&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This command is not in the example, because I don&amp;#8217;t think it&amp;#8217;s strictly necessary for a functioning chess engine.
However, it&amp;#8217;s very useful, so I&amp;#8217;m including it in this list.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;position fen&lt;/code&gt; takes a &lt;em&gt;Forsyth-Edwards Notation&lt;/em&gt;, or &lt;strong&gt;FEN&lt;/strong&gt; code,
which is a string that represents a chess position.
FEN is used commonly; for example, in Lichess, the &lt;a href=&quot;https://lichess.org/analysis/pgn/&quot;&gt;analysis board&lt;/a&gt; always shows the current position&amp;#8217;s FEN.
You can then copy-paste the position into your engine, which is quite convenient.&lt;/p&gt;
&lt;p&gt;For example, after &lt;em&gt;1. e4&lt;/em&gt; from the starting position,
you would have:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;position fen rnbqkbnr&amp;#47;pppppppp&amp;#47;8&amp;#47;8&amp;#47;8&amp;#47;8&amp;#47;PPPPPPPP&amp;#47;RNBQKBNR w KQkq - 0 1 moves e2e4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For brevity&amp;#8217;s sake, I will not fully explain how FEN works; please read the &lt;a href=&quot;https://en.m.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation&quot;&gt;Wikipedia article&lt;/a&gt; about it.
However, I will give some extra tips.&lt;/p&gt;
&lt;p&gt;First, remember that &lt;em&gt;half-moves&lt;/em&gt; and &lt;em&gt;moves&lt;/em&gt; are different.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;half-move&lt;/strong&gt;, or &lt;strong&gt;ply&lt;/strong&gt; is a single movement of a piece, e.g. &lt;em&gt;e4&lt;/em&gt; is a half-move.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;full move&lt;/strong&gt;, or just &lt;strong&gt;move&lt;/strong&gt; is when White makes a half-move, then Black makes a half-move, e.g. &lt;em&gt;1. e4 e5&lt;/em&gt; is a full move.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This distinction is important, because FEN tracks full move and half move numbers.&lt;/p&gt;
&lt;p&gt;Another thing is that the en-passant square in the FEN is the &lt;em&gt;target&lt;/em&gt; square that can be captured.
For example, if White plays pawn to &lt;em&gt;e4&lt;/em&gt;, then the target square is &lt;em&gt;e3&lt;/em&gt;, the square the pawn crossed over.
Black could then play &lt;em&gt;cxe3&lt;/em&gt;, capturing the pawn en passant.&lt;/p&gt;
&lt;p&gt;FEN implementations differ on how they treat the en passant target square.
Some, like Lichess, only include the en passant square in the FEN if it would be legal to take en passant.
In the Wikipedia example, after pawn &lt;em&gt;e4&lt;/em&gt;, the en passant square is &lt;em&gt;e3&lt;/em&gt;;
Lichess would say &lt;a href=&quot;https://github.com/lichess-org/lila/issues/6827&quot;&gt;there is no en passant square&lt;/a&gt;, because Black&amp;#8217;s pawns are not in the right place to take en passant.&lt;/p&gt;
&lt;p&gt;When writing an engine, you may also notice that UCI GUIs usually send &lt;code&gt;position startpos moves ...&lt;/code&gt; instead of a &lt;code&gt;position fen ...&lt;/code&gt;.
This is because FEN is not actually able to fully represent a chess position.
Due to the &lt;a href=&quot;https://en.m.wikipedia.org/wiki/Threefold_repetition&quot;&gt;threefold repetition&lt;/a&gt; rule,
you also need the board history to accurately evaluate a position.
This is why a series of moves from the start position, which does encode the full history, is used instead.&lt;/p&gt;
&lt;h3 id=&quot;stop&quot;&gt;&lt;code&gt;stop&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Another command you should technically handle is
&lt;code&gt;stop&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;While the engine is thinking, if it receives a &lt;code&gt;stop&lt;/code&gt; command,
it should stop thinking and be ready to process other commands from the GUI.&lt;/p&gt;
&lt;p&gt;This is relatively difficult to implement,
since programs usually can&amp;#8217;t process user input while doing computations simultaneously.
Typically, chess engines will have two threads, one input thread, and one engine thread.
The input thread processes the UCI commands,
and the engine thread thinks.
At regular intervals (but not too often to avoid performance issues), the engine thread will check in
with the input thread to see if it has received a stop.
If so, the engine will abort its thinking.&lt;/p&gt;
&lt;h2 id=&quot;move-generation&quot;&gt;Move generation&lt;/h2&gt;
&lt;p&gt;Now that we know how to get the engine to talk to the GUI, 
we need to figure out the best moves to play.
To do this, our engine needs to figure out what moves &lt;em&gt;can&lt;/em&gt; be played legally.
This is known as &lt;em&gt;move generation&lt;/em&gt;.
As mentioned in the introduction section,
I assume that you know the rules of chess,
so I will not go over how each piece can move.&lt;/p&gt;
&lt;h3 id=&quot;board-representation&quot;&gt;Board representation&lt;/h3&gt;
&lt;p&gt;To generate moves, we must first have a board representation,
i.e. a data structure in code that represents a chessboard.
Two important ways of doing this are the &lt;em&gt;mailbox&lt;/em&gt; and &lt;em&gt;bitboard&lt;/em&gt; approaches.&lt;/p&gt;
&lt;p&gt;In a &lt;strong&gt;mailbox&lt;/strong&gt; board representation, the board is represented as a 2D grid, or array of 64 squares.
Each square has a value that indicates either no piece, or the color and type of the piece on it.
As I understand it, &amp;#8220;mailbox&amp;#8221; refers to the squares, and the pieces are the &amp;#8220;mail&amp;#8221; in the boxes.
Mailbox board representation is usually the first implementation that programmers think of, and it is the simplest.&lt;/p&gt;
&lt;p&gt;In a &lt;strong&gt;bitboard&lt;/strong&gt; representation, each piece type and color (e.g. black rook, white pawn, white queen), has a 64-bit binary value
that represents which squares it occupies, i.e. its &amp;#8220;occupancy&amp;#8221;.
For example, in the starting position, the white rook bitboard could be &lt;code&gt;0b10000001&lt;/code&gt;, which means there is a white rook on &lt;em&gt;a1&lt;/em&gt;,
and a white rook on &lt;em&gt;h1&lt;/em&gt;.
Each bit in the binary value represents one of the 64 squares of the chessboard; typically the least significant (rightmost) bit is &lt;em&gt;a1&lt;/em&gt;, and the most significant (leftmost) is &lt;em&gt;h8&lt;/em&gt;.
(See &lt;a href=&quot;https://pages.cs.wisc.edu/~psilord/blog/data/chess-pages/rep.html&quot;&gt;this page&lt;/a&gt; for a neat visualization and explanation of bitboards.)&lt;/p&gt;
&lt;p&gt;A way to distinguish bitboards and mailbox is to think of them as piece-centric and square-centric.
The bitboard answers the question &amp;#8220;where is this piece?&amp;#8221; while the mailbox answers the question &amp;#8220;what is on this square?&amp;#8221;.
Because of their complementary nature, it can be useful to use both representations despite their redundancy.&lt;/p&gt;
&lt;p&gt;The advantage of bitboards is that modern computers love dealing with 64-bit binary values,
and as such they can be optimized really really well.
However, algorithms using bitboards can be relatively complicated.
The mailbox representation&amp;#8217;s biggest advantage is that it&amp;#8217;s simple to understand,
because it maps well to our idea of what a chessboard is like.&lt;/p&gt;
&lt;p&gt;In my chess engine, I decided to mostly use mailbox board representation,
because it does a good enough job and it is simple to implement.&lt;/p&gt;
&lt;p&gt;I&amp;#8217;m skipping over many details about board representation above;
if you&amp;#8217;re curious, see the relevant &lt;a href=&quot;https://www.chessprogramming.org/Board_Representation&quot;&gt;CPW article&lt;/a&gt; on the topic.&lt;/p&gt;
&lt;h3 id=&quot;move-encoding&quot;&gt;Move encoding&lt;/h3&gt;
&lt;p&gt;After representing the chessboard, we also need to figure out how to represent moves.&lt;/p&gt;
&lt;p&gt;A &lt;a href=&quot;https://www.chessprogramming.org/Encoding_Moves&quot;&gt;typical representation&lt;/a&gt; in strong engines packs moves into 16 bits.
The source square takes 6 bits, and the destination square takes 6 bits,
leaving 4 bits to encode things like the piece being promoted to,
or any other information.
In chess-inator, efficiency is not the primary concern,
so moves are represented as a 24-bit Rust &lt;code&gt;struct&lt;/code&gt; containing the same information.&lt;/p&gt;
&lt;p&gt;Another important point is how moves are applied.&lt;/p&gt;
&lt;p&gt;At first, in chess-inator, I thought it would be conceptually neat
to implement chessboards as immutable states,
such that moves could take in a chessboard to produce a distinct, updated chessboard state.
The issue with this method is that copying entire chessboards around is painfully slow.&lt;/p&gt;
&lt;p&gt;A faster method is &lt;strong&gt;make-unmake&lt;/strong&gt;, where the engine has a single chessboard in memory.
When the engine calculates, it makes moves on this board,
then &amp;#8220;unmakes&amp;#8221; (undoes) the moves when it&amp;#8217;s done with that line of thought.&lt;/p&gt;
&lt;h3 id=&quot;move-legality&quot;&gt;Move legality&lt;/h3&gt;
&lt;p&gt;Coding the basic piece movement rules can be relatively simple.
For instance, the rook can move in any square along 4 directions,
but it is blocked by other pieces.
Similar logic applies for the queen and bishop.&lt;/p&gt;
&lt;p&gt;What complicates move generation is that in chess, players must not make moves that put their king in check.
Such moves are illegal, and can not be played.&lt;/p&gt;
&lt;p&gt;Engines therefore have to filter out illegal moves.
A simple way to do this, and the way chess-inator does it,
is to try every move, and determine if the king is in check.
If it is, then the move is illegal.&lt;/p&gt;
&lt;p&gt;A common method to determine if the king is in check is the
&amp;#8220;I see you, you see me&amp;#8221; method.
Let&amp;#8217;s say we want to detect checks from rooks.
Pretend the king can move like a rook.
If the king, with rook movement, could capture an enemy rook,
then that means the enemy rook can capture the king.&lt;/p&gt;
&lt;p&gt;This method works for all other pieces (be careful with pawns, since their moves aren&amp;#8217;t symmetric). It&amp;#8217;s also decently
efficient, since this method only needs to trace rays for 8 directions, and
check for knight attacks, which will always be less than 64 squares checked.
Still, my benchmarks show that check detection is one of the most expensive operations
in my engine, since it&amp;#8217;s called so often.&lt;/p&gt;
&lt;h3 id=&quot;testing-with-perft&quot;&gt;Testing with perft&lt;/h3&gt;
&lt;p&gt;Once you&amp;#8217;ve written a move generation algorithm,
you need to make sure it actually works.&lt;/p&gt;
&lt;p&gt;The canonical test for move generation is the &lt;a href=&quot;https://www.chessprogramming.org/Perft&quot;&gt;perft&lt;/a&gt; (performance test) function.
&lt;code&gt;perft(n)&lt;/code&gt; is the count of all unique chess &amp;#8220;games&amp;#8221; you can play, where each game lasts at most &lt;code&gt;n&lt;/code&gt; half-moves.
In other words, perft counts the leaf nodes of a legal game tree limited to some depth &lt;code&gt;n&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For instance, from the starting position,
&lt;code&gt;perft(1) = 20&lt;/code&gt;.
This is because there are 20 &amp;#8220;games&amp;#8221; of 1 half-move:
White has 16 pawn moves (8 pawns can be pushed one or two squares),
and 4 knight moves,
giving &lt;code&gt;16 + 4 = 20&lt;/code&gt;.
Then &lt;code&gt;perft(2) = 400&lt;/code&gt;, because for each of those 20 opening moves from White,
Black has 20 responses, giving &lt;code&gt;20 * 20 = 400&lt;/code&gt; unique games of two half-moves.&lt;/p&gt;
&lt;p&gt;Using your own move generation code,
you can write a perft function,
and generate your own numbers.
If your results are different from &lt;a href=&quot;https://www.chessprogramming.org/Perft_Results&quot;&gt;known numbers&lt;/a&gt;,
you can quickly deduce that there is a bug in the code.
&lt;em&gt;However, if your results are the same, that does not prove that there are no bugs.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A test suite I&amp;#8217;ve often seen recommended is Andy Grant&amp;#8217;s &lt;a href=&quot;https://github.com/AndyGrant/Ethereal/raw/refs/heads/master/src/perft/standard.epd&quot;&gt;&lt;code&gt;standard.epd&lt;/code&gt;&lt;/a&gt;,
which contains different positions, as well as their expected perft results for different depths.&lt;/p&gt;
&lt;h2 id=&quot;the-bare-minimum-engine&quot;&gt;The bare minimum engine&lt;/h2&gt;
&lt;p&gt;Now that we have both UCI, and move generation,
we have the tools to make the most basic engine,
the equivalent of a &amp;#8220;Hello World&amp;#8221;:
an engine that plays random moves.&lt;/p&gt;
&lt;p&gt;My engine chess-inator achieved this milestone
with commit &lt;a href=&quot;https://github.com/dogeystamp/chess_inator/commit/5751215ffa8f48a2b109c10f6c6a4dcb9debc3e2&quot;&gt;&lt;code&gt;5751215&lt;/code&gt;&lt;/a&gt;,
implementing an engine that plays the first move it generates.
This took a while to implement properly,
so finally putting the executable in the GUI and seeing my code play moves was pretty exciting.&lt;/p&gt;
&lt;p&gt;For reference, here&amp;#8217;s a game of me playing against this engine (credit to Lichess for the animation).
As you can see, it&amp;#8217;s not that great at chess, though nor am I:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../public/img/chess1/game.gif&quot; alt=&quot;An animated GIF of a chess game. &amp;quot;dogeystamp&amp;quot; is White, and &amp;quot;chess_inator&amp;quot; is Black.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;We&amp;#8217;re only getting started though; take a look at the next part of this series,
which talks about ways the engine can more intelligently select the next move.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/chess2&quot;&gt;Next part →&lt;/a&gt;&lt;/p&gt;</content>
		<link href="https://www.dogeystamp.com/chess1"/>
		<id>https://www.dogeystamp.com/chess1</id>
		<updated>2025-03-06T10:00:00Z</updated>
		<published>2025-03-06T10:00:00Z</published>
	</entry>
	<entry>
		<title>Chess engine, pt. 0: Index</title>
		<content type="html">&lt;h1 id=&quot;chess-engine-pt.-0-index&quot;&gt;Chess engine, pt. 0: Index&lt;/h1&gt;
&lt;div class=&quot;creation-date&quot;&gt;
2025-03-05
&lt;/div&gt;
&lt;link rel=&quot;next&quot; href=&quot;/chess1&quot; /&gt;
&lt;p&gt;Building chess engines,
that is, making computers play chess,
is seemingly a &lt;a href=&quot;https://youtube.com/watch?v=DpXy041BIlA&quot;&gt;common&lt;/a&gt;
&lt;a href=&quot;https://youtube.com/watch?v=U4ogK0MIzqk&quot;&gt;pastime&lt;/a&gt;
of programmers.
Of course, I had to try it too, so I created my own engine, &lt;a href=&quot;https://github.com/dogeystamp/chess_inator&quot;&gt;chess-inator&lt;/a&gt;, from scratch.
As it turns out, chess engine development is incredibly addicting (I say this from experience),
because there is always more room for improvement in your engine.
Now though, chess-inator can beat most chess players I know personally,
so I&amp;#8217;m stopping before I spend way too many hours on this project.&lt;/p&gt;
&lt;p&gt;This blog post series is essentially a log of chess-inator&amp;#8217;s development, including
important tips and information that I had difficulty finding from other online
sources.
Even though the series is mostly about my own engine,
I wrote these posts as if they were a tutorial, because I like that tone.
These posts are intended to focus on practical rather than theoretical aspects;
you should technically be able to write an engine by following along.&lt;/p&gt;
&lt;p&gt;The target audience of this blog series is
me from 2024,
that is a version of myself that doesn&amp;#8217;t know anything about chess engine development.
Ideally, this series contains all the essentials to get started with writing an engine
without any prior experience.
I assume you, the reader, are proficient with some programming language,&lt;sup id=&quot;fnref1&quot;&gt;&lt;a href=&quot;#fn1&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;
you&amp;#8217;ve played chess before, and you know the rules of the game (google en passant).
In this series, I use pseudo-Python for code examples, but my real chess engine project uses Rust for engine code.&lt;/p&gt;
&lt;p&gt;Before I begin, here are the mandatory acknowledgements.
Thanks to&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the &lt;a href=&quot;https://www.chessprogramming.org/Main_Page&quot;&gt;Chess Programming Wiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;the members of the &lt;a href=&quot;https://discord.com/invite/F6W6mMsTGN&quot;&gt;Engine Programming Discord&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;and all the other people who publish resources about chess engine development online&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;for providing valuable information about chess programming
and making this domain accessible to everyone.
This project would really not be possible without them.&lt;/p&gt;
&lt;h2 id=&quot;index&quot;&gt;Index&lt;/h2&gt;
&lt;p&gt;Here is an overview of all the posts in this series:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/chess1&quot;&gt;Part 1: Getting started&lt;/a&gt;: In this post, I cover enough for you to write a chess engine that plays random moves.
This is the base that the next parts will build upon.
If you follow along with this post, you will quickly be able to play games against the engine you create.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/chess2&quot;&gt;Part 2: Negamax search&lt;/a&gt;: I cover the &lt;em&gt;negamax&lt;/em&gt; search algorithm, which is a variation of minmax.
Once negamax is implemented, the chess engine will play moves more intelligently (i.e. not random moves).&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/chess3&quot;&gt;Part 3: Elo, and rigorous SPRT testing&lt;/a&gt;: I introduce the Sequential Probability Ratio Test,
which is a statistical tool that can be used to rigorously test a chess engine&amp;#8217;s performance.
SPRT is very important in modern chess engine development.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/chess4&quot;&gt;Part 4: α-β pruning and better search&lt;/a&gt;: I go over some of the main optimizations to negamax:
alpha-beta pruning, the transposition table, move ordering, and iterative deepening.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/chess5&quot;&gt;Part 5: Quiescence search, endgames, repetition avoidance&lt;/a&gt;: In this part, I explain how to
fix some really annoying bugs in the chess engine.
These bugs are inherent to how we implement negamax.
After this part, the engine plays chess at a beginner level, albeit with a quite unnatural (i.e. bad) playstyle.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/chess6&quot;&gt;Part 6: Neural-net evaluation&lt;/a&gt;: I explain how neural networks, specifically the NNUE, can be
used to improve a chess engine&amp;#8217;s performance.
This post goes over chess-inator&amp;#8217;s NNUE architecture in great detail.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some of my favourite posts in this series are &lt;a href=&quot;/chess3&quot;&gt;part 3 (SPRT)&lt;/a&gt; and &lt;a href=&quot;/chess6&quot;&gt;part 6 (NNUE)&lt;/a&gt;.
SPRT is something you should use as soon as possible when developing a chess engine,
yet I&amp;#8217;ve never heard of it before starting this project.
If I had to recommend one part of this blog series, it would be the part about SPRT.
It&amp;#8217;s not an exciting subject, but it is essential to making a good chess engine.&lt;/p&gt;
&lt;p&gt;I also had fun writing the NNUE post,
because I think there is a shortage of resources for beginners.
In my research, I found lots of general summaries that explain NNUE&amp;#8217;s general concept,
and also &lt;a href=&quot;https://github.com/official-stockfish/nnue-pytorch/blob/master/docs/nnue.md&quot;&gt;deep dives&lt;/a&gt;
about Stockfish&amp;#8217;s complex NNUE architecture.
However, resources for my skill level are rarer.
The aim in my NNUE post is to explain how a simple (and easy to understand) NNUE architecture works,
with enough detail for you to follow along with its implementation.&lt;/p&gt;
&lt;p&gt;In total, this blog series was written over a period of a few months,
which is unsurprising given that it is really long&lt;sup id=&quot;fnref2&quot;&gt;&lt;a href=&quot;#fn2&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; compared to my other writings.
I hope that some of you find will it useful.
If you find any mistakes in the posts (such as mixing up an &lt;code&gt;i&lt;/code&gt; and a &lt;code&gt;j&lt;/code&gt;, or a typo),
please &lt;a href=&quot;/about#contact-information&quot;&gt;tell me&lt;/a&gt;.
Feel free to write to me if you have any other comments.&lt;/p&gt;
&lt;p&gt;Now, let&amp;#8217;s begin: &lt;a href=&quot;/chess1&quot;&gt;Part 1 →&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;notecard note markdown-alert markdown-alert-note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The dates given for all the blog posts are not the publication date;
they&amp;#8217;re the date that I created each document.
Part 0 (this page) was written on 2025-07-07, but it has been backdated so that the order of the posts on the home page makes sense.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;footnotes&quot;&gt;
&lt;hr/&gt;
&lt;ol&gt;

&lt;li id=&quot;fn1&quot;&gt;
&lt;p&gt;Ideally, the programming language you use for a chess engine is a performant language like C++, Rust, Java, C#.
If your language is generally faster, then your engine will be better because it can
do more calculations in the same amount of time.&amp;#160;&lt;a href=&quot;#fnref1&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;li id=&quot;fn2&quot;&gt;
&lt;p&gt;This series is roughly 25k words, which is five times more than the &lt;a href=&quot;/lc3&quot;&gt;LC-3 article&lt;/a&gt;, the previous record holder.&amp;#160;&lt;a href=&quot;#fnref2&quot; rev=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;
&lt;/div&gt;</content>
		<link href="https://www.dogeystamp.com/chess0"/>
		<id>https://www.dogeystamp.com/chess0</id>
		<updated>2025-03-05T10:00:00Z</updated>
		<published>2025-03-05T10:00:00Z</published>
	</entry>
	<entry>
		<title>Box-drawing test</title>
		<content type="html">&lt;h1 id=&quot;box-drawing-test&quot;&gt;Box-drawing test&lt;/h1&gt;
&lt;div class=&quot;creation-date&quot;&gt;
2024-02-03
&lt;/div&gt;
&lt;p&gt;In my &lt;a href=&quot;/lc3&quot;&gt;last blog post&lt;/a&gt; about the LC-3 virtual machine, I made a few diagrams with ASCII art.
I found out recently that I didn&amp;#8217;t have to use &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;-&lt;/code&gt; and &lt;code&gt;|&lt;/code&gt; symbols to make boxes; Unicode has symbols specifically for that.
So I&amp;#8217;m going to use this blog post as a playground for box-drawing.&lt;/p&gt;
&lt;p&gt;The symbols used here are all copied directly from &lt;a href=&quot;https://en.m.wikipedia.org/wiki/Box-drawing_character&quot;&gt;this Wikipedia article&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;boxdraw kit

smooth box
╭─╮││╰─╯

jagged box
┌─┐││└─┘

heavy box
┏━┓┃┃┗━┛

wires
┌─┐│└┘┬┴┤├┼

arrows
←→↓↑↔↕

dotted
⋯⋮┄┈┊
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To make boxes using the kit, copy-paste one of the &amp;#8220;box&amp;#8221; symbol sets,
put newlines, and then expand the box to fit the text:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;╭─╮││╰─╯

╭─╮
││
╰─╯
╭────────────╮
│ hello guys │
╰────────────╯
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Put spacing around the text, too.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;the diagram from last time but smoother ooOOOooo

    registers
    ┌─┐┌─┐┌─┐┌─┐┌─┐
    └─┘└─┘└─┘└─┘└─┘
    ┌─┐┌─┐┌─┐┌─┐┌─┐
    └─┘└─┘└─┘└─┘└─┘
    ╭─────────────╮     ╭────────────────────────╮
    │             │     │                        │
    │  processor  ├─────┤         memory         │
    │             │     │                        │
    ╰─────────────╯     │                        │
                        ╰────────────────────────╯
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note how the place the wire connects to each box is a different character (&lt;code&gt;┤&lt;/code&gt;) that makes it attached.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;╭───────────────────────────────────────────────────╮
│                                                   │
│                                                   │
│                                                   │
│             a big funny box (smooth)              │
│                                                   │
│                                                   │
│                                                   │
╰───────────────────────┬───────────────────────────╯
                        │
              ╭─────────┴─────────╮
              │ boxes interlinked │
              ╰───────────────────╯
                 ┌────────────┐
                 │ not smooth │
                 └────────────┘

               ┏━━━━━━━━━━━━━━━┓
               ┃ a weighty box ┃
               ┗━━━━━━━━━━━━━━━┛
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;╭─────────────────────╮
│ wires (plus arrows) │
╰──┬──────────────────╯
   ├────────┬────┐
 ←─┼──────┐ │    └─→         
   │      │ │                
   └──────┴─┤              
            │ ↑               
            │ │              
            ├─┘
            ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(The vertical arrows are slightly janky.)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          ╭────────────────────────────╮
⋯ ┄┄┄┄┄┄┄┄│ this goes off somewhere... │
          ╰────────────────────────────╯
                       ┊
                       ┊
                       ⋮
&lt;/code&gt;&lt;/pre&gt;</content>
		<link href="https://www.dogeystamp.com/boxdraw"/>
		<id>https://www.dogeystamp.com/boxdraw</id>
		<updated>2024-02-03T10:00:00Z</updated>
		<published>2024-02-03T10:00:00Z</published>
	</entry>
	<entry>
		<title>Making custom Arch Linux live ISOs without Arch Linux</title>
		<content type="html">&lt;h1 id=&quot;making-custom-arch-linux-live-isos-without-arch-linux&quot;&gt;Making custom Arch Linux live ISOs without Arch Linux&lt;/h1&gt;
&lt;div class=&quot;creation-date&quot;&gt;
2023-06-07
&lt;/div&gt;
&lt;p&gt;If you&amp;#8217;ve ever installed any Linux distro, you probably had to do it using a live install environment.
Once you flashed the USB drive, you could boot off it and you&amp;#8217;d be greeted with a Linux system.&lt;/p&gt;
&lt;p&gt;I&amp;#8217;ve always found it fascinating that you could pack the entire OS onto a stick and bring it around with you.
However, something even more enticing is being able to customize that system.&lt;/p&gt;
&lt;p&gt;This is where archiso comes in handy:
it&amp;#8217;s a tool that can be used to generate ISOs with Arch Linux on them.
You can flash these ISOs to USB drives to make portable Arch Linux systems.
archiso is also incredibly flexible, and you can customize it very well.
In fact, it&amp;#8217;s the tool that Arch&amp;#8217;s maintainers use to generate the official live installation images.&lt;/p&gt;
&lt;p&gt;The one issue I have, though is that you need to be running Arch Linux to run archiso:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;#8220;Currently creating the images is only supported on Arch Linux but may work on other operating systems as well.&amp;#8221;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;As I run another distro on my laptop, I could not use archiso.
So, in the last few days, I set out to make archiso run on my system.
The final results of this endeavour can be found &lt;a href=&quot;https://github.com/dogeystamp/archiso-portable&quot;&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;pacstrap&quot;&gt;Pacstrap&lt;/h2&gt;
&lt;p&gt;One of the essential dependencies of archiso is pacstrap.
Essentially, what pacstrap does is that it creates a small Arch Linux system in a folder.
This is the bootstrapped root filesystem (root FS).
Later, archiso takes that folder, compresses it, and puts it in the final ISO file.&lt;/p&gt;
&lt;p&gt;The problem is that pacstrap needs your host system to be Arch Linux so that it can bootstrap a new Arch system.
In fact, if you look at the &lt;a href=&quot;https://github.com/archlinux/arch-install-scripts/blob/master/pacstrap.in&quot;&gt;source code&lt;/a&gt;,
you&amp;#8217;ll find that the host system uses its own pacman to install everything to the bootstrapped system:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pacman -r &quot;$newroot&quot; &quot;${pacman_args[@]}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;pacstrap only has to create a few directories, but the rest is done by installing the &lt;code&gt;base&lt;/code&gt; metapackage through pacman.
&lt;code&gt;base&lt;/code&gt; includes both &lt;code&gt;filesystem&lt;/code&gt; and &lt;code&gt;pacman&lt;/code&gt;.
As far as I understand, this means that all the important files in an Arch Linux system come from installing the &lt;code&gt;filesystem&lt;/code&gt; package.
Once the host pacman installs &lt;code&gt;base&lt;/code&gt; in the bootstrapped system, we have a full Arch Linux root FS, including its own pacman.&lt;/p&gt;
&lt;p&gt;However, since I do not have pacman to bootstrap this way, I needed another way to obtain an Arch root FS.
I came across &lt;a href=&quot;https://github.com/wick3dr0se/archstrap&quot;&gt;archstrap&lt;/a&gt;, which does exactly that.
archstrap downloads a pre-built Arch Linux root FS as a tarball, then installs packages to it just like regular pacstrap.
Cleverly, this script removes the need for arch-chroot on the host system:
it runs the arch-chroot inside the downloaded Arch filesystem.&lt;/p&gt;
&lt;p&gt;Somewhat annoyingly though, archstrap does not operate exactly the same way pacstrap does:
I had to patch it to get it working with archiso.
Also, I patched archiso itself to remove some flags archstrap doesn&amp;#8217;t parse.&lt;/p&gt;
&lt;h2 id=&quot;other-changes&quot;&gt;Other changes&lt;/h2&gt;
&lt;p&gt;The other main dependency missing in archiso is pacman, Arch&amp;#8217;s package manager.
Since we aren&amp;#8217;t running Arch, we of course do not have it on our host system.
However, the bootstrapped Arch root FS we downloaded earlier does have pacman inside of it.
Therefore, I replaced all invocations of pacman inside archiso with invocations of the bootstrapped pacman.&lt;/p&gt;
&lt;p&gt;archiso also includes a small script to test your generated ISOs in a QEMU virtual machine.
I added a check to it that switches some hard-coded paths.
In Arch, the path is &lt;code&gt;&amp;#47;usr&amp;#47;share&amp;#47;edk2-ovmf&amp;#47;x64&lt;/code&gt;,
but on my system it was at &lt;code&gt;&amp;#47;usr&amp;#47;share&amp;#47;edk2-ovmf&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;It turns out that modifying open source software isn&amp;#8217;t that difficult.
Given that archiso&amp;#8217;s maintainers wrote very structured and organized code,
it&amp;#8217;s surprisingly easy to navigate around the script to patch things.&lt;/p&gt;
&lt;p&gt;Of course, I definitely ruined the quality of archiso by doing this:
there&amp;#8217;s missing features and everything is untested and unlinted.
Then again, this isn&amp;#8217;t going to be production-grade software;
I just wanted to make a custom portable Arch USB while using Gentoo on my PC.&lt;/p&gt;
&lt;p&gt;Anyways, if you want to make your own custom Arch USBs but don&amp;#8217;t have Arch,
check out &lt;a href=&quot;https://github.com/dogeystamp/archiso-portable&quot;&gt;archiso-portable&lt;/a&gt; on GitHub.&lt;/p&gt;
&lt;p&gt;As a bonus, here&amp;#8217;s a screenshot of the Arch Linux live environment I made earlier on a Gentoo system:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/public/img/archiso-portable-desktop.jpg&quot; alt=&quot;preview&quot; /&gt;&lt;/p&gt;</content>
		<link href="https://www.dogeystamp.com/archiso-portable"/>
		<id>https://www.dogeystamp.com/archiso-portable</id>
		<updated>2023-06-07T10:00:00Z</updated>
		<published>2023-06-07T10:00:00Z</published>
	</entry>
</feed>
