<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Geek Life</title>
        <link>https://alexblog.net</link>
        <description>A literary, self-ironic dev blog</description>
        <lastBuildDate>Sat, 13 Jun 2026 01:13:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <copyright>Alex Kucherenko</copyright>
        <item>
            <title><![CDATA[Monaco 2026: One Overtake, All the Drama in the World]]></title>
            <link>https://alexblog.net/monaco-2026-drama/</link>
            <guid isPermaLink="false">https://alexblog.net/monaco-2026-drama/</guid>
            <pubDate>Sun, 07 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[One overtake in two hours, six pit-lane penalties, two heroes in the same wall, and a broken car on the podium. Everyone says Monaco is boring. My wife and I had the time of our lives.]]></description>
            <content:encoded><![CDATA[<p>One overtake. In two hours of Monaco, a car passed another car on track exactly once — and that was Arvid Lindblad, surfing a wagon-load of pure luck into the points. One move, a fistful of points by the end, and if the red flag hadn't fallen exactly his way it would have been a far less cheerful afternoon for him. By every metric the purists love, this was a dead race.</p>
<p>It was also the most fun my wife and I have had in front of a screen all season.</p>
<p>Kimi Antonelli won it, by the way — lights to flag, utterly in control. I'd genuinely forgotten he won, and honestly nobody gave a damn — and that tells you everything about where the story actually lived.</p>
<h2>A race with one overtake and a hundred plot twists</h2>
<p>Monaco isn't a race you win. It's a race you survive. And this year the survival rate was comedy.</p>
<h2>Hadjar drove a wreck onto the podium</h2>
<p>Isack Hadjar spent what felt like sixty laps nursing a broken Red Bull around the houses, collecting what felt like every penalty in the world on the way. Then the stewards quietly dropped the one that mattered — the team had started illegal work on his car under the red flag and then thought better of it — and he climbed out of that mess onto his first podium for Red Bull. P3. Not because he was fast, but because everyone ahead of him found a more creative way to lose.</p>
<p>George Russell got the opposite gift. A five-second pit-lane penalty he couldn't even serve properly — because under the late safety car one half of the pit wall thought they had to sit out the five seconds and the other half thought they didn't, so the crew just went ahead and changed the tyres while George sat there asking the radio whether he was even stopping. The FIA was not amused, and the five seconds became a drive-through that folded his afternoon shut like a cheap deck chair. Two drivers, the same circuit, opposite endings. That's the whole sport in one stop.</p>
<h2>Stroll started it, Leclerc had to one-up him</h2>
<p>Lance Stroll planted his Aston in the barriers first — first to get bored, evidently, first to decide he'd seen quite enough and head home early. Then Charles Leclerc, apparently deciding the drama wasn't quite sharp enough, repeated the trick almost note for note at the restart — and binned a podium doing it. The FIA pointed at the track surface breaking up at exactly the spot where both of them lost the car. Both drivers shrugged and blamed their own machinery — engine braking, brakes, take your pick. Nobody agreed on anything, which is the most Monaco outcome imaginable.</p>
<p>And the Ferrari heartbreak underneath it all: Leclerc, at home, throwing away a podium car. Rage? An honest mistake? He came out of it sad and furious in equal measure, and you genuinely couldn't tell which one put him in the wall. Piastri, for the record, dragged his McLaren home a frustrated fourth — a podium that was right there and simply never arrived.</p>
<h2>Six speeding tickets in one afternoon</h2>
<p>And the penalties. Oh, the penalties. Six drivers — <em>six</em> — got done for speeding in the pit lane. Half the grid, near enough, all caught doing the one thing every team has a literal button to prevent.</p>
<pre class="shiki warm-syntax" style="background-color:#181412;color:#d4c8b8" tabindex="0"><code><span class="line"><span>THE PENALTY BOX · Monaco 2026</span></span>
<span class="line"><span>Gasly       P3 on the road  ->  P7   (two separate 5s pit-lane penalties)</span></span>
<span class="line"><span>Russell     5s pit-lane     ->  drive-through, afternoon over</span></span>
<span class="line"><span>Hulkenberg  10s             ->  for collecting Sainz</span></span>
<span class="line"><span>Perez       false start     ->  P10 stripped, sent to the back</span></span>
<span class="line"><span>... and two more for the road. Six speeding tickets in total.</span></span></code></pre>
<p>So which is it? Did half the paddock set their limiters wrong on the same day, or did something go sideways on the FIA's side of the measurement? They're usually surgical about this stuff. This time something smells off. Gasly is the one I ache for — he crossed the line third for Alpine and got demoted to seventh by two separate five-second penalties. A Monaco podium, gone, over a handful of km/h.</p>
<p>I haven't even named everyone who got burned. Carlos Sainz — forgot him completely, poor guy, just unlucky from lights to flag. Hülkenberg picked up a penalty for tipping Carlos into trouble. The casualty list reads like a phone book.</p>
<h2>The footnotes nobody clapped for</h2>
<p>Nearly forgot the best little story of all: the final point. Fernando Alonso, P10 — Aston Martin's first point and first top-10 of the entire 2026 season. Let that land. Their year is so grim that one point, handed to them by someone else's penalty, counts as a party. They will not get this lucky twice.</p>
<p>Then there's Pérez, in a league of his own — only the league turned out to be the losers' one. How do you line up in the wrong grid box, <em>and then</em> fumble the restart, knowing full well you've already been warned? He was genuinely quick — close to winning the loser's cup outright — and then a false start dropped him to the back and stripped his P10, denying Cadillac their first-ever point. Second place in the championship nobody wants.</p>
<p>But honestly? Cadillac keep getting better and the Astons keep getting worse, so my prediction is they level it out at the bottom. And as much as Alonso charms me, it's no comfort — I reckon he and Stroll finish dead last in this loser's championship, together.</p>
<div class="section-divider"><span class="section-divider-ornament">***</span></div>
<h2>What Monaco actually needs</h2>
<blockquote>
<p>Everyone's begging for more overtakes at Monaco. Wrong fix. We don't want more passing — we want more carnage. And this year Monaco delivered.</p>
</blockquote>
<p>Here's my unpopular take, and my wife co-signs it. People moan that Monaco needs eight overtakes, ten, fifteen, whatever number makes a spreadsheet smile. Others want the V8s screaming back, as if engine noise ever won anyone a race. I could not care less about any of it. Give us drama. Give us a broken car on the podium, two heroes in the same wall, six speeding tickets, and a first point that feels like a trophy.</p>
<p>That is what makes this thing watchable for hamsters like us.</p>
<p>More drama. More spectacle. That's the whole review.</p>]]></content:encoded>
            <author>Alex Kucherenko</author>
        </item>
        <item>
            <title><![CDATA[The Birth of Moku Core]]></title>
            <link>https://alexblog.net/birth-of-moku-core/</link>
            <guid isPermaLink="false">https://alexblog.net/birth-of-moku-core/</guid>
            <pubDate>Wed, 18 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Smarter models promised everyone a blog engine in three hours for twenty bucks. And they deliver — beautiful, working-ish spaghetti that has no idea what it's doing. So I sat down and spent a month writing a spec instead of code.]]></description>
            <content:encoded><![CDATA[<p>The moment the models got even slightly smarter, everyone had the same thought at once: what if I could just generate the software I need and not pay a single cent to those programmers of yours? You tell the model: I want a blog engine. Make it beautiful, the fastest, the best, not a single bug — or you're going to jail. And three hours later, for a twenty-dollar subscription, you have everything you asked for. With zero mental effort on your end. You fire off a prompt and go re-watch old seasons of <em>House</em> — it feels new, because nothing interesting has shipped in about ten years anyway.</p>
<p>And of course, the moment that idea landed, everyone started bolting on clever little plugins, skills, call them what you like, anything to make the prompt behave, and behave long enough to deliver. There will be stages. There will be a review of every stage of the project. GSD was the first of these I tried, and I was genuinely stunned: it created the appearance of serious engineering. Then came a thousand more equally clever schemes for manufacturing activity and the feeling that the software you ordered is <em>almost</em> here. You have a plan. You have a spec. Everything is under control. The thing is so easy to drive that I sat my wife down in front of it, and she had no trouble at all; she only called me over to answer the technical questions. Tell me that's not a fairy tale.</p>
<h2>Not quite a fairy tale</h2>
<p>The idea is genius from every angle: pennies in, everything you ever wanted out, all rosy.</p>
<p>...Well. Not quite. And not so rosy. What you actually get goes more like this: it'll probably even boot, but the bug count will be spectacular. You'll debug till you're blue in the face, fixing it through the AI, naturally, and what you end up with is a hundred spaghetti functions. It sort of works, but at any given moment something somewhere is broken, visually or invisibly, and working out what's actually going on is no longer humanly possible.</p>
<p>And none of the generated stuff, I noticed, has a unifying concept. No idea of how the whole thing hangs together. Everything touches everything, every API gets yanked from everywhere, state gets dumped wherever it happened to land. That is not how you build software. Okay, fine, it is, people do. But if you want something that works <em>and</em> can be maintained and extended later, you need an idea of the architecture. A simple one, like a tree. Because the AI isn't fond of following instructions, so the architecture has to be obvious. To the human and to the machine.</p>
<h2>So Moku Core was born</h2>
<p>A system of plugins that together assemble an application. Each piece lives isolated inside its own plugin. What goes in and how it's configured is the entry point's call. How a plugin works on the inside is nobody's business, as long as it works and honours its contract.</p>
<pre class="shiki warm-syntax" style="background-color:#181412;color:#d4c8b8" tabindex="0"><code><span class="line"><span style="color:#F59E0B;font-style:italic">// A plugin is one self-contained contract: its config, its state, and the API it hands out.</span></span>
<span class="line"><span style="color:#F97316">export</span><span style="color:#F97316"> const</span><span style="color:#D4C8B8"> routerPlugin </span><span style="color:#F97316">=</span><span style="color:#FDE68A"> createPlugin</span><span style="color:#968B80">(</span><span style="color:#968B80">"</span><span style="color:#84CC16">router</span><span style="color:#968B80">"</span><span style="color:#968B80">,</span><span style="color:#968B80"> {</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F59E0B;font-style:italic">  // config — the defaults; every key becomes optional for whoever uses the plugin.</span></span>
<span class="line"><span style="color:#D4C8B8">  config</span><span style="color:#968B80">:</span><span style="color:#968B80"> {</span><span style="color:#D4C8B8"> basePath</span><span style="color:#968B80">:</span><span style="color:#968B80"> "</span><span style="color:#84CC16">/</span><span style="color:#968B80">"</span><span style="color:#968B80">,</span><span style="color:#D4C8B8"> notFoundRedirect</span><span style="color:#968B80">:</span><span style="color:#968B80"> "</span><span style="color:#84CC16">/404</span><span style="color:#968B80">"</span><span style="color:#968B80"> },</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F59E0B;font-style:italic">  // createState — private mutable state, owned by this plugin and nobody else.</span></span>
<span class="line"><span style="color:#FDE68A">  createState</span><span style="color:#968B80">:</span><span style="color:#968B80"> ()</span><span style="color:#F97316"> =></span><span style="color:#968B80"> ({</span><span style="color:#D4C8B8"> currentPath</span><span style="color:#968B80">:</span><span style="color:#968B80"> "</span><span style="color:#84CC16">/</span><span style="color:#968B80">"</span><span style="color:#968B80">,</span><span style="color:#D4C8B8"> history</span><span style="color:#968B80">:</span><span style="color:#968B80"> []</span><span style="color:#F97316"> as</span><span style="color:#FDBA74"> string</span><span style="color:#968B80">[]</span><span style="color:#968B80"> }),</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F59E0B;font-style:italic">  // events — declare what this plugin emits, with typed payloads.</span></span>
<span class="line"><span style="color:#FDE68A">  events</span><span style="color:#968B80">:</span><span style="color:#968B80"> (</span><span style="color:#D4C8B8">register</span><span style="color:#968B80">)</span><span style="color:#F97316"> =></span><span style="color:#968B80"> ({</span></span>
<span class="line"><span style="color:#968B80">    "</span><span style="color:#84CC16">router:navigate</span><span style="color:#968B80">"</span><span style="color:#968B80">:</span><span style="color:#FDE68A"> register</span><span style="color:#968B80">&#x3C;{</span><span style="color:#D4C8B8"> from</span><span style="color:#F97316">:</span><span style="color:#FDBA74"> string</span><span style="color:#968B80">;</span><span style="color:#D4C8B8"> to</span><span style="color:#F97316">:</span><span style="color:#FDBA74"> string</span><span style="color:#968B80"> }>(</span><span style="color:#968B80">"</span><span style="color:#84CC16">Fired after navigation</span><span style="color:#968B80">"</span><span style="color:#968B80">)</span></span>
<span class="line"><span style="color:#968B80">  }),</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F59E0B;font-style:italic">  // api — the public surface, mounted on `app.router`.</span></span>
<span class="line"><span style="color:#FDE68A">  api</span><span style="color:#968B80">:</span><span style="color:#968B80"> (</span><span style="color:#D4C8B8">ctx</span><span style="color:#968B80">)</span><span style="color:#F97316"> =></span><span style="color:#968B80"> ({</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F59E0B;font-style:italic">    // Become: app.router.navigate("/about");</span></span>
<span class="line"><span style="color:#FDE68A">    navigate</span><span style="color:#968B80">:</span><span style="color:#968B80"> (</span><span style="color:#D4C8B8">path</span><span style="color:#F97316">:</span><span style="color:#FDBA74"> string</span><span style="color:#968B80">)</span><span style="color:#F97316"> =></span><span style="color:#968B80"> {</span></span>
<span class="line"><span style="color:#D4C8B8">      ctx</span><span style="color:#968B80">.</span><span style="color:#D4C8B8">state</span><span style="color:#968B80">.</span><span style="color:#D4C8B8">history</span><span style="color:#968B80">.</span><span style="color:#FDE68A">push</span><span style="color:#968B80">(</span><span style="color:#D4C8B8">ctx</span><span style="color:#968B80">.</span><span style="color:#D4C8B8">state</span><span style="color:#968B80">.</span><span style="color:#D4C8B8">currentPath</span><span style="color:#968B80">);</span><span style="color:#F59E0B;font-style:italic"> // remember where we were</span></span>
<span class="line"><span style="color:#D4C8B8">      ctx</span><span style="color:#968B80">.</span><span style="color:#D4C8B8">state</span><span style="color:#968B80">.</span><span style="color:#D4C8B8">currentPath </span><span style="color:#F97316">=</span><span style="color:#D4C8B8"> path</span><span style="color:#968B80">;</span><span style="color:#F59E0B;font-style:italic"> // move to the new path</span></span>
<span class="line"><span style="color:#F59E0B;font-style:italic">      // emit — announce it so any plugin listening to "router:navigate" can react</span></span>
<span class="line"><span style="color:#D4C8B8">      ctx</span><span style="color:#968B80">.</span><span style="color:#FDE68A">emit</span><span style="color:#968B80">(</span><span style="color:#968B80">"</span><span style="color:#84CC16">router:navigate</span><span style="color:#968B80">"</span><span style="color:#968B80">,</span><span style="color:#968B80"> {</span><span style="color:#D4C8B8"> from</span><span style="color:#968B80">:</span><span style="color:#D4C8B8"> ctx</span><span style="color:#968B80">.</span><span style="color:#D4C8B8">state</span><span style="color:#968B80">.</span><span style="color:#D4C8B8">history</span><span style="color:#968B80">.</span><span style="color:#FDE68A">at</span><span style="color:#968B80">(</span><span style="color:#F97316">-</span><span style="color:#FDA4AF">1</span><span style="color:#968B80">)</span><span style="color:#F97316">!</span><span style="color:#968B80">,</span><span style="color:#D4C8B8"> to</span><span style="color:#968B80">:</span><span style="color:#D4C8B8"> path </span><span style="color:#968B80">});</span></span>
<span class="line"><span style="color:#968B80">    },</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F59E0B;font-style:italic">    // Become: app.router.current();</span></span>
<span class="line"><span style="color:#FDE68A">    current</span><span style="color:#968B80">:</span><span style="color:#968B80"> ()</span><span style="color:#F97316"> =></span><span style="color:#D4C8B8"> ctx</span><span style="color:#968B80">.</span><span style="color:#D4C8B8">state</span><span style="color:#968B80">.</span><span style="color:#D4C8B8">currentPath </span><span style="color:#F59E0B;font-style:italic">// read the current path</span></span>
<span class="line"><span style="color:#968B80">  })</span></span>
<span class="line"><span style="color:#968B80">});</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F59E0B;font-style:italic">// Subscribing — another plugin depends on router and reacts to its events:</span></span>
<span class="line"><span style="color:#F97316">export</span><span style="color:#F97316"> const</span><span style="color:#D4C8B8"> analyticsPlugin </span><span style="color:#F97316">=</span><span style="color:#FDE68A"> createPlugin</span><span style="color:#968B80">(</span><span style="color:#968B80">"</span><span style="color:#84CC16">analytics</span><span style="color:#968B80">"</span><span style="color:#968B80">,</span><span style="color:#968B80"> {</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F59E0B;font-style:italic">  // defaults again — the entry point overrides this below</span></span>
<span class="line"><span style="color:#D4C8B8">  config</span><span style="color:#968B80">:</span><span style="color:#968B80"> {</span><span style="color:#D4C8B8"> trackingId</span><span style="color:#968B80">:</span><span style="color:#968B80"> ""</span><span style="color:#968B80"> },</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F59E0B;font-style:italic">  // unlocks the typed "router:*" events below</span></span>
<span class="line"><span style="color:#D4C8B8">  depends</span><span style="color:#968B80">:</span><span style="color:#968B80"> [</span><span style="color:#D4C8B8">routerPlugin</span><span style="color:#968B80">],</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F59E0B;font-style:italic">  // runs on every "router:navigate" — the payload type comes from the declaration</span></span>
<span class="line"><span style="color:#FDE68A">  hooks</span><span style="color:#968B80">:</span><span style="color:#968B80"> (</span><span style="color:#D4C8B8">ctx</span><span style="color:#968B80">)</span><span style="color:#F97316"> =></span><span style="color:#968B80"> ({</span></span>
<span class="line"><span style="color:#968B80">    "</span><span style="color:#84CC16">router:navigate</span><span style="color:#968B80">"</span><span style="color:#968B80">:</span><span style="color:#968B80"> ({</span><span style="color:#D4C8B8"> from</span><span style="color:#968B80">,</span><span style="color:#D4C8B8"> to </span><span style="color:#968B80">})</span><span style="color:#F97316"> =></span></span>
<span class="line"><span style="color:#D4C8B8">      console</span><span style="color:#968B80">.</span><span style="color:#FDE68A">log</span><span style="color:#968B80">(</span><span style="color:#968B80">`</span><span style="color:#84CC16">[</span><span style="color:#968B80">${</span><span style="color:#D4C8B8">ctx</span><span style="color:#968B80">.</span><span style="color:#D4C8B8">config</span><span style="color:#968B80">.</span><span style="color:#D4C8B8">trackingId</span><span style="color:#968B80">}</span><span style="color:#84CC16">] page view: </span><span style="color:#968B80">${</span><span style="color:#D4C8B8">from</span><span style="color:#968B80">}</span><span style="color:#84CC16"> -> </span><span style="color:#968B80">${</span><span style="color:#D4C8B8">to</span><span style="color:#968B80">}`</span><span style="color:#968B80">)</span></span>
<span class="line"><span style="color:#968B80">  })</span></span>
<span class="line"><span style="color:#968B80">});</span></span></code></pre>
<p>Every plugin declares its contract: its state, the events it subscribes to, the API it exposes, and the helpers it throws out into the open. Which means that by reading one file, you can tell exactly how badly the AI botched the design of that plugin. And the entry point just gathers them up:</p>
<pre class="shiki warm-syntax" style="background-color:#181412;color:#d4c8b8" tabindex="0"><code><span class="line"><span style="color:#F59E0B;font-style:italic">// The entry point decides what goes in and how it's configured:</span></span>
<span class="line"><span style="color:#F97316">const</span><span style="color:#D4C8B8"> app </span><span style="color:#F97316">=</span><span style="color:#FDE68A"> createApp</span><span style="color:#968B80">({</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F59E0B;font-style:italic">  // order matters — analytics depends on router, so router comes first</span></span>
<span class="line"><span style="color:#D4C8B8">  plugins</span><span style="color:#968B80">:</span><span style="color:#968B80"> [</span><span style="color:#D4C8B8">routerPlugin</span><span style="color:#968B80">,</span><span style="color:#D4C8B8"> analyticsPlugin</span><span style="color:#968B80">,</span><span style="color:#D4C8B8"> blogPlugin</span><span style="color:#968B80">],</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D4C8B8">  pluginConfigs</span><span style="color:#968B80">:</span><span style="color:#968B80"> {</span></span>
<span class="line"><span style="color:#D4C8B8">    router</span><span style="color:#968B80">:</span><span style="color:#968B80"> {</span><span style="color:#D4C8B8"> basePath</span><span style="color:#968B80">:</span><span style="color:#968B80"> "</span><span style="color:#84CC16">/blog</span><span style="color:#968B80">"</span><span style="color:#968B80"> },</span><span style="color:#F59E0B;font-style:italic"> // overrides the "/" default declared by the plugin</span></span>
<span class="line"><span style="color:#D4C8B8">    analytics</span><span style="color:#968B80">:</span><span style="color:#968B80"> {</span><span style="color:#D4C8B8"> trackingId</span><span style="color:#968B80">:</span><span style="color:#968B80"> "</span><span style="color:#84CC16">G-XXXXX</span><span style="color:#968B80">"</span><span style="color:#968B80"> },</span></span>
<span class="line"><span style="color:#D4C8B8">    blog</span><span style="color:#968B80">:</span><span style="color:#968B80"> {</span><span style="color:#D4C8B8"> postsPerPage</span><span style="color:#968B80">:</span><span style="color:#FDA4AF"> 5</span><span style="color:#968B80"> }</span></span>
<span class="line"><span style="color:#968B80">  }</span></span>
<span class="line"><span style="color:#968B80">});</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F59E0B;font-style:italic">// In client code you just call the typed API — autocompleted, no imports, no globals:</span></span>
<span class="line"><span style="color:#D4C8B8">app</span><span style="color:#968B80">.</span><span style="color:#D4C8B8">router</span><span style="color:#968B80">.</span><span style="color:#FDE68A">navigate</span><span style="color:#968B80">(</span><span style="color:#968B80">"</span><span style="color:#84CC16">/about</span><span style="color:#968B80">"</span><span style="color:#968B80">);</span><span style="color:#F59E0B;font-style:italic"> // analytics logs: [G-XXXXX] page view: / -> /about</span></span>
<span class="line"><span style="color:#D4C8B8">app</span><span style="color:#968B80">.</span><span style="color:#D4C8B8">router</span><span style="color:#968B80">.</span><span style="color:#FDE68A">current</span><span style="color:#968B80">();</span><span style="color:#F59E0B;font-style:italic"> // "/about"</span></span>
<span class="line"><span style="color:#D4C8B8">app</span><span style="color:#968B80">.</span><span style="color:#D4C8B8">blog</span><span style="color:#968B80">.</span><span style="color:#FDE68A">listPosts</span><span style="color:#968B80">();</span><span style="color:#F59E0B;font-style:italic"> // 5 per page — straight from the config above</span></span></code></pre>
<p>You can extend them, complicate them. There's a way to test them deterministically. And the whole thing is packed as minimally as it goes, plus it hands you the freshest guarantees there are, like type safety — the kind TypeScript can actually prove. The point is to shrink the room for error as hard as the language will let you.</p>
<h2>A month on a spec, not on code</h2>
<p>I sat with this idea for about a month, not over code, over the spec. Mostly I had the AI model different situations against my API. I've tried to build a plugin system like this many times before, at work and in my own game engines; the idea keeps resurfacing. Case in point: Beavy, my dream project — a game engine in Rust. I think it's genuinely brilliant, and I draw inspiration from it (lying) at every opportunity.</p>
<p>The spec took a month. I had to work through a pile of variants: how to run it in the browser, how from the console, how on Node, how to make things isomorphic, how to drive coupling down to nothing. I have never in my life sweated so hard over documentation and the endless debugging of this stuff. The AI writes docs beautifully. Coding, though, is not the AI's strong suit.</p>
<h2>Three layers</h2>
<p>I also arrived at the idea that the structure has to be three layers deep.</p>
<figure class="mermaid-diagram"><svg id="mermaid-0" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 276px;" viewBox="0 0 276 381.5" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#mermaid-0{font-family:"Fira Code","IBM Plex Mono",monospace;font-size:13px;fill:#d4c8b8;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-0 .error-icon{fill:#1c1917;}#mermaid-0 .error-text{fill:#d4c8b8;stroke:#d4c8b8;}#mermaid-0 .edge-thickness-normal{stroke-width:1px;}#mermaid-0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-0 .marker{fill:#968b80;stroke:#968b80;}#mermaid-0 .marker.cross{stroke:#968b80;}#mermaid-0 svg{font-family:"Fira Code","IBM Plex Mono",monospace;font-size:13px;}#mermaid-0 p{margin:0;}#mermaid-0 .label{font-family:"Fira Code","IBM Plex Mono",monospace;color:#d4c8b8;}#mermaid-0 .cluster-label text{fill:#d4c8b8;}#mermaid-0 .cluster-label span{color:#d4c8b8;}#mermaid-0 .cluster-label span p{background-color:transparent;}#mermaid-0 .label text,#mermaid-0 span{fill:#d4c8b8;color:#d4c8b8;}#mermaid-0 .node rect,#mermaid-0 .node circle,#mermaid-0 .node ellipse,#mermaid-0 .node polygon,#mermaid-0 .node path{fill:#231f1b;stroke:#3d3530;stroke-width:1px;}#mermaid-0 .rough-node .label text,#mermaid-0 .node .label text,#mermaid-0 .image-shape .label,#mermaid-0 .icon-shape .label{text-anchor:middle;}#mermaid-0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-0 .rough-node .label,#mermaid-0 .node .label,#mermaid-0 .image-shape .label,#mermaid-0 .icon-shape .label{text-align:center;}#mermaid-0 .node.clickable{cursor:pointer;}#mermaid-0 .root .anchor path{fill:#968b80!important;stroke-width:0;stroke:#968b80;}#mermaid-0 .arrowheadPath{fill:#e7ebed;}#mermaid-0 .edgePath .path{stroke:#968b80;stroke-width:1px;}#mermaid-0 .flowchart-link{stroke:#968b80;fill:none;}#mermaid-0 .edgeLabel{background-color:#292420;text-align:center;}#mermaid-0 .edgeLabel p{background-color:#292420;}#mermaid-0 .edgeLabel rect{opacity:0.5;background-color:#292420;fill:#292420;}#mermaid-0 .labelBkg{background-color:rgba(41, 36, 32, 0.5);}#mermaid-0 .cluster rect{fill:#1c1917;stroke:#3d3530;stroke-width:1px;}#mermaid-0 .cluster text{fill:#d4c8b8;}#mermaid-0 .cluster span{color:#d4c8b8;}#mermaid-0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"Fira Code","IBM Plex Mono",monospace;font-size:12px;background:#1c1917;border:1px solid #3d3530;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#d4c8b8;}#mermaid-0 rect.text{fill:none;stroke-width:0;}#mermaid-0 .icon-shape,#mermaid-0 .image-shape{background-color:#292420;text-align:center;}#mermaid-0 .icon-shape p,#mermaid-0 .image-shape p{background-color:#292420;padding:2px;}#mermaid-0 .icon-shape .label rect,#mermaid-0 .image-shape .label rect{opacity:0.5;background-color:#292420;fill:#292420;}#mermaid-0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-0 .node .neo-node{stroke:#3d3530;}#mermaid-0 [data-look="neo"].node rect,#mermaid-0 [data-look="neo"].cluster rect,#mermaid-0 [data-look="neo"].node polygon{stroke:url(#mermaid-0-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#mermaid-0 [data-look="neo"].node path{stroke:url(#mermaid-0-gradient);stroke-width:1px;}#mermaid-0 [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#mermaid-0 [data-look="neo"].node .neo-line path{stroke:#3d3530;filter:none;}#mermaid-0 [data-look="neo"].node circle{stroke:url(#mermaid-0-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#mermaid-0 [data-look="neo"].node circle .state-start{fill:#000000;}#mermaid-0 [data-look="neo"].icon-shape .icon{fill:url(#mermaid-0-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#mermaid-0 [data-look="neo"].icon-shape .icon-neo path{stroke:url(#mermaid-0-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#mermaid-0 :root{--mermaid-font-family:arial,sans-serif;}#mermaid-0 .l1>*{fill:rgb(35, 31, 27)!important;stroke:rgb(249, 115, 22)!important;color:rgb(253, 186, 116)!important;}#mermaid-0 .l1 span{fill:rgb(35, 31, 27)!important;stroke:rgb(249, 115, 22)!important;color:rgb(253, 186, 116)!important;}#mermaid-0 .l1 tspan{fill:rgb(253, 186, 116)!important;}#mermaid-0 .l2>*{fill:rgb(35, 31, 27)!important;stroke:rgb(245, 158, 11)!important;color:rgb(253, 230, 138)!important;}#mermaid-0 .l2 span{fill:rgb(35, 31, 27)!important;stroke:rgb(245, 158, 11)!important;color:rgb(253, 230, 138)!important;}#mermaid-0 .l2 tspan{fill:rgb(253, 230, 138)!important;}#mermaid-0 .l3>*{fill:rgb(35, 31, 27)!important;stroke:rgb(132, 204, 22)!important;color:rgb(217, 249, 157)!important;}#mermaid-0 .l3 span{fill:rgb(35, 31, 27)!important;stroke:rgb(132, 204, 22)!important;color:rgb(217, 249, 157)!important;}#mermaid-0 .l3 tspan{fill:rgb(217, 249, 157)!important;}</style><g><marker id="mermaid-0_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-0_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-0_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-0_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"></polygon></marker><marker id="mermaid-0_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></circle></marker><marker id="mermaid-0_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></circle></marker><marker id="mermaid-0_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"></circle></marker><marker id="mermaid-0_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"></circle></marker><marker id="mermaid-0_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-0_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-0_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"></path></marker><marker id="mermaid-0_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"></path></marker><g class="root"><g class="clusters"></g><g class="edgePaths"><path d="M138,285L138,280.833C138,276.667,138,268.333,138,260.667C138,253,138,246,138,242.5L138,239" id="mermaid-0-L_core_fw_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_core_fw_0" data-points="W3sieCI6MTM4LCJ5IjoyODV9LHsieCI6MTM4LCJ5IjoyNjB9LHsieCI6MTM4LCJ5IjoyMzV9XQ==" data-look="classic" marker-end="url(#mermaid-0_flowchart-v2-pointEnd)"></path><path d="M138,146.5L138,142.333C138,138.167,138,129.833,138,122.167C138,114.5,138,107.5,138,104L138,100.5" id="mermaid-0-L_fw_app_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_fw_app_0" data-points="W3sieCI6MTM4LCJ5IjoxNDYuNX0seyJ4IjoxMzgsInkiOjEyMS41fSx7IngiOjEzOCwieSI6OTYuNX1d" data-look="classic" marker-end="url(#mermaid-0_flowchart-v2-pointEnd)"></path></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_core_fw_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_fw_app_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default l1" id="mermaid-0-flowchart-core-0" data-look="classic" transform="translate(138, 329.25)"><rect class="basic label-container" style="fill:#231f1b !important;stroke:#f97316 !important" x="-130" y="-44.25" width="260" height="88.5"></rect><g class="label" style="color:#fdba74 !important" transform="translate(-100, -29.25)"><rect></rect><foreignObject width="200" height="58.5"><div style="color: rgb(253, 186, 116) !important; display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;" xmlns="http://www.w3.org/1999/xhtml"><span style="color:#fdba74 !important" class="nodeLabel"><p>Layer 1 · @moku-labs/core<br></br>the kernel. Ascetic. Pure machinery.</p></span></div></foreignObject></g></g><g class="node default l2" id="mermaid-0-flowchart-fw-1" data-look="classic" transform="translate(138, 190.75)"><rect class="basic label-container" style="fill:#231f1b !important;stroke:#f59e0b !important" x="-130" y="-44.25" width="260" height="88.5"></rect><g class="label" style="color:#fde68a !important" transform="translate(-100, -29.25)"><rect></rect><foreignObject width="200" height="58.5"><div style="color: rgb(253, 230, 138) !important; display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;" xmlns="http://www.w3.org/1999/xhtml"><span style="color:#fde68a !important" class="nodeLabel"><p>Layer 2 · Framework<br></br>web, client, whatever. Patterns live here.</p></span></div></foreignObject></g></g><g class="node default l3" id="mermaid-0-flowchart-app-2" data-look="classic" transform="translate(138, 52.25)"><rect class="basic label-container" style="fill:#231f1b !important;stroke:#84cc16 !important" x="-130" y="-44.25" width="260" height="88.5"></rect><g class="label" style="color:#d9f99d !important" transform="translate(-100, -29.25)"><rect></rect><foreignObject width="200" height="58.5"><div style="color: rgb(217, 249, 157) !important; display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;" xmlns="http://www.w3.org/1999/xhtml"><span style="color:#d9f99d !important" class="nodeLabel"><p>Layer 3 · Application<br></br>the app itself. Minimal. Just the visuals.</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="mermaid-0-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"></feDropShadow></filter></defs><defs><filter id="mermaid-0-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"></feDropShadow></filter></defs><linearGradient id="mermaid-0-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#3d3530" stop-opacity="1"></stop><stop offset="100%" stop-color="#3d3530" stop-opacity="1"></stop></linearGradient></svg></figure>
<p>The kernel stays ascetic. The framework on top of it exists to hoard all the patterns of one specific kind of software (web, a client app, whatever), purely to spare you the endless AI-debugging when you get to the app. And the app on top of that just uses the framework. So the bottom is a licked-clean kernel; one floor up we relax a little and generate a ton of code, sorted neatly by plugin; and at the top sits client code that answers for the visual part alone. The interface, roughly speaking.</p>
<h2>For now, it's all theory</h2>
<p>And that is how the project was born.</p>
<p>One thing to keep in mind: there is no second layer yet, and no third. It's all pure theory. I'm hoping they show up soon, so I can finally test the architecture I've been dreaming about.</p>]]></content:encoded>
            <author>Alex Kucherenko</author>
        </item>
        <item>
            <title><![CDATA[Eight Days of Unscrewing a Game Out of an AI]]></title>
            <link>https://alexblog.net/screw-master/</link>
            <guid isPermaLink="false">https://alexblog.net/screw-master/</guid>
            <pubDate>Wed, 07 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[My company announced an AI week. My wife, at that exact moment, was unscrewing little bolts on her phone. I put the two together and gave myself exactly eight days: all code and all art by AI, me just conducting.]]></description>
            <content:encoded><![CDATA[<p>My company announced an AI week: every developer could try AI for any purpose they wanted. Subscriptions, any tools we asked for — within reason, obviously. And my wife, at that exact moment, was playing a game where you unscrew little bolts. I don't know what the genre is called. Some kind of puzzle: you unscrew bolts in the right order, sort them into colored trays, and basically win by unscrewing bolts.</p>
<p>I studied the mechanic and set myself a task. I had exactly eight days — no more, no less. I had to make the game with AI as fast as possible, at the best quality possible. All the code, all the graphics, the concept art, everything — AI. Me, I only orchestrate: I organize the gameplay and decide what gets done and how. That's how Screw Master happened: my first attempt at making a game without making it with my own hands.</p>
<h2>I'm Not an Animal — I Don't Poke Buttons</h2>
<p>I started with the code: ODIE (one of our internal engines, a classic ECS) with PixiJS 8 doing the rendering. The first thing I did was tighten every TypeScript validation as strictly as it would go, and I decided up front: testing would be automatic, and the game would be built from day one with self-verification in mind. I'm not an animal — I don't poke buttons by hand.</p>
<p>The idea was this: Playwright plays the game. It gets the whole harness and the instrumentation to do it: access to input, access to the render graph, access to the ECS. Every feature is proven by video: the AI builds something, opens a PR, attaches a recording — I watch it, decide whether I like what I see, and give feedback. No "trust me, it works." Here's the footage, see for yourself.</p>
<p>And here, in fact, is what came out of it. You can unscrew a few bolts without leaving the article:</p>
<figure class="lazy-embed" data-component="lazy-embed" data-embed-src="https://screw-master.kucherenko-email.workers.dev/" data-embed-title="Screw Master"><button type="button" class="lazy-embed-button" aria-label="Load embed: Screw Master"><span class="lazy-embed-title">Screw Master</span></button></figure>
<h2>Figma, MCP, and the Battle for Sanity</h2>
<p>The entire interface went through Figma. I assembled the scenes the way they were supposed to look, then the Figma MCP gave the model access to the data and the assets — and the UI got built from that.</p>
<p>A whole separate problem was teaching that MCP and the model to be friends and not build nonsense. I had to go deep into Figma: structure every component with ruthless logic, so that what came out the other end was something sane instead of gibberish. I spent ages picking components so they'd all sit in more or less one style. Generating identical models in a different color — same suffering.</p>
<h2>Bugs Are What AI Generates Best</h2>
<p>And now for the biggest problem of all.</p>
<blockquote>
<p>Bugs are what AI generates best. And what bugs — honestly, you just stand there marveling.</p>
</blockquote>
<p>The real challenge was even <em>explaining</em> what the bug was. Especially when the AI had to do something concrete: say, several animations have to drop bolts onto their slots <em>at the same time</em>. It simply lost its mind over that. No, you can't do it the pretty way, with a queue or a stack — you need exactly this weird contraption and nothing else: cancelling promises, pausing them. Basically the most direct way to pile up problems for yourself. Hundreds of prompts. Pure pain.</p>
<h2>So, Can You Make Games This Way?</h2>
<p>I'd say the experiment ended on a positive note: I took it all the way to the finish. What I learned for myself: building a prototype with AI is completely fine. Building a product… that I honestly don't know. You'd have to be cocky. Very, <em>very</em> cocky.</p>
<p>Image generation wasn't great either: you'd order red bolts and get mushrooms. Why? No idea. Maybe the shape, maybe the color. But like everything in this world, enough work, nerves, and swearing gets you something more or less not-terrible.</p>
<p>Is it faster to just do it by hand? For code — the other way around, actually: by hand is faster. But only if you don't count the tests, of which there are more than the code itself — so, nuances. For the art — I could never have drawn it myself, but any artist would have done it better. So draw your own conclusions. The bar will probably rise, and projects like this will become genuinely possible. Right now, the level of quality AI can carry is an MVP or a prototype. Past that, it's a lottery.</p>
<p>But I learned a lot for myself: Figma, how to do e2e tests — and how not to do them. So I count the experience as a win. I'd love to have more time for things like this, but alas: a day has 24 hours, food doesn't earn itself, and the kid needs to get to school. So I had my fun. That'll do.</p>]]></content:encoded>
            <author>Alex Kucherenko</author>
        </item>
        <item>
            <title><![CDATA[What's the Best Board Game?]]></title>
            <link>https://alexblog.net/best-board-games/</link>
            <guid isPermaLink="false">https://alexblog.net/best-board-games/</guid>
            <pubDate>Sat, 26 Aug 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Three hundred-odd boxes later, people still ask me what to play. Fine. Here's a pick of games across different categories that won't let you down.]]></description>
            <content:encoded><![CDATA[<p>People ask me this all the time. At its peak my collection ran to <a href="https://boardgamegeek.com/collection/user/AlexKucherenko">three hundred-something boxes</a>, I've played through a lot of them, trust me, and the question "so what should we play, what do you recommend?" has been chasing me for years. So I decided to put together a few games, across different categories, that won't let you down.</p>
<h2>Gloomhaven: Jaws of the Lion</h2>
<p><a href="https://boardgamegeek.com/boardgame/291457/gloomhaven-jaws-of-the-lion">A masterpiece of a dungeon crawler</a>. On paper it's a fairly brainy game and you need to know how to play it, but god, the balance, the synergy between the cards and your character. I don't know how they do it, but you win on your last card, on your last hit point. And when you lose, you always know exactly what you did wrong. Tons of scenarios, four characters in the box with real progression, a quick and painless setup, and the best part — the price: fifty bucks for a good fifty hours of content! I don't recommend its big brother with a 120-hour campaign, or the new Frosthaven, which runs god knows how long, because realistically you don't have that much life left, and your friends certainly don't. Components, gameplay, replayability, and that price — I don't think anything has topped this little box. An absolutely brilliant game, can't recommend it enough. My wife and I run it as a two-player game; getting a bigger party together is the hard part.</p>
<p><img src="https://alexblog.net/best-board-games/images/gloomhaven-jotl.webp" alt="Official spread of Gloomhaven: Jaws of the Lion: the box surrounded by the scenario book, character mats, cards, standees and miniatures" loading="lazy"></p>
<h2>Terraforming Mars</h2>
<p><a href="https://boardgamegeek.com/boardgame/167791/terraforming-mars">Terraforming Mars</a> is something else. Over two hundred cards, unbelievable combinations of things you can build, and no two games ever look alike. I don't know what you call the genre where you get a huge pile of cards and try to squeeze the maximum out of a random deal every time, but every game is a battle with my wife and every game is a new experience. Fair warning: the standard edition has some issues with component quality and the player boards, so buy the organizer and the proper dual-layer boards right away. I also picked up metal cubes, but that's pure indulgence. As a two-player game it's an absolute hit in our family.</p>
<p><img src="https://alexblog.net/best-board-games/images/terraforming-mars.webp" alt="Our Terraforming Mars game in progress: the board, wooden organizer trays and player mats" loading="lazy">
<img src="https://alexblog.net/best-board-games/images/terraforming-mars-2.webp" alt="Project cards, organizer trays and metal resource cubes up close" loading="lazy"></p>
<h2>Clank!</h2>
<p>The <a href="https://boardgamegeek.com/boardgame/201808/clank-a-deck-building-adventure">Clank!</a> series, from the regular one to the legacy version: grab any of them, you can't go wrong. Very simple, very fun, best with three to five players. It doesn't run long, if you don't drag it out, and there are plenty of ways to throw a friend under the bus. What else do you need.</p>
<p><img src="https://alexblog.net/best-board-games/images/clank.webp" alt="Clank! box with the dungeon board, meeples and dragon silhouette in the publisher&#x27;s render" loading="lazy"></p>
<h2>Detective</h2>
<p>My wife and I adore detective shows: Castle, The Mentalist, Bones, Columbo (if you're old enough) — all rewatched twenty times over. And there are board games that let you step into those detectives' shoes and try to crack a case yourself. There are quite a few, some harder than others, but the one that stuck with me is <a href="https://boardgamegeek.com/boardgame/223321/detective-a-modern-crime-board-game">Detective: A Modern Crime Board Game</a>. You'll need a computer. The game essentially hands you a simulated police database and leans heavily on the actual internet for solving its puzzles. I can't spoil any more than that, sadly. You have to play it yourself and enjoy the process. Not all the cases are equally good, but some are genuinely excellent.</p>
<p><img src="https://alexblog.net/best-board-games/images/detective.webp" alt="Detective board close-up: the city map with the time track, the Case 1 &#x22;A Man with a Golden Watch&#x22; case book and stacks of clue tokens" loading="lazy"></p>
<h2>Unlock!</h2>
<p>Escape rooms: you can play these at the table instead of grimy basements and secret rooms in apartment blocks (those who've been know exactly what I mean). Sure, the weird rooms and flats have their charm, all those physical gimmicks. But if you really want to play at home, the <a href="https://boardgamegeek.com/boardgame/213460/unlock-escape-adventures">Unlock!</a> series is our favorite: clever, logical escape quests (hi, Exit), an app that gives you hints and makes the whole thing more interactive, and a different style and theme every time — you genuinely feel the atmosphere of each adventure. The downside, same as every game of this kind: they're one-shot. Once you've solved it, sell it, gift it, or shelve it until you forget what was inside.</p>
<p><img src="https://alexblog.net/best-board-games/images/unlock.webp" alt="Four players leaning over Unlock! cards, with the box and the companion-app tablet nearby" loading="lazy"></p>
<h2>Brass: Birmingham</h2>
<p>I love economic games: money, income, all of that. But I despise Monopoly — a game where you literally decide nothing and nothing depends on you. It's nonsense; how people keep fussing over it, and why, is beyond me. Thank god there are proper games where your decisions matter and decide the outcome, and most importantly, shape the outcome for your opponents. The <a href="https://boardgamegeek.com/boardgame/224517/brass-birmingham">Brass</a> series by Martin Wallace is my favorite. The components are superb: the deluxe version has literal poker chips, the board looks downright gorgeous, and the whole art direction is stylish as hell. Very unusual mechanics (this isn't your dice-chucking) and the best interaction model I've seen in this kind of game: everyone at the table depends on everyone else and on each other's decisions. I've seen proper backstabbing there, and I've seen genius moves where you literally force your opponent to play the way you need them to. Nobody here gets to quietly tend their own garden: it's direct competition inside one interconnected system. Highly recommended. Two to four players, but four is where it gets really interesting.</p>
<p><img src="https://alexblog.net/best-board-games/images/brass-birmingham.webp" alt="Brass: Birmingham mid-game: the dark canal-era board with industry tiles, coins and player mats" loading="lazy"></p>
<h2>London</h2>
<p>My little love, and Martin Wallace again, see above. <a href="https://boardgamegeek.com/boardgame/236191/london-second-edition">London</a> is a small box with a ridiculous number of clever mechanics inside. It's strictly a card game: there is a board, but it's nominal. There's a caveat, of course. Know the deck well and some cards are clearly better than others. I honestly can't explain why I love it so much, but I do, for the life of me. It's a balancing act between wealth and poverty, where you rake in a pile of money and then blow it all on other cards, where you live on the edge between failure and success. One wrong decision and you're simply done; one right one, plus some luck with the cards, and you're riding high. That emotional ride is probably why I love it. Strictly two players. And I won't even recommend it: it's my game, and good luck finding a copy these days anyway. But if you ever see one — take it, you won't regret it.</p>
<p><img src="https://alexblog.net/best-board-games/images/london.webp" alt="First-edition London mid-game: the city map with wooden buildings, rows of cards and loan tokens" loading="lazy"></p>
<h2>Through the Ages</h2>
<p>The monster civilization game, the mother and father of all civilization games. <a href="https://boardgamegeek.com/boardgame/182028/through-the-ages-a-new-story-of-civilization">Through the Ages</a> is a minimum, again, a <em>minimum</em> of eight hours of uninterrupted play with two players. Don't even attempt it with three or four: you will never, ever finish. The game has three small boards, no world map at all, really just mats for holding playing cards. And hundreds of cards: war is in there, peace, science, culture. How any of this works, I have no idea, but it works. You sit down intending to quietly build a civilization with your wife and see who scores more points, and it all rapidly turns into a bloodbath: the constant threat of war, resource shortages, the fight for colonies. And all of it on ordinary playing cards. How, tell me, how does this all work?! I can only recommend it if you're a proper geek. Not just any geek — that exact breed of nerd who's ready to fight with a friend or a spouse for eight hours straight — and not fall out, and not get divorced, when that snake declares war on you out of nowhere.</p>
<p><img src="https://alexblog.net/best-board-games/images/through-the-ages.webp" alt="Our Through the Ages session: card rows, a civilization board and resource cubes" loading="lazy">
<img src="https://alexblog.net/best-board-games/images/through-the-ages-2.webp" alt="The full Through the Ages table: card rows and player boards" loading="lazy">
<img src="https://alexblog.net/best-board-games/images/through-the-ages-3.webp" alt="Through the Ages card row up close: Moses and the score track" loading="lazy"></p>
<h2>Mage Knight</h2>
<p>Some games have mechanics better than the game itself. The deck-building in <a href="https://boardgamegeek.com/boardgame/96848/mage-knight-board-game">Mage Knight</a> is absolutely brilliant, but everything around it is so convoluted that I'm not prepared to actually play it. I just sit down sometimes purely for the deck-building system itself, for that card synergy: chaining combo after combo, wriggling like an eel in a frying pan to squeeze out one more play and deal damage to some nameless monster. At its core it's tabletop Heroes of Might and Magic: you move, you flip a tile, you hire units, you kill monsters — that's it, there's nothing else. I could spend hours criticizing this game: how tangled it is, how dull the missions are, how arbitrary the random monsters feel. But god, the card mechanics in there are gorgeous. Oh, if only there were an option to get this engine in some other wrapper.</p>
<p><img src="https://alexblog.net/best-board-games/images/mage-knight.webp" alt="Our Mage Knight session: the hex map with hero miniatures and enemy tokens" loading="lazy">
<img src="https://alexblog.net/best-board-games/images/mage-knight-2.webp" alt="Mage Knight fame board, cards and skill tokens" loading="lazy">
<img src="https://alexblog.net/best-board-games/images/mage-knight-3.webp" alt="The full Mage Knight table: hex map, fame board and fans of cards" loading="lazy"></p>
<h2>Alchemists</h2>
<p>There's this thing called a puzzle. Now imagine solving a puzzle while playing a board game: that's <a href="https://boardgamegeek.com/boardgame/161970/alchemists">Alchemists</a>. I haven't played a harder game — and not because the rules are complicated. The point is that you have to crack something like a sudoku puzzle and somehow play the game at the same time. Your solution, even a partial one, has to be woven into your play: publishing theories, debunking your rival's publications and even your own, taking risks, guessing, exploiting your opponent to extract more information for the puzzle. Only to find out later that you got it wrong, solved the whole thing incorrectly — and won anyway. That's Alchemists for you.</p>
<p><img src="https://alexblog.net/best-board-games/images/alchemists.webp" alt="Our Alchemists game: the main board, a lab screen with the potion grid and the ingredient triangle" loading="lazy">
<img src="https://alexblog.net/best-board-games/images/alchemists-2.webp" alt="Top-down view of an Alchemists game: the board and players&#x27; hands over the tokens" loading="lazy"></p>
<h2>Cosmic Encounter</h2>
<p>There's a pile of social games out there: all the Mafias, Dixits, Imaginariums, Codenames. They're fun for a couple of plays, then it all starts wearing thin: you get the emotions, but mechanically it's, well, whatever. And there's one game that simply tears those Mafias and Dixits apart in a single breath. Betrayal on the level of <a href="https://boardgamegeek.com/boardgame/39463/cosmic-encounter">Cosmic Encounter</a> you have never seen or experienced in your life. The box holds fifty aliens, broken from start to finish. Some swap cards with you. Some get to peek at the combat card the whole battle hinges on. There's even a Bride character who picks herself a husband and can look at his cards, the husband can't attack her, and when she divorces him, she takes half his cards. Can you picture the level of madness at a table with characters like these? If you haven't played it you won't get it, and if you have, you already know exactly what I mean. This game destroyed every social game for me. The fun bar is set so high that nothing else even comes close.</p>
<p><img src="https://alexblog.net/best-board-games/images/cosmic-encounter.webp" alt="Cosmic Encounter in progress: planet discs with stacked plastic ships and the warp in the middle of the table" loading="lazy"></p>
<div class="section-divider"><span class="section-divider-ornament">***</span></div>
<p>So that's my little selection. Have fun out there.</p>
<p><em>Photos: <a href="https://commons.wikimedia.org/wiki/File:Playing_board_game_-_Play_1048_1724352635173.jpg">El Pantera</a>, <a href="https://www.flickr.com/photos/wetwebwork/14291336656">wetwebwork</a>, <a href="https://commons.wikimedia.org/wiki/File:Cosmic_Encounter.jpg">JIP</a> · CC BY-SA / CC BY. Press photos: © <a href="https://cephalofair.com/products/gloomhaven-jaws-of-the-lion">Cephalofair Games</a>, <a href="https://www.direwolfdigital.com/clank/">Dire Wolf Digital</a>, <a href="https://en.portalgames.pl/detective-photo-gallery/">Portal Games</a>, <a href="https://www.spacecowboys-games.com/game/unlock/">Space Cowboys</a>. The rest of the photos are mine.</em></p>]]></content:encoded>
            <author>Alex Kucherenko</author>
        </item>
        <item>
            <title><![CDATA[New in the Collection: Descent Journeys in the Dark]]></title>
            <link>https://alexblog.net/descent-journeys-in-the-dark/</link>
            <guid isPermaLink="false">https://alexblog.net/descent-journeys-in-the-dark/</guid>
            <pubDate>Sat, 27 Feb 2016 00:00:00 GMT</pubDate>
            <description><![CDATA[It finally arrived -- the long-awaited and controversial purchase. I really wanted it, but the price and reviews about its balance kept me from pulling the trigger. A bit of spontaneity and here it is.]]></description>
            <content:encoded><![CDATA[<p>It finally arrived -- the long-awaited and controversial purchase. I really wanted it, but the price and reviews about its balance kept me from pulling the trigger. A bit of spontaneity and here it is.</p>
<blockquote>
<p>In the darkest depths of Terrinoth, an ambitious overlord gathers his minions to lay siege on the world above. Only a small band of heroes, gifted with courage and power, will be able to save the land from the cold grip of domination. Now is the time to venture into the dark and unravel the overlord's plot before it's too late...</p>
</blockquote>
<p><img src="https://alexblog.net/descent-journeys-in-the-dark/images/dj-1.webp" alt="Descent board game box contents: rulebooks, campaign map, cards, dice, tokens, and miniatures laid out" loading="lazy"></p>
<h2>What the game is about</h2>
<p>The game is about the epic adventure of heroes in the dungeons of a Dark Evil Overlord who won't miss a chance to show the heroes who's boss. It's a game for 2--5 players -- where am I supposed to find that many friends? One player takes on the role of universal evil and becomes the Overlord. The rest become regular heroes, divided into classes and archetypes. Heroes roam dungeons, search for treasure, fight the Overlord's minions, buy equipment... in short, they go all out.</p>
<p>All combat is dice-rolling -- there are both melee and ranged attacks. And of course spells, lots of spells. Can't do without them -- it's fantasy, after all.</p>
<p>The game features both standalone missions and campaigns. A campaign is a set of individual missions tied together by an overarching story. Heroes can carry their hard-earned (read: looted) gear into the next mission, accumulating a small fortune by the end of the campaign. You won't finish a campaign in a single day -- it takes dozens of hours, and the game thoughtfully provides a save system: a notebook and pencil.</p>
<p><img src="https://alexblog.net/descent-journeys-in-the-dark/images/dj-5.jpg" alt="Campaign map board showing the land of Terrinoth with cities and quest locations" loading="lazy"></p>
<p><img src="https://alexblog.net/descent-journeys-in-the-dark/images/dj-6.webp" alt="Descent rulebooks, campaign board, cards, dice, and token bags spread out on table" loading="lazy"></p>
<h2>Impressions</h2>
<p>Haven't actually played yet -- haven't even finished reading the rules (a 20-page booklet). But I'll definitely report back once I get the chance to try it.</p>
<p>The miniatures are impressive -- they're quite detailed and genuinely menacing. I liked the tile system: you lay out the dungeon map from tiles, and since there are so many of them, it gives the game great replayability and variety.</p>
<p><img src="https://alexblog.net/descent-journeys-in-the-dark/images/dj-2.webp" alt="Hero and monster miniatures: small gray heroes, cream creatures, and large red demons including a dragon" loading="lazy"></p>
<p><img src="https://alexblog.net/descent-journeys-in-the-dark/images/dj-3.webp" alt="Close-up of a red Overlord monster miniature with detailed scales and claws" loading="lazy"></p>
<p><img src="https://alexblog.net/descent-journeys-in-the-dark/images/dj-4.webp" alt="White and red dragon miniatures with spread wings towering over smaller figures" loading="lazy"></p>
<p>On the downside, there's the balance issue. From what I've read in the rules, playing the Overlord seems kind of dull, and the heroes almost always have the upper hand -- they're essentially immortal.</p>
<p><img src="https://alexblog.net/descent-journeys-in-the-dark/images/dj-7.webp" alt="All miniatures lined up with cards, dice, and token bags in front" loading="lazy"></p>
<p><img src="https://alexblog.net/descent-journeys-in-the-dark/images/dj-8.webp" alt="Red winged demon miniature close-up standing on the rulebook cover" loading="lazy"></p>
<p><img src="https://alexblog.net/descent-journeys-in-the-dark/images/dj-9.webp" alt="Cream-colored tentacled monster miniature close-up on the rulebook" loading="lazy"></p>
<p><img src="https://alexblog.net/descent-journeys-in-the-dark/images/dj-10.webp" alt="Dungeon map tiles assembled: fiery caverns, stone halls, and treasure rooms" loading="lazy"></p>
<p><img src="https://alexblog.net/descent-journeys-in-the-dark/images/dj-11.webp" alt="Dungeon tiles showing icy caves and underground corridors" loading="lazy"></p>
<p><img src="https://alexblog.net/descent-journeys-in-the-dark/images/dj-12.webp" alt="Close-up of a dungeon tile with scattered treasure and skeleton details" loading="lazy"></p>
<p><img src="https://alexblog.net/descent-journeys-in-the-dark/images/dj-13.webp" alt="Full dungeon layout assembled from multiple interlocking map tiles" loading="lazy"></p>]]></content:encoded>
            <author>Alex Kucherenko</author>
        </item>
        <item>
            <title><![CDATA[Starting Development of STDS]]></title>
            <link>https://alexblog.net/stds/</link>
            <guid isPermaLink="false">https://alexblog.net/stds/</guid>
            <pubDate>Sat, 20 Feb 2016 00:00:00 GMT</pubDate>
            <description><![CDATA[Spring is coming and I'm getting restless. An idea was born: build a multiplayer top-down shooter in the browser with JavaScript, set in space. Working title: STDS (Space Top Down Shooter).]]></description>
            <content:encoded><![CDATA[<p>Spring is coming and I'm getting restless. An idea was born: build a multiplayer top-down shooter in the browser with JavaScript, set in space. Working title: STDS (Space Top Down Shooter).</p>
<h2>Main stack</h2>
<p>Plain JavaScript ES5</p>
<p>JS modules in CommonJS format (like Node.js, but on the frontend)</p>
<p><a href="https://webpack.github.io/">Webpack 1.x</a> -- not sure 2.x is solid yet; even the first branch is still rough (IMHO). We'll see, maybe we'll migrate.</p>
<p>Rendering by <a href="http://www.pixijs.com/">PIXI JS</a></p>
<p>Using <a href="http://phaser.io/">PHASER IO</a> as the game engine</p>
<p>CSS generated from <a href="http://lesscss.org/">Less</a> -- I just like it</p>
<p>You can follow the project's progress on GitHub: <a href="https://github.com/AlexTiTanium/Space-Top-Down-Shooter">STDS</a>. Too early to think about the server side, but it'll most likely be Node.js.</p>]]></content:encoded>
            <author>Alex Kucherenko</author>
        </item>
        <item>
            <title><![CDATA[Bad Monday]]></title>
            <link>https://alexblog.net/bad-monday/</link>
            <guid isPermaLink="false">https://alexblog.net/bad-monday/</guid>
            <pubDate>Sat, 05 Dec 2015 00:00:00 GMT</pubDate>
            <description><![CDATA[It's 4 AM, insomnia got me again. Usually I go work. For me this is a blessed time of peace and quiet -- what more do you need to write beautiful, maintainable code?]]></description>
            <content:encoded><![CDATA[<p>It's 4 AM, insomnia got me again. Usually I go work. For me this is a blessed time of peace and quiet -- what more do you need to write beautiful, maintainable code?</p>
<p>Beautiful code... let's talk about that, shall we?</p>
<p>Recently I was listening to Radio-T (a popular Russian tech podcast) -- don't remember which episode, one of the recent ones -- where they discussed an article about outsourcing.</p>
<p>Yet another client decided to save money and ordered iOS app development from the cheapest company they could find on the internet. The result is obvious: a complete disaster. And he goes point by point explaining why, in his opinion, everything went wrong.</p>
<p>Of course nobody's willing to admit the correlation between price and quality. It must be something else, right? You want to believe you can get the best for five bucks an hour. Though you should understand that a big price tag doesn't guarantee anything either.</p>
<p>There was a lot of back-and-forth on this topic. Umputun's brief take was that there's a difference in mentality: for Americans, the result is everything, the process is nothing. For Russians, the process is everything, and the result is a side effect. For Indians, one thing's rubbish, another thing's rubbish -- life's too short to sweat it.</p>
<p>So I'm sitting there thinking: damn, there's something to this. Should you even bother with the process?</p>
<p>After all, the client needs results, and what am I doing? I'm fussing over the process. Sure, I fuss over it to produce the best result I'm capable of, but you could get results faster.</p>
<p>The task: write a simple autocomplete for a service. Just shove everything into the damn controller that renders the list of results and call it a day.</p>
<p>But no, I have to extract it into a separate controller, refactor it -- because someone will come along to maintain this, and what will they think of me?</p>
<p>And what about the server side? A normal, efficient programmer would just regex through the database and be done with it. Result achieved.</p>
<p>But no, we're "special." Everything has to be fast, and the autocomplete has to actually help you type something. Instead of regex search, let's build a prefix tree? And let's add tag weights to surface the most popular ones first. And of course we need tests and documentation.</p>
<p>And just like that, I'm deep in the weeds of process.</p>
<p>This is not results. The client doesn't need any of this. Not only doesn't need it, but it's actively harmful -- financially and deadline-wise. And in a year they'll rewrite the whole thing anyway, if anyone still cares.</p>
<p>Maybe subconsciously I realize I'm paid by the hour, and if I do it in one hour instead of ten, I'll earn less? No, that's not it -- I did the same thing back when I worked for a fixed price.</p>
<p>What's bad is that for me this is a conscious choice. I don't want to change, to adapt to the market. (Probably why the last time I looked for a job it took me about three months.)</p>
<p>Kill me, but I want to write proper code. And proper code takes time, and time is money. The conclusion: by all rights, I should be fired. In this unequal battle between process and results, I'm the broken gear.</p>
<p>Beautiful code to you all, friends!</p>]]></content:encoded>
            <author>Alex Kucherenko</author>
        </item>
        <item>
            <title><![CDATA[The Making of Fun Da Vinci]]></title>
            <link>https://alexblog.net/fun-da-vinci/</link>
            <guid isPermaLink="false">https://alexblog.net/fun-da-vinci/</guid>
            <pubDate>Wed, 04 May 2011 00:00:00 GMT</pubDate>
            <description><![CDATA[Here's where it all started, and here's what we ended up with. We changed a lot, assembled a solid team, and gained experience selling a game. We'll share all of it with you.]]></description>
            <content:encoded><![CDATA[<p><a href="https://geeklife.in.ua/2010/07/27/make-game-in-nine-days/">Here's</a> where it all started, and <a href="http://armorgames.com/play/10964/fun-da-vinci">here's</a> what we ended up with. We changed a lot, assembled a solid team, and gained experience selling a game -- we'll share all of it with you, dear reader. Interested? Welcome aboard.</p>
<h2>Programming</h2>
<p>This part's simple -- me and Nikita Sidorenko (Division). People say two programmers on one simple Flash game is too many. I categorically disagree. In practice, if you write a game solo, you eventually give up. Maybe it's just us, but trust me, we tried -- working as a pair is both more fun and more productive. I explain it through guilt: if one person is working on something, the other feels ashamed for slacking and starts pitching in.</p>
<p><img src="https://alexblog.net/fun-da-vinci/images/fdv-1.webp" alt="Four art style iterations of the game: early cartoon prototype, item sprites, neon concept, and final Leonardo manuscript style" loading="lazy"></p>
<h2>Design</h2>
<p>Everything starts with design, and in a quality game it's front and center. As you know, I was the one who drew (read: borrowed) the art for the previous version. That flies for a contest, but for a real product you need proper, and most importantly, original art. Kherson is a small city -- everyone knows everyone -- and there aren't many good designers around, so we didn't have to search long for <a href="https://www.youtube.com/watch?v=K7r9dbqYrHs">Semyon Khramtsov, the "freelancer from the provinces"</a>.</p>
<p><em>"Even though I didn't particularly like puzzles, the guys' enthusiasm was infectious. First I proposed several art styles, including a stylization of Leonardo's manuscripts. That's the one everyone fell for. Sure, it's a pretty mainstream trick in graphic design, but the allure of synthesizing an interactive environment with that famous sketch style of the Renaissance mega-designer was too strong. I re-read his biography, googled all his works to mine as much flavor for the game as possible -- basically, immersed myself in the right atmosphere as deeply as I could. Of course, I consider the result an affront to his image and inventions, but who are we compared to the Master? Thank God I didn't repaint the game in bright 'casual' colors, as one of the judges suggested at the game-lynch -- we'd definitely be burning in hell for that. My biggest achievement in this project was diving into level design and combining the artist's grim self-portrait with the Mona Lisa's smile :)" -- Semyon Khramtsov</em></p>
<p><img src="https://alexblog.net/fun-da-vinci/images/fdv-2.jpg" alt="Da Vinci self-portrait evolution: original sketch, grayscale adaptation, and final game version with Mona Lisa smile" loading="lazy"></p>
<h2>Sound and Music</h2>
<p>The last but by no means least area -- the musical and sound design of the game. It doesn't just bring the game to life; it keeps the player from getting bored -- at least until it starts to annoy them. Semyon happened to know a very talented composer he'd been working with on small Flash projects for a while -- <a href="http://requix.promodj.ru/">Vladimir Marinichev</a> -- which was perfect timing.</p>
<p><em>"First things first, to immerse myself in the Renaissance atmosphere, I re-watched a biographical film about Leonardo da Vinci and listened to works by composers of that era. For the menu theme, I based it on a piece by Claude Gervaise that would convey the mood of a thoughtful Creator and transport the player to the Renaissance. I tried to keep the arrangement and instrumentation true to the style of that period.
For the gameplay sound design, many sounds were recorded using everyday objects. We decided to leave the levels without a musical score and instead convey the atmosphere of a workshop. During gameplay you can hear Leonardo constantly scribbling notes and humming something to himself, engrossed in yet another experiment.
Unfortunately, in trying to reduce the game's file size, we had to degrade the quality of all sounds and music, which of course made it harder to realistically convey all the nuances and details. But overall, many people were happy with the result, and it was very gratifying to receive a high rating from FGL." -- Vladimir Marinichev</em></p>
<p>And so, with the team assembled, off we went...</p>
<h2>The Beginning</h2>
<p>The first commit was on August 4, 2010 -- that's when you can mark the start of development. We slowly began fixing bugs, and since we all had day jobs, progress was very slow. We set up a task manager, created a pile of tasks, had lots of discussions -- all about nothing, really. In its final form, if you took all the tasks into account, the game should have looked like a mini Starcraft 2 with its own BattleNet (which had just come out at the time -- very distracting). The unrealistic scope of our plans was our first mistake. Don't build enormous castles. You'll abandon most of it once you realize your plans would have surprised even Napoleon. That's exactly what happened to us. We came to our senses when we decided to participate in FlashGamm KYIV 2010. We needed something to show, and we had nothing -- not even a working prototype. That's when the real work began. First we axed most of the tasks, minimized and optimized everything we could. And that produced results.</p>
<p><img src="https://alexblog.net/fun-da-vinci/images/fdv-4.webp" alt="Task manager screenshot: a to-do list including &#x22;Stop playing Starcraft 2&#x22; with humorous entries" loading="lazy"></p>
<h2>FlashGamm KYIV 2010</h2>
<p>By the start of the conference we'd managed to cobble together something resembling a prototype and build a few lackluster levels. With that, Semyon and Nikita headed to Kyiv. We competed in the Indie category, plus we signed up for the game-lynch (a public critique session) -- oh boy, did they tear us apart... But the game-lynch itself was incredibly useful. They pointed out our game's shortcomings, our mistakes, and gave us advice on where to go next. The encouraging part was that we'd already anticipated many of the flaws they called out. During the audience vote for best game-lynch game, ours scored exactly 0 points (zen mode) -- and after that, none of us expected to win anything at all. But out of nowhere, we won the "Future Hit" nomination! The joy was immense. I think this event had a really positive effect on the team's morale -- we realized there was something to this game and we needed to finish it no matter what.</p>
<p><img src="https://alexblog.net/fun-da-vinci/images/fdv-5.webp" alt="The Fun Da Vinci team receiving the Future Hit award at FlashGamm KYIV 2010 conference" loading="lazy"></p>
<h2>Wrapping Up</h2>
<p>After the conference, development settled into a steady rhythm and everything ran like clockwork. But there were annoying bugs. The first was objects falling through other objects -- a ghost that had haunted us since Ball Factory (the first prototype). We solved it pragmatically, by trial and error. I started tinkering with Box2D settings and found a quirk: changing <code>b2_aabbMultiplier</code> from 0.2 to 0.1 magically fixed the fall-through issue. The second bug crept up from where we least expected. Everywhere we needed a button, we used <code>SimpleButton</code>, and it never even crossed our minds that in newer Flash Player versions, button states would start "sticking" -- and there was no way to fix it no matter what hacks we tried. We had to build our own solution. Discussion on <a href="http://www.flasher.ru/forum/showthread.php?t=148860">flasher.ru</a>.</p>
<h2>Selling the Game</h2>
<p>Another thing happened at FlashGamm -- we met Stefan Keisch. We had no idea how to sell a game. There was some information online about FGL (Flash Game License), but it was all vague, and you needed to attract sponsors. That's exactly what Stefan does -- not for free, of course: 30% of the game's sale price. After some thought and deliberation, we accepted the offer. Stefan also gave recommendations on how to make the game more attractive to sponsors. The first thing he pointed out was the name. Our working title was "Balls Da Vinci" -- that's the name we competed under at FlashGamm. Unfortunately, the name triggered too many unwanted associations with Leonardo's manhood (which we found hilarious, admittedly), so we rebranded to "Fun Da Vinci" (maybe it's only a puzzle for us -- for da Vinci's brain, it's child's play).</p>
<p>The listing went up on FGL on January 14, 2011. FGL reviews every game submitted.</p>
<p><em>Review Information:</em></p>
<p>Intuitiveness: 7 Good
Fun: 6 Average
Graphics: 7 Good
Sound: 8 Great
Quality: 7 Good
Overall: 7 Good</p>
<p>Comments:
Standard execution of a traditional physics game with a Leonardo da Vinci theme
Creative level design, familiar game mechanics, polished interface
Lacks depth beyond first playthrough. Consider adding achievements or a scoring mechanism of some sort</p>
<p>Right away someone offered us $1,000, but we wanted more -- at least $3K. That bid sat there for about a month. We were starting to lose hope when suddenly several major portals began competing for the game (I suspect Stefan had something to do with it). Bids rose and fell. We noticed that sponsor activity picked up toward the end of each week. When the bid hit $3,000, we nearly caved and accepted, but a certain bald someone suggested we hold out -- right at that moment, we'd received an offer from <a href="http://jayisgames.com/">jayisgames.com</a> to write a <a href="http://jayisgames.com/archives/2011/03/fun_da_vinci.php">review</a> of the game. We were thrilled -- besides the "street cred" from a well-known portal, it could trigger a new wave of bids. And it did... After another round of sponsor bidding wars, the price settled at $5,600. We strangled our inner cheapskate and accepted. That was another victory. From listing on FGL to accepting the bid: exactly 2 months.</p>
<p>As you've probably guessed, Armor Games won. Sponsors usually ask you to brand the game for their portal -- add an intro, integrate their API, and so on. All easy enough, but we ran into problems with the Armor Games API, which absolutely refused to connect. We made a blank project -- worked perfectly there. In the actual game project -- nope. By some miracle, Nikita figured out that if you initialized the API in the preloader, everything was fine. Weird, but whatever.</p>
<h2>Splitting the Money</h2>
<p>So, you all want to know how much we made? Here's the breakdown:
FGL takes 10% for its services (half of which Stefan pays by agreement);
10% of 5,600 = 560 (FGL's cut);
560 / 2 = 280 (our share of FGL's fee);
30% of 5,600 = 1,680 (Stefan's cut);
Bottom line: 5,600 -- 280 -- 1,680 = 3,640.</p>
<p>Three and a half grand split four ways over four months -- not a bad result, for Nigerian guest workers. How to make a living from games alone? -- a question that puzzled us. But our enthusiasm only grew, and we're not giving up this hobby. We'll figure out the answer eventually -- maybe even in the comments to this post ;)</p>
<h2>Future Plans</h2>
<p>We're already working on a second version with new items, levels, and features. We have ideas for interesting game mechanics and unexplored themes. On the technical side, we're looking toward the App Store and Android Market, with Unity3D to help us get there.</p>
<p><img src="https://alexblog.net/fun-da-vinci/images/fdv-3.webp" alt="The Fun Da Vinci development team: Division (Lead Programmer), Titanum (Programmer), Xsem (Art and Design), Requix (Sound)" loading="lazy"></p>]]></content:encoded>
            <author>Alex Kucherenko</author>
        </item>
        <item>
            <title><![CDATA[Making a Game in Nine Days -- Is That Even Possible?!]]></title>
            <link>https://alexblog.net/ball-factory/</link>
            <guid isPermaLink="false">https://alexblog.net/ball-factory/</guid>
            <pubDate>Tue, 27 Jul 2010 00:00:00 GMT</pubDate>
            <description><![CDATA[I'm not really the adventurous type, but sometimes it happens. And ten days ago, it happened. A buddy suggested I enter an IGDC game jam. Here are my impressions.]]></description>
            <content:encoded><![CDATA[<p>I'm not really the adventurous type, but sometimes it happens. And ten days ago, it happened. Exactly ten days ago, my buddy Nikita (who goes by Division) suggested I enter an IGDC game jam.
Here are my impressions...</p>
<h2>The Contest</h2>
<p>For those who don't know, IGDC is a community of people who enjoy making games. A contest is announced, rules are set, and off you go -- anyone can participate. The theme for this particular contest was genuinely interesting: Indirect Control. The idea is that the player has no direct control over the game process. You can only influence things indirectly.</p>
<p>I'd never entered one of these contests before, unlike Nikita, who had even placed in the top ranks. We decided to write in ActionScript 3, which I knew only vaguely. Why AS? Simply because Division had an engine built on AS that used the well-known Decorator pattern -- used it so heavily, in fact, that the engine itself was called Decorator.</p>
<p>When I heard "indirect," the famous puzzle game "The Incredible Machine" came to mind. I got the idea to make something similar but change the goal. In The Incredible Machine, the objective is to get a mechanism working. In our game, you'd make all the balls roll into a specific pipe using various boxes, planks, and -- of course -- physics. Luckily, the Decorator engine supported Box2D, though looking at the relative sizes of the engine and Box2D, it's hard to tell who's supporting whom. Anyway, physics sorted.</p>
<p>Design, hmm... neither I nor Division could draw at all. We decided to offer the honorary position of designer to an acquaintance of mine. He drew a couple of scenes, but then something came up, and we had to draw everything ourselves -- by "ourselves" I mean me.</p>
<p><img src="https://alexblog.net/ball-factory/images/bf-1.webp" alt="Ball Factory main menu with various sports balls and options: Start Game, Editor, About Us" loading="lazy"></p>
<p><img src="https://alexblog.net/ball-factory/images/bf-2.webp" alt="Ball Factory gameplay on level 2: wooden crates, planks, and pipes on a colorful hillside" loading="lazy"></p>
<h2>Development</h2>
<p>One key rule of the contest: you have to make the game in 9 days. So work kicked off at a lively pace. I was coming up with the concept, sketching out what everything should look like, while Division was preparing the engine. We used Dropbox for file sharing -- once again convinced that app is indispensable.</p>
<p>The engine prep took about three days and fell squarely on Division's shoulders. But for collaborative work we needed version control. The choice was between Git and SVN. We chose SVN, which we would later regret.</p>
<p>We got into a rhythm pretty quickly. Development happened at night. Since my AS experience left much to be desired, Division would explain over Skype what I was doing wrong. Skype was a huge help on this project, actually -- first, it made things more fun, and second, we could quickly coordinate our tasks and actions.</p>
<p>I fondly remember those sleepless nights. It was genuinely exciting to learn something new (AS) by doing it. I used to think you had to read a thick book before you could start writing in a new language -- how wrong I was. In those 4 days of coding, I learned more than I would from a month of reading a smart book. Sure, I lacked theoretical knowledge, but that was quickly fixed by the school of hard knocks...</p>
<p><img src="https://alexblog.net/ball-factory/images/bf-3.webp" alt="Ball Factory level 4 gameplay: triangular ramps, pipes, and ball counter showing caught and total balls" loading="lazy"></p>
<p><img src="https://alexblog.net/ball-factory/images/bf-4.webp" alt="Ball Factory level editor showing XML level data with entity definitions and coordinates" loading="lazy"></p>
<h2>The SVN Disaster</h2>
<p>Things went smoothly for about four days, but then Saturday came -- the turning point. We wrecked SVN. Here's how it went down... I pushed a new commit, Division was supposed to update his repo, but there was a conflict that needed manual resolution. Out of inexperience, Division clicked the wrong thing or pressed the wrong button, and wrecked one file. All his changes in that file were lost. And of course, stupidity never comes alone... In a move that was more dumb than inexperienced, I advised him to roll back to the previous revision, assuming he'd committed before that. Naturally, the previous revision wiped out an entire day's work... We were livid. The submission deadline was the next day, and we hadn't even built the levels yet -- and as if by Murphy's Law, the most complex and tangled part got erased.</p>
<p>Nothing to do about it -- Division had to restore everything. Morale was dented, but the payoff was worth it: we ended up with a solid level editor and a pretty good-looking game (you're welcome, that was my design work).</p>
<h2>The Disqualification and Redemption</h2>
<p>Murphy's Law, act two... According to contest rules, you could be a day late with a 30% penalty, but you had to notify the organizer. Of course we weren't going to make it, and we'd known it beforehand. In the relevant IGDC forum thread, Nikita had been voicing concerns and guesses that we wouldn't finish on time. We weren't sure yet, so we warned them more explicitly once we realized our earlier posts hadn't been taken as an official late notice. The organizer said "why so late?" but nothing more. We were drawing levels until about 3 AM (with work in 4 hours). We'd wanted to submit earlier, but as always, things never go the way you want. Realizing we were looking at the full 30% penalty, we went to sleep. During the day, in breaks between my day job, I kept tweaking levels, fixing issues, preparing the game for release. And then, like a bolt from the blue -- we get disqualified. For being late. I was honestly in shock. So much effort and soul poured in, it was really upsetting. But what can you do -- we packed the archive and sent it off.</p>
<p>Dragging myself home, I bought a bunch of junk food -- chips and cookies -- to at least console myself somehow. Got home and crashed without even reaching the cookies or chips. Around 10 PM, my wife wakes me up with great news -- the admin reversed the disqualification! The joy was beyond words. Special thanks to the admin! The world was right again... We're in the fight... We're competing... And we wait for the scores...</p>
<h2>Lessons Learned</h2>
<p>Lessons I took away:
-- Learn by doing.
-- Making games is fun.
-- To hell with SVN. Next project, we're using Git.</p>
<p><img src="https://alexblog.net/ball-factory/images/bf-5.webp" alt="Ball Factory level 3 gameplay: balls bouncing along a circular path with pipes and ramps" loading="lazy"></p>
<p><img src="https://alexblog.net/ball-factory/images/bf-6.webp" alt="Ball Factory level selection screen with 16 numbered levels in a green grid" loading="lazy"></p>]]></content:encoded>
            <author>Alex Kucherenko</author>
        </item>
    </channel>
</rss>