<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[LK Forge]]></title><description><![CDATA[Engineering notes from building LK Forge — free browser games and tools]]></description><link>https://lkforge.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>LK Forge</title><link>https://lkforge.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 25 Sep 2026 03:04:43 GMT</lastBuildDate><atom:link href="https://lkforge.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Proving It's Actually Unbeatable: How I Benchmark a Game AI Before Publishing a Number]]></title><description><![CDATA[This was originally published on the LK Forge blog, where the charts are interactive and you can play the AI it talks about.
"Unbeatable" is a testable claim, not a marketing word. Before that word go]]></description><link>https://lkforge.hashnode.dev/proving-it-s-actually-unbeatable-how-i-benchmark-a-game-ai-before-publishing-a-number</link><guid isPermaLink="true">https://lkforge.hashnode.dev/proving-it-s-actually-unbeatable-how-i-benchmark-a-game-ai-before-publishing-a-number</guid><category><![CDATA[AI]]></category><category><![CDATA[GameDev]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[benchmarking]]></category><dc:creator><![CDATA[Lucian (LKB)]]></dc:creator><pubDate>Mon, 20 Jul 2026 20:15:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a53a7d752c8743b948449b8/b4f370af-3a87-4866-8967-a23baf80a03b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This was originally published on <a href="https://lkforge.com/blog/benchmarking-game-ai/">the LK Forge blog</a>, where the charts are interactive and you can play the AI it talks about.</p>
<p>"Unbeatable" is a testable claim, not a marketing word. Before that word goes anywhere near the <a href="https://lkforge.com/games/tictactoe/">tic-tac-toe page</a>, the shipped AI has to survive a headless self-play harness — the same functions the browser runs, played thousands of times with no human at the keyboard. Here is what that harness measured, including the part that didn't look good at first.</p>
<h2>The counterintuitive result</h2>
<p>A tic-tac-toe AI with an <strong>87% win rate still lost games</strong> that the full-depth engine never would. "It's minimax, so it's unbeatable" is true at exactly one depth setting — and the only way to find which is to run every depth and count the losses.</p>
<h2>The numbers</h2>
<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody><tr>
<td>Tic-tac-toe games simulated</td>
<td>1,200</td>
</tr>
<tr>
<td>Losses recorded</td>
<td>0</td>
</tr>
<tr>
<td>2048 self-play runs</td>
<td>250</td>
</tr>
<tr>
<td>Nodes / opening (α-β)</td>
<td>36,528</td>
</tr>
</tbody></table>
<p>Against 1,000 games of a random-move opponent, the AI went 825 wins, 175 draws, <strong>0 losses</strong>. Against 200 games of the AI playing a perfect copy of itself, every game ended in a draw — the correct result for perfect tic-tac-toe play, and the harder claim to satisfy. Across both runs, 1,200 games total, the loss count is 0. That is the number the word "unbeatable" is actually standing on.</p>
<h2>The harness</h2>
<p>The benchmark is headless self-play: the exact move-selection functions from the shipped page, imported into a script with no DOM and no human, playing thousands of games back to back. A benchmark written against a <em>re-implementation</em> of the algorithm proves the algorithm is sound in theory; it says nothing about the code a browser actually executes. Pulling the real functions out of the production page means the numbers describe the shipped build, not a clean-room stand-in that happens to share a name with it.</p>
<p>It also has to be re-runnable. A number from one afternoon of manual play-testing is an anecdote; a script that plays 1,000 games and produces the same counts every time is a measurement. Mean move time came out at about 0.3 ms across 500 sampled positions.</p>
<h2>Depth is the proof</h2>
<p>The shipped engine searches to depth 9 — the full game tree for a 3×3 board — which is what guarantees the zero-loss result. To find out whether that depth is <em>necessary</em>, the harness re-ran the same 200-game vs-random test with the search capped at each depth from 1 to 9:</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/6a53a7d752c8743b948449b8/3e81929d-8019-4edd-9f6a-1673da8435b1.png" alt="chart-depth" /></p>
<table>
<thead>
<tr>
<th>Search depth</th>
<th>Win % (vs random, 200 games)</th>
<th>Losses</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>86%</td>
<td>0</td>
</tr>
<tr>
<td>2</td>
<td>87%</td>
<td><strong>2</strong></td>
</tr>
<tr>
<td>3</td>
<td>90.5%</td>
<td>0</td>
</tr>
<tr>
<td>4</td>
<td>86%</td>
<td>0</td>
</tr>
<tr>
<td>5</td>
<td>83.5%</td>
<td>0</td>
</tr>
<tr>
<td>6</td>
<td>77%</td>
<td>0</td>
</tr>
<tr>
<td>7</td>
<td>85%</td>
<td>0</td>
</tr>
<tr>
<td>8</td>
<td>79%</td>
<td>0</td>
</tr>
<tr>
<td>9 (shipped)</td>
<td>81%</td>
<td>0</td>
</tr>
</tbody></table>
<p>The honest finding is in the depth-2 row: at a search depth of 2, the AI <strong>lost 2 of 200 games</strong>. An 87% win rate looks fine on its own, until you notice the two losses hiding next to it. Depth 9 is the only depth with a 0-loss guarantee behind it; every shallower depth is a different, weaker program that happens to share the same UI.</p>
<p>That is the entire argument for benchmarking instead of reasoning from the algorithm's name. "It's minimax, so it's unbeatable" is true only at one specific depth setting, and the only way to know which is to run all of them and look for the losses.</p>
<h2>The 2048 case</h2>
<p>Tic-tac-toe has a small enough state space that "unbeatable" is checkable in an absolute sense. Most games don't — <a href="https://lkforge.com/games/2048/">2048</a> has no realistic path to "solved," so the only honest claim is a measured reach-rate: 250 headless self-play games with the shipped expectimax-plus-corner-snake solver.</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/6a53a7d752c8743b948449b8/abfefc1a-62b3-4ce9-89cf-ba9bb2257a92.png" alt="chart-2048" /></p>
<p>A strength profile, not a single number — and one that would be indistinguishable from a coin flip if I'd run it 10 times instead of 250.</p>
<h2>Reproduce it yourself</h2>
<p>The harness is public and seeded, so runs are deterministic. Each file copies the shipped game code verbatim and drives it headlessly:</p>
<p><a href="https://gist.github.com/lucian-devops/3de1641f4adb831ff9b8827acd8eb6d5"><strong>The benchmark harness (public gist)</strong></a></p>
<pre><code class="language-plaintext">node tictactoe-benchmark.mjs   # 549,945 -&gt; 36,528 nodes, 0 losses, depth-2 loses 2
node 2048-benchmark.mjs        # reach-rate profile
</code></pre>
<p>The deterministic numbers (node counts, 0 losses at full depth, "only depth 2 loses") reproduce to the exact digit. The stochastic ones (win/draw split, 2048 reach %) reproduce as the same profile with seed-dependent variance. I also included the one number that <em>didn't</em> cleanly reproduce — a Color Lines pathfinding stat that turned out to be board-sampling-dependent — and documented why, rather than curve-fitting to hit it.</p>
<h2>The rule this enforces</h2>
<p>Benchmark before you publish a number. Any performance or strength claim has to come from a measurement that can be re-run, not a description of what the algorithm should do in theory. The depth-2 result is exactly the kind of thing that rule is for — a plausible-sounding claim ("it's minimax, it can't lose") that turns out to be depth-dependent, and would have shipped wrong without a script that actually played the games.</p>
]]></content:encoded></item><item><title><![CDATA[26 Repos in 29 Days With an AI Pipeline: What Actually Broke]]></title><description><![CDATA[This was originally published on the LK Forge blog, where the commit chart is interactive and you can play the AI games it talks about.

Between June 5 and July 3, 2026, I took an empty domain to 26 r]]></description><link>https://lkforge.hashnode.dev/26-repos-in-29-days-with-an-ai-pipeline-what-actually-broke</link><guid isPermaLink="true">https://lkforge.hashnode.dev/26-repos-in-29-days-with-an-ai-pipeline-what-actually-broke</guid><category><![CDATA[AI]]></category><category><![CDATA[webdev]]></category><category><![CDATA[Productivity]]></category><dc:creator><![CDATA[Lucian (LKB)]]></dc:creator><pubDate>Sun, 12 Jul 2026 15:12:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a53a7d752c8743b948449b8/21e63256-2182-4860-ae22-99f421baf096.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>This was originally published on <a href="https://lkforge.com/blog/26-repos-in-29-days/">the LK Forge blog</a>, where the commit chart is interactive and you can play the AI games it talks about.</p>
</blockquote>
<p>Between June 5 and July 3, 2026, I took an empty domain to <strong>26 repositories, 1,549 commits, and 335 live pages</strong> — one developer, working with Claude Code. The interesting part isn't the volume. It's <em>which</em> failure modes showed up, because none of them were the ones the AI-coding debate argues about.</p>
<h2>The numbers</h2>
<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody><tr>
<td>Repositories</td>
<td>26</td>
</tr>
<tr>
<td>Days</td>
<td>29</td>
</tr>
<tr>
<td>Commits</td>
<td>1,549</td>
</tr>
<tr>
<td>Pages shipped</td>
<td>335</td>
</tr>
</tbody></table>
<p>Commit cadence: <strong>~53/day average</strong>, <strong>peak 122 on June 20</strong>, exactly <strong>one</strong> zero-commit day in the sprint. The two heaviest days at the tail (114 each) were a site-wide URL-structure migration — which is itself one of the failure stories below.</p>
<h2>The setup</h2>
<p>Deliberately boring: one developer, Claude Code in the terminal, git for everything, static-first architecture. Games and tools are self-contained repos of vanilla HTML/CSS/JS; the hub site is Astro. Everything deploys to Cloudflare Workers with static assets at the edge — no backend, no database, nothing to babysit at 3am.</p>
<p>Every session ran the same loop: describe the goal, let the model plan and build, review the diff, make it verify its own work against the live site, commit. That verification step is what earns its keep — every failure below was caught by a check, not by luck.</p>
<h2>What the pipeline was genuinely good at</h2>
<p><strong>Working classical-AI engines, first try or close.</strong> The 2048 solver is real expectimax search with a corner-snake heuristic and adaptive depth. Before publishing any performance number, I ran the exact production code through 250 headless self-play games: it reaches the 2048 tile in <strong>69.6% of games at ~0.5ms per move</strong>. The tic-tac-toe opponent is minimax with alpha-beta; the pathfinding in Color Lines is BFS. Textbook algorithms, correctly implemented, shipped in days. That part of the hype is real.</p>
<p><strong>Volume with consistency.</strong> 335 pages sharing one brand system, one URL convention, one schema pattern. Once a convention was written into a project memory file, the model applied it across dozens of pages without drift. Those memory files turned out to be the highest-leverage artifact in the whole pipeline.</p>
<p><strong>Audits at a depth a human won't sustain.</strong> Full link-graph crawls, redirect-chain verification across hundreds of URLs, per-page canonical checks against live HTTP. The model does the 400-URL tedium without getting bored — which matters, because tedium is where site-wide bugs hide.</p>
<h2>What actually broke</h2>
<p>Not one failure was a syntax error, a broken build, or code that didn't run. Every real problem was <strong>structural</strong> — invisible in any single diff, only visible when you look at the whole system.</p>
<h3>1. The pipeline competed with itself</h3>
<p>Asked for a word-tools hub, the pipeline built one — at <code>/word-tools/</code>, while the existing tools lived under <code>/tools/</code>. Two pages on the same domain targeting the same queries: textbook SEO cannibalization, self-inflicted. Search Console showed both URLs impressing for the same terms before I consolidated with a 301. Each page was locally correct; nobody was watching the query-level picture.</p>
<p><strong>Lesson: the model optimizes the page you asked for, not the site you already have.</strong></p>
<h3>2. Twenty-six repos, two URL conventions, weeks of cleanup</h3>
<p>Some tools were built as flat files (<code>page.html</code>), others as directories (<code>page/index.html</code>). On Cloudflare's asset serving those get opposite trailing-slash behavior — so the site accumulated canonical mismatches, two-hop redirect chains, and Search Console redirect errors. The fix consumed the two biggest commit days of the sprint and produced a written URL convention plus a pre-deploy crawl checker that now gates every release.</p>
<p><strong>Lesson: conventions the model must follow have to be written before repo #2, not after repo #20.</strong></p>
<h3>3. Source and production drifted apart silently</h3>
<p>Game repos are mirrored into the hub site for deployment. Over weeks, SEO improvements were applied to the production mirror and never back-ported to source. The trap armed itself: the obvious "sync" — copy source over mirror — would have silently destroyed live metadata. It was caught only because a diff-before-copy check is now mandatory.</p>
<p><strong>Lesson: any two copies of the same file will diverge, and the AI won't notice unless a check forces the comparison.</strong></p>
<h3>4. The verification tools lied too</h3>
<p>The first link-graph audit reported a wave of orphaned pages. False alarm: it compared absolute URLs against unresolved relative hrefs. A later canonical audit reported 123 mismatches — also false, because the checker assumed file paths equal serving paths, and the CDN serves clean URLs. In both cases the <em>audit tooling</em> — also AI-written — had the bug, and acting on its output would have "fixed" a healthy site into a broken one. Both caught the same way: probe the live site before believing static analysis.</p>
<p><strong>Lesson: verify the verifier. An AI-written check inherits every blind spot of the AI that wrote it.</strong></p>
<h2>The economics: 93% of the cost was re-reading, not writing</h2>
<p>The sprint produced 44 working sessions and 826MB of transcripts. When I audited the token bill, the headline wasn't generation cost: <strong>roughly 93% of token consumption was cached context being re-read</strong>, turn after turn, inside marathon sessions that should have been split up.</p>
<p>The mechanics are mundane. A long session accumulates giant context; every subsequent turn re-reads it; a session that drifts across three unrelated tasks pays the full history of tasks one and two as a tax on task three. The model never complains, so nothing forces you to notice.</p>
<p>The fix cost nothing: clear context between tasks, keep durable knowledge in small memory files, treat "one session = one task" as the default. If you run an AI coding workflow and have never audited where the tokens actually go, that single check is probably worth more than any prompt engineering.</p>
<h2>What it earned (honest version)</h2>
<p>Over the last 28 days the site drew <strong>207 clicks from 7,320 impressions</strong> across 257 pages with search data. For a domain about five weeks old, that's a normal, healthy trajectory — and nobody's growth-hack screenshot.</p>
<p>The detail worth reporting: the single biggest click-earner after the homepage is the 2048 game with the visible AI solver — the page where the most genuine engineering lives. Search demand followed the depth, not the page count. 300 thin pages didn't beat one page with something real on it.</p>
<h2>The rules that survived</h2>
<p>Every one exists because its absence caused a real incident above. That's the only rule-making process that works.</p>
<ol>
<li><p><strong>Benchmark before you publish a number.</strong> If it isn't measured, it doesn't ship.</p>
</li>
<li><p><strong>Diff before you copy.</strong> No exceptions for "they should be identical."</p>
</li>
<li><p><strong>Probe live before believing static analysis.</strong> An audit result is a hypothesis until production confirms it over HTTP.</p>
</li>
<li><p><strong>Write conventions down before scaling them</strong> — in a memory file the model loads every session, not in your head.</p>
</li>
<li><p><strong>One session, one task.</strong> Context is the real cost center.</p>
</li>
<li><p><strong>Check the whole site, not the diff.</strong> Cannibalization, drift, and convention splits are invisible at diff level.</p>
</li>
</ol>
<hr />
<p><em>The live version, the interactive commit chart, and the AI games are at</em> <a href="https://lkforge.com/blog/26-repos-in-29-days/"><em>lkforge.com</em></a><em>. The 2048 solver writeup with the full benchmark is</em> <a href="https://lkforge.com/games/2048/blog-how-the-ai-solver-works"><em>here</em></a><em>.</em></p>
]]></content:encoded></item></channel></rss>