<?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>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>
