<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://cbarrick.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://cbarrick.dev/" rel="alternate" type="text/html" hreflang="en-US" /><updated>2025-12-02T01:44:41+00:00</updated><id>https://cbarrick.dev/feed.xml</id><title type="html">Chris Barrick</title><subtitle>Chris Barrick&apos;s personal website and blog</subtitle><author><name>Chris Barrick</name></author><entry><title type="html">Encoding tic-tac-toe in 15 bits</title><link href="https://cbarrick.dev/posts/2024/02/19/tic-tac-toe" rel="alternate" type="text/html" title="Encoding tic-tac-toe in 15 bits" /><published>2024-02-19T00:00:00+00:00</published><updated>2024-02-19T00:00:00+00:00</updated><id>https://cbarrick.dev/posts/2024/02/19/tic-tac-toe</id><content type="html" xml:base="https://cbarrick.dev/posts/2024/02/19/tic-tac-toe"><![CDATA[<p>I recently stumbled upon a <a href="https://blog.goose.love/posts/tictactoe/">blog post</a> by Alejandra González (a.k.a <a href="https://tech.lgbt/@blyxyas">@blyxyas</a>)
that seeks to compress a tic-tac-toe game state into as few bits as possible.
She arrived at a solution in 18 bits. This got me thinking, can we do better?</p>

<p>As Alejandra points out, there are 765 possible game states<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>. We could simply
assign a number to all of the sates, which would take up 10 bits<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>. But in
Alejandra’s words, that’s “boring.” More specifically, there’s not much we can
do with a representation like that. Whether we want to read the value of a given
cell or update from one state to another, in practice we’re going to need a
lookup table to map each number to a larger, more structured representation,
which defeats the whole idea behind a compressed representation.</p>

<figure>
  <img src="/assets/tic-tac-toe-game.svg" alt="A game of tic-tac-toe" width="100%" />
  <figcaption>
    A game of tic-tac-toe /
    © <a href="https://commons.wikimedia.org/wiki/User:Stannered">User:Stannered</a> /
    <a href="https://commons.wikimedia.org">Wikimedia Commons</a> /
    <a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA-3.0</a>
  </figcaption>
</figure>

<h3 id="an-18-bit-solution">An 18 bit solution</h3>

<p>Alejandra came up with a better solution, where each cell is represented by
a pair of bits, and the grid is represented as a concatenation of nine of these
bit pairs. Within a bit pair, one bit represents a circle and the other
represents a cross; at most one bit of the pair can be set.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// The representation of a single cell.</span>
<span class="k">typedef</span> <span class="k">enum</span> <span class="n">cell</span> <span class="p">{</span>
    <span class="n">EMPTY</span>  <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="c1">// Binary 00</span>
    <span class="n">CROSS</span>  <span class="o">=</span> <span class="mi">1</span><span class="p">,</span> <span class="c1">// Binary 01</span>
    <span class="n">CIRCLE</span> <span class="o">=</span> <span class="mi">2</span><span class="p">,</span> <span class="c1">// Binary 10</span>
<span class="p">}</span> <span class="n">cell</span><span class="p">;</span>

<span class="c1">// The concatenation of 9 cells. We only care about the lower 18 bits.</span>
<span class="k">typedef</span> <span class="kt">uint32_t</span> <span class="n">state</span><span class="p">;</span>
</code></pre></div></div>

<p>The core methods that we would like to have on our state type are getting and
setting cell values at a given index. This is pretty easy to implement with
some quick bit-twiddling.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cell</span> <span class="nf">get_cell</span><span class="p">(</span><span class="n">state</span> <span class="n">s</span><span class="p">,</span> <span class="kt">int</span> <span class="n">i</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="n">pos</span> <span class="o">=</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">i</span><span class="p">;</span>        <span class="c1">// Bit offset of cell i.</span>
    <span class="k">return</span> <span class="p">(</span><span class="n">s</span> <span class="o">&gt;&gt;</span> <span class="n">pos</span><span class="p">)</span> <span class="o">%</span> <span class="mi">4</span><span class="p">;</span>  <span class="c1">// Read the cell.</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="nf">set_cell</span><span class="p">(</span><span class="n">state</span> <span class="o">*</span><span class="n">s</span><span class="p">,</span> <span class="kt">int</span> <span class="n">i</span><span class="p">,</span> <span class="n">cell</span> <span class="n">val</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="n">pos</span> <span class="o">=</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">i</span><span class="p">;</span>    <span class="c1">// Bit offset of cell i.</span>
    <span class="o">*</span><span class="n">s</span> <span class="o">&amp;=</span> <span class="o">~</span><span class="p">(</span><span class="mi">3</span> <span class="o">&lt;&lt;</span> <span class="n">pos</span><span class="p">);</span>  <span class="c1">// Clear the old value.</span>
    <span class="o">*</span><span class="n">s</span> <span class="o">|=</span> <span class="n">val</span> <span class="o">&lt;&lt;</span> <span class="n">pos</span><span class="p">;</span>   <span class="c1">// Set the new value.</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This is a fantastic, efficient solution.</p>

<h3 id="getting-smaller-with-base-3">Getting smaller with base-3</h3>

<p>In practice, the number of bits in an integer needs to be a power of two. In the
code above, we used a 32 bit integer to hold our state, when we really only
needed 18 bits. If we could save just two more bits, we could cut our memory
usage in half by using a 16 bit integer for the game state.</p>

<p>In the code above, we’ve conceived the game state as the concatenation of nine
cell states. This is a good idea because it makes it simple to implement our
core methods. We can think of this as a base-4 number where each cell state is a
base-4 digit having values 0 (empty), 1 (cross), 2 (circle), and 3 (invalid).
This conception shows up in the code too, where we convert our base-4 index into
a base-2 index by multiplying it by 2, so that we can use bitwise operations to
access the data.</p>

<p>The problem is that pesky invalid cell state. What if we instead conceive the
game state as a base-3 number and a cell state as a base-3 digit? In this case
we need nine base-3 digits, which maxes out at \(3^9-1\) or 19,682.
Representing this in binary will cost us… 15 bits<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>!</p>

<p>So we can use a base-3 representation to hit our 16 bit target. But how do we
implement our methods?</p>

<p>The trick is to generalize our bit-twiddling to arbitrary bases. In binary, the
left-shift operation <code class="language-plaintext highlighter-rouge">x &lt;&lt; i</code> is equivalent to \(x \cdot 2^i\), and likewise
the right-shift operation <code class="language-plaintext highlighter-rouge">x &gt;&gt; i</code> is equivalent to \(x \div 2^i \). To
generalize these operations from base-2 to base-n, just replace 2 with n. For
the other bitwise operations, we can use a combination of addition and
subtraction.</p>

<p>The new code looks like this:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// The representation of a single cell.</span>
<span class="k">typedef</span> <span class="k">enum</span> <span class="n">cell</span> <span class="p">{</span>
    <span class="n">EMPTY</span>  <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
    <span class="n">CROSS</span>  <span class="o">=</span> <span class="mi">1</span><span class="p">,</span>
    <span class="n">CIRCLE</span> <span class="o">=</span> <span class="mi">2</span><span class="p">,</span>
<span class="p">}</span> <span class="n">cell</span><span class="p">;</span>

<span class="c1">// Think of the game sate as a base-3 number with 9 digits.</span>
<span class="k">typedef</span> <span class="kt">uint16_t</span> <span class="n">state</span><span class="p">;</span>

<span class="c1">// A helper to compute pow(3, i), when 0 &lt;= i &lt; 9.</span>
<span class="k">static</span> <span class="kt">int</span> <span class="nf">pow3</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">i</span> <span class="o">&lt;</span> <span class="mi">0</span> <span class="o">||</span> <span class="mi">9</span> <span class="o">&lt;=</span> <span class="n">i</span><span class="p">)</span> <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
    <span class="k">static</span> <span class="kt">int</span> <span class="n">p</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span><span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">27</span><span class="p">,</span> <span class="mi">81</span><span class="p">,</span> <span class="mi">243</span><span class="p">,</span> <span class="mi">729</span><span class="p">,</span> <span class="mi">2187</span><span class="p">,</span> <span class="mi">6561</span><span class="p">};</span>
    <span class="k">return</span> <span class="n">p</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
<span class="p">}</span>

<span class="n">cell</span> <span class="nf">get_cell</span><span class="p">(</span><span class="n">state</span> <span class="n">s</span><span class="p">,</span> <span class="kt">int</span> <span class="n">i</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="n">div</span> <span class="o">=</span> <span class="n">pow3</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>     <span class="c1">// Get the base-3 offset of the cell.</span>
    <span class="k">return</span> <span class="p">(</span><span class="n">s</span> <span class="o">/</span> <span class="n">div</span><span class="p">)</span> <span class="o">%</span> <span class="mi">3</span><span class="p">;</span>  <span class="c1">// "Shift" the base-3 number and read the cell.</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="nf">set_cell</span><span class="p">(</span><span class="n">state</span> <span class="o">*</span><span class="n">s</span><span class="p">,</span> <span class="kt">int</span> <span class="n">i</span><span class="p">,</span> <span class="n">cell</span> <span class="n">val</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="n">div</span> <span class="o">=</span> <span class="n">pow3</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>         <span class="c1">// Get the base-3 offset of the cell.</span>
    <span class="kt">int</span> <span class="n">old</span> <span class="o">=</span> <span class="p">(</span><span class="o">*</span><span class="n">s</span> <span class="o">/</span> <span class="n">div</span><span class="p">)</span> <span class="o">%</span> <span class="mi">3</span><span class="p">;</span>  <span class="c1">// Read the old value of the cell.</span>
    <span class="o">*</span><span class="n">s</span> <span class="o">-=</span> <span class="n">old</span> <span class="o">*</span> <span class="n">div</span><span class="p">;</span>           <span class="c1">// Reset the cell to empty.</span>
    <span class="o">*</span><span class="n">s</span> <span class="o">+=</span> <span class="n">val</span> <span class="o">*</span> <span class="n">div</span><span class="p">;</span>           <span class="c1">// Set the cell value.</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="conclusion">Conclusion</h3>

<p>Is this any better? It depends, but probably not.</p>

<p>If you had a very large number of game states that you needed to store, you
could pack them tightly using 18 bits for the base-4 representation or 15 bits
for the base-3 representation. That’s a savings of 16%, which may or may not
be worth it.</p>

<p>And if you’re chosing a representation for CPU performance, then the base-4
representation wins hands down. The base-3 representation has a lof of
multiplication and division that can’t be easily optimized away.</p>

<p>But if you had some wild application where you needed to keep trillions of game
states unpacked in memory, then sure, use base-3.</p>

<p>This is a wild case of premature optimization that nobody asked for. 😅</p>

<p>You can find <a href="https://gist.github.com/cbarrick/8c5726dcca7f5f1e436585e672bc7f1f">test cases on GitHub</a>.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p><a href="https://www.egr.msu.edu/~kdeb/papers/k2007002.pdf">https://www.egr.msu.edu/~kdeb/papers/k2007002.pdf</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>Technically, we need \(\log_2(765) \approx 9.58\) bits, but there is no
  good way to use that final fraction of a bit. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p>Again, we technically only need \(\log_2(3^9) \approx 14.26\) bits. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Chris Barrick</name></author><summary type="html"><![CDATA[I recently stumbled upon a blog post by Alejandra González (a.k.a @blyxyas) that seeks to compress a tic-tac-toe game state into as few bits as possible. She arrived at a solution in 18 bits. This got me thinking, can we do better?]]></summary></entry></feed>