<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
  xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>atomic14</title>
    <description>A collection of slightly mad projects, instructive/educational <a href="https://www.youtube.com/c/atomic14">videos</a>, and generally interesting stuff.
Building projects around the Arduino and ESP32 platforms - we'll be exploring AI, Computer Vision, Audio, 3D Printing - it may get a bit eclectic...
</description>
    <link>https://www.atomic14.com/</link>
    <atom:link href="https://www.atomic14.com/feed.xml" rel="self" type="application/rss+xml" />
    <pubDate>Fri, 02 Oct 2026 12:25:28 +0000</pubDate>
    <lastBuildDate>Fri, 02 Oct 2026 12:25:28 +0000</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    
    <item>
      <title>Jev Plays Manic Miner</title>
      <description>&lt;p&gt;In my &lt;a href=&quot;/2026/09/28/manic-miner-from-reinforcement-learning-to-search&quot;&gt;last post&lt;/a&gt; Claude and I failed to get reinforcement learning to play Manic Miner, and we ended up solving the whole game with plain old search.&lt;/p&gt;

&lt;p&gt;Around the same time, TypeSafe released &lt;a href=&quot;https://docs.typesafe.ai/introduction&quot;&gt;Jev&lt;/a&gt;. Jev is what they call a “System One” model - the name comes from the fast, instinctive kind of thinking, as opposed to slow, deliberate reasoning. It doesn’t generate text, it makes decisions.&lt;/p&gt;

&lt;p&gt;You give it some information about the situation (as JSON), a paragraph of instructions, and a list of options, and it tells you how likely it thinks each option is to be the right one. It’s fast (around 270ms per decision for me) and cheap - a complete attempt at a cavern costs about half a cent.&lt;/p&gt;

&lt;p&gt;Jev has already been used to play plenty of games. TypeSafe’s launch demos had it &lt;a href=&quot;https://www.theregister.com/ai-and-ml/2026/09/16/typesafe-ai-debuts-model-for-machines-that-plays-doom/5296711&quot;&gt;playing Doom&lt;/a&gt; from structured game state, and playing &lt;a href=&quot;https://typesafe.ai/blog/introducing-system-one-models-and-jev&quot;&gt;Wikiracing&lt;/a&gt; (get from one Wikipedia page to another using only links). People have since built &lt;a href=&quot;https://www.mindstudio.ai/blog/jev-real-time-game-demos&quot;&gt;Minecraft, Subway Surfers and driving demos&lt;/a&gt;, and there’s a &lt;a href=&quot;https://github.com/rmalde/minecraft-agent&quot;&gt;Minecraft speedrun agent&lt;/a&gt; that pairs a planner with Jev.&lt;/p&gt;

&lt;p&gt;So: can Jev play Manic Miner? For anyone who missed the last post - you control Miner Willy, and in each of the 20 caverns he has to collect all the keys and then get to the exit, without touching an enemy, falling too far, or running out of air.&lt;/p&gt;

&lt;p&gt;This has been a pretty interesting experiment. There is also a big caveat in this work - there are probably better ways to present the information to Jev and get better decisions. More research is required.&lt;/p&gt;

&lt;p&gt;It’s also entirely possible that we just aren’t giving Jev good facts to work on. How do you describe a cave from Manic Miner in text?&lt;/p&gt;

&lt;p&gt;At first I thought Jev was smashing it - performing much better than I expected.&lt;/p&gt;

&lt;p&gt;Then I tested things using a very simple algorithm-based solution - this made me think that maybe Jev wasn’t doing anything clever at all.&lt;/p&gt;

&lt;p&gt;But, after a bit more investigation, I came back to the conclusion that Jev was actually doing something useful.&lt;/p&gt;

&lt;p&gt;And then we fixed the information we were giving it, and the simple algorithm pulled ahead again.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-09-28/jev-plays.webp&quot; alt=&quot;Jev playing Central Cavern (one of our recorded runs). Willy is the white figure. The yellow box is the key Jev has chosen to go for, and the bars are its probabilities for the top three moves - this is sped up quite a bit&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;describing-the-game-to-jev&quot;&gt;Describing the game to Jev&lt;/h2&gt;

&lt;p&gt;Jev isn’t multimodal - I can’t just hand it a screenshot and say “what should Willy do?”. It also doesn’t do reasoning or planning. It makes one quick judgement from what’s in front of it.&lt;/p&gt;

&lt;p&gt;My first thought was to give it a map of the cavern as text. This is an ASCII map of Central Cavern at the start of a run:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;#........A.X....X............B.#
#...............C..............#
#..............................#
#..............................#
#......................XD..X...#
#=============~~~~=~~~~========#
#.............................E#
#===.........GG................#
#............GG..###.X.........#
#====...&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;...#
#............................==#
#..............................#
#...........X.......###~~~~~===#
#.WW.===============.........PP#
#.WW.........................PP#
#==============================#
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Each character is one cell of the screen. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WW&lt;/code&gt; is Willy (he’s two cells wide and two high, and so are the guardians and the portal), &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;E&lt;/code&gt; are the keys, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;P&lt;/code&gt; is the exit portal, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;G&lt;/code&gt; is a guardian, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;X&lt;/code&gt; is a nasty (the poisonous plants and stalactites), &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;=&lt;/code&gt; is floor, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~&lt;/code&gt; is crumbling floor, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;&lt;/code&gt; is the conveyor, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#&lt;/code&gt; is wall, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.&lt;/code&gt; is empty space. Jev gets a legend that explains all of this.&lt;/p&gt;

&lt;p&gt;To a person, that’s a pretty good description - you can trace the whole route on it. Jev can’t.&lt;/p&gt;

&lt;p&gt;You can see this when we ask Jev which key to go for next. It gets this map, plus a few facts about each key, like which side it’s on and how far away it is. Key D sits on the same top floor as keys A, C and B, and the only way up to that floor is at its far left end. So once Willy has collected E, a good order is A, C, D, B, walking right along the top floor.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-09-28/jev-key-decision.webp&quot; alt=&quot;Jev&apos;s key decision in Central Cavern, straight after collecting E. The key letters are drawn on for this post - Jev only sees them in its text map&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Jev picks D, the key that looks nearest - on the facts, D is 5 cells away and A is 20. And it picks D every single time - in all 977 of our recorded runs that got this far, through nearly two weeks of changes to the harness. D gets a probability of about 0.8, and A about 0.04.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-09-28/jev-key-problem.webp&quot; alt=&quot;The key problem in Central Cavern: Jev&apos;s choice after E, and the route that works&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Going for D isn’t a disaster - Willy has to go all the way round to the left either way. But Jev didn’t see that. In one run, Willy collected E at decision 14, and it took until decision 67 before he found the way up at the left and got his next key (he picked up A, then C, on the way to D). When asked again at decisions 40 and 65, Jev was even more sure about D (0.92 and 0.90).&lt;/p&gt;

&lt;p&gt;Claude (a reasoning model) worked it out from the same map: Willy can only reach the top floor from the far left, so he meets the keys in the order A, C, D, B as he walks right. Following a route across four floors takes several steps of reasoning, and Jev makes one quick judgement.&lt;/p&gt;

&lt;p&gt;It’s the same for moves. We tried showing Jev the map as it would look after each possible move, with no hint about which way to go. It finished 1 run of Central Cavern out of 20, and most runs got one key or none.&lt;/p&gt;

&lt;p&gt;Reading a route off a map is not Jev’s strong point. We need to describe what is happening - including which way to go.&lt;/p&gt;

&lt;p&gt;This description is what our harness is responsible for - all the code that sits between the game and Jev. Claude built it &lt;a href=&quot;https://github.com/atomic14/manic-miner-jev&quot;&gt;around the emulator&lt;/a&gt; and it does quite a bit of work. A slightly worrying amount of work to me.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;It reads Willy, the guardians, the keys, and the tiles out of the Spectrum’s memory.&lt;/li&gt;
  &lt;li&gt;It tries all six moves (walk left/right, jump left/right/up, and wait) in the emulator. So we know exactly what each move does.&lt;/li&gt;
  &lt;li&gt;It throws away any move that kills Willy or does nothing (i.e. Willy stays in the same place). It also throws away moves into a dead end: for each move, it plays up to four more moves ahead in the emulator to check that Willy can still stay alive - this is where it starts to feel like we are getting into the realm of cheating.&lt;/li&gt;
  &lt;li&gt;It turns everything into facts relative to Willy, including some memory: “Willy has been here before”, “Willy already tried this move from here”.&lt;/li&gt;
  &lt;li&gt;For each move, it adds a fact called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;progress&lt;/code&gt;: does this move take Willy “nearer” to where he needs to go, or “farther” away?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To play the game, we run the cavern, collect all the facts and possible moves and then ask Jev to pick the best move.&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;target&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;what&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;selected key&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;side&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;right&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;horizontal_cells&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;height&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;higher&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;way_up&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;side&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;right&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;horizontal_cells&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;to_the_left&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;first_thing&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;nasty&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;distance_cells&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;guardians&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;side&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;same column&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;height&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;higher&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;direction&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;at Willy&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;progress_measures&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;distance to the way up&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;moves&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;walk_right&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;movement&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Willy moves 1 cell to the right&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;progress&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;nearer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;place&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;new place&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;tried_from_here&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;no&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;jump_left&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;movement&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Willy moves 4 cells to the left&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;progress&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;farther&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;place&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;visited before&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;tried_from_here&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;no&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;wait&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;       &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;movement&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Willy stays in the same place&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;progress&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;same&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;place&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;visited before&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;tried_from_here&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;no&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;moves_not_offered&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;walk_left&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;kills Willy: nasty&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;jump_right&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;kills Willy: guardian&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;jump_up&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;no effect: Willy stays in the same place&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Along with this comes a paragraph of instructions that explains the goal and what each fact means. Jev’s answer: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;walk_right&lt;/code&gt; 0.71, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wait&lt;/code&gt; 0.18, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jump_left&lt;/code&gt; 0.11. It picks &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;walk_right&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every so often, Jev also decides which key to go for next, as in the Central Cavern example above. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;progress&lt;/code&gt; is then measured towards whichever key it picked.&lt;/p&gt;

&lt;h2 id=&quot;first-impression-jev-is-smashing-it---this-is-amazing&quot;&gt;First impression: Jev is smashing it - this is amazing!&lt;/h2&gt;

&lt;p&gt;With all that in place, Jev completed Central Cavern in 10 runs out of 10. The Menagerie and a couple of the one-key caverns went well too. It felt pretty good - although, looking back, those are the easier caverns, and most of the caverns were never completed at all.&lt;/p&gt;

&lt;p&gt;The map did help Jev’s key choices in a direct test. We asked it for the next key in 13 situations from the first four caverns. With the map and the facts together, it picked the same key as our completed runs in 12 of 13, against 5 of 13 with the facts alone. In Central Cavern, the map made it start with E instead of D. But better key choices didn’t lead to more completed runs. When Claude chose the key order for the first four caverns and Jev did the moves, Jev didn’t complete any more runs than with its own key choices.&lt;/p&gt;

&lt;h2 id=&quot;second-thoughts-is-jev-actually-doing-anything&quot;&gt;Second thoughts: is Jev actually doing anything?&lt;/h2&gt;

&lt;p&gt;Working with Claude driving Jev was an interesting experience. Claude was able to analyse each run and determine where and why Jev got stuck. And Claude was able to suggest better facts to get it unstuck.&lt;/p&gt;

&lt;p&gt;The problem is, every “better” fact moves more of the “intelligence” into the harness.&lt;/p&gt;

&lt;p&gt;The biggest one of these is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;progress&lt;/code&gt;. When the key is on the same floor as Willy, “nearer” means nearer to the key. When the key is on a different floor, the harness works out a way up (or down) from the map, and “nearer” means nearer to that spot.&lt;/p&gt;

&lt;p&gt;After a while, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;progress&lt;/code&gt; starts to resemble a very obvious hint of the route to take through the cavern. There’s a real danger of solving the cavern using search and then presenting Jev with a bunch of moves along with a massive hint on which one to pick.&lt;/p&gt;

&lt;p&gt;Instructions did seem to make a big difference. One of the difficulties here is that Claude does talk like an absolute lunatic sometimes, and it channels this into the instructions:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“If Willy comes back to the same places again and again, the direct way is closed, and he must go a different way, even if that way goes away from the target first.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Amazingly, this did actually seem to help!&lt;/p&gt;

&lt;p&gt;So, we’ve got a real danger of actually just being able to run the harness and then pick the “best” move presented by the harness without needing to use Jev at all.&lt;/p&gt;

&lt;p&gt;This basic algorithm works very well:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;If a move completes the cavern or collects a key, pick it.&lt;/li&gt;
  &lt;li&gt;Otherwise pick a move that’s “nearer” according to the harness.&lt;/li&gt;
  &lt;li&gt;Otherwise pick any move.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This simple set of rules can’t choose the key order, so we gave it a known-good key order for each cavern, and gave Jev the same order for comparison. We ran each cavern 10 times, and measured how far each run got: the share of the cavern’s keys and portal it reached. A complete run is 100%, and a run that collects 3 of 5 keys is 50% (3 of 6).&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-09-28/jev-progress-first.svg&quot; alt=&quot;The first comparison: how far Jev and the three-line rule got in each cavern&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Across the whole game, Jev reached 31% of all the keys and portals, and the rule 26%. Both finished Central Cavern every time and The Cold Room most of the time. Jev picked a move the rule could also have picked in 93% of its decisions, against 54% for a random move. Take &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;progress&lt;/code&gt; away and Jev almost never finishes a cavern. Random moves barely get started (4%).&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-09-28/jev-vs-rule.webp&quot; alt=&quot;Willy&apos;s path in Central Cavern with Jev (teal, 112 decisions) and with the three-line rule (orange, 130 decisions). Same key order, almost the same route&quot; /&gt;&lt;/p&gt;

&lt;p&gt;So my conclusion at this point was: Jev can play Manic Miner, but the harness is actually doing all the work.&lt;/p&gt;

&lt;p&gt;Why do so many caverns stop partway? Most of those runs didn’t die - they got stuck, walking back and forth until 30 moves passed with nothing new. In several caverns Jev got about halfway and then stalled. The harness only knows the way to the next floor, and in caverns where the route winds across several floors, “nearer” leads straight into a loop. It’s highly likely that the “facts” we are presenting Jev work well on the first three caverns, because they are the ones we tested and tuned against.&lt;/p&gt;

&lt;h2 id=&quot;digging-deeper&quot;&gt;Digging deeper&lt;/h2&gt;

&lt;p&gt;After some deep thinking from Claude, we tried some more experiments.&lt;/p&gt;

&lt;p&gt;We added a second simple rule, just for keys - “go for the nearest key” - and gave it to both Jev and the algorithm.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-09-28/jev-progress-like-for-like.svg&quot; alt=&quot;Like for like: how far Jev and the three-line rule got in each cavern, over 20 runs each&quot; /&gt;&lt;/p&gt;

&lt;p&gt;With the same key choice, Jev now got clearly further than the rule: 39% of all the keys and portals against 25%, and it finished caverns about twice as often. The same happened when both used the nearest-key rule (32% against 19%). That’s very unlikely to be luck - roughly a 1 in 1,000 chance.&lt;/p&gt;

&lt;h2 id=&quot;on-some-caverns-jev-is-better&quot;&gt;On some caverns Jev is better&lt;/h2&gt;

&lt;p&gt;Jev stands out in two caverns, Wacky Amoebatrons and Amoebatrons’ Revenge: it finishes most runs there, and the algorithm almost never does. It also gets much further than the algorithm in Processing Plant and The Sixteenth Cavern, although it doesn’t finish those.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-09-28/jev-amoebatrons.webp&quot; alt=&quot;Wacky Amoebatrons: Jev&apos;s routes, and where the algorithm died&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In Wacky Amoebatrons, Jev completed 9/10 runs. The algorithm completed 1/10 runs. The algorithm almost always ends up at the same spot on the bottom floor, where it shuffles back and forth until it’s trapped by the guardians coming down - by the time it dies, every move is fatal. Jev passes through that spot in some of its runs too, but it moves on. In Amoebatrons’ Revenge, Jev completed 7/10 runs, and the algorithm 1/10, mostly because it got stuck going round in a loop.&lt;/p&gt;

&lt;p&gt;So what is Jev doing that the algorithm isn’t? The algorithm only looks at two things: does a move collect a key, and is it “nearer”. Jev also gets facts about the guardians that walk left and right: which side they’re on, how far away they are, and whether they’re heading towards Willy. (It gets nothing about the up-and-down ones - we tried that early on, and it made things worse. Maybe information overload?)&lt;/p&gt;

&lt;p&gt;To test whether those guardian facts matter, we ran Jev without them. In 18 of the 20 caverns it made no measurable difference - removing the moves that kill Willy is enough. But in the two Amoebatron caverns, Jev’s results roughly halved: 17/20 down to 7/20 complete runs in Wacky Amoebatrons, and 13/20 down to 6/20 in Amoebatrons’ Revenge. So in exactly the caverns where Jev beats the algorithm, it’s the guardian facts that it’s using.&lt;/p&gt;

&lt;p&gt;Even without the guardian facts, Jev still did a bit better than the algorithm there, so it gets something from the other facts too - probably the memory facts (“Willy has been here before”, “Willy already tried this move from here”), which the simple algorithm ignores. We gave the algorithm the memory facts as well: it now prefers a “nearer” move that Willy hasn’t tried before. That helped it a little in these two caverns, but it got nowhere near Jev.&lt;/p&gt;

&lt;p&gt;Two more things we checked:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Jev’s top choice really matters.&lt;/strong&gt; Instead of playing Jev’s top choice, we tried picking a move at random, weighted by Jev’s probabilities. That got through only 18% of the game, against 33% for Jev’s top choice (both with Jev choosing its own keys). Jev’s top choice is far better than a weighted coin toss.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The difference builds up over a run.&lt;/strong&gt; We took 300 moments where Jev chose a move the algorithm wouldn’t have, played Jev’s move and the rule’s move from the same saved game, and then let the rule play on from both. On average, Jev’s move led to slightly more keys. Each of Jev’s choices is a little better than the algorithm’s.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;fixing-the-facts&quot;&gt;Fixing the facts&lt;/h2&gt;

&lt;p&gt;After all that, Claude went back through the failed runs and asked why it failed - was it Jev or our harness. In 122 of the 166 failed runs of one measurement, the facts had let Jev down: either no move was “nearer”, or the “nearer” moves went round in a loop.&lt;/p&gt;

&lt;p&gt;So we fixed the facts. Instead of guessing a “way up” from the text map, the harness now explores the cavern in the emulator. It tries every move from every place Willy can reach, with the guardians switched off. “Nearer” now means fewer moves along a route that really exists. We’re back to having search solve the cavern…&lt;/p&gt;

&lt;p&gt;It also turned up something embarrassing. With our six moves, three of the caverns looked impossible. A search found no route through Processing Plant, Return of the Alien Kong Beast or Solar Power Generator, and none of our 1,349 runs of them had ever finished. A walk always ends on the cell grid, and some jumps have to start half a cell over. Two extra “half step” moves fixed that - with them, the search finds a route through every cavern.&lt;/p&gt;

&lt;p&gt;With the true facts, both Jev and the algorithm did far better - and this time the algorithm came out on top. Here, Jev chose its own keys and the algorithm went for the nearest key:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Share of all the keys and portals&lt;/th&gt;
      &lt;th&gt;Old facts&lt;/th&gt;
      &lt;th&gt;True facts&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Jev&lt;/td&gt;
      &lt;td&gt;33%&lt;/td&gt;
      &lt;td&gt;60%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Algorithm&lt;/td&gt;
      &lt;td&gt;22%&lt;/td&gt;
      &lt;td&gt;71%&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;In complete runs, Jev went from 28 to 89 out of 200, and the algorithm from 20 to 114. With the same known-good key order for both, it’s 71% against 55%, and 125 complete runs against 86. That’s very unlikely to be luck - about a 1 in 7,000 chance. Between them, they’ve now finished 19 of the 20 caverns.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-09-28/jev-progress-true-facts.svg&quot; alt=&quot;True facts, like for like: how far Jev and the three-line rule got in each cavern, over 10 runs each&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Two caverns got worse for both of them. The true facts know the route, but not the guardians. In The Sixteenth Cavern, guardians trapped Willy at the left end of the bottom floor in every run, before he got a single key. Solar Power Generator went much the same way with the known-good key order.&lt;/p&gt;

&lt;p&gt;Jev still picks a “nearer” move about 90% of the time when there is one. But now that the facts are right, its other choices cost it runs: it waits just as a guardian comes down, or it goes round in a loop. When several moves are “nearer”, the algorithm picks one at random, and that does better. Even Jev’s lead in the two Amoebatron caverns shrinks, because the algorithm no longer gets trapped there.&lt;/p&gt;

&lt;h2 id=&quot;so-is-jev-any-good&quot;&gt;So is Jev any good?&lt;/h2&gt;

&lt;p&gt;For this game, most of Jev’s edge came from coping with bad facts. Where the harness’s hints led into a loop or a trap, Jev sometimes found its way out. Once the harness knows the real route, the three-line algorithm beats it.&lt;/p&gt;

&lt;p&gt;We also tried Claude Haiku (through the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;claude&lt;/code&gt; command-line tool) with the same facts. It completed Central Cavern in the one run we tried, in 123 decisions, which is not far off what Jev needs (78 to 138 in our runs). But each decision took almost 18 seconds - against Jev’s quarter of a second.&lt;/p&gt;

&lt;p&gt;Everything is on &lt;a href=&quot;https://github.com/atomic14/manic-miner-jev&quot;&gt;GitHub&lt;/a&gt;: the harness, the summaries of every measurement in this post, and a viewer that replays the recorded example runs and shows exactly what Jev was given for each decision.&lt;/p&gt;

&lt;p&gt;If you see an “AI plays a game” demo - with Jev or anything else - swap the model for a dumb algorithm and see what happens…&lt;/p&gt;

&lt;p&gt;Moral of the story? Same as last time - you can get a very long way with basic algorithms.&lt;/p&gt;
</description>
      <pubDate>Tue, 29 Sep 2026 12:00:00 +0000</pubDate>
      <link>https://www.atomic14.com/2026/09/29/jev-plays-manic-miner</link>
      <!-- guid kept as the original .html permalink so existing items are not re-flagged as new in feed readers -->
      <guid isPermaLink="true">https://www.atomic14.com/2026/09/29/jev-plays-manic-miner.html</guid>
        
    </item>
    
    <item>
      <title>Failing to solve Manic Miner with RL</title>
      <description>&lt;p&gt;Around 10 years ago Google DeepMind showed that you could use &lt;a href=&quot;https://www.bbc.co.uk/news/science-environment-31623427&quot;&gt;deep reinforcement learning to play video games&lt;/a&gt;. It’s something that I found fascinating at the time and wanted to try but never had the time to really look into it.&lt;/p&gt;

&lt;p&gt;With LLMs now being able to do a lot of the heavy lifting for me, I’ve started to work through my backlog of “this is interesting - I’d like to try it out”.&lt;/p&gt;

&lt;p&gt;I grew up in the 70s and 80s and my childhood computer was the ZX Spectrum, and my &lt;a href=&quot;https://www.esp32rainbow.com/&quot;&gt;ESP32 Rainbow project&lt;/a&gt; has given me a nice emulator that I can use to both play games and experiment with.&lt;/p&gt;

&lt;p&gt;Manic Miner was one of the first games we purchased and I wasted a lot of time playing it. I have vague memories of progressing reasonably far, but never actually completing the game. So I naively thought - Reinforcement Learning - why not? Some &lt;a href=&quot;https://www.mrkwatkins.co.uk/teaching-an-ai-to-play-zx-spectrum-games/&quot;&gt;cursory googling&lt;/a&gt; would have told me it was not suitable, but I’ve never really been one for doing a lot of up-front research before diving in.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-09-28/title-and-start.webp&quot; alt=&quot;The title screen, and the game starting&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The emulator that I have for the ESP32 also compiles into a desktop application (and there’s a WebAssembly version as well). Running it at full speed is about 1000 times faster than the original machine - so in theory we are in a good position to get something working.&lt;/p&gt;

&lt;p&gt;There are also some really good resources for Manic Miner - &lt;a href=&quot;https://skoolkit.ca/disassemblies/manic_miner/&quot;&gt;this site&lt;/a&gt; is particularly good. Very handy when Claude is being an absolute doofus head.&lt;/p&gt;

&lt;p&gt;The idea that Claude and I came up with (and of course Claude was super enthusiastic about…) was to wrap the emulator as a &lt;a href=&quot;https://gymnasium.farama.org/index.html&quot;&gt;Gym&lt;/a&gt; environment, give the agent a reward for doing well, and run &lt;a href=&quot;https://en.wikipedia.org/wiki/Proximal_policy_optimization&quot;&gt;PPO&lt;/a&gt; until Willy walks out of Central Cavern. How hard can this possibly be…&lt;/p&gt;

&lt;p&gt;This failed miserably. The initial attempt was to just use joystick movements to drive Willy around. We then tried creating a set of move “macros”, e.g. jump right, jump left, walk right. And still didn’t get anywhere with the RL approach.&lt;/p&gt;

&lt;p&gt;Claude then suggested that what we were lacking was a good reward function, and that what we should do was solve the problem via search and then feed the route into the RL algorithm and use that to help guide the algorithm.&lt;/p&gt;

&lt;p&gt;This really should have set off some alarm bells in my head.&lt;/p&gt;

&lt;p&gt;We spent a lot of time messing around with searching and then feeding the path into the RL algorithm and started to get some results - but ultimately the question ends up being - “if we can solve this with search, what is RL adding?”.&lt;/p&gt;

&lt;p&gt;So, eventually, after a lot of wasted time, we pivoted to just doing that - why don’t we just try and find the fastest route through each cavern using search.&lt;/p&gt;

&lt;p&gt;Should be simple, shouldn’t it?&lt;/p&gt;

&lt;p&gt;There are a couple of things that make it tricky. There isn’t a simple graph that we can search (see later to learn I was wrong). The search space is very large, we have moving guardians, sometimes quite a few of them, this explodes the search space. Every position Willy can end up in gets multiplied by all the possible guardian positions.&lt;/p&gt;

&lt;p&gt;The order you collect items in can have a big influence on the length of the path - and you need to collect all the keys - this turns into a bit of a travelling salesman problem. What order should we collect them in?&lt;/p&gt;

&lt;p&gt;Some caverns have switches that change the layout - once again the search space gets quite big.&lt;/p&gt;

&lt;p&gt;Our first working version was a two pass system. The first pass runs with the guardians set as harmless and ignored. This lets us find the optimum path and item order.&lt;/p&gt;

&lt;p&gt;The second pass takes this route and searches along it but waits and jumps to avoid the guardians.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-09-28/paths-cavern0.webp&quot; alt=&quot;Pass one&apos;s path and pass two&apos;s route in Central Cavern&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-09-28/cavern0-harmless-vs-live.webp&quot; alt=&quot;Central Cavern side by side: harmless guardians (pass one) vs live guardians (pass two)&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The most expensive cavern is Return of the Alien Kong Beast, which has five items and two switches, and so 5,040 possible orders of collecting items. That one cavern took 8 hours to search.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-09-28/paths-cavern11.webp&quot; alt=&quot;Return of the Alien Kong Beast, with both switches&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Some of the caverns are very complicated, there are a lot of guardians, so waiting and jumping at the right time becomes critical.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-09-28/cavern10-live.webp&quot; alt=&quot;Attack of the Mutant Telephones&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This did get us a complete run through of Manic Miner - every cavern solved. 39,091 frames, which is about 13 minutes of play.&lt;/p&gt;

&lt;lite-youtube videoid=&quot;MITLC1E9R_Y&quot; playlabel=&quot;The two pass run through Manic Miner&quot;&gt;&lt;/lite-youtube&gt;

&lt;p&gt;The problem was it took around a day of computer time to find it, and 92% of that time was spent in the emulator. Every move the search tried had to be run through a full Z80 emulation, and even at 1000 times real speed it’s pretty slow.&lt;/p&gt;

&lt;p&gt;So we took the emulator out of the search. Willy’s movement is pretty simple - he walks 2 pixels per tick, jumps follow a fixed table, falls speed up and kill you if they’re too long, and crumbling floors and conveyors do what you’d expect. Claude went through the disassembly and wrote a model of all of this, and of the guardians, so the search can step through the game without running the emulator.&lt;/p&gt;

&lt;p&gt;Most of the guardians don’t react to Willy at all - where they are just depends on how long you’ve been in the cavern. Eugene, the Kong Beast are the exceptions. So we could drop the harmless guardians pass and do a single A* search with the guardians live. The inner loop is in C++ and does around 690,000 states a second.&lt;/p&gt;

&lt;p&gt;Every route the search finds still gets played back through the emulator, and if Willy dies we throw it away - this was added a belt and braces check - just in case Claude had got its model of the game wrong.&lt;/p&gt;

&lt;p&gt;All the caverns are searched in order, each one starting from the state the previous one finished in.&lt;/p&gt;

&lt;p&gt;There were bugs in the model. Eugene could walk off the bottom of his shaft, so Willy was dying to a guardian the model didn’t know was there. We also found that pressing fire to start the game was making Willy jump at the start of the first cavern.&lt;/p&gt;

&lt;p&gt;The full run is now 36,791 frames, about 2,300 fewer than the two pass version, and takes about two hours to find instead of a day. Fifteen of the twenty caverns are faster and the other five are slower by between 2 and 40 frames.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-09-28/frames-per-cavern.svg&quot; alt=&quot;Frames to finish each cavern with each approach&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Moral of the story? Forget all this ML and AI nonsense and get back to studying basic algorithms…&lt;/p&gt;
</description>
      <pubDate>Mon, 28 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://www.atomic14.com/2026/09/28/manic-miner-from-reinforcement-learning-to-search</link>
      <!-- guid kept as the original .html permalink so existing items are not re-flagged as new in feed readers -->
      <guid isPermaLink="true">https://www.atomic14.com/2026/09/28/manic-miner-from-reinforcement-learning-to-search.html</guid>
        
    </item>
    
    <item>
      <title>Why is Google still serving dodgy ads?</title>
      <description>&lt;p&gt;TLDR:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI is really good at detecting deceptive adverts - why isn’t Google using it?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A couple of months ago I noticed this advert popping up in the YouTube app. The main reason I noticed it is that I accidentally clicked it - I knew I was running out of space on my iPhone and a minor lapse of concentration caught me out.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-09-13/dodgy.webp&quot; alt=&quot;Well dodgy&quot; /&gt;&lt;/p&gt;

&lt;p&gt;These things can slip through even the best review processes, so I did my internet duty and reported the advert.&lt;/p&gt;

&lt;p&gt;The response I got back was less than satisfactory:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-09-13/hopeless.webp&quot; alt=&quot;It&apos;s all fine&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Again, sometimes things slip through the net and people don’t always check things properly, so I reported it again. And got the same response.&lt;/p&gt;

&lt;p&gt;In fact, multiple people have reported this advert and received the same:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;We found that the ad doesn’t go against Google’s policies, which prohibit certain content and practices that we believe to be harmful to users and the overall online ecosystem.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I’m a big fan of &lt;a href=&quot;https://en.wikipedia.org/wiki/Hanlon&apos;s_razor&quot;&gt;Hanlon’s razor&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Never attribute to malice that which is adequately explained by stupidity&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;However, if you were being uncharitable, you do have to question whether it’s in Google’s interest to remove adverts that get lots of clicks. After all, these adverts are probably performing well and bringing in lots of lovely 🤑.&lt;/p&gt;

&lt;p&gt;A simpler explanation is that the people reviewing adverts cannot properly review everything so things get missed.&lt;/p&gt;

&lt;p&gt;You can’t help thinking that AI should come to the rescue here. Given Google has some pretty good models - surely it can put them to use?&lt;/p&gt;

&lt;p&gt;Here’s what Google’s Gemini has to say about the advert:&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;classification-disapproved&quot;&gt;&lt;strong&gt;Classification: DISAPPROVED&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Policy Violations:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Misrepresentation: Misleading Ad Design&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Misrepresentation: Unreliable / Deceptive Claims&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;detailed-reasons-for-disapproval&quot;&gt;&lt;strong&gt;Detailed Reasons for Disapproval&lt;/strong&gt;&lt;/h3&gt;

&lt;h4 id=&quot;1-mimicking-system-alerts--ui-elements-misleading-ad-design&quot;&gt;&lt;strong&gt;1. Mimicking System Alerts / UI Elements (Misleading Ad Design)&lt;/strong&gt;&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;The Violation:&lt;/strong&gt; Google Ads policy strictly prohibits advertisements that imitate operating system dialogs, system warnings, error messages, or interactive system notifications.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The Evidence:&lt;/strong&gt; The banner embedded inside the ad explicitly mimics an &lt;strong&gt;iOS system alert modal&lt;/strong&gt; (“iPhone Storage is Full”) complete with standard iOS typography, container styling, and mock system buttons (“Yes” / “No”). This trick visually deceives users into believing their device is throwing a native system-level alert, coercing them to click.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;2-non-functional--deceptive-ui-components&quot;&gt;&lt;strong&gt;2. Non-Functional / Deceptive UI Components&lt;/strong&gt;&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;The Violation:&lt;/strong&gt; Ads cannot feature non-functional or misleading interactive elements, such as fake dialog options, false close buttons, or radio choices that do not perform their implied system function.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The Evidence:&lt;/strong&gt; The “Yes” and “No” options inside the graphic image are static visuals designed to capture clicks anywhere on the ad banner to trigger a store/landing page redirect, rather than acting as true system controls.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;3-deceptive-fear-based-tactics--unverified-claims&quot;&gt;&lt;strong&gt;3. Deceptive Fear-Based Tactics &amp;amp; Unverified Claims&lt;/strong&gt;&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;The Violation:&lt;/strong&gt; Advertising policies forbid using deceptive claims or scare tactics to induce panic and force immediate user action.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The Evidence:&lt;/strong&gt; Stating &lt;em&gt;“If you don’t free up space soon, some features may not work properly”&lt;/em&gt; inside a fake system popup fabricates a urgent state of failure on the user’s personal device, misleading them about their hardware status to drive app installs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;required-action&quot;&gt;&lt;strong&gt;Required Action&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;Disapprove the ad creative immediately. Issue a policy violation warning/disapproval flag to the advertiser account under &lt;strong&gt;Misrepresentation (Misleading Ad Design)&lt;/strong&gt;. Repeated violations of this nature may result in full account suspension for deceptive practices.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;Google’s own model rejects the ad in seconds, yet Google’s review process approved it twice.&lt;/p&gt;

&lt;p&gt;Come on, meatbags - use some of the amazing AI tools you have access to.&lt;/p&gt;
</description>
      <pubDate>Sun, 13 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://www.atomic14.com/2026/09/13/why-is-google-still-serving-dodgy-ads</link>
      <!-- guid kept as the original .html permalink so existing items are not re-flagged as new in feed readers -->
      <guid isPermaLink="true">https://www.atomic14.com/2026/09/13/why-is-google-still-serving-dodgy-ads.html</guid>
        
    </item>
    
    <item>
      <title>Detecting Claude and ChatGPT using letter counting.</title>
      <description>&lt;p&gt;&lt;a href=&quot;/2026/08/18/detecting-claude-with-letter-counts&quot;&gt;Last time&lt;/a&gt; I built a detector that counts
four-letter runs (n-grams) and tells you whether Claude Opus 5 wrote something. It works well - about 91% on
text it has never read.&lt;/p&gt;

&lt;p&gt;It has one obvious hole: it only knows one robot (Claude Opus 5). Paste ChatGPT output
into it and it says “meatbag”.&lt;/p&gt;

&lt;p&gt;So I’ve fixed that. I’ve added a ChatGPT detector and you can
&lt;a href=&quot;https://meatbag.atomic14.com&quot;&gt;try it here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-19/three-readings.webp&quot; alt=&quot;Robot or Meatbag? The detector calling a passage &amp;quot;Written by a robot&amp;quot;, with Claude Opus 5 at 1.00 and ChatGPT at 0.34 underneath&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;getting-the-chatgpt-data&quot;&gt;Getting the ChatGPT data&lt;/h2&gt;

&lt;p&gt;We did the same as last time, have the model rewrite the same 519 human passages, three ways. That’s 1,557
rewrites, plus 519 passages written from scratch that I keep out of training and use as a test.
2,076 generations in total.&lt;/p&gt;

&lt;p&gt;Last time this went through Anthropic’s batch API and cost about $30. This time I just ran the command
line codex using the subscription I’m already paying. So it didn’t cost anything extra (I’m wishing I’d done
that with claude last time!),&lt;/p&gt;

&lt;p&gt;There is a small issue with this - coodex is a coding agent, so it will have a system prompt that will
influence the output, so our trained up detector might not do as well on text from the ChatGPT app.&lt;/p&gt;

&lt;h2 id=&quot;the-chatgpt-detector&quot;&gt;The ChatGPT detector&lt;/h2&gt;

&lt;p&gt;We built this in exactly the same was as the Claude detector - and it works really well!&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Detector&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Cross-validated&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Held-out authors&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Claude Opus 5&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.906&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.925&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;ChatGPT&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;0.930&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;0.935&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;ChatGPT seems to be easier to detect than Claude. Not by a lot, but consistently.&lt;/p&gt;

&lt;p&gt;It also seems to handle shorter text better than the Claude detector.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Letters&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Claude detector&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;ChatGPT detector&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;100&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.645&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;0.709&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;200&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.740&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;0.798&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;432&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.783&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;0.885&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1,000&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.858&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;0.952&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;At a thousand letters the ChatGPT detector is on 0.952 against 0.858 for the Claude one, and it’s
ahead on short text as well.&lt;/p&gt;

&lt;h3 id=&quot;do-the-two-detectors-work-on-each-other&quot;&gt;Do the two detectors work on each other?&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Detector&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Reading its own model&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Reading the other one&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Claude&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.906&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.792&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;ChatGPT&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.930&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.800&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Each one is clearly best on the model it was trained on, but neither is terrible on the other - 0.79
and 0.80, against 0.5 for a coin toss.&lt;/p&gt;

&lt;h2 id=&quot;a-third-detector&quot;&gt;A third detector&lt;/h2&gt;

&lt;p&gt;Can we build a detector that does Claude or ChatGPT?&lt;/p&gt;

&lt;p&gt;We trained s third detector from scratch on all the data from both models.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Detector&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;ROC AUC&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Held-out authors&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Was this a robot?&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.897&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.911&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Does it look like Claude?&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.906&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.925&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Does it look like ChatGPT?&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.930&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.935&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;They’re three separate questions rather than slices of one pie, so they won’t add up to anything. A
passage can read as 0.85 robot while both model scores sit lower, which just means it looks machine
written without strongly resembling either one.&lt;/p&gt;

&lt;h2 id=&quot;what-it-still-cant-do&quot;&gt;What it still can’t do&lt;/h2&gt;

&lt;p&gt;Everything from last time, plus a couple of new ones.&lt;/p&gt;

&lt;p&gt;It knows two robots. Gemini, Llama, DeepSeek, anything else - it has never seen them. A low score is
not evidence that a person wrote something.&lt;/p&gt;

&lt;p&gt;It still detects a style rather than a model. The most useful result from last time hasn’t
changed: a writing style held out of training scored 0.153, meaning the detector confidently rated it
as more human than the humans. Both new detectors train on all three instruction styles for that
reason, but there’s nothing magic about three - we need to add more data.&lt;/p&gt;

&lt;p&gt;It’s still for education and amusement, and it’s still not evidence. Please continue not accusing
anyone of anything based on my stupid website.&lt;/p&gt;

&lt;h2 id=&quot;try-it-out&quot;&gt;Try it out&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://meatbag.atomic14.com&quot;&gt;meatbag.atomic14.com&lt;/a&gt; - paste something in and see which robot it
thinks you are.&lt;/p&gt;
</description>
      <pubDate>Wed, 19 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://www.atomic14.com/2026/08/19/now-it-spots-chatgpt-too</link>
      <!-- guid kept as the original .html permalink so existing items are not re-flagged as new in feed readers -->
      <guid isPermaLink="true">https://www.atomic14.com/2026/08/19/now-it-spots-chatgpt-too.html</guid>
        
    </item>
    
    <item>
      <title>Detecting Claude by counting letters</title>
      <description>&lt;p&gt;On 2 August 2026 Anthropic &lt;a href=&quot;https://www.anthropic.com/news/claude-text-watermark&quot;&gt;began watermarking&lt;/a&gt; the text that new Claude models produce - their support pages cover &lt;a href=&quot;https://support.claude.com/en/articles/16266773-how-claude-marks-ai-generated-content&quot;&gt;which products are marked&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The watermark is not a hidden character or a bit of metadata. It is statistical. A secret key nudges the generated output in a way that isn’t obvious to a person, but that can be detected if you have the key. This means that Anthropic can detect it but we common folk cannot.&lt;/p&gt;

&lt;p&gt;Other providers are doing &lt;a href=&quot;https://www.nature.com/articles/s41586-024-08025-4&quot;&gt;similar things&lt;/a&gt;, or will be soon.&lt;/p&gt;

&lt;p&gt;Can we build our own detector? Given a piece of text, can you tell that a model wrote it?&lt;/p&gt;

&lt;p&gt;Let’s simplify things and just try one narrow target: text written by Claude Opus 5. Mostly because that’s what I have a subscription to…&lt;/p&gt;

&lt;p&gt;And - spoiler alert - it turns out you can. I’ve deployed a &lt;a href=&quot;https://meatbag.atomic14.com&quot;&gt;web version of it here&lt;/a&gt;. Generate some text in Claude using Opus 5 and see if it can detect it.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-18/meatbag.webp&quot; alt=&quot;Robot or Meatbag? - the detector calling a pasted passage &amp;quot;Written by a robot&amp;quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;And yes, if you feed this blog post in, despite me editing it substantially - it’s comes out as written by Claude.&lt;/p&gt;

&lt;h2 id=&quot;how-does-it-work&quot;&gt;How does it work?&lt;/h2&gt;

&lt;p&gt;A long time ago I did a PhD and, as part of that, I looked into &lt;a href=&quot;https://en.wikipedia.org/wiki/Questioned_document_examination&quot;&gt;questioned document examinations&lt;/a&gt; - a field that tries to work out who wrote a document. Did Shakespeare write a play, or was it someone else? Which of the Federalists wrote which paper? Who sent these threatening letters?&lt;/p&gt;

&lt;p&gt;There’s a really nice simple technique that seems to work very well. You can often identify an author by counting short runs of characters. Ignore the words, sentence structure etc… just the occurrences of runs of letters. These runs are called n-grams.&lt;/p&gt;

&lt;p&gt;An n-gram is simply a run of n items taken from a sequence. You can build them from words or from characters. Here I’m using characters.&lt;/p&gt;

&lt;p&gt;Take “the quick brown fox”. Strip the spaces, the punctuation and the capitals and you have &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;thequickbrownfox&lt;/code&gt;. Slide a four-character window along it, one position at a time, and record what you see:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;theq  hequ  equi  quic  uick  ickb  ckbr  kbro  brow  rown  ownf  wnfo  nfox
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Do that to a whole passage and you end up with a table of counts: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;that&lt;/code&gt; appeared 40 times, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ting&lt;/code&gt; appeared 22 times, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;quic&lt;/code&gt; appeared once, and so on for every run that turned up. Throw the passage away. The table of counts is all the detector ever sees.&lt;/p&gt;

&lt;p&gt;The counts are normalised so, in theory, the length of the text doesn’t matter. Obviously there is a limit to this - if your text is too short then the counts are not statistically valid and you won’t be able to predict anything.&lt;/p&gt;

&lt;p&gt;The idea is that the statistical distribution is not something you are aware of as a writer and it’s quite hard to obfuscate. It exposes unconscious tells.&lt;/p&gt;

&lt;h2 id=&quot;getting-the-data&quot;&gt;Getting the data&lt;/h2&gt;

&lt;p&gt;As with any problem of this type, the hard part is getting data, the actual “science” part is easy.&lt;/p&gt;

&lt;p&gt;I needed human writing that was definitely human. The safest definition of “definitely” is “written before ChatGPT existed”, so everything I used was published before 2021. Claude helped me pull it together from three places:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Books&lt;/strong&gt; from &lt;a href=&quot;https://www.gutenberg.org/&quot;&gt;Project Gutenberg&lt;/a&gt;, all pre-1929.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Blog posts&lt;/strong&gt; from the &lt;a href=&quot;https://u.cs.biu.ac.il/~koppel/BlogCorpus.htm&quot;&gt;Blog Authorship Corpus&lt;/a&gt;, written in 2004 or earlier. Non-commercial research use only.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;News articles&lt;/strong&gt; from &lt;a href=&quot;https://huggingface.co/datasets/stanford-oval/ccnews&quot;&gt;CC-News&lt;/a&gt;, published between 2016 and 2019.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gave me 519 passages by 26 authors: 180 from books, 179 from news, 160 from blogs. Three different kinds of writing, and a spread of dates from Victorian to nearly modern.&lt;/p&gt;

&lt;h2 id=&quot;generating-the-ai-side&quot;&gt;Generating the AI side&lt;/h2&gt;

&lt;p&gt;The obvious way to get AI text is to ask Claude to write something.&lt;/p&gt;

&lt;p&gt;So I asked it to rewrite the human text. For every human passage I asked Claude Opus 5 to produce the same passage again, preserving the meaning, the level of detail and roughly the length. That gives me matched pairs of content. Within a pair the subject is identical, the facts are identical and the length is close, so the only thing left to separate them is the writing itself.&lt;/p&gt;

&lt;p&gt;Writing style is not one thing, so I asked three different ways:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;plain&lt;/strong&gt; - “Rewrite the following passage in clear natural English.”&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;direct&lt;/strong&gt; - “Rewrite the following passage in plain, direct modern English.”&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;formal&lt;/strong&gt; - “Rewrite the following passage in a formal, precise register.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That comes to 1,557 rewrites, generated through the batch API. I also had Claude write fresh passages from scratch on the same topics, which I kept out of all the training and used later as a test.&lt;/p&gt;

&lt;h2 id=&quot;does-it-work&quot;&gt;Does it work?&lt;/h2&gt;

&lt;p&gt;First, how the accuracy is measured. The number below is &lt;a href=&quot;https://developers.google.com/machine-learning/crash-course/classification/roc-and-auc&quot;&gt;ROC AUC&lt;/a&gt;. Take one human passage and one AI passage at random, score both, and ask whether the AI one got the higher score. ROC AUC is the proportion of random pairs where it did.&lt;/p&gt;

&lt;p&gt;0.5 is a coin toss. 1.0 means it never gets a pair the wrong way round. It measures ordering, not whether any particular score is correct.&lt;/p&gt;

&lt;p&gt;Second, the split. Before training anything I set four of the 26 authors aside and did not touch them. Everything else was measured by cross-validation, where the authors used for testing are never the authors used for training. Splitting by author rather than by passage is important. Split by passage and the classifier learns to recognise individual writers, which it is very good at, but it’s not what we want!&lt;/p&gt;

&lt;p&gt;Then I swept the n-gram length from 1 to 8, holding everything else fixed:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Run length&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Features&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;ROC AUC&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;28&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.651&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;673&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.757&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;8,590&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.857&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;4&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;60,211&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.906&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;5&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;214,949&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;0.912&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;6&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;444,691&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.907&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;7&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;668,979&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.890&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;8&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;843,596&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.863&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Some surprising results.&lt;/p&gt;

&lt;p&gt;Counting individual letters works! 28 features, just the letter frequencies, reach 0.651. That is not much, but it is well clear of a coin toss, from a really simple feature set.&lt;/p&gt;

&lt;p&gt;Accuracy climbs steeply to 4, peaks at 5, and then falls away. Longer runs carry more information, right up until there is too little data to support them. This may hint that using word counts would work quite well too - but maybe that would require more data? Something to research…&lt;/p&gt;

&lt;p&gt;I also tried spaced n-grams, where you take every second or every third character instead of consecutive ones. This didn’t work at all - it made the accuracy much worse.&lt;/p&gt;

&lt;h2 id=&quot;i-ended-up-picking-4-instead-of-5&quot;&gt;I ended up picking 4 instead of 5&lt;/h2&gt;

&lt;p&gt;The peak is at 5. But I decided to use 4, and there are a couple of reasons for this:&lt;/p&gt;

&lt;p&gt;The first reason is that going to 5 letters gives a marginal improvement in accuracy at the cost of a lot more features.&lt;/p&gt;

&lt;p&gt;The second is that it only gives an improvement on long passages of text. The reason is how thinly the data spreads. There are 26⁴ = 457,000 possible 4-grams and 26⁵ = 11.9 million possible 5-grams.&lt;/p&gt;

&lt;p&gt;If you take a typical passage from my corpus, with a length of 2,259 letters:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Run length&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Distinct runs found&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Appearing exactly once&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1,215&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;63%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;4&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1,750&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;81%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;5&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1,991&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;89%&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;At 5 characters, nearly nine in ten of the runs in a passage appear only once and it gets worse as the text gets shorter: at 200 letters, 90% of 3-grams and 97% of 5-grams appear only once.&lt;/p&gt;

&lt;p&gt;Here is the same detector built at each length, tested on 320 passages Claude wrote from prompts in a public benchmark against their 320 human source documents, with the text truncated to various lengths:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Letters&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;2&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;3&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;4&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;5&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;100&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.559&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.603&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;0.645&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.630&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;200&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.632&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.682&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;0.740&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.732&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;432&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.679&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.754&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;0.783&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.773&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1,000&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.719&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.827&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;0.867&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.866&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The 4-grams win at every length.&lt;/p&gt;

&lt;h2 id=&quot;the-final-detector&quot;&gt;The final detector&lt;/h2&gt;

&lt;p&gt;The finished thing is not complicated:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Take the text. Strip everything that is not a letter (including any spaces), and lowercase what remains.&lt;/li&gt;
  &lt;li&gt;Count all the 4-grams. Each passage becomes a list of 60,211 numbers.&lt;/li&gt;
  &lt;li&gt;Normalise, so the numbers are proportions and independent of passage length.&lt;/li&gt;
  &lt;li&gt;Feed it to a linear support vector machine, which learns which counts run higher in Claude’s writing.&lt;/li&gt;
  &lt;li&gt;Convert the result to a number between 0 and 1, where higher means more like Claude.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The n-gram counting and the classifier are both stock &lt;a href=&quot;https://scikit-learn.org/&quot;&gt;scikit-learn&lt;/a&gt; components.&lt;/p&gt;

&lt;p&gt;Because of step 1 it cannot see punctuation, sentence length, paragraph structure, capitalisation or any individual word. Because of step 3 it cannot see how long the passage is. It sees letter proportions and nothing else.&lt;/p&gt;

&lt;h2 id=&quot;testing-it&quot;&gt;Testing it&lt;/h2&gt;

&lt;p&gt;The four authors set aside at the beginning were scored once, at the end:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Test&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;ROC AUC&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;The four authors not in the training set&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.925&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Cross-validation, authors held out each round&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.906&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The two numbers are close, which is the thing I most wanted to see. It means the classifier was not quietly learning to recognise particular authors.&lt;/p&gt;

&lt;p&gt;Then I pointed it at text with no connection to the training data at all. First, 175 posts from my own blog written between 2006 and 2022, against eight passages written by Claude:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Group&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Passages&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Median score&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Above 0.8&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Human&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;175&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.09&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Claude Opus 5&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;8&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.85&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;63%&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Not bad!&lt;/p&gt;

&lt;p&gt;Second, a harder test. &lt;a href=&quot;https://aclanthology.org/2024.acl-long.674/&quot;&gt;RAID&lt;/a&gt; is a public benchmark for machine-generated text detection, containing &lt;a href=&quot;https://huggingface.co/datasets/liamdugan/raid&quot;&gt;six million passages&lt;/a&gt; from 11 generators across eight subject domains. I took 320 of its prompts, had Claude Opus 5 answer them, and scored those answers against the 320 human source documents the prompts came from. Different corpus, and a different task, because these were written from scratch rather than rewritten. It scores 0.889.&lt;/p&gt;

&lt;p&gt;And the fresh passages I held out of training entirely, written from scratch on the four unseen authors’ topics, score 0.977.&lt;/p&gt;

&lt;p&gt;So: counting four-letter sequences, with no understanding of the text whatsoever, separates human writing from Claude’s rewriting of that same writing about 93% of the time on authors it has never read. I did not expect it to work that well.&lt;/p&gt;

&lt;h2 id=&quot;what-is-it-actually-picking-up&quot;&gt;What is it actually picking up?&lt;/h2&gt;

&lt;p&gt;The model is simple enough that we can see what it’s doing (well, I can get Claude to do it anyway).&lt;/p&gt;

&lt;p&gt;Here are the strongest features on each side for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;plain&lt;/code&gt; rewrites, at 3 characters so they are short enough to read:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Pushes towards Claude&lt;/th&gt;
      &lt;th&gt;Pushes towards human&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ing&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tha&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hat&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;one&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ive&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hic&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ich&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Iam&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;not&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;but&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ugh&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;oug&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ver&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ely&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rou&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;whi&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wil&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;was&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ill&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Iwi&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ong&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;out&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hin&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ple&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tly&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ion&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ati&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tio&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;enc&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;con&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gth&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ctl&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;set&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ine&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eth&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fth&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;oft&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ofa&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hav&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dby&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;They are fragments, not words, but you can read them. On the Claude side: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-ing&lt;/code&gt; endings, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-ly&lt;/code&gt; adverbs (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ely&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tly&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ctl&lt;/code&gt; for “entirely”, “directly”, “exactly”), “that”, “though” and “through”, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ive&lt;/code&gt;, the contraction “I’ve” with the apostrophe stripped out.&lt;/p&gt;

&lt;p&gt;On the human side: “which”, “not”, “but”, “will”, “was”, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-tion&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-ation&lt;/code&gt; noun endings (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ion&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ati&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tio&lt;/code&gt;), “of the” and “of a” (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fth&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;oft&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ofa&lt;/code&gt;), and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Iam&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Iwi&lt;/code&gt;, which are “I am” and “I will” written out in full.&lt;/p&gt;

&lt;p&gt;Looking at individual letters is even more interesting when we compare each AI passage against the specific human passage it was rewritten from. Any difference inside a pair is a difference in writing, not in subject or era.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Instruction&lt;/th&gt;
      &lt;th&gt;Letters more common in Claude’s version&lt;/th&gt;
      &lt;th&gt;Letters less common&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;plain&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;g&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;h&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;l&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;r&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;n&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;y&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;i&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;s&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;o&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;f&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;m&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;direct&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;g&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;h&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;w&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;k&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;l&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;y&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;i&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;f&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;e&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;s&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;m&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;According to Claude, the pattern is recognisable. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;i&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;f&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;s&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;m&lt;/code&gt; are the letters of words English borrowed from Latin and French: “consideration”, “significance”, “specific”. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;g&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;h&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;w&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;k&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;y&lt;/code&gt; are the letters of the older Germanic core: “think”, “though”, “work”, “know”, “high”. Claude is moving the vocabulary from one to the other.&lt;/p&gt;

&lt;p&gt;Checking at the word level confirms it. Words with Latin endings drop in both, and in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;plain&lt;/code&gt; rewrites the mean word length barely moves, so the shift is which words Claude chose rather than how long they were.&lt;/p&gt;

&lt;p&gt;I guess this is what: “clear natural English”, “plain, direct modern English” means.&lt;/p&gt;

&lt;p&gt;Now the third instruction, the one asking for “a formal, precise register”:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Instruction&lt;/th&gt;
      &lt;th&gt;Letters more common in Claude’s version&lt;/th&gt;
      &lt;th&gt;Letters less common&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;formal&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;e&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;r&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;i&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;n&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;h&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;w&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;k&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;l&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;o&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;g&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Every letter has swapped sides. At the word level it is not subtle: Latin endings go &lt;strong&gt;up&lt;/strong&gt; and mean word length also goes up.&lt;/p&gt;

&lt;h2 id=&quot;how-generalised-is-it&quot;&gt;How generalised is it?&lt;/h2&gt;

&lt;p&gt;We built the detector twice. Once on all three instructions, which is the version described above, and once on the plain and direct rewrites only, leaving the formal ones out. Everything else is identical.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Tested on&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Trained on plain + direct&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Trained on all three&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;plain rewrites&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.827&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;0.899&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;direct rewrites&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.879&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;0.900&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;formal rewrites&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;0.153&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;0.977&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;fresh writing, in neither training set&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.881&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;0.977&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;RAID passages, outside the corpus&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.794&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;0.889&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;my blog, 8 AI passages&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;0.986&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.981&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;A detector that never saw the formal rewrites cannot detect them. It scores 0.153, which means it reliably rates them as &lt;strong&gt;more human-looking than the actual human writing beside them&lt;/strong&gt;. It is not confused and it is not broken. It learned “text that got plainer is AI”, it was handed text that got more formal, and it answered accordingly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fresh writing goes from 0.881 to 0.977, and it is in neither training set.&lt;/strong&gt; Passages Claude wrote from scratch, with no original to follow. Adding a third writing style did not just teach the model a third writing style - it generalised to a fourth condition it had never seen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The same happens outside the corpus&lt;/strong&gt;, where RAID goes from 0.794 to 0.889.&lt;/p&gt;

&lt;p&gt;Adding more styles would probably help to make the detector even more general.&lt;/p&gt;

&lt;p&gt;Unfortunately it also indicates that you can cheat the detector by using a style it has not bene trained on.&lt;/p&gt;

&lt;h2 id=&quot;try-it-out&quot;&gt;Try it out&lt;/h2&gt;

&lt;p&gt;The whole thing runs &lt;a href=&quot;https://meatbag.atomic14.com&quot;&gt;in a browser&lt;/a&gt;, and the text never leaves your machine.&lt;/p&gt;
</description>
      <pubDate>Tue, 18 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://www.atomic14.com/2026/08/18/detecting-claude-with-letter-counts</link>
      <!-- guid kept as the original .html permalink so existing items are not re-flagged as new in feed readers -->
      <guid isPermaLink="true">https://www.atomic14.com/2026/08/18/detecting-claude-with-letter-counts.html</guid>
        
    </item>
    
    <item>
      <title>Don&apos;t blame Claude - It&apos;s me, I&apos;m the problem, it&apos;s me...</title>
      <description>&lt;p&gt;F’you, Claude, you are driving me absolutely bananas:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The house convention is a short phrase naming the defect, and the archive is full of it: “A citation that names nothing”, “The safe zone that only the spawner obeys”, “One word that means five things”. I matched that convention.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href=&quot;/2026/08/15/output-style-does-nothing&quot;&gt;We agreed&lt;/a&gt; that you should use &lt;a href=&quot;https://en.wikipedia.org/wiki/Simplified_Technical_English&quot;&gt;ASD-STE100&lt;/a&gt; - Simplified Technical English. Why are you still producing this nonsense!&lt;/p&gt;

&lt;p&gt;What do you mean your weird text is “the house convention”? &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; says to use ASD-STE100, the output style says to use ASD-STE100 - arrrrgh!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-17/rage-work.webp&quot; alt=&quot;Man in a cubicle beating a keyboard against his monitor&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Take a deep breath. And repeat to yourself: “It’s not Claude’s fault”. Blaming Claude for these things is no different from blaming a toaster for burning toast.&lt;/p&gt;

&lt;p&gt;So, what’s going on? Why is Claude driving me nuts? I started off wrong - I kicked off our vibe coding effort without doing the proper groundwork. Over &lt;a href=&quot;https://github.com/atomic14/harmless/graphs/commit-activity&quot;&gt;700 commits in&lt;/a&gt;, I decided that I couldn’t stand how Claude communicated and asked it to switch to ASD-STE100. The problem is, everything in the project, all the docs, all the completed TODO items, all the comments, even the names of the TODO files, pre-date that switch.&lt;/p&gt;

&lt;p&gt;Everything Claude looks at after it’s read CLAUDE.md undermines what I’m asking it to do. “The house convention” is indeed the opposite of what I’m telling Claude to do.&lt;/p&gt;

&lt;p&gt;Trying to correct course after creating a lot of code and documentation is like trying to turn a massive tanker - we’ve got way too much momentum. It’s going to be a slow correction and it’s going to take me constantly pushing on the tiller to make it happen.&lt;/p&gt;

&lt;p&gt;Time to roll up my sleeves and help Claude. My &lt;a href=&quot;https://github.com/atomic14/harmless&quot;&gt;vibe-coded Elite clone&lt;/a&gt; is going to need both of us.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-15/harmless.webp&quot; alt=&quot;Three pirates at Orerve, condition red - the combat computer earning its keep&quot; /&gt;&lt;/p&gt;
</description>
      <pubDate>Sun, 16 Aug 2026 12:00:00 +0000</pubDate>
      <link>https://www.atomic14.com/2026/08/16/its-me-hi-im-the-problem</link>
      <!-- guid kept as the original .html permalink so existing items are not re-flagged as new in feed readers -->
      <guid isPermaLink="true">https://www.atomic14.com/2026/08/16/its-me-hi-im-the-problem.html</guid>
        
    </item>
    
    <item>
      <title>Forget the Robot Tax. Tax the Tokens.</title>
      <description>&lt;p&gt;I read a paper this week called &lt;a href=&quot;https://papers.ssrn.com/sol3/papers.cfm?abstract_id=6448898&quot;&gt;The AI Layoff Trap&lt;/a&gt;. It has an interesting thesis.&lt;/p&gt;

&lt;p&gt;If you look at what is happening with automation and laying people off, and take it to the ultimate conclusion, no one will have a job. If that happens no one can afford to pay for a company’s goods or services. So, logically, it doesn’t make sense for a company to automate all the jobs and lay everyone off. If they do that, they have no customers.&lt;/p&gt;

&lt;p&gt;The paper argues that even if it doesn’t make sense, businesses have no choice but to do it. Automate and lay everyone off - and have no customers.&lt;/p&gt;

&lt;p&gt;When a company automates work and replaces people with AI, it keeps all of the savings. The laid off people have less money to spend, but not all companies are moving at the same speed, so there are still employed people who can spend money. The loss of demand from the people laid off is spread over all the companies providing the services.&lt;/p&gt;

&lt;p&gt;This leads to an arms race. Everyone agrees that mass layoffs will eventually hurt them, but nobody wants to be the one company that holds off whilst its competitors get cheaper.&lt;/p&gt;

&lt;p&gt;The paper suggests a &lt;a href=&quot;https://en.wikipedia.org/wiki/Pigouvian_tax&quot;&gt;Pigouvian tax&lt;/a&gt; on automation - charge companies for the cost they’re pushing onto everyone else when they swap human labour for machines.&lt;/p&gt;

&lt;p&gt;I think there’s a big problem with that - how do you measure automation?&lt;/p&gt;

&lt;p&gt;Say a company employs 10,000 people one year and 8,000 the next.&lt;/p&gt;

&lt;p&gt;How many of those 2,000 jobs went because of AI?&lt;/p&gt;

&lt;p&gt;The company will tell you it was restructuring. Or outsourcing. Or natural attrition. Or a soft market, or a merger, or just a bit of belated efficiency improvement - the old “we overhired and now we’re just correcting”.&lt;/p&gt;

&lt;p&gt;And things are never simple in business - nobody sits down and decides to make 2,000 people redundant for one clean, well-documented reason. There’s a whole bunch of horse trading and carve outs that makes it almost impossible to know the real reason.&lt;/p&gt;

&lt;p&gt;Working out which specific jobs were “lost to AI” turns into an accounting exercise, then a lobbying exercise, and finally a legal one. The elegant tax starts to look impossible to actually collect.&lt;/p&gt;

&lt;p&gt;Here’s the alternative: don’t tax the job losses, tax the AI.&lt;/p&gt;

&lt;p&gt;A very small levy on inference - tokens consumed, or more sensibly the compute needed to produce them - collected from the model providers and the big compute platforms. Nobody has to audit anybody’s HR decisions.&lt;/p&gt;

&lt;p&gt;It’s a blunt instrument. A company using AI to make its existing staff more productive pays the tax without laying off a single person. So do researchers, startups, and people doing things nobody has thought of yet.&lt;/p&gt;

&lt;p&gt;You lose precision and you gain something you can actually measure.&lt;/p&gt;

&lt;p&gt;The rate doesn’t need to be big either. The point isn’t to make AI expensive enough to stop automation happening - the productivity is real, and trying to hold it back would be both futile and a bit daft. Think of it as a small levy on something that’s quietly becoming a factor of production alongside land, labour and capital.&lt;/p&gt;

&lt;p&gt;The paper’s model has a nice property: the problem shrinks when displaced workers get more of their income back. Retraining, wage insurance, actually finding people new jobs - all of it reduces the hole in demand that automation creates.&lt;/p&gt;

&lt;p&gt;So the loop looks like this:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;AI usage → small compute tax → worker transition fund → more income replacement → less demand destruction&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The tax never has to work out which API call replaced which person. It just skims a little off the top of the activity that’s driving the transition and puts it towards helping people through it.&lt;/p&gt;

&lt;p&gt;What are the problems? And there are plenty of them…&lt;/p&gt;

&lt;p&gt;Open-source models and self-hosted inference are awkward to collect from. Offshore compute is an obvious escape hatch. And deciding what actually counts as taxable AI compute would keep a room full of lawyers busy for a good while.&lt;/p&gt;

&lt;p&gt;None of that is trivial. But it feels more tractable than asking a tax inspector to rule on whether a particular redundancy was caused by AI.&lt;/p&gt;

&lt;p&gt;A token tax would be theoretically wrong in all sorts of ways. But a slightly wrong tax on something you can measure is probably better than a theoretically perfect tax on something you can’t.&lt;/p&gt;

&lt;p&gt;What do you think? Let me know in the comments.&lt;/p&gt;
</description>
      <pubDate>Sun, 16 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://www.atomic14.com/2026/08/16/forget-the-robot-tax-tax-the-tokens</link>
      <!-- guid kept as the original .html permalink so existing items are not re-flagged as new in feed readers -->
      <guid isPermaLink="true">https://www.atomic14.com/2026/08/16/forget-the-robot-tax-tax-the-tokens.html</guid>
        
    </item>
    
    <item>
      <title>The Output Style, It Does Nothing! - Learning to live with Claude</title>
      <description>&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-15/the-goggles.webp&quot; alt=&quot;My eyes! The output style does nothing!&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Here’s a response Claude came up with recently. I was asking it to review some of the code it had been writing for my &lt;a href=&quot;https://github.com/atomic14/harmless&quot;&gt;“Open Vibe Code” Elite clone&lt;/a&gt; - you can &lt;a href=&quot;https://harmless.atomic14.com&quot;&gt;play it here in your browser&lt;/a&gt;, it’s surprisingly good.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Twenty-seven echoes; most are two halves of a seam stated from each side, which is correct. Four are true duplicates. Checking two of them:”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There’s a lot of this kind of output coming out of Claude. The internet is full of people complaining. Sometimes reading what Claude says is very much like being stuck at the bar with the world’s most boring know-it-all. Lots of clever phrases and convoluted sentences - but ultimately vacuous and empty.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-15/boring.webp&quot; alt=&quot;Being cornered in the pub by someone describing their seams&quot; /&gt;&lt;/p&gt;

&lt;p&gt;What’s really interesting, though, is that the code and docs it generates are pretty good (at least the tests are green, the game plays well etc…). It’s just the communication with me that’s the problem.&lt;/p&gt;

&lt;p&gt;A popular recommended remedy for this is to change the output style:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-15/output-style.webp&quot; alt=&quot;The output style picker in Claude Code, with ASD-STE100 added as a custom style&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Some people suggest using &lt;a href=&quot;https://en.wikipedia.org/wiki/Simplified_Technical_English&quot;&gt;ASD-STE100&lt;/a&gt; - Simplified Technical English. The theory is that this should force Claude into communicating with you in a sensible way.&lt;/p&gt;

&lt;p&gt;There’s a bit of controversy over whether you change the output style or you just add it to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt;, but whichever you choose, the result is supposed to be substantially better.&lt;/p&gt;

&lt;p&gt;Does it work?&lt;/p&gt;

&lt;p&gt;At first, absolutely. It feels better - easier to read, shorter sentences, fewer random, weird analogies.&lt;/p&gt;

&lt;p&gt;But I think that feeling of it being better is an illusion. You think it’s better because it’s different. It’s just a relief to your brain not having to parse Claude’s madness. But after a while you get used to the change and slowly start to realise that the prose is just as vacuous and empty as before.&lt;/p&gt;

&lt;p&gt;And the real kicker… the quote at the top of the page was produced &lt;em&gt;after&lt;/em&gt; I’d changed the output style and added it to my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt;…&lt;/p&gt;

&lt;p&gt;The big question is: Does it matter? And I suspect, probably not.&lt;/p&gt;

&lt;p&gt;I’m starting to think of Claude’s ramblings in the same way as the “Thinking…” content - it’s not necessarily meant for me to read. The actual output that matters is the working code. And to get quality there you need programmatic constraints and constant adversarial review.&lt;/p&gt;

&lt;p&gt;It’s time to make peace with Claude’s nonsense. Stop reading the prose and judge things purely by the produced artefacts - once you do that, the quality of the commentary stops mattering.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-15/shakespear.webp&quot; alt=&quot;It&apos;s not Shakespeare, but it&apos;s a pretty decent sonnet about bananas&quot; /&gt;&lt;/p&gt;

&lt;p&gt;So judge the artefact rather than the commentary. This is what all that vacuous prose actually built:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-15/harmless.webp&quot; alt=&quot;Three pirates at Orerve, condition red - the combat computer earning its keep&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://harmless.atomic14.com&quot;&gt;Harmless is playable in your browser&lt;/a&gt; - no install, nothing to sign up to, just open it and fly - and the &lt;a href=&quot;https://github.com/atomic14/harmless&quot;&gt;source is on GitHub&lt;/a&gt; if you want to see how it’s put together. Let me know how you get on in the comments.&lt;/p&gt;

</description>
      <pubDate>Sat, 15 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://www.atomic14.com/2026/08/15/output-style-does-nothing</link>
      <!-- guid kept as the original .html permalink so existing items are not re-flagged as new in feed readers -->
      <guid isPermaLink="true">https://www.atomic14.com/2026/08/15/output-style-does-nothing.html</guid>
        
    </item>
    
    <item>
      <title>Rescuing my 2006 Travel Blog</title>
      <description>&lt;p&gt;In November 2006 I put everything I owned in my parents’ loft, had a great many
injections, and flew to Guatemala. I came back thirteen months later having
crossed fourteen countries between Antigua and Tierra del Fuego, and I’d written
the whole thing up as it happened — 182 posts typed into internet cafés, hostel
lobbies and the occasional boat.&lt;/p&gt;

&lt;p&gt;That blog is still at &lt;a href=&quot;https://travellingchris.blogspot.com&quot;&gt;travellingchris.blogspot.com&lt;/a&gt;. Most of the photos are still on
Flickr. Both the blogging service and Flickr still work, but Flickr is now a paid service with limits.
And Blogspot is now blogger (or is it the other way round?). Flickr has
changed hands three times and now charges for more than 1000 photos.&lt;/p&gt;

&lt;p&gt;So I took exports from both and fed them into Claude: “Rebuild my old blog as a static site - here’s the blog export and here’s a bunch of photos”. And this is what I now have:
&lt;a href=&quot;https://travellingchris.atomic14.com/&quot;&gt;travellingchris.atomic14.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://travellingchris.atomic14.com/&quot;&gt;&lt;img src=&quot;/assets/article_images/2026-08-10/before-after.webp&quot; alt=&quot;The old Blogspot template beside the rebuilt archive&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The tech itself is pretty boring — Astro, static HTML, photos on Cloudflare
R2 - it should run for next to nothing. There were some interesting things along
the way.&lt;/p&gt;

&lt;h2 id=&quot;two-archives-to-reconcile-and-fix&quot;&gt;Two archives to reconcile and fix&lt;/h2&gt;

&lt;p&gt;Google Takeout gave me an Atom feed with 942 entries in it: 182 posts, 203 real
comments, and 557 spam comments accumulated over the last couple of decades. Flickr
gave me back my 2,819 photos with some JSON metadata.&lt;/p&gt;

&lt;p&gt;The two kind of match up, although I didn’t start uploading to Flickr until half
way through the trip.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Flickr&lt;/strong&gt; covers April 2007 onwards — South America — with capture dates and some tags.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The Blog photos&lt;/strong&gt; covers November 2006 to March 2007 — Central America.
Sadly, for that half of the trip, that’s all I have. And they are quite low resolution. I’m hoping somewhere I have a backup - but I haven’t found it yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;missing-photos&quot;&gt;Missing photos&lt;/h2&gt;

&lt;p&gt;Tracking down the photos for every posts was a bit of a mission. In all
there are 182 posts with 778 images in them.&lt;/p&gt;

&lt;p&gt;Some of these turned out to be photos hotlinked from other places. These are pretty much gone forever.&lt;/p&gt;

&lt;p&gt;Some of the earliest posts reference &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;photos1.blogger.com&lt;/code&gt;, the image host Blogger used in 2006. Google
Takeout didn’t include those files. But the host is still up and serving files
nineteen years on. Claude wrote a script and pulled thirteen
photographs back out of it - so those photos are saved! Though again, quite low resolution.&lt;/p&gt;

&lt;h2 id=&quot;dodgy-html&quot;&gt;Dodgy HTML&lt;/h2&gt;

&lt;p&gt;A fair number of the posts were broken in various ways - for example we’ve got
things like this:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://farm3.static.flickr.com/2229/1518290776_54f431e902.jpg?v=0&amp;lt;br /&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Claude is pretty good at this kind of things though and fell back to just finding
things that looked like URLs.&lt;/p&gt;

&lt;p&gt;After all this, we managed to get 766 photos out of the 778 image references. That’s not
bad for something that’s been rotting away for 19 years.&lt;/p&gt;

&lt;h2 id=&quot;fixing-the-overlap&quot;&gt;Fixing the overlap&lt;/h2&gt;

&lt;p&gt;The two archives overlap between April and June 2007, when I sometimes uploaded the
same photo on the blog and on Flickr. Those images were then resized by each service
and have different filename - so we end up with dupiclates.&lt;/p&gt;

&lt;p&gt;Claude’s initial attempt to fix this was a bit naive and didn’t work well.&lt;/p&gt;

&lt;p&gt;The problem is, the collection is full of similar pictures. Waterfalls and ancient ruins
can look almost like each other and there’s quite a few “burst” shots. Here are three pairs my first pass
happily merged:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-10/burst-frames.webp&quot; alt=&quot;Burst frames that are not duplicates&quot; /&gt;&lt;/p&gt;

&lt;p&gt;So Claude tried something else: cosine similarity between
mean-subtracted 32×32 greyscale thumbnails. 1,024 dimensions instead of 64 bits,
and blind to brightness and contrast. The numbers separate beautifully on this
collection:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt;score&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Burst frames (must &lt;strong&gt;not&lt;/strong&gt; merge)&lt;/td&gt;
      &lt;td&gt;≤ 0.9793&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Genuine re-encodes (must merge)&lt;/td&gt;
      &lt;td&gt;≥ 0.9993&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;That worked pretty well and we didn’t lose any photos.&lt;/p&gt;

&lt;h2 id=&quot;all-the-dates-are-wrong&quot;&gt;All the dates are wrong&lt;/h2&gt;

&lt;p&gt;None of the 2,819 Flickr photos has a geotag. These were all taken before
good mobile cameras were ubiquitus and I took them on all on a normal camera.&lt;/p&gt;

&lt;p&gt;There is no location data anywhere in either export.&lt;/p&gt;

&lt;p&gt;But there are 25,000 words of me saying where I was.&lt;/p&gt;

&lt;p&gt;So the locations come from the writing. Each post is matched against a the places it mentions - including all my misspellings (my spelling is attrocious).&lt;/p&gt;

&lt;p&gt;Photos actually embedded in a post are easy - every post has some kind of location
infomation in it. A photo in the Machu Picchu post is Machu Picchu, and it’s dated 1 September 2007.
That gives Claude 762 dated anchors scattered across the year. Using that, every remaining
photograph can be approximately placed by interpolating its own date against them.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://travellingchris.atomic14.com/map/&quot;&gt;&lt;img src=&quot;/assets/article_images/2026-08-10/route-map.webp&quot; alt=&quot;The route map&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two things Claude got wrong first time:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Post dates are not photo dates.&lt;/strong&gt; I blogged from internet cafés days after the
fact. “Back from the wilderness” was published a week after the photos in it were
taken. Using publish dates as location anchors put a whole trek in the wrong
place. A post’s own date has to only be used when it has no dated photos for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Upload dates are not capture dates.&lt;/strong&gt; The Blogger images have no EXIF
capture date, so we fell back to upload time — and a set of beach photos from
Mexico got placed in New York, because that’s where I was when I uploaded them
two days later. Photos published in a post need to take the post’s location directly
rather than going via the date timeline.&lt;/p&gt;

&lt;p&gt;Every photograph carries a &lt;a href=&quot;https://travellingchris.atomic14.com/about/&quot;&gt;confidence level&lt;/a&gt;: 762 certain,
2,220 confident, 119 inferred. It could be better, but it’s good enough. And I’m not
going to hand curate 2000+ photos…&lt;/p&gt;

&lt;h2 id=&quot;there-are-a-lot-of-beaches---all-called-beachjpg&quot;&gt;There are a lot of beaches - all called beach.jpg…&lt;/h2&gt;

&lt;p&gt;This one I spotted after Claude has finished - haven’t I seen that beach already on a previous post?&lt;/p&gt;

&lt;p&gt;In 2007 I uploaded a photo, called it &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;beach.jpg&lt;/code&gt;, and posted it. Three weeks
later I did exactly the same thing. Seven times over seven weeks.&lt;/p&gt;

&lt;p&gt;Google Takeout disambiguates the &lt;em&gt;files&lt;/em&gt; — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;beach.jpg&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;beach(1).jpg&lt;/code&gt;, through
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;beach(6).jpg&lt;/code&gt; — but the URL inside each post carries only the original name. So
if you resolve photos by filename, as Claude did, all seven posts get the same
photograph and the other six are orphaned. Thirty-four filenames in the album
collide this way.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-10/seven-beaches.webp&quot; alt=&quot;Seven different photographs, all uploaded as beach.jpg&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Each photo needed to include its upload date to disambiguate it.&lt;/p&gt;

&lt;p&gt;There were a couple of other issues like this, including two photos called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;garden.jpg&lt;/code&gt;.
My london flat did not have a tropical garden…&lt;/p&gt;

&lt;p&gt;Fixing both took the number of distinct photographs actually visible in posts
from 723 to 762.&lt;/p&gt;

&lt;h2 id=&quot;more-photos&quot;&gt;More photos!&lt;/h2&gt;

&lt;p&gt;I fond another stash of photos on a CD-ROM I’d burned as backup in June 2007. These
were all original photos straight off the camera’s SDCard 3072x2304 and covered
April and May. I’m pretty sure that somewhere there’s a nother CD with more photos.
But these ones covered Ecuador and the Galápagos.&lt;/p&gt;

&lt;p&gt;The obvious question: are these already in the archive, or are they new? By hand
that’s 145 photos to match up against 3,073, which is not happening. But Claude had already built a
matching tool so it was pretty easy to check.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Matched something in the archive&lt;/td&gt;
      &lt;td&gt;127 photos, all scoring ≥ 0.995&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Closest non-match&lt;/td&gt;
      &lt;td&gt;0.7537&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;All the 127 matches were photographs that had been uploaded to Flickr as a 1600px copy - and one of them
that had survived only as a 640px thumbnail. The remaining 18 were never uploaded anywhere.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-10/resolution-gain.webp&quot; alt=&quot;The same frame from the Flickr copy and the CD-ROM original&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;even-more-photos&quot;&gt;Even more photos&lt;/h2&gt;

&lt;p&gt;I thought I’d lost all the Death Road photos — mountain biking down the Yungas Road. They’d been 
hotlinked from the tour company’s Flickr account, which no longer exists.&lt;/p&gt;

&lt;p&gt;But - some of the photos were on the backup CD-ROM! So there is some record of my madness.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-10/death-road.webp&quot; alt=&quot;The Death Road, from the tour company&apos;s own CD&quot; /&gt;&lt;/p&gt;

&lt;p&gt;They don’t quite match up to the original photos I’d put in the blog, so Claude decided that we
should be honest and put a note in their place:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;A photograph stood here. It was hotlinked from an account that no longer
exists, and is in neither export. Other photographs from these days did
survive — 18 of them are in the gallery.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;267-photographs-with-strange-orientations&quot;&gt;267 photographs with strange orientations&lt;/h2&gt;

&lt;p&gt;Some frames in the archive are rotated 90 degrees.&lt;/p&gt;

&lt;p&gt;Claudes first attempt was a simple “find the sky and check it is at the top.”&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Take a strip off each of the four edges, score each strip for brightness and blueness, and
the winning edge should be the top. When it is decisively the left or the right,
the frame is on its side.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This didn’t work very well:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Upright photographs wrongly flagged&lt;/td&gt;
      &lt;td&gt;4%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Turned photographs caught&lt;/td&gt;
      &lt;td&gt;38%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;…and turned back the right way&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;97%&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;No verdict at all&lt;/td&gt;
      &lt;td&gt;802 photographs&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Claude tried a bunch of other things: &lt;strong&gt;upright scenes carry
more horizontal structure than vertical&lt;/strong&gt; — horizons, walls, tables, waterlines
— so a 90 degree turn should swap the balance of edge gradients. It is cited
everywhere.&lt;/p&gt;

&lt;p&gt;On this collection: 56% accuracy overall, we’d have been better off tossing a coin.&lt;/p&gt;

&lt;h3 id=&quot;using-some-proper-machine-learning&quot;&gt;Using some proper machine learning&lt;/h3&gt;

&lt;p&gt;After a bit of googling, I found this model: &lt;a href=&quot;https://pypi.org/project/check-orientation/&quot;&gt;check-orientation&lt;/a&gt;,
a ResNeXt-50 trained on four rotations. Way better results:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt;heuristic&lt;/th&gt;
      &lt;th&gt;model&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Flagged&lt;/td&gt;
      &lt;td&gt;163&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;267&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Finds 180° flips&lt;/td&gt;
      &lt;td&gt;no&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt; (8 of them)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Works indoors, at night&lt;/td&gt;
      &lt;td&gt;no&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Abstains on&lt;/td&gt;
      &lt;td&gt;802&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;none&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Claude checked the results by looking at the flagged images and every one of the 162
photographs it found was genuinely on its side. It runs on Apple Silicon in about a
minute for the whole archive.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-10/orientation.webp&quot; alt=&quot;Photographs as the archive stored them, and after correction&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;267 photographs, 8.6% of the archive, were lying on their side.&lt;/strong&gt; Four of the
six above are cases the simple sky heuristic could never have caught.&lt;/p&gt;

&lt;h2 id=&quot;these-photos-are-rubbish&quot;&gt;These photos are rubbish…&lt;/h2&gt;

&lt;p&gt;Next step was to actually do some QA on the site and the first thing I noticed
was that there were several places where images were needed to “represent” something:
a country, a period of the trip etc.., and the simple “take the first picture in the set”
approach didn’t reallty work.&lt;/p&gt;

&lt;p&gt;Peru, with 1,612 photographs, was represented by a picture of a passport stamp.
Interesting, but not exactly a “hero” shot.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-10/cover-choice.webp&quot; alt=&quot;The first photograph of a country, and the best one&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;what-it-costs-to-run&quot;&gt;What it costs to run.&lt;/h2&gt;

&lt;p&gt;The whole thing is static HTML on Cloudflare Pages with 2 GB of photos in R2.&lt;/p&gt;

&lt;p&gt;It should cost pennies to serve.&lt;/p&gt;

&lt;p&gt;Will it still be live in 19 years time? Maybe I’ll be writing another blog post with whatever the latest AI is
about how I rescued my old travelling posts…&lt;/p&gt;

&lt;h2 id=&quot;who-did-all-the-work&quot;&gt;Who did all the work?&lt;/h2&gt;

&lt;p&gt;Claude, obviously. The current crop of LLMs seem to excel at this kind of static site generation and data extraction. My only input was a few idea and a bit of helpful googling.&lt;/p&gt;

&lt;p&gt;What a time to be alive!&lt;/p&gt;
</description>
      <pubDate>Mon, 03 Aug 2026 12:00:00 +0000</pubDate>
      <link>https://www.atomic14.com/2026/08/03/rescuing-a-2006-travel-blog</link>
      <!-- guid kept as the original .html permalink so existing items are not re-flagged as new in feed readers -->
      <guid isPermaLink="true">https://www.atomic14.com/2026/08/03/rescuing-a-2006-travel-blog.html</guid>
        
    </item>
    
    <item>
      <title>I Asked AI to Design 12 Dev Boards. It Told Me to Write a Script.</title>
      <description>&lt;p&gt;I keep needing the same thing - a really basic dev board for whatever Espressif
module I’m playing with. USB-C, a 3.3V regulator, BOOT and EN buttons, three
LEDs (5V present, 3.3V present, and one on a GPIO so you can run the blink
sketch), and every GPIO broken out on headers. Nothing clever, just the minimum
board that gets a bare module doing something.&lt;/p&gt;

&lt;p&gt;While building &lt;a href=&quot;https://www.atomic14.com/esp32/&quot;&gt;the ESP32 module database&lt;/a&gt; I
noticed there are twelve modules that fit one sensible recipe - a PCB antenna
and native USB - spread across the S2, S3, C3, C5, C6 and H2 families. Native
USB is the bit that keeps the component count down. There’s no USB to UART
bridge to buy and route, just a USB-C socket and a couple of 5.1K resistors on
the CC pins so the host actually hands over power.&lt;/p&gt;

&lt;p&gt;Twelve near-identical boards is exactly the kind of job that makes you question
your life choices in KiCad.&lt;/p&gt;

&lt;p&gt;So I did what everyone on the internet is currently telling you to do and asked
Claude to spin up a team of AI agents to design the boards for me.&lt;/p&gt;

&lt;p&gt;And it said no.&lt;/p&gt;

&lt;p&gt;Not a flat no, more of a “this is beneath me” no. Its argument, roughly - this
is a mechanical, repetitive job, and you don’t want a swarm of agents
improvising twelve layouts. What you want is deterministic scripts that produce
the same board every time, with AI involved only where there’s a real judgement
call to make.&lt;/p&gt;

&lt;p&gt;Which is a surprisingly sensible thing for it to say. So I let it write the
scripts instead, and the AI’s contribution to each &lt;em&gt;board&lt;/em&gt; shrank down to a
single file. More on that below.&lt;/p&gt;

&lt;lite-youtube videoid=&quot;gt-0t01COPw&quot; playlabel=&quot;I Asked AI to Design 12 Dev Boards&quot;&gt;&lt;/lite-youtube&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-03/montage_top_scale.webp&quot; alt=&quot;All 12 boards, to scale&quot; /&gt;&lt;/p&gt;

&lt;p&gt;That montage is to scale - the render pipeline normalises the zoom so a
29×33 mm C3-MINI board looks properly tiny next to a 28×55 mm S2-MINI.&lt;/p&gt;

&lt;p&gt;The whole thing is on GitHub at
&lt;a href=&quot;https://github.com/atomic14/kicad-esp32-dev-boards&quot;&gt;atomic14/kicad-esp32-dev-boards&lt;/a&gt;,
with fab-ready Gerbers and openable KiCad projects for all twelve boards on the
&lt;a href=&quot;https://github.com/atomic14/kicad-esp32-dev-boards/releases&quot;&gt;releases page&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;so-whats-left-for-a-human-to-do&quot;&gt;So what’s left for a human to do?&lt;/h2&gt;

&lt;p&gt;Almost all of the pipeline is just deterministic scripting. The only real
judgement call is a small &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;board.yaml&lt;/code&gt; per module, and it holds the things a
script can’t work out for itself from the symbol and footprint:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Which pins must not be broken out. This is sneakier than it sounds, because
it’s symbol specific rather than name based - &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SPIIO6&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SPIIO7&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SPIDQS&lt;/code&gt;
are reserved for the octal flash on an S3-WROOM-1, but the same names are
perfectly usable GPIO on the quad S3-MINI-1.&lt;/li&gt;
  &lt;li&gt;The boot-strap pin that the BOOT button pulls low. GPIO0 on the Xtensa parts,
GPIO9 on most of the RISC-V parts, and GPIO28 on the C5, because nothing is
ever completely consistent…&lt;/li&gt;
  &lt;li&gt;The strapping and input only pins to steer the on-board LED away from. Given
that list the generator picks the LED pin itself - the safe pad physically
nearest the LED on the laid out board, so the trace stays nice and short.
Hang the LED off an input only pin and it simply never lights up, so the
build hard-errors rather than let that ship.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-03/board-yaml.webp&quot; alt=&quot;The whole hand-written input for one board&quot; /&gt;&lt;/p&gt;

&lt;p&gt;That’s the whole hand written input for a board. Everything else gets derived
from the module’s symbol and footprint - power, GND, EN, the USB D+/D- pair,
the GPIO net labels.&lt;/p&gt;

&lt;p&gt;And this file is where the AI actually lives. All those judgement calls are
already written up in plain English on each module’s page of
&lt;a href=&quot;https://www.atomic14.com/esp32/&quot;&gt;the module database&lt;/a&gt; - for humans. So the job
I hand to the AI is a simple one: read the page, fill in the file. Messy prose
in, clean data out, which is the one thing a script can’t do.&lt;/p&gt;

&lt;p&gt;It works under a safety net too. If it picks an unsafe pin for the LED then the
build hard-errors and refuses to generate the board at all.&lt;/p&gt;

&lt;h2 id=&quot;so-how-does-the-generator-work&quot;&gt;So how does the generator work?&lt;/h2&gt;

&lt;p&gt;Each board starts from a pre-built skeleton project - the USB-C connector, LDO,
buttons and LEDs, already placed, routed and sanity checked.&lt;/p&gt;

&lt;p&gt;There are actually &lt;em&gt;two&lt;/em&gt; skeletons, mirror images of each other, one with EN on
the left and BOOT on the right and one the other way round. That one came out
of playing about with the AI. Depending on which edge a module’s reset pin
comes out on, putting the matching button on that side makes the routing much
easier, so the generator has a look at the footprint and picks whichever
skeleton fits.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-03/two-skeletons.webp&quot; alt=&quot;Spot the difference: the two skeletons, same parts, mirrored buttons&quot; /&gt;&lt;/p&gt;

&lt;p&gt;From there the script does four things:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Builds the schematic with &lt;a href=&quot;https://github.com/psychogenic/kicad-skip&quot;&gt;kicad-skip&lt;/a&gt;,
dropping in the module symbol and two break-out headers, wired up with net
labels.&lt;/li&gt;
  &lt;li&gt;Places the PCB. USB-C at the bottom edge with the buttons either side of it,
support components above that, and the module at the top with the antenna
hanging over the edge, as the datasheets demand. The two headers run up the
left and right sides, split by a perimeter walk of the module’s &lt;em&gt;physical&lt;/em&gt;
pin edges, so every GPIO lands on the nearest accessible header pin instead
of dragging a track right across the board.&lt;/li&gt;
  &lt;li&gt;Autoroutes with &lt;a href=&quot;https://github.com/drandyhaas/KiCadRoutingTools&quot;&gt;KiCadRoutingTools&lt;/a&gt;
(I run &lt;a href=&quot;https://github.com/atomic14/KiCadRoutingTools&quot;&gt;a small fork&lt;/a&gt; that
keeps vias out of same-net pads). USB D+/D- goes down as a proper coupled
differential pair, 0.3 mm centre to centre for 92 to 96% of the run on every
board. Every signal is 0.15 mm/0.15 mm, power traces are a wide 0.4 mm, and
the whole lot sits under a GND pour with stitching vias. Two layers only,
because two layer boards are the cheap ones. All twelve come out fully
connected and DRC clean.&lt;/li&gt;
  &lt;li&gt;Exports the 3D renders (including the to-scale montages in this post), a
validation PDF, and a flat-zipped Gerber+drill package ready to upload to
JLCPCB or PCBWay.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-03/diff-pair.webp&quot; alt=&quot;The USB pair routed as a coupled differential pair&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The back of each board gets the pin names in silk next to the headers, and one
little touch I’m quite pleased with - a vertical identifier down the middle
with the module name and the git revision that generated it. That’s just
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git describe&lt;/code&gt; baked into the silkscreen. Tag the repo &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;v1.0&lt;/code&gt; and every board
fabbed from that tag says so.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-03/version-stamp.webp&quot; alt=&quot;The version stamp in silkscreen down the back of the board&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-03/montage_bottom_scale.webp&quot; alt=&quot;The backs of all twelve, with pin labels and the version stamp&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;getting-it-all-to-run-in-ci&quot;&gt;Getting it all to run in CI&lt;/h2&gt;

&lt;p&gt;Every push builds all twelve boards on GitHub Actions, and pushing a version
tag publishes a release with the Gerber zips, KiCad projects and montage
renders attached. It all runs inside KiCad’s official Docker image, which is
the supported way to run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kicad-cli&lt;/code&gt; headless.&lt;/p&gt;

&lt;p&gt;Even then, three things bit me. They might save you an afternoon.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kicad-cli&lt;/code&gt; segfaulted on startup - even for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kicad-cli version&lt;/code&gt;. It turned
out to be crashing while it scanned the Plugin &amp;amp; Content Manager addon
directory, where the Espressif libraries live, when the global library
tables don’t exist yet. Which is true on any machine where the KiCad GUI has
never been run. The fix is to seed empty &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sym-lib-table&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fp-lib-table&lt;/code&gt;
files first.&lt;/li&gt;
  &lt;li&gt;The zone fills silently didn’t happen. The GND pour is filled by a pcbnew
Python script, and “python3” inside a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt; environment resolves to the venv
interpreter, which has no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pcbnew&lt;/code&gt; module. Every board came out “routed” but
with an empty ground plane and a pile of DRC errors. The fix is to use the
python that lives next to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kicad-cli&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git describe&lt;/code&gt; refused to run at all, throwing git’s “dubious ownership”
error, because the Actions workspace is bind mounted into the container
under a different uid. The boards built fine, but the version stamp quietly
fell back to just the module name. One &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git config --global --add
safe.directory&lt;/code&gt; sorts it out, and there’s now a canary step so it can never
fail quietly again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-08-03/ci-run.webp&quot; alt=&quot;A green build: all twelve boards routed, rendered and packaged in one job&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;getting-the-boards---but-do-your-own-checks&quot;&gt;Getting the boards - but do your own checks&lt;/h2&gt;

&lt;p&gt;The Gerber zips and full KiCad projects for all twelve boards are on the
&lt;a href=&quot;https://github.com/atomic14/kicad-esp32-dev-boards/releases&quot;&gt;releases page&lt;/a&gt;,
ready to upload to your board house of choice.&lt;/p&gt;

&lt;p&gt;One honest caveat before you spend any money - these boards are machine
generated. They pass DRC with zero errors, they add no new ERC errors against
the skeleton, and I do eyeball every render, but I haven’t yet had physical
copies of all twelve back from a fab and tested them.&lt;/p&gt;

&lt;p&gt;So treat them like any open source hardware design - trust, but verify. Open
the KiCad project, sanity check the schematic against the
&lt;a href=&quot;https://www.atomic14.com/esp32/&quot;&gt;module’s page&lt;/a&gt;, and have a proper look at the
board before you order. Which is good practice for &lt;em&gt;any&lt;/em&gt; design you download
off the internet, very much including mine…&lt;/p&gt;

&lt;p&gt;I am going to get a batch of these made up, so let me know in the comments
which ones you’d like to see. And if you do build one, I’d genuinely love to
hear how it goes. If you find a problem, file an issue on the
&lt;a href=&quot;https://github.com/atomic14/kicad-esp32-dev-boards&quot;&gt;repo&lt;/a&gt; so the fix lands in
the generator rather than just on one board.&lt;/p&gt;

&lt;lite-youtube videoid=&quot;gt-0t01COPw&quot; playlabel=&quot;I Asked AI to Design 12 Dev Boards&quot;&gt;&lt;/lite-youtube&gt;

&lt;div class=&quot;esp32-cta&quot;&gt;
  &lt;p&gt;&lt;strong&gt;Picking a module for your own project?&lt;/strong&gt; I&apos;ve built an interactive ESP32 reference with specs and pinouts for every Espressif module: browse the &lt;a href=&quot;https://www.atomic14.com/esp32/&quot;&gt;module database&lt;/a&gt;, or &lt;a href=&quot;https://www.atomic14.com/esp32/table/&quot;&gt;compare them all in one big table&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
</description>
      <pubDate>Mon, 03 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://www.atomic14.com/2026/08/03/twelve-dev-boards-from-one</link>
      <!-- guid kept as the original .html permalink so existing items are not re-flagged as new in feed readers -->
      <guid isPermaLink="true">https://www.atomic14.com/2026/08/03/twelve-dev-boards-from-one.html</guid>
        
    </item>
    
  </channel>
</rss>
