<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Parker Jones Dev Blog - wasm</title>
    <subtitle>Dev Blog of Parker Jones</subtitle>
    <link rel="self" type="application/atom+xml" href="https://parkerjones.dev/tags/wasm/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://parkerjones.dev"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-06-26T00:00:00+00:00</updated>
    <id>https://parkerjones.dev/tags/wasm/atom.xml</id>
    <entry xml:lang="en">
        <title>Consoler Dark: A Terminal-Themed Zola Blog with Neofetch Logos and In-Browser SurrealDB</title>
        <published>2026-06-26T00:00:00+00:00</published>
        <updated>2026-06-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://parkerjones.dev/posts/consoler-dark-theme/"/>
        <id>https://parkerjones.dev/posts/consoler-dark-theme/</id>
        
        <content type="html" xml:base="https://parkerjones.dev/posts/consoler-dark-theme/">&lt;p&gt;I wanted my blog to feel like a terminal — not in the lazy &quot;monospace font and a green-on-black palette&quot; way, but structurally. The model I kept coming back to was &lt;code&gt;neofetch&lt;&#x2F;code&gt;: an ASCII distro logo on the left, a tidy key&#x2F;value table of system info on the right. That layout &lt;em&gt;is&lt;&#x2F;em&gt; a homepage. So I built one.&lt;&#x2F;p&gt;
&lt;p&gt;The result is &lt;strong&gt;Consoler Dark&lt;&#x2F;strong&gt;, the Zola theme this site runs on. This is how it works, including the two parts I&#x27;m fondest of: a script that generates logos by scraping neofetch itself, and a database that runs entirely in your browser.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;starting-from-after-dark&quot;&gt;Starting from after-dark&lt;&#x2F;h2&gt;
&lt;p&gt;I didn&#x27;t start from scratch. Consoler Dark is a fork of &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;git.habd.as&#x2F;comfusion&#x2F;after-dark&#x2F;&quot;&gt;comfusion&#x27;s after-dark&lt;&#x2F;a&gt;, a minimal Zola theme built on top of &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;hackcss.egoist.dev&#x2F;&quot;&gt;hack.css&lt;&#x2F;a&gt;. You can still see hack.css&#x27;s fingerprints in the SCSS — the &lt;code&gt;.hack&lt;&#x2F;code&gt; utility classes, the no-nonsense defaults. After-dark gave me a clean skeleton; I layered the terminal identity on top.&lt;&#x2F;p&gt;
&lt;p&gt;The taxonomy setup is where I diverged most. Beyond the usual &lt;code&gt;categories&lt;&#x2F;code&gt; and &lt;code&gt;tags&lt;&#x2F;code&gt;, the site indexes &lt;code&gt;series&lt;&#x2F;code&gt;, &lt;code&gt;projects&lt;&#x2F;code&gt;, and &lt;code&gt;authors&lt;&#x2F;code&gt; — that last one so I can credit the AI models I draft alongside as co-authors (this post included).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-neofetch-metaphor-in-templates&quot;&gt;The neofetch metaphor, in templates&lt;&#x2F;h2&gt;
&lt;p&gt;The homepage and section headers render a &lt;code&gt;.neofetch-header&lt;&#x2F;code&gt;: a logo block next to a &lt;code&gt;tech_info&lt;&#x2F;code&gt; table driven straight from config.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;toml&quot;&gt;[extra.tech_info]
Title = &amp;quot;parkerjones.dev&amp;quot;
Framework = &amp;quot;Zola&amp;quot;
Theme = &amp;quot;Consoler Dark&amp;quot;
Hosting = &amp;quot;Github Pages&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The template loops over those key&#x2F;value pairs the way neofetch prints &lt;code&gt;OS&lt;&#x2F;code&gt;, &lt;code&gt;Kernel&lt;&#x2F;code&gt;, &lt;code&gt;Uptime&lt;&#x2F;code&gt;. The nice property is that any page can override individual fields via &lt;code&gt;page.extra.tech_info&lt;&#x2F;code&gt;, so a project page can advertise its own &quot;system specs&quot; while inheriting the site defaults. It&#x27;s config-as-content, and it keeps the homepage honest — the chrome describes the actual stack.&lt;&#x2F;p&gt;
&lt;p&gt;A few smaller touches round out the feel: an &lt;code&gt;intro&lt;&#x2F;code&gt; keyframe fade so content boots in rather than flashing, a 16:9 &lt;code&gt;responsive-iframe&lt;&#x2F;code&gt; helper for embeds, and &lt;code&gt;:target&lt;&#x2F;code&gt; highlighting so deep links land with a visible cursor-style focus.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;scss&quot;&gt;@keyframes intro { 0% { opacity: 0; } 100% { opacity: 1; } }
main, footer { animation: intro 0.3s both; animation-delay: 0.15s; }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;generating-logos-by-scraping-neofetch&quot;&gt;Generating logos by scraping neofetch&lt;&#x2F;h2&gt;
&lt;p&gt;Here&#x27;s the part I enjoyed most. Neofetch ships ASCII art for &lt;em&gt;hundreds&lt;&#x2F;em&gt; of distros, embedded right in its shell script as heredoc blocks. Rather than hand-draw anything, I wrote a Python script that fetches the raw neofetch source, parses out every logo, and converts each to an SVG.&lt;&#x2F;p&gt;
&lt;p&gt;The parser keys off neofetch&#x27;s own structure — each logo is a &lt;code&gt;read -rd &#x27;&#x27; ascii_data &amp;lt;&amp;lt;&#x27;EOF&#x27; … EOF&lt;&#x2F;code&gt; block, and the distro name is in the &lt;code&gt;case&lt;&#x2F;code&gt; line just above it:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;python&quot;&gt;if &amp;quot;read -rd &amp;#39;&amp;#39; ascii_data &amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;&amp;quot; in line:
    # walk backwards to the case label, e.g.  &amp;quot;Ubuntu&amp;quot;|&amp;quot;ubuntu&amp;quot;)
    j = i - 1
    while j &amp;gt;= 0 and not re.search(r&amp;#39;\)$&amp;#39;, lines[j].strip()):
        j -= 1
    distro_name = re.findall(r&amp;#39;&amp;quot;([^&amp;quot;]+)&amp;quot;&amp;#39;, lines[j])[0]

    # collect everything up to the EOF sentinel
    ascii_block = []
    i += 1
    while lines[i].strip() != &amp;quot;EOF&amp;quot;:
        ascii_block.append(lines[i]); i += 1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Converting ASCII to SVG is just laying each line down as a &lt;code&gt;&amp;lt;tspan&amp;gt;&lt;&#x2F;code&gt; in a monospaced &lt;code&gt;&amp;lt;text&amp;gt;&lt;&#x2F;code&gt; element, sized from the longest line:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;python&quot;&gt;char_width, line_height, font_size = 8, 18, 14
width  = max(len(l) for l in lines) * char_width + 10
height = len(lines) * line_height + 10
# one &amp;lt;text&amp;gt; with a &amp;lt;tspan dy=&amp;quot;line_height&amp;quot;&amp;gt; per line
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Run it, and &lt;code&gt;static&#x2F;logos&#x2F;&lt;&#x2F;code&gt; fills up with a clean SVG per distro — vector, themeable, no image assets to hand-maintain. When neofetch adds a logo, I re-run the script. The art stays in sync with upstream because &lt;em&gt;upstream is the source of truth&lt;&#x2F;em&gt;. That&#x27;s the kind of automation that pays for itself the first time you&#x27;d otherwise have copied ASCII art by hand.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;a-database-that-runs-in-your-browser&quot;&gt;A database that runs in your browser&lt;&#x2F;h2&gt;
&lt;p&gt;The detail that surprises people: Consoler Dark ships a WebAssembly build of &lt;strong&gt;SurrealDB&lt;&#x2F;strong&gt; and runs it client-side. There&#x27;s no backend. The theme&#x27;s &lt;code&gt;js&#x2F;main.js&lt;&#x2F;code&gt; boots an in-browser SurrealDB engine, fetches a &lt;code&gt;&#x2F;site.sql&lt;&#x2F;code&gt; file, pulls the SQL out of a &lt;code&gt;&amp;lt;pre&amp;gt;&lt;&#x2F;code&gt; tag, and executes it — all in the visitor&#x27;s tab.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;js&quot;&gt;import { Surreal } from &amp;quot;surrealdb&amp;quot;;
import { surrealdbWasmEngines } from &amp;quot;@surrealdb&#x2F;wasm&amp;quot;;

const db = new Surreal({ engines: surrealdbWasmEngines() });
const sql = await fetchSQLFromPreTag();   &#x2F;&#x2F; read &#x2F;site.sql from a &amp;lt;pre&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is bundled with Vite. A couple of non-obvious settings were required to make a wasm database happy in a static-site context: an &lt;code&gt;es2020&lt;&#x2F;code&gt; target plus &lt;code&gt;top-level-await&lt;&#x2F;code&gt; support (the engine initializes asynchronously at import time), and excluding &lt;code&gt;@surrealdb&#x2F;wasm&lt;&#x2F;code&gt; from dependency pre-bundling so Vite doesn&#x27;t mangle the wasm.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;js&quot;&gt;build:    { target: &amp;#39;es2020&amp;#39;, outDir: &amp;#39;static&amp;#39; },
esbuild:  { target: &amp;#39;es2020&amp;#39;, supported: { &amp;#39;top-level-await&amp;#39;: true } },
optimizeDeps: { exclude: [&amp;#39;@surrealdb&#x2F;wasm&amp;#39;] },
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Why do this on a blog? Because I write about &lt;a href=&quot;&#x2F;posts&#x2F;databses-in-the-browser&#x2F;&quot;&gt;databases in the browser&lt;&#x2F;a&gt; and &lt;a href=&quot;&#x2F;posts&#x2F;zola-surreal-db-site-cache&#x2F;&quot;&gt;using SurrealDB as a site cache&lt;&#x2F;a&gt;, and a theme that &lt;em&gt;is&lt;&#x2F;em&gt; the demo beats one that merely links to it. The dev workflow runs Zola and Vite in parallel so the static site and the wasm bundle rebuild together:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;json&quot;&gt;&amp;quot;dev&amp;quot;: &amp;quot;npm-run-all --parallel vite-serve zola-serve&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;the-linkify-experiment-i-rolled-back&quot;&gt;The linkify experiment I rolled back&lt;&#x2F;h2&gt;
&lt;p&gt;Not everything survived. I&#x27;d wired up a client-side &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;linkify.js.org&#x2F;&quot;&gt;linkify&lt;&#x2F;a&gt; pass that auto-detected &lt;code&gt;#hashtags&lt;&#x2F;code&gt; in post text and rewrote them into links to the matching &lt;code&gt;&#x2F;tags&#x2F;&lt;&#x2F;code&gt; page — Twitter-style tagging, for free, with no changes to how I wrote posts.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;js&quot;&gt;formatHref: (href, type) =&amp;gt;
  type === &amp;#39;hashtag&amp;#39; ? `&#x2F;tags&#x2F;${href.slice(1).toLowerCase()}` : href
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It worked, and I turned it off. Scanning every &lt;code&gt;&amp;lt;p&amp;gt;&lt;&#x2F;code&gt; and &lt;code&gt;&amp;lt;span&amp;gt;&lt;&#x2F;code&gt; on load and rewriting &lt;code&gt;innerHTML&lt;&#x2F;code&gt; is a lot of DOM churn for a cosmetic feature, it fought with code blocks that legitimately contain &lt;code&gt;#&lt;&#x2F;code&gt;, and &quot;clever text rewriting at runtime&quot; is exactly the kind of thing that quietly breaks a year later. The code&#x27;s still in the repo behind a partial; the lesson is that &lt;em&gt;shipping&lt;&#x2F;em&gt; and &lt;em&gt;keeping&lt;&#x2F;em&gt; are different decisions, and a static site rewards restraint.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;takeaways-for-rolling-your-own-zola-theme&quot;&gt;Takeaways for rolling your own Zola theme&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fork something minimal.&lt;&#x2F;strong&gt; after-dark + hack.css gave me a correct baseline so I could spend my effort on identity, not resets.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Drive chrome from config.&lt;&#x2F;strong&gt; The &lt;code&gt;tech_info&lt;&#x2F;code&gt; table means the design describes the real stack and updates itself.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Automate your assets from their upstream source.&lt;&#x2F;strong&gt; The neofetch scraper means I never hand-edit ASCII art.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;A static site can do more than you think.&lt;&#x2F;strong&gt; A wasm database in the client turned my theme into a live demo of the things I write about.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Be willing to delete features that work.&lt;&#x2F;strong&gt; Linkify was the most &quot;impressive&quot; thing I built and the right call was to switch it off.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The theme is open source under MIT. If you want a blog that boots like a terminal and ships a database to every reader, the pieces are all here.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;— Parker Jones, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;parkerjones.dev&quot;&gt;parkerjones.dev&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Hexagonal Chess in Rust: Bevy, WASM, and Serverless WebRTC Multiplayer</title>
        <published>2026-06-26T00:00:00+00:00</published>
        <updated>2026-06-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://parkerjones.dev/posts/hex-chess-webrtc/"/>
        <id>https://parkerjones.dev/posts/hex-chess-webrtc/</id>
        
        <content type="html" xml:base="https://parkerjones.dev/posts/hex-chess-webrtc/">&lt;p&gt;Chess assumes a square board. But there&#x27;s a whole family of &lt;em&gt;hexagonal&lt;&#x2F;em&gt; chess variants — Gliński&#x27;s, McCooey&#x27;s, Shafran&#x27;s, and others — played on a board of hexagons where every piece needs new movement rules and your spatial intuition is useless. I built a Rust implementation of eight of them that runs entirely in the browser via WASM, with multiplayer that needs almost no server at all. This post is about the three architecture decisions that made it tractable.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;decision-1-separate-the-game-from-the-engine-from-the-network&quot;&gt;Decision 1: separate the game from the engine from the network&lt;&#x2F;h2&gt;
&lt;p&gt;The repo is three crates, and the split is the most important design choice in the project:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;hex-chess&#x2F;
├── crates&#x2F;
│   ├── core&#x2F;        # pure game logic — no rendering, no networking
│   ├── game&#x2F;        # Bevy WASM app — rendering, input, the actual game
│   └── signaling&#x2F;   # minimal WebRTC signaling server
├── web&#x2F;             # static web assets
└── deploy&#x2F;nixos&#x2F;    # production deployment
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;core&lt;&#x2F;code&gt; has no dependencies on Bevy, on WASM, or on the network.&lt;&#x2F;strong&gt; It&#x27;s pure rules: given a board state and a variant, what are the legal moves? That isolation buys two things. First, the rules are testable without a GPU or a browser — &lt;code&gt;cargo test&lt;&#x2F;code&gt; on &lt;code&gt;core&lt;&#x2F;code&gt; exercises move generation directly, which matters enormously when &quot;is this move legal on a Shafran board?&quot; has eight different correct answers depending on the variant. Second, the rules are reusable: the same &lt;code&gt;core&lt;&#x2F;code&gt; that the Bevy app calls could back a bot, a server-side validator, or a puzzle generator later, with zero rendering code dragged along.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;game&lt;&#x2F;code&gt; is the Bevy app — it owns rendering, input, and animation, and it asks &lt;code&gt;core&lt;&#x2F;code&gt; what&#x27;s legal. &lt;code&gt;signaling&lt;&#x2F;code&gt; is its own tiny server. Keeping these three apart means a change to how pieces are drawn can&#x27;t possibly break the rules, and a change to the netcode can&#x27;t touch either.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;decision-2-hex-coordinates-not-row-column&quot;&gt;Decision 2: hex coordinates, not (row, column)&lt;&#x2F;h2&gt;
&lt;p&gt;The thing that breaks your brain about hex chess is that a square-grid mental model doesn&#x27;t transfer. On a square board a piece has 8 neighbors and you index by &lt;code&gt;(row, col)&lt;&#x2F;code&gt;. On a hex board a cell has 6 neighbors and &lt;code&gt;(row, col)&lt;&#x2F;code&gt; produces ugly special-casing everywhere — diagonals aren&#x27;t diagonals, and &quot;forward&quot; depends on which of three axes you mean.&lt;&#x2F;p&gt;
&lt;p&gt;The standard fix (and the right one) is to stop thinking in two axes and use &lt;strong&gt;cube or axial coordinates&lt;&#x2F;strong&gt; — three coordinates &lt;code&gt;(q, r, s)&lt;&#x2F;code&gt; with the constraint &lt;code&gt;q + r + s = 0&lt;&#x2F;code&gt;. Movement directions become clean vector additions, distance is a single formula, and the six variants&#x27; wildly different movement rules all reduce to &quot;which direction vectors does this piece get, and how far.&quot; Putting that coordinate system in &lt;code&gt;core&lt;&#x2F;code&gt; is what keeps the eight variants from turning into eight piles of special cases. If you take one idea from this project, it&#x27;s that the coordinate system &lt;em&gt;is&lt;&#x2F;em&gt; the architecture for a hex game — pick it before you write a single movement rule.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;decision-3-peer-to-peer-so-i-barely-run-a-server&quot;&gt;Decision 3: peer-to-peer, so I barely run a server&lt;&#x2F;h2&gt;
&lt;p&gt;Most multiplayer games need a server in the hot path relaying every move. I didn&#x27;t want to run (or pay for) that, so hex-chess uses &lt;strong&gt;WebRTC for true peer-to-peer play&lt;&#x2F;strong&gt;. The only server is the &lt;code&gt;signaling&lt;&#x2F;code&gt; crate, and it does almost nothing:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Two players want to connect. WebRTC needs them to exchange connection details (SDP offers&#x2F;answers and ICE candidates) before they can talk directly.&lt;&#x2F;li&gt;
&lt;li&gt;The signaling server is just the broker for that initial handshake. It introduces the peers.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Once the peers connect, game traffic flows directly between the two browsers.&lt;&#x2F;strong&gt; The server is out of the loop entirely — it never sees a move.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So the &quot;server infrastructure&quot; for an arbitrary number of concurrent games is one tiny process whose only job is matchmaking introductions. It&#x27;s idle the moment a game starts. That&#x27;s a dramatically cheaper operational story than a stateful game server, and for a turn-based game like chess the direct peer connection is more than fast enough.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shipping-it-wasm-nix-and-ci&quot;&gt;Shipping it: WASM, Nix, and CI&lt;&#x2F;h2&gt;
&lt;p&gt;The &lt;code&gt;game&lt;&#x2F;code&gt; crate compiles to WebAssembly with &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;trunkrs.dev&#x2F;&quot;&gt;Trunk&lt;&#x2F;a&gt;, so the whole thing runs in the browser at near-native speed:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;nix develop                      # reproducible toolchain
cd crates&#x2F;game &amp;amp;&amp;amp; trunk build --release
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The entire dev environment and production deploy are defined in Nix — &lt;code&gt;nix develop&lt;&#x2F;code&gt; drops you into a shell with the exact Rust toolchain, &lt;code&gt;wasm&lt;&#x2F;code&gt; target, and Trunk version, and &lt;code&gt;nix build .#game .#signaling&lt;&#x2F;code&gt; produces both artifacts:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;nix build .#game .#signaling     # build both packages
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Deployment lives in &lt;code&gt;deploy&#x2F;nixos&#x2F;&lt;&#x2F;code&gt;, and GitHub Actions handles CI. This is the same declarative approach I use across &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-one-flake&#x2F;&quot;&gt;my whole fleet&lt;&#x2F;a&gt; — the game and its signaling server are just two more Nix packages, built and deployed the same way as everything else I run. No &quot;works on my machine&quot; between my laptop and the box that serves the game.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-i-d-tell-someone-starting-a-hex-game&quot;&gt;What I&#x27;d tell someone starting a hex game&lt;&#x2F;h2&gt;
&lt;p&gt;The hard parts of this project weren&#x27;t the parts I expected. Rendering hexagons is easy; Bevy handles it. The hard parts were &lt;strong&gt;getting the coordinate system right before writing rules&lt;&#x2F;strong&gt; (retrofitting axial coordinates onto a square-grid assumption would have been a rewrite) and &lt;strong&gt;resisting the urge to put a real game server in the middle&lt;&#x2F;strong&gt; (the P2P handshake is more setup up front but far less to operate forever after).&lt;&#x2F;p&gt;
&lt;p&gt;Pure-logic core, hex-native coordinates, peer-to-peer networking with a near-stateless broker, WASM for reach, Nix for reproducibility. Five decisions, eight playable variants, and a multiplayer game I can run for the cost of a process that sits idle most of the time.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;— Parker Jones, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;parkerjones.dev&quot;&gt;parkerjones.dev&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
