<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Claude-Code on hippotion</title><link>https://blog.hippotion.com/tags/claude-code/</link><description>Recent content in Claude-Code on hippotion</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Fri, 27 Feb 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.hippotion.com/tags/claude-code/index.xml" rel="self" type="application/rss+xml"/><item><title>🌱 My Second Brain Weeds Itself Now</title><link>https://blog.hippotion.com/posts/an-ai-gardener-for-your-second-brain/</link><pubDate>Fri, 27 Feb 2026 00:00:00 +0000</pubDate><guid>https://blog.hippotion.com/posts/an-ai-gardener-for-your-second-brain/</guid><description>I gave my markdown knowledge base a nightly gardener — an AI that finds orphan notes and missing links and fixes them, every change a reviewable git commit. The fun part was the Kubernetes wall I hit on the way.</description><content:encoded><![CDATA[<p>A few weeks ago I <a href="/posts/a-second-brain-you-can-git-clone/">rebuilt my second brain as a folder of markdown in git</a> — vault is the source of truth, everything else (search index, graph, 3D viewer) is a derived layer I can delete and rebuild. I love it. But a knowledge base has a dirty secret: <strong>it rots.</strong></p>
<p>Not the files — those are fine. The <em>connections</em> rot. You capture a note at 11pm and never link it to anything, so it becomes an orphan floating off the graph. A project note&rsquo;s one-line summary describes what the project was three weeks ago. Two notes are obviously about the same thing and neither knows the other exists. Do this for a few months and you don&rsquo;t have a second brain, you have a junk drawer with good search.</p>
<p>The honest fix is to weed the garden regularly. The honest truth is that nobody does, including me.</p>
<p>So I stopped relying on myself and built a gardener.</p>
<h2 id="what-it-actually-does">What it actually does</h2>
<p>Every night at 3am, on my homelab box, a script runs:</p>
<ol>
<li><strong>Detect</strong> — <code>exo garden</code>, a plain query over the index, produces a report: here are the orphans, here are notes that should probably link to each other, here are summaries that look stale. <strong>No AI in this step.</strong> It&rsquo;s SQL and graph traversal. Deterministic, boring, trustworthy.</li>
<li><strong>Decide and write</strong> — that report gets piped to <code>claude -p</code> (Claude Code in headless mode). Claude reads the vault&rsquo;s operating contract, makes <em>only high-confidence</em> edits — add a <code>[[wikilink]]</code> between two genuinely related notes, refresh a stale summary — caps itself at ~10 notes a night, and writes a dated log note explaining exactly what it changed and what it deliberately skipped.</li>
<li><strong>Commit</strong> — the wrapper reindexes and lands everything as a single <code>garden: 2026-06-09 …</code> git commit, then pushes. My 3D graph viewer picks it up on the next sync.</li>
</ol>
<p>The first real run, it found one orphan (<code>90-meta/README</code>), linked it into the notes it actually indexes, and then — this is the part I liked — <em>declined</em> to touch the 12 &ldquo;stale summary&rdquo; candidates because, on inspection, every one of them was already accurate. It wrote: <em>&ldquo;flagged by length, not staleness; churning them would add noise.&rdquo;</em> A gardener that knows when <strong>not</strong> to prune is the one you can leave alone.</p>
<h2 id="isnt-this-a-solved-problem">&ldquo;Isn&rsquo;t this a solved problem?&rdquo;</h2>
<p>Mostly, no — but partly, yes, and I want to be straight about it. AI-assisted note-linking exists: Obsidian plugins like Smart Connections suggest related notes, and apps like Mem and Reflect auto-organize as you write. They&rsquo;re good.</p>
<p>Three things make this different enough to build:</p>
<ul>
<li><strong>Every change is a reviewable git diff, authored by a named agent.</strong> Not silent magic that rearranges your notes while you&rsquo;re not looking. <code>git log -p</code> shows you exactly what the gardener did last night; <code>git revert</code> undoes a bad night in one command. For something as personal as a knowledge base, &ldquo;show me the diff&rdquo; beats &ldquo;trust me.&rdquo;</li>
<li><strong>It&rsquo;s mine, end to end.</strong> Runs on my hardware, on my schedule, with a model I point at. No SaaS holds my brain hostage.</li>
<li><strong>The detection is deterministic; the model only acts.</strong> The LLM never decides <em>what&rsquo;s wrong</em> — a boring query does that. The model only decides <em>how to fix the things already found</em>. That split keeps the whole thing auditable and cheap.</li>
</ul>
<p>If you already live in a tool that does this and you trust it, great. I wanted the git-diff trail and the local control.</p>
<h2 id="the-part-i-actually-want-to-tell-you-about">The part I actually want to tell you about</h2>
<p>The plan was tidy: I run n8n on the same cluster, so n8n would be the scheduler — fire nightly, <strong>SSH into the node</strong>, run the gardener. Clean, visual, one workflow.</p>
<p>n8n could not reach the node. At all. Every port: <code>ECONNREFUSED</code>.</p>
<p>This sent me down a genuinely interesting hole, because the homelab runs <strong>Cilium</strong> for networking, and Cilium has opinions about your own node that plain Kubernetes does not.</p>
<p>First instinct: a NetworkPolicy allowing egress to the node&rsquo;s IP. Wrote it, synced it, still refused. The reason is a Cilium subtlety worth knowing: <strong>the node isn&rsquo;t a CIDR, it&rsquo;s an identity.</strong> Cilium classifies your cluster&rsquo;s own node as the special <code>host</code> identity, and ordinary <code>ipBlock</code> CIDR rules <em>do not match it</em> unless you flip a cluster-wide setting (<code>policy-cidr-match-mode: nodes</code>). My <code>192.168.0.109/32</code> rule was a no-op.</p>
<p>So I switched to the Cilium-native tool: a <code>CiliumNetworkPolicy</code> with <code>toEntities: [host]</code>. Confirmed it applied — I could see <code>reserved:host</code> allowed right there in the datapath&rsquo;s BPF policy map. I confirmed the node&rsquo;s IP really does resolve to identity <code>1</code> (host). I confirmed the host firewall was <em>disabled</em>. Everything said &ldquo;allowed.&rdquo;</p>
<p>Still <code>ECONNREFUSED</code>.</p>
<p>That&rsquo;s the wall. The packet leaves the pod with Cilium&rsquo;s blessing, hits the host&rsquo;s own network stack, and <em>something there</em> sends a reset — and I couldn&rsquo;t see what, because inspecting the host firewall needs root, and this automation deliberately doesn&rsquo;t have it. I could have kept digging with a password. But I stopped and asked a better question: <strong>why am I making a pod reach back into the host it&rsquo;s running on at all?</strong></p>
<p>That&rsquo;s an awkward direction. The work has to happen <em>on</em> the host (that&rsquo;s where the vault, git creds, and Claude live). A pod straining to SSH into its own node is fighting the grain of the platform.</p>
<p>So I inverted it. <strong>The node schedules itself</strong> — a plain cron entry, rock-solid, no network gymnastics. And n8n, instead of <em>triggering</em> the job, <em>receives</em> it: at the end of each run the node POSTs a summary to an n8n webhook. Node→n8n works perfectly (it&rsquo;s just an outbound HTTPS call to a URL). n8n keeps the run history and is the place I&rsquo;ll later wire a phone notification.</p>
<p>I lost nothing that mattered. n8n is still my dashboard; the schedule just lives where the work lives. And I deleted the SSH key and the network-policy hole I&rsquo;d opened — the cleanup felt better than the original plan would have.</p>
<h2 id="the-lesson-such-as-it-is">The lesson, such as it is</h2>
<p>Two, actually.</p>
<p><strong>One:</strong> when you&rsquo;re automating something to run unattended, the bug you want to find is the one that shows up in a <em>dry run at 2pm</em>, not at <em>3am three weeks from now</em>. I almost shipped a version where a brand-new note (untracked by git) was invisible to my change-detection and would&rsquo;ve been silently wiped each night. The dry run caught it. Always build the dry run.</p>
<p><strong>Two, the bigger one:</strong> I spent an hour trying to make a pod punch into its host because that was <em>my</em> plan, and the platform kept saying no in increasingly specific ways. The fix wasn&rsquo;t a cleverer NetworkPolicy. It was noticing I was pushing against the design and turning around. The node scheduling itself and <em>reporting up</em> to n8n is simpler, safer, and more honest about where the work actually lives.</p>
<p>My brain weeds itself now. Every morning there&rsquo;s maybe one small, sensible commit waiting — a link I&rsquo;d have never made, a summary nudged back to true — and I can read exactly what changed before my coffee&rsquo;s done. That&rsquo;s the whole dream of a second brain that isn&rsquo;t a junk drawer: it stays a garden, and I barely have to touch it.</p>
]]></content:encoded></item><item><title>🪟 I Built Yet Another Claude Code Session Switcher</title><link>https://blog.hippotion.com/posts/hand-rolled-claude-session-switcher/</link><pubDate>Fri, 30 Jan 2026 00:00:00 +0000</pubDate><guid>https://blog.hippotion.com/posts/hand-rolled-claude-session-switcher/</guid><description>The web is flooded with Claude Code session managers. I built one more anyway — and the part worth sharing isn&amp;rsquo;t the tool, it&amp;rsquo;s what I had to learn about where Claude actually keeps your sessions.</description><content:encoded><![CDATA[<h2 id="the-confession-first">The confession first</h2>
<p>There are, at last count, a small army of tools that list your Claude Code
sessions and let you jump back into one. tmux wrappers
(<a href="https://github.com/nielsgroen/claude-tmux">claude-tmux</a>,
<a href="https://github.com/0xkaz/claunch">claunch</a>), keyword resumers
(<a href="https://github.com/MaxGhenis/tmux-claude-code">tmux-claude-code</a>), fleet
managers (<a href="https://github.com/raphaelbgr/claude-manager">claude-manager</a>), and a
whole macOS menu-bar genre (<a href="https://github.com/sverrirsig/claude-control">claude-control</a>,
cmux, and friends). They&rsquo;re good. Several are better-engineered than mine.</p>
<p>I built one more anyway.</p>
<p>Not because the others are wrong — because none of them were shaped like <em>my</em>
day, and the cost of hand-rolling a 300-line script turned out to be smaller than
the cost of bending my workflow around someone else&rsquo;s defaults. That&rsquo;s the whole
pitch, and it&rsquo;s a boring one. The interesting part is what I had to understand to
build it, because it corrected a mental model I&rsquo;d had backwards for months.</p>
<h2 id="my-day-concretely">My day, concretely</h2>
<p>I work off a single Linux box over SSH, from a few different machines. A session
might be a homelab change, a side project, a blog post. I drop one mid-thought,
my laptop sleeps, I pick it up that evening from a different terminal. The thing I
kept doing was running <code>claude --resume</code> and squinting at a list of UUIDs trying
to remember which <code>7f3a…</code> was the one about the broken redirect.</p>
<p>I wanted one command — <code>wt</code> — that shows me every session with a human summary
and tells me, truthfully, which ones are still alive. Then lets me pick one.</p>
<p>Simple ask. It sent me reading the on-disk format, and that&rsquo;s where it got
educational.</p>
<h2 id="what-i-had-backwards-tmux-is-not-how-you-keep-a-session">What I had backwards: tmux is not how you keep a session</h2>
<p>Every tmux-first tool sells the same promise: run Claude <em>inside</em> tmux so your
session survives a disconnect. I&rsquo;d internalized that as <strong>&ldquo;tmux is how Claude
sessions persist.&rdquo;</strong></p>
<p>That&rsquo;s wrong, and realizing it deleted half the code I thought I&rsquo;d need.</p>
<p>A Claude Code session is one <code>claude</code> process, keyed by a <code>sessionId</code> UUID. Its
entire transcript — every message, every tool call and result — is appended to a
file:</p>
<pre tabindex="0"><code>~/.claude/projects/&lt;cwd-slug&gt;/&lt;sessionId&gt;.jsonl
</code></pre><p>It&rsquo;s append-only, and it has <strong>no &ldquo;end&rdquo; marker</strong>. When you <code>--resume</code>, Claude
reopens that same file and replays it. One of my session files spans three
calendar days across half a dozen resumes — same file, same UUID, the whole
conversation reconstructed from disk each time.</p>
<p>Which means: <strong>the history is durable independent of any running process.</strong> You
do not need tmux to land exactly where you left off. <code>claude --resume &lt;id&gt;</code> does
that from the transcript alone, on a box with no tmux installed at all.</p>
<p>So what <em>is</em> tmux for, then? Exactly one thing: keeping a process <em>running</em> while
you&rsquo;re disconnected — a long job, an agent grinding away, or re-attaching the
<em>same live process</em> from your phone. That&rsquo;s real, but it&rsquo;s the exception, not the
default. So in my tool, plain resume is the default and tmux is an opt-in flag.
The inversion fell straight out of reading the format honestly.</p>
<h2 id="the-other-thing-the-transcript-doesnt-tell-you-is-it-alive">The other thing the transcript doesn&rsquo;t tell you: is it alive?</h2>
<p>Here&rsquo;s the subtle bit. The transcript tells you the <em>history</em> of a session. It
does <strong>not</strong> tell you whether a <code>claude</code> process is running right now. There&rsquo;s no
&ldquo;closed&rdquo; record — the file for a long-dead session looks identical to one you
left open thirty seconds ago.</p>
<p>Liveness lives somewhere else:</p>
<pre tabindex="0"><code>~/.claude/sessions/&lt;pid&gt;.json   →   { pid, sessionId, cwd, procStart, ... }
</code></pre><p>A session is alive if that pid is actually running. But you can&rsquo;t just trust the
file&rsquo;s existence — it can linger after a crash — and you can&rsquo;t just <code>kill -0</code> the
pid either, because the kernel recycles pids and you might be poking a process
that <em>reused</em> the number. So the honest check is two-factor:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">alive</span><span class="p">(</span><span class="n">pid</span><span class="p">,</span> <span class="n">procstart</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">os</span><span class="o">.</span><span class="n">kill</span><span class="p">(</span><span class="n">pid</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>          <span class="c1"># exists and signalable?</span>
</span></span><span class="line"><span class="cl">    <span class="k">except</span> <span class="p">(</span><span class="ne">ProcessLookupError</span><span class="p">,</span> <span class="ne">OSError</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ...and is it the SAME process, not a pid-recycle?</span>
</span></span><span class="line"><span class="cl">    <span class="n">stat</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;/proc/</span><span class="si">{</span><span class="n">pid</span><span class="si">}</span><span class="s2">/stat&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">read_text</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">starttime</span> <span class="o">=</span> <span class="n">stat</span><span class="p">[</span><span class="n">stat</span><span class="o">.</span><span class="n">rindex</span><span class="p">(</span><span class="s2">&#34;)&#34;</span><span class="p">)</span> <span class="o">+</span> <span class="mi">2</span><span class="p">:]</span><span class="o">.</span><span class="n">split</span><span class="p">()[</span><span class="mi">19</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">starttime</span> <span class="o">==</span> <span class="nb">str</span><span class="p">(</span><span class="n">procstart</span><span class="p">)</span>
</span></span></code></pre></div><p>That <code>/proc/&lt;pid&gt;/stat</code> start-time comparison is the difference between &ldquo;I think
it&rsquo;s live&rdquo; and &ldquo;it&rsquo;s live.&rdquo; It&rsquo;s the kind of detail you only get right by caring
about the boring case.</p>
<p>With that, every session resolves to a real state:</p>
<ul>
<li><code>● live</code> — a process is running now</li>
<li><code>⧗ waiting</code> — no process; you left mid-conversation (last line was Claude)</li>
<li><code>· idle</code> — no process; stale</li>
</ul>
<p>And the payoff for getting <em>liveness</em> right: if you try to resume a session that&rsquo;s
still live in another terminal, the tool refuses to double-open it — two processes
appending to one transcript is how you corrupt your own history — and offers a
clean <code>--fork-session</code> instead.</p>
<h2 id="the-summaries-were-free-the-whole-time">The summaries were free the whole time</h2>
<p>The feature I assumed I&rsquo;d have to build — a short, human description of each
session — I didn&rsquo;t build at all. Claude Code already writes one. Buried in the
transcript is a record type:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span><span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;ai-title&#34;</span><span class="p">,</span> <span class="nt">&#34;aiTitle&#34;</span><span class="p">:</span> <span class="s2">&#34;Investigate nested o directories&#34;</span><span class="p">,</span> <span class="nt">&#34;sessionId&#34;</span><span class="p">:</span> <span class="s2">&#34;...&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>Claude titles your sessions for you. The &ldquo;summary&rdquo; column in my tool is just that
field, with a fallback to your last prompt. The best line of code is the one you
delete after noticing the platform already did the work.</p>
<h2 id="so-what-did-i-actually-build">So what did I actually build</h2>
<p>Not much, and that&rsquo;s the point. <code>wt</code> is one Python file, standard library only,
no daemon. It globs the transcripts, reads each one&rsquo;s title and last-activity,
joins that against the pid-verified live registry, sorts live-first, and prints a
numbered list. Pick a number and it <code>exec</code>s into <code>claude --resume</code>. There&rsquo;s a
<code>-t</code> for tmux when I genuinely need it, a <code>d</code> to archive old sessions (a file
move, fully reversible), and a guarded hook that turns it into an SSH login
greeting so the box tells me what&rsquo;s on it the moment I land.</p>
<pre tabindex="0"><code>  watchtower · 5 session(s)
   1) ●  live       16s  homelab    595e931d  Investigate nested o directories
   2) ·  idle     1d07h  notes-app  6565b121  Migrate to server components
  [#]resume  [t#]tmux  [d#]archive  [n]ew  [Enter]shell  [q]uit ▸
</code></pre><p>If you want it, it&rsquo;s <a href="https://github.com/janos-gyorgy/claude-watchtower">on GitHub</a>,
MIT. But honestly, I&rsquo;d rather you take the three things I had to learn than the
tool:</p>
<ol>
<li><strong>Your Claude history lives in a plain append-only JSONL on disk, not in
tmux.</strong> <code>--resume</code> works without any wrapper. Back up <code>~/.claude/projects/</code> and
you&rsquo;ve backed up every conversation you&rsquo;ve had.</li>
<li><strong>Liveness is a separate fact from history</strong>, and checking it honestly means
verifying the pid <em>is the same process</em> — not just that something answers to
the number.</li>
<li><strong>The platform probably already did the boring work</strong> (here: the titles). Read
the format before you write the feature.</li>
</ol>
<p>The flooded-market thing turns out not to matter. A tool that fits your own hands
is worth building even when fifty others exist — <em>especially</em> when it&rsquo;s small
enough that &ldquo;build&rdquo; and &ldquo;understand the system underneath&rdquo; are the same afternoon.</p>
]]></content:encoded></item><item><title>🎲 I Built a Browser Game to Learn AI Coding Tools. It Turned Into Something Else.</title><link>https://blog.hippotion.com/posts/dice-and-shrines/</link><pubDate>Fri, 04 Jul 2025 00:00:00 +0000</pubDate><guid>https://blog.hippotion.com/posts/dice-and-shrines/</guid><description>What started as a Claude Code / Codex sandbox became a territory conquest game with five asymmetric guardians, procedurally generated hex maps, and a stats service to balance them. Here&amp;rsquo;s what happened.</description><content:encoded><![CDATA[<h2 id="it-started-as-a-sandbox">It started as a sandbox</h2>
<p>I wanted to get a feel for AI-assisted coding tools — Claude Code, Codex — in a low-stakes environment where breaking things was fine. A browser game seemed like the right vehicle: self-contained, no prod database, no users to disappoint.</p>
<p>I picked a premise I knew was fun: <strong>Dice Wars</strong>. The Flash-era classic. Roll dice to attack adjacent territories, biggest army snowballs. Simple enough that I could focus on the tooling rather than the design. Or so I thought.</p>
<p>Six weeks later I had five asymmetric character classes, a procedural hex map generator with acceptance criteria, a FastAPI telemetry service recording every dice roll, and a stat dashboard I check more than I probably should. The tooling became a background concern. The game took over.</p>
<hr>
<h2 id="how-the-game-works">How the game works</h2>
<p>The rules are genuinely minimal.</p>
<p>You start with a random slice of a procedurally generated map — a patchwork of irregular coloured territories, each one a cluster of hex tiles that reads as a solid blob. You and up to seven opponents begin scattered across it. Objective: own everything.</p>
<p><strong>Attacking</strong> is one click. Select your territory, click an adjacent enemy territory. Both sides roll all their dice and sum. Higher total wins. Attacker wins: you capture the territory, your dice advance in minus one. Defender wins: your attack is repelled, you&rsquo;re reduced to a single die on the attacking territory.</p>
<p><strong>Reinforcements</strong> are the mechanic that makes this a strategy game. At the end of your turn, you receive bonus dice equal to the size of your <strong>largest contiguous group of territories</strong>. Not total territories — the biggest connected blob. Fragmented territory generates almost nothing. A solid connected bloc snowballs.</p>
<p>That one rule creates the entire strategic texture. Grab fast but stay connected. Chokepoints are worth defending at a loss. Cutting an opponent in half collapses their income immediately. The late game turns into tense standoffs until one roll cracks something open.</p>
<p><img alt="An 8-player Epic game in progress — territories changing hands, rankings shifting in the side panel" loading="lazy" src="/posts/dice-and-shrines/midgame.png"></p>
<hr>
<h2 id="the-shrines">The shrines</h2>
<p>Early on I added shrines — special territories marked with a ★. They behave differently from normal territories in a few ways:</p>
<ul>
<li><strong>Higher dice cap</strong>: normal territories max out at 8 dice, shrines at 10</li>
<li><strong>Minimum floor</strong>: a shrine never drops below 2 dice after attacking, win or loss — it can&rsquo;t be stripped bare</li>
<li><strong>Guaranteed reinforcement</strong>: the shrine gets a die first at end of turn, before random distribution</li>
<li><strong>Aura</strong>: each of your own territories adjacent to the shrine gets a +1 guaranteed die (shown with a dim ◆ indicator)</li>
</ul>
<p>The shrine mechanic does something interesting to the risk calculus. Holding a shrine isn&rsquo;t just a territory — it&rsquo;s a node that warps the value of everything adjacent to it. You&rsquo;ll defend an aura territory harder than a territory of equivalent size elsewhere on the map, because losing it means losing the aura bonus. It also means attacking <em>into</em> a shrine aura is expensive: the shrine can&rsquo;t be worn down easily, and the neighboring territories keep refilling.</p>
<p>Shrines turned out to be the moment the math got interesting.</p>
<hr>
<h2 id="five-guardians">Five guardians</h2>
<p>The game has a character select screen. Each player — human or AI — picks a guardian before the map generates. Five options, each with a passive and an active ability that meaningfully change how you play:</p>
<p><strong>Hippo</strong> gets to manually place one reinforcement die each turn before the rest distribute randomly. One die placement doesn&rsquo;t sound like much until you realise you always have a frontline territory that needs it more than anywhere else. High floor, consistent.</p>
<p><strong>Hedgehog</strong> fills weakest territories first during reinforcement. Defensively solid — you never bleed out a border territory to zero while inland territories sit at cap. It doesn&rsquo;t generate more dice, but it wastes fewer.</p>
<p><strong>Fox</strong> banks a stored die every other turn and can spend it as a critical multiplier on an attack. The stored dice accumulate (up to a cap), so the power compounds if you resist using it. Two-hop flanking passive: Fox can attack across two territory hops from border territories, making it very hard to feel safely tucked away from.</p>
<p><strong>Owl</strong> has a passive two-hop attack range like Fox but for all attacks — Owl sees further. The active ability is a Dice Transfer: move dice from one of your territories to an adjacent friendly one. Lets you concentrate force without committing to an attack.</p>
<p><strong>Turtle</strong> gets 2 dice back to a neighboring territory any time it loses a defense. It&rsquo;s the only guardian that turns <em>taking damage</em> into a resource. Hard to snowball, but very hard to finish off.</p>
<p>The AI cycles through the five guardians deterministically, so in a full 8-player game you&rsquo;ll face at least one of each.</p>
<hr>
<h2 id="the-map">The map</h2>
<p>The map generator produces irregular coloured territories from a hex grid. Hexes are the visual scaffolding — what matters in gameplay is the blob they form. Internal edges within a territory are hidden; only the outer border of each cluster is drawn. The result reads like a contested piece of ground rather than a grid.</p>
<p><img alt="A freshly generated map before the first move — eight starting positions across irregular territory blobs" loading="lazy" src="/posts/dice-and-shrines/freshmap.png"></p>
<p>The generator has acceptance criteria. A proposed map is rejected if territory sizes are too uneven — a tiny starting territory with 2 hexes versus a sprawling 15-hex territory produces a wildly unfair game. The stats service actually records generation attempts and acceptance rates per map, so I can see how often the map generator throws away its own work.</p>
<p>The <code>mapId</code> is stamped on every game record, so I can eventually correlate map topology with game outcomes. I haven&rsquo;t done that analysis yet, but the data is there.</p>
<hr>
<h2 id="what-the-tools-actually-felt-like">What the tools actually felt like</h2>
<p>Claude Code handles the kind of work that benefits from holding a lot of context simultaneously: &ldquo;this change to <code>resolveAttack()</code> in <code>game.js</code> needs corresponding updates to the AI logic in <code>ai.js</code> and the render pass in <code>render.js</code>.&rdquo; That&rsquo;s tedious to track manually and exactly the kind of thing where the tool earns its keep.</p>
<p>Codex was more useful for the boilerplate-heavy parts — filling in schema.sql, wiring up FastAPI endpoints, the chart.js setup in the dashboard. Directed generation of code you know exactly how should look.</p>
<p>Neither tool replaces thinking. The guardian ability design, the shrine balance, the question of whether Fox&rsquo;s stored critical is too swingy in epic mode — that&rsquo;s all still just sitting down and working it out. The tools speed up the translation from &ldquo;I know what I want&rdquo; to &ldquo;here is working code.&rdquo; The game design itself doesn&rsquo;t compress.</p>
<hr>
<h2 id="its-live">It&rsquo;s live</h2>
<p>The game runs at <a href="https://dice.hippotion.com">dice.hippotion.com</a>. Single HTML file served from a Kubernetes ConfigMap on my homelab. No accounts, no install. It runs fast — AI turns are instant when you toggle off animations, and a full game can be over in five minutes or stretch to twenty depending on how the map falls.</p>
<p>Stats dashboard at <a href="https://game-stats.hippotion.com">game-stats.hippotion.com</a>. More on that in the next post.</p>
]]></content:encoded></item></channel></rss>