<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Renderscribe</title>
    <description>The latest articles on DEV Community by Renderscribe (@renderscribe).</description>
    <link>https://dev.to/renderscribe</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4114366%2Feb5216a8-6afa-4994-8f32-7d5181fedc7c.png</url>
      <title>DEV Community: Renderscribe</title>
      <link>https://dev.to/renderscribe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/renderscribe"/>
    <language>en</language>
    <item>
      <title>I Built an Idle Game With an AI Pair Programmer. Here's What Actually Got Hard.</title>
      <dc:creator>Renderscribe</dc:creator>
      <pubDate>Wed, 30 Sep 2026 09:55:34 +0000</pubDate>
      <link>https://dev.to/renderscribe/i-built-an-idle-game-with-an-ai-pair-programmer-heres-what-actually-got-hard-53mc</link>
      <guid>https://dev.to/renderscribe/i-built-an-idle-game-with-an-ai-pair-programmer-heres-what-actually-got-hard-53mc</guid>
      <description>&lt;p&gt;I've spent the last few months building &lt;strong&gt;Galactic Idle&lt;/strong&gt;, a space-empire incremental game, using Vue 3, TypeScript, Capacitor (for Android), and an AI pair programmer. This isn't a "AI will replace us" or "AI is overhyped" post. It's a field report on where the time actually went.&lt;/p&gt;

&lt;p&gt;Short version: the AI collapsed the cost of writing code. The hard parts moved somewhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vue 3 + TypeScript + Vite&lt;/strong&gt; for the game and UI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capacitor&lt;/strong&gt; to ship the same code as an Android app&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vitest + Playwright&lt;/strong&gt; for unit and e2e tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostHog&lt;/strong&gt; for opt-in, consent-gated analytics&lt;/li&gt;
&lt;li&gt;Nine locales, with a coverage check so missing translation keys fail loudly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All game state lives in one big composable, &lt;code&gt;useGameStore.ts&lt;/code&gt;. That is not a recommendation. It's a confession, and it's why I'll come back to cleanup in a minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the AI made cheap
&lt;/h2&gt;

&lt;p&gt;Things that used to cost me days and now cost me an afternoon:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wiring a consent-gated analytics layer that forwards from a central &lt;code&gt;logEvent&lt;/code&gt; function&lt;/li&gt;
&lt;li&gt;i18n across nine languages&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;trailer capture rig&lt;/strong&gt;: a Playwright script that plays the game with an injected cursor ghost and a virtual clock, then renders both 16:9 and 9:16 gameplay trailers. I'll probably write that one up separately.&lt;/li&gt;
&lt;li&gt;Refactors that touch dozens of call sites, like replacing scattered &lt;code&gt;toast()&lt;/code&gt; calls with a structured &lt;code&gt;logEvent&lt;/code&gt;/&lt;code&gt;grantResources&lt;/code&gt; spine and result cards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your bottleneck was "I know what I want, I just have to type it," you'll feel this immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it did &lt;em&gt;not&lt;/em&gt; make cheap
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Balance is a data problem, not a code problem
&lt;/h3&gt;

&lt;p&gt;Idle games are economies. Early on, tapping was too strong, so I capped &lt;code&gt;clickPower&lt;/code&gt; (5000 → 900) and sqrt-dampened the upgrade stack. Then conquest rewards were too generous, so I cut the ×3/×5 global reward upgrades. The AI implemented each change in minutes. Knowing &lt;em&gt;which&lt;/em&gt; change to make required numbers.&lt;/p&gt;

&lt;p&gt;So I gave myself a way to get them. The store exposes a hook that only exists when the URL contains &lt;code&gt;?__sim&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Balance-sim hook — only active when the URL contains ?__sim&lt;/span&gt;
&lt;span class="c1"&gt;// Lets an offline harness fast-forward the real economy and read/drive it.&lt;/span&gt;
&lt;span class="c1"&gt;// VITE_SIM_HOOK=1 opts a production build in (scripts/capture-trailer.mjs builds that&lt;/span&gt;
&lt;span class="c1"&gt;// into dist-capture/ so the shipped bundle never carries the hook).&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DEV&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_SIM_HOOK&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(\?&lt;/span&gt;&lt;span class="sr"&gt;|&amp;amp;&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;__sim&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;__SIM&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;__SIM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;useGameStore&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;recalc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fireRandomEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;setDtCap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;_tickDtCap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Node script drives a headless browser, calls &lt;code&gt;tick&lt;/code&gt; with a raised dt cap, and reads the real store, not a re-implementation of the economy that can drift from the real one. That's the bit I'd recommend stealing: &lt;strong&gt;simulate the real code, not a model of it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What it found: after the nerfs, content lasts roughly 8 to 20 hours, and there's a credits-negative trough somewhere around hours 3 to 8 that I never would have noticed by playing. I only found it because I could fast-forward.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The data told me I was fixing the wrong problem
&lt;/h3&gt;

&lt;p&gt;I shipped opt-in PostHog analytics and looked at the first real numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~&lt;strong&gt;40%&lt;/strong&gt; of players left before completing their first goal&lt;/li&gt;
&lt;li&gt;Median playtime: &lt;strong&gt;5 minutes&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The funnel cliffs were building → research → ship&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd been agonizing over a 2-hour progression wall. The real problem was the first five minutes. No pair programmer, human or otherwise, was going to tell me that. Only data does.&lt;/p&gt;

&lt;p&gt;One practical gotcha for anyone doing the same: my &lt;code&gt;session_end&lt;/code&gt; event turned out to be effectively a heartbeat, so raw counts were inflated until I deduped. Check what your events actually mean before you trust a funnel.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Fast code generation grows dead code
&lt;/h3&gt;

&lt;p&gt;When implementation is cheap, it's very easy to end up with a parallel, half-abandoned copy of your own app. I did. A "product cohesion" audit deleted a whole dead parallel app stack, merged two drifting friction models into one &lt;code&gt;upkeep&lt;/code&gt; system, and merged two overlapping conquest features into one.&lt;/p&gt;

&lt;p&gt;Rule I now follow: &lt;strong&gt;every time the AI adds something, I ask what it replaced.&lt;/strong&gt; If the answer is "nothing," that's a smell.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Offline earnings are a design decision hiding in a number
&lt;/h3&gt;

&lt;p&gt;Offline income is capped to up to 30 seconds of live production, with plasma excluded. That one line has generated more back-and-forth than most features, because it's balance, monetization (there's a rewarded-ad hook to boost it), and retention all at once. The code is trivial. The decision isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell another dev
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Give the AI a clear goal and a way to verify.&lt;/strong&gt; It's fast when it can check its own work (tests, a sim, a build) and confidently wrong when it can't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build measurement early.&lt;/strong&gt; Analytics and a simulator will teach you more than any amount of playtesting on yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat deletion as a feature.&lt;/strong&gt; Budget time for cleanup, because generation makes mess cheaper too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never let a secret touch tool output.&lt;/strong&gt; I learned that the hard way with a token. Pipe or assign directly, always.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expect your job to shift&lt;/strong&gt; from "person who writes the code" to "person who decides what the code should do and checks that it did."&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The next post in this series digs into the capture rig and how the virtual clock keeps trailers deterministic. If you've built an idle game or any economy-driven app, I'd love to hear how you approach balancing, and especially how you handle the first-five-minutes problem.&lt;/p&gt;

&lt;p&gt;Galactic Idle is live on itch.io and in progress for Google Play. I'd genuinely love to hear where the first five minutes lose you.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>gamedev</category>
      <category>programming</category>
      <category>vue</category>
    </item>
    <item>
      <title>Systemic Interactions &amp; Emergence: The Depth We Didn't Design</title>
      <dc:creator>Renderscribe</dc:creator>
      <pubDate>Wed, 16 Sep 2026 10:50:24 +0000</pubDate>
      <link>https://dev.to/renderscribe/systemic-interactions-emergence-the-depth-we-didnt-design-242k</link>
      <guid>https://dev.to/renderscribe/systemic-interactions-emergence-the-depth-we-didnt-design-242k</guid>
      <description>&lt;p&gt;&lt;em&gt;Notes from building Galactic Idle, a browser/mobile idle empire builder&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every idle game starts as a spreadsheet: resource in, resource out, multiply&lt;br&gt;
by upgrade, repeat. The genre's real trick isn't the spreadsheet — it's what&lt;br&gt;
happens when you stack five or six spreadsheets on top of each other and let&lt;br&gt;
them read from the same state. That's where the interesting behavior lives,&lt;br&gt;
and almost none of it is behavior you designed. It's behavior you&lt;br&gt;
&lt;em&gt;discovered&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Here's what that's actually looked like for us, with the real code behind&lt;br&gt;
each example.&lt;/p&gt;
&lt;h2&gt;
  
  
  The setup: independent systems, shared state
&lt;/h2&gt;

&lt;p&gt;Galactic Idle runs on five resources (energy, minerals, credits, research,&lt;br&gt;
plasma), a building economy, a research tree, a conquest layer, a diplomacy&lt;br&gt;
layer, and — the piece that turned out to matter most — an &lt;strong&gt;Upkeep&lt;/strong&gt;&lt;br&gt;
system: ongoing drain across four facets (Power, Fleet, Research, Colony)&lt;br&gt;
that scales as a capped percentage of gross production, growing with empire&lt;br&gt;
size.&lt;/p&gt;

&lt;p&gt;None of these were designed together as one mechanic. Upkeep was bolted on&lt;br&gt;
specifically to solve a problem: once a player had enough buildings,&lt;br&gt;
production compounded without resistance and the back half of the game&lt;br&gt;
turned into watching numbers go up with no decisions left to make. Upkeep&lt;br&gt;
was the fix — friction that scales with success.&lt;/p&gt;

&lt;p&gt;What we didn't fully predict was what Upkeep would do once it started&lt;br&gt;
talking to the &lt;em&gt;other&lt;/em&gt; systems.&lt;/p&gt;
&lt;h2&gt;
  
  
  Emergent tension #1: conquest now competes with itself
&lt;/h2&gt;

&lt;p&gt;Conquering planets is supposed to feel unambiguously good — more territory,&lt;br&gt;
more resources, more empire. But Upkeep's Colony facet reads how many&lt;br&gt;
planets you've conquered as one of its inputs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;flatColony&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;planets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;conquered&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;flatColony&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;strain&lt;/span&gt;
&lt;span class="nx"&gt;flatColony&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="nx"&gt;fx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;colonyStrainMult&lt;/span&gt;
&lt;span class="c1"&gt;// Admin load grows with colony structures, not just planet count, so&lt;/span&gt;
&lt;span class="c1"&gt;// stacking them to maxCount is self-limiting on the minerals side.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;colonyAxis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;conqueredCount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;totalColonyBuildings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the generic upkeep function that consumes it doesn't know or care that&lt;br&gt;
&lt;code&gt;colonyAxis&lt;/code&gt; came from Conquest — it just sees a number that scales drain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyUpkeep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;facet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UpkeepFacetId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;gross&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;flat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reductionMult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;skimSurplus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UPKEEP_DRAG&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;facet&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dragPct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;perUnit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;reductionMult&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;skimSurplus&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gross&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;flat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gross&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;upkeep&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;facet&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;flat&lt;/span&gt;
  &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dragPct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dragPct&lt;/span&gt;
  &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;drain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;flat&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dragPct&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;drain&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;dragPct&lt;/code&gt; is a percentage of &lt;em&gt;gross&lt;/em&gt; production — not just the new&lt;br&gt;
planet's production, all of it. So every planet you take doesn't just add&lt;br&gt;
resources; it adds to the denominator that skims off the top of your&lt;br&gt;
entire empire.&lt;/p&gt;

&lt;p&gt;That means aggressive early conquest — which looks correct from a "bigger&lt;br&gt;
number is better" instinct — can actively work against a player who hasn't&lt;br&gt;
built up the production base to absorb the extra drain. Nobody wrote a rule&lt;br&gt;
that says "don't over-expand." It fell out of two systems (Conquest,&lt;br&gt;
Upkeep) that were never designed to talk to each other, connected only by&lt;br&gt;
reusing the same empire-size variable.&lt;/p&gt;

&lt;p&gt;We only found this by running it, not by reading the code. Once&lt;br&gt;
conquest-heavy playtests started stalling out, we traced it back to&lt;br&gt;
&lt;code&gt;colonyAxis&lt;/code&gt; reading &lt;code&gt;conqueredCount&lt;/code&gt; as an input it was never explicitly&lt;br&gt;
balanced against. That's the emergent-depth story working in our favor: a&lt;br&gt;
genuine strategic trade-off (expand fast vs. expand sustainably) we get&lt;br&gt;
credit for without having designed it directly.&lt;/p&gt;
&lt;h2&gt;
  
  
  Emergent tension #2: the fix and the disease share a cause
&lt;/h2&gt;

&lt;p&gt;Command Points — earned from conquest and mid-tier goals — fund a Doctrine&lt;br&gt;
tree gated by planets conquered. On paper, this is a completely separate&lt;br&gt;
progression track: spend a currency, pick a branch, get a bonus.&lt;/p&gt;

&lt;p&gt;In practice, Doctrines became the system that makes emergent tension #1&lt;br&gt;
survivable. A player who over-expands and gets hit by rising Upkeep has, by&lt;br&gt;
virtue of having conquered those same planets, also unlocked Doctrine tiers&lt;br&gt;
that can offset the drain (&lt;code&gt;de.colonyStrainMult&lt;/code&gt;, &lt;code&gt;de.fleetUpkeepMult&lt;/code&gt;,&lt;br&gt;
etc., feeding straight back into &lt;code&gt;applyUpkeep&lt;/code&gt;'s &lt;code&gt;reductionMult&lt;/code&gt; argument&lt;br&gt;
above). The two systems weren't built as a matched pair, but because they&lt;br&gt;
both key off the same underlying variable — planets conquered — they ended&lt;br&gt;
up in a feedback loop: the thing that causes the pain and the thing that&lt;br&gt;
funds the cure grow at the same rate.&lt;/p&gt;

&lt;p&gt;That's not something a design doc predicted. It's something that fell out&lt;br&gt;
of reusing one game-state variable as an input to two unrelated systems —&lt;br&gt;
which is, in miniature, the entire argument for why emergence tends to come&lt;br&gt;
from &lt;em&gt;shared state&lt;/em&gt;, not from any single system's internal complexity.&lt;/p&gt;
&lt;h2&gt;
  
  
  Emergent tension #3: the one that was a bug, not a feature
&lt;/h2&gt;

&lt;p&gt;Not every emergent interaction is a gift. A player on itch.io reported that&lt;br&gt;
building costs "don't go up until they're actually completed, so you can&lt;br&gt;
queue up a ton of them at a really low cost." Here's why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getCost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Building&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Resource&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Resource&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseCost&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;getCost&lt;/code&gt; scales off &lt;code&gt;b.count&lt;/code&gt; — units actually completed. Compare that to&lt;br&gt;
&lt;code&gt;buyBuilding&lt;/code&gt;, which queues a purchase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buyBuilding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Building&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getCost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;spend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buildQueues&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;buildQueues&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;lt;- a SEPARATE counter from b.count&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;buildQueues&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timeLeft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;totalTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;buildQueues[b.id].count&lt;/code&gt; and &lt;code&gt;b.count&lt;/code&gt; are two different numbers, and&lt;br&gt;
&lt;code&gt;getCost&lt;/code&gt; only ever reads the second one. Queue ten of the same building&lt;br&gt;
and every one of them prices at the &lt;em&gt;first&lt;/em&gt; unit's cost, because none of&lt;br&gt;
them have "completed" yet as far as the cost function is concerned. Neither&lt;br&gt;
function is wrong in isolation — the interaction between them is wrong.&lt;/p&gt;

&lt;p&gt;The interesting part: we'd already solved this exact shape of problem&lt;br&gt;
somewhere else in the codebase. &lt;code&gt;buyColonyBuilding&lt;/code&gt; (a different building&lt;br&gt;
category, with hard caps) already accounts for owned &lt;em&gt;plus&lt;/em&gt; queued:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buyColonyBuilding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ColonyBuilding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Built + already-queued must never exceed the cap — otherwise queuing&lt;/span&gt;
  &lt;span class="c1"&gt;// a batch up front keeps constructing past maxCount as each one finishes.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buildQueues&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix pattern existed in the codebase before the bug was even reported —&lt;br&gt;
it just hadn't been applied everywhere the same category of interaction&lt;br&gt;
could occur. The practical lesson: once you know two of your systems share&lt;br&gt;
state in a way you didn't fully map, go looking for every other place the&lt;br&gt;
same shape of interaction could be silently happening. It usually is.&lt;/p&gt;
&lt;h2&gt;
  
  
  How we actually find this stuff
&lt;/h2&gt;

&lt;p&gt;We don't rely on hunches. The game exposes a debug hook that lets an&lt;br&gt;
external harness drive the &lt;em&gt;real&lt;/em&gt; simulation loop, not a separate model of&lt;br&gt;
it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Balance-sim hook — only active when the URL contains ?__sim.&lt;/span&gt;
&lt;span class="c1"&gt;// Lets an offline harness fast-forward the real economy and read/drive it.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(\?&lt;/span&gt;&lt;span class="sr"&gt;|&amp;amp;&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;__sim&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;__SIM&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;__SIM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;useGameStore&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;recalc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fireRandomEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;setDtCap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;_tickDtCap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A headless script loads the page with &lt;code&gt;?__sim&lt;/code&gt;, waits for &lt;code&gt;window.__SIM&lt;/code&gt;,&lt;br&gt;
and then calls &lt;code&gt;tick()&lt;/code&gt; in a tight loop with an inflated &lt;code&gt;dt&lt;/code&gt; cap — the&lt;br&gt;
exact same &lt;code&gt;tick&lt;/code&gt;/&lt;code&gt;recalc&lt;/code&gt; functions a real browser session calls once a&lt;br&gt;
frame, just run thousands of times a second with no rendering in between.&lt;br&gt;
When we tune one system, we re-run the harness across the &lt;em&gt;entire&lt;/em&gt; game&lt;br&gt;
arc, not just the system we touched, specifically because the interesting&lt;br&gt;
failures show up two or three systems away from the one we edited.&lt;/p&gt;

&lt;p&gt;The most recent finding from that harness: a tuning pass on research costs&lt;br&gt;
eliminated a "front-loading" exploit where the early game moved faster than&lt;br&gt;
intended — but in fixing it, produced a new credits-negative trough three&lt;br&gt;
to eight hours in, a dip that existed in neither the pre-nerf nor the&lt;br&gt;
intended post-nerf design. Nobody wrote that trough into a spec. It emerged&lt;br&gt;
from two changes that were each individually correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual takeaway
&lt;/h2&gt;

&lt;p&gt;"Emergence" gets talked about like a feature you add. It isn't. It's a&lt;br&gt;
&lt;em&gt;cost&lt;/em&gt; you pay for having systems that share state at all, and the only&lt;br&gt;
lever you have is how fast you notice it — through simulation, through&lt;br&gt;
player reports, through deliberately stress-testing the seams between&lt;br&gt;
systems instead of just the systems themselves.&lt;/p&gt;

&lt;p&gt;If your game has more than two or three systems reading the same&lt;br&gt;
underlying numbers, you don't get to choose whether emergent behavior&lt;br&gt;
happens. You only get to choose whether you find it before your players do.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>systemdesign</category>
      <category>design</category>
    </item>
    <item>
      <title>Turning game telemetry into better onboarding decisions</title>
      <dc:creator>Renderscribe</dc:creator>
      <pubDate>Wed, 09 Sep 2026 08:57:49 +0000</pubDate>
      <link>https://dev.to/renderscribe/turning-game-telemetry-into-better-onboarding-decisions-5850</link>
      <guid>https://dev.to/renderscribe/turning-game-telemetry-into-better-onboarding-decisions-5850</guid>
      <description>&lt;p&gt;The most useful analytics result is not always a surprising number. Sometimes it is the moment when the data shows you where your assumptions end.&lt;/p&gt;

&lt;p&gt;That happened with Galactic Idle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7v42bo8qo94068npnu0i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7v42bo8qo94068npnu0i.png" alt=" " width="799" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The first five minutes told us more than the late game
&lt;/h2&gt;

&lt;p&gt;We shipped opt-in, anonymous analytics and looked at the first 15 real players. This is a tiny sample, so it is not a retention study or a claim about our audience. It is a directional signal: enough to tell us where the onboarding deserves attention.&lt;/p&gt;

&lt;p&gt;The early funnel looked like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Milestone&lt;/th&gt;
&lt;th&gt;Players&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Opened the game&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reached the first goal&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;53%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reached Industrial Revolution&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Played past seven minutes&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reached the endgame&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;7%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The median observed playtime was five minutes. Six people never produced a game event at all.&lt;/p&gt;

&lt;p&gt;That immediately changed the question we were asking. We had been thinking about mid-game balance: research costs, content density, and the economy after the first hour. But most new players were not getting anywhere near that part of the game.&lt;/p&gt;

&lt;p&gt;The onboarding problem was earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  A funnel is useful, but only if the events are meaningful
&lt;/h2&gt;

&lt;p&gt;Our first implementation was goal-heavy. We recorded milestone goals, so the data gave us a clear view of progression after a player started playing.&lt;/p&gt;

&lt;p&gt;It also created a blind spot.&lt;/p&gt;

&lt;p&gt;There was an &lt;code&gt;app_open&lt;/code&gt; event, then eventually a first goal. Between those two points, we recorded almost nothing. When someone opened the game and left before the first goal, we could see the drop-off but not the cause.&lt;/p&gt;

&lt;p&gt;That gap might mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the game was slow to become interactive;&lt;/li&gt;
&lt;li&gt;the title screen did not make the next action obvious;&lt;/li&gt;
&lt;li&gt;the first screen was confusing;&lt;/li&gt;
&lt;li&gt;the player simply decided the game was not for them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are four different product decisions. A single “player left” number cannot distinguish them.&lt;/p&gt;

&lt;p&gt;The lesson was simple: instrument the questions you want to answer, not only the achievements you are proud of.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we added to the first 90 seconds
&lt;/h2&gt;

&lt;p&gt;The next build adds a small set of events around the opening:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app_ready&lt;/code&gt; records when the game becomes interactive and includes the load time.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;first_interaction&lt;/code&gt; records the first meaningful tap or building action.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tutorial_step&lt;/code&gt; records progress through the opening sequence.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;session_end&lt;/code&gt; records the session duration, furthest milestone, tap count, and a resource snapshot.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app_resume&lt;/code&gt; separates a return to an existing page from a genuinely new open. This matters because fullscreen toggles and iframe reloads can otherwise make one sitting look like several sessions.&lt;/p&gt;

&lt;p&gt;None of these events needs a name, email address, session recording, or a replay of what a player did. They answer product questions while keeping the collection deliberately narrow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning observations into onboarding decisions
&lt;/h2&gt;

&lt;p&gt;Telemetry is not the fix. It is the instrument panel that helps you choose the fix.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Observation&lt;/th&gt;
&lt;th&gt;Likely next decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Long delay between &lt;code&gt;app_open&lt;/code&gt; and &lt;code&gt;app_ready&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Reduce startup work, improve loading feedback, or show a clearer ready state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;app_ready&lt;/code&gt; is fast but &lt;code&gt;first_interaction&lt;/code&gt; is missing&lt;/td&gt;
&lt;td&gt;Improve the title screen, affordances, and first-call-to-action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Players interact but fail to reach the first goal&lt;/td&gt;
&lt;td&gt;Rework the opening objective or explain the feedback loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Players reach several goals, then disappear in a burst&lt;/td&gt;
&lt;td&gt;Reduce the early goal firehose and add breathing room&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sessions end before the first system unlock&lt;/td&gt;
&lt;td&gt;Move a meaningful glimpse of the wider game earlier&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is the difference between “our onboarding has a 53% conversion rate” and “we need to learn whether the first minute is slow, unclear, or simply unappealing.” The second statement leads to a testable change.&lt;/p&gt;

&lt;h2&gt;
  
  
  The activity log was a clue, not a conclusion
&lt;/h2&gt;

&lt;p&gt;One screenshot from a fresh save showed a wall of notifications: Solar Pioneer, First Spark, Mineral Extraction, Open for Business, Solar Farm, Industrial Revolution, and several unlocks in quick succession.&lt;/p&gt;

&lt;p&gt;The log looked exciting. It also explained why the opening might feel noisy.&lt;/p&gt;

&lt;p&gt;Goals were intended to provide pacing. In practice, several of them fired within the first few minutes, then the game became quieter while the player was still learning what mattered. The data did not prove that the activity log caused people to leave, but it gave us a concrete hypothesis to test.&lt;/p&gt;

&lt;p&gt;That is a healthy role for analytics: narrow the search space, then let design and player feedback do the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the data still cannot tell us
&lt;/h2&gt;

&lt;p&gt;Fifteen players are enough to find a suspicious cliff, not enough to declare a universal truth. We still need to measure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether the same pattern holds across more traffic;&lt;/li&gt;
&lt;li&gt;whether players return on another day;&lt;/li&gt;
&lt;li&gt;whether the first conquest becomes a stronger onboarding milestone;&lt;/li&gt;
&lt;li&gt;whether changes to the title screen and early goals improve the funnel;&lt;/li&gt;
&lt;li&gt;whether the English-only interface affects players in non-English locales.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also need to be careful with our definitions. &lt;code&gt;app_open&lt;/code&gt; is not automatically a session, and a missing event is not automatically a negative experience. Better instrumentation reduces those ambiguities, but it never removes the need for judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical takeaway
&lt;/h2&gt;

&lt;p&gt;Start with the smallest set of events that can answer the next onboarding question.&lt;/p&gt;

&lt;p&gt;For a new game, that might be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When is the game ready?&lt;/li&gt;
&lt;li&gt;When does the player first interact?&lt;/li&gt;
&lt;li&gt;What is the first meaningful outcome?&lt;/li&gt;
&lt;li&gt;When does the session end?&lt;/li&gt;
&lt;li&gt;What was the furthest useful milestone?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then connect each event to a decision you are willing to make. If you would not change the game based on the result, the event probably does not belong in the first version of the dashboard.&lt;/p&gt;

&lt;p&gt;The best outcome of our first analytics pass was not discovering that the endgame was too hard. It was realizing that we had been trying to optimize a part of the game most new players never saw.&lt;/p&gt;

&lt;p&gt;Before tuning the galaxy, we need to make sure players can find it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Galactic Idle analytics are opt-in and anonymous. We do not collect names, email addresses, IP addresses, or session recordings. Players can disable analytics in Settings.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Building Galactic Idle: Our First Public Balancing Pass</title>
      <dc:creator>Renderscribe</dc:creator>
      <pubDate>Mon, 07 Sep 2026 17:22:28 +0000</pubDate>
      <link>https://dev.to/renderscribe/building-galactic-idle-our-first-public-balancing-pass-311c</link>
      <guid>https://dev.to/renderscribe/building-galactic-idle-our-first-public-balancing-pass-311c</guid>
      <description>&lt;p&gt;We recently released the first public build of &lt;strong&gt;Galactic Idle&lt;/strong&gt;, a browser-based sci-fi strategy game about resource management, research, fleets, and planetary conquest.&lt;/p&gt;

&lt;p&gt;Play it here:&lt;br&gt;&lt;br&gt;
&lt;a href="https://renderscribe-interactive.itch.io/galactic-idle" rel="noopener noreferrer"&gt;Galactic Idle&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The game is built around a simple loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate resources.&lt;/li&gt;
&lt;li&gt;Invest in buildings and research.&lt;/li&gt;
&lt;li&gt;Grow your fleet.&lt;/li&gt;
&lt;li&gt;Expand across the galaxy.&lt;/li&gt;
&lt;li&gt;Return later and decide what comes next.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The challenge has been making that loop feel rewarding without allowing progress to become meaningless.&lt;/p&gt;

&lt;h2&gt;
  
  
  The balancing problem
&lt;/h2&gt;

&lt;p&gt;Idle games have a difficult balance to find.&lt;/p&gt;

&lt;p&gt;If resources accumulate too slowly, players feel blocked. If they accumulate too quickly, the player can return after a short absence and immediately unlock half the game.&lt;/p&gt;

&lt;p&gt;We have been tuning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Offline resource accumulation&lt;/li&gt;
&lt;li&gt;Building and research costs&lt;/li&gt;
&lt;li&gt;Production rates&lt;/li&gt;
&lt;li&gt;Fleet growth&lt;/li&gt;
&lt;li&gt;Planet conquest requirements&lt;/li&gt;
&lt;li&gt;Ten-minute conquest timers&lt;/li&gt;
&lt;li&gt;Mission rewards&lt;/li&gt;
&lt;li&gt;Research progression&lt;/li&gt;
&lt;li&gt;Resource caps and economy pacing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our current goal is for progress to feel generous early on, but increasingly deliberate as the player expands.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are trying to learn
&lt;/h2&gt;

&lt;p&gt;We would especially value feedback from developers and players who have worked on idle, incremental, strategy, or management games.&lt;/p&gt;

&lt;p&gt;A few questions we are investigating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How long should a player be away before offline rewards feel meaningful?&lt;/li&gt;
&lt;li&gt;What percentage of normal production is reasonable for offline progress?&lt;/li&gt;
&lt;li&gt;When does a resource cap become helpful structure rather than frustration?&lt;/li&gt;
&lt;li&gt;Should conquest timers be shortened by research, fleet upgrades, or special bonuses?&lt;/li&gt;
&lt;li&gt;How many meaningful decisions should a player make during a typical ten-minute session?&lt;/li&gt;
&lt;li&gt;How do you communicate that a slow upgrade is building toward a larger strategic payoff?&lt;/li&gt;
&lt;li&gt;How do you prevent late-game numbers from making early systems irrelevant?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We are also interested in how players interpret the economy. A resource can be mathematically balanced and still feel wrong if its purpose is unclear or its feedback is too subtle.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we have learned so far
&lt;/h2&gt;

&lt;p&gt;One lesson is that visible feedback matters as much as the underlying numbers.&lt;/p&gt;

&lt;p&gt;Players need to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What they can afford&lt;/li&gt;
&lt;li&gt;What they are working toward&lt;/li&gt;
&lt;li&gt;Why a resource matters&lt;/li&gt;
&lt;li&gt;What they will receive for completing a goal&lt;/li&gt;
&lt;li&gt;How long a timed action will take&lt;/li&gt;
&lt;li&gt;What changed while they were away&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We have added requirement tooltips, mission reward reminders, planet information panels, conquest timers, and next-goal notifications to make those relationships clearer.&lt;/p&gt;

&lt;p&gt;The numbers still need testing, but clearer information makes balancing feedback much more useful. A player can tell us that a conquest feels too slow only if they understand how long it is supposed to take.&lt;/p&gt;

&lt;h2&gt;
  
  
  We would love your feedback
&lt;/h2&gt;

&lt;p&gt;If you try Galactic Idle, please tell us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How far you progressed&lt;/li&gt;
&lt;li&gt;How long you played&lt;/li&gt;
&lt;li&gt;Which system felt most satisfying&lt;/li&gt;
&lt;li&gt;Where progression felt too fast or too slow&lt;/li&gt;
&lt;li&gt;Whether the offline rewards felt fair&lt;/li&gt;
&lt;li&gt;Whether the conquest timer created anticipation or friction&lt;/li&gt;
&lt;li&gt;What you would change first&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Specific examples are especially helpful. “Research is too expensive after the second tier” gives us something actionable to investigate.&lt;/p&gt;

&lt;p&gt;Give us feedback here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://itch.io/t/6909091/what-should-we-add-to-the-galaxy-next" rel="noopener noreferrer"&gt;Post Feedback&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We are sharing the game early because balancing is better when it happens in conversation with real players. If you are also working on an idle or strategy game, feel free to share what balancing problem you are currently wrestling with.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Background music by DavidKBD, used under the applicable license: &lt;a href="https://davidkbd.itch.io/" rel="noopener noreferrer"&gt;https://davidkbd.itch.io/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  gamedev #indiedev #webdev #gamebalance #indiegames
&lt;/h1&gt;

</description>
      <category>testing</category>
      <category>gamedev</category>
      <category>development</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
