<?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: ethantao14</title>
    <description>The latest articles on DEV Community by ethantao14 (@ethantao14).</description>
    <link>https://dev.to/ethantao14</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%2F4095983%2F7fa09170-be78-45cf-9f23-051fdb4e5c8b.png</url>
      <title>DEV Community: ethantao14</title>
      <link>https://dev.to/ethantao14</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ethantao14"/>
    <language>en</language>
    <item>
      <title>Testing game rules with bots: Monte Carlo on a TypeScript reducer</title>
      <dc:creator>ethantao14</dc:creator>
      <pubDate>Sun, 04 Oct 2026 00:36:32 +0000</pubDate>
      <link>https://dev.to/ethantao14/testing-game-rules-with-bots-monte-carlo-on-a-typescript-reducer-2d3a</link>
      <guid>https://dev.to/ethantao14/testing-game-rules-with-bots-monte-carlo-on-a-typescript-reducer-2d3a</guid>
      <description>&lt;p&gt;If your game logic is a pure reducer, &lt;code&gt;(state, action) =&amp;gt; newState&lt;/code&gt;, then a bot is just a function from state to action. That means you can measure your rules by playing thousands of games, not just test that they do not crash.&lt;/p&gt;

&lt;p&gt;I did this for Game Mode Sandbox, a front-end-only Next.js and TypeScript site that I build with two teammates. It runs one stock-drafting game under six rule sets, with fake companies and generated prices. I own the engine. This post walks through the "Mode Lab" I added this week: five bots, a seeded runner, a terminal command, and a browser page. Players see nothing new. It is a developer tool. (I built it with Cline.)&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A bot is a function
&lt;/h2&gt;

&lt;p&gt;The engine exposes &lt;code&gt;createGameReducer(market)&lt;/code&gt;, which returns &lt;code&gt;(state, action) =&amp;gt; newState&lt;/code&gt;. An action the rules do not allow returns the same state object, unchanged, and never throws. Given that, a bot only needs this:&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;// A bot sees what a player sees and returns one action. The runner calls it until the game ends.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Strategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;chooseAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GameState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;market&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Market&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Rng&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;GameAction&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bigSpender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Strategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Big spender&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Buys every round it can, always at the largest allowed amount. Never sells.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;chooseAction&lt;/span&gt;&lt;span class="p"&gt;(&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;_market&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rng&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="nf"&gt;buyOrSkip&lt;/span&gt;&lt;span class="p"&gt;(&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;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;largestAmount&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;I wrote five, each one a probe for a specific rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Random:&lt;/strong&gt; sells now and then (25% chance when it can), skips 1 round in 5, and spends a random allowed amount.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Big spender:&lt;/strong&gt; always buys at the largest allowed amount. Never sells.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Penny pincher:&lt;/strong&gt; always buys at the smallest allowed amount. Never sells. Probes the minimum-spend rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loyalist:&lt;/strong&gt; only buys from the industry of the oldest stock it owns. Probes the industry limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Churner:&lt;/strong&gt; sells everything the rules allow each round, then buys at the largest amount. Probes the selling rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. The runner
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;playGame&lt;/code&gt; in &lt;code&gt;lib/lab/run.ts&lt;/code&gt; starts a game with &lt;code&gt;startGame(config, market, seed)&lt;/code&gt;, then loops: ask the bot for an action, feed it to the reducer, count what happened. Here is a simplified version:&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;// Simplified from lib/lab/run.ts. Helper names are shortened.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reduce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createGameReducer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;market&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;startGame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;market&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isOver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&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="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;hasLegalAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stuck&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;break&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;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chooseAction&lt;/span&gt;&lt;span class="p"&gt;(&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;market&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;actionRng&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;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&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;action&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;next&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; made an illegal move`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;,&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;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;next&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;Three choices are worth calling out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Illegal moves fail loudly.&lt;/strong&gt; The reducer returns the same object for an illegal move, so &lt;code&gt;next === state&lt;/code&gt; means the bot broke the rules. Throwing there means a buggy bot cannot quietly skew the averages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A stuck check.&lt;/strong&gt; Before each move, the runner checks that the player has at least one legal action (buy, skip, or sell). An earlier version of the engine had a real stuck-game bug, so this is a regression guard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate randomness.&lt;/strong&gt; Actions use their own random stream derived from the game seed, so any single game can be replayed exactly.&lt;/p&gt;

&lt;p&gt;Per game, the lab records buys, skips, rounds with nothing buyable, the share of cards blocked by rules, sales, auto-sells, idle cash, final value, and stuck games. The table shows per-game averages plus the 10th percentile, median, and 90th percentile of final value. From the terminal it runs with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run lab &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--games&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1000 &lt;span class="nt"&gt;--seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2024
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Fair comparisons: same seeds for every bot
&lt;/h2&gt;

&lt;p&gt;Every bot plays the same seeds within a mode. Without that, one bot could simply draw better boards and you would be measuring luck. With it, differences between rows come from behavior.&lt;/p&gt;

&lt;p&gt;A nice side effect is a built-in sanity check. In modes without selling, the Churner cannot sell, so its row should be identical to Big spender's. It is.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Watch out: count transitions, not steps
&lt;/h2&gt;

&lt;p&gt;Before I opened the first PR, a review flagged that auto-sells were being double-counted.&lt;/p&gt;

&lt;p&gt;Auto-sells happen as a new round starts, and the engine records them as events on that round's state. When the last round ends, the engine marks the game done but keeps that round's events on the final state. The lab counted auto-sell events after every move, so the last round's auto-sells got counted twice. Big spender in Active Trader showed 8.0 auto-sells per game instead of 7.0.&lt;/p&gt;

&lt;p&gt;The check was arithmetic. Active Trader has 10 rounds and a 3-year hold, so stocks bought in rounds 1 to 7 reach their hold by round 10: 7 auto-sells.&lt;/p&gt;

&lt;p&gt;The fix is to count only on transitions:&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;// Simplified: only count auto-sells when the round actually advances.&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;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;round&lt;/span&gt; &lt;span class="o"&gt;!==&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;round&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;autoSells&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;countAutoSells&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&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;I wrote the test first. It failed on the old code (got 4, expected 3 in Sector Focus), then passed with the fix. All 347 existing tests had passed with the bug present.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; when state carries events forward, count them when the state changes phase, not on every step.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Running it in the browser without freezing
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;/dev/lab&lt;/code&gt; page runs the same lab in a React client component. You pick games per bot and a seed, press "Run the lab", and the tables fill in one mode and bot pair at a time. Between pairs, it yields so the page can repaint.&lt;/p&gt;

&lt;p&gt;My first version yielded with &lt;code&gt;setTimeout(0)&lt;/code&gt;. A full run took 13.0 seconds in Chrome, even though the terminal does the same work in about a second. The tell: 100 games per bot took as long as 1,000.&lt;/p&gt;

&lt;p&gt;Chrome throttles chained timers in pages it treats as hidden. In my test, 20 chained &lt;code&gt;setTimeout(0)&lt;/code&gt; calls took 11.5 seconds, and 20 &lt;code&gt;MessageChannel&lt;/code&gt; messages took 0ms. So I yield the way React's scheduler does:&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;const&lt;/span&gt; &lt;span class="nx"&gt;nextTick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&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;resolve&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MessageChannel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&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;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&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;The run dropped from 13.0s to 4.2s in a background window. A Web Worker would move the work off the main thread entirely. This was the smaller change, and it was enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Testing the page
&lt;/h2&gt;

&lt;p&gt;The page and the terminal script share one list of column definitions, so they always print the same numbers. A test renders the page, runs it, and compares the cells to the runner's output for the same seed.&lt;/p&gt;

&lt;p&gt;That test passed on my laptop and failed on CI after 1,267ms. Testing Library's &lt;code&gt;waitFor&lt;/code&gt; gives up after 1 second by default, and the CI runner was just slower. I gave that &lt;code&gt;waitFor&lt;/code&gt; a 10-second timeout and the test 20 seconds. A test that does real work has a time budget, so set it on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Was it useful?
&lt;/h2&gt;

&lt;p&gt;Here is Sector Swap at 1,000 games per bot, seed 2024 (per-game averages):&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%2Fbhk7tc2oy7rah0qijl06.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%2Fbhk7tc2oy7rah0qijl06.png" alt="Sector Swap results from the Mode Lab page" width="800" height="659"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bot&lt;/th&gt;
&lt;th&gt;Buys&lt;/th&gt;
&lt;th&gt;Sales&lt;/th&gt;
&lt;th&gt;Idle cash&lt;/th&gt;
&lt;th&gt;Median final value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Big spender&lt;/td&gt;
&lt;td&gt;8.0&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$11,571&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Penny pincher&lt;/td&gt;
&lt;td&gt;8.0&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;td&gt;$6,000&lt;/td&gt;
&lt;td&gt;$10,402&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Churner&lt;/td&gt;
&lt;td&gt;8.0&lt;/td&gt;
&lt;td&gt;7.0&lt;/td&gt;
&lt;td&gt;$8,038&lt;/td&gt;
&lt;td&gt;$10,601&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two findings from the full run:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;In Sector Swap, selling every round caps you at one stock.&lt;/strong&gt; A sale must be replaced from the same industry, and the replacement uses that round's pick. Two reasonable rules combine into a play style nobody designed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In Classic Draft, the Loyalist makes 1 buy in 8 rounds&lt;/strong&gt; and leaves $8,750 idle, because the one-per-industry rule blocks the rest of its industry.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither is a bug. Both are things we would not have known from reading the rules. The suite is at 355 tests, and the whole lab is a few hundred lines on top of an engine that was already pure.&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%2Fuxo2750376vm453el65q.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%2Fuxo2750376vm453el65q.png" alt="Full npm run lab output" width="800" height="861"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/AryamanGandhi/app-store" rel="noopener noreferrer"&gt;github.com/AryamanGandhi/app-store&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>react</category>
      <category>testing</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Three Bugs That Survived Every Automated Check (And How Line-by-Line Review Caught Them)</title>
      <dc:creator>ethantao14</dc:creator>
      <pubDate>Wed, 16 Sep 2026 20:45:30 +0000</pubDate>
      <link>https://dev.to/ethantao14/three-bugs-that-survived-every-automated-check-and-how-line-by-line-review-caught-them-401h</link>
      <guid>https://dev.to/ethantao14/three-bugs-that-survived-every-automated-check-and-how-line-by-line-review-caught-them-401h</guid>
      <description>&lt;p&gt;Game Mode Sandbox is a new project our team is building alongside our stock market game: an app-store-style site where each app is a front-end-only version of the stock draft game running on fake data, one per rule variant, so we can playtest which rules feel best before locking anything into the real product. Kenny owns the store pages and data, Jane owns the gameplay screens, and I own the engine, the pure logic layer that decides what a player can do and what happens when they do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design principles for the engine
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pure functions, no UI&lt;/strong&gt;: every rule can be tested directly, no clicking through screens required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seeded randomness&lt;/strong&gt;: every random draw, like which stocks appear each round, comes from the game's seed, so the same seed replays the same game and bugs are reproducible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invalid actions are no-ops&lt;/strong&gt;: they leave the game state unchanged instead of crashing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Money is always rounded to whole cents&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Market data is passed in, never hardcoded&lt;/strong&gt;, so tests can supply their own&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;p&gt;Four pull requests merged this week, +1,703/-25 lines total:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Config validation&lt;/strong&gt; (+792/-10): checks whether a rule setup is actually playable and explains any problem in plain English, pointing at the setting to fix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Board drawing&lt;/strong&gt; (+456/-5): picks which stocks appear each round across all three board types, greys out unpickable stocks with a reason, and guarantees at least one pickable stock every round&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An off-by-one fix&lt;/strong&gt; (+30/-5), covered below&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spend validation&lt;/strong&gt; (+425/-5): works out how much a player can spend per pick under four different money rules, holding back enough cash to cover remaining picks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;140 tests pass on main, 122 of them for the engine, roughly two lines of test code for every line of engine code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three bugs, all caught after everything was green
&lt;/h2&gt;

&lt;p&gt;Lint, typecheck, the full test suite, and the build all passed on every pull request. An automated code reviewer ran on two of them and reported no actionable defects. It also could not run the tests in its own sandbox. All three bugs below were found by reading the code line by line against the written spec, not by any of the automated tooling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 1: the shrinking board
&lt;/h3&gt;

&lt;p&gt;Classic Draft is supposed to show 5 stocks every round. Instead it went 5, 5, 5, 5, 4, 3, 2, 1.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unownedIndustries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;industriesWithStock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;industry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;ownsIndustry&lt;/span&gt;&lt;span class="p"&gt;(&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;industry&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;The one-per-industry board type was filtering out sectors the player already owned before drawing, a rule meant only for the same-industry board type. One committed test even asserted the shrinking 1-card board as correct behavior, since it was written from the buggy code instead of the spec. The fix draws from every industry with an available stock and greys out owned ones instead of removing them. Verified against 5,000 randomly generated game states across five rule sets: 0 violations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 2: the hold that never ends
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="nx"&gt;holdYears&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;rounds&lt;/span&gt;
&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="nx"&gt;holdYears&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;rounds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A stock bought in round 1 of a 5-round game with a 5-year hold needs a 6th round to auto-sell, which never comes. The check only warned when the hold was strictly longer than the game, not equal to it, so this exact setup passed with no warning. The original tests asserted that this case should not warn, which is how it slipped through.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 3: the lost cent
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&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;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cash&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;picks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="c1"&gt;// After&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;floor&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cash&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;picks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Floating point decimals like $0.57 are stored as something closer to 0.5699999, and flooring that drops a cent. Tested across every dollar amount from $1.00 to $10,000.00, split 2, 3, 5, and 8 ways: the old formula was wrong in 98,440 cases. The fix does the arithmetic in whole cents, which are exact integers. After the fix: 0 errors across 714,215 checked combinations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;A green checkmark tells you the code matches the tests. It does not tell you the code matches the spec, and a test written from the code instead of the spec can lock in a bug with just as much confidence as it confirms a fix.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/AryamanGandhi/app-store" rel="noopener noreferrer"&gt;https://github.com/AryamanGandhi/app-store&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Two Cline Features That Actually Changed How I Work: Permissions and Diff Explanations</title>
      <dc:creator>ethantao14</dc:creator>
      <pubDate>Wed, 26 Aug 2026 19:19:49 +0000</pubDate>
      <link>https://dev.to/ethantao14/two-cline-features-that-actually-changed-how-i-work-permissions-and-diff-explanations-1mk4</link>
      <guid>https://dev.to/ethantao14/two-cline-features-that-actually-changed-how-i-work-permissions-and-diff-explanations-1mk4</guid>
      <description>&lt;p&gt;I have been building the draft and portfolio builder side of a stock draft game using Cline, running on Sonnet through a Claude Console API key. Most of the writing about AI coding tools focuses on whether the code is good. That matters, but two other things ended up mattering just as much day to day: how much control I actually had over what the tool could do, and whether I could tell what it had just done. Here is a concrete look at both, pulled directly from this project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permissions: a settings menu, not a guessing game
&lt;/h2&gt;

&lt;p&gt;Every agentic coding tool has to answer the same question. How much should it be allowed to do without asking first. Too restrictive and you spend your day approving trivial actions. Too permissive and one bad command becomes a real problem.&lt;/p&gt;

&lt;p&gt;Cline handles this with a straightforward auto-approve menu rather than anything hidden or inferred from a conversation. Here is the exact configuration used throughout this project.&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%2Fzywuwk9ut0p85ex4jlu3.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%2Fzywuwk9ut0p85ex4jlu3.png" alt="Cline's auto-approve settings menu" width="668" height="746"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Cline's auto-approve settings menu.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Breaking down what each toggle actually does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read project files / Read all files&lt;/strong&gt;, both on. The first scopes reads to files inside the current project. The second extends that to anywhere on disk. With this on, Cline never had to ask before opening a file for context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edit project files&lt;/strong&gt;, on. &lt;strong&gt;Edit all files&lt;/strong&gt;, off. Cline could freely modify anything inside the project without asking, but editing something outside the project's boundaries required explicit approval each time. This is the one I would flag as the most important default. It gives an agent real freedom inside the sandbox of your repo while keeping a hard stop at the edge of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execute safe commands&lt;/strong&gt;, on. &lt;strong&gt;Execute all commands&lt;/strong&gt;, off. Cline distinguishes between commands it considers safe, things like running a linter or test suite, and anything it cannot classify that way. Safe commands ran without interruption. Anything else needed a manual yes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the browser&lt;/strong&gt;, off. Cline never had a reason to browse during this project, so this stayed off by default rather than being something to remember to disable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use MCP servers&lt;/strong&gt;, on. This project used MCP tooling, so it was left enabled.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical effect: Cline moved quickly on everything that mattered for iteration speed, reading context, running lint and test commands, while the higher risk categories, editing arbitrary files outside the project, running arbitrary shell commands, browsing, stayed off unless deliberately turned on. None of this required trusting an opaque memory system or hoping a written instruction got remembered three sessions later. It was a set of checkboxes, visible at any time, adjustable in seconds.&lt;/p&gt;

&lt;p&gt;If you are evaluating a coding agent for a real project, this is worth checking early. Can you see, in one place, exactly what it is allowed to do, or do you have to infer it from behavior?&lt;/p&gt;
&lt;h2&gt;
  
  
  Clarity: a diff explanation you actually get, not one you have to ask for twice
&lt;/h2&gt;

&lt;p&gt;After Cline finishes a task, it shows two buttons next to its summary: View Changes and Explain Changes. View Changes is the raw diff. Explain Changes, when clicked, generates a plain-language explanation placed directly under the relevant code, file by file, with a reply box so you can ask a follow-up question right there, the same way you would comment on a pull request.&lt;/p&gt;

&lt;p&gt;Here is a real example from this project. A review pass caught that four tickers in the shared sector data list had no matching historical price file, meaning a user could draft one and have it silently vanish from their results. The fix was a small, scoped helper function rather than editing the shared data file directly:&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;// lib/available-stocks.ts&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;STOCKS_BY_SECTOR&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/data/sectors&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Sector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Stock&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/types&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// A handful of tickers in the shared sector lists (data/sectors.ts) don't have&lt;/span&gt;
&lt;span class="c1"&gt;// a matching file in data/historical/, so picking them would silently show&lt;/span&gt;
&lt;span class="c1"&gt;// "No data" on the results page. Filtered out here, scoped to the draft flow,&lt;/span&gt;
&lt;span class="c1"&gt;// rather than editing the shared sectors list.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TICKERS_WITHOUT_HISTORICAL_DATA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HES&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PXD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EMR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ETN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAvailableStocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Sector&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Stock&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;STOCKS_BY_SECTOR&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;sector&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;TICKERS_WITHOUT_HISTORICAL_DATA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ticker&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;Clicking Explain Changes on the commit that wired this helper into the draft page produced this, directly under the diff:&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%2Fc10znvkdu1c6ev23tucd.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%2Fc10znvkdu1c6ev23tucd.png" alt="Cline explaining why the getAvailableStocks wiring change mattered" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Cline explaining why the getAvailableStocks wiring change mattered.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That explanation is not a changelog entry someone wrote after the fact. It is generated at the point of the change, describing not just what moved but why the one-line wiring change was the part that actually made the fix take effect in the running app. The reply box underneath means if that explanation is not enough, you do not have to open a new conversation and re-explain your own codebase to ask a follow-up. You ask it right there, with the diff still on screen.&lt;/p&gt;

&lt;p&gt;The same pattern showed up when a review pass flagged that the homepage was still 100% unmodified &lt;code&gt;create-next-app&lt;/code&gt; boilerplate, with zero link into the actual feature:&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%2F2x9opc3xzksu2bm8rxdn.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%2F2x9opc3xzksu2bm8rxdn.png" alt="Cline explaining the homepage rewrite" width="800" height="695"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Cline explaining the homepage rewrite.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Across a project where a dozen or more files can change in a single task, this is the difference between being able to explain your own code to a teammate afterward and quietly hoping they do not ask too many questions about a diff you half remember.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why both of these matter together
&lt;/h2&gt;

&lt;p&gt;Permissions and clarity solve different problems, but they compound. A tool that moves fast without asking permission for every action is only comfortable to use if you also know, clearly and immediately, what it just did and why. Cline's auto-approve menu gives you control over the blast radius. Explain Changes gives you visibility into what happened inside that radius. Neither one alone would have been enough to make me comfortable handing over a real feature build. Together, they made it a normal part of the workflow rather than something to babysit.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/ethantao14/cline-stock-market-game" rel="noopener noreferrer"&gt;github.com/ethantao14/cline-stock-market-game&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
