<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>tag:ajitek.net,2024:blog</id>
  <title>aji's Blog</title>
  <updated>2026-05-22T20:55:12.194546Z</updated>
  <link rel="self" href="https://aji.github.io/blog/feed.xml" />
  <link href="https://aji.github.io/blog" />
  <icon>https://aji.github.io/blog/images/icon.png</icon>
  <author>
    <name>aji</name>
  </author>
  
    <entry>
      <id>tag:ajitek.net,2024:blog/posts/2026-05-22-this-could-be-a-notebook.md</id>
      <title>This Could Be a Notebook</title>
      <link rel="alternate" type="text/html" href="https://aji.github.io/blog/posts/2026-05-22-this-could-be-a-notebook.html" />
      <published>2026-05-22T12:00:00Z</published>
      <updated>2026-05-22T20:54:33Z</updated>
      <rights>
        This work (c) 2026 by Alex Iadicicco is licensed under CC BY-NC-SA 4.0.
        To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/
      </rights>
      
        <summary>Jupyter notebooks are a cool format, but could be cooler</summary>
      
      <content type="xhtml" xml:lang="en" xml:base="https://aji.github.io/blog">
        <div xmlns="http://www.w3.org/1999/xhtml">
          
          <p>I’ve been using Jupyter notebooks for a while now, mostly for small things that
are the kind of thing Jupyter is good at: loading some data, processing that
data, plotting some stuff, going back and redoing stuff differently, etc. I like
that cells can produce rich outputs, even things like progress bars or
interactive plots. I also like being able to include Markdown cells, which feels
like it achieves what literate programming was meant to.</p>
<p>I keep trying to think up ways to use notebooks for other things in a way that
actually adds value and isn’t just a tech demo, and so far my attempts have
been unsuccessful. I got a decent part of the way through setting up Hakyll so
I could use <code>.ipynb</code> files as posts on this blog, but the rate at which jank was
accumulating made me give up on that idea pretty quickly. (I still think this
is practical, it just has some rough spots that need to be worked out first.)
I’ve experimented with using notebooks (with both Python and Deno kernels) for
simple ops tasks, and keep feeling like a script would do the job better on
too many axes. I do think there’s value in using notebooks as a format for
things like analyzing logs, but this is just an ops-flavored version of a task
we already know notebooks are good for, with CSVs swapped for JSONL files.</p>
<p>Maybe there’s not actually a problem here, but I think the raw material of the
notebook workflow has a lot of potential for ops work. To me, the following
points are critical limitations of the status quo:</p>
<ul>
<li><p>Cells can be run out of order, multiple times, etc. This is an important part
of exploration and experimentation, but makes notebooks pretty ineffective at
ensuring a series of steps are executed in a specific order a specific number
of times.</p></li>
<li><p>Re-running a cell erases the output of previous executions. This means a
notebook doesn’t tell you what was <em>actually</em> done, only what was done last.</p></li>
</ul>
<h2 id="what-i-would-do-differently">What I would do differently</h2>
<p>I’m imagining a notebook-based tool that enables the following style of
automation:</p>
<ul>
<li><p>A workflow is generated from a parameterized notebook.</p></li>
<li><p>The workflow executes one cell at a time.</p></li>
<li><p>The workflow stops on failed cells. The operator can choose to either re-run
the cell or cancel the workflow. If retrying a failed cell, the workflow will be
marked as having gone off script.</p></li>
<li><p>The operator can edit cells, re-run successful cells, or run cells out of
order. Any of these will also mark the workflow as having gone off script.</p></li>
<li><p>The exact sequence of cells that were run is logged, with timestamps.</p></li>
</ul>
<p>This would be a relatively small addition to current notebook tooling,
essentially being a “restricted mode” for notebook execution with some
additional logging. It has a number of properties that I think make it an
interesting possibility for ops work:</p>
<ul>
<li><p>The ability to include rich outputs such as graphs, tables, etc. is useful
both during and after workflow execution.</p></li>
<li><p>Auth is consolidated, since all actions are performed by the notebook kernel
on behalf of the operator. There’s no need to think about SSH keys, API tokens,
web UI access, etc. Such a tool could also conceivably allow notebooks to be
executed with a particular role, allowing finer grained control over what
actions the notebook is allowed to perform.</p></li>
<li><p>Logging all cell executions provides both valuable audit information as well
as a good starting point for investigating why a workflow went off script and
how it might need to be changed.</p></li>
<li><p>If <code>.ipynb</code> files can be imported as new workflows, then developing a new
workflow is as simple as opening the notebook editor of your choice and getting
to work. (Although this workflow tool as described would need to include all
the functionality necessary for a notebook editor, and in the context of the
aforementioned auth point, it might make sense to write new workflows directly
in tool.)</p></li>
</ul>
<p>I’m gonna have a look around and see if there’s anything like this out there.</p>
        </div>
      </content>
    </entry>
  
    <entry>
      <id>tag:ajitek.net,2024:blog/posts/2026-05-15-hexbot-part-4.md</id>
      <title>Hexbot Part 4: Did It Work?</title>
      <link rel="alternate" type="text/html" href="https://aji.github.io/blog/posts/2026-05-15-hexbot-part-4.html" />
      <published>2026-05-15T12:00:00Z</published>
      <updated>2026-05-19T04:41:07Z</updated>
      <rights>
        This work (c) 2026 by Alex Iadicicco is licensed under CC BY-NC-SA 4.0.
        To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/
      </rights>
      
        <summary>Measuring the strength of our AlphaZero-style bot</summary>
      
      <content type="xhtml" xml:lang="en" xml:base="https://aji.github.io/blog">
        <div xmlns="http://www.w3.org/1999/xhtml">
          
            <img src="https://aji.github.io/blog/images/hex-banner.png" />
          
          <blockquote>
<p>This is the final post in a series of posts about my foray into building a bot
to play Hex. The focus of this post is on quantifying the strength of our
shiny new bot, plus some miscellaneous followups.</p>
<p><a href="./2026-05-10-hexbot-part-1.html">Hexbot Part 1: The 101 Unit</a><br />
<a href="./2026-05-12-hexbot-part-2.html">Hexbot Part 2: The Board and Our First Bot</a><br />
<a href="./2026-05-13-hexbot-part-3.html">Hexbot Part 3: Using Neural Networks</a><br />
Hexbot Part 4: Did It Work?</p>
</blockquote>
<p>Now that we’ve trained up a neural network to play Hex, it would be nice to know
if our effort (and cloud computing bill) have paid off, and there are a number
of things we can do to get some insight here.</p>
<p>The first is to simply play against the bot to get a qualitative sense for how
well it plays. I’ve played a few games against it and while I’m still kind of
a beginner, it’s clearly much stronger than me.<a href="#fn1" class="footnote-ref" id="fnref1" role="doc-noteref"><sup>1</sup></a> I copied the moves
between it and the “Impossible” CPU in <em>Clubhouse Games</em> and it won. Both of
these with the model playing as the second player without the swap rule, and
with 2 seconds of thinking time per turn. Neither of these achievements is very
impressive though (even the <em>Clubhouse Games</em> “Impossible” difficulty is still
pretty weak in the grand scheme of Hex-playing programs) and they don’t tell
us much about the bot’s strength quantitatively.<a href="#fn2" class="footnote-ref" id="fnref2" role="doc-noteref"><sup>2</sup></a></p>
<h2 id="benchmarking-against-mcts">Benchmarking against MCTS</h2>
<p>In <a href="./2026-05-12-hexbot-part-2.html">part 2</a> we noted that the strength of MCTS can be configured by
changing the number of search iterations, i.e. the number of rollouts. Thus, we
might ask how many rollouts are required for MCTS to play at roughly the same
strength as our bot. Furthermore, we can check if this number actually increased
over the course of training, and at what rate.</p>
<p>As described in <a href="./2026-05-13-hexbot-part-3.html">part 3</a>, the purpose of the neural network is to
augment an MCTS search with a value function and a prior over available moves,
but we can actually just use the prior (i.e. the policy) directly, evaluating
the current board once and immediately picking the move considered “most likely”
instead of searching the tree deeper. By finding the MCTS configuration that
wins about half of its games against this type of model-only strategy, we can
get a sense for the amount of “MCTS knowledge” contained within a model
checkpoint.</p>
<p>In principle we could run hundreds or thousands of games at different MCTS
strengths, but each game can take minutes to run so we would like to need as few
as them as possible since we have 142 checkpoints to evaluate. Instead we can
use Bayesian inference on a distribution over a range of MCTS strengths, where
the likelihood is given by the win rate predictor we calibrated in
<a href="./2026-05-12-hexbot-part-2.html">part 2</a>. The prior informs the choice of which strength to try next, then we
calculate the mean and variance of the posterior distribution after a number of
experiments have been run.</p>
<p>The result of this analysis is the following plot:</p>
<figure>
<img src="../images/hex-strength-plot.png" alt="A plot of model checkpoint strength over time, shown as confidence intervals." />
<figcaption aria-hidden="true">A plot of model checkpoint strength over time, shown as confidence intervals.</figcaption>
</figure>
<p>The X axis is checkpoint index (correlating roughly with training time)
and the Y axis is the MCTS strength (<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mrow><mi mathvariant="normal">log</mi><mo>&#8289;</mo></mrow><mn>10</mn></msub><annotation encoding="application/x-tex">\log_{10}</annotation></semantics></math> of rollout count) required to
get a roughly 50/50 win rate against the checkpoint playing in model-only mode.
(The dashed line marks the MCTS strength where the amount of time spent thinking
is the same for MCTS and a model-only move.) The Y axis stops at 8 because
100,000,000 rollouts is already quite a lot for my laptop to handle and the
analysis would take way too long if the MCTS were allowed to think any longer.
Clearly the latest checkpoints are much stronger than pure MCTS!</p>
<h2 id="generalized-benchmarking">Generalized benchmarking</h2>
<p>The same type of analysis from the last section can be extended to <em>any</em> two
Hex-playing programs, since we have a single configuration axis we can use
for all of them: thinking time. That is, instead of asking how many rollouts
are required for pure MCTS to play at the same strength as a model checkpoint,
we can ask what ratio of thinking time is required for two programs to play
at the same strength.<a href="#fn3" class="footnote-ref" id="fnref3" role="doc-noteref"><sup>3</sup></a></p>
<p>With this in mind, an interesting thing we can try is to train a <em>smaller</em> model
on the self-play data generated when training a bigger one. A small model needs
less time to think for the same number of search iterations, so it might
actually need less time to play at the same level.</p>
<p>To test this out, I ran the optimizer for a much smaller model (about 6x smaller)
using the same self-play data as generated for the model we’ve been using until
now, then benchmarked it against the bigger model trained on the same data.
Unsurprisingly, the smaller model plays just as well, and much more quickly!
This is the <code>models/model-v0-16-8-64-20260514</code> file in the repo, which when
benchmarked against the <code>v0-16-32-256</code> model comes in around 10x faster for the
same level of gameplay. (I’m training a <code>12-16-64</code> model and it’s even stronger.
I think 24 hours wasn’t enough for the big model!)</p>
<h2 id="whats-next">What’s next?</h2>
<p>This is the end of this series of blog posts at least. I might write some
additional posts if I do anything else interesting that I feel like sharing,
but this first iteration is done for now.</p>
<p>As far as the project itself, I feel like I’ve mostly gotten what I wanted out
of it. I had a lot of fun and learned a lot along the way, and got a pretty
decent Hex-playing program out of it too! There are a number of additional
directions this project could go that I might go in if I feel up to it someday:</p>
<ul>
<li><p>Hinted at in the previous section, there’s still room to optimize thinking
time by training smaller models. At some point the model is too small to play
well, but it would be interesting to try to find this point.</p></li>
<li><p>Every time we do a search, we start with an empty move tree. However, it’s
possible to reuse the previous move tree, thus saving some computation,
especially for moves that were expected (and thus have well-explored subtrees).
A bot can also be programmed to “ponder”, i.e. to think while it’s the
opponent’s turn to play. For simplicity I chose not to introduce either of
these things when evaluating bot strength, but for the strongest possible play
these are important (and straightforward) optimizations.</p></li>
<li><p>The big training run only went for 24 hours. While the resulting model maxed
out our MCTS benchmark, there’s no reason to believe it’s as strong as it will
ever be for that model size. Unfortunately, training is time-consuming and
expensive, so I’m not very keen on digging much deeper here unless a bunch of
GPUs fall in my lap. (Also, I would want to get CUDA and quantization working
before I do that.) Some experimentation here with a <code>12-16-64</code> model is
producing some good results, and I’m going to keep pushing it until it seems
like it’s topping out.</p></li>
<li><p>Our model can only play 11x11 Hex without the swap rule, but “real” Hex bots
like KataHex can play on a variety of board sizes with or without the swap rule.</p></li>
<li><p>We only explored pure MCTS and AlphaZero-style MCTS, but there are many other
ways to write a Hex-playing program! Claude Shannon built an analog Hex-playing
device that used features of an electric potential field to choose moves. It
would be interesting to try some other architectures and to benchmark them
against the ones we have.</p></li>
<li><p>The UI, <code>bin/play.rs</code>, is not very good! A more full-featured UI, with
configurable bot players, time controls, analysis features, etc. would be nice
to have around. (Maybe you could benchmark <em>yourself</em> against the AI!)</p></li>
</ul>
<p>But I think for now I’m going to take a break. I’m starting to see hexagons
whenever I close my eyes.</p>
<section id="footnotes" class="footnotes footnotes-end-of-document" role="doc-endnotes">
<hr />
<ol>
<li id="fn1"><p>If you’d like to play against it yourself, you can check out the code
at https://github.com/aji/hex-table and run
<code>cargo run --release --bin play -F ui,candle -- --model models/model-v0-16-32-256-20260510</code>
but the UI is not very good at the moment :( I’m not sure if I’ll ever get
around to making it better, but I hope to!<a href="#fnref1" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn2"><p>A more interesting followup would be to set up some tournaments or
leagues with much more established bots like MoHex or KataHex. This is a lot of
work to set up though and I might get around to it someday. I would be surprised
if my little bot does very well but I won’t know for sure until I try.<a href="#fnref2" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn3"><p>There are a lot of assumptions buried in here. For example, we’re
assuming that the relative change in strength due to extra thinking time is
roughly the same for all programs, that the strength difference between 2
minutes and 1 minute is the same as the difference between 2ms and 1ms, etc.<a href="#fnref3" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
</ol>
</section>
        </div>
      </content>
    </entry>
  
    <entry>
      <id>tag:ajitek.net,2024:blog/posts/2026-05-13-hexbot-part-3.md</id>
      <title>Hexbot Part 3: Using Neural Networks</title>
      <link rel="alternate" type="text/html" href="https://aji.github.io/blog/posts/2026-05-13-hexbot-part-3.html" />
      <published>2026-05-13T12:00:00Z</published>
      <updated>2026-05-15T05:59:08Z</updated>
      <rights>
        This work (c) 2026 by Alex Iadicicco is licensed under CC BY-NC-SA 4.0.
        To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/
      </rights>
      
        <summary>How MCTS can be augmented with a neural network</summary>
      
      <content type="xhtml" xml:lang="en" xml:base="https://aji.github.io/blog">
        <div xmlns="http://www.w3.org/1999/xhtml">
          
            <img src="https://aji.github.io/blog/images/hex-banner.png" />
          
          <blockquote>
<p>This is the third post in a series of posts about my foray into
building a bot to play Hex. The focus of this post is on how to augment MCTS
with a neural network, and in particular the AlphaZero-style strategy for
doing so.</p>
<p><a href="./2026-05-10-hexbot-part-1.html">Hexbot Part 1: The 101 Unit</a><br />
<a href="./2026-05-12-hexbot-part-2.html">Hexbot Part 2: The Board and Our First Bot</a><br />
Hexbot Part 3: Using Neural Networks<br />
<a href="./2026-05-15-hexbot-part-4.html">Hexbot Part 4: Did It Work?</a></p>
</blockquote>
<h2 id="improving-mcts">Improving MCTS</h2>
<p>In <a href="./2026-05-12-hexbot-part-2.html">part 2</a> we went over MCTS, which serves as a tunable baseline for
how to choose moves in a game of Hex: more rollouts means stronger play at the
cost of thinking time, where “strength” grows logarithmically with rollout
count.</p>
<p>We can, of course, do a lot better than basic MCTS, though many paths forward
incorporate domain knowledge about Hex strategy. In <a href="./2026-05-10-hexbot-part-1.html">part 1</a> we already
mentioned alpha-beta search, a different search algorithm entirely which relies
on a value function, often a hand-crafted one.</p>
<p>MCTS itself can also be improved in a number of ways. The expand step, for
example, can be modified to use <em>heavy rollouts</em> where a heuristic is used
instead of choosing moves completely at random. The search step can be augmented
by calculating a <em>prior</em> over the possible moves, which influences the weight
assigned to moves with low visit count.</p>
<p>The AlphaZero architecture, which we will be applying to Hex, takes essentially
both of these approaches, using a neural network to augment an MCTS in two ways:</p>
<ul>
<li><p>A <strong>value network</strong> is used instead of a pure rollout, giving each new leaf
node a number indicating how favorable the network considers the board state
for either player, and these are aggregated up the tree in the same way as
rollout outcomes.</p></li>
<li><p>A <strong>policy network</strong> initializes each node with a prior distribution over child
nodes, i.e. available moves, which biases the search towards moves the network
deems more likely.</p></li>
</ul>
<p>The code I used for this type of search is <a href="https://github.com/aji/hex-table/blob/800e2e1566f9a1358c734b1de849336ad592d99b/src/nn/search.rs">on GitHub</a>, but
it’s very similar to the basic MCTS we discussed before just with a different
function to maximize when descending the tree that accounts for the values from
the policy network.</p>
<h2 id="improving-the-neural-network">Improving the neural network</h2>
<p>The question of course is how to train these networks. One strategy is to
use expert games as a training set, and this was how AlphaGo was initially
trained. However a key insight of theirs was that the network-augmented MCTS is
an algorithm for taking policy calculations from the model and aggregating them
into a better policy. By having the model play games against itself and training
the model to predict the improved policy returned by MCTS, the model can improve
through self-play. The discovery of AlphaGo Zero is that this idea of
improvement through self-play works <em>even when starting from a
randomly-initialized network</em>.<a href="#fn1" class="footnote-ref" id="fnref1" role="doc-noteref"><sup>1</sup></a></p>
<p>It may seem a little mysterious that this works at all, and indeed there are a
lot of ways for it to go wrong, but the basic idea makes sense. The model is
essentially learning to predict what an MCTS search will do, and the self-play
is structured to ensure that a diversity of positions are explored and that
the problem is computationally feasible by gradually “compressing” recent
MCTS effort into the model. The model improves the MCTS, and the MCTS improves
the model.</p>
<h2 id="the-model-architecture">The model architecture</h2>
<p>The model is fundamentally a convolutional neural network, taking an
“image”<a href="#fn2" class="footnote-ref" id="fnref2" role="doc-noteref"><sup>2</sup></a> of the board and applying a convolution and several residual
convolutional layers to it before taking the final image and flattening it to
be mapped into a policy and value output.</p>
<p>The model size has three configuration parameters:</p>
<ul>
<li><code>conv_channels</code>, the number of channels in the intermediate board images.</li>
<li><code>conv_layers</code>, the number of residual convolutional layers to apply.</li>
<li><code>value_hidden</code>, the number of hidden neurons in the value calculation.</li>
</ul>
<p>In pseudo-Python, the model calculation looks like this:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> evaluate(<span class="va">self</span>, board):</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>    im_input <span class="op">=</span> <span class="va">self</span>.board_to_image(board)</span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>    im <span class="op">=</span> <span class="va">self</span>.conv_input(im_input)</span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>    <span class="cf">for</span> (ker0, ker1) <span class="kw">in</span> <span class="va">self</span>.residual_conv_layers:</span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>        im <span class="op">=</span> <span class="va">self</span>.residual_conv_layer(im, ker0, ker1)</span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>    policy <span class="op">=</span> <span class="va">self</span>.policy_head(im)</span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>    value <span class="op">=</span> <span class="va">self</span>.value_head(im)</span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> policy, value</span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> board_to_image(<span class="va">self</span>, board):</span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a>    <span class="co"># an 11x11 image with 2 channels, one for red pieces</span></span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a>    <span class="co"># and one for blue pieces where a &quot;pixel&quot; is 1.0 if</span></span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a>    <span class="co"># there is a piece and 0.0 otherwise</span></span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> ...</span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> conv_input(<span class="va">self</span>, im):</span>
<span id="cb1-20"><a href="#cb1-20" aria-hidden="true" tabindex="-1"></a>    <span class="co"># conv2d with conv_channels 3x3 filters</span></span>
<span id="cb1-21"><a href="#cb1-21" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> im</span>
<span id="cb1-22"><a href="#cb1-22" aria-hidden="true" tabindex="-1"></a>        .hex_conv2d(<span class="va">self</span>.conv_input_kernels)</span>
<span id="cb1-23"><a href="#cb1-23" aria-hidden="true" tabindex="-1"></a>        .batch_norm()</span>
<span id="cb1-24"><a href="#cb1-24" aria-hidden="true" tabindex="-1"></a>        .leaky_relu()</span>
<span id="cb1-25"><a href="#cb1-25" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-26"><a href="#cb1-26" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> residual_conv_layer(<span class="va">self</span>, im, ker0, ker1):</span>
<span id="cb1-27"><a href="#cb1-27" aria-hidden="true" tabindex="-1"></a>    <span class="co"># conv2d with conv_channels 3x3 filters</span></span>
<span id="cb1-28"><a href="#cb1-28" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> im</span>
<span id="cb1-29"><a href="#cb1-29" aria-hidden="true" tabindex="-1"></a>        .hex_conv2d(ker0)</span>
<span id="cb1-30"><a href="#cb1-30" aria-hidden="true" tabindex="-1"></a>        .batch_norm()</span>
<span id="cb1-31"><a href="#cb1-31" aria-hidden="true" tabindex="-1"></a>        .leaky_relu()</span>
<span id="cb1-32"><a href="#cb1-32" aria-hidden="true" tabindex="-1"></a>        .hex_conv2d(ker1)</span>
<span id="cb1-33"><a href="#cb1-33" aria-hidden="true" tabindex="-1"></a>        .batch_norm()</span>
<span id="cb1-34"><a href="#cb1-34" aria-hidden="true" tabindex="-1"></a>        .add(im) <span class="co"># skip connection</span></span>
<span id="cb1-35"><a href="#cb1-35" aria-hidden="true" tabindex="-1"></a>        .leaky_relu()</span>
<span id="cb1-36"><a href="#cb1-36" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-37"><a href="#cb1-37" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> policy_head(<span class="va">self</span>, im):</span>
<span id="cb1-38"><a href="#cb1-38" aria-hidden="true" tabindex="-1"></a>    <span class="co"># conv2d with two 1x1 filters</span></span>
<span id="cb1-39"><a href="#cb1-39" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> im</span>
<span id="cb1-40"><a href="#cb1-40" aria-hidden="true" tabindex="-1"></a>        .hex_conv2d(<span class="va">self</span>.policy_kernels)</span>
<span id="cb1-41"><a href="#cb1-41" aria-hidden="true" tabindex="-1"></a>        .batch_norm()</span>
<span id="cb1-42"><a href="#cb1-42" aria-hidden="true" tabindex="-1"></a>        .leaky_relu()</span>
<span id="cb1-43"><a href="#cb1-43" aria-hidden="true" tabindex="-1"></a>        .flatten()</span>
<span id="cb1-44"><a href="#cb1-44" aria-hidden="true" tabindex="-1"></a>        .matmul(<span class="va">self</span>.policy_linear) <span class="co"># 242 -&gt; 121</span></span>
<span id="cb1-45"><a href="#cb1-45" aria-hidden="true" tabindex="-1"></a>        .log_softmax()</span>
<span id="cb1-46"><a href="#cb1-46" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-47"><a href="#cb1-47" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> value_head(<span class="va">self</span>, im):</span>
<span id="cb1-48"><a href="#cb1-48" aria-hidden="true" tabindex="-1"></a>    <span class="co"># conv2d with one 1x1 filter</span></span>
<span id="cb1-49"><a href="#cb1-49" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> im</span>
<span id="cb1-50"><a href="#cb1-50" aria-hidden="true" tabindex="-1"></a>        .hex_conv2d(<span class="va">self</span>.value_kernel)</span>
<span id="cb1-51"><a href="#cb1-51" aria-hidden="true" tabindex="-1"></a>        .batch_norm()</span>
<span id="cb1-52"><a href="#cb1-52" aria-hidden="true" tabindex="-1"></a>        .leaky_relu()</span>
<span id="cb1-53"><a href="#cb1-53" aria-hidden="true" tabindex="-1"></a>        .flatten()</span>
<span id="cb1-54"><a href="#cb1-54" aria-hidden="true" tabindex="-1"></a>        .matmul(<span class="va">self</span>.value_linear0) <span class="co"># 121 -&gt; value_hidden</span></span>
<span id="cb1-55"><a href="#cb1-55" aria-hidden="true" tabindex="-1"></a>        .leaky_relu()</span>
<span id="cb1-56"><a href="#cb1-56" aria-hidden="true" tabindex="-1"></a>        .matmul(<span class="va">self</span>.value_linear1) <span class="co"># value_hidden -&gt; 1</span></span></code></pre></div>
<p>The actual model code is <a href="https://github.com/aji/hex-table/blob/800e2e1566f9a1358c734b1de849336ad592d99b/src/nn/model.rs">on GitHub</a>, and is built on
<a href="https://burn.dev/">Burn</a>, a tensor and autodiff library for Rust. <code>hex_conv2d</code> here refers
to a normal convolution but with some of the kernel values set to zero, making
e.g. a 3x3 kernel only able to incorporate information from cells that are up
to 1 cell away. (I was sort of just winging it when I decided to do this but I
was worried information being able to travel more quickly in one direction might
result in weird behavior.)</p>
<p>This architecture is lifted more or less directly from the
<a href="https://discovery.ucl.ac.uk/id/eprint/10045895/1/agz_unformatted_nature.pdf">AlphaGo Zero paper</a>, with the aforementioned “hex convolution” change.</p>
<h2 id="training-it">Training it</h2>
<p>The training procedure at this point is straightforward in concept but full of
many little optimization<a href="#fn3" class="footnote-ref" id="fnref3" role="doc-noteref"><sup>3</sup></a> problems<a href="#fn4" class="footnote-ref" id="fnref4" role="doc-noteref"><sup>4</sup></a> and fiddly implementation<a href="#fn5" class="footnote-ref" id="fnref5" role="doc-noteref"><sup>5</sup></a> details<a href="#fn6" class="footnote-ref" id="fnref6" role="doc-noteref"><sup>6</sup></a>. I’ll go
over the main points quickly here but will leave discussion of smaller points
for another time.</p>
<p>I wrote some code to set up distributed training with components that interact
over HTTP:</p>
<ul>
<li><p>A <strong>controller</strong>, which serves model checkpoints and the self-play log. This
was just an HTTP server with a light enough workload to run on my laptop.</p></li>
<li><p>A <strong>self-play</strong> daemon, which periodically downloads the latest checkpoint
and generates self-play data. I ran three instances of this in a Google Cloud
Run worker pool with NVIDIA L4 GPUs.</p></li>
<li><p>An <strong>optimizer</strong> daemon, which follows the self-play log and runs gradient
descent on the latest model checkpoint, periodically uploading a new
checkpoint to the controller. I ran this component on my laptop while at my
computer and moved it to Cloud Run while away (because running my M4
unattended at high loads makes me nervous.)</p></li>
</ul>
<p>I trained the model for about 24 hours with the above setup and the following
model configuration:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode json"><code class="sourceCode json"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="fu">{</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;conv_layers&quot;</span><span class="fu">:</span> <span class="dv">16</span><span class="fu">,</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;conv_channels&quot;</span><span class="fu">:</span> <span class="dv">32</span><span class="fu">,</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;value_hidden&quot;</span><span class="fu">:</span> <span class="dv">256</span></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a><span class="fu">}</span></span></code></pre></div>
<h2 id="up-next">Up next</h2>
<p>In the <a href="./2026-05-15-hexbot-part-4.html">next post</a>, I’ll explain how to decide if the work we’ve done is
any good.</p>
<section id="footnotes" class="footnotes footnotes-end-of-document" role="doc-endnotes">
<hr />
<ol>
<li id="fn1"><p>This paragraph only describes how the policy network is trained, but
the value network is an equally important part of the model. However, its
training target is rather straightforward: the value network is simply trained
to predict the outcome of the game. An obvious question is why the self-play
data shouldn’t use the MCTS-reported value as the training target. I don’t know
for sure and imagine there is some reinforcement learning wisdom at play here,
since MCTS <em>also</em> takes value calculations and aggregates them into a better
value estimate. But it’s worth pointing out that the final outcome is a more
“clean” signal that directly reflects the rules, and as a training target it
lets the information learned at the end of the game affect how <em>all</em> earlier
positions are evaluated, instead of each position only seeing as far ahead as
the MCTS was able to get.<a href="#fnref1" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn2"><p>Not a literal image, but I’ll use this term to refer to any
two-dimensional array of vectors. In machine learning the two arguments to a
convolution are often referred to as the image and the kernel, regardless of
whether the image is actually an “image” in the conventional sense. Likewise,
the dimensions of the vectors are referred to as “channels”, and I may even
refer to an element of the two-dimensional array as a “pixel” sometimes although
this is a bit of an abuse of terminology in this context.<a href="#fnref2" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn3"><p>Keeping the GPU while doing self-play is an interesting performance
challenge. The board image and model outputs need to be sent between the GPU and
CPU for the search to work, which introduces a lot of latency especially for
smaller models. The obvious solution is to batch model evaluations by sending
multiple board states at once, but basic MCTS only looks at one state at a time.
There are strategies for parallelizing MCTS, but these are tricky to implement
in Rust since they involve concurrent edits to a data structure. Instead my
strategy was to simply run many self-play games concurrently and to send their
evaluation requests to another thread which batches them up for execution on the
GPU. This resulted in significantly more positions per second per GPU, which
means more self-play data generated per second.<a href="#fnref3" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn4"><p>A weird problem I ran into is that the performance of the L4s was not
actually very impressive by comparison with my laptop. I assume this is because
I was using <code>wgpu</code> in both places, which means the kernels get compiled into
Metal shaders or Vulkan shaders depending on the available hardware, and I
imagine I would have gotten better performance from the L4s if I had used Burn’s
CUDA backend instead, but this seemed like enough of a headache to get working
that I punted on it. I would like to come back on this some point and maybe also
look at quantization and other performance tricks, but at the time I mostly
wanted to get a single training run taken care of just to see if things were
moving in the right direction.<a href="#fnref4" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn5"><p>It turns out that simply installing Vulkan inside a container is not
enough to actually be able to use the NVIDIA L4s attached to a Cloud Run
instance due to the fact that certain configuration files are still missing.
This was a little tricky to figure out and came with an overwhelming feeling of
being well off the beaten path, and I think should have been a strong indicator
that CUDA would have been the right choice.<a href="#fnref5" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn6"><p>One interesting thing to point out about training via self-play is
that the self-play portion turns out to be the most computationally intensive
part, and not by a small amount. I used 800 model evaluations per turn, and each
turn results in one new example for the self-play training set. Meanwhile each
iteration of the optimizer is doing only one model evaluation per example in the
minibatch, plus autodiff overhead. It’s difficult to quantify the relative value
of generating self-play examples and doing optimizer iterations, but the
asymmetric computational requirements should be clear, and we would hope each
GPU-hour of optimization would be matched by tens or hundreds of GPU-hours of
self-play. Indeed, according to <a href="https://arxiv.org/pdf/1712.01815">the AlphaZero paper</a> they used 64
second-generation TPUs for optimization and 5000 first-generation TPUs for
self-play, a massive FLOPS asymmetry even accounting for the different hardware.
Running the optimizer on my laptop felt much more reasonable when considering
that I had “only” three L4s to use for generating self-play data.<a href="#fnref6" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
</ol>
</section>
        </div>
      </content>
    </entry>
  
    <entry>
      <id>tag:ajitek.net,2024:blog/posts/2026-05-12-hexbot-part-2.md</id>
      <title>Hexbot Part 2: The Board and Our First Bot</title>
      <link rel="alternate" type="text/html" href="https://aji.github.io/blog/posts/2026-05-12-hexbot-part-2.html" />
      <published>2026-05-12T12:00:00Z</published>
      <updated>2026-05-15T05:58:07Z</updated>
      <rights>
        This work (c) 2026 by Alex Iadicicco is licensed under CC BY-NC-SA 4.0.
        To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/
      </rights>
      
        <summary>Representing the board state and building a simple MCTS-based bot</summary>
      
      <content type="xhtml" xml:lang="en" xml:base="https://aji.github.io/blog">
        <div xmlns="http://www.w3.org/1999/xhtml">
          
            <img src="https://aji.github.io/blog/images/hex-banner.png" />
          
          <blockquote>
<p>This is the second post in a series of posts about my foray into
building a bot to play Hex. The focus of this post is on the bitboard
implementation and an MCTS-based bot using it, as well as some analysis of the
MCTS algorithm’s relative strength for different iteration counts.</p>
<p><a href="./2026-05-10-hexbot-part-1.html">Hexbot Part 1: The 101 Unit</a><br />
Hexbot Part 2: The Board and Our First Bot<br />
<a href="./2026-05-13-hexbot-part-3.html">Hexbot Part 3: Using Neural Networks</a><br />
<a href="./2026-05-15-hexbot-part-4.html">Hexbot Part 4: Did It Work?</a></p>
</blockquote>
<h2 id="the-bitboard">The bitboard</h2>
<p>The first step in implementing a bot for any board game is to have logic and
data structures for the game itself. For Hex, as with any game, there are a
variety of approaches that would work here, but for our purposes, and for MCTS
especially, we want this code to be as lightweight as possible.</p>
<p>To start, let’s consider how to represent a hexagonal grid on a computer in the
first place. Luckily for us, the grid for Hex is a square that’s been skewed
into a rhombus. We can simply represent the data in memory as a square and then
skew or un-skew as appropriate, e.g. for displaying the board:</p>
<pre class="text"><code>AA  AB  AC  AD  AE  AF  AG  AH  AI  AJ  AK
  BA  BB  BC  BD  BE  BF  BG  BH  BI  BJ  BK
    CA  CB  CC  CD  CE  CF  CG  CH  CI  CJ  CK
      DA  DB  DC  DD  DE  DF  DG  DH  DI  DJ  DK
        EA  EB  EC  ED  EE  EF  EG  EH  EI  EJ  EK
          FA  FB  FC  FD  FE  FF  FG  FH  FI  FJ  FK
            GA  GB  GC  GD  GE  GF  GG  GH  GI  GJ  GK
              HA  HB  HC  HD  HE  HF  HG  HH  HI  HJ  HK
                IA  IB  IC  ID  IE  IF  IG  IH  II  IJ  IK
                  JA  JB  JC  JD  JE  JF  JG  JH  JI  JJ  JK
                    KA  KB  KC  KD  KE  KF  KG  KH  KI  KJ  KK</code></pre>
<p>We also define the red player as the one attempting to connect the left and
right edges and the blue player as the one attempting to connect the top and
bottom edges.</p>
<p>Bitboards are one class of board representation that use bitmap layers to
represent various features of the game. This works nicely for Hex because
each of the 121 cells of an 11x11 board can be in one of 3 states. We’ll use
two <code>u128</code>s, one for the red pieces and one for the blue pieces:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode rust"><code class="sourceCode rust"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>derive<span class="at">(</span><span class="bu">Copy</span><span class="op">,</span> <span class="bu">Clone</span><span class="at">)]</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="kw">struct</span> Bitboard <span class="op">{</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>    <span class="kw">pub</span> red<span class="op">:</span> <span class="dt">u128</span><span class="op">,</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>    <span class="kw">pub</span> blue<span class="op">:</span> <span class="dt">u128</span><span class="op">,</span></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>With some bits left over, we can also choose one of the unused bits to represent
the player whose turn it is to move:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode rust"><code class="sourceCode rust"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> NEXT_MOVE<span class="op">:</span> <span class="dt">u128</span> <span class="op">=</span> <span class="dv">1</span> <span class="op">&lt;&lt;</span> <span class="dv">127</span><span class="op">;</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="kw">fn</span> next_move(<span class="op">&amp;</span><span class="kw">self</span>) <span class="op">-&gt;</span> Player <span class="op">{</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>    <span class="cf">match</span> <span class="kw">self</span><span class="op">.</span>red <span class="op">&amp;</span> NEXT_MOVE <span class="op">!=</span> <span class="dv">0</span> <span class="op">{</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a>        <span class="cn">true</span> <span class="op">=&gt;</span> <span class="pp">Player::</span>Red<span class="op">,</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a>        <span class="cn">false</span> <span class="op">=&gt;</span> <span class="pp">Player::</span>Blue<span class="op">,</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>    <span class="op">}</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>This struct is only 32 bytes, making it very cheap to copy around, and we can
even derive a <code>Copy</code> impl for it. With this representation, the 121 least
significant bits of <code>red</code> represent cells containing a red piece, and likewise
for <code>blue</code>. (Strictly speaking it’s possible for both planes to have a 1 in the
same position, but we’ll simply assume this never happens.) A function for
mapping a row and column to a cell on the board is rather simple:</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode rust"><code class="sourceCode rust"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="kw">fn</span> rc_mask(<span class="op">&amp;</span><span class="kw">self</span><span class="op">,</span> r<span class="op">:</span> <span class="dt">usize</span><span class="op">,</span> c<span class="op">:</span> <span class="dt">usize</span>) <span class="op">-&gt;</span> <span class="dt">u128</span> <span class="op">{</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>    <span class="dv">1</span> <span class="op">&lt;&lt;</span> (<span class="dv">120</span> <span class="op">-</span> r <span class="op">*</span> <span class="dv">11</span> <span class="op">-</span> c)</span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a><span class="kw">fn</span> rc(<span class="op">&amp;</span><span class="kw">self</span><span class="op">,</span> r<span class="op">:</span> <span class="dt">usize</span><span class="op">,</span> c<span class="op">:</span> <span class="dt">usize</span>) <span class="op">-&gt;</span> <span class="dt">Option</span><span class="op">&lt;</span>Player<span class="op">&gt;</span> <span class="op">{</span></span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a>    <span class="kw">let</span> mask <span class="op">=</span> <span class="kw">self</span><span class="op">.</span>rc_mask(r<span class="op">,</span> c)<span class="op">;</span></span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a>    <span class="kw">let</span> is_red <span class="op">=</span> (<span class="kw">self</span><span class="op">.</span>red <span class="op">&amp;</span> mask) <span class="op">!=</span> <span class="dv">0</span><span class="op">;</span></span>
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a>    <span class="kw">let</span> is_blue <span class="op">=</span> (<span class="kw">self</span><span class="op">.</span>blue <span class="op">&amp;</span> mask) <span class="op">!=</span> <span class="dv">0</span><span class="op">;</span></span>
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a>    <span class="cf">match</span> (is_red<span class="op">,</span> is_blue) <span class="op">{</span></span>
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a>        (<span class="cn">true</span><span class="op">,</span> _) <span class="op">=&gt;</span> <span class="cn">Some</span>(<span class="pp">Player::</span>Red)<span class="op">,</span></span>
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a>        (_<span class="op">,</span> <span class="cn">true</span>) <span class="op">=&gt;</span> <span class="cn">Some</span>(<span class="pp">Player::</span>Blue)<span class="op">,</span></span>
<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a>        _ <span class="op">=&gt;</span> <span class="cn">None</span><span class="op">,</span></span>
<span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a>    <span class="op">}</span></span>
<span id="cb4-14"><a href="#cb4-14" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>We can set cells and do other basic manipulations in a similar manner, and
anyone familiar with bit manipulation should feel quite comfortable with the
ideas expressed so far. Queries such as calculating the number of available
moves should also be fairly obvious at this point (look up <code>popcount</code> if you’re
stuck).</p>
<p>The more interesting problem in front of us is how to check whether the game is
over, i.e. which player has connected the two edges. This brings us to another
strength of bitboards: we can represent a translated board state using bit
manipulation. In our case, we want to be able to translate the board by one
cell in each of the 6 directions. The basic premise for any given direction
is to use a mask to select the bits that will move then use a bit shift to move
them to their new locations. The reason this works is because the bit offset
between the cell at <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false" form="prefix">(</mo><mi>r</mi><mo>,</mo><mi>c</mi><mo stretchy="false" form="postfix">)</mo></mrow><annotation encoding="application/x-tex">(r,c)</annotation></semantics></math> and the cell at <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false" form="prefix">(</mo><mi>r</mi><mo>+</mo><mi>d</mi><mi>r</mi><mo>,</mo><mi>c</mi><mo>+</mo><mi>d</mi><mi>c</mi><mo stretchy="false" form="postfix">)</mo></mrow><annotation encoding="application/x-tex">(r+dr,c+dc)</annotation></semantics></math> is the same for all
such pairs of cells, <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>d</mi><mi>r</mi><mo>×</mo><mn>11</mn><mo>+</mo><mi>d</mi><mi>c</mi></mrow><annotation encoding="application/x-tex">dr\times 11 + dc</annotation></semantics></math>.</p>
<p>To check whether a player has connected their edges, we start with the cells
on one edge, traverse the graph of adjacent cells of their color, then check
if the traversal reached any cells on the opposite edge. The function that
implements the graph traversal on bit sets is called <code>bb_fill</code> by analogy with
flood filling:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode rust"><code class="sourceCode rust"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="kw">fn</span> bb_fill(start<span class="op">:</span> <span class="dt">u128</span><span class="op">,</span> traversable<span class="op">:</span> <span class="dt">u128</span>) <span class="op">-&gt;</span> <span class="dt">u128</span> <span class="op">{</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a>    <span class="kw">let</span> <span class="kw">mut</span> cur <span class="op">=</span> start <span class="op">&amp;</span> traversable<span class="op">;</span></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a>    <span class="cf">loop</span> <span class="op">{</span></span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a>        <span class="kw">let</span> <span class="kw">mut</span> next <span class="op">=</span> cur<span class="op">;</span></span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a>        next <span class="op">|=</span> (cur <span class="op">&amp;</span> ADJ0_KEEP) <span class="op">&gt;&gt;</span> ADJ0_SHR<span class="op">;</span></span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a>        next <span class="op">|=</span> (cur <span class="op">&amp;</span> ADJ1_KEEP) <span class="op">&gt;&gt;</span> ADJ1_SHR<span class="op">;</span></span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a>        next <span class="op">|=</span> (cur <span class="op">&amp;</span> ADJ2_KEEP) <span class="op">&gt;&gt;</span> ADJ2_SHR<span class="op">;</span></span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a>        next <span class="op">|=</span> (cur <span class="op">&amp;</span> ADJ3_KEEP) <span class="op">&lt;&lt;</span> ADJ3_SHL<span class="op">;</span></span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a>        next <span class="op">|=</span> (cur <span class="op">&amp;</span> ADJ4_KEEP) <span class="op">&lt;&lt;</span> ADJ4_SHL<span class="op">;</span></span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a>        next <span class="op">|=</span> (cur <span class="op">&amp;</span> ADJ5_KEEP) <span class="op">&lt;&lt;</span> ADJ5_SHL<span class="op">;</span></span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a>        next <span class="op">&amp;=</span> traversable<span class="op">;</span></span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> next <span class="op">==</span> cur <span class="op">{</span></span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a>            <span class="cf">return</span> cur<span class="op">;</span></span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a>        <span class="op">}</span></span>
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a>        cur <span class="op">=</span> next<span class="op">;</span></span>
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a>    <span class="op">}</span></span>
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>Where <code>ADJn_KEEP</code> and <code>ADJn_SHd</code> are masks and offsets for translations as
described above. <code>cur</code> stores the set of currently reachable cells, and the loop
body translates this set in all 6 directions and masks it with <code>traversable</code> to
calculate the new set of reachable cells. This process is repeated until no new
cells are added to <code>cur</code> and returns the full set of cells that are reachable
from <code>start</code> through <code>traversable</code>. With this function, checking if a player has
won is fairly straightforward:</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode rust"><code class="sourceCode rust"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> RED_START <span class="op">=</span> <span class="op">...;</span> <span class="co">// left edge bits</span></span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> RED_END <span class="op">=</span> <span class="op">...;</span> <span class="co">// right edge bits</span></span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> BLUE_START <span class="op">=</span> <span class="op">...;</span> <span class="co">// top edge bits</span></span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> BLUE_END <span class="op">=</span> <span class="op">...;</span> <span class="co">// bottom edge bits</span></span>
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a><span class="kw">fn</span> win(<span class="op">&amp;</span><span class="kw">self</span>) <span class="op">-&gt;</span> <span class="dt">Option</span><span class="op">&lt;</span>Player<span class="op">&gt;</span> <span class="op">{</span></span>
<span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a>    <span class="kw">let</span> r <span class="op">=</span> bb_fill(RED_START<span class="op">,</span> <span class="kw">self</span><span class="op">.</span>red) <span class="op">&amp;</span> RED_END<span class="op">;</span></span>
<span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a>    <span class="kw">let</span> b <span class="op">=</span> bb_fill(BLUE_START<span class="op">,</span> <span class="kw">self</span><span class="op">.</span>blue) <span class="op">&amp;</span> BLUE_END<span class="op">;</span></span>
<span id="cb6-9"><a href="#cb6-9" aria-hidden="true" tabindex="-1"></a>    <span class="cf">match</span> (r <span class="op">!=</span> <span class="dv">0</span><span class="op">,</span> b <span class="op">!=</span> <span class="dv">0</span>) <span class="op">{</span></span>
<span id="cb6-10"><a href="#cb6-10" aria-hidden="true" tabindex="-1"></a>        (<span class="cn">true</span><span class="op">,</span> _) <span class="op">=&gt;</span> <span class="cn">Some</span>(<span class="pp">Player::</span>Red)<span class="op">,</span></span>
<span id="cb6-11"><a href="#cb6-11" aria-hidden="true" tabindex="-1"></a>        (_<span class="op">,</span> <span class="cn">true</span>) <span class="op">=&gt;</span> <span class="cn">Some</span>(<span class="pp">Player::</span>Blue)<span class="op">,</span></span>
<span id="cb6-12"><a href="#cb6-12" aria-hidden="true" tabindex="-1"></a>        _ <span class="op">=&gt;</span> <span class="cn">None</span><span class="op">,</span></span>
<span id="cb6-13"><a href="#cb6-13" aria-hidden="true" tabindex="-1"></a>    <span class="op">}</span></span>
<span id="cb6-14"><a href="#cb6-14" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>And for a basic Hex board, that’s really all there is to it! Bitboards are still
used for games with much more complex rules (I’ve even
<a href="https://github.com/aji/khet-table/blob/main/src/bb.rs">written one for Khet</a>), but the simplicity of Hex works
tremendously in our favor and makes an efficient bitboard possible with very
little code.</p>
<p>The full code for the bitboard module is <a href="https://github.com/aji/hex-table/blob/main/src/bb.rs">on GitHub</a>. (The actual
code differs from the code in this post in a number of more or less trivial
ways, such as “black and white” instead of “red and blue” and <code>bool</code>
instead of <code>Player</code>, but the basic idea is the same.)</p>
<h2 id="monte-carlo-tree-search">Monte Carlo tree search</h2>
<p>As discussed in <a href="./2026-05-10-hexbot-part-1.html">part 1</a>, a basic MCTS requires very little
domain-specific code beyond what have in our bitboard, i.e. a way to enumerate
valid moves and check if the game has ended. Described briefly, the algorithm
is as follows:</p>
<blockquote>
<p>An MCTS search tree contains a node for each visited state, where the node’s
children represent states reachable by making a valid move from that state.
The edges from state <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>s</mi><annotation encoding="application/x-tex">s</annotation></semantics></math> to <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>a</mi><annotation encoding="application/x-tex">a</annotation></semantics></math> track various statistics:</p>
<ul>
<li><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>N</mi><mo stretchy="false" form="prefix">(</mo><mi>s</mi><mo>,</mo><mi>a</mi><mo stretchy="false" form="postfix">)</mo><mo>=</mo></mrow><annotation encoding="application/x-tex">N(s,a) =</annotation></semantics></math> the number of times <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false" form="prefix">(</mo><mi>s</mi><mo>,</mo><mi>a</mi><mo stretchy="false" form="postfix">)</mo></mrow><annotation encoding="application/x-tex">(s, a)</annotation></semantics></math> has been traversed during the search.</li>
<li><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>V</mi><mo stretchy="false" form="prefix">(</mo><mi>s</mi><mo>,</mo><mi>a</mi><mo stretchy="false" form="postfix">)</mo><mo>=</mo></mrow><annotation encoding="application/x-tex">V(s,a) =</annotation></semantics></math> the total value of actions in the subtree rooted at <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>a</mi><annotation encoding="application/x-tex">a</annotation></semantics></math>.</li>
</ul>
<p>Each iteration of an MCTS search proceeds as follows:</p>
<ol type="1">
<li><p><strong>Select.</strong> Starting from the root, descend the move tree until hitting a
node <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>L</mi><annotation encoding="application/x-tex">L</annotation></semantics></math> which hasn’t been seen yet. From a node <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>s</mi><annotation encoding="application/x-tex">s</annotation></semantics></math>, choose the child node <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>a</mi><annotation encoding="application/x-tex">a</annotation></semantics></math>
which maximizes a function <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi mathvariant="normal">s</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">l</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">c</mi><mi mathvariant="normal">t</mi></mrow><mo stretchy="false" form="prefix">(</mo><mi>s</mi><mo>,</mo><mi>a</mi><mo stretchy="false" form="postfix">)</mo></mrow><annotation encoding="application/x-tex">\mathrm{select}(s, a)</annotation></semantics></math>. One popular choice is called
Upper Confidence on Trees (UCT):</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi mathvariant="normal">U</mi><mi mathvariant="normal">C</mi><mi mathvariant="normal">T</mi></mrow><mo stretchy="false" form="prefix">(</mo><mi>s</mi><mo>,</mo><mi>a</mi><mo stretchy="false" form="postfix">)</mo><mo>=</mo><mfrac><mrow><mi>V</mi><mo stretchy="false" form="prefix">(</mo><mi>s</mi><mo>,</mo><mi>a</mi><mo stretchy="false" form="postfix">)</mo></mrow><mrow><mi>N</mi><mo stretchy="false" form="prefix">(</mo><mi>s</mi><mo>,</mo><mi>a</mi><mo stretchy="false" form="postfix">)</mo></mrow></mfrac><mo>+</mo><mi>C</mi><msqrt><mfrac><mrow><mrow><mi mathvariant="normal">ln</mi><mo>&#8289;</mo></mrow><mi>N</mi><mo stretchy="false" form="prefix">(</mo><mi>s</mi><mo stretchy="false" form="postfix">)</mo></mrow><mrow><mi>N</mi><mo stretchy="false" form="prefix">(</mo><mi>s</mi><mo>,</mo><mi>a</mi><mo stretchy="false" form="postfix">)</mo></mrow></mfrac></msqrt></mrow><annotation encoding="application/x-tex">\mathrm{UCT}(s, a) = \frac{V(s, a)}{N(s, a)} + C \sqrt{\frac{\ln N(s)}{N(s, a)}}</annotation></semantics></math></p></li>
<li><p><strong>Expand.</strong> Evaluate <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>L</mi><annotation encoding="application/x-tex">L</annotation></semantics></math> with a <em>rollout</em>, playing random moves until the
game ends. The winner determines the value <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>v</mi><annotation encoding="application/x-tex">v</annotation></semantics></math> which will be used in the next
step.</p></li>
<li><p><strong>Backpropagate.</strong> Returning to the root of the tree, update
<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>V</mi><mo stretchy="false" form="prefix">(</mo><mi>s</mi><mo>,</mo><mi>a</mi><mo stretchy="false" form="postfix">)</mo><mo>=</mo><mi>V</mi><mo stretchy="false" form="prefix">(</mo><mi>s</mi><mo>,</mo><mi>a</mi><mo stretchy="false" form="postfix">)</mo><mo>+</mo><mi>v</mi></mrow><annotation encoding="application/x-tex">V(s,a) = V(s,a) + v</annotation></semantics></math> and <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>N</mi><mo stretchy="false" form="prefix">(</mo><mi>s</mi><mo>,</mo><mi>a</mi><mo stretchy="false" form="postfix">)</mo><mo>=</mo><mi>N</mi><mo stretchy="false" form="prefix">(</mo><mi>s</mi><mo>,</mo><mi>a</mi><mo stretchy="false" form="postfix">)</mo><mo>+</mo><mn>1</mn></mrow><annotation encoding="application/x-tex">N(s,a) = N(s,a) + 1</annotation></semantics></math>, altering <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>v</mi><annotation encoding="application/x-tex">v</annotation></semantics></math>
as appropriate depending on whose turn it is to play from <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>s</mi><annotation encoding="application/x-tex">s</annotation></semantics></math>.</p></li>
</ol>
<p>Once enough iterations have been performed, the <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>a</mi><annotation encoding="application/x-tex">a</annotation></semantics></math> that maximizes
<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>N</mi><mo stretchy="false" form="prefix">(</mo><mrow><mi mathvariant="normal">r</mi><mi mathvariant="normal">o</mi><mi mathvariant="normal">o</mi><mi mathvariant="normal">t</mi></mrow><mo>,</mo><mi>a</mi><mo stretchy="false" form="postfix">)</mo></mrow><annotation encoding="application/x-tex">N(\mathrm{root},a)</annotation></semantics></math> is played.</p>
</blockquote>
<p>For a more in-depth explanation of MCTS, there are a variety of helpful
resources online and I won’t try to outdo them here. The
<a href="https://en.wikipedia.org/wiki/Monte_Carlo_tree_search">Wikipedia article</a> and <a href="https://www.chessprogramming.org/Monte-Carlo_Tree_Search">Chess Programming Wiki article</a>
provide a good starting point for more information.</p>
<p>The baseline MCTS implementation used <a href="https://github.com/aji/hex-table/blob/main/src/mcts.rs">in the code</a> more or less
follows the algorithm described above, but with one important optimization
which is worth pointing out. A naive rollout would play randomly and check the
win condition, but for Hex we can make a much faster and much more efficient
approximation, once again owing to the simplicity of Hex. We know that every
additional move selects an unoccupied cell, and that a completely filled board
is guaranteed to be a terminal state. Thus, in a single step, we can perform an
MCTS rollout by assigning the empty cells randomly and checking who has won:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode rust"><code class="sourceCode rust"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="kw">fn</span> mcts_rollout(<span class="op">&amp;</span><span class="kw">self</span>) <span class="op">-&gt;</span> Player <span class="op">{</span></span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>    <span class="kw">let</span> empty <span class="op">=</span> <span class="kw">self</span><span class="op">.</span>empty_bits()<span class="op">;</span></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>    <span class="kw">let</span> mask <span class="op">=</span> <span class="pp">rand::random::</span><span class="op">&lt;</span><span class="dt">u128</span><span class="op">&gt;</span>()<span class="op">;</span></span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a>    <span class="kw">let</span> red <span class="op">=</span> <span class="kw">self</span><span class="op">.</span>red <span class="op">|</span> (mask <span class="op">&amp;</span> empty)<span class="op">;</span></span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a>    <span class="cf">match</span> bb_fill(RED_START<span class="op">,</span> red) <span class="op">&amp;</span> RED_END <span class="op">!=</span> <span class="dv">0</span> <span class="op">{</span></span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a>        <span class="cn">true</span> <span class="op">=&gt;</span> <span class="pp">Player::</span>Red<span class="op">,</span></span>
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a>        <span class="cn">false</span> <span class="op">=&gt;</span> <span class="pp">Player::</span>Blue<span class="op">,</span></span>
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a>    <span class="op">}</span></span>
<span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>This code assigns a random subset of empty cells to red and checks if this is a
winning placement of red cells. If not, its complement must be a winning
placement of blue cells. Note that this is not <em>strictly</em> correct, since it
will likely result in too many or too few cells being assigned to red. It’s
equvialent to making random moves in a game where players flip a coin to decide who plays next.
However, this difference does not significantly reduce the effectiveness of
MCTS, and the amount of computation saved is well worth it. (An experiment
worth doing would be to compare the relative strength of “proper” rollouts, but
I haven’t taken the time to do this.)</p>
<h2 id="measuring-the-relative-strength-of-mcts">Measuring the relative strength of MCTS</h2>
<p>An MCTS search can be stopped at any time for any reason and return a result.
This lets us use <em>rollout count</em> (i.e. iteration count) as a way of configuring
or measuring the “strength” of an MCTS search. In practice, such as when playing
with a clock, the decision of how much computation to spend per move becomes an
important strategic one, but a fixed rollout count per move works well as a
simple reference point.</p>
<p>What we would like is to model the probability that an MCTS playing as red with
<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>n</mi><annotation encoding="application/x-tex">n</annotation></semantics></math> rollouts wins against an MCTS playing as blue with <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>m</mi><annotation encoding="application/x-tex">m</annotation></semantics></math> rollouts. A simple
model is to use logistic regression with <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi mathvariant="normal">log</mi><mo>&#8289;</mo></mrow><mi>n</mi><mo>−</mo><mrow><mi mathvariant="normal">log</mi><mo>&#8289;</mo></mrow><mi>m</mi></mrow><annotation encoding="application/x-tex">\log n - \log m</annotation></semantics></math> as the independent
variable. This is essentially the same idea as the Elo rating system, where
<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi mathvariant="normal">log</mi><mo>&#8289;</mo></mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">\log n</annotation></semantics></math> plays the role of the rating. Logistic regression lets us calibrate
our rating system so we can turn rollout count ratios into win probabilities.
The choice of <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi mathvariant="normal">log</mi><mo>&#8289;</mo></mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">\log n</annotation></semantics></math> instead of simply <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>n</mi><annotation encoding="application/x-tex">n</annotation></semantics></math> or some other function of <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>n</mi><annotation encoding="application/x-tex">n</annotation></semantics></math> is
motivated by two important assumptions: <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi mathvariant="normal">log</mi><mo>&#8289;</mo></mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">\log n</annotation></semantics></math> correlates with the average
depth of the search tree, and the depth of the search tree correlates with
strength. An effect of this assumption is that only the <em>ratio</em> of computational
effort matters: thinking 20x as long is assumed to be equally strong no matter
the absolute amount of computational effort invested.</p>
<p>By generating pairs <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false" form="prefix">(</mo><mi>n</mi><mo>,</mo><mi>m</mi><mo stretchy="false" form="postfix">)</mo></mrow><annotation encoding="application/x-tex">(n, m)</annotation></semantics></math> (sampled so <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi mathvariant="normal">log</mi><mo>&#8289;</mo></mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">\log n</annotation></semantics></math> and <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi mathvariant="normal">log</mi><mo>&#8289;</mo></mrow><mi>m</mi></mrow><annotation encoding="application/x-tex">\log m</annotation></semantics></math> are uniform and
i.i.d.) and running games, we can generate a dataset of outcomes and perform
logistic regression to calibrate our rating system. Something like the following
turns out to be a decent predictor:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode rust"><code class="sourceCode rust"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="kw">fn</span> p_red_win(red_rollouts<span class="op">:</span> <span class="dt">usize</span><span class="op">,</span> blue_rollouts<span class="op">:</span> <span class="dt">usize</span>) <span class="op">-&gt;</span> <span class="dt">f64</span> <span class="op">{</span></span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a>    <span class="kw">let</span> red_rank <span class="op">=</span> (red_rollouts <span class="kw">as</span> <span class="dt">f64</span>)<span class="op">.</span>log10()<span class="op">;</span></span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a>    <span class="kw">let</span> blue_rank <span class="op">=</span> (blue_rollouts <span class="kw">as</span> <span class="dt">f64</span>)<span class="op">.</span>log10()<span class="op">;</span></span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a>    <span class="kw">let</span> x <span class="op">=</span> red_rank <span class="op">-</span> blue_rank<span class="op">;</span></span>
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a>    (<span class="dv">0.3</span> <span class="op">+</span> <span class="dv">1.8</span> <span class="op">*</span> x)<span class="op">.</span>sigmoid()</span>
<span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb8-7"><a href="#cb8-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-8"><a href="#cb8-8" aria-hidden="true" tabindex="-1"></a><span class="kw">fn</span> sigmoid(x<span class="op">:</span> <span class="dt">f64</span>) <span class="op">-&gt;</span> <span class="dt">f64</span> <span class="op">{</span></span>
<span id="cb8-9"><a href="#cb8-9" aria-hidden="true" tabindex="-1"></a>    <span class="dv">1.0</span> <span class="op">/</span> (<span class="dv">1.0</span> <span class="op">+</span> (<span class="op">-</span>x)<span class="op">.</span>exp())</span>
<span id="cb8-10"><a href="#cb8-10" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>(The mathematically inclined will notice that I’ve got some terms with an
<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi mathvariant="normal">e</mi><mi mathvariant="normal">x</mi><mi mathvariant="normal">p</mi></mrow><mo stretchy="false" form="prefix">(</mo><mrow><mrow><mi mathvariant="normal">log</mi><mo>&#8289;</mo></mrow><mi>m</mi><mo>−</mo><mrow><mi mathvariant="normal">log</mi><mo>&#8289;</mo></mrow><mi>n</mi></mrow><mo stretchy="false" form="postfix">)</mo></mrow><annotation encoding="application/x-tex">\mathrm{exp}({\log m - \log n})</annotation></semantics></math> shape here meaning we can simplify to a
ratio of powers of <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>m</mi><annotation encoding="application/x-tex">m</annotation></semantics></math> and <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>n</mi><annotation encoding="application/x-tex">n</annotation></semantics></math> but I’ll leave that as an exercise.)</p>
<p>The <a href="https://github.com/aji/hex-table/blob/main/src/bin/mcts.rs">code</a> and <a href="https://github.com/aji/hex-table/blob/main/notebooks/skill.ipynb">notebook</a> for this
analysis are on GitHub.</p>
<h2 id="up-next">Up next</h2>
<p>In <a href="./2026-05-13-hexbot-part-3.html">the next post</a>, I’ll talk about how we can use an AlphaZero-style
neural network to significantly improve the effectiveness of MCTS.</p>
        </div>
      </content>
    </entry>
  
    <entry>
      <id>tag:ajitek.net,2024:blog/posts/2026-05-10-hexbot-part-1.md</id>
      <title>Hexbot Part 1: The 101 Unit</title>
      <link rel="alternate" type="text/html" href="https://aji.github.io/blog/posts/2026-05-10-hexbot-part-1.html" />
      <published>2026-05-10T12:00:00Z</published>
      <updated>2026-05-15T05:57:51Z</updated>
      <rights>
        This work (c) 2026 by Alex Iadicicco is licensed under CC BY-NC-SA 4.0.
        To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/
      </rights>
      
        <summary>A brief discussion of Hex and algorithms like Monte Carlo tree search (MCTS)</summary>
      
      <content type="xhtml" xml:lang="en" xml:base="https://aji.github.io/blog">
        <div xmlns="http://www.w3.org/1999/xhtml">
          
            <img src="https://aji.github.io/blog/images/hex-banner.png" />
          
          <blockquote>
<p>This is the first post in a series of posts about my foray into
building a bot to play Hex. The focus of this post is on Hex itself and board
game algorithms more generally.</p>
<p>Hexbot Part 1: The 101 Unit<br />
<a href="./2026-05-12-hexbot-part-2.html">Hexbot Part 2: The Board and Our First Bot</a><br />
<a href="./2026-05-13-hexbot-part-3.html">Hexbot Part 3: Using Neural Networks</a><br />
<a href="./2026-05-15-hexbot-part-4.html">Hexbot Part 4: Did It Work?</a></p>
</blockquote>
<p>I’ve been vaguely aware of Hex for years, but didn’t really <em>discover</em> it until
playing it in <em>Clubhouse Games: 51 Worldwide Classics</em> for Nintendo Switch. I
figured out rather quickly that it’s a very strategically interesting game,
kind of the checkers to go’s chess.</p>
<p>The game takes place on a grid of hexagonal tiles arranged in a rhombus. Many
sizes are possible, but <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>11</mn><mo>×</mo><mn>11</mn></mrow><annotation encoding="application/x-tex">11 \times 11</annotation></semantics></math> is popular:</p>
<figure>
<img src="../images/hex-board-empty.png" alt="An empty hex board." />
<figcaption aria-hidden="true">An empty hex board.</figcaption>
</figure>
<p>Players take turns placing pieces in unoccupied hexagons racing to be the first
to make a path between the edges of their color:</p>
<figure>
<img src="../images/hex-board-example.png" alt="A completed game in which red has won, since red has successfully made a path of red tiles between the two red edges." />
<figcaption aria-hidden="true">A completed game in which red has won, since red has successfully made a path of red tiles between the two red edges.</figcaption>
</figure>
<p>That’s all the rules!<a href="#fn1" class="footnote-ref" id="fnref1" role="doc-noteref"><sup>1</sup></a> And yet despite this simplicity, Hex has a lot of
strategic depth. (I won’t go into Hex strategy here. I’m not very good at the
game, and there are plenty of resources online for how to play hex including the
<a href="https://en.wikipedia.org/wiki/Hex_(board_game)">Wikipedia article</a>. However, if you want a taste for it, the best
thing to do would be to simply <a href="https://playhex.org/">try playing a few games</a>!)</p>
<blockquote>
<p><em>Terminology note:</em> The choice of colors for the first and second player
are a matter of convention. I’ve chosen to represent the first and second
players as red and blue respectively, and I’ll use this terminology
consistently throughout these posts.</p>
</blockquote>
<p>The simplicity of Hex gives it a number of interesting properties that are
easily shown from the rules.</p>
<ul>
<li><p>Games can never end in a draw, since blocking all paths for one color requires
making a complete path of the opposite color. This fact was known in 1942 by
Piet Hein, the game’s inventor.</p></li>
<li><p>It’s known that there exists a strategy for the first player to force a win,
although the specific strategy is not yet known. This fact is reflected in the
strong first-player advantage in pure Hex, which is commonly corrected using
a swap rule. The first written existence proof was given by John Nash
in 1952, and board sizes up to 9x9 have been completely solved by computers.</p></li>
</ul>
<p>The <a href="https://en.wikipedia.org/wiki/Hex_(board_game)">Wikipedia article</a> and <a href="https://mathworld.wolfram.com/GameofHex.html">Wolfram MathWorld page</a> for
Hex have additional information. Hex has also been written about extensively
in an academic context, especially in regards to game-playing AI, and there are
plenty of papers and other resources that discuss aspects of Hex in great
detail.</p>
<h2 id="lets-make-a-bot">Let’s make a bot!</h2>
<p>Making bots to play board games has always been an application of AI that
fascinates me, so naturally upon encountering Hex I decided I wanted to try
making a bot for it. I’ve made bots for board games before, such as for
<a href="https://github.com/aji/khet-table">Khet</a> (also known as Laser Chess), and most of what I’ve been doing
with Hex has been essentially a further iteration of that work. (The main
differences are that the Khet bots were all CPU-based, even the neural network
ones, and also that my math and programming skills have grown a bit since then.)</p>
<p>Our ultimate goal will be to build a bot similar to AlphaZero, an MCTS-based
search algorithm augmented by a neural network trained on self-play. Owing to
the popularity of Hex there are already plenty of <em>very</em> strong bots out there,
including prior art for this exact idea: <a href="https://www.hexwiki.net/index.php/KataHex">KataHex</a>, a fork of KataGo,
is a popular AlphaZero-style bot for Hex. But retracing those steps is
educational and fun, and what are board games about if not fun. The resulting
implementation is small, hackable, and surprisingly strong.</p>
<h2 id="how-a-computer-plays-a-board-game">How a computer plays a board game</h2>
<p>Hex, like many abstract strategy games, can be thought of as tree of board
positions where the edges represent moves. The root of the tree is the initial
board state, and a game proceeds by descending down the move tree. This tree
is <em>enormous</em>, making it infeasible to search exhaustively even up to a fairly
shallow depth. Most algorithms still search the move tree, but use heuristics to
decide which parts of the tree are worth spending time exploring. Search
algorithms vary wildly, from highly complex algorithms incorporating a lot of
domain knowledge, to very general ones that need no more than just an
understanding of the rules.</p>
<p>A popular search algorithm is <a href="https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning">alpha-beta search</a>, which
traverses the move tree using a number of heuristics in order to reduce the
branching factor. One such heuristic is a <em>value function</em> that can assign an
approximate “value” for a board state, e.g. for chess by scoring material
advantage. Though even simple value functions can result in good gameplay, the
strength of the algorithm relies on this function’s accuracy, and necessarily
incorporates domain knowledge about the game, e.g. for chess the relative value
of the different pieces. Additionally, alpha-beta search performs better when
visiting the most promising moves first, called <em>move ordering</em>, which is yet
another way its performance relies on domain knowledge. While the sensitivity
of alpha-beta search to domain knowledge is not by itself a problem (and indeed,
strong chess programs such as <a href="https://en.wikipedia.org/wiki/Stockfish_(chess)">Stockfish</a> and
<a href="https://en.wikipedia.org/wiki/Komodo_(chess)">Komodo</a> have used this algorithm to great success), it does
require the author to have some understanding about the game’s strategy.<a href="#fn2" class="footnote-ref" id="fnref2" role="doc-noteref"><sup>2</sup></a></p>
<p>The most general practical search algorithm is arguably a pure <strong>Monte Carlo
tree search</strong> (MCTS), where one only has to be able to take a board state and
determine if it’s terminal (win/loss/draw) or enumerate all the valid moves from
it. The <a href="https://en.wikipedia.org/wiki/Monte_Carlo_tree_search">Wikipedia article</a> provides a good explanation of how it
works, but since it forms the foundation of the AlphaZero-based model we will be
implementing, it’s worth going over.</p>
<h2 id="monte-carlo-tree-search-mcts">Monte Carlo tree search (MCTS)</h2>
<p>An MCTS search is iterative, where each iteration adds a new leaf node to a game
tree data structure kept in memory. The algorithm chooses where to add the node
by descending the tree, choosing edges in a way that balances <em>exploration</em> of
under-explored subtrees and <em>exploitation</em> of promising subtrees (from the
perspective of which player is to make the given move). Upon descending to
where a new leaf node is to be added, the algorithm performs a
<em>rollout</em> (also sometimes called a “simulation”) to assign a value
(win/loss/draw) to the new node: random moves are played until reaching a
terminal state, which becomes the initial value of this node. This value is
then propagated back up the tree, with the average value of each subtree
being aggregated all the way up to the root.</p>
<p>These iterations can be performed as many times as desired, with each iteration
adding to the amount of information available at the root node. When the search
is stopped (e.g. by hitting an iteration count or computation time limit) the
algorithm plays the move represented by the node with the largest visit count,
i.e. the number of times the algorithm descended to that node. (Note that the
node with the largest <em>value</em> is not chosen, but the search algorithm will
visit high-value nodes many times, so this is a subtle but minor distinction.)</p>
<p>The reason MCTS works at all might not be immediately obvious, since valuing new
leaf nodes by playing random moves to the end of the game might seem like it
provides very little useful information at all. However, the idea is that each
iteration descends the move tree all the way to a single terminal state, using
information from previous traversals when available and random moves otherwise,
then aggregates this information up the tree for later iterations. In other
words each iteration of MCTS is <em>sampling</em> the entire move tree, with each new
sample adding more detail.</p>
<h2 id="up-next">Up next</h2>
<p>In <a href="./2026-05-12-hexbot-part-2.html">the next post</a>, I’ll be covering the board representation and go
more in depth on the baseline MCTS implementation.</p>
<section id="footnotes" class="footnotes footnotes-end-of-document" role="doc-endnotes">
<hr />
<ol>
<li id="fn1"><p>An additional rule called the “swap rule” is commonly implemented as
well, in order to correct for the rather significant first-player advantage.
If playing with the swap rule, then after the first move the second player can
choose to swap sides, thus incentivizing the first player to choose a move that
is not too strong for either player.<a href="#fnref1" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn2"><p>It’s interesting to think what kind of value function and move
ordering strategy could be used for a Hex bot based on alpha-beta search. Hex
strategy revolves essentially entirely around positional judgement, and concepts
like “material advantage” don’t exist at all. There <em>are</em> alpha-beta
implementations for Hex and I’ll leave it as a homework assignment to go learn
about what value functions they use.<a href="#fnref2" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
</ol>
</section>
        </div>
      </content>
    </entry>
  
    <entry>
      <id>tag:ajitek.net,2024:blog/posts/2024-07-11-undefined-behavior.md</id>
      <title>Undefined Behavior</title>
      <link rel="alternate" type="text/html" href="https://aji.github.io/blog/posts/2024-07-11-undefined-behavior.html" />
      <published>2024-07-11T12:00:00Z</published>
      <updated>2025-11-13T05:18:30Z</updated>
      <rights>
        This work (c) 2024 by Alex Iadicicco is licensed under CC BY-NC-SA 4.0.
        To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/
      </rights>
      
        <summary>It's really not that complicated</summary>
      
      <content type="xhtml" xml:lang="en" xml:base="https://aji.github.io/blog">
        <div xmlns="http://www.w3.org/1999/xhtml">
          
          <p>There’s a lot of confusion among programmers about what C’s “undefined
behavior”, or “UB” as it is commonly abbreviated, is for, and why C compilers
are allowed to assume UB never happens. This post won’t talk about why the spec
contains UB, but will attempt to shed some light on what from my perspective is
a rather confusing aspect of how compilers work with it.</p>
<p>A particularly tricky aspect of UB for the typical C programmer is that it
sometimes causes the compiler to do counterintuitive things, even to seemingly
unrelated parts of the program. It’s a confusing moment at first when disabling
optimizations changes the visible effects of your program. If UB represents some
kind of abstract “out-of-spec” error condition, why is the compiler allowed to
change the behavior of statements leading up to it? There is a very reasonable
explanation you could give here, how the spec covers the meaning of whole
programs and not just individual statements, but I think it’s much easier
understood via time travel.</p>
<p>The <code>unreachable()</code> macro is one way for a program to explicitly invoke
undefined behavior. Because the behavior is undefined, the implementation of
<code>unreachable()</code> could be a program that sends a robot back in time to prevent
the code from running, then destroys the entire world so that none of us are
alive in a timeline where <code>unreachable()</code> finishes execution. A final
<code>printf("Undefined behavior can *never* occur.\n");</code> is the last thing the
universe sees.</p>
<p>This solution may of course have some retroactive effects that at first seem
unrelated. The time traveling robot may choose not to simply terminate the
program right before it enters <code>unreachable()</code>, and may instead prevent the
program from being started in the first place. It might even delete the code
that was compiled to produce the program, leaving a cryptic commit message
before deactivating itself in a car crusher. It might go even further back in
time to prevent the programmer’s parents from meeting. An oversight in the
robot’s programming might even cause it to be overzealous in its interpretation
of its mission, as it points the time machine to “1971, Bell Labs.” Technically,
the specification prohibits none of these things.</p>
<p>Pending the development of time travel, though, foresight will have to do.
Timelines containing an invocation of UB can be safely ignored, and while
compilers are not mandated to <em>prevent</em> its occurrence, or even informed that
they should assume it never occurs, it’s a useful model for what a conforming
compiler is allowed to do in the remaining timelines. The futures where the
optimizer breaks its neck doing assembly parkour are not futures we expect to
find ourselves in. Or at least, their behavior is undefined.</p>
        </div>
      </content>
    </entry>
  
    <entry>
      <id>tag:ajitek.net,2024:blog/posts/2024-07-01-fast-dice.md</id>
      <title>Fast Dice</title>
      <link rel="alternate" type="text/html" href="https://aji.github.io/blog/posts/2024-07-01-fast-dice.html" />
      <published>2024-07-01T12:00:00Z</published>
      <updated>2025-11-13T05:18:30Z</updated>
      <rights>
        This work (c) 2024 by Alex Iadicicco is licensed under CC BY-NC-SA 4.0.
        To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/
      </rights>
      
        <summary>Don't take any chances without knowing the odds</summary>
      
      <content type="xhtml" xml:lang="en" xml:base="https://aji.github.io/blog">
        <div xmlns="http://www.w3.org/1999/xhtml">
          
            <img src="https://aji.github.io/blog/images/2024-07-01-fast-dice.jpg" />
          
          <p>I got nerd sniped by a fun JavaScript performance puzzle recently, having to do
with efficiently calculating the probability of a dice-based Bernoulli trial, for
the purpose of a game I’m working on. It goes like this:</p>
<ul>
<li><p>The player works to collect sets of up to 4 dice, which can be d4s, d6s, or d10s.</p></li>
<li><p>The player then chooses up to 10 sets of dice from their collection and rolls them.</p></li>
<li><p>If at least 3 of the sets rolled have a total of 10 or more, the player
advances to the next round. All rolled sets are removed from the player’s
collection, regardless of whether they advance or not.</p></li>
</ul>
<p>It’s helpful to be able to calculate the exact probabilities involved when
balancing the game, in addition to playtesting normally. Furthermore, having the
exact probabilities allows game design elements to reflect the chance of success
(e.g. showing things in a different color) in a way that gives the player <em>some</em>
useful information while still leaving a bit of uncertainty. However, with the
way this process is designed, calculating the probabilities presents a few
challenges:</p>
<ul>
<li><p>Sets can have any combination of dice, e.g. 4d6, 2d4+d10, 3d6+d10, etc.</p></li>
<li><p>Sets of dice are tested based on their sum.</p></li>
<li><p>The player advances if at least 3 sets score 10 or more, but have the option
to play more than 3 if they think it would help their chances enough to be worth
it.</p></li>
</ul>
<p>So, to get this problem out of the way and avoid coming up with any tricky
equations, I went with the most general solution I know: convolutions of
probability distributions. (This formulation is equivalent to multiplying
probability generating functions, since we are dealing with discrete random
variables.)</p>
<h2 id="background-too-convoluted">Background: Too convoluted?</h2>
<p>The calculation works in two passes:</p>
<ol type="1">
<li><p>For each set of dice, generate a CDF of the dice total by reducing the PMFs
of the dice with convolutions and doing a cumulative sum. The CDF at 9
represents the Bernoulli parameter for whether the set fails to reach a score
of 10.</p></li>
<li><p>For each Bernoulli parameter <em>p</em> calculated in the previous step, generate a
PMF [<em>p</em>, 1-<em>p</em>] to represent a random variable that has a value of 1 if
the test succeeds and 0 otherwise. Reduce these by convolution and do a
cumulative sum to get the CDF for the sum of these scores. The CDF at 2
represents the Bernoulli parameter for whether fewer than 3 sets succeeded.</p></li>
</ol>
<p>If you’re not familiar with convolution, <a href="https://www.youtube.com/watch?v=KuXjwB4LzSA">the 3blue1brown video</a> about it
is an excellent introduction. However, if you’re in a hurry, you can think of it
as a multiplication of two polynomials, where the <em>i</em>-th element of each input
is the coefficient of <em>x</em><sup><em>i</em></sup>. The fact that the probability
distribution of a sum of random variables is a convolution of their individual
distributions is extremely useful for numerically calculating probabilities
where simpler analytic solutions are out of reach, and is the foundation of the
approach outlined above.</p>
<h2 id="attempt-1-just-write-the-code">Attempt 1: Just write the code</h2>
<p>These days JavaScript VMs are very fast, so I generally try not to overthink
things unless there is a clear need for it. Naturally I wrote some code that
looked like this:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode javascript"><code class="sourceCode javascript"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">range</span>(n) {</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>  <span class="cf">return</span> [<span class="op">...</span><span class="bu">Array</span>(n)<span class="op">.</span><span class="fu">keys</span>()]<span class="op">;</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">sum</span>(a) {</span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>  <span class="cf">return</span> a<span class="op">.</span><span class="fu">reduce</span>((x<span class="op">,</span> y) <span class="kw">=&gt;</span> x <span class="op">+</span> y<span class="op">,</span> <span class="dv">0</span>)<span class="op">;</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">convolve</span>(a<span class="op">,</span> b) {</span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>  <span class="cf">return</span> <span class="fu">range</span>(a<span class="op">.</span><span class="at">length</span> <span class="op">+</span> b<span class="op">.</span><span class="at">length</span> <span class="op">-</span> <span class="dv">1</span>)<span class="op">.</span><span class="fu">map</span>((i) <span class="kw">=&gt;</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>    <span class="fu">sum</span>(<span class="fu">range</span>(b<span class="op">.</span><span class="at">length</span>)<span class="op">.</span><span class="fu">map</span>((j) <span class="kw">=&gt;</span> (a[i <span class="op">-</span> j] <span class="op">??</span> <span class="dv">0</span>) <span class="op">*</span> b[j]))</span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a>  )<span class="op">;</span></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">cumsum</span>(a) {</span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a>  <span class="kw">let</span> sum <span class="op">=</span> <span class="dv">0</span><span class="op">;</span></span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a>  <span class="cf">return</span> a<span class="op">.</span><span class="fu">map</span>((x) <span class="kw">=&gt;</span> (sum <span class="op">+=</span> x))<span class="op">;</span></span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">dicepmf</span>(n) {</span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a>  <span class="cf">return</span> <span class="fu">range</span>(n <span class="op">+</span> <span class="dv">1</span>)<span class="op">.</span><span class="fu">map</span>((i) <span class="kw">=&gt;</span> (<span class="dv">1</span> <span class="op">&lt;=</span> i <span class="op">&amp;&amp;</span> i <span class="op">&lt;=</span> n <span class="op">?</span> <span class="dv">1</span> <span class="op">/</span> n <span class="op">:</span> <span class="dv">0</span>))<span class="op">;</span></span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-20"><a href="#cb1-20" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">pSetFail</span>(ds) {</span>
<span id="cb1-21"><a href="#cb1-21" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> cdf <span class="op">=</span> <span class="fu">cumsum</span>(ds<span class="op">.</span><span class="fu">map</span>((n) <span class="kw">=&gt;</span> <span class="fu">dicepmf</span>(n))<span class="op">.</span><span class="fu">reduce</span>(convolve<span class="op">,</span> [<span class="dv">1</span>]))<span class="op">;</span></span>
<span id="cb1-22"><a href="#cb1-22" aria-hidden="true" tabindex="-1"></a>  <span class="cf">return</span> cdf[<span class="dv">9</span>] <span class="op">??</span> <span class="dv">1</span><span class="op">;</span></span>
<span id="cb1-23"><a href="#cb1-23" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb1-24"><a href="#cb1-24" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">pSetsFail</span>(sets) {</span>
<span id="cb1-25"><a href="#cb1-25" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> pdf <span class="op">=</span> sets</span>
<span id="cb1-26"><a href="#cb1-26" aria-hidden="true" tabindex="-1"></a>    <span class="op">.</span><span class="fu">map</span>(pSetFail)</span>
<span id="cb1-27"><a href="#cb1-27" aria-hidden="true" tabindex="-1"></a>    <span class="op">.</span><span class="fu">map</span>((p) <span class="kw">=&gt;</span> [p<span class="op">,</span> <span class="dv">1</span> <span class="op">-</span> p])</span>
<span id="cb1-28"><a href="#cb1-28" aria-hidden="true" tabindex="-1"></a>    <span class="op">.</span><span class="fu">reduce</span>(convolve<span class="op">,</span> [<span class="dv">1</span>])<span class="op">;</span></span>
<span id="cb1-29"><a href="#cb1-29" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> cdf <span class="op">=</span> <span class="fu">cumsum</span>(pdf)<span class="op">;</span></span>
<span id="cb1-30"><a href="#cb1-30" aria-hidden="true" tabindex="-1"></a>  <span class="cf">return</span> cdf[<span class="dv">2</span>] <span class="op">??</span> <span class="dv">1</span><span class="op">;</span></span>
<span id="cb1-31"><a href="#cb1-31" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb1-32"><a href="#cb1-32" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-33"><a href="#cb1-33" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> sets <span class="op">=</span> [</span>
<span id="cb1-34"><a href="#cb1-34" aria-hidden="true" tabindex="-1"></a>  [<span class="dv">4</span><span class="op">,</span> <span class="dv">6</span><span class="op">,</span> <span class="dv">6</span>]<span class="op">,</span></span>
<span id="cb1-35"><a href="#cb1-35" aria-hidden="true" tabindex="-1"></a>  [<span class="dv">4</span><span class="op">,</span> <span class="dv">6</span><span class="op">,</span> <span class="dv">6</span>]<span class="op">,</span></span>
<span id="cb1-36"><a href="#cb1-36" aria-hidden="true" tabindex="-1"></a>  [<span class="dv">4</span><span class="op">,</span> <span class="dv">6</span><span class="op">,</span> <span class="dv">6</span>]<span class="op">,</span></span>
<span id="cb1-37"><a href="#cb1-37" aria-hidden="true" tabindex="-1"></a>]<span class="op">;</span></span>
<span id="cb1-38"><a href="#cb1-38" aria-hidden="true" tabindex="-1"></a><span class="bu">console</span><span class="op">.</span><span class="fu">log</span>(<span class="dv">1</span> <span class="op">-</span> <span class="fu">pSetsFail</span>(sets))<span class="op">;</span></span></code></pre></div>
<p>If we run the above, we get <code>0.125</code>, which is what we would expect, since the
odds of d4+2d6 totaling 10 or more is exactly 50%. In other words, we’re
flipping a coin 3 times and trying to get HHH.</p>
<p>So this solution works and has the benefit of being concise and readable, but
how fast is it? Naive convolution is O(<em>n</em><sup>2</sup>), so let’s time its worst
case in our context, 10 attempts of 4d10:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode javascript"><code class="sourceCode javascript"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> worstcase <span class="op">=</span> <span class="fu">range</span>(<span class="dv">10</span>)<span class="op">.</span><span class="fu">map</span>(() <span class="kw">=&gt;</span> [<span class="dv">10</span><span class="op">,</span> <span class="dv">10</span><span class="op">,</span> <span class="dv">10</span><span class="op">,</span> <span class="dv">10</span>])<span class="op">;</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> start <span class="op">=</span> <span class="bu">performance</span><span class="op">.</span><span class="fu">now</span>()<span class="op">;</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> (<span class="kw">let</span> i <span class="op">=</span> <span class="dv">0</span><span class="op">;</span> i <span class="op">&lt;</span> <span class="dv">1000</span><span class="op">;</span> i<span class="op">++</span>) <span class="fu">pSetsFail</span>(worstcase)<span class="op">;</span></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> end <span class="op">=</span> <span class="bu">performance</span><span class="op">.</span><span class="fu">now</span>()<span class="op">;</span></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a><span class="bu">console</span><span class="op">.</span><span class="fu">log</span>((start <span class="op">-</span> end) <span class="op">/</span> <span class="dv">1000</span>)<span class="op">;</span></span></code></pre></div>
<p>This comes out to about 1.4ms on my machine in Node.js, which is definitely
usable if you only need it occasionally. Okay, problem solved, let’s move onto
the next thing, we’ve got a game to build.</p>
<p>…But it does seem a bit slow, doesn’t it? We’re only dealing with 40 dice
here, and only up to d10. What if we want to offer d20s or d100s? We’re just
doing multiplications and additions, so surely we can do better, right? We
should be able to call this 100 times a frame if we want!</p>
<h2 id="attempt-2-loops">Attempt 2: Loops</h2>
<p>The <code>convolve</code> function we wrote is definitely a big part of the problem, and
you don’t need a profiler to figure that out. It’s two loops, where the inner
loop is generating an array just to calculate its sum. We’re also deliberately
looking up keys that don’t exist and using the nullish coalescing operator to
convert them to 0s, instead of explicitly checking the index. Furthermore,
we’re calculating up to 41 elements of each PMF when we only need the first 10.
We can omit those without changing the result. There’s a lot of room for some
cheap improvements and we owe it to ourselves to at least try, don’t we?</p>
<p>In particular, we suspect the following things might help:</p>
<ol type="1">
<li><p>Replace most calls to <code>range()</code>, <code>map()</code>, and <code>reduce()</code> with explicit loops and mutation.</p></li>
<li><p>Replace the nullish coalescing operator with an explicit bounds check.</p></li>
<li><p>Truncate the PMFs.</p></li>
</ol>
<p>When we make the above changes, the code looks like this:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode javascript"><code class="sourceCode javascript"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">convolve</span>(a<span class="op">,</span> b<span class="op">,</span> limit) {</span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> n <span class="op">=</span> <span class="bu">Math</span><span class="op">.</span><span class="fu">min</span>(limit<span class="op">,</span> a<span class="op">.</span><span class="at">length</span> <span class="op">+</span> b<span class="op">.</span><span class="at">length</span> <span class="op">-</span> <span class="dv">1</span>)<span class="op">;</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> out <span class="op">=</span> []<span class="op">;</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>  <span class="cf">for</span> (<span class="kw">let</span> i <span class="op">=</span> <span class="dv">0</span><span class="op">;</span> i <span class="op">&lt;</span> n<span class="op">;</span> i<span class="op">++</span>) {</span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a>    out[i] <span class="op">=</span> <span class="dv">0</span><span class="op">;</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a>    <span class="cf">for</span> (<span class="kw">let</span> j <span class="op">=</span> <span class="dv">0</span><span class="op">;</span> j <span class="op">&lt;</span> b<span class="op">.</span><span class="at">length</span><span class="op">;</span> j<span class="op">++</span>) {</span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>      <span class="kw">const</span> k <span class="op">=</span> i <span class="op">-</span> j<span class="op">;</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a>      <span class="cf">if</span> (<span class="dv">0</span> <span class="op">&lt;=</span> k <span class="op">&amp;&amp;</span> k <span class="op">&lt;</span> a<span class="op">.</span><span class="at">length</span>) {</span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a>        out[i] <span class="op">+=</span> a[k] <span class="op">*</span> b[j]<span class="op">;</span></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a>      }</span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a>    }</span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a>  }</span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a>  <span class="cf">return</span> out<span class="op">;</span></span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">cumsum</span>(a) {</span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a>  <span class="cf">for</span> (<span class="kw">let</span> i <span class="op">=</span> <span class="dv">1</span><span class="op">;</span> i <span class="op">&lt;</span> a<span class="op">.</span><span class="at">length</span><span class="op">;</span> i<span class="op">++</span>) {</span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a>    a[i] <span class="op">+=</span> a[i <span class="op">-</span> <span class="dv">1</span>]<span class="op">;</span></span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a>  }</span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a>  <span class="cf">return</span> a<span class="op">;</span></span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">dicepmf</span>(n) {</span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> out <span class="op">=</span> [<span class="dv">0</span>]<span class="op">;</span></span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a>  <span class="cf">for</span> (<span class="kw">let</span> i <span class="op">=</span> <span class="dv">1</span><span class="op">;</span> i <span class="op">&lt;=</span> n<span class="op">;</span> i<span class="op">++</span>) {</span>
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a>    out[i] <span class="op">=</span> <span class="dv">1</span> <span class="op">/</span> n<span class="op">;</span></span>
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a>  }</span>
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a>  <span class="cf">return</span> out<span class="op">;</span></span>
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-29"><a href="#cb3-29" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">pSetFail</span>(ds) {</span>
<span id="cb3-30"><a href="#cb3-30" aria-hidden="true" tabindex="-1"></a>  <span class="kw">let</span> pdf <span class="op">=</span> [<span class="dv">1</span>]<span class="op">;</span></span>
<span id="cb3-31"><a href="#cb3-31" aria-hidden="true" tabindex="-1"></a>  <span class="cf">for</span> (<span class="kw">const</span> n <span class="kw">of</span> ds) {</span>
<span id="cb3-32"><a href="#cb3-32" aria-hidden="true" tabindex="-1"></a>    pdf <span class="op">=</span> <span class="fu">convolve</span>(pdf<span class="op">,</span> <span class="fu">dicepmf</span>(n)<span class="op">,</span> <span class="dv">10</span>)<span class="op">;</span></span>
<span id="cb3-33"><a href="#cb3-33" aria-hidden="true" tabindex="-1"></a>  }</span>
<span id="cb3-34"><a href="#cb3-34" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> cdf <span class="op">=</span> <span class="fu">cumsum</span>(pdf)<span class="op">;</span></span>
<span id="cb3-35"><a href="#cb3-35" aria-hidden="true" tabindex="-1"></a>  <span class="cf">return</span> cdf<span class="op">.</span><span class="at">length</span> <span class="op">&lt;</span> <span class="dv">10</span> <span class="op">?</span> <span class="dv">1</span> <span class="op">:</span> cdf[<span class="dv">9</span>]<span class="op">;</span></span>
<span id="cb3-36"><a href="#cb3-36" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb3-37"><a href="#cb3-37" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">pSetsFail</span>(sets) {</span>
<span id="cb3-38"><a href="#cb3-38" aria-hidden="true" tabindex="-1"></a>  <span class="kw">let</span> pdf <span class="op">=</span> [<span class="dv">1</span>]<span class="op">;</span></span>
<span id="cb3-39"><a href="#cb3-39" aria-hidden="true" tabindex="-1"></a>  <span class="cf">for</span> (<span class="kw">const</span> ds <span class="kw">of</span> sets) {</span>
<span id="cb3-40"><a href="#cb3-40" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> p <span class="op">=</span> <span class="fu">pSetFail</span>(ds)<span class="op">;</span></span>
<span id="cb3-41"><a href="#cb3-41" aria-hidden="true" tabindex="-1"></a>    pdf <span class="op">=</span> <span class="fu">convolve</span>(pdf<span class="op">,</span> [p<span class="op">,</span> <span class="dv">1</span> <span class="op">-</span> p]<span class="op">,</span> <span class="dv">3</span>)<span class="op">;</span></span>
<span id="cb3-42"><a href="#cb3-42" aria-hidden="true" tabindex="-1"></a>  }</span>
<span id="cb3-43"><a href="#cb3-43" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> cdf <span class="op">=</span> <span class="fu">cumsum</span>(pdf)<span class="op">;</span></span>
<span id="cb3-44"><a href="#cb3-44" aria-hidden="true" tabindex="-1"></a>  <span class="cf">return</span> cdf<span class="op">.</span><span class="at">length</span> <span class="op">&lt;</span> <span class="dv">3</span> <span class="op">?</span> <span class="dv">1</span> <span class="op">:</span> cdf[<span class="dv">2</span>]<span class="op">;</span></span>
<span id="cb3-45"><a href="#cb3-45" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
<p>We’ve sacrificed a bit of conciseness perhaps but this is still quite readable.
After quickly double-checking that our first example still prints 0.125, we time
it and find that each calculation of the worst case input only takes around 15
<em>microseconds</em>. That’s a 90x improvement!</p>
<p>Of course, we should probably identify how much of an impact each of the above
changes had, so here’s rough timing figures for each one individually:</p>
<ul>
<li><p>Loops and mutation: 340us, 4.0x improvement</p></li>
<li><p>Bounds checks: 1.0ms, 1.3x improvement</p></li>
<li><p>PMF truncation: 670us, 2.0x improvement</p></li>
</ul>
<p>Loops definitely seem to have the biggest impact by themselves… but where is
the 90x improvement coming from when taken together? Microbenching a language
like JavaScript in the particular way I’m doing isn’t an exact science but there
is definitely something fishy going on. Let’s try the other direction and remove
each optimization from the fastest solution to see which one results in the
biggest slowdown:</p>
<ul>
<li><p>No loops and mutation: 410us, 28x slowdown</p></li>
<li><p>No bounds checks: 290us, 20x slowdown</p></li>
<li><p>No PMF truncation: 39us, 2.6x slowdown</p></li>
</ul>
<p>Bizarre! Loops once again seem to responsible for the biggest improvement, but
the bounds checks are an impressive factor as well. I double checked the code to
make sure I didn’t get something wrong here, but using explicit bounds checks
does seem to be responsible for a noticeable improvement. I don’t know enough
about V8 to know why this would be the case, but it’s an interesting thing to
keep in mind when trying to write JIT-friendly code I suppose. Maybe I’ll do a
deep dive someday to figure out why this happens.</p>
<p>But okay, 15 microseconds is pretty dang fast, and that’s a worst case! If we
use the 3 sets of d4+2d6 input we’ve been using for validation, we get speeds
closer to 2.5 microseconds. So we’re done, right?</p>
<p>Right??</p>
<h2 id="attempt-3-insanity">Attempt 3: Insanity</h2>
<p>Our calculation has a very predictable shape: ten times do 10-element
convolutions of four sequences and sum their entries, then do 3-element
convolutions of ten sequences and sum those. What if we just… hard-coded this?
No loops, minimal branches. How fast would it be?</p>
<p>I won’t bore you with the details and will just show you the code I came up
with. I’m not going to <em>use</em> this code, of course. It just felt like a fun
puzzle. Here’s the function that calculates the same result as <code>pSetFail()</code>
above, with slightly different parameters:</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode javascript"><code class="sourceCode javascript"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">pSetFail</span>(d0<span class="op">,</span> d1<span class="op">,</span> d2<span class="op">,</span> d3) {</span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>  <span class="kw">let</span> w0<span class="op">,</span> w1<span class="op">,</span> w2<span class="op">,</span> w3<span class="op">,</span> w4<span class="op">,</span> w5<span class="op">,</span> w6<span class="op">,</span> w7<span class="op">,</span> w8<span class="op">,</span> w9<span class="op">;</span></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a>  <span class="kw">let</span> x0<span class="op">,</span> x1<span class="op">,</span> x2<span class="op">,</span> x3<span class="op">,</span> x4<span class="op">,</span> x5<span class="op">,</span> x6<span class="op">,</span> x7<span class="op">,</span> x8<span class="op">,</span> x9<span class="op">;</span></span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a>  <span class="kw">let</span> y0<span class="op">,</span> y1<span class="op">,</span> y2<span class="op">,</span> y3<span class="op">,</span> y4<span class="op">,</span> y5<span class="op">,</span> y6<span class="op">,</span> y7<span class="op">,</span> y8<span class="op">,</span> y9<span class="op">;</span></span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a>  <span class="kw">let</span> z0<span class="op">,</span> z1<span class="op">,</span> z2<span class="op">,</span> z3<span class="op">,</span> z4<span class="op">,</span> z5<span class="op">,</span> z6<span class="op">,</span> z7<span class="op">,</span> z8<span class="op">,</span> z9<span class="op">;</span></span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a>  <span class="kw">let</span> m<span class="op">;</span></span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a>  w0 <span class="op">=</span> d0 <span class="op">===</span> <span class="dv">0</span> <span class="op">?</span> <span class="dv">1</span> <span class="op">:</span> <span class="dv">0</span><span class="op">;</span></span>
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a>  w1 <span class="op">=</span> d0  <span class="op">&gt;=</span> <span class="dv">1</span> <span class="op">?</span> <span class="dv">1</span> <span class="op">:</span> <span class="dv">0</span><span class="op">;</span></span>
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a>  w2 <span class="op">=</span> d0  <span class="op">&gt;=</span> <span class="dv">2</span> <span class="op">?</span> <span class="dv">1</span> <span class="op">:</span> <span class="dv">0</span><span class="op">;</span></span>
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a>  w3 <span class="op">=</span> d0  <span class="op">&gt;=</span> <span class="dv">3</span> <span class="op">?</span> <span class="dv">1</span> <span class="op">:</span> <span class="dv">0</span><span class="op">;</span></span>
<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a>  w4 <span class="op">=</span> d0  <span class="op">&gt;=</span> <span class="dv">4</span> <span class="op">?</span> <span class="dv">1</span> <span class="op">:</span> <span class="dv">0</span><span class="op">;</span></span>
<span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a>  w5 <span class="op">=</span> d0  <span class="op">&gt;=</span> <span class="dv">5</span> <span class="op">?</span> <span class="dv">1</span> <span class="op">:</span> <span class="dv">0</span><span class="op">;</span></span>
<span id="cb4-14"><a href="#cb4-14" aria-hidden="true" tabindex="-1"></a>  w6 <span class="op">=</span> d0  <span class="op">&gt;=</span> <span class="dv">6</span> <span class="op">?</span> <span class="dv">1</span> <span class="op">:</span> <span class="dv">0</span><span class="op">;</span></span>
<span id="cb4-15"><a href="#cb4-15" aria-hidden="true" tabindex="-1"></a>  w7 <span class="op">=</span> d0  <span class="op">&gt;=</span> <span class="dv">7</span> <span class="op">?</span> <span class="dv">1</span> <span class="op">:</span> <span class="dv">0</span><span class="op">;</span></span>
<span id="cb4-16"><a href="#cb4-16" aria-hidden="true" tabindex="-1"></a>  w8 <span class="op">=</span> d0  <span class="op">&gt;=</span> <span class="dv">8</span> <span class="op">?</span> <span class="dv">1</span> <span class="op">:</span> <span class="dv">0</span><span class="op">;</span></span>
<span id="cb4-17"><a href="#cb4-17" aria-hidden="true" tabindex="-1"></a>  w9 <span class="op">=</span> d0  <span class="op">&gt;=</span> <span class="dv">9</span> <span class="op">?</span> <span class="dv">1</span> <span class="op">:</span> <span class="dv">0</span><span class="op">;</span></span>
<span id="cb4-18"><a href="#cb4-18" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-19"><a href="#cb4-19" aria-hidden="true" tabindex="-1"></a>  m <span class="op">=</span> <span class="dv">1</span><span class="op">;</span></span>
<span id="cb4-20"><a href="#cb4-20" aria-hidden="true" tabindex="-1"></a>  m <span class="op">*=</span> d0 <span class="op">===</span> <span class="dv">0</span> <span class="op">?</span> <span class="dv">1</span> <span class="op">:</span> d0<span class="op">;</span></span>
<span id="cb4-21"><a href="#cb4-21" aria-hidden="true" tabindex="-1"></a>  m <span class="op">*=</span> d1 <span class="op">===</span> <span class="dv">0</span> <span class="op">?</span> <span class="dv">1</span> <span class="op">:</span> d1<span class="op">;</span></span>
<span id="cb4-22"><a href="#cb4-22" aria-hidden="true" tabindex="-1"></a>  m <span class="op">*=</span> d2 <span class="op">===</span> <span class="dv">0</span> <span class="op">?</span> <span class="dv">1</span> <span class="op">:</span> d2<span class="op">;</span></span>
<span id="cb4-23"><a href="#cb4-23" aria-hidden="true" tabindex="-1"></a>  m <span class="op">*=</span> d3 <span class="op">===</span> <span class="dv">0</span> <span class="op">?</span> <span class="dv">1</span> <span class="op">:</span> d3<span class="op">;</span></span>
<span id="cb4-24"><a href="#cb4-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-25"><a href="#cb4-25" aria-hidden="true" tabindex="-1"></a>  <span class="cf">switch</span> (d1) {</span>
<span id="cb4-26"><a href="#cb4-26" aria-hidden="true" tabindex="-1"></a>    <span class="cf">case</span> <span class="dv">0</span><span class="op">:</span></span>
<span id="cb4-27"><a href="#cb4-27" aria-hidden="true" tabindex="-1"></a>      x0<span class="op">=</span>w0<span class="op">;</span>       x1<span class="op">=</span>w1<span class="op">;</span>       x2<span class="op">=</span>w2<span class="op">;</span>       x3<span class="op">=</span>w3<span class="op">;</span>       x4<span class="op">=</span>w4<span class="op">;</span></span>
<span id="cb4-28"><a href="#cb4-28" aria-hidden="true" tabindex="-1"></a>      x5<span class="op">=</span>w5<span class="op">;</span>       x6<span class="op">=</span>w6<span class="op">;</span>       x7<span class="op">=</span>w7<span class="op">;</span>       x8<span class="op">=</span>w8<span class="op">;</span>       x9<span class="op">=</span>w9<span class="op">;</span>       <span class="cf">break</span><span class="op">;</span></span>
<span id="cb4-29"><a href="#cb4-29" aria-hidden="true" tabindex="-1"></a>    <span class="cf">case</span> <span class="dv">4</span><span class="op">:</span></span>
<span id="cb4-30"><a href="#cb4-30" aria-hidden="true" tabindex="-1"></a>      x0<span class="op">=</span><span class="dv">0</span><span class="op">;</span>        x1<span class="op">=</span>w0<span class="op">+</span>x0<span class="op">;</span>    x2<span class="op">=</span>w1<span class="op">+</span>x1<span class="op">;</span>    x3<span class="op">=</span>w2<span class="op">+</span>x2<span class="op">;</span>    x4<span class="op">=</span>w3<span class="op">+</span>x3<span class="op">;</span></span>
<span id="cb4-31"><a href="#cb4-31" aria-hidden="true" tabindex="-1"></a>      x5<span class="op">=</span>w4<span class="op">+</span>x4<span class="op">-</span>w0<span class="op">;</span> x6<span class="op">=</span>w5<span class="op">+</span>x5<span class="op">-</span>w1<span class="op">;</span> x7<span class="op">=</span>w6<span class="op">+</span>x6<span class="op">-</span>w2<span class="op">;</span> x8<span class="op">=</span>w7<span class="op">+</span>x7<span class="op">-</span>w3<span class="op">;</span> x9<span class="op">=</span>w8<span class="op">+</span>x8<span class="op">-</span>w4<span class="op">;</span> <span class="cf">break</span><span class="op">;</span></span>
<span id="cb4-32"><a href="#cb4-32" aria-hidden="true" tabindex="-1"></a>    <span class="cf">case</span> <span class="dv">6</span><span class="op">:</span></span>
<span id="cb4-33"><a href="#cb4-33" aria-hidden="true" tabindex="-1"></a>      x0<span class="op">=</span><span class="dv">0</span><span class="op">;</span>        x1<span class="op">=</span>w0<span class="op">+</span>x0<span class="op">;</span>    x2<span class="op">=</span>w1<span class="op">+</span>x1<span class="op">;</span>    x3<span class="op">=</span>w2<span class="op">+</span>x2<span class="op">;</span>    x4<span class="op">=</span>w3<span class="op">+</span>x3<span class="op">;</span></span>
<span id="cb4-34"><a href="#cb4-34" aria-hidden="true" tabindex="-1"></a>      x5<span class="op">=</span>w4<span class="op">+</span>x4<span class="op">;</span>    x6<span class="op">=</span>w5<span class="op">+</span>x5<span class="op">;</span>    x7<span class="op">=</span>w6<span class="op">+</span>x6<span class="op">-</span>w0<span class="op">;</span> x8<span class="op">=</span>w7<span class="op">+</span>x7<span class="op">-</span>w1<span class="op">;</span> x9<span class="op">=</span>w8<span class="op">+</span>x8<span class="op">-</span>w2<span class="op">;</span> <span class="cf">break</span><span class="op">;</span></span>
<span id="cb4-35"><a href="#cb4-35" aria-hidden="true" tabindex="-1"></a>    <span class="cf">case</span> <span class="dv">10</span><span class="op">:</span></span>
<span id="cb4-36"><a href="#cb4-36" aria-hidden="true" tabindex="-1"></a>      x0<span class="op">=</span><span class="dv">0</span><span class="op">;</span>        x1<span class="op">=</span>w0<span class="op">+</span>x0<span class="op">;</span>    x2<span class="op">=</span>w1<span class="op">+</span>x1<span class="op">;</span>    x3<span class="op">=</span>w2<span class="op">+</span>x2<span class="op">;</span>    x4<span class="op">=</span>w3<span class="op">+</span>x3<span class="op">;</span></span>
<span id="cb4-37"><a href="#cb4-37" aria-hidden="true" tabindex="-1"></a>      x5<span class="op">=</span>w4<span class="op">+</span>x4<span class="op">;</span>    x6<span class="op">=</span>w5<span class="op">+</span>x5<span class="op">;</span>    x7<span class="op">=</span>w6<span class="op">+</span>x6<span class="op">;</span>    x8<span class="op">=</span>w7<span class="op">+</span>x7<span class="op">;</span>    x9<span class="op">=</span>w8<span class="op">+</span>x8<span class="op">;</span>    <span class="cf">break</span><span class="op">;</span></span>
<span id="cb4-38"><a href="#cb4-38" aria-hidden="true" tabindex="-1"></a>  }</span>
<span id="cb4-39"><a href="#cb4-39" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-40"><a href="#cb4-40" aria-hidden="true" tabindex="-1"></a>  <span class="cf">switch</span> (d2) {</span>
<span id="cb4-41"><a href="#cb4-41" aria-hidden="true" tabindex="-1"></a>    <span class="cf">case</span> <span class="dv">0</span><span class="op">:</span></span>
<span id="cb4-42"><a href="#cb4-42" aria-hidden="true" tabindex="-1"></a>      y0<span class="op">=</span>x0<span class="op">;</span>       y1<span class="op">=</span>x1<span class="op">;</span>       y2<span class="op">=</span>x2<span class="op">;</span>       y3<span class="op">=</span>x3<span class="op">;</span>       y4<span class="op">=</span>x4<span class="op">;</span></span>
<span id="cb4-43"><a href="#cb4-43" aria-hidden="true" tabindex="-1"></a>      y5<span class="op">=</span>x5<span class="op">;</span>       y6<span class="op">=</span>x6<span class="op">;</span>       y7<span class="op">=</span>x7<span class="op">;</span>       y8<span class="op">=</span>x8<span class="op">;</span>       y9<span class="op">=</span>x9<span class="op">;</span>       <span class="cf">break</span><span class="op">;</span></span>
<span id="cb4-44"><a href="#cb4-44" aria-hidden="true" tabindex="-1"></a>    <span class="cf">case</span> <span class="dv">4</span><span class="op">:</span></span>
<span id="cb4-45"><a href="#cb4-45" aria-hidden="true" tabindex="-1"></a>      y0<span class="op">=</span><span class="dv">0</span><span class="op">;</span>        y1<span class="op">=</span>x0<span class="op">+</span>y0<span class="op">;</span>    y2<span class="op">=</span>x1<span class="op">+</span>y1<span class="op">;</span>    y3<span class="op">=</span>x2<span class="op">+</span>y2<span class="op">;</span>    y4<span class="op">=</span>x3<span class="op">+</span>y3<span class="op">;</span></span>
<span id="cb4-46"><a href="#cb4-46" aria-hidden="true" tabindex="-1"></a>      y5<span class="op">=</span>x4<span class="op">+</span>y4<span class="op">-</span>x0<span class="op">;</span> y6<span class="op">=</span>x5<span class="op">+</span>y5<span class="op">-</span>x1<span class="op">;</span> y7<span class="op">=</span>x6<span class="op">+</span>y6<span class="op">-</span>x2<span class="op">;</span> y8<span class="op">=</span>x7<span class="op">+</span>y7<span class="op">-</span>x3<span class="op">;</span> y9<span class="op">=</span>x8<span class="op">+</span>y8<span class="op">-</span>x4<span class="op">;</span> <span class="cf">break</span><span class="op">;</span></span>
<span id="cb4-47"><a href="#cb4-47" aria-hidden="true" tabindex="-1"></a>    <span class="cf">case</span> <span class="dv">6</span><span class="op">:</span></span>
<span id="cb4-48"><a href="#cb4-48" aria-hidden="true" tabindex="-1"></a>      y0<span class="op">=</span><span class="dv">0</span><span class="op">;</span>        y1<span class="op">=</span>x0<span class="op">+</span>y0<span class="op">;</span>    y2<span class="op">=</span>x1<span class="op">+</span>y1<span class="op">;</span>    y3<span class="op">=</span>x2<span class="op">+</span>y2<span class="op">;</span>    y4<span class="op">=</span>x3<span class="op">+</span>y3<span class="op">;</span></span>
<span id="cb4-49"><a href="#cb4-49" aria-hidden="true" tabindex="-1"></a>      y5<span class="op">=</span>x4<span class="op">+</span>y4<span class="op">;</span>    y6<span class="op">=</span>x5<span class="op">+</span>y5<span class="op">;</span>    y7<span class="op">=</span>x6<span class="op">+</span>y6<span class="op">-</span>x0<span class="op">;</span> y8<span class="op">=</span>x7<span class="op">+</span>y7<span class="op">-</span>x1<span class="op">;</span> y9<span class="op">=</span>x8<span class="op">+</span>y8<span class="op">-</span>x2<span class="op">;</span> <span class="cf">break</span><span class="op">;</span></span>
<span id="cb4-50"><a href="#cb4-50" aria-hidden="true" tabindex="-1"></a>    <span class="cf">case</span> <span class="dv">10</span><span class="op">:</span></span>
<span id="cb4-51"><a href="#cb4-51" aria-hidden="true" tabindex="-1"></a>      y0<span class="op">=</span><span class="dv">0</span><span class="op">;</span>        y1<span class="op">=</span>x0<span class="op">+</span>y0<span class="op">;</span>    y2<span class="op">=</span>x1<span class="op">+</span>y1<span class="op">;</span>    y3<span class="op">=</span>x2<span class="op">+</span>y2<span class="op">;</span>    y4<span class="op">=</span>x3<span class="op">+</span>y3<span class="op">;</span></span>
<span id="cb4-52"><a href="#cb4-52" aria-hidden="true" tabindex="-1"></a>      y5<span class="op">=</span>x4<span class="op">+</span>y4<span class="op">;</span>    y6<span class="op">=</span>x5<span class="op">+</span>y5<span class="op">;</span>    y7<span class="op">=</span>x6<span class="op">+</span>y6<span class="op">;</span>    y8<span class="op">=</span>x7<span class="op">+</span>y7<span class="op">;</span>    y9<span class="op">=</span>x8<span class="op">+</span>y8<span class="op">;</span>    <span class="cf">break</span><span class="op">;</span></span>
<span id="cb4-53"><a href="#cb4-53" aria-hidden="true" tabindex="-1"></a>  }</span>
<span id="cb4-54"><a href="#cb4-54" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-55"><a href="#cb4-55" aria-hidden="true" tabindex="-1"></a>  <span class="cf">switch</span> (d3) {</span>
<span id="cb4-56"><a href="#cb4-56" aria-hidden="true" tabindex="-1"></a>    <span class="cf">case</span> <span class="dv">0</span><span class="op">:</span></span>
<span id="cb4-57"><a href="#cb4-57" aria-hidden="true" tabindex="-1"></a>      z0<span class="op">=</span>y0<span class="op">;</span>       z1<span class="op">=</span>y1<span class="op">;</span>       z2<span class="op">=</span>y2<span class="op">;</span>       z3<span class="op">=</span>y3<span class="op">;</span>       z4<span class="op">=</span>y4<span class="op">;</span></span>
<span id="cb4-58"><a href="#cb4-58" aria-hidden="true" tabindex="-1"></a>      z5<span class="op">=</span>y5<span class="op">;</span>       z6<span class="op">=</span>y6<span class="op">;</span>       z7<span class="op">=</span>y7<span class="op">;</span>       z8<span class="op">=</span>y8<span class="op">;</span>       z9<span class="op">=</span>y9<span class="op">;</span>       <span class="cf">break</span><span class="op">;</span></span>
<span id="cb4-59"><a href="#cb4-59" aria-hidden="true" tabindex="-1"></a>    <span class="cf">case</span> <span class="dv">4</span><span class="op">:</span></span>
<span id="cb4-60"><a href="#cb4-60" aria-hidden="true" tabindex="-1"></a>      z0<span class="op">=</span><span class="dv">0</span><span class="op">;</span>        z1<span class="op">=</span>y0<span class="op">+</span>z0<span class="op">;</span>    z2<span class="op">=</span>y1<span class="op">+</span>z1<span class="op">;</span>    z3<span class="op">=</span>y2<span class="op">+</span>z2<span class="op">;</span>    z4<span class="op">=</span>y3<span class="op">+</span>z3<span class="op">;</span></span>
<span id="cb4-61"><a href="#cb4-61" aria-hidden="true" tabindex="-1"></a>      z5<span class="op">=</span>y4<span class="op">+</span>z4<span class="op">-</span>y0<span class="op">;</span> z6<span class="op">=</span>y5<span class="op">+</span>z5<span class="op">-</span>y1<span class="op">;</span> z7<span class="op">=</span>y6<span class="op">+</span>z6<span class="op">-</span>y2<span class="op">;</span> z8<span class="op">=</span>y7<span class="op">+</span>z7<span class="op">-</span>y3<span class="op">;</span> z9<span class="op">=</span>y8<span class="op">+</span>z8<span class="op">-</span>y4<span class="op">;</span> <span class="cf">break</span><span class="op">;</span></span>
<span id="cb4-62"><a href="#cb4-62" aria-hidden="true" tabindex="-1"></a>    <span class="cf">case</span> <span class="dv">6</span><span class="op">:</span></span>
<span id="cb4-63"><a href="#cb4-63" aria-hidden="true" tabindex="-1"></a>      z0<span class="op">=</span><span class="dv">0</span><span class="op">;</span>        z1<span class="op">=</span>y0<span class="op">+</span>z0<span class="op">;</span>    z2<span class="op">=</span>y1<span class="op">+</span>z1<span class="op">;</span>    z3<span class="op">=</span>y2<span class="op">+</span>z2<span class="op">;</span>    z4<span class="op">=</span>y3<span class="op">+</span>z3<span class="op">;</span></span>
<span id="cb4-64"><a href="#cb4-64" aria-hidden="true" tabindex="-1"></a>      z5<span class="op">=</span>y4<span class="op">+</span>z4<span class="op">;</span>    z6<span class="op">=</span>y5<span class="op">+</span>z5<span class="op">;</span>    z7<span class="op">=</span>y6<span class="op">+</span>z6<span class="op">-</span>y0<span class="op">;</span> z8<span class="op">=</span>y7<span class="op">+</span>z7<span class="op">-</span>y1<span class="op">;</span> z9<span class="op">=</span>y8<span class="op">+</span>z8<span class="op">-</span>y2<span class="op">;</span> <span class="cf">break</span><span class="op">;</span></span>
<span id="cb4-65"><a href="#cb4-65" aria-hidden="true" tabindex="-1"></a>    <span class="cf">case</span> <span class="dv">10</span><span class="op">:</span></span>
<span id="cb4-66"><a href="#cb4-66" aria-hidden="true" tabindex="-1"></a>      z0<span class="op">=</span><span class="dv">0</span><span class="op">;</span>        z1<span class="op">=</span>y0<span class="op">+</span>z0<span class="op">;</span>    z2<span class="op">=</span>y1<span class="op">+</span>z1<span class="op">;</span>    z3<span class="op">=</span>y2<span class="op">+</span>z2<span class="op">;</span>    z4<span class="op">=</span>y3<span class="op">+</span>z3<span class="op">;</span></span>
<span id="cb4-67"><a href="#cb4-67" aria-hidden="true" tabindex="-1"></a>      z5<span class="op">=</span>y4<span class="op">+</span>z4<span class="op">;</span>    z6<span class="op">=</span>y5<span class="op">+</span>z5<span class="op">;</span>    z7<span class="op">=</span>y6<span class="op">+</span>z6<span class="op">;</span>    z8<span class="op">=</span>y7<span class="op">+</span>z7<span class="op">;</span>    z9<span class="op">=</span>y8<span class="op">+</span>z8<span class="op">;</span>    <span class="cf">break</span><span class="op">;</span></span>
<span id="cb4-68"><a href="#cb4-68" aria-hidden="true" tabindex="-1"></a>  }</span>
<span id="cb4-69"><a href="#cb4-69" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-70"><a href="#cb4-70" aria-hidden="true" tabindex="-1"></a>  <span class="cf">return</span> (z0<span class="op">+</span>z1<span class="op">+</span>z2<span class="op">+</span>z3<span class="op">+</span>z4<span class="op">+</span>z5<span class="op">+</span>z6<span class="op">+</span>z7<span class="op">+</span>z8<span class="op">+</span>z9)<span class="op">/</span>m<span class="op">;</span></span>
<span id="cb4-71"><a href="#cb4-71" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
<p>And here’s the function that computes the same result as <code>pSetsFail()</code> above,
again with slightly different parameters:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode javascript"><code class="sourceCode javascript"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">pSetsFail</span>(ds) {</span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> p0 <span class="op">=</span> <span class="fu">pSetFail</span>(ds[ <span class="dv">0</span>]<span class="op">,</span> ds[ <span class="dv">1</span>]<span class="op">,</span> ds[ <span class="dv">2</span>]<span class="op">,</span> ds[ <span class="dv">3</span>])<span class="op">;</span></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> p1 <span class="op">=</span> <span class="fu">pSetFail</span>(ds[ <span class="dv">4</span>]<span class="op">,</span> ds[ <span class="dv">5</span>]<span class="op">,</span> ds[ <span class="dv">6</span>]<span class="op">,</span> ds[ <span class="dv">7</span>])<span class="op">;</span></span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> p2 <span class="op">=</span> <span class="fu">pSetFail</span>(ds[ <span class="dv">8</span>]<span class="op">,</span> ds[ <span class="dv">9</span>]<span class="op">,</span> ds[<span class="dv">10</span>]<span class="op">,</span> ds[<span class="dv">11</span>])<span class="op">;</span></span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> p3 <span class="op">=</span> <span class="fu">pSetFail</span>(ds[<span class="dv">12</span>]<span class="op">,</span> ds[<span class="dv">13</span>]<span class="op">,</span> ds[<span class="dv">14</span>]<span class="op">,</span> ds[<span class="dv">15</span>])<span class="op">;</span></span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> p4 <span class="op">=</span> <span class="fu">pSetFail</span>(ds[<span class="dv">16</span>]<span class="op">,</span> ds[<span class="dv">17</span>]<span class="op">,</span> ds[<span class="dv">18</span>]<span class="op">,</span> ds[<span class="dv">19</span>])<span class="op">;</span></span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> p5 <span class="op">=</span> <span class="fu">pSetFail</span>(ds[<span class="dv">20</span>]<span class="op">,</span> ds[<span class="dv">21</span>]<span class="op">,</span> ds[<span class="dv">22</span>]<span class="op">,</span> ds[<span class="dv">23</span>])<span class="op">;</span></span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> p6 <span class="op">=</span> <span class="fu">pSetFail</span>(ds[<span class="dv">24</span>]<span class="op">,</span> ds[<span class="dv">25</span>]<span class="op">,</span> ds[<span class="dv">26</span>]<span class="op">,</span> ds[<span class="dv">27</span>])<span class="op">;</span></span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> p7 <span class="op">=</span> <span class="fu">pSetFail</span>(ds[<span class="dv">28</span>]<span class="op">,</span> ds[<span class="dv">29</span>]<span class="op">,</span> ds[<span class="dv">30</span>]<span class="op">,</span> ds[<span class="dv">31</span>])<span class="op">;</span></span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> p8 <span class="op">=</span> <span class="fu">pSetFail</span>(ds[<span class="dv">32</span>]<span class="op">,</span> ds[<span class="dv">33</span>]<span class="op">,</span> ds[<span class="dv">34</span>]<span class="op">,</span> ds[<span class="dv">35</span>])<span class="op">;</span></span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> p9 <span class="op">=</span> <span class="fu">pSetFail</span>(ds[<span class="dv">36</span>]<span class="op">,</span> ds[<span class="dv">37</span>]<span class="op">,</span> ds[<span class="dv">38</span>]<span class="op">,</span> ds[<span class="dv">39</span>])<span class="op">;</span></span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> a1 <span class="op">=</span>          <span class="dv">1</span><span class="op">-</span>p0<span class="op">;</span></span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> b0 <span class="op">=</span> p1<span class="op">*</span>p0<span class="op">;</span></span>
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> b1 <span class="op">=</span> p1<span class="op">*</span>a1 <span class="op">+</span> (<span class="dv">1</span><span class="op">-</span>p1)<span class="op">*</span>p0<span class="op">;</span></span>
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> b2 <span class="op">=</span>         (<span class="dv">1</span><span class="op">-</span>p1)<span class="op">*</span>a1<span class="op">;</span></span>
<span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> c0 <span class="op">=</span> p2<span class="op">*</span>b0<span class="op">;</span></span>
<span id="cb5-20"><a href="#cb5-20" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> c1 <span class="op">=</span> p2<span class="op">*</span>b1 <span class="op">+</span> (<span class="dv">1</span><span class="op">-</span>p2)<span class="op">*</span>b0<span class="op">;</span></span>
<span id="cb5-21"><a href="#cb5-21" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> c2 <span class="op">=</span> p2<span class="op">*</span>b2 <span class="op">+</span> (<span class="dv">1</span><span class="op">-</span>p2)<span class="op">*</span>b1<span class="op">;</span></span>
<span id="cb5-22"><a href="#cb5-22" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-23"><a href="#cb5-23" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> d0 <span class="op">=</span> p3<span class="op">*</span>c0<span class="op">;</span></span>
<span id="cb5-24"><a href="#cb5-24" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> d1 <span class="op">=</span> p3<span class="op">*</span>c1 <span class="op">+</span> (<span class="dv">1</span><span class="op">-</span>p3)<span class="op">*</span>c0<span class="op">;</span></span>
<span id="cb5-25"><a href="#cb5-25" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> d2 <span class="op">=</span> p3<span class="op">*</span>c2 <span class="op">+</span> (<span class="dv">1</span><span class="op">-</span>p3)<span class="op">*</span>c1<span class="op">;</span></span>
<span id="cb5-26"><a href="#cb5-26" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-27"><a href="#cb5-27" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> e0 <span class="op">=</span> p4<span class="op">*</span>d0<span class="op">;</span></span>
<span id="cb5-28"><a href="#cb5-28" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> e1 <span class="op">=</span> p4<span class="op">*</span>d1 <span class="op">+</span> (<span class="dv">1</span><span class="op">-</span>p4)<span class="op">*</span>d0<span class="op">;</span></span>
<span id="cb5-29"><a href="#cb5-29" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> e2 <span class="op">=</span> p4<span class="op">*</span>d2 <span class="op">+</span> (<span class="dv">1</span><span class="op">-</span>p4)<span class="op">*</span>d1<span class="op">;</span></span>
<span id="cb5-30"><a href="#cb5-30" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-31"><a href="#cb5-31" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> f0 <span class="op">=</span> p5<span class="op">*</span>e0<span class="op">;</span></span>
<span id="cb5-32"><a href="#cb5-32" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> f1 <span class="op">=</span> p5<span class="op">*</span>e1 <span class="op">+</span> (<span class="dv">1</span><span class="op">-</span>p5)<span class="op">*</span>e0<span class="op">;</span></span>
<span id="cb5-33"><a href="#cb5-33" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> f2 <span class="op">=</span> p5<span class="op">*</span>e2 <span class="op">+</span> (<span class="dv">1</span><span class="op">-</span>p5)<span class="op">*</span>e1<span class="op">;</span></span>
<span id="cb5-34"><a href="#cb5-34" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-35"><a href="#cb5-35" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> g0 <span class="op">=</span> p6<span class="op">*</span>f0<span class="op">;</span></span>
<span id="cb5-36"><a href="#cb5-36" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> g1 <span class="op">=</span> p6<span class="op">*</span>f1 <span class="op">+</span> (<span class="dv">1</span><span class="op">-</span>p6)<span class="op">*</span>f0<span class="op">;</span></span>
<span id="cb5-37"><a href="#cb5-37" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> g2 <span class="op">=</span> p6<span class="op">*</span>f2 <span class="op">+</span> (<span class="dv">1</span><span class="op">-</span>p6)<span class="op">*</span>f1<span class="op">;</span></span>
<span id="cb5-38"><a href="#cb5-38" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-39"><a href="#cb5-39" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> h0 <span class="op">=</span> p7<span class="op">*</span>g0<span class="op">;</span></span>
<span id="cb5-40"><a href="#cb5-40" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> h1 <span class="op">=</span> p7<span class="op">*</span>g1 <span class="op">+</span> (<span class="dv">1</span><span class="op">-</span>p7)<span class="op">*</span>g0<span class="op">;</span></span>
<span id="cb5-41"><a href="#cb5-41" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> h2 <span class="op">=</span> p7<span class="op">*</span>g2 <span class="op">+</span> (<span class="dv">1</span><span class="op">-</span>p7)<span class="op">*</span>g1<span class="op">;</span></span>
<span id="cb5-42"><a href="#cb5-42" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-43"><a href="#cb5-43" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> i0 <span class="op">=</span> p8<span class="op">*</span>h0<span class="op">;</span></span>
<span id="cb5-44"><a href="#cb5-44" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> i1 <span class="op">=</span> p8<span class="op">*</span>h1 <span class="op">+</span> (<span class="dv">1</span><span class="op">-</span>p8)<span class="op">*</span>h0<span class="op">;</span></span>
<span id="cb5-45"><a href="#cb5-45" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> i2 <span class="op">=</span> p8<span class="op">*</span>h2 <span class="op">+</span> (<span class="dv">1</span><span class="op">-</span>p8)<span class="op">*</span>h1<span class="op">;</span></span>
<span id="cb5-46"><a href="#cb5-46" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-47"><a href="#cb5-47" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> j0 <span class="op">=</span> p9<span class="op">*</span>i0<span class="op">;</span></span>
<span id="cb5-48"><a href="#cb5-48" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> j1 <span class="op">=</span> p9<span class="op">*</span>i1 <span class="op">+</span> (<span class="dv">1</span><span class="op">-</span>p9)<span class="op">*</span>i0<span class="op">;</span></span>
<span id="cb5-49"><a href="#cb5-49" aria-hidden="true" tabindex="-1"></a>  <span class="kw">const</span> j2 <span class="op">=</span> p9<span class="op">*</span>i2 <span class="op">+</span> (<span class="dv">1</span><span class="op">-</span>p9)<span class="op">*</span>i1<span class="op">;</span></span>
<span id="cb5-50"><a href="#cb5-50" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-51"><a href="#cb5-51" aria-hidden="true" tabindex="-1"></a>  <span class="cf">return</span> j0<span class="op">+</span>j1<span class="op">+</span>j2<span class="op">;</span></span>
<span id="cb5-52"><a href="#cb5-52" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
<p>When running this on the worst case input with the same benchmarking strategy as
before, I get times of <strong>400 nanoseconds</strong>.</p>
<p>Why is it so much faster? Well, I can’t be 100% sure without digging deeper, but
my guess is this is very JIT-friendly code, and that the CPU code it generates
is very cache-friendly. <code>pSetFail</code> is also all integer sums and products until
the return statement, which probably helps a bit. Maybe with a bit more digging
it could be made even faster. But I think 400ns is pretty fast. That’s a 3000x
improvement from where we started. Not bad!</p>
<p>By the way, if you look very closely at <code>pSetFail</code>, you might notice that it
looks a lot like the ultimate brute force solution: counting up the possible
ways to get different sums.</p>
<h2 id="the-future">The future?</h2>
<p>We’re still using a naive convolution algorithm, one essentially based directly
on the definition. For small sequences this works well enough, but for larger
sequences this becomes prohibitive. This type of convolution comes up in signal
processing a lot, where you might want to compute the convolution of two
sequences that have tens of thousands of elements. In those scenarios you can
take advantage of the convolution theorem, which lets you multiply the Fourier
transforms of the inputs point-wise and do an inverse Fourier transform to get
the same result. This is also the basis of some fast integer multiplication
algorithms.</p>
<p>Are our inputs too small to benefit from this knowledge? Probably. But I can’t
help but be a little curious. If I ever decide to give it a shot, I’ll be sure
to write about it.</p>
        </div>
      </content>
    </entry>
  
    <entry>
      <id>tag:ajitek.net,2024:blog/posts/2024-01-29-reader-mode-it-just-works.md</id>
      <title>Reader Mode: It Just Works</title>
      <link rel="alternate" type="text/html" href="https://aji.github.io/blog/posts/2024-01-29-reader-mode-it-just-works.html" />
      <published>2024-01-29T12:00:00Z</published>
      <updated>2025-11-13T05:18:30Z</updated>
      <rights>
        This work (c) 2024 by Alex Iadicicco is licensed under CC BY-NC-SA 4.0.
        To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/
      </rights>
      
        <summary>And maybe that's fine</summary>
      
      <content type="xhtml" xml:lang="en" xml:base="https://aji.github.io/blog">
        <div xmlns="http://www.w3.org/1999/xhtml">
          
            <img src="https://aji.github.io/blog/images/2024-01-29-reader-mode-it-just-works.png" />
          
          <p>I’m the kind of internet user for whom the <a href="https://support.mozilla.org/en-US/kb/firefox-reader-view-clutter-free-web-pages">Firefox Reader View
feature</a> is only occasionally useful. Most mainstream browsers have
something like it these days. I’m sure there are people out there who can’t
imagine using the internet without it. For me, it’s a lot like having a
unique screwdriver that’s a better choice for some jobs than resorting to a
suitably-sized flathead and hoping for the best: most of the time you can’t
use it, but when you can, you’re really glad to have it.</p>
<p>I’ve often wondered how exactly this feature works. This is partially out of
curiosity. Sometimes it gets things slightly wrong, which, sadly, makes me
reluctant to trust it. Its shortcomings suggest something really tricky going
on under the hood. I can’t help but wonder where in the pinball machine of
<code>&lt;div&gt;</code> tags and CSS class names <em>that</em> particular bit of authorial intent got
lost. Sometimes you notice the little Reader View icon in the address bar for
pages that can’t plausibly look decent that way, or sometimes it’s missing from
pages that by all accounts <em>should</em> have it. But in addition to rubbernecking
and taking cheap shots at a brave attempt to address a difficult problem,
there’s also a more cooperative motivation: how can I structure the stuff I put
online in a way that will reliably look good in Reader View, and reader modes
in general?</p>
<p>This is, sadly, a poorly-documented topic. Resources consist mainly of
<a href="https://stackoverflow.com/questions/47822691/how-do-you-create-a-web-page-for-reader-mode">Stack</a> <a href="https://stackoverflow.com/questions/30661650/how-does-firefox-reader-view-operate">Overflow</a> <a href="https://stackoverflow.com/questions/30730300/optimize-website-to-show-reader-view-in-firefox">questions</a>, questions on the <a href="https://webmasters.stackexchange.com/questions/83058/how-do-i-make-my-site-compatible-with-firefoxs-reader-view-feature">Webmasters
Stack Exchange</a>, threads on <a href="https://support.mozilla.org/en-US/questions/1067528">Mozilla’s</a> <a href="https://support.mozilla.org/en-US/questions/1140969">support
forums</a>, now-deleted <a href="https://web.archive.org/web/20200719050912/http://zumguy.com/enabling-reading-mode-on-your-website/">blog posts</a>, etc. all of which link to
each other. The advice seems to boil down to “It applies a bunch of heuristics
that work pretty well if your HTML is good.”</p>
<h2 id="why-isnt-it-standardized-yet">Why isn’t it standardized yet?</h2>
<p>After some digging I found <a href="https://www.ctrl.blog/entry/browser-reading-mode-parsers.html">this excellent series of 4 articles by Daniel
Aleksandersen from 2018</a> about features like Reader View that
discusses their history, the failed attempts at standardization, an overview of
how these features work and how they differ across implementations, and a plea
to finally standardize the dang thing. The fact that Daniel is writing from
2018 is a little disheartening. A lot of what he writes still feels very
relatable. It’s a relatively simple problem, at least compared to what’s
typical for the web, so how could we not have made any progress on it in almost
6 years?</p>
<p>An uncharitable reading of the situation might conclude that Reader View and
features like it are useless, something completely forgotten by browser vendors
and content authors alike, the kind of problem that standardization alone can’t
solve. But a quick scan of the <a href="https://github.com/mozilla/readability/issues">issues</a> and <a href="https://github.com/mozilla/readability/pulls">pull
requests</a> for Readability.js (the tool used for implementing the
Firefox Reader View) suggest that there are at least a <em>few</em> people who want
the feature to be good, and some of them even work at Mozilla. From another
perspective, the scattered and wishy-washy advice for how to cooperate with
Reader View is a testament to the feature’s overall reliability, an indication
that people are content to accept the situation as-is. If it’s not broken, why
fix it? How broken is it actually?</p>
<p>To Mozilla’s credit, they, like all browser vendors, are up against some pretty
steep odds. These days, most of the HTML on the internet is written with
desktop and mobile users in mind, and occasionally will also consider screen
readers and dark modes. For everything else, you just have to make do with what
you’ve got. A well known fact about standards is that their mere existence
doesn’t mean much on its own, and this plays out on the web with discouraging
regularity. Many of the web’s standards, such as CSS media queries, semantic
HTML, ARIA definitions, and even private standards like AMP, feel hopelessly
optimistic when pitted against the chaotic and laissez-faire reality of web
content. In a world where <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Quirks_Mode_and_Standards_Mode">quirks mode</a> exists, is it anything
but a waste of time to give people the option to ditch the separate “print
view” and instead do it all in one page with HTML and CSS? Maybe some problems
are doomed by human nature to remain unsolved forever, and a reliable reader
mode is one of them.</p>
<p>Unfortunately, the incentives are rarely aligned between browser vendors and
web content authors (although the situation is improving), and vendors are
significantly outnumbered. There are only a handful of browsers, even including
those with just a small fraction of the total market, and they are motivated to
be secure, robust, and compliant with all the latest standards. Content
authors, however, vary wildly in their goals, and not all of them will
necessarily care (or have the time and resources to care) about things like
whether the page works on a phone, whether “Print to PDF” looks good, whether
CSS grid would have been a better choice than <code>&lt;table&gt;</code> (what year is it?),
whether repeated <code>&amp;nbsp;</code> is the right way to do indentation, etc. If any of
these things creates a problem for users, realistically it’s up to the browser
vendors to do something about it (usually something hacky) since you can’t pin
your hopes of capturing market share on content authors doing the right thing:
if CNN looks good in every browser but yours, that’s <em>your</em> problem, even if
it’s really CNN’s fault.</p>
<h4 id="denial">Denial</h4>
<p>And this is why reader modes are interesting to me. The incentives <em>are</em>
aligned here. Browser vendors (with the possible exception of Google) want them
to work well, users want them to work well, and, clearly, there are more than a
few web content authors that want them to work well for their sites.
Furthermore, the idea seems pretty aligned with accessibility. So why does the
implementation still feel like a hack? Why has nobody tried to standardize
this? At this point it’s clear that reader modes are here to stay, so why not
try to make them good? How is this different from AMP, which for a brief time
was everywhere <a href="https://en.wikipedia.org/wiki/Accelerated_Mobile_Pages#Reception">even though people hated it</a>? How is this different from
the Open Graph protocol? How is this different from Sitemaps or WebP or
Flexbox?</p>
<h4 id="anger">Anger</h4>
<p>The main difference? A standard won’t help. Not that much, anyway. The web
community finds itself in a situation which is all too familiar to software
engineers: things are good <em>enough</em>, and the problems aren’t a big deal. The
only changes to reader modes that anyone feels are worth the time and energy
are the small, incremental ones that gradually improve the situation, and
widespread adoption of a standard is simply not one of those things.
Ultimately, most of the people who would play nice with a standardized reader
mode are already reciting the <code>&lt;article&gt;</code> and <code>&lt;p&gt;</code> incantations they got from
Stack Overflow.</p>
<h4 id="bargaining">Bargaining</h4>
<p>Furthermore, for the small number of people that want a good reader mode
experience for their website but can’t make it work, a standard won’t
necessarily help them. Standards can be poorly thought out, can be caught off
guard by broader changes, can be merely a restatement of an existing
implementation, can specify things that never get implemented, etc. Standards
can be bad too, and fixing a broken standard is not an easy task. You’re much
more likely to get the <code>&lt;table&gt;</code> extraction for your page fixed by submitting
an issue to a GitHub repo (or fixing it yourself, maybe) than by going through
the process of having the standard amended in a way that everyone feels will
fix all <code>&lt;table&gt;</code> extraction everywhere for all time.</p>
<h4 id="depression">Depression</h4>
<p>Cynically, a standard could even make things worse. The whole point of a reader
mode is to <em>reduce</em> clutter, and a standard would only provide a jumping-off
point for innovations in content treachery. The effectiveness of things like
search rankings, ad blockers, and tracking prevention rely on website authors
<em>not</em> knowing how they work, or not caring enough to know. Perhaps it’s a good
thing that reader modes, an obscure, subtly-broken, and poorly-documented
feature, function similarly by pure chance. I can’t imagine any ad-supported
websites would be particularly enthusiastic to help browsers show users just
what they came for, with no ads, no links to other articles, no comments
sections, no social media buttons. Imagine the HTML crimes they’d do to sneak
them back in. Imagine the cat-and-mouse game that would ensue. Imagine the
ways they’d meddle in the standardization process.</p>
<h4 id="acceptance">Acceptance</h4>
<p>This is not to suggest that a lack of a standard is a <em>good</em> thing, or that
it’s a purposeful effort by browser vendors to thwart those who would ruin it
for everybody. But I also don’t think the lack of a standard is a <em>bad</em> thing
either. Standards are nice to have, but they aren’t strictly necessary, and it
seems difficult to create a standard that would benefit users for whom the
current implementation is inadequate, but not those for whom a large population
of reader mode users would be valuable clickbait targets. Perhaps a solution is
possible, but it doesn’t seem trivial, both technically and socially, and it
really isn’t such a big deal. The reader mode we’ve got isn’t perfect, but it
works. It’ll be our little secret.</p>
        </div>
      </content>
    </entry>
  
</feed>
