<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.9.4">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2025-07-04T06:41:00+00:00</updated><id>/feed.xml</id><title type="html">Robbie Ostrow</title><subtitle>A collection of entirely useless tidbits.</subtitle><author><name>Robbie Ostrow</name><email>robbie@ostro.ws</email></author><entry><title type="html">Fun with Timing Attacks</title><link href="/post-timing-attacks" rel="alternate" type="text/html" title="Fun with Timing Attacks" /><published>2024-03-09T00:00:00+00:00</published><updated>2024-03-09T00:00:00+00:00</updated><id>/timing-attacks</id><content type="html" xml:base="/post-timing-attacks"><![CDATA[<style>
  .loader {
    border: 16px solid #f3f3f3; /* Light grey */
    border-top: 16px solid #3498db; /* Blue */
    border-radius: 50%;
    width: 120px;
    height: 120px;
    animation: spin 2s linear infinite;
  }

  @keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
  }

  input:invalid {
    border: red solid 3px;
  }

  .loading-text {
    color: gray;
  }

</style>

<p><a href="#demo">Skip straight to the demo</a></p>

<p>Let’s say you’re writing a function that takes user input and checks if it matches some secret.</p>

<p>You’ll be exposing this <code class="language-plaintext highlighter-rouge">checkSecret</code> function to external users so you want to make sure it’s safe to use without leaking the secret. As long as your secret is long enough, it’s unlikely to be brute-forced. You’re feeling pretty confident that this simple function that does nothing but check equality doesn’t have any glaring security flaws.</p>

<details>
<summary><strong>Code for <code>checkSecret</code></strong></summary>

<pre>
const SUPER_SECRET_VALUE = "hunter2";
function checkSecret(guess: string) {
  return guess.startsWith(SUPER_SECRET_VALUE);
}
&gt; checkSecret("hunter2")
&lt; true
&gt; checkSecret("nothunter2")
&lt; false
</pre>
</details>

<p><br />
Anyway, <strong>an adversary who can call this function repeatedly can derive a 10-character secret in just a few thousand calls to checkSecret.</strong></p>

<h2 id="how">How?</h2>

<p>Builtins checking equality are implemented in native code that may differ per runtime, but it’s straightforward to imagine how anybody would implement it. <code class="language-plaintext highlighter-rouge">===</code> has some details regarding string interning that make analysis a lot more complicated, so we’ll use <code class="language-plaintext highlighter-rouge">startsWith</code> for this post. Ignoring details, <code class="language-plaintext highlighter-rouge">startsWith</code> might look something like this:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">startsWith</span><span class="p">(</span><span class="nx">target</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">searchString</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">searchString</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">a</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="o">!==</span> <span class="nx">b</span><span class="p">[</span><span class="nx">i</span><span class="p">])</span> <span class="p">{</span>
      <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Just iterate through the input and check if the character at each position matches. If not, <em>break early</em> and return <code class="language-plaintext highlighter-rouge">false</code>. If so, return <code class="language-plaintext highlighter-rouge">true</code>. While <a href="https://tc39.es/ecma262/#sec-string.prototype.startswith">the spec</a> doesn’t technically require early exit, most implementations use something like the above. (See V8’s implementation <a href="https://github.com/v8/v8/blob/f7b140d8f91d2ce3db60c49a41ae1c164c88e69d/src/builtins/string-endswith.tq#L7">here</a>.)</p>

<p><em>Break early</em> is the important part. On average, <strong>guesses whose prefixes partially match the secret’s are going to take a bit longer to return than guesses that are totally wrong</strong>, and the longer the prefix match, the longer the function is going to take. We can measure this directly in your browser:</p>

<p class="loading-text" id="startswith-loading-text">Running <code>startsWith</code> a few thousand times...</p>
<div id="startswith-plot" style="height:400px;display: flex;justify-content: center;align-items: center;">
  <div id="startswith-loader" class="loader">
  </div>
</div>

<p>If your browser supports accurate-ish timers, you’ll see that the distributions overlap but the blue plot is shifted to the right, because, on average over many calls, <code class="language-plaintext highlighter-rouge">"password".startsWith("pxxxxxxx");</code> takes longer than <code class="language-plaintext highlighter-rouge">"password".startsWith("xxxxxxxx");</code>.</p>

<p>We can use this characteristic to guess the secret by timing lots of calls to <code class="language-plaintext highlighter-rouge">checkSecret</code>. Using information like time or memory usage is known as a <a href="https://csrc.nist.gov/glossary/term/side_channel_attack">side-channel attack</a>.</p>

<p>Remote timing attacks were generally considered to be impractical until the estimable <a href="https://users.ece.cmu.edu/~dbrumley/">David Brumley</a> and <a href="https://crypto.stanford.edu/~dabo/">Dan Boneh</a> (for whom I happened to TA back in grad school) <a href="https://crypto.stanford.edu/~dabo/papers/ssl-timing.pdf">showed a practical attack on OpenSSL in 2003</a>. Since then, side-channel attacks have had a bit of a renaissance, with <a href="https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)">Spectre and Meltdown</a> the most famous examples.</p>

<p>Let’s see if we can run a timing attack on this page! This is a toy example, but the ideas apply to real-world systems.</p>

<h2 id="demo">Demo</h2>

<p>I’m not paying for a server for this post; Cloudflare<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> is kindly hosting this site for free. Instead, we’re going to simulate the server by running the <code class="language-plaintext highlighter-rouge">checkSecret</code> function in a web worker. This is a separate thread that can’t access the DOM but is still running in your browser. We’ll measure the time it takes to run <code class="language-plaintext highlighter-rouge">checkSecret</code> and use that to guess the secret<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>.</p>

<p>We need to check a few things before we can run the demo. First, let’s make sure that your browser supports web workers:</p>

<p class="loading-text" id="web-workers"> Checking for web worker support... </p>

<p>We also need to ensure that it supports cross-origin isolation. Browser timers are <a href="https://developer.mozilla.org/en-US/docs/Web/API/Performance/now#security_requirements">coarsened to 100μs</a> precision unless isolated
to prevent attacks just like this. It’ll still work with the imprecise timers, but the demo will take much (much) longer.</p>

<p class="loading-text" id="cross-origin-isolated"> Testing cross-origin isolation... </p>

<p>Note that this example is fastest on desktop Chrome and Chromium-based browsers, which support 5μs precision, matching the spec. Firefox, Safari, and mobile WebKit-based browsers are stuck at 20μs because they were <a href="https://www.mozilla.org/en-US/security/advisories/mfsa2018-01/">scared off by Spectre in 2018</a> and haven’t come up with a better solution yet.</p>

<p>Since the browser is rudely not giving us the time resolution we need<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>, we need to repeat each trial enough times to
actually register on the clock. Below, we’re running a benchmark to see how many times we need to call <code class="language-plaintext highlighter-rouge">checkSecret</code> in order to spread the result across a couple of buckets. Otherwise, all times will be 0 and it’ll be hard to do any statistics.</p>

<p class="loading-text" id="benchmarking-page">Benchmarking page to choose good parameters...</p>
<div id="benchmark-plot" style="height:400px;display: flex;justify-content: center;align-items: center;">
  <div id="benchmark-loader" class="loader">
  </div>
</div>

<p>Running this outside a browser context or anywhere with more precise timings, you could stick with a much smaller number of iterations.</p>

<p>Let’s guess your password! Enter a lowercase 8-character password below (I promise the web worker isn’t cheating, but this post comes with source maps so you can check for yourself):</p>

<p><input type="text" id="password-input" placeholder="password" pattern="[a-z]{8}" minlength="8" maxlength="8" title="Please enter 8 lowercase English characters" required="" />
<button type="submit" id="password-guess-button" disabled="">start guessing</button></p>

<div id="scrolling-passwords"> </div>
<p><br />
How’d we do? There are ~209 billion 8-character lowercase passwords. We guessed yours in</p>

<div class="loading-text" id="num-guesses"> not guessed yet... </div>
<p><br />
guesses. Since this approach scales linearly(ish) with the length of the password, rather than exponentially, longer passwords aren’t enough to mitigate the issue.</p>

<h2 id="optimizing-the-guessing-technique">Optimizing the guessing technique</h2>

<p>What is the guesser above actually doing? The obvious attack approach here is to try to derive one character at a time in the prefix. Make lots of guesses, measuring the time per-prefix. Once it’s clear that one consistently takes longer than the others, fix it and repeat with the next character.</p>

<p>The problem with this approach is that computers are noisy and – especially over a network boundary or with limited timing capabilities, it might take a lot of samples to come to that clarity. We want to minimize the number of guesses we expect to need to make so this attack is feasible in a reasonably short amount of time.</p>

<p>First, let’s formalize the problem a bit. Assume for simplicity that the secret length \(l\) and the permissible character set \(C\) are fixed<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>. We have a list of zero or more <code class="language-plaintext highlighter-rouge">(guess, time(checkSecretNTimes(guess)))</code> measurements that we’ve already taken that we’ll call \(M\). We want to land on some strategy \(makeGuess(l, C, M) \rightarrow guess\) that emits guesses that yield the most expected additional information.</p>

<h3 id="thompson-sampling">Thompson Sampling</h3>

<p>A simple but effective approach is <a href="https://en.wikipedia.org/wiki/Thompson_sampling">Thompson Sampling</a>. If we build up our guesses one character at a time, we can reframe this problem similar to a stochastic <a href="https://en.wikipedia.org/wiki/Multi-armed_bandit">multi-armed bandit</a> problem – at each step, we have a set of possible arms (characters by which we could extend the prefix) and we want to pick the best one to make the most effective guess.</p>

<p>Since we have the list of measurements \(M\), we can always estimate the distribution \(D_{\text{prefix}}\) by filtering to all of the data that begins with that prefix and updating our prior distributions given that data. Thompson Sampling is simply sampling a single value from each distribution \(D_{\text{prefix} + c}\) for \(c \in C\) and choosing the \(c\) with the largest value. In pseudocode, \(makeGuess(l, C, M)\) looks like:</p>

<ul>
  <li>Set <code class="language-plaintext highlighter-rouge">prefix</code> to the empty string</li>
  <li>While <code class="language-plaintext highlighter-rouge">length(prefix)</code> \(&lt; l\):
    <ul>
      <li>Set <code class="language-plaintext highlighter-rouge">bestSample</code> to \(-\infty\) and <code class="language-plaintext highlighter-rouge">bestGuess</code> to <code class="language-plaintext highlighter-rouge">nil</code></li>
      <li>For each character \(c\) in \(C\):
        <ul>
          <li>Fit a distribution \(D_{\text{prefix} + c}\) to \(M\)</li>
          <li>Take a single sample from \(D_{\text{prefix} + c}\)</li>
          <li>If the sample is greater than <code class="language-plaintext highlighter-rouge">bestSample</code>, set <code class="language-plaintext highlighter-rouge">bestSample</code> to the sample and <code class="language-plaintext highlighter-rouge">bestGuess</code> to \(c\)</li>
        </ul>
      </li>
      <li>Set the prefix to <code class="language-plaintext highlighter-rouge">prefix + bestGuess</code></li>
    </ul>
  </li>
  <li>Return <code class="language-plaintext highlighter-rouge">prefix</code></li>
</ul>

<p>Intuitively, this means that the more confident we are in a prefix, the more likely it is that we’ll choose it, and the more data
we have the more we can build our confidence levels to make our next guess even better.</p>

<h3 id="but-what-does-fit-a-distribution-mean">But what does “fit a distribution” mean?</h3>

<p>We have no idea what the distribution of <code class="language-plaintext highlighter-rouge">time(checkSecret(guess))</code> might be. Luckily, due to the <a href="https://en.wikipedia.org/wiki/Central_limit_theorem">Central Limit Theorem</a>, we can assume<sup id="fnref:6" role="doc-noteref"><a href="#fn:6" class="footnote" rel="footnote">5</a></sup> that samples from <code class="language-plaintext highlighter-rouge">time(checkSecretNTimes(guess))</code> are approximately normal with mean \(N \cdot \mu\) as long as <code class="language-plaintext highlighter-rouge">N</code> is sufficiently large. Because of the timing constraints outlined above, we need to repeat our guesses many times anyway, so our samples are going to look approximately normal no matter what the real distribution of <code class="language-plaintext highlighter-rouge">time(checkSecret)</code> on your computer might be.</p>

<p>Assuming normality is especially convenient because the algorithm described above would be devastatingly slow if we had to iterate through all of \(M\) every time we wanted to estimate some distribution’s parameters. Instead, we can just keep track of the sample mean and variance of each distribution and update them as we go. We’re going to make a lot of guesses, so we need online updates to make guesses in \(O(l * \vert C\vert)\) time rather than \(O(\vert M\vert)\). We can use the <a href="https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm">Welford algorithm</a> to update the mean and variance of each distribution in constant time as we gather more samples.</p>

<h3 id="storing-lots-of-distributions">Storing lots of distributions</h3>

<p>To make a guess, we need to quickly sample from the normal distributions that we’re storing per-prefix. We can use a <a href="https://en.wikipedia.org/wiki/Trie">Trie</a> where each node stores the parameters of the normal distribution that we believe describes the prefix defined by that node and where the edges are the elements of \(C\). This way, we can quickly update the mean and variance of each distribution and quickly sample from them.</p>

<p>Whenever we get a new measurement, we can update the mean and variance of each node on the path as we walk the trie.</p>

<h4 id="example-updating-the-trie">Example: updating the trie</h4>

<p>As we take measurements, we can quickly update the trie to reflect our new data.</p>

<details>
  <summary>
    <strong>Expand to see example.</strong>
  </summary>
<p>
First, we guess `abc` and measure time `(0.4)`. We update the trie:

<pre>
'' (prefix='', mean=0.4, variance=0, count=1)
  'a' (prefix='a', mean=0.4, variance=0, count=1)
    'b' (prefix='ab', mean=0.4, variance=0, count=1)
      'c' (prefix='abc', mean=0.4, variance=0, count=1)
</pre>

Next, we guess `abd` and time `(0.5)`. We update the trie again:

<pre>
'' (prefix='', mean=0.45, variance=0.005, count=2)
  'a' (prefix='a', mean=0.45, variance=0.005, count=2)
    'b' (prefix='ab', mean=0.45, variance=0.005, count=2)
      'c' (prefix='abc', mean=0.4, variance=0, count=1)
      'd' (prefix='abd', mean=0.5, variance=0, count=1)
</pre>

And finally, we guess `abc` and time `(0.4)` again. We update the trie once more:

<pre>
'' (prefix='', mean=0.433, variance=0.0033, count=3)
  'a' (prefix='a', mean=0.433, variance=0.0033, count=3)
    'b' (prefix='ab', mean=0.433, variance=0.0033, count=3)
      'c' (prefix='abc', mean=0.4, variance=0, count=2)
      'd' (prefix='abd', mean=0.5, variance=0, count=1)
</pre>
</p>
</details>
<p><br />
Updating the trie takes only \(O(l)\) time, because we only have to update the mean and variance of the nodes on the path to the leaf, and each online update is constant time.</p>

<p>If \(M\) is large, we might have a lot of nodes in the trie. We could prune the trie by removing nodes that have too few measurements – however, in practice, we can usually just keep the whole trie in memory since the search usually completes within a few thousand guesses.</p>

<h3 id="sampling-from-the-trie">Sampling from the trie</h3>

<p>We walk the trie, taking a sample from each node’s distribution, and choose the character that leads to the largest sample. However, we may not have samples from all nodes, so with some probability we choose a random character to ensure we continue to explore the space so we don’t get stuck.</p>

<p>Our guessing algorithm is similar to that described above but takes two important parameters:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">NOISE_PER_STEP</code>: the probability that we choose a uniformly random character out of \(C\) instead of sampling from the trie.</li>
  <li><code class="language-plaintext highlighter-rouge">MIN_SAMPLES</code>: the minimum number of samples we need to have from a node before we consider it. With only a few samples, the parameters of the normal distribution are not very reliable and we’re likely to make a bad guess. There are more sophisticated statistical techniques to express certainty here, but just not considering nodes with count &lt; 3 works fine in practice. This way, we don’t need to build a robust prior distribution instead just ignore any prefixes with too few measurements until they have &gt; MIN_SAMPLES measurements in \(M\).</li>
</ol>

<p>So, extending the algorithm from above, we update \(makeGuess(l, C, M)\) to:</p>

<ul>
  <li>Set the prefix to the empty string</li>
  <li>While <code class="language-plaintext highlighter-rouge">length(prefix)</code> \(&lt; l\):
    <ul>
      <li>Look up the prefix in the trie
        <ul>
          <li>If the prefix isn’t in the trie, return the prefix + random characters up to length \(l\)</li>
        </ul>
      </li>
      <li>Set <code class="language-plaintext highlighter-rouge">bestSample</code> to \(-\infty\) and <code class="language-plaintext highlighter-rouge">bestGuess</code> to <code class="language-plaintext highlighter-rouge">nil</code></li>
      <li>For each character \(c\) that are children of the current node and have count &gt; MIN_SAMPLES:
        <ul>
          <li>Look up the distribution \(D_{\text{prefix} + c}\) in the trie</li>
          <li>Take a single sample from \(D_{\text{prefix} + c}\) using the <a href="https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform">Box-Muller transform</a></li>
          <li>If the sample is greater than <code class="language-plaintext highlighter-rouge">bestSample</code>, set <code class="language-plaintext highlighter-rouge">bestSample</code> to the sample and <code class="language-plaintext highlighter-rouge">bestGuess</code> to \(c\)</li>
        </ul>
      </li>
      <li>Set the prefix to <code class="language-plaintext highlighter-rouge">prefix + bestGuess</code></li>
    </ul>
  </li>
  <li>Return the prefix</li>
</ul>

<p>We repeat this many times, adding the <code class="language-plaintext highlighter-rouge">(guess, time)</code> data to the trie describing \(M\) to inform our guesses, until hopefully we guess the right secret.</p>

<h2 id="using-this-approach-over-the-network">Using this approach over the network</h2>

<p>There may be situations in which you’re trying to exfiltrate a secret locally – however, the majority of the time you’ll be trying to guess a secret over a network boundary. In this case, you’ll need to take into account the noise of the network.</p>

<p>This is a bit trickier because the network noise is probably greater than the time it takes to run <code class="language-plaintext highlighter-rouge">checkPassword</code>, and other traffic may impact timing information, so each measurement is generally going to yield a lot less information than the relatively direct measurements we’re taking here. However, if you’re allowed enough trials, the same approach works with no code changes, since we didn’t make any assumptions about the underlying distribution and the network jitter just increases the mean and variance of the distributions that we’re measuring.</p>

<p>Running this algorithm against a much noisier distribution is much slower so isn’t as conducive to an inline demo but if there’s interest, I may host an endpoint with a vulnerable checkSecret to see who can break it first!</p>

<h2 id="finally">Finally</h2>

<p>When you’re writing software that compares user-provided data against sensitive values:</p>

<ol>
  <li>Don’t. You should almost never be comparing directly against secret values. Use hashes, pdkf2, or whatever is appropriate for your situation. But even these algorithms may perform differently with different inputs, so be careful!</li>
  <li>If you <em>must</em> use a secret value as a function input, do so very carefully! Network noise doesn’t necessarily save you. Use a vetted library that does sensitive operations for you instead of trying to implement it yourself, and always ensure solid rate limits are in place.</li>
</ol>

<p>While I used <code class="language-plaintext highlighter-rouge">startsWith</code> in this post, almost any non-constant-time comparison is susceptible to the same attack. Even the <code class="language-plaintext highlighter-rouge">===</code> operator is likely to be vulnerable given enough trials if you’re careful about avoiding string interning that leads to constant-time comparison (I couldn’t get this working, but I’d be curious to see if anyone can.)</p>

<hr />

<h4 id="footnotes">Footnotes</h4>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>I had to move this damn site from Github Pages to Cloudflare because Github Pages doesn’t support custom headers, which are <a href="https://developer.mozilla.org/en-US/docs/Web/API/Performance/now#security_requirements">needed for cross-origin isolation</a>. Cloudflare has been great so far but I refuse to be trapped into blogging about setting up a blog. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>To make the demo run snappily, we aren’t sending messages back and forth between the web worker and the main thread. Instead, we’re just running the <code class="language-plaintext highlighter-rouge">checkSecret</code> function in the web worker and measuring the time it takes to run. This is a bit of a cheat, but the same code would work to guess secrets over an actual noisy network boundary – it might just take a bunch more guesses. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p>I originally wrote this code in Node and was flummoxed when all timings were exactly 0.0000000000000ms when running code in the browser that I knew took at least 30 or 40 μs in Node. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p>In the real world, you will often know the length of the secret (e.g. an API key) but even if you don’t, this same technique works – just run the same algorithm repeatedly, fixing the length each time. Similarly, in the real world, “printable ascii characters” is usually a pretty good bet for an allowable character set. <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:6" role="doc-endnote">
      <p>Assuming these measurements are independent – which they might not be, since they’re taken back-to-back on a machine, but close enough. <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Robbie Ostrow</name><email>robbie@ostro.ws</email></author><summary type="html"><![CDATA[Guess secrets in your browser by timing some stuff!]]></summary></entry><entry><title type="html">Stop Fearing Incidental Findings</title><link href="/post-healthcare" rel="alternate" type="text/html" title="Stop Fearing Incidental Findings" /><published>2024-03-01T00:00:00+00:00</published><updated>2024-03-01T00:00:00+00:00</updated><id>/healthcare</id><content type="html" xml:base="/post-healthcare"><![CDATA[<p><sub><sup>(Disclosure<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>)</sup></sub></p>

<p>We have two options for the next hundred years of healthcare:</p>

<ol>
  <li>Gather as much data as possible, and make healthcare better (and false positives less common).</li>
  <li>Don’t – because we’re afraid that people will get scared and act unwisely when they see an incidental finding.</li>
</ol>

<p>Doctors in <a href="https://www.newyorker.com/science/annals-of-medicine/will-a-full-body-mri-scan-help-you-or-hurt-you">popular</a> <a href="https://www.health.com/full-body-mri-maria-menounos-7496814">media</a> are lining up to say that (1) is a Bad Idea.</p>

<p>From the New York Times (<a href="https://www.nytimes.com/2023/09/19/well/live/mri-prenuvo-full-body-scan.html">paywall</a>, emphasis mine):</p>

<blockquote>
  <p>In April, the American College of Radiology released a statement . . . expressing concern that scans could lead to “nonspecific findings” that <strong>require</strong> extensive, expensive follow-up.</p>
</blockquote>

<p>In fact, that isn’t quite what the American College of Radiology said. Their <a href="https://www.acr.org/Media-Center/ACR-News-Releases/2023/ACR-Statement-on-Screening-Total-Body-MRI">statement</a> didn’t use the word require, but instead expressed concern that scans could lead to nonspecific findings that <strong>result in</strong> extensive, expensive follow-up.</p>

<p>The <strong>require</strong> vs <strong>result in</strong> mixup seems like a semantic point but it’s an important one. The ACR is not saying that follow-ups are necessary, only that they are common. These often spurious, dangerous, and expensive follow-ups are a symptom of broken processes in healthcare, not a predetermined consequence of an untargeted MRI.</p>

<h2 id="more-data-is-better">More data is better</h2>

<p>“If you scan more, you see more” is certainly true. But “if you see more, you do dangerous invasive follow-ups” doesn’t have to be. Unless we change the culture from “if you see something incidental, you must act” to “if you see something incidental, you must act if the disease is likelier to harm<sup id="fnref:6" role="doc-noteref"><a href="#fn:6" class="footnote" rel="footnote">2</a></sup> you than the follow-up,” we’ll be stuck fearing incidental findings forever.</p>

<p>As a bonus, by gathering more data about you, we can regularly update our priors about whether each disease is likely to cause harm.</p>

<p>There’s no Platonic ideal of a human body. Instead of comparing your body to some ideal from which it will always deviate, we should be comparing it
against your baseline. If you see something suspicious – but it hasn’t changed from your last scan – it’s likely not suspicious after all. If we do it right, more scanning should <em>decrease</em> the number of false positives, not <em>increase</em> them – but only if we’re good about not panicking when we see something outside of the “normal” range the first time we measure it<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">3</a></sup>.</p>

<h2 id="philosophical-point-taken-but-if-i-test-positive-for-something-im-still-going-to-get-the-follow-up">Philosophical point taken, but if I test positive for something, I’m still going to get the follow-up!</h2>

<p>I’m certainly not arguing that everyone should get all of the tests all of the time (yet). They cost money and resources,
and because medicine is so intervention-focused today, if you get a test, your doctors may very well feel compelled to act on it – if only to protect themselves from a lawsuit<sup id="fnref:7" role="doc-noteref"><a href="#fn:7" class="footnote" rel="footnote">4</a></sup>.</p>

<p>But we should be working to change that. We should be working to make each piece of data as cheap and safe<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">5</a></sup> as possible to gather,
and we should measure baselines to rule out items of concern that haven’t changed from exam to exam. We should be working to make healthcare more data-driven and less likely to skip straight to dangerous interventions. We should also be working to stop treating every piece of data as a binary “positive” or “negative” result and avoid punishing doctors for making the right statistical decisions.</p>

<p>Nikhil Krishnan has a great piece on <a href="https://www.outofpocket.health/p/why-dont-we-screen-healthy-people">why we don’t screen healthy people to catch diseases early</a>. Cribbing directly from that piece (which is worth a read), let’s consider the extreme example of Nikhilitis, a disease that affects 1/1000 people. We have a cheap screen that is safe and 99% sensitive (if you have it, the test will be positive 99% of the time) and 90% specific (if you don’t have it, the test will be negative 90% of the time).</p>

<p>This means that if a random member of the population tests positive, they have a 1% chance of having Nikhilitis (math in footnote<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">6</a></sup>). Subsequent health decisions should be based on that number, not on the scary word “positive.”</p>

<p>Let’s also imagine that the confirmatory diagnosis is a lobotomy with a 0.1% mortality rate. If the expected mortality rate of Nikhilitis is less than 10%, don’t get the freaking lobotomy! (The same footnote<sup id="fnref:3:1" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">6</a></sup> goes on to explain your expected mortality rate via confirmatory biopsy vs Nikhilitis.) But, if you’re in a special population where Nikhilitis is especially dangerous, the lobotomy might be the right choice.</p>

<p>Even Nikhil, whose post is otherwise excellent, implies that everyone who tests positive <em>needs</em> a biopsy. They don’t <em>need</em> a biopsy. The screen cannot ever tell you directly whether or not to get a biopsy; it just gives you more information about your risk for Nikhilitis. You only <em>need</em> a biopsy if that new information (plus other information about you) indicates that you’re safer to get the lobotomy than to let the possibility of Nikhilitis ride.</p>

<h4 id="so-if-no-one-should-get-the-follow-up-what-was-the-point-of-the-screen-anyway">So if no one should get the follow-up, what was the point of the screen anyway?</h4>

<p>Unlike the Nikhilitis screen, baseline measurements like blood panels or full-body MRIs have utility outside of a binary decision on a specific day. If you get a full-body MRI, you have information that is affirmatively useful for your healthcare down the line. How useful will depend on how many people get these MRIs and the quality of science we can do with the data. It’s a tricky cold-start problem but, in my view, it’s the most important one that we have to solve to stop people dying of preventable diseases.</p>

<h2 id="but-we-live-in-the-real-world-where-people-dont-understand-conditional-probability-and-will-get-the-follow-up-anyway">But we live in the real world, where people don’t understand conditional probability and will get the follow-up anyway</h2>

<p>A few (untested) suggestions that I think might marginally improve how we discuss preventative healthcare:</p>

<h3 id="report-screening-results-as-percentages-rather-than-positive-or-negative">Report screening results as percentages rather than “positive” or “negative”</h3>

<p>If your screen comes back positive for Nikhilitis with no other indicators, the report (and your doctor) should explain that “our data tells us that you have about a 1% chance of having Nikhilitis given everything else we know about you.” The words “positive,” “reactive,” “abnormal,” and similar should be reserved for extremely significant results. <a href="https://slatestarcodex.com/2013/12/17/statistical-literacy-among-doctors-now-lower-than-chance/">Doctors have trouble understanding this too</a>, so the labs themselves should be careful about the language in their reports.</p>

<h3 id="stop-calling-full-body-mri-a-screen">Stop calling full-body MRI a “screen”</h3>

<p>We shouldn’t be thinking of full-body MRI as a “screen.” Instead, it’s just a series of measurements. In the same way that most of a normal panel of blood tests are not a “screen” so much as they are individual measurements of your cholesterol, your liver function, and so on, a full-body MRI is just a series of measurements of your body’s structure and function. While many companies in the space are marketing themselves as screeners, the responsible ones are quietly figuring out how to gather repeatable data as cheaply and easily as a blood test.</p>

<p>Unless you’re using an MRI to screen for specific diseases (in which case you should be giving careful Bayesian treatment as above) we should be using these technologies to gather baselines and understand change, rather than treating them as solely point-in-time mechanisms to catch specific diseases early.</p>

<h3 id="longitudinal-data-is-useful">Longitudinal data is useful</h3>

<p>We should track changes over time. If something is stable, it’s probably not a problem. If something is changing, it might be. For example, I have a cyst in my pelvis. It hasn’t changed in years. In 20 years, when I’m getting an MRI for one reason or another, the doctor will likely want to do something about the cyst. But I will know that the cyst has been there, perfectly safe and unchanging, for decades. That information about one of many little ways I’m a bit abnormal may prevent me from going under the knife.</p>

<h3 id="incentive-alignment-is-hard-so-we-have-to-keep-driving-costs-down">Incentive alignment is hard, so we have to keep driving costs down</h3>

<p>What’s the incentive for you to get tests if they’re not immediately actionable? It’s hard to justify spending a big chunk of money (or a big chunk of shared healthcare bandwidth) on something with no immediate payoff. If the Nikhilitis test doesn’t give you any actionable insight, what was the point? As costs decrease and science improves, the value per datapoint gathered will increase while the cost to gather each datapoint will decrease until eventually the lines cross. To get to that point, we need to subsidize the cost of gathering data and make it as easy as possible to gather across healthy and unhealthy populations.</p>

<h2 id="finally">Finally</h2>

<p>Don’t be afraid to get the full-body MRI – but before you look at the results, be prepared to (maybe) ignore (only maybe) scary findings. Remember, without the test, you wouldn’t have found out about these findings anyway! Armed with new information, you can track if there is change over time; and if there’s something obvious and immediately actionable, you’ve caught it early.</p>

<p>We should be encouraging data gathering at scale. The alternative – keeping our heads in the sand – will keep us dying in
the dark as people continue to suffer from diseases we would have caught early if we’d been brave enough to look.</p>

<hr />

<p>Thanks to <a href="https://linkedin.com/in/benweems">Ben Weems</a> and Elisabeth Ostrow for their feedback on this post!</p>

<hr />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>I used to run the software engineering team at <a href="https://q.bio">Q Bio</a>, a company that works on full-body MRI. We didn’t think of our scans as screening for specific diseases so much as a way to gather a ton of data about a person’s health over time, and to use that data to make that person’s healthcare better. That said, it’s always hard to gather data about a person and tell them “don’t do anything about it!” <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:6" role="doc-endnote">
      <p>Including financially, emotionally, or however else you define harm. <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p>It isn’t currently permissible within our ethical framework, but imagine a situation where everything about you got measured every year from, say, 18-35 and kept hidden from you and all of your doctors for the first 15 years. Think of all the useful data you would have when you got knee pain at 35! “Oh, this cyst in my knee has been here the whole time; it’s probably not that.” <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:7" role="doc-endnote">
      <p>Malpractice law is not set up for this kind of thinking. I don’t have an easy solution for this. <a href="#fnref:7" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>Note also that, unlike a CT scan, MRI is near-100% safe (modulo getting accidentally smashed by an oxygen canister that somebody accidentally brings into the room,) so \(P(\text{test harms you})\) is near 0. So, we should be giving people MRIs regularly, (if they’re cheap enough), and updating our priors about our health with the information they provide. Just don’t do a biopsy or an X-ray unless those priors are high enough! <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">

      <p>First, let’s calculate the probability that you have Nikhilitis given a positive test. We know that:</p>

\[\begin{align*}
P(\text{positive} | \text{nikhilitis}) &amp;= 0.99 \\
P(\text{nikhilitis}) &amp;= 0.001 \\
P(\text{positive}) &amp;= P(\text{positive} | \text{nikhilitis}) P(\text{nikhilitis}) + P(\text{positive}|\neg \text{nikhilitis}) P(\neg \text{nikhilitis}) \\
&amp;= 0.99 \times 0.001 + 0.1 \times 0.999 \approx 0.101 \\
\end{align*}\]

      <p>Thus, applying Bayes’ theorem</p>

\[\begin{align*}
P(\text{nikhilitis} | \text{positive}) &amp;= \frac{P(\text{positive} | \text{nikhilitis}) P(\text{nikhilitis})}{P(\text{positive})} \\
&amp;\approx \frac{0.99 \times 0.001}{0.101}\\
&amp;\approx 0.01\\
\end{align*}\]

      <p>Now, if the baseline mortality rate of Nikhilitis is 1% and assuming that the test sensitivity is independent from the mortality rate, if you have a positive test, your posterior probability of dying from Nikhilitis is 1% (chance of having it) * 1% (its mortality rate) = 0.01%. If the mortality rate of the confirmatory diagnosis or other follow-ups are themselves greater than 0.01%, (or otherwise have financial/psychological/quality-of-life harm equivalent to that risk) you shouldn’t follow up.</p>

      <p>Concretely, in our example, the confirmatory test has a 0.1% mortality rate. You shouldn’t get the confirmatory test, since 0.1% &gt; 0.01%! If you’re in a population where Nikhilitis is &gt;10% fatal, though, you probably should – because all of a sudden, your chance of dying from Nikhilitis is at least 1% * 10% = 0.1%, so the lobotomy is at least as safe as chancing Nikhilitis.</p>

      <p>Disclaimers abound here, of course. Mortality is not the only danger, money is not the only cost, and these numbers are extreme. But the reasoning holds even if the real-world calculations need more terms. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:3:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p>
    </li>
  </ol>
</div>]]></content><author><name>Robbie Ostrow</name><email>robbie@ostro.ws</email></author><summary type="html"><![CDATA[We have two options for the next hundred years of healthcare: gather as much data as possible and make healthcare better, or – don't – because we're afraid that people will get scared and act unwisely when they see an incidental finding.]]></summary></entry><entry><title type="html">Permiscuchess</title><link href="/post-permiscuchess" rel="alternate" type="text/html" title="Permiscuchess" /><published>2021-03-16T00:00:00+00:00</published><updated>2021-03-16T00:00:00+00:00</updated><id>/permiscuchess</id><content type="html" xml:base="/post-permiscuchess"><![CDATA[<p>Chess for people who are good at computers and bad at chess.</p>

<p>Many of my colleagues are excellent chess players. I am not. Something had to be done.</p>

<p>Enter a new chess variant: Permiscuchess! Permiscuchess is a chess variant that is entirely pre-played. Instead of a real-time battle of wits, Permiscuchess is played by devising a clever strategy – a permutation of all chess moves, to be applied one at a time until you win, lose, or draw against thousands of random adversaries.</p>

<p><a href="https://permiscuchess.ostro.ws">Play Permiscuchess here.</a> All simulations are done client-side and there is no leaderboard. But feel free to email me to gloat if you find a really good strategy, or send me a Lichess link to a funny game<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>.</p>

<p>It doesn’t work very well on phones.</p>

<p>I won’t get into all of the rules here – click on “How to play!”</p>

<p><img src="/assets/images/permiscuchess/screenshot.png" alt="A screenshot of the Permiscuchess app" /></p>

<p>Going for scholar’s mate as white (pe:e4, bf:c4, q:f3, q:f7, then random) wins about 35% of the time. Can you do better?</p>

<hr />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>Open a game in Lichess by selecting the game, clicking “PGN,” and clicking “Open in Lichess.” <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Robbie Ostrow</name><email>robbie@ostro.ws</email></author><summary type="html"><![CDATA[Chess for people who are good at computers and bad at chess.]]></summary></entry><entry><title type="html">Improving Type Safety with ts-json-validator</title><link href="/post-ts-json-validator" rel="alternate" type="text/html" title="Improving Type Safety with ts-json-validator" /><published>2020-02-15T00:00:00+00:00</published><updated>2020-02-15T00:00:00+00:00</updated><id>/ts-json-validator</id><content type="html" xml:base="/post-ts-json-validator"><![CDATA[<p>Let JSON play nicely with Typescript using <a href="https://github.com/ostrowr/ts-json-validator">ts-json-validator</a>.</p>

<h2 id="why">Why?</h2>

<p>Naturally, all of the code you write is typed perfectly. But you’re not in charge of all that pesky data that
comes from other places.</p>

<p><code class="language-plaintext highlighter-rouge">JSON.parse</code> returns type <code class="language-plaintext highlighter-rouge">any</code>, which mangles all of your hard-earned strictness.</p>

<p>JSON validators are great, but they usually require you to define two things: the validation function and the
Typescript type to go along with it. These can get out of sync and are generally a pain to maintain. <a href="https://json-schema.org/">JSON schema</a> is a terrific idea, but the schemas are often tricky to write and even trickier to understand.</p>

<p><code class="language-plaintext highlighter-rouge">ts-json-validator</code> allows you to define everything in one place. It generates a compliant JSON schema, a Typescript type that matches objects that can be parsed by that schema, and provides a typesafe <code class="language-plaintext highlighter-rouge">parse</code> that throws if the JSON you get doesn’t match the type you’re expecting.</p>

<p>See <a href="https://github.com/ostrowr/ts-json-validator/blob/master/README.md">the readme</a> or <a href="https://www.npmjs.com/package/ts-json-validator">install from npm</a> if you don’t care about how the library works and just want to use it.</p>

<h2 id="how">How?</h2>

<p><img src="/assets/images/ts-json-validator/example.gif" alt="A gif showing type-hints from ts-json-validator" /></p>]]></content><author><name>Robbie Ostrow</name><email>robbie@ostro.ws</email></author><summary type="html"><![CDATA[Let JSON play nicely with Typescript using ts-json-validator.]]></summary></entry><entry><title type="html">Taking Types Too Far</title><link href="/post-taking-types-too-far" rel="alternate" type="text/html" title="Taking Types Too Far" /><published>2019-12-09T00:00:00+00:00</published><updated>2019-12-09T00:00:00+00:00</updated><id>/taking-types-too-far</id><content type="html" xml:base="/post-taking-types-too-far"><![CDATA[<p>Herein we check the Collatz conjecture using only Typescript’s type system.</p>

<p>I love Typescript, but it isn’t nearly ambitious enough. It would be vastly improved with an <code class="language-plaintext highlighter-rouge">--extremelyStrict</code> flag enforcing that your Typescript code is free of side-effects; that is – no Javascript code is generated at all. <a href="https://web.mit.edu/humor/Computers/real.programmers">Real programmers</a> do all of their computation within the type system. Otherwise, they can’t be sure their program will work in production and should be duly fired.</p>

<p>“But you can’t even do arithmetic in the type system,” you complain. Not so.</p>

<p>Javascript is all well and good, but nobody <em>really</em> knows how the integers are defined. If we’re going to do Real Number Theory, we need to be sure that our numbers are formal enough to pass muster. Trust your own logic, not the Ecma committee’s. Javascript doesn’t have a natural number type, and you never know what might happen when you try to use Javascript <code class="language-plaintext highlighter-rouge">number</code>s when you’re trying to do Precise Mathematics.</p>

<hr />

<p>So let’s define the natural numbers.</p>

<p>Here we are:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span> <span class="nx">Natural</span> <span class="o">=</span> <span class="p">{</span> <span class="na">prev</span><span class="p">:</span> <span class="nx">Natural</span> <span class="p">};</span>
</code></pre></div></div>

<p>Easy. We have a type that we’ve called Natural. It’s not a natural number yet, though. Let’s see if we can get it to comply with the <a href="https://en.wikipedia.org/wiki/Peano_axioms">Peano axioms</a>, which is a set of axioms that formalizes the properties of these natural numbers.</p>

<p>We need zero:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span> <span class="nx">Zero</span> <span class="o">=</span> <span class="p">{</span> <span class="na">prev</span><span class="p">:</span> <span class="nx">never</span> <span class="p">};</span>
</code></pre></div></div>

<p>We also need an equality relation:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span> <span class="nx">Equals</span><span class="o">&lt;</span><span class="nx">A</span> <span class="kd">extends</span> <span class="nx">Natural</span><span class="p">,</span> <span class="nx">B</span> <span class="kd">extends</span> <span class="nx">Natural</span><span class="o">&gt;</span> <span class="o">=</span> <span class="nx">A</span> <span class="kd">extends</span> <span class="nx">B</span>
  <span class="p">?</span> <span class="nx">B</span> <span class="kd">extends</span> <span class="nx">A</span>
    <span class="p">?</span> <span class="kc">true</span>
    <span class="p">:</span> <span class="kc">false</span>
  <span class="p">:</span> <span class="kc">false</span><span class="p">;</span>
</code></pre></div></div>

<p>It’s not that useful to have a set of natural numbers that doesn’t do anything. We need a way to transform one into another – a successor function <code class="language-plaintext highlighter-rouge">S</code> will do the trick:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span> <span class="nx">S</span><span class="o">&lt;</span><span class="nx">T</span> <span class="kd">extends</span> <span class="nx">Natural</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">prev</span><span class="p">:</span> <span class="nx">T</span> <span class="p">};</span>
</code></pre></div></div>

<p>Let’s check the first Peano axiom – namely, <code class="language-plaintext highlighter-rouge">Zero</code> is a natural number:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span> <span class="nx">isZeroANaturalNumber</span> <span class="o">=</span> <span class="nx">Zero</span> <span class="kd">extends</span> <span class="nx">Natural</span> <span class="p">?</span> <span class="kc">true</span> <span class="p">:</span> <span class="kc">false</span><span class="p">;</span>
<span class="c1">// type isZeroANaturalNumber = true</span>
</code></pre></div></div>

<p>Nice. You can check the rest on your own time.</p>

<p>Let’s write out the first few numbers:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span> <span class="nx">One</span> <span class="o">=</span> <span class="nx">S</span><span class="o">&lt;</span><span class="nx">Zero</span><span class="o">&gt;</span><span class="p">;</span> <span class="c1">// type One = { prev: Zero }</span>
<span class="kd">type</span> <span class="nx">Two</span> <span class="o">=</span> <span class="nx">S</span><span class="o">&lt;</span><span class="nx">One</span><span class="o">&gt;</span><span class="p">;</span>
<span class="kd">type</span> <span class="nx">Three</span> <span class="o">=</span> <span class="nx">S</span><span class="o">&lt;</span><span class="nx">Two</span><span class="o">&gt;</span><span class="p">;</span>
<span class="kd">type</span> <span class="nx">Four</span> <span class="o">=</span> <span class="nx">S</span><span class="o">&lt;</span><span class="nx">Three</span><span class="o">&gt;</span><span class="p">;</span>
<span class="kd">type</span> <span class="nx">Five</span> <span class="o">=</span> <span class="nx">S</span><span class="o">&lt;</span><span class="nx">Four</span><span class="o">&gt;</span><span class="p">;</span>
<span class="kd">type</span> <span class="nx">Six</span> <span class="o">=</span> <span class="nx">S</span><span class="o">&lt;</span><span class="nx">Five</span><span class="o">&gt;</span><span class="p">;</span> <span class="c1">// type Six = { prev: S&lt;S&lt;S&lt;S&lt;S&lt;Zero&gt;&gt;&gt;&gt;&gt; }</span>
<span class="kd">type</span> <span class="nx">Seven</span> <span class="o">=</span> <span class="nx">S</span><span class="o">&lt;</span><span class="nx">Six</span><span class="o">&gt;</span><span class="p">;</span>
<span class="kd">type</span> <span class="nx">Eight</span> <span class="o">=</span> <span class="nx">S</span><span class="o">&lt;</span><span class="nx">Seven</span><span class="o">&gt;</span><span class="p">;</span>
<span class="kd">type</span> <span class="nx">Nine</span> <span class="o">=</span> <span class="nx">S</span><span class="o">&lt;</span><span class="nx">Eight</span><span class="o">&gt;</span><span class="p">;</span>
<span class="c1">// ... and so on</span>
</code></pre></div></div>

<p>Sweet! We’ve defined the natural numbers. Everything else is just notation.</p>

<p><em>Following along? <a href="http://www.typescriptlang.org/play/?ssl=19&amp;ssc=21&amp;pln=21&amp;pc=5#code/FAFwngDgpgBAcgQxAVwE4IDYwLwwN4wSpQBuAXPEmpjAL6iSwBaUqA9jvoceTAHalWdYA2gwAlgGcW7AIKIU6DHGQBbAEZDcMjlAAeIKHwAmkyopoB+GCFTJYFAGaZJUEeDEBRAI7IXAHlkYfUMTMwVqDAAaGAAhYIMjU3NIgD4cYBgsmCCQpLN4y0zs+LywnJgi7Js7B2Ks5wxXGDJ6mEbXd0YYAGV-ABUE0OSIpXTcAiJSCkH6UVgAeQFOPp1U+Zh+gHcOXD6lqHWPWH6AC2JYPYGdo+6AMTY0FYHzqEONu-ESS97-B7RbmIeuI9M9Pt9AbAeoI+M9gXpITBPOIAOanEBwmGIuDiZZXeHrAD0hJgADpyTAECYYJIOGxYUA">Here</a> is a Typescript playground with what we’ve done so far.</em></p>

<hr />

<p>This seems like a good time to introduce the <a href="https://en.wikipedia.org/wiki/Collatz_conjecture">Collatz conjecture</a>. Unproven until today (and after today, despite my best efforts,) it states:</p>

<p>Take any natural number. If it’s even, divide it by two. If it’s odd, multiply it by three and add one. Repeat. Eventually, you’ll end up at one.</p>

<p>Or, more formally:</p>

\[\begin{equation*}
f(n) =
\begin{cases}
  n/2 &amp;\text{n is even}\\
  3n+1 &amp;\text{n is odd}
  \end{cases}
\end{equation*}\]

<p>Apply <code class="language-plaintext highlighter-rouge">f</code> enough times to any natural number, and the Collatz conjecture conjects that you’ll eventually end up at \(1\).</p>

<hr />

<p>To check Collatz using our fancy new <code class="language-plaintext highlighter-rouge">Natural</code> type, we need to define division by two, multiplication by three, and addition by one. To be safe, let’s just define addition, subtraction, multiplication, and division.</p>

<p>First, let’s define a predecessor function <code class="language-plaintext highlighter-rouge">P</code>, which is the opposite of our successor function <code class="language-plaintext highlighter-rouge">S</code>. <code class="language-plaintext highlighter-rouge">P&lt;Zero&gt;</code> doesn’t make sense, so it is of type <code class="language-plaintext highlighter-rouge">never</code>.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span> <span class="nx">P</span><span class="o">&lt;</span><span class="nx">T</span> <span class="kd">extends</span> <span class="nx">Natural</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">prev</span><span class="p">:</span> <span class="nx">T</span><span class="p">[</span><span class="dl">"</span><span class="s2">prev</span><span class="dl">"</span><span class="p">][</span><span class="dl">"</span><span class="s2">prev</span><span class="dl">"</span><span class="p">]</span> <span class="p">};</span>
</code></pre></div></div>

<p>Addition is very straightforward. We can define it recursively as</p>

<ol>
  <li>
\[A + 0 = A\]
  </li>
  <li>
\[A + S(B) = S(A + B)\]
  </li>
</ol>

<p>Intuitively,</p>

<ol>
  <li>Zero is the additive identity</li>
  <li>A + (B + 1) = (A + B) + 1</li>
</ol>

<p>Defining addition over our <code class="language-plaintext highlighter-rouge">Natural</code> type:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span> <span class="nx">Add</span><span class="o">&lt;</span><span class="nx">A</span> <span class="kd">extends</span> <span class="nx">Natural</span><span class="p">,</span> <span class="nx">B</span> <span class="kd">extends</span> <span class="nx">Natural</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span>
  <span class="mi">0</span><span class="p">:</span> <span class="nx">A</span><span class="p">;</span>
  <span class="mi">1</span><span class="p">:</span> <span class="nx">S</span><span class="o">&lt;</span><span class="nx">Add</span><span class="o">&lt;</span><span class="nx">A</span><span class="p">,</span> <span class="nx">P</span><span class="o">&lt;</span><span class="nx">B</span><span class="o">&gt;&gt;&gt;</span><span class="p">;</span>
<span class="p">}[</span><span class="nx">B</span> <span class="kd">extends</span> <span class="nx">Zero</span> <span class="p">?</span> <span class="mi">0</span> <span class="p">:</span> <span class="mi">1</span><span class="p">];</span>
</code></pre></div></div>

<p><em>It might be clearer to write <code class="language-plaintext highlighter-rouge">type Add&lt;A extends Natural, B extends Natural&gt; = B extends Zero ? A : S&lt;Add&lt;A, P&lt;B&gt;&gt;</code> but we need to use this indexing hack to get around Typescript’s limitations on circular references in types.</em></p>

<p>Subtraction isn’t too hard either:</p>

<ol>
  <li>
\[A - 0 = A\]
  </li>
  <li>
\[0 - S(B) = never\]
  </li>
  <li>
\[S(A) - S(B) = A - B\]
  </li>
</ol>

<p>Intuitively,</p>

<ol>
  <li>Zero is also the subtractive(?) identity</li>
  <li>Zero minus a positive number is undefined (natural numbers start at zero!)</li>
  <li>(A + 1) - (B + 1) = A - B</li>
</ol>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span> <span class="nx">Subtract</span><span class="o">&lt;</span><span class="nx">A</span> <span class="kd">extends</span> <span class="nx">Natural</span><span class="p">,</span> <span class="nx">B</span> <span class="kd">extends</span> <span class="nx">Natural</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span>
  <span class="mi">0</span><span class="p">:</span> <span class="nx">A</span><span class="p">;</span>
  <span class="mi">1</span><span class="p">:</span> <span class="nx">A</span> <span class="kd">extends</span> <span class="nx">Zero</span> <span class="p">?</span> <span class="nx">never</span> <span class="p">:</span> <span class="nx">Subtract</span><span class="o">&lt;</span><span class="nx">P</span><span class="o">&lt;</span><span class="nx">A</span><span class="o">&gt;</span><span class="p">,</span> <span class="nx">P</span><span class="o">&lt;</span><span class="nx">B</span><span class="o">&gt;&gt;</span><span class="p">;</span>
<span class="p">}[</span><span class="nx">B</span> <span class="kd">extends</span> <span class="nx">Zero</span> <span class="p">?</span> <span class="mi">0</span> <span class="p">:</span> <span class="mi">1</span><span class="p">];</span>
</code></pre></div></div>

<p>Multiplication is almost as simple as addition:</p>

<ol>
  <li>
\[A \times 0 = 0\]
  </li>
  <li>
\[A \times S(B) = A + (A \times B)\]
  </li>
</ol>

<p>Intuitively,</p>

<ol>
  <li>Anything times zero is zero</li>
  <li>A(1 + B) = A + AB</li>
</ol>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span> <span class="nx">Multiply</span><span class="o">&lt;</span><span class="nx">A</span> <span class="kd">extends</span> <span class="nx">Natural</span><span class="p">,</span> <span class="nx">B</span> <span class="kd">extends</span> <span class="nx">Natural</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span>
  <span class="mi">0</span><span class="p">:</span> <span class="nx">Zero</span><span class="p">;</span>
  <span class="mi">1</span><span class="p">:</span> <span class="nx">Add</span><span class="o">&lt;</span><span class="nx">Multiply</span><span class="o">&lt;</span><span class="nx">A</span><span class="p">,</span> <span class="nx">P</span><span class="o">&lt;</span><span class="nx">B</span><span class="o">&gt;&gt;</span><span class="p">,</span> <span class="nx">A</span><span class="o">&gt;</span><span class="p">;</span>
<span class="p">}[</span><span class="nx">B</span> <span class="kd">extends</span> <span class="nx">Zero</span> <span class="p">?</span> <span class="mi">0</span> <span class="p">:</span> <span class="mi">1</span><span class="p">];</span>
</code></pre></div></div>

<p><em>Following along? <a href="http://www.typescriptlang.org/play/#code/FAFwngDgpgBAcgQxAVwE4IDYwLwwN4wSpQBuAXPEmpjAL6iSwBaUqA9jvoceTAHalWdYA2gwAlgGcW7AIKIU6DHGQBbAEZDcMjlAAeIKHwAmkyopoB+GCFTJYFAGaZJUEeDEBRAI7IXAHlkYfUMTMwVqDAAaGAAhYIMjU3NIgD4cYBgsmCCQpLN4y0zs+LywnJgi7Js7B2Ks5wxXGDJ6mEbXd0YYAGV-ABUE0OSIpXTcAiJSCkH6UVgAeQFOPp1U+Zh+gHcOXD6lqHWPWH6AC2JYPYGdo+6AMTY0FYHzqEONu-ESS97-B7RbmIeuI9M9Pt9AbAeoI+M9gXpITBPOIAOanEBwmGIuDiZZXeHrAD0hJgADpyTAECYYJIOGxYV0xAAFAZDfIpMacSY8GYAbQARFMSPyALoCoWi4QbWTGYyBNnlUaYGKlRKKqiciZtAAMFFkUTaAEYKH0ZXL9TAWbFUjbgLRearhmYdJUYNqWjBDSLGVDkOpbAgAMYgeVlEYa5VxBXhiwYcb4HV6g3VY0VMPO1gcawCb6oD09P0B4P+FmyVIxK22+2O9ku6zuihen0wACyyAwIHEEAwYFDapjkRV0fCEbjXMTMB0yeyqbN-jbHa7PcCFf81vLOXW1eHk8zrobnpFQA">Playground here</a></em></p>

<p>Division is the trickiest of the bunch, but still not too bad.</p>

<p>Since we’re only dealing with the natural numbers, we can assign <code class="language-plaintext highlighter-rouge">never</code> to types that represent division of a number by a non-factor.</p>

<ol>
  <li>
\[A / 0 = never\]
  </li>
  <li>
\[0 / S(B) = 0\]
  </li>
  <li>
\[S(A) / S(B) = S((A - B)/S(B))\]
  </li>
</ol>

<p>Intuitively,</p>

<ol>
  <li>Anything divided by zero is undefined</li>
  <li>Zero divided by a positive number is zero</li>
  <li>Each time we can subtract B from A, we add one to the result; that is: \((A + 1) / (B + 1) = ((A + 1) - (B + 1)) / (B + 1) + 1 = (A - B) / (B + 1) + 1\)</li>
</ol>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span> <span class="nx">Divide</span><span class="o">&lt;</span><span class="nx">A</span> <span class="kd">extends</span> <span class="nx">Natural</span><span class="p">,</span> <span class="nx">B</span> <span class="kd">extends</span> <span class="nx">Natural</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span>
  <span class="mi">0</span><span class="p">:</span> <span class="nx">never</span><span class="p">;</span>
  <span class="mi">1</span><span class="p">:</span> <span class="nx">A</span> <span class="kd">extends</span> <span class="nx">Zero</span> <span class="p">?</span> <span class="nx">Zero</span> <span class="p">:</span> <span class="nx">S</span><span class="o">&lt;</span><span class="nx">Divide</span><span class="o">&lt;</span><span class="nx">Subtract</span><span class="o">&lt;</span><span class="nx">P</span><span class="o">&lt;</span><span class="nx">A</span><span class="o">&gt;</span><span class="p">,</span> <span class="nx">P</span><span class="o">&lt;</span><span class="nx">B</span><span class="o">&gt;&gt;</span><span class="p">,</span> <span class="nx">B</span><span class="o">&gt;&gt;</span><span class="p">;</span>
<span class="p">}[</span><span class="nx">B</span> <span class="kd">extends</span> <span class="nx">Zero</span> <span class="p">?</span> <span class="mi">0</span> <span class="p">:</span> <span class="mi">1</span><span class="p">];</span>
</code></pre></div></div>

<p>And we’ve defined basic arithmetic!</p>

<p>Since we’re really abusing the type system, if we want to write division like this without any warnings, we need to use a Typescript branch like <a href="https://github.com/microsoft/TypeScript/pull/29602">this one</a> that allows for deeper instantiated types, since we’re generating types that quickly exceed the default depth of 50.</p>

<hr />

<p>Finally, it’s time we actually check the Collatz conjecture. We need one more utility, to check if a number is even.</p>

<p>We could do this using our <code class="language-plaintext highlighter-rouge">Divide</code> type but it’s a bit unwieldy. Instead, we can define even numbers using the following recurrence:</p>

<ol>
  <li>
\[Even(0) = true\]
  </li>
  <li>
\[Even(1) = false\]
  </li>
  <li>
\[Even(S(S(N))) = Even(N)\]
  </li>
</ol>

<p>Intuitively,</p>

<ol>
  <li>Zero is even</li>
  <li>One is not even</li>
  <li>If N + 2 is even, then N is even</li>
</ol>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span> <span class="nx">Even</span><span class="o">&lt;</span><span class="nx">T</span> <span class="kd">extends</span> <span class="nx">Natural</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span>
  <span class="mi">0</span><span class="p">:</span> <span class="nx">never</span><span class="p">;</span>
  <span class="mi">1</span><span class="p">:</span> <span class="nx">T</span> <span class="kd">extends</span> <span class="nx">Zero</span> <span class="p">?</span> <span class="kc">true</span> <span class="p">:</span> <span class="nx">T</span> <span class="kd">extends</span> <span class="nx">One</span> <span class="p">?</span> <span class="kc">false</span> <span class="p">:</span> <span class="nx">Even</span><span class="o">&lt;</span><span class="nx">P</span><span class="o">&lt;</span><span class="nx">P</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;&gt;&gt;</span><span class="p">;</span>
<span class="p">}[</span><span class="nx">T</span> <span class="kd">extends</span> <span class="nx">Natural</span> <span class="p">?</span> <span class="mi">1</span> <span class="p">:</span> <span class="mi">0</span><span class="p">];</span>
</code></pre></div></div>

<hr />

<p>Behold!</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span> <span class="nx">Collatz</span><span class="o">&lt;</span><span class="nx">T</span> <span class="kd">extends</span> <span class="nx">Natural</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span>
    <span class="mi">0</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="mi">1</span><span class="p">:</span> <span class="nx">Collatz</span><span class="o">&lt;</span><span class="nx">Even</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span> <span class="kd">extends</span> <span class="kc">true</span> <span class="p">?</span>
        <span class="nx">Divide</span><span class="o">&lt;</span><span class="nx">T</span><span class="p">,</span> <span class="nx">Two</span><span class="o">&gt;</span> <span class="p">:</span> <span class="c1">// If even, divide by two</span>
        <span class="nx">S</span><span class="o">&lt;</span><span class="nx">Multiply</span><span class="o">&lt;</span><span class="nx">T</span><span class="p">,</span> <span class="nx">Three</span><span class="o">&gt;&gt;</span> <span class="c1">// Otherwise, multiply by 3 and add 1</span>
<span class="p">}[</span><span class="nx">Equals</span><span class="o">&lt;</span><span class="nx">T</span><span class="p">,</span> <span class="nx">One</span><span class="o">&gt;</span> <span class="kd">extends</span> <span class="kc">true</span> <span class="p">?</span> <span class="mi">0</span> <span class="p">:</span> <span class="mi">1</span><span class="p">]</span> <span class="c1">// One? True, otherwise Collatz&lt;T&gt;</span>
</code></pre></div></div>

<p>To be fair, we can only check Collatz chains that are very short. Without configuring Typescript’s maxInstantiationDepth, we get lots of <code class="language-plaintext highlighter-rouge">Type instantiation is excessively deep and possibly infinite</code> errors when we check massive numbers like three.</p>

<p>But we don’t bother ourselves with silly details like reality; after all, if we had infinite memory we’d have a perfectly good Turing machine. We could optimize our types to be less greedy about using levels we don’t need, but that’s not really the point here.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span> <span class="nx">c1</span> <span class="o">=</span> <span class="nx">Collatz</span><span class="o">&lt;</span><span class="nx">One</span><span class="o">&gt;</span><span class="p">;</span> <span class="c1">// true (1)</span>
<span class="kd">type</span> <span class="nx">c2</span> <span class="o">=</span> <span class="nx">Collatz</span><span class="o">&lt;</span><span class="nx">Two</span><span class="o">&gt;</span><span class="p">;</span> <span class="c1">// true (2 -&gt; 1)</span>
<span class="kd">type</span> <span class="nx">c3</span> <span class="o">=</span> <span class="nx">Collatz</span><span class="o">&lt;</span><span class="nx">Three</span><span class="o">&gt;</span><span class="p">;</span> <span class="c1">// Error: Type instantiation is excessively deep and possibly infinite. (True if we configure the depth to be larger; (3 -&gt; 10 -&gt; 5 -&gt; 16 -&gt; 8 -&gt; 4 -&gt; 2 -&gt; 1))</span>
<span class="kd">type</span> <span class="nx">c4</span> <span class="o">=</span> <span class="nx">Collatz</span><span class="o">&lt;</span><span class="nx">Four</span><span class="o">&gt;</span><span class="p">;</span> <span class="c1">// true (4 -&gt; 2 -&gt; 1)</span>
<span class="kd">type</span> <span class="nx">c8</span> <span class="o">=</span> <span class="nx">Collatz</span><span class="o">&lt;</span><span class="nx">Eight</span><span class="o">&gt;</span><span class="p">;</span> <span class="c1">// true (8 -&gt; 4 -&gt; 2 -&gt; 1)</span>
</code></pre></div></div>

<p><em><a href="http://www.typescriptlang.org/play/#code/FAFwngDgpgBAcgQxAVwE4IDYwLwwN4wSpQBuAXPEmpjAL6iSwBaUqA9jvoceTAHalWdYA2gwAlgGcW7AIKIU6DHGQBbAEZDcMjlAAeIKHwAmkyopoB+GCFTJYFAGaZJUEeDEBRAI7IXAHlkYfUMTMwVqDAAaGAAhYIMjU3NIgD4cYBgsmCCQpLN4y0zs+LywnJgi7Js7B2Ks5wxXGDJ6mEbXd0YYAGV-ABUE0OSIpXTcAiJSCkH6UVgAeQFOPp1U+Zh+gHcOXD6lqHWPWH6AC2JYPYGdo+6AMTY0FYHzqEONu-ESS97-B7RbmIeuI9M9Pt9AbAeoI+M9gXpITBPOIAOanEBwmGIuDiZZXeHrAD0hJgADpyTAECYYJIOGxYV0xAAFAZDfIpMacSY8GYAbQARFMSPyALoCoWi4QbWTGYyBNnlUaYGKlRKKqiciZtAAMFFkUTaAEYKH0ZXL9TAWbFUjbgLRearhmYdJUYNqWjBDSLGVDkOpbAgAMYgeVlEYa5VxBXhiwYcb4HV6g3VY0VMPO1gcawCb6oD09P0B4P+FmyVIxK22+2O9ku6zuihen0wACyyAwIHEEAwYFDapjkRV0fCEbjXMTMB0yeyqbN-jbHa7PcCFf81vLOXW1eHk8zrobnu9GwAIl9xMYoH2nRzIzX1bH43gJznWNOsrOd3Xd+x8-5TyRz0vAt-XQYtSw3SsN3XO0HU-Pd6w9JsNk8b4+FZdMbzHLVql1fhBFQN9PRmOCf2sWx7A9QYMIOV0OgcJFUJLJj+htLdeSo-sR1jV1DQ9bUj2OGAAGE2AwDAkAAL3QzjMMfCdyKgQjUxEsTJP8FCjAGdIMIUyo2mqf9AIGGJtjYdIKGJGAAElHGCVCYmMM8LxgdQwBsHZ9OyPoF07bte36EzXkOdJLIWEBTlYLYpEUmBVHbXyexctyAGZKWpBBZU9GCfD8JpjJgA5tJk3SEMbEUYFCgRrH6WoYjYcLIui4TRPEkApJY5tA143AVNaqTCoqkldIACkNABKDZAwAJk4Xq1NMkKhtqGBhpmgBadJxsm1Kepa+agsWpFUHYVAZm6XFJBAKlOyQcR6QkMx9EDKBJEkL4oESi8oAgNLjEINhXvEdREtxRxcXEQxSRWmqKPEWytlgQN6TBlE0FgBqYAvCBwpsDhNBgcTUBRVgAG4VtSjbPXdSmAFYYEpw0ADZ6fSAAOFmYAAFg59bNrGibBMDbndtUtq-keVBDpG7nKd5z0Be6QN2ZFvr1NRdEpeW4b2cpmX0jl8agA">Here’s a typescript playground</a> with all of the code in this post. You can use the primitives here to do other cool stuff – how about a primality checker?</em></p>

<hr />

<p>Armed with tools to do arbitrary computation within the Typescript type system – go forth and make your teammates dread reviewing your code! Godspeed.</p>

<p>See <a href="https://github.com/ostrowr/ts-json-validator">my library, ts-json-validator</a> if you want to use the flexibility of the type system for actual good, or <a href="https://github.com/Microsoft/TypeScript/issues/14833">this Github issue</a> for some more fun around the Turing-completeness of the type system.</p>]]></content><author><name>Robbie Ostrow</name><email>robbie@ostro.ws</email></author><summary type="html"><![CDATA[Herein we check the Collatz conjecture using only Typescript’s type system.]]></summary></entry><entry><title type="html">TINE-CNN Augmentation</title><link href="/post-tine-cnn" rel="alternate" type="text/html" title="TINE-CNN Augmentation" /><published>2018-08-01T00:00:00+00:00</published><updated>2018-08-01T00:00:00+00:00</updated><id>/tine-cnn</id><content type="html" xml:base="/post-tine-cnn"><![CDATA[<p>TINE-CNN Augmentation (pronounced Tiny-CNN Augmentation) is a new way to perform automatic data augmentation for any image classification task.</p>

<p>Read the <a href="/assets/PDFs/tine-cnn.pdf">paper</a>!</p>

<p>Or just check out the poster:
<a href="/assets/PDFs/tine-cnn-poster.pdf"><img src="/assets/images/tine-cnn/tine-cnn-poster.jpg" /></a></p>

<h1 id="why">Why?</h1>

<p>Neural networks need a great deal of data on which to train. On many tasks, this problem is partially mitigated by data augmentation and transfer learning. However, it’s not always clear what type of data augmentation is reasonable for a specialized dataset – for example, adding rotation into MNIST will result in confusing 6 and 9. In addition, deciding how to effectively augment a training dataset adds yet more hyperparameters to an already large model-design decision space. Augmentation is typically an ad-hoc process, and although there is recent work attempting to integrate it more naturally into the training process, there is no generic framework that researchers tend to use. Even so, data augmentation has long been in the toolbox of any neural-net designer and frameworks such as Keras often provide easy augmentation APIs. Unfortunately, these frameworks run into all of the above problems. Modern regularization techniques such as dropout can also help train CNNs on comparatively small datasets, but nothing replaces simply having more data with which to train.</p>]]></content><author><name>Robbie Ostrow</name><email>robbie@ostro.ws</email></author><summary type="html"><![CDATA[TINE-CNN Augmentation (pronounced Tiny-CNN Augmentation) is a new way to perform automatic data augmentation for any image classification task.]]></summary></entry><entry><title type="html">Do Basketball Referees Favor the Losing Team?</title><link href="/post-fouls" rel="alternate" type="text/html" title="Do Basketball Referees Favor the Losing Team?" /><published>2014-12-11T00:00:00+00:00</published><updated>2014-12-11T00:00:00+00:00</updated><id>/fouls</id><content type="html" xml:base="/post-fouls"><![CDATA[<p>Basketball fans love a close game. A nail-biter with fifteen lead changes is much more fun to watch than a blowout.
But do the refs like close games as much as the rest of us?</p>

<p><em>Motivation to read on</em>: <strong>they do</strong>.</p>

<div id="point_differential"> </div>

<p>The play-by-play data used in this analysis is curated by <a href="http://basketballvalue.com/index.php">BasketballValue.com</a>. You can find their files <a href="http://basketballvalue.com/downloads.php">on this page</a>. Alternatively, you can download the data in the final form that I used – regular-season data from 2006-2012 <a href="https://s3.amazonaws.com/viscous/regular_season_data.csv.zip">here</a>, or the playoff data from 2007-2011 <a href="https://s3.amazonaws.com/viscous/playoff_data.csv.zip">here</a>.</p>

<h3 id="notes-about-the-upcoming-analysis">Notes about the upcoming analysis</h3>

<ul>
  <li>It uses the <a href="https://s3.amazonaws.com/viscous/regular_season_data.csv.zip">7111 game regular season dataset</a>.</li>
  <li>All fouls with fewer than 3:00 minutes remaining were ignored, to minimize the impact of intentional fouls.</li>
  <li>Only regular fouls were considered; technical fouls were ignored.</li>
</ul>

<h2 id="home-vs-away">Home vs. Away</h2>

<p>The first obvious task when collecting foul data is to examine the difference in the number of fouls recorded by the home and away teams. Here’s a <a href="http://en.wikipedia.org/wiki/Histogram">histogram</a> comparing them. Data above 0 means that there were more fouls called on the home team:</p>

<div id="home_minus_away_raw"> </div>

<p>It looks pretty <a href="http://en.wikipedia.org/wiki/Normal_distribution">normal</a>, with a mean just below 0 \((-.837)\). Sure, this is interesting, but it doesn’t reveal too much because differences in games with many fouls are weighted the same as differences in games with very few. Imagine a game in which the home team recorded 5 fouls and the visiting team 10. In this graph, it looks the same as a game in which the home team recorded 35 and the visiting team 40, though these games were fundamentally different. In the first, the away team had twice as many fouls; in the second, only 14% more. Thus, it’s more useful to look at a normalized measure.</p>

<p>This measure is expected “percentage difference” between the two foul categories, defined as 100 times the difference divided by the average, or \(200\times\frac{a-b}{a+b}\). For example, the percentage difference between 40 and 35 is \(13.33\%\).</p>

<p>Here is the same data graphed to reflect percentage difference rather than absolute difference:</p>

<div id="percentage_difference"> </div>

<p>Now that we have a normalized measure, we can find out whether the variation from zero is statistically significant. If we assume that the regular season data is a representative sample from the total pool of all NBA games, (it’s not, but such is life) we can determine a confidence interval for the <em>real</em> mean.</p>

<p>The mean percentage difference between the number of fouls recorded by the home team and the away team is \(-3.93\%\). A \(95\%\) confidence interval yields bounds between \(-4.57\%\) and \(-3.30\%\). The home team gets called for fewer fouls. This result is consistent with prior research; see <a href="http://www.sloansportsconference.com/?p=663">this talk</a>, presented at the Sloan conference at MIT, <a href="http://home.kelley.iupui.edu/kyjander/Officiating%20paper%20-%20Final%20draft%20version.pdf">this paper</a> examining NCAA basketball and coming to the same result, or <a href="http://papers.ssrn.com/sol3/papers.cfm?abstract_id=1377964">this paper</a> from the <em>Journal of Economics and Management Strategy</em>. The last paper also shows that the home team’s advantage rises as the number of fans increases, which suggests what Paul Aufiero (Patton Oswalt) was definitely thinking in <em>Big Fan</em>: refs favor the home team (at least in part) because of the crowd.</p>

<h2 id="winning-vs-losing">Winning vs. Losing</h2>

<p>If refs want games to be close, we might expect that they (consciously or unconsciously) call more fouls on the team that’s currently in the lead.</p>

<p>Still ignoring the last three minutes of the game, this graph shows the percentage difference between fouls called on the team that is currently in the lead and the team that is currently losing. A number above 0 means that the leading team is more likely to get called for a foul.</p>

<div id="percentage_difference_win_loss"> </div>

<p>This seems a little more biased.</p>

<p>Here, the mean percentage difference is \(6.91\%\), with a \(95\%\) confidence interval from \(6.20\%\) to \(7.63\%\)</p>

<p>Interestingly, despite the fact that the home team is less likely to foul and that the home team won almost \(60\%\) of their games during these seasons, the winning team at any given moment is more likely to record a foul. In fact, the percentage difference is almost \(7\%\). This is striking; make of it what you will.</p>

<h2 id="winning-by-a-lot-vs-losing-by-a-lot">Winning (by a lot) vs. Losing (by a lot)</h2>

<p>If we make the same analysis, but only consider fouls that occurred when the point differential was 10 or more, we get the following graph (again, above 0 means the winning team fouled more):</p>

<div id="percentage_difference_win_loss_big"> </div>

<p>There are peaks at \(\pm 200\%\) because many games only have one or two fouls when one team is winning by at least 10.</p>

<p>Now, the mean percentage difference is \(22.30\%\). Twenty-two percent!</p>

<p>This suggests one of two things:</p>

<ol>
  <li>The leading team is more likely to foul.</li>
  <li>Referees are more likely to call fouls on the leading team.</li>
</ol>

<p>This data can’t tell us <em>why</em> the winning team tends to foul more, but it seems unlikely that the winning team is actually more likely to commit a foul. Why would a team foul when they were 20 up – ever? It stops the clock, makes for easy points, (during bonus and for shooting fouls) etc. In fact, to me, it looks as though referees are very sympathetic to the losing team.</p>

<h2 id="how-sympathetic">How Sympathetic?</h2>

<p>If we vary the point differential that we examine, it’s clear that the greater the point differential, the more likely that a foul will be called on the leading team.</p>

<div id="percentage_differential"> </div>

<p>This graph is insane. It’s practically (within the error bars) increasing linearly. The greater the lead, the more likely that a foul will be called.</p>

<p>The same analysis on the playoff data yields the same trend, possibly even amplified. (We have less data, so the error is more severe.)</p>

<div id="percentage_differential_playoffs"> </div>

<h2 id="conspiracy">Conspiracy?</h2>

<p>There’s a simple economic motivation to keep games close and exciting, but this data doesn’t offer any insight into whether the referees actually have that motivation in mind. <a href="http://triceratops.brynmawr.edu/dspace/bitstream/handle/10066/8134/2012HanemanP_thesis.pdf?sequence=9">Other studies</a> suggest that teams facing elimination in the playoffs tend to be given an advantage by the refs, forcing each series to seven games as often as possible (and thus increasing revenues). However, I’m not comfortable saying that the NBA is encouraging this behavior by its referees; I imagine word would have gotten out by now. It’s probably just human nature. Nature that the NBA needs to fix!</p>

<p>There’s a lot more information to be teased out of this data. I’d love to hear any more results or insights.</p>]]></content><author><name>Robbie Ostrow</name><email>robbie@ostro.ws</email></author><summary type="html"><![CDATA[Basketball fans love a close game. A nail-biter with fifteen lead changes is much more fun to watch than a blowout. But do the refs like close games as much as the rest of us?]]></summary></entry><entry><title type="html">Demystifying Ghost</title><link href="/post-demystifying-ghost" rel="alternate" type="text/html" title="Demystifying Ghost" /><published>2014-11-18T00:00:00+00:00</published><updated>2014-11-18T00:00:00+00:00</updated><id>/ghost</id><content type="html" xml:base="/post-demystifying-ghost"><![CDATA[<p>My mother loves words. To this day, she insists that I should become a poet. During a road trip when I was little, she introduced me to a game called <a href="https://en.wikipedia.org/wiki/Ghost_(game)">Ghost</a>, and we would play all the time.</p>

<p>To play Ghost, players take turns choosing letters, creating a growing fragment of a word. The goal is to avoid completing a word while still maintaining the fragment’s property that it begins some word. There are two ways to lose a round: complete a word or fail to produce a word that begins with your fragment when challenged by an opponent. Of course, if the challenge is unsuccessful and you come up with a legitimate word, the challenger loses the round. Only words with more than three letters count. Generally, each player begins with five lives, one for each letter of <code class="language-plaintext highlighter-rouge">G-H-O-S-T</code>.</p>

<p>For example, imagine that Ada, Babbage, and Church are playing a friendly game of Ghost. Ada goes first and plays <strong>e</strong>. Babbage cleverly follows Ada’s <strong>e</strong> with an <strong>n</strong> of his own. Play continues: Church plays <strong>g</strong>, Ada plays <strong>i</strong>, and Babbage plays <strong>n</strong>. “Shucks,” Church says. I guess I’ll play <strong>e</strong>. Ada points and laughs and assigns Church a <strong><code class="language-plaintext highlighter-rouge">G</code></strong> for spelling “engine.” Simple enough.</p>

<p>Whenever I played with just my mom, she would start with the letter <strong>z</strong>. With only two players, it seemed as though there was no way to counter – she always won when she started with <strong>z</strong>. But luckily, instead of training to become a poet, I learned a little Python; now I’m the dominant Ghost player in the household.</p>

<p><em>Attention: If you plan to play Ghost with your friends, you should probably stop reading now so you don’t have a tiresome advantage.</em></p>

<p>When I initially solved Ghost to defeat my mother, I only looked into the two-player case. It turns out that <a href="https://xkcd.com">Randall Munroe</a> has, in his infinite wisdom, already <a href="https://blog.xkcd.com/2007/12/31/ghost/">solved Ghost</a> for two players. In an effort not to be entirely redundant in this post, I’m sharing a solution here for <code class="language-plaintext highlighter-rouge">n</code> players. When Randall and I end up playing with a mysterious third party, he won’t know what hit him.</p>

<p>Enough talk. Let’s solve Ghost.</p>

<h2 id="pruning-a-dictionary">Pruning a Dictionary</h2>

<p>First, we need a dictionary. The most reasonable one I could find is the <code class="language-plaintext highlighter-rouge">american-english</code> dictionary that ships with some versions of Ubuntu. (Find it at: https://packages.debian.org/wheezy/wamerican). I’m pretty sure this is the same dictionary Randall used.</p>

<p>You can use any dictionary you want, of course. I’m on OSX right now, so I also have Webster’s Second International built in: <code class="language-plaintext highlighter-rouge">/usr/share/dict/web2</code>. It has 234,936 words, most of which I don’t know. Some cursory googling also revealed TWL06, which seems to be a version of the American Scrabble dictionary.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># DICTIONARY = "/usr/share/dict/web2"
</span>
<span class="c1"># DICTIONARY = "/usr/share/dict/TWL06" # scrabble dictionary
</span>
<span class="n">DICTIONARY</span> <span class="o">=</span> <span class="s">"/usr/share/dict/american-english"</span> <span class="c1"># abridged ubuntu dictionary
</span></code></pre></div></div>

<p>Then, we prune this dictionary to contain only words that are lowercase (Proper Nouns Aren’t Allowed) and more than three letters long:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">is_legal_word</span><span class="p">(</span><span class="n">word</span><span class="p">,</span> <span class="n">min_length</span><span class="p">):</span>
    <span class="s">"""
    returns True if the string `word` is longer than min_length
    and consists entirely of lowercase letters.
    """</span>
    <span class="k">return</span> <span class="nb">len</span><span class="p">(</span><span class="n">word</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">min_length</span> <span class="ow">and</span> <span class="n">word</span><span class="p">.</span><span class="n">islower</span><span class="p">()</span>

<span class="k">def</span> <span class="nf">gen_word_list</span><span class="p">(</span><span class="n">dictionary</span><span class="p">,</span> <span class="n">min_length</span><span class="p">):</span>
    <span class="s">"""
    returns a list of legal words parsed from a file where
    each word is lowercase and seperated by a newline
    character.
    """</span>
    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">dictionary</span><span class="p">,</span> <span class="s">'r'</span><span class="p">)</span> <span class="k">as</span> <span class="n">w</span><span class="p">:</span>
    <span class="n">words</span> <span class="o">=</span> <span class="p">[</span><span class="n">line</span><span class="p">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="n">w</span> <span class="k">if</span> <span class="n">is_legal_word</span><span class="p">(</span><span class="n">line</span><span class="p">.</span><span class="n">strip</span><span class="p">(),</span> <span class="n">min_length</span><span class="p">)]</span>
    <span class="k">return</span> <span class="n">words</span>

<span class="n">words</span> <span class="o">=</span> <span class="n">gen_word_list</span><span class="p">(</span><span class="n">DICTIONARY</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span>
<span class="nb">len</span><span class="p">(</span><span class="n">words</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt;&gt;&gt; 81995
</code></pre></div></div>

<p>Lists in Python are great, but they’re not the optimal structure for storing words (and – more importantly – prefixes of words) in this case. Enter the <a href="https://en.wikipedia.org/wiki/Trie">trie</a> (from re<em>trie</em>val). A trie is an ordered tree in which every descendant of a node shares the same prefix. A picture would probably be useful; in the diagram below, each complete word is labeled with an arbitrary numeric key. From Wikimedia commons:</p>

<p><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/b/be/Trie_example.svg/250px-Trie_example.svg.png" alt="Example trie from Wikipedia commons" /></p>

<p>Tries are pretty memory efficient too – as you can see in the figure above, we’re storing 8 words using 11 nodes. Finding if a specific word is in a trie is trivially \(O(l)\) where \(l\) is the length of the word, because all you need to do is spell the word to find it. In an unsorted list, such an operation is \(O(n)\), and it’s \(O(log(n))\) using binary search on a sorted list, where \(n\) is the size of the corpus. Finding common prefixes and similar operations is equivalently easy. We won’t implement a trie or get into the nitty-gritty details – that might be a topic for another day. Luckily, there are already a few implementations of Python tries available. This solution uses <a href="https://github.com/kmike/datrie/">datrie</a>.</p>

<p>The first thing to do is make the trie from the corpus we just loaded into memory:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">datrie</span>
<span class="kn">import</span> <span class="nn">string</span>

<span class="k">def</span> <span class="nf">make_trie_from_word_list</span><span class="p">(</span><span class="n">word_list</span><span class="p">):</span>
    <span class="s">"""
    given a list of lowercase words, constructs a trie
    with `None` as each value.
    """</span>
    <span class="n">trie</span> <span class="o">=</span> <span class="n">datrie</span><span class="p">.</span><span class="n">Trie</span><span class="p">(</span><span class="n">string</span><span class="p">.</span><span class="n">ascii_lowercase</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">word</span> <span class="ow">in</span> <span class="n">word_list</span><span class="p">:</span>
    <span class="n">trie</span><span class="p">[</span><span class="n">word</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="k">return</span> <span class="n">trie</span>

<span class="n">orig_trie</span> <span class="o">=</span> <span class="n">make_trie_from_word_list</span><span class="p">(</span><span class="n">words</span><span class="p">)</span>
</code></pre></div></div>

<p>Notice that we’re never going to use a word that has a prefix that is another word. We’ll never get to “rainbow” because somebody will have already lost on “rain.” Let’s prune the trie to contain only words that don’t have legal words as prefixes:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c1"># We'll actually create a whole new trie, because deleting from
</span>
<span class="c1"># a trie is unpleasant.
</span>
<span class="k">def</span> <span class="nf">prune_trie</span><span class="p">(</span><span class="n">trie</span><span class="p">):</span>
    <span class="s">"""
    Given a lowercase datrie.Trie, returns a new trie in which no word
    has any prefixes that are also legal words.
    """</span>
    <span class="n">prefixless</span> <span class="o">=</span> <span class="p">[</span><span class="n">word</span> <span class="k">for</span> <span class="n">word</span> <span class="ow">in</span> <span class="n">trie</span><span class="p">.</span><span class="n">keys</span><span class="p">()</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">trie</span><span class="p">.</span><span class="n">prefixes</span><span class="p">(</span><span class="n">word</span><span class="p">))</span> <span class="o">==</span> <span class="mi">1</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">make_trie_from_word_list</span><span class="p">(</span><span class="n">prefixless</span><span class="p">)</span>

<span class="n">trie</span> <span class="o">=</span> <span class="n">prune_trie</span><span class="p">(</span><span class="n">orig_trie</span><span class="p">)</span>
<span class="nb">len</span><span class="p">(</span><span class="n">trie</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt;&gt;&gt; 19631
</code></pre></div></div>

<p>Now we’re in prime Ghost-solving territory.</p>

<h1 id="solving-ghost">Solving Ghost</h1>

<p>To solve Ghost means to find out which players can lose from every possible game state, assuming all players play perfectly.</p>

<p>We’ll store this solution in another trie, where the keys are every substring in the original trie and the values are sets with the possible losers from that node.</p>

<h2 id="the-algorithm">The Algorithm</h2>

<p>We could solve this with some clever recursion, but it’s easier to take advantage of the structure of the trie. To determine the losers in every possible game state, we perform a level-order traversal of the trie and follow these rules at every node:</p>

<ul>
  <li>If the node is a leaf node, then it completes a word. In that case, the loser is <code class="language-plaintext highlighter-rouge">get_turn(word_length, num_players)</code> where word_length is equal to the depth of the trie at the leaf node.</li>
  <li>If the node is not a leaf node:
    <ul>
      <li>If all of the node’s children include the current player in the set of losers, then the player is indifferent between options because any move is a losing move. In that case, the set of losers at the current node is the union over all of the children’s losers.</li>
      <li>If there is at least one child whose set of losers does not include the current player, then the current player does not lose and is indifferent between all non-losing options. In that case, the set of losers at the current node is the union over all of the sets of children’s losers that do not include the current player.</li>
    </ul>
  </li>
</ul>

<p>For an example, consider a trie with the words “aa”, “ab”, “baa”, “bb”, “bcaa”, “bcab”, “caaaa”, and “caab”:</p>

<p><img src="/assets/images/ghost/trie1.png" width="400" /></p>

<p>If we follow the algorithm above, then it’s easy to derive the losers at each node:</p>

<p><img src="/assets/images/ghost/trie2.png" width="400" /></p>

<p>As you can see, player 2 loses. Player 1 will play either ‘a’ or ‘c,’ forcing player 2 into a losing situation. Even though player 1 is not guaranteed to lose with a move of ‘b,’ it counts as a losing node because the other players can coordinate to make player 1 lose.</p>

<p>Finally, it’s time for the actual implementation. Instead of doing a formal level-order traversal, we’re just examining substrings from longest to shortest (ordered arbitrarily within those equivalence classes). This works because once we have solved for all strings of length \(n\), we can solve for all strings of length \(n-1\).</p>

<p>Before we get ahead of ourselves, we need an easy way to find out whose turn it is at any node in the trie:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_turn</span><span class="p">(</span><span class="n">word_length</span><span class="p">,</span> <span class="n">num_players</span><span class="p">):</span>
    <span class="s">"""
    Returns the id of the player who would finish a word
    word_length letters long. Player ids are 1-indexed in
    {1... num_players}.
    """</span>
    <span class="k">return</span> <span class="p">(</span><span class="n">word_length</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">%</span> <span class="n">num_players</span> <span class="o">+</span> <span class="mi">1</span>

<span class="k">print</span><span class="p">(</span><span class="n">get_turn</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="mi">3</span><span class="p">))</span> <span class="c1"># 1231231**2**
</span><span class="k">print</span><span class="p">(</span><span class="n">get_turn</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="mi">4</span><span class="p">))</span> <span class="c1"># 123**4**
</span><span class="k">print</span><span class="p">(</span><span class="n">get_turn</span><span class="p">(</span><span class="mi">9</span><span class="p">,</span> <span class="mi">2</span><span class="p">))</span> <span class="c1"># 12121212**1**
</span></code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt;&gt;&gt; 2
&gt;&gt;&gt; 4
&gt;&gt;&gt; 1
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c1"># This function replaces a true level order traveral.
</span>
<span class="c1"># We need it to specify the keys in our solution trie.
</span>
<span class="k">def</span> <span class="nf">get_all_substrings</span><span class="p">(</span><span class="n">trie</span><span class="p">):</span>
    <span class="s">"""
    Parameters:
    trie: a datrie.Trie

    Returns: a list of all possible substrings of
      words in the trie, sorted by length from
      longest to shortest.
    """</span>
    <span class="n">substrings</span> <span class="o">=</span> <span class="nb">set</span><span class="p">([</span><span class="s">''</span><span class="p">])</span>
    <span class="k">for</span> <span class="n">word</span> <span class="ow">in</span> <span class="n">trie</span><span class="p">.</span><span class="n">keys</span><span class="p">():</span>
        <span class="n">substrings</span><span class="p">.</span><span class="n">update</span><span class="p">([</span><span class="n">word</span><span class="p">[:</span><span class="n">p</span><span class="p">]</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">word</span><span class="p">))])</span>
    <span class="n">substrings</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">substrings</span><span class="p">)</span>
    <span class="n">substrings</span><span class="p">.</span><span class="n">sort</span><span class="p">(</span><span class="n">key</span><span class="o">=</span><span class="nb">len</span><span class="p">,</span> <span class="n">reverse</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">substrings</span>

<span class="c1"># This is the "meat" of the algorithm that's discussed in the bullets above.
</span>
<span class="k">def</span> <span class="nf">get_losers</span><span class="p">(</span><span class="n">solution</span><span class="p">,</span> <span class="n">substring</span><span class="p">,</span> <span class="n">turn</span><span class="p">):</span>
    <span class="s">"""
    Parameters:
    solution: a datrie.Trie containing the working solution
    (must be solved for all substrings longer than substring)
    substring: the current position in the trie
    turn: the current player

    Returns: the set of losers from the node reached by spelling `substring`
    """</span>
    <span class="c1"># Lists the losers of all of the immediate children
</span>    <span class="n">next_losers</span> <span class="o">=</span> <span class="p">[</span><span class="n">b</span> <span class="k">for</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="ow">in</span> <span class="n">solution</span><span class="p">.</span><span class="n">items</span><span class="p">(</span><span class="n">substring</span><span class="p">)</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">a</span><span class="p">)</span> <span class="o">==</span> <span class="nb">len</span><span class="p">(</span><span class="n">substring</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">]</span>
    <span class="n">curr_player_loss</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
    <span class="n">other_player_loss</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
    <span class="n">curr_player_loses</span> <span class="o">=</span> <span class="bp">True</span>
    <span class="k">for</span> <span class="n">loser</span> <span class="ow">in</span> <span class="n">next_losers</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">curr_player_loses</span> <span class="ow">and</span> <span class="n">turn</span> <span class="ow">in</span> <span class="n">loser</span><span class="p">:</span>
            <span class="c1"># So far, the current player loses no matter what,
</span>            <span class="c1"># so the player is indifferent between all losing
</span>            <span class="c1"># situations.
</span>            <span class="n">curr_player_loss</span> <span class="o">|=</span> <span class="n">loser</span>
        <span class="k">elif</span> <span class="n">turn</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">loser</span><span class="p">:</span>
            <span class="c1"># indifferent between all winning situations.
</span>            <span class="n">other_player_loss</span> <span class="o">|=</span> <span class="n">loser</span>
            <span class="n">curr_player_loses</span> <span class="o">=</span> <span class="bp">False</span>
    <span class="k">if</span> <span class="n">curr_player_loses</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">curr_player_loss</span>
    <span class="k">return</span> <span class="n">other_player_loss</span>

<span class="k">def</span> <span class="nf">solve</span><span class="p">(</span><span class="n">trie</span><span class="p">,</span> <span class="n">num_players</span><span class="p">):</span>
    <span class="s">"""
    Parameters:
    trie: a datrie.Trie, pruned such that there are no
    words that have prefixes that are also words
    num_players:
    the number of players for which to solve

    Returns: a 2-tuple (solution, num_players) where `solution`
      is a datrie.Trie where every node stores the losing set in its
      value. num_players is just along for the ride.
    """</span>
    <span class="n">solution_trie</span> <span class="o">=</span> <span class="n">datrie</span><span class="p">.</span><span class="n">Trie</span><span class="p">(</span><span class="n">string</span><span class="p">.</span><span class="n">ascii_lowercase</span><span class="p">)</span>
    <span class="n">substrings</span> <span class="o">=</span> <span class="n">get_all_substrings</span><span class="p">(</span><span class="n">trie</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">word</span> <span class="ow">in</span> <span class="n">trie</span><span class="p">.</span><span class="n">keys</span><span class="p">():</span>
        <span class="c1"># base case: complete words
</span>        <span class="n">loser</span> <span class="o">=</span> <span class="n">get_turn</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">word</span><span class="p">),</span> <span class="n">num_players</span><span class="p">)</span>
        <span class="n">solution_trie</span><span class="p">[</span><span class="n">word</span><span class="p">]</span> <span class="o">=</span> <span class="nb">set</span><span class="p">([</span><span class="n">loser</span><span class="p">])</span>
    <span class="k">for</span> <span class="n">substring</span> <span class="ow">in</span> <span class="n">substrings</span><span class="p">:</span>
        <span class="c1"># once we have solved for every leaf node, we
</span>        <span class="c1"># can work our way up the trie.
</span>        <span class="n">turn</span> <span class="o">=</span> <span class="n">get_turn</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">substring</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">num_players</span><span class="p">)</span>
        <span class="n">solution_trie</span><span class="p">[</span><span class="n">substring</span><span class="p">]</span> <span class="o">=</span> <span class="n">get_losers</span><span class="p">(</span><span class="n">solution_trie</span><span class="p">,</span> <span class="n">substring</span><span class="p">,</span> <span class="n">turn</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">solution_trie</span><span class="p">,</span> <span class="n">num_players</span>

</code></pre></div></div>

<p>Let’s find out who loses from each initial position in a 2-player game.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">solution_trie</span><span class="p">,</span> <span class="n">num_players</span> <span class="o">=</span> <span class="n">solve</span><span class="p">(</span><span class="n">trie</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\'\'</span><span class="s">"</span><span class="p">,</span> <span class="n">solution_trie</span><span class="p">[</span><span class="s">''</span><span class="p">])</span>
<span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">string</span><span class="p">.</span><span class="n">ascii_lowercase</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="n">c</span><span class="p">,</span> <span class="n">solution_trie</span><span class="p">[</span><span class="n">c</span><span class="p">])</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>'' {2}
a {1}
b {1}
c {1}
d {1}
e {1}
f {1}
g {1}
h {2}
i {1}
j {2}
k {1}
l {1}
m {2}
n {1}
o {1}
p {1}
q {1}
r {2}
s {1}
t {1}
u {1}
v {1}
w {1}
x {1}
y {1}
z {2}
</code></pre></div></div>

<p>There, Ghost is solved! <code class="language-plaintext highlighter-rouge">solution[prefix]</code> gives a set of losers from that prefix. Any self-respecting game theorist would stop now, but I’m neither of those things. It’s no fun to only know who loses with optimal play – it’s a lot better to actually know how to play optimally in any non-doomed situation. This function takes the solution we just computed, along with the current state of the game (some prefix to a word) and returns the winning moves for the current player.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">list_winning_moves</span><span class="p">(</span><span class="n">solution</span><span class="p">,</span> <span class="n">current_state</span><span class="p">):</span>
    <span class="s">"""
    Parameters:
    solution: the 2-tuple returned from solve
    current_state: the incomplete word that has been spelled
    so far

    Returns: a set of winning moves for the current player. The
      empty set if there are no winning moves.
    """</span>
    <span class="n">player</span> <span class="o">=</span> <span class="n">get_turn</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">current_state</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">solution</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
    <span class="n">winning_moves</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
    <span class="k">if</span> <span class="n">player</span> <span class="ow">in</span> <span class="n">solution</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="n">current_state</span><span class="p">]:</span>
        <span class="c1"># player loses, return empty set
</span>        <span class="k">return</span> <span class="n">winning_moves</span>
    <span class="n">paths</span> <span class="o">=</span> <span class="p">[(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span> <span class="k">for</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="ow">in</span> <span class="n">solution</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">items</span><span class="p">(</span><span class="n">current_state</span><span class="p">)</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">a</span><span class="p">)</span> <span class="o">==</span> <span class="nb">len</span><span class="p">(</span><span class="n">current_state</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">]</span>
    <span class="k">for</span> <span class="n">word</span><span class="p">,</span> <span class="n">losers</span> <span class="ow">in</span> <span class="n">paths</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">player</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">losers</span><span class="p">:</span>
            <span class="n">winning_moves</span><span class="p">.</span><span class="n">update</span><span class="p">(</span><span class="n">word</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span>
    <span class="k">return</span> <span class="n">winning_moves</span>

<span class="n">solution</span> <span class="o">=</span> <span class="p">(</span><span class="n">solution_trie</span><span class="p">,</span> <span class="n">num_players</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">list_winning_moves</span><span class="p">(</span><span class="n">solution</span><span class="p">,</span> <span class="s">''</span><span class="p">))</span>
<span class="k">print</span><span class="p">(</span><span class="n">list_winning_moves</span><span class="p">(</span><span class="n">solution</span><span class="p">,</span> <span class="s">'h'</span><span class="p">))</span>
<span class="k">print</span><span class="p">(</span><span class="n">list_winning_moves</span><span class="p">(</span><span class="n">solution</span><span class="p">,</span> <span class="s">'b'</span><span class="p">))</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt;&gt;&gt; {'z', 'm', 'j', 'r', 'h'}
&gt;&gt;&gt; set()
&gt;&gt;&gt; {'r', 'l'}
</code></pre></div></div>

<p>If you arbitrarily choose a letter from the above function, you have an infallible Ghost AI.</p>

<h2 id="minimal-strategies">Minimal Strategies</h2>

<p>Because it would be nearly impossible to memorize optimal moves from every game state, the last task is to find a minimal list of winning words. Some dictionaries have many such lists – this function finds an arbitrary one. First, let’s just find the list of <em>all</em> “winning words,” or those that can be reached without going into a losing state (we’ll store these in a trie again):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">list_winning_words</span><span class="p">(</span><span class="n">trie</span><span class="p">,</span> <span class="n">solution</span><span class="p">,</span> <span class="n">current_state</span><span class="p">):</span>
    <span class="s">"""
    Parameters:
    trie: The pruned trie initialized from the dictionary
    solution: the 2-tuple returned from solve
    current_state: the incomplete word that has been spelled
    so far

    Returns: the set of winning words for the current player. The
      empty set if there are no winning moves.
    """</span>
    <span class="n">current_player</span> <span class="o">=</span> <span class="n">get_turn</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">current_state</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">solution</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
    <span class="n">winning_words</span> <span class="o">=</span> <span class="nb">set</span><span class="p">(</span><span class="n">trie</span><span class="p">.</span><span class="n">keys</span><span class="p">(</span><span class="n">current_state</span><span class="p">))</span>
    <span class="k">for</span> <span class="n">word</span> <span class="ow">in</span> <span class="n">trie</span><span class="p">.</span><span class="n">keys</span><span class="p">(</span><span class="n">current_state</span><span class="p">):</span>
        <span class="k">for</span> <span class="n">substr</span> <span class="ow">in</span> <span class="p">(</span><span class="n">word</span><span class="p">[:</span><span class="n">p</span><span class="p">]</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">current_state</span><span class="p">),</span> <span class="nb">len</span><span class="p">(</span><span class="n">word</span><span class="p">))):</span>
            <span class="c1"># If the current player can lose on the way to the winning word,
</span>            <span class="c1"># it is not a "winning word"
</span>            <span class="k">if</span> <span class="n">current_player</span> <span class="ow">in</span> <span class="n">solution</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="n">substr</span><span class="p">]:</span>
                <span class="n">winning_words</span> <span class="o">-=</span> <span class="nb">set</span><span class="p">([</span><span class="n">word</span><span class="p">])</span>
                <span class="k">continue</span>
    <span class="k">return</span> <span class="n">make_trie_from_word_list</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="n">winning_words</span><span class="p">))</span>

<span class="n">winning_trie</span> <span class="o">=</span> <span class="n">list_winning_words</span><span class="p">(</span><span class="n">trie</span><span class="p">,</span> <span class="n">solution</span><span class="p">,</span> <span class="s">''</span><span class="p">)</span>
</code></pre></div></div>

<p>To minimize, we recursively examine the size of the winning set that each possible set of moves would create. This is a slow approach with no pruning, but the winning sets at this point are relatively small so it’s not too much of a concern.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">minimize_strategy</span><span class="p">(</span><span class="n">winning_trie</span><span class="p">,</span> <span class="n">solution</span><span class="p">,</span> <span class="n">current_state</span><span class="p">):</span>
    <span class="s">"""
    Parameters:
    winning_trie: a trie initialized from the output of list_winning_words
    with the same current_state
    solution: the 2-tuple returned from solve
    current_state: the incomplete word that has been spelled
    so far

    Returns: the minimal set of winning words for the current player, assuming.
      all players play perfectly. The empty set if there are no winning moves.
    """</span>
    <span class="k">if</span> <span class="n">current_state</span> <span class="ow">in</span> <span class="n">winning_trie</span><span class="p">:</span>
        <span class="k">return</span> <span class="nb">set</span><span class="p">([</span><span class="n">current_state</span><span class="p">])</span>
    <span class="n">possible_moves</span> <span class="o">=</span> <span class="n">list_winning_moves</span><span class="p">(</span><span class="n">solution</span><span class="p">,</span> <span class="n">current_state</span><span class="p">)</span>
    <span class="n">num_players</span> <span class="o">=</span> <span class="n">solution</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
    <span class="n">best_move</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="k">for</span> <span class="n">move</span> <span class="ow">in</span> <span class="n">possible_moves</span><span class="p">:</span>
        <span class="n">this_move</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
        <span class="n">state</span> <span class="o">=</span> <span class="n">current_state</span> <span class="o">+</span> <span class="n">move</span>
        <span class="k">for</span> <span class="n">word</span> <span class="ow">in</span> <span class="n">winning_trie</span><span class="p">.</span><span class="n">keys</span><span class="p">(</span><span class="n">state</span><span class="p">):</span>
            <span class="n">this_move</span><span class="p">.</span><span class="n">update</span><span class="p">(</span><span class="n">minimize_strategy</span><span class="p">(</span><span class="n">winning_trie</span><span class="p">,</span> <span class="n">solution</span><span class="p">,</span> <span class="n">word</span><span class="p">[:</span><span class="nb">len</span><span class="p">(</span><span class="n">current_state</span><span class="p">)</span> <span class="o">+</span> <span class="n">num_players</span><span class="p">]))</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">best_move</span> <span class="ow">or</span> <span class="nb">len</span><span class="p">(</span><span class="n">best_move</span><span class="p">)</span> <span class="o">&gt;</span> <span class="nb">len</span><span class="p">(</span><span class="n">this_move</span><span class="p">):</span>
            <span class="n">best_move</span> <span class="o">=</span> <span class="n">this_move</span>
    <span class="k">return</span> <span class="n">best_move</span>

<span class="n">minimize_strategy</span><span class="p">(</span><span class="n">winning_trie</span><span class="p">,</span> <span class="n">solution</span><span class="p">,</span> <span class="s">''</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt;&gt;&gt; {'jail', 'jejune', 'jilt', 'jowl', 'juvenile'}
</code></pre></div></div>

<p>We can use this function to find a memorizable (or at least minimal) set of winning words from every starting state.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">itertools</span>

<span class="k">def</span> <span class="nf">get_minimal_strategies</span><span class="p">(</span><span class="n">dictionary</span><span class="p">,</span> <span class="n">num_players</span><span class="p">,</span> <span class="n">min_length</span><span class="p">):</span>
    <span class="n">words</span> <span class="o">=</span> <span class="n">gen_word_list</span><span class="p">(</span><span class="n">dictionary</span><span class="p">,</span> <span class="n">min_length</span><span class="p">)</span>
    <span class="n">trie</span> <span class="o">=</span> <span class="n">prune_trie</span><span class="p">(</span><span class="n">make_trie_from_word_list</span><span class="p">(</span><span class="n">words</span><span class="p">))</span>
    <span class="n">solution</span> <span class="o">=</span> <span class="n">solve</span><span class="p">(</span><span class="n">trie</span><span class="p">,</span> <span class="n">num_players</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">num_players</span> <span class="o">+</span> <span class="mi">1</span><span class="p">):</span>
        <span class="k">print</span><span class="p">()</span>
        <span class="k">print</span><span class="p">(</span><span class="s">"Player "</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="o">+</span> <span class="s">"'s minimal strategy:"</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">prefix</span> <span class="ow">in</span> <span class="n">itertools</span><span class="p">.</span><span class="n">permutations</span><span class="p">(</span><span class="n">string</span><span class="p">.</span><span class="n">ascii_lowercase</span><span class="p">,</span> <span class="n">i</span> <span class="o">-</span> <span class="mi">1</span><span class="p">):</span>
            <span class="n">prefix</span> <span class="o">=</span> <span class="s">''</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">prefix</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">trie</span><span class="p">.</span><span class="n">keys</span><span class="p">(</span><span class="n">prefix</span><span class="p">):</span>
                <span class="n">winning_words</span> <span class="o">=</span> <span class="n">list_winning_words</span><span class="p">(</span><span class="n">trie</span><span class="p">,</span> <span class="n">solution</span><span class="p">,</span> <span class="n">prefix</span><span class="p">)</span>
                <span class="n">s</span> <span class="o">=</span> <span class="n">minimize_strategy</span><span class="p">(</span><span class="n">winning_words</span><span class="p">,</span> <span class="n">solution</span><span class="p">,</span> <span class="n">prefix</span><span class="p">)</span>
                <span class="k">if</span> <span class="ow">not</span> <span class="n">s</span><span class="p">:</span>
                    <span class="n">s</span> <span class="o">=</span> <span class="s">"No winning moves."</span>
            <span class="k">print</span><span class="p">(</span><span class="n">prefix</span><span class="p">,</span> <span class="s">": "</span><span class="p">,</span> <span class="n">s</span><span class="p">)</span>

<span class="n">get_minimal_strategies</span><span class="p">(</span><span class="n">DICTIONARY</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Player 1's minimal strategy:
    :  {'juvenile', 'jail', 'jilt', 'jowl', 'jejune'}

Player 2's minimal strategy:
a :  {'aorta'}
b :  {'blimp', 'blemish', 'bloat', 'black', 'blubber'}
c :  {'crack', 'crepe', 'crick', 'crozier', 'crypt', 'crept', 'crucial'}
d :  {'djinn'}
e :  {'ejaculate', 'ejaculation', 'eject'}
f :  {'fjord'}
g :  {'gherkin', 'ghastliness', 'ghastly', 'ghoul'}
h :  No winning moves.
i :  {'iffiest'}
j :  No winning moves.
k :  {'khaki'}
l :  {'llama'}
m :  No winning moves.
n :  {'nymph', 'nylon'}
o :  {'ozone'}
p :  {'pneumatic'}
q :  {'quoit', 'quack', 'quest', 'quibble', 'quibbling'}
r :  No winning moves.
s :  {'squeeze', 'squelch', 'squeamish', 'squeezing'}
t :  {'trochee', 'truffle', 'tryst', 'traffic', 'triumvirate', 'trefoil'}
u :  {'uvula'}
v :  {'vulva'}
w :  {'wrath', 'wrought', 'wrist', 'wrung', 'wryly', 'wreck'}
x :  {'xylem'}
y :  {'yttrium'}
z :  No winning moves.
</code></pre></div></div>

<h2 id="3-players">3+ Players</h2>

<p>Below is the minimal strategy for three players. It’s a little unwieldy to memorize, though.</p>

<p>The minimal strategy for 3+ players isn’t particularly useful in a real game, however, because it assumes that all players play perfectly. It’s possible that one of the other 2+ players won’t play in his/her/its own best interest, so the final word could end up being outside <em>any</em> winning strategy.</p>

<p>Interestingly, it’s possible for all 3+ players to be in a losing situation in the same context. Since no player knows what the others will do if indifferent between options, some prefix could be equally “dangerous” for all players.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">get_minimal_strategies</span><span class="p">(</span><span class="n">DICTIONARY</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Player 1's minimal strategy:
    :  {'quorum', 'quixotic', 'quest', 'quatrain'}

Player 2's minimal strategy:
a :  {'ajar'}
b :  {'byes', 'bypass', 'byplay', 'bystander', 'byelaw', 'byproduct', 'byte', 'bygone', 'bypast'}
c :  {'czar'}
d :  No winning moves.
e :  {'eons'}
f :  No winning moves.
g :  {'gaff', 'gaps', 'gavotte', 'gage', 'gander', 'gantlet', 'gawk', 'gaping', 'gather', 'gave', 'gape', 'gating', 'gang', 'gags', 'gagged', 'gate', 'gannet', 'gaging'}
h :  No winning moves.
i :  {'iamb'}
j :  No winning moves.
k :  {'krypton', 'kronor'}
l :  No winning moves.
m :  {'myna', 'myopia', 'myth', 'myself', 'myopic', 'mysteries', 'mystery'}
n :  No winning moves.
o :  {'oyster'}
p :  {'pneumonia', 'pneumatic'}
q :  No winning moves.
r :  No winning moves.
s :  {'svelte'}
t :  {'tzar'}
u :  {'ubiquitous'}
v :  No winning moves.
w :  {'wuss'}
x :  No winning moves.
y :  {'yttrium'}
z :  {'zygote'}

Player 3's minimal strategy:
ab :  {'abhor'}
ac :  {'acme'}
ad :  {'adze'}
ae :  {'aegis'}
af :  {'afar'}
ag :  {'ague'}
ah :  {'ahoy'}
ai :  {'aisle'}
aj :  {'ajar'}
ak :  No winning moves.
al :  {'alum'}
am :  {'amnesia', 'amniocenteses'}
an :  {'anus'}
ao :  {'aorta'}
ap :  {'apse'}
aq :  {'aquiline', 'aquiculture', 'aqueous', 'aqueduct', 'aquifer', 'aqua'}
ar :  {'arks'}
as :  {'asocial'}
at :  {'atelier'}
au :  {'auxiliaries'}
av :  {'avow', 'avoirdupois', 'avocado', 'avoid'}
aw :  {'awry'}
ax :  {'axon'}
ay :  {'ayes'}
az :  {'azure'}
ba :  {'baffling'}
be :  {'bebop'}
bi :  {'bizarre'}
bl :  {'blow', 'bloc', 'blooper', 'blond', 'bloom', 'blog', 'blossom', 'blood', 'blob', 'bloat', 'blousing', 'blot'}
bo :  {'bozo'}
br :  {'bras', 'bravado', 'brackish', 'bravo', 'bran', 'brawl', 'brawn', 'bracing', 'brace', 'brag', 'bramble', 'bravura', 'brave', 'brain', 'braising', 'brat', 'braille', 'braving', 'bray', 'braking', 'brad', 'braid', 'bract', 'brake'}
bu :  {'buzz'}
by :  {'byte'}
ca :  {'cayenne'}
ce :  {'cephalic'}
ch :  {'chlorophyll'}
ci :  {'cistern'}
cl :  No winning moves.
co :  {'cove'}
cr :  No winning moves.
cu :  {'cuff'}
cy :  {'cyanide'}
cz :  {'czar'}
da :  {'dawdling', 'dawn'}
de :  {'demo', 'demur'}
dh :  {'dhoti'}
di :  {'dibbling'}
dj :  {'djinn'}
do :  {'doff'}
dr :  {'drub', 'drunk', 'drug', 'drudging', 'druid', 'drum'}
du :  {'duff'}
dw :  {'dwarves', 'dwarf'}
dy :  {'dyke'}
ea :  {'eave'}
eb :  {'ebullience'}
ec :  {'ecumenical'}
ed :  {'educable'}
ef :  No winning moves.
eg :  {'egalitarian'}
ei :  {'eider'}
ej :  {'eject'}
ek :  {'eking'}
el :  {'elms'}
em :  {'emcee'}
en :  {'envelop', 'envy', 'envoy'}
eo :  {'eons'}
ep :  {'epaulet'}
eq :  {'equinoctial', 'equestrian', 'equability', 'equilateral', 'equip', 'equivalent', 'equal', 'equidistant', 'equator', 'equities', 'equivalence', 'equinox', 'equanimity'}
er :  {'erect'}
es :  {'esquire'}
et :  {'eternal'}
eu :  {'eucalyptus'}
ev :  {'ever', 'eves', 'even'}
ew :  {'ewer', 'ewes'}
ex :  {'exult', 'exude', 'exuberance', 'exuding'}
ey :  {'eyrie'}
fa :  {'favor'}
fe :  {'fever'}
fi :  {'five'}
fj :  {'fjord'}
fl :  {'flea', 'flex', 'fleck', 'fled', 'flee', 'flesh', 'flew'}
fo :  {'foyer'}
fr :  No winning moves.
fu :  {'fuel'}
ga :  {'gaff'}
ge :  {'gear'}
gh :  {'ghoul', 'ghost'}
gi :  {'gift'}
gl :  {'glee', 'glean', 'glen', 'gleam'}
gn :  {'gnus'}
go :  {'gown'}
gr :  {'gryphon'}
gu :  {'guzzling'}
gy :  {'gyrating', 'gyration', 'gyro'}
ha :  {'haemophilia'}
he :  {'heft'}
hi :  {'hims'}
ho :  {'hock'}
hu :  {'huff'}
hy :  {'hyena'}
ia :  {'iamb'}
ib :  {'ibex'}
ic :  {'icon'}
id :  {'idyl'}
if :  {'iffiest', 'iffy'}
ig :  {'igloo'}
ik :  {'ikon'}
il :  {'ilks'}
im :  No winning moves.
in :  {'inquietude', 'inquest'}
io :  {'iota'}
ip :  No winning moves.
ir :  {'iron'}
is :  {'isms'}
it :  {'itch'}
iv :  {'ivies'}
ja :  {'jazz'}
je :  {'jewel'}
ji :  {'jiujitsu'}
jo :  {'jowl'}
ju :  {'just'}
ka :  {'kazoo'}
ke :  {'kestrel'}
kh :  {'khaki', 'khan'}
ki :  {'kiwi'}
kl :  {'klutz'}
kn :  {'knavish', 'knave', 'knapsack', 'knack'}
ko :  {'koala'}
kr :  {'krypton'}
ku :  {'kumquat'}
la :  {'lallygag'}
le :  {'left'}
li :  {'lion'}
lo :  {'lozenge'}
lu :  {'luau'}
ly :  {'lymph'}
ma :  {'maxed', 'maxes'}
me :  {'meow'}
mi :  {'miff'}
mn :  {'mnemonic'}
mo :  {'mozzarella'}
mu :  {'muzzling'}
my :  {'myth'}
na :  {'nays'}
ne :  {'need'}
ni :  {'nirvana'}
no :  {'noxious'}
nu :  {'nuzzling'}
ny :  {'nymph'}
oa :  {'oasis', 'oases'}
ob :  {'obit'}
oc :  {'ocarina'}
od :  {'odes'}
of :  {'often'}
og :  {'ogre'}
oh :  {'ohms'}
oi :  {'oink', 'ointment'}
ok :  {'okay'}
ol :  {'olfactories'}
om :  {'ominous', 'omit', 'omission'}
on :  {'onion'}
op :  {'opossum'}
or :  {'orotund'}
os :  {'osier'}
ot :  {'other'}
ou :  {'ours'}
ov :  {'ovoid'}
ow :  {'owing'}
ox :  {'oxbow'}
oy :  No winning moves.
oz :  {'ozone'}
pa :  {'paean'}
pe :  {'pejorative'}
ph :  {'phrasal', 'phrenology'}
pi :  {'pirouetting'}
pl :  No winning moves.
pn :  No winning moves.
po :  {'poxes'}
pr :  No winning moves.
ps :  {'psalm'}
pt :  {'pterodactyl'}
pu :  {'puzzling'}
py :  {'pyorrhea'}
qu :  No winning moves.
ra :  {'raja'}
re :  {'request', 'requiem'}
rh :  {'rhubarb'}
ri :  {'riot'}
ro :  {'royal'}
ru :  {'ruff'}
sa :  {'sahib'}
sc :  {'scything'}
se :  {'sever', 'seven'}
sh :  No winning moves.
si :  {'sift'}
sk :  {'skating', 'skate'}
sl :  No winning moves.
sm :  {'smear', 'smelt', 'smell'}
sn :  {'sneezing', 'sneer', 'sneak'}
so :  {'sojourn'}
sp :  {'sphinges', 'spheroid'}
sq :  No winning moves.
st :  No winning moves.
su :  {'suturing'}
sv :  No winning moves.
sw :  {'swum', 'swung'}
sy :  No winning moves.
ta :  {'tadpole', 'tads'}
te :  {'tequila'}
th :  {'thalamus', 'that', 'thallium', 'thaw', 'than', 'thalami'}
ti :  {'tiff'}
to :  {'toque'}
tr :  No winning moves.
ts :  {'tsar'}
tu :  {'tuft'}
tw :  No winning moves.
ty :  {'tyke'}
tz :  {'tzar'}
ub :  {'ubiquitous', 'ubiquity'}
ud :  {'udder'}
ug :  {'ugliness', 'ugliest', 'ugly'}
uk :  {'ukulele'}
ul :  {'ulcer'}
um :  {'umiak'}
un :  {'unzip'}
up :  {'upend'}
ur :  {'uranium'}
us :  {'using'}
ut :  {'utter'}
uv :  {'uvula'}
va :  {'vain'}
ve :  {'veal'}
vi :  {'vixen'}
vo :  {'vouch'}
vu :  No winning moves.
vy :  {'vying'}
wa :  {'waffling', 'wafer', 'waft'}
we :  {'were'}
wh :  {'whys'}
wi :  {'wife'}
wo :  {'wove'}
wr :  {'wrung'}
wu :  {'wuss'}
xe :  {'xerography', 'xerographic'}
xy :  {'xylophonist', 'xylem'}
ya :  {'yank'}
ye :  {'yews'}
yi :  {'yield'}
yo :  {'yore'}
yt :  {'yttrium'}
yu :  {'yule'}
za :  No winning moves.
ze :  {'zero'}
zi :  {'zilch', 'zillion'}
zo :  {'zombi'}
zu :  {'zucchini'}
zw :  {'zwieback'}
zy :  No winning moves.
</code></pre></div></div>

<h2 id="losers">Losers</h2>

<p>It is of the utmost importance to decide on a dictionary before partaking in a friendly game of Ghost. Player 2 can win using the Scrabble dictionary, but player 1 always wins with the other two.</p>

<table>
  <thead>
    <tr>
      <th>Dictionary</th>
      <th>Minimum Word Length</th>
      <th>Number of players</th>
      <th>Losers</th>
      <th>Winning moves for player 1</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>web2</td>
      <td>3</td>
      <td>2</td>
      <td>{2}</td>
      <td>{‘a’, ‘h’, ‘l’}</td>
    </tr>
    <tr>
      <td>web2</td>
      <td>3</td>
      <td>3</td>
      <td>{3}</td>
      <td>{‘t’, ‘a’, ‘d’, ‘n’}</td>
    </tr>
    <tr>
      <td>web2</td>
      <td>3</td>
      <td>4</td>
      <td>{3, 4}</td>
      <td>{‘j’, ‘l’}</td>
    </tr>
    <tr>
      <td><strong>web2</strong></td>
      <td><strong>4</strong></td>
      <td><strong>2</strong></td>
      <td><strong>{2}</strong></td>
      <td><strong>{‘a’, ‘h’, ‘l’, ‘e’}</strong></td>
    </tr>
    <tr>
      <td><strong>web2</strong></td>
      <td><strong>4</strong></td>
      <td><strong>3</strong></td>
      <td><strong>{3}</strong></td>
      <td><strong>{‘t’, ‘a’, ‘d’}</strong></td>
    </tr>
    <tr>
      <td><strong>web2</strong></td>
      <td><strong>4</strong></td>
      <td><strong>4</strong></td>
      <td><strong>{3, 4}</strong></td>
      <td><strong>{‘j’, ‘r’, ‘l’}</strong></td>
    </tr>
    <tr>
      <td>TWL06</td>
      <td>3</td>
      <td>2</td>
      <td>{1}</td>
      <td>None</td>
    </tr>
    <tr>
      <td>TWL06</td>
      <td>3</td>
      <td>3</td>
      <td>{3}</td>
      <td>{‘m’, ‘n’}</td>
    </tr>
    <tr>
      <td>TWL06</td>
      <td>3</td>
      <td>4</td>
      <td>{3, 4}</td>
      <td>{‘h’, ‘q’}</td>
    </tr>
    <tr>
      <td><strong>TWL06</strong></td>
      <td><strong>4</strong></td>
      <td><strong>2</strong></td>
      <td><strong>{2}</strong></td>
      <td><strong>{‘m’, ‘n’}</strong></td>
    </tr>
    <tr>
      <td><strong>TWL06</strong></td>
      <td><strong>4</strong></td>
      <td><strong>3</strong></td>
      <td><strong>{1,2,3}</strong></td>
      <td><strong>None</strong></td>
    </tr>
    <tr>
      <td><strong>TWL06</strong></td>
      <td><strong>4</strong></td>
      <td><strong>4</strong></td>
      <td><strong>{3, 4}</strong></td>
      <td><strong>{‘h’, ‘q’}</strong></td>
    </tr>
    <tr>
      <td>american-english</td>
      <td>3</td>
      <td>2</td>
      <td>{2}</td>
      <td>{‘j’, ‘m’, ‘h’, ‘z’}</td>
    </tr>
    <tr>
      <td>american-english</td>
      <td>3</td>
      <td>3</td>
      <td>{2, 3}</td>
      <td>{‘n’, ‘s’, ‘q’, ‘r’, ‘z’, ‘p’}</td>
    </tr>
    <tr>
      <td>american-english</td>
      <td>3</td>
      <td>4</td>
      <td>{3, 4}</td>
      <td>{‘r’, ‘z’}</td>
    </tr>
    <tr>
      <td><strong>american-english</strong></td>
      <td><strong>4</strong></td>
      <td><strong>2</strong></td>
      <td><strong>{2}</strong></td>
      <td><strong>{‘j’, ‘m’, ‘h’, ‘r’, ‘z’}</strong></td>
    </tr>
    <tr>
      <td><strong>american-english</strong></td>
      <td><strong>4</strong></td>
      <td><strong>3</strong></td>
      <td><strong>{2, 3}</strong></td>
      <td><strong>{‘s’, ‘p’, ‘q’}</strong></td>
    </tr>
    <tr>
      <td><strong>american-english</strong></td>
      <td><strong>4</strong></td>
      <td><strong>4</strong></td>
      <td><strong>{4}</strong></td>
      <td><strong>{‘m’, ‘z’}</strong></td>
    </tr>
  </tbody>
</table>

<p><strong>The Moral</strong>: if you’re going to play Ghost with your mother, make sure you use the Scrabble dictionary. Then you can win when she plays ‘z.’</p>

<h2 id="download">Download</h2>

<p>To experiment yourself, you can download this post as an iPython notebook. However, I’ve also implemented a more flexible “Ghost” class that you can download <a href="https://ostrowr.github.io/content/ghost/ghost.py">here</a> along with a basic test suite <a href="https://ostrowr.github.io/content/ghost/ghost_tests.py">here</a>.</p>

<p>As always, any suggestions, corrections, criticisms (constructive or otherwise), and witticisms welcome.</p>]]></content><author><name>Robbie Ostrow</name><email>robbie@ostro.ws</email></author><summary type="html"><![CDATA[My mother loves words. To this day, she insists that I should become a poet. During a road trip when I was little, she introduced me to a game called Ghost, and we would play all the time.]]></summary></entry></feed>