{
  "authors": [
    {
      "name": "Mikey Nichols",
      "url": "https://mnix.dev"
    }
  ],
  "description": "Practical web development, real projects, and lessons from the build.",
  "feed_url": "https://mnixblog.netlify.app/feed.json",
  "home_page_url": "https://mnixblog.netlify.app/",
  "items": [
    {
      "content_html": "<p>You push, and a minute later you see it. An API key sits in <code>.env</code>, in a commit that is already on GitHub.</p>\n<p>The quick fix is to delete the file and commit again. That feels like it should work. It does not, and we will see exactly why in a scratch repository.</p>\n<p>Think of history as a chain where every link carries a fingerprint of the link before it. You cannot pull one link out. You have to rebuild every link that came after it.</p>\n<p>There is an order to this, and the first step is not a Git command:</p>\n<ol>\n<li>Revoke or rotate the key.</li>\n<li>Rewrite history so the key is in no commit.</li>\n<li>Force-push the new history.</li>\n<li>Get your teammates onto it.</li>\n<li>Ask the host about the copies it keeps.</li>\n</ol>\n<p>We will do steps 2 and 3 on a real repository and check the result with two commands. The end state looks like this:</p>\n<pre><code class=\"language-console\">$ git log --oneline -S\"FAKE-not-a-real-key\"\n$ git count-objects -vH\nsize-pack: 1.98 KiB\n</code></pre>\n<aside class=\"ob-callout ob-callout-note\" role=\"note\"><p class=\"ob-callout-title\"><strong>Note</strong></p>\n<p>Every command and output here comes from Git 2.45.2 on Windows 11, run in Git Bash, with <code>git-filter-repo</code> 2.47.0. The secret is fake on purpose: <code>FAKE-not-a-real-key-123</code>. Never practice with a real one. The commit hashes you get will differ from ours every time, because a hash covers the commit time. Never type one of ours. Copy yours from your own <code>git log</code>.</p>\n</aside>\n<h2 id=\"step-1-make-the-key-worthless\">Step 1: make the key worthless<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/you-committed-a-secret-what-to-do-in-order/#step-1-make-the-key-worthless\" aria-label=\"Link to this section\">#</a></h2>\n<p>Do this before anything else, and before you touch Git.</p>\n<p>A key that was pushed has been seen. It may be cached somewhere, copied by a build bot, or scraped by something that watches public pushes. Rewriting history afterward does not un-see it. GitHub's own page on this says to revoke or rotate the secret first, and that doing so may be enough on its own.</p>\n<p>So open the service that issued the key, create a new one, and delete the old one. Then put the new one somewhere that Git never sees.</p>\n<p>Everything after this is cleanup. The key is already dead, so we can take our time.</p>\n<h2 id=\"build-the-leak-yourself\">Build the leak yourself<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/you-committed-a-secret-what-to-do-in-order/#build-the-leak-yourself\" aria-label=\"Link to this section\">#</a></h2>\n<p>This sets up the same mess on your machine. The folder <code>hub.git</code> plays the part of GitHub. Paste it into Git Bash, in an empty folder.</p>\n<details>\n<summary>Build the leaked repository (Git Bash)</summary>\n<pre><code class=\"language-bash\">mkdir secret-lab &amp;&amp; cd secret-lab\ngit init --bare -b main hub.git\ngit clone hub.git alice\ncd alice\necho 'console.log(\"hello\");' &gt; app.js\ngit add app.js\ngit commit -m \"Add app\"\necho 'API_KEY=FAKE-not-a-real-key-123' &gt; .env\nhead -c 3000000 /dev/urandom &gt; db-backup.dump\ngit add .\ngit commit -m \"Add settings\"\necho 'console.log(\"v2\");' &gt;&gt; app.js\ngit commit -am \"Update app\"\ngit rm --cached .env db-backup.dump\nprintf '.env\\ndb-backup.dump\\n' &gt; .gitignore\ngit add .gitignore\ngit commit -m \"Stop tracking .env\"\ngit push origin main\ncd ..\ngit clone hub.git bob\ncd alice\n</code></pre>\n<p>The last three lines make <code>bob</code>, a teammate's clone that we use near the end. It has to exist now, before the history changes. Git says <code>You appear to have cloned an empty repository.</code> after the first clone. That is fine, <code>hub.git</code> is empty at that point.</p>\n<p><code>git add .</code> is the slip. It added the key file and a 3 MB database dump, which stands in for the thing you did not mean to commit. The last commit is the quick fix: it stops tracking both files and ignores them.</p>\n</details>\n<h2 id=\"why-deleting-the-file-is-not-enough\">Why deleting the file is not enough<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/you-committed-a-secret-what-to-do-in-order/#why-deleting-the-file-is-not-enough\" aria-label=\"Link to this section\">#</a></h2>\n<p>Ask Git which commits ever changed that text:</p>\n<pre><code class=\"language-console\">$ git log --oneline -S\"FAKE-not-a-real-key\"\nfbe993a Stop tracking .env\n65bf43d Add settings\n</code></pre>\n<p><code>-S</code> lists the commits where the number of times the text appears went up or down. <code>Add settings</code> put it in. <code>Stop tracking .env</code> took it out. The key is still inside the older commit, and anyone can read it:</p>\n<pre><code class=\"language-console\">$ git show 65bf43d:.env\nAPI_KEY=FAKE-not-a-real-key-123\n</code></pre>\n<p>Replace <code>65bf43d</code> with the hash of your own <code>Add settings</code> commit. To find it, run the <code>git log --oneline -S\"FAKE-not-a-real-key\"</code> command above and copy the hash from the lower line, the older commit that added the key. For your own secret, put a few characters of the real one inside the quotes. The latest commit is clean, and the history is not.</p>\n<details>\n<summary>Install git-filter-repo (once)</summary>\n<p><code>git filter-repo</code> is not part of Git. It is a separate Python script that Git can call as if it were a command, and it rewrites every commit in one pass. The older <code>git filter-branch</code> does a similar job, and we will not use it here.</p>\n<p>The project's own install notes (read October 2026) assume you already have Git and Python. Their routes are a package manager (they name Scoop for Windows), PyPI through <code>pipx</code>, or downloading the single script <code>git-filter-repo</code> and putting it on your <code>PATH</code>.</p>\n<p>What we tested: in a Python virtual environment (a private folder of Python packages), <code>python -m pip install git-filter-repo</code> put a <code>git-filter-repo.exe</code> in the environment's <code>Scripts</code> folder. Pip does not put that folder on your <code>PATH</code>. Before we added it, Git said:</p>\n<pre><code class=\"language-console\">$ git filter-repo --version\ngit: 'filter-repo' is not a git command. See 'git --help'.\n</code></pre>\n<p>In Git Bash, from the folder that holds the environment (here named <code>venv</code>), either of these fixes it for that window:</p>\n<pre><code class=\"language-bash\">export PATH=\"$PWD/venv/Scripts:$PATH\"\n</code></pre>\n<pre><code class=\"language-bash\">source venv/Scripts/activate\n</code></pre>\n<p>The second one turns the whole environment on. The first one only adds the folder. Open a new window and you have to do it again.</p>\n<p>The project's INSTALL file gives two other commands, <code>pipx install git-filter-repo</code> and, for a package manager such as Scoop, <code>PACKAGE_TOOL install git-filter-repo</code>, which for Scoop reads <code>scoop install git-filter-repo</code>. We did not run either of those, or the manual copy, so check the INSTALL file if one fails.</p>\n<p>To check the install, ask for the version:</p>\n<pre><code class=\"language-console\">$ git filter-repo --version\na40bce548d2c\n</code></pre>\n<p>That is a build hash, not a version number. <code>pip show git-filter-repo</code> prints <code>Version: 2.47.0</code>. The <code>--sensitive-data-removal</code> option we use needs 2.47 or newer, according to GitHub's page.</p>\n</details>\n<h2 id=\"remove-the-key-from-every-commit\">Remove the key from every commit<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/you-committed-a-secret-what-to-do-in-order/#remove-the-key-from-every-commit\" aria-label=\"Link to this section\">#</a></h2>\n<p>filter-repo refuses to run in a repository you have been working in. Try it in <code>alice</code>:</p>\n<pre><code class=\"language-console\">$ cd ../alice\n$ git filter-repo --sensitive-data-removal --invert-paths --path .env --path db-backup.dump\nAborting: Refusing to destructively overwrite repo history since\nthis does not look like a fresh clone.\n  (expected at most one entry in the reflog for HEAD)\n</code></pre>\n<p>Good. The rewrite throws the old history away, so it wants a copy where nothing else is at stake. Make a fresh clone and measure it first:</p>\n<pre><code class=\"language-console\">$ cd ..\n$ git clone hub.git fresh\n$ cd fresh\n$ git count-objects -vH\ncount: 13\nsize: 2.86 MiB\nin-pack: 0\npacks: 0\nsize-pack: 0 bytes\nprune-packable: 0\ngarbage: 0\nsize-garbage: 0 bytes\n</code></pre>\n<p>Thirteen loose objects, 2.86 MiB. Almost all of it is the dump. (A clone from GitHub arrives as one pack, so there you would read <code>size-pack</code> instead of <code>size</code>.)</p>\n<p>Now the rewrite:</p>\n<pre><code class=\"language-bash\">git filter-repo --sensitive-data-removal --invert-paths --path .env --path db-backup.dump\n</code></pre>\n<p>Here is what each part does:</p>\n<ul>\n<li><code>--path .env --path db-backup.dump</code> names the two files.</li>\n<li><code>--invert-paths</code> means everything except those paths. Leave it out and <code>--path</code> does the opposite: it keeps only those files and drops the rest. We ran that by mistake on purpose, and the repository kept only the commits that touched <code>.env</code>.</li>\n<li><code>--sensitive-data-removal</code> fetches every ref from <code>origin</code> first, so no branch is left holding the old history, and it prints the follow-up steps.</li>\n</ul>\n<details>\n<summary>The output, trimmed</summary>\n<pre><code class=\"language-text\">NOTICE: Fetching all refs from origin to make sure we rewrite\n        all history that may reference the sensitive data, via\n      git fetch -q --prune --update-head-ok --refmap \"\" origin +refs/*:refs/*\nParsed 4 commits\nHEAD is now at 35cccfa Stop tracking .env\n\nNew history written in 1.42 seconds; now repacking/cleaning...\nYou rewrote 4 (of 4) commits.\n\nNOTE: First Changed Commit(s) is/are:\n  25b989967cec4c5eb38a6818ae42853374f55c8a\nNOTE: LFS object orphaning not checked (LFS not in use)\n\nRepacking your repo and cleaning out old unneeded objects\nCompletely finished after 2.70 seconds.\n\nNEXT STEPS FOR YOUR SENSITIVE DATA REMOVAL:\n  * ...\n</code></pre>\n<p>The <code>NEXT STEPS</code> list that follows is long. It covers what we do next. Keep the <code>First Changed Commit</code> hash, because GitHub support asks for it.</p>\n</details>\n<pre><code class=\"language-console\">$ git log --oneline\n35cccfa Stop tracking .env\n648e253 Update app\n1de2032 Add app\n</code></pre>\n<p>Four commits became three. <code>Add settings</code> only added the two files, so once they were gone it had nothing left and Git dropped it. Every hash is new, even the first one, <code>Add app</code>, which never touched the secret. A commit's hash covers its parent, and the first commit changed first.</p>\n<p>Run the same search again, then measure:</p>\n<pre><code class=\"language-console\">$ git log --oneline -S\"FAKE-not-a-real-key\"\n$ git count-objects -vH\ncount: 0\nsize: 0 bytes\nin-pack: 9\npacks: 1\nsize-pack: 1.98 KiB\n</code></pre>\n<p>The search prints nothing. The repository went from 2.86 MiB to 1.98 KiB. Your sizes will differ if you drop a different file, and a small secret on its own would not move the number much.</p>\n<h2 id=\"replace-the-history-on-github\">Replace the history on GitHub<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/you-committed-a-secret-what-to-do-in-order/#replace-the-history-on-github\" aria-label=\"Link to this section\">#</a></h2>\n<p>The server still has the old commits. Replace them from inside <code>fresh</code>, the clone we just rewrote (<code>alice</code> is untouched, and pushing from it would do nothing useful):</p>\n<pre><code class=\"language-console\">$ git push --force --mirror origin\n + fbe993a...35cccfa main -&gt; main (forced update)\n</code></pre>\n<p><code>--force</code> lets the push overwrite a branch that is not a descendant of the old one. <code>--mirror</code> makes the server match this clone for every branch and tag, so no old tag keeps the key alive. GitHub's page adds that this command cannot update the <code>refs/pull/</code> references, which GitHub marks read-only, and that you may need to turn off branch protection for a moment if a protected branch rejects the push.</p>\n<p>Notice that <code>origin</code> is still configured here. That is <code>--sensitive-data-removal</code> at work. Without it, filter-repo removes the <code>origin</code> remote after a rewrite, and you have to add it back before you can push.</p>\n<h2 id=\"what-happens-to-everyone-else\">What happens to everyone else<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/you-committed-a-secret-what-to-do-in-order/#what-happens-to-everyone-else\" aria-label=\"Link to this section\">#</a></h2>\n<p>Take <code>bob</code>, who cloned before the rewrite. We made his clone in the build box, with <code>git clone hub.git bob</code>. <code>bob</code> sits next to <code>fresh</code>, so <code>../bob</code> reaches it. He runs the habit of a lifetime:</p>\n<pre><code class=\"language-console\">$ cd ../bob\n$ git pull\n + fbe993a...35cccfa main       -&gt; origin/main  (forced update)\nfatal: refusing to merge unrelated histories\n</code></pre>\n<p>Git is right. The old history and the new one share no commit. That refusal is protection. Bob can push past it, and this is how the secret comes back:</p>\n<pre><code class=\"language-bash\">git pull --no-rebase --allow-unrelated-histories --no-edit\ngit log --oneline -S\"FAKE-not-a-real-key\"\n</code></pre>\n<p>The search lists the old commits again. One merge commit reintroduced everything we purged. GitHub's page warns about the same thing and says branches built on the old history should be rebased onto the new one, never merged.</p>\n<p>The simple fix is to discard the old clone and clone again:</p>\n<pre><code class=\"language-bash\">cd ..\ngit clone hub.git bob-again\n</code></pre>\n<pre><code class=\"language-console\">$ git log --oneline -S\"FAKE-not-a-real-key\"\n$\n</code></pre>\n<p>If Bob has unpushed work, he copies those files out first, re-clones, and puts them back. The filter-repo manual describes a way to clean an existing clone, but a fresh clone has fewer ways to go wrong.</p>\n<details>\n<summary>What the server keeps after a force-push</summary>\n<p>We tested our own <code>hub.git</code>, which is a plain bare repository. After the force-push, the old commit was still in it:</p>\n<pre><code class=\"language-console\">$ cd hub.git\n$ git cat-file -t fbe993a\ncommit\n</code></pre>\n<p>(<code>fbe993a</code> is our old tip, the one the force-push replaced. In your push output it is the first hash in the <code>+ old...new</code> line.)</p>\n<p>The server had 17 loose objects and 2.86 MiB, the old ones included. They are unreachable from any branch now, but they are still on disk. They go away when the server runs garbage collection, and by default <code>git gc</code> only deletes unreachable objects that are older than two weeks.</p>\n<p>GitHub's page lists more, and we cannot test any of it from here:</p>\n<ul>\n<li><strong>Cached views.</strong> The data may stay reachable by its SHA-1 hash in cached views on GitHub. GitHub Support can remove them.</li>\n<li><strong>Forks.</strong> If the commit exists in any fork, it stays accessible there. You have to ask the fork owners to delete the data or the fork, and GitHub says it cannot give you their contact details.</li>\n<li><strong>Pull requests.</strong> Rewriting history can break diff views and invalidate comments on open pull requests. GitHub recommends merging or closing them first.</li>\n<li><strong>Support.</strong> You ask through the support portal and give the repository, the number of affected pull requests, and the <code>First Changed Commit</code> hash. The page says support only helps with removal when rotating the credential cannot take care of the risk. That is one more reason step 1 comes first.</li>\n</ul>\n<p>That page is the source for all four, read in October 2026. Things like this change, so read the current version before you send anyone to it.</p>\n</details>\n<h2 id=\"where-git-maintenance-fits\">Where <code>git maintenance</code> fits<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/you-committed-a-secret-what-to-do-in-order/#where-git-maintenance-fits\" aria-label=\"Link to this section\">#</a></h2>\n<p><code>git maintenance</code> runs housekeeping tasks that Git skips during everyday commands, such as <code>gc</code>, <code>commit-graph</code> and <code>prefetch</code>. <code>git maintenance start</code> schedules them hourly in the background; on Windows it uses the Task Scheduler (<code>schtasks</code>).</p>\n<p>It does not remove a secret quickly. On a copy of our <code>hub.git</code>, we ran:</p>\n<pre><code class=\"language-bash\">git maintenance run --task=gc\ngit cat-file -t fbe993a\n</code></pre>\n<p>The commit was still there. It was packed, but not pruned, because it was younger than two weeks. Only this removed it:</p>\n<pre><code class=\"language-console\">$ git reflog expire --expire=now --all\n$ git gc --prune=now\n$ git cat-file -t fbe993a\nfatal: Not a valid object name fbe993a\n</code></pre>\n<p><code>--prune=now</code> deletes unreachable objects of any age. Git's manual warns it can corrupt a repository if another process is writing to it at the same moment, so it belongs on a repository nobody is using. On GitHub you cannot run it. That is the job of the support request above.</p>\n<h2 id=\"try-it\">Try it<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/you-committed-a-secret-what-to-do-in-order/#try-it\" aria-label=\"Link to this section\">#</a></h2>\n<p>This is the same loop in six commands. The practice repository is the one we just built: the key is in an old commit, <code>origin</code> is set, and the key has already been rotated.</p>\n<figure class=\"ob-demo ob-portable-demo\" data-demo=\"git-practice\"><figcaption class=\"ob-code-title\">Practice terminal · remove a leaked secret from history</figcaption><p>Type these commands in order, in a fresh clone of the leaked repository:</p>\n<ol>\n<li>Find the commits that held the key. Try <code>git log --oneline -S\"FAKE-not-a-real-key\"</code>. It lists Add settings and Stop tracking .env.</li>\n<li>Measure before you change anything. Try <code>git count-objects -vH</code>. It reports 13 objects and 2.86 MiB.</li>\n<li>Rewrite history without the two files. Try <code>git filter-repo --sensitive-data-removal --invert-paths --path .env --path db-backup.dump</code>. Every commit gets a new hash.</li>\n<li>Run the search from step 1 again. It prints nothing.</li>\n<li>Measure again. Try <code>git count-objects -vH</code>. It reports 9 objects and 1.98 KiB.</li>\n<li>Replace the server's history. Try <code>git push --force --mirror origin</code>. A normal push is rejected.</li>\n</ol>\n<p class=\"ob-portable-note\">Try the interactive Git practice terminal on <a href=\"https://mnixblog.netlify.app/you-committed-a-secret-what-to-do-in-order/\">the original article</a>.</p></figure>\n<p>Two things to try in the terminal: leave out <code>--invert-paths</code> in step 3 and read the message, and run a plain <code>git push origin main</code> in step 6.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>You pushed a key in <code>.env</code>, then deleted the file in your next commit and pushed again. Is the key still in the repository?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Yes, the older commit still holds it, and anyone can read it with <code>git show &lt;commit&gt;:.env</code></li>\n<li>No, the newest commit no longer contains the file</li>\n<li>Only until the next <code>git gc</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. Yes, the older commit still holds it, and anyone can read it with <code>git show &lt;commit&gt;:.env</code></p>\n<p>The newest commit is clean, but history keeps every older commit. <code>git log -S\"&lt;text&gt;\"</code> lists the commits where the text was added and removed.</p>\n</div>\n</div>\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>On a fresh clone, you run <code>git filter-repo --path .env</code> with no <code>--invert-paths</code>. What happens to the history?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Only the files you named are kept, so the commits that never touched <code>.env</code> are gone</li>\n<li>Only <code>.env</code> is removed from history</li>\n<li>Nothing, because filter-repo refuses to run without <code>--invert-paths</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. Only the files you named are kept, so the commits that never touched <code>.env</code> are gone</p>\n<p>In our test, the repository kept two commits, the ones that touched <code>.env</code>, and the working tree was empty. <code>--invert-paths</code> flips the meaning to \"everything except these\".</p>\n</div>\n</div>\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>After you force-push a rewritten history, a teammate who cloned earlier runs <code>git pull</code> and then <code>git pull --allow-unrelated-histories</code>. What does the second command do to the secret?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>It merges the old history back in, so the secret is reachable again</li>\n<li>It deletes the old commits from their clone</li>\n<li>It does nothing, because the secret was already removed on the server</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. It merges the old history back in, so the secret is reachable again</p>\n<p>In our test the search listed the old commits again after that merge. A teammate with an old clone should re-clone, or rebase onto the new history, and never merge.</p>\n</div>\n</div>\n</section>\n<p><strong>Challenge: remove the key and keep the dump</strong></p>\n<p>Sometimes the file has to stay in history and only the text has to go. <code>git filter-repo</code> can replace text with <code>--replace-text</code>, which takes a file of replacements.</p>\n<p>Build the leaked repository again in a new, empty folder (the box above does the whole thing). Make a fresh clone of its <code>hub.git</code>. Outside the clone, create <code>expressions.txt</code> with one line:</p>\n<pre><code class=\"language-text\">FAKE-not-a-real-key-123==&gt;REMOVED\n</code></pre>\n<p>Then run <code>git filter-repo --sensitive-data-removal</code> with <code>--replace-text</code> pointing at that file.</p>\n<p><strong>Expected result:</strong> all three of these hold in the clone.</p>\n<pre><code class=\"language-console\">$ git log --oneline -S\"FAKE-not-a-real-key\"\n$ git log --oneline\n(four commits, with new hashes)\n$ git show HEAD~2:.env\nAPI_KEY=REMOVED\n</code></pre>\n<details>\n<summary>Answer</summary>\n<pre><code class=\"language-bash\">git clone hub.git fresh\ncd fresh\ngit filter-repo --sensitive-data-removal --replace-text ../expressions.txt\n</code></pre>\n<p>The text is replaced inside every old copy of <code>.env</code>, and all four commits are still there, with new hashes. The dump was never touched, so <code>git count-objects -vH</code> still shows about 2.86 MiB in <code>size-pack</code>. Replacing text hides the key. It does not shrink anything.</p>\n</details>\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/you-committed-a-secret-what-to-do-in-order/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<ul>\n<li>Revoke or rotate the key first. A pushed key is compromised even after history is rewritten.</li>\n<li>Deleting the file in a new commit leaves the key in the older commit. <code>git log -S\"&lt;text&gt;\"</code> finds every commit that added or removed it.</li>\n<li><code>git filter-repo</code> is a separate install that needs Python. It runs only in a fresh clone, and <code>--sensitive-data-removal --invert-paths --path &lt;file&gt;</code> removes a file from every commit.</li>\n<li><code>git push --force --mirror origin</code> replaces every branch and tag. Teammates re-clone, or rebase onto the new history. A merge brings the secret back.</li>\n<li>The host may keep cached views and forks, and a plain bare repository keeps unreachable objects until a garbage collection prunes them. <code>git maintenance</code> alone does not do that within two weeks.</li>\n</ul>\n<p>Rotating the key is what protects you. Rewriting history only removes the copies that anyone could still read.</p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/you-committed-a-secret-what-to-do-in-order/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "You Committed a Secret: What to Do, in Order.\nCanonical: https://mnixblog.netlify.app/you-committed-a-secret-what-to-do-in-order/\nCancel the Key First, Then Clean Up the History, and Know What Stays on the Server\nYou push, and a minute later you see it. An API key sits in .env, in a commit that is already on GitHub.\nThe quick fix is to delete the file and commit again. That feels like it should work. It does not, and we will see exactly why in a scratch repository.\nThink of history as a chain where every link carries a fingerprint of the link before it. You cannot pull one link out. You have to rebuild every link that came after it.\nThere is an order to this, and the first step is not a Git command:\nRevoke or rotate the key.\nRewrite history so the key is in no commit.\nForce-push the new history.\nGet your teammates onto it.\nAsk the host about the copies it keeps.\nWe will do steps 2 and 3 on a real repository and check the result with two commands. The end state looks like this:\n$ git log --oneline -S\"FAKE-not-a-real-key\"\n$ git count-objects -vH\nsize-pack: 1.98 KiB\nNote:\nEvery command and output here comes from Git 2.45.2 on Windows 11, run in Git Bash, with git-filter-repo 2.47.0. The secret is fake on purpose: FAKE-not-a-real-key-123. Never practice with a real one. The commit hashes you get will differ from ours every time, because a hash covers the commit time. Never type one of ours. Copy yours from your own git log.\nStep 1: make the key worthless\nDo this before anything else, and before you touch Git.\nA key that was pushed has been seen. It may be cached somewhere, copied by a build bot, or scraped by something that watches public pushes. Rewriting history afterward does not un-see it. GitHub's own page on this says to revoke or rotate the secret first, and that doing so may be enough on its own.\nSo open the service that issued the key, create a new one, and delete the old one. Then put the new one somewhere that Git never sees.\nEverything after this is cleanup. The key is already dead, so we can take our time.\nBuild the leak yourself\nThis sets up the same mess on your machine. The folder hub.git plays the part of GitHub. Paste it into Git Bash, in an empty folder.\n<details>\n<summary>Build the leaked repository (Git Bash)</summary>\nmkdir secret-lab && cd secret-lab\ngit init --bare -b main hub.git\ngit clone hub.git alice\ncd alice\necho 'console.log(\"hello\");' > app.js\ngit add app.js\ngit commit -m \"Add app\"\necho 'API_KEY=FAKE-not-a-real-key-123' > .env\nhead -c 3000000 /dev/urandom > db-backup.dump\ngit add .\ngit commit -m \"Add settings\"\necho 'console.log(\"v2\");' >> app.js\ngit commit -am \"Update app\"\ngit rm --cached .env db-backup.dump\nprintf '.env\\ndb-backup.dump\\n' > .gitignore\ngit add .gitignore\ngit commit -m \"Stop tracking .env\"\ngit push origin main\ncd ..\ngit clone hub.git bob\ncd alice\nThe last three lines make bob, a teammate's clone that we use near the end. It has to exist now, before the history changes. Git says You appear to have cloned an empty repository. after the first clone. That is fine, hub.git is empty at that point.\ngit add . is the slip. It added the key file and a 3 MB database dump, which stands in for the thing you did not mean to commit. The last commit is the quick fix: it stops tracking both files and ignores them.\n</details>\nWhy deleting the file is not enough\nAsk Git which commits ever changed that text:\n$ git log --oneline -S\"FAKE-not-a-real-key\"\nfbe993a Stop tracking .env\n65bf43d Add settings\n-S lists the commits where the number of times the text appears went up or down. Add settings put it in. Stop tracking .env took it out. The key is still inside the older commit, and anyone can read it:\n$ git show 65bf43d:.env\nAPI_KEY=FAKE-not-a-real-key-123\nReplace 65bf43d with the hash of your own Add settings commit. To find it, run the git log --oneline -S\"FAKE-not-a-real-key\" command above and copy the hash from the lower line, the older commit that added the key. For your own secret, put a few characters of the real one inside the quotes. The latest commit is clean, and the history is not.\n<details>\n<summary>Install git-filter-repo (once)</summary>\ngit filter-repo is not part of Git. It is a separate Python script that Git can call as if it were a command, and it rewrites every commit in one pass. The older git filter-branch does a similar job, and we will not use it here.\nThe project's own install notes (read October 2026) assume you already have Git and Python. Their routes are a package manager (they name Scoop for Windows), PyPI through pipx, or downloading the single script git-filter-repo and putting it on your PATH.\nWhat we tested: in a Python virtual environment (a private folder of Python packages), python -m pip install git-filter-repo put a git-filter-repo.exe in the environment's Scripts folder. Pip does not put that folder on your PATH. Before we added it, Git said:\n$ git filter-repo --version\ngit: 'filter-repo' is not a git command. See 'git --help'.\nIn Git Bash, from the folder that holds the environment (here named venv), either of these fixes it for that window:\nexport PATH=\"$PWD/venv/Scripts:$PATH\"\nsource venv/Scripts/activate\nThe second one turns the whole environment on. The first one only adds the folder. Open a new window and you have to do it again.\nThe project's INSTALL file gives two other commands, pipx install git-filter-repo and, for a package manager such as Scoop, PACKAGE_TOOL install git-filter-repo, which for Scoop reads scoop install git-filter-repo. We did not run either of those, or the manual copy, so check the INSTALL file if one fails.\nTo check the install, ask for the version:\n$ git filter-repo --version\na40bce548d2c\nThat is a build hash, not a version number. pip show git-filter-repo prints Version: 2.47.0. The --sensitive-data-removal option we use needs 2.47 or newer, according to GitHub's page.\n</details>\nRemove the key from every commit\nfilter-repo refuses to run in a repository you have been working in. Try it in alice:\n$ cd ../alice\n$ git filter-repo --sensitive-data-removal --invert-paths --path .env --path db-backup.dump\nAborting: Refusing to destructively overwrite repo history since\nthis does not look like a fresh clone.\n(expected at most one entry in the reflog for HEAD)\nGood. The rewrite throws the old history away, so it wants a copy where nothing else is at stake. Make a fresh clone and measure it first:\n$ cd ..\n$ git clone hub.git fresh\n$ cd fresh\n$ git count-objects -vH\ncount: 13\nsize: 2.86 MiB\nin-pack: 0\npacks: 0\nsize-pack: 0 bytes\nprune-packable: 0\ngarbage: 0\nsize-garbage: 0 bytes\nThirteen loose objects, 2.86 MiB. Almost all of it is the dump. (A clone from GitHub arrives as one pack, so there you would read size-pack instead of size.)\nNow the rewrite:\ngit filter-repo --sensitive-data-removal --invert-paths --path .env --path db-backup.dump\nHere is what each part does:\n--path .env --path db-backup.dump names the two files.\n--invert-paths means everything except those paths. Leave it out and --path does the opposite: it keeps only those files and drops the rest. We ran that by mistake on purpose, and the repository kept only the commits that touched .env.\n--sensitive-data-removal fetches every ref from origin first, so no branch is left holding the old history, and it prints the follow-up steps.\n<details>\n<summary>The output, trimmed</summary>\nNOTICE: Fetching all refs from origin to make sure we rewrite\nall history that may reference the sensitive data, via\ngit fetch -q --prune --update-head-ok --refmap \"\" origin +refs/*:refs/*\nParsed 4 commits\nHEAD is now at 35cccfa Stop tracking .env\nNew history written in 1.42 seconds; now repacking/cleaning...\nYou rewrote 4 (of 4) commits.\nNOTE: First Changed Commit(s) is/are:\n25b989967cec4c5eb38a6818ae42853374f55c8a\nNOTE: LFS object orphaning not checked (LFS not in use)\nRepacking your repo and cleaning out old unneeded objects\nCompletely finished after 2.70 seconds.\nNEXT STEPS FOR YOUR SENSITIVE DATA REMOVAL:\n* ...\nThe NEXT STEPS list that follows is long. It covers what we do next. Keep the First Changed Commit hash, because GitHub support asks for it.\n</details>\n$ git log --oneline\n35cccfa Stop tracking .env\n648e253 Update app\n1de2032 Add app\nFour commits became three. Add settings only added the two files, so once they were gone it had nothing left and Git dropped it. Every hash is new, even the first one, Add app, which never touched the secret. A commit's hash covers its parent, and the first commit changed first.\nRun the same search again, then measure:\n$ git log --oneline -S\"FAKE-not-a-real-key\"\n$ git count-objects -vH\ncount: 0\nsize: 0 bytes\nin-pack: 9\npacks: 1\nsize-pack: 1.98 KiB\nThe search prints nothing. The repository went from 2.86 MiB to 1.98 KiB. Your sizes will differ if you drop a different file, and a small secret on its own would not move the number much.\nReplace the history on GitHub\nThe server still has the old commits. Replace them from inside fresh, the clone we just rewrote (alice is untouched, and pushing from it would do nothing useful):\n$ git push --force --mirror origin\n+ fbe993a...35cccfa main -> main (forced update)\n--force lets the push overwrite a branch that is not a descendant of the old one. --mirror makes the server match this clone for every branch and tag, so no old tag keeps the key alive. GitHub's page adds that this command cannot update the refs/pull/ references, which GitHub marks read-only, and that you may need to turn off branch protection for a moment if a protected branch rejects the push.\nNotice that origin is still configured here. That is --sensitive-data-removal at work. Without it, filter-repo removes the origin remote after a rewrite, and you have to add it back before you can push.\nWhat happens to everyone else\nTake bob, who cloned before the rewrite. We made his clone in the build box, with git clone hub.git bob. bob sits next to fresh, so ../bob reaches it. He runs the habit of a lifetime:\n$ cd ../bob\n$ git pull\n+ fbe993a...35cccfa main       -> origin/main  (forced update)\nfatal: refusing to merge unrelated histories\nGit is right. The old history and the new one share no commit. That refusal is protection. Bob can push past it, and this is how the secret comes back:\ngit pull --no-rebase --allow-unrelated-histories --no-edit\ngit log --oneline -S\"FAKE-not-a-real-key\"\nThe search lists the old commits again. One merge commit reintroduced everything we purged. GitHub's page warns about the same thing and says branches built on the old history should be rebased onto the new one, never merged.\nThe simple fix is to discard the old clone and clone again:\ncd ..\ngit clone hub.git bob-again\n$ git log --oneline -S\"FAKE-not-a-real-key\"\n$\nIf Bob has unpushed work, he copies those files out first, re-clones, and puts them back. The filter-repo manual describes a way to clean an existing clone, but a fresh clone has fewer ways to go wrong.\n<details>\n<summary>What the server keeps after a force-push</summary>\nWe tested our own hub.git, which is a plain bare repository. After the force-push, the old commit was still in it:\n$ cd hub.git\n$ git cat-file -t fbe993a\ncommit\n(fbe993a is our old tip, the one the force-push replaced. In your push output it is the first hash in the + old...new line.)\nThe server had 17 loose objects and 2.86 MiB, the old ones included. They are unreachable from any branch now, but they are still on disk. They go away when the server runs garbage collection, and by default git gc only deletes unreachable objects that are older than two weeks.\nGitHub's page lists more, and we cannot test any of it from here:\nCached views. The data may stay reachable by its SHA-1 hash in cached views on GitHub. GitHub Support can remove them.\nForks. If the commit exists in any fork, it stays accessible there. You have to ask the fork owners to delete the data or the fork, and GitHub says it cannot give you their contact details.\nPull requests. Rewriting history can break diff views and invalidate comments on open pull requests. GitHub recommends merging or closing them first.\nSupport. You ask through the support portal and give the repository, the number of affected pull requests, and the First Changed Commit hash. The page says support only helps with removal when rotating the credential cannot take care of the risk. That is one more reason step 1 comes first.\nThat page is the source for all four, read in October 2026. Things like this change, so read the current version before you send anyone to it.\n</details>\nWhere git maintenance fits\ngit maintenance runs housekeeping tasks that Git skips during everyday commands, such as gc, commit-graph and prefetch. git maintenance start schedules them hourly in the background; on Windows it uses the Task Scheduler (schtasks).\nIt does not remove a secret quickly. On a copy of our hub.git, we ran:\ngit maintenance run --task=gc\ngit cat-file -t fbe993a\nThe commit was still there. It was packed, but not pruned, because it was younger than two weeks. Only this removed it:\n$ git reflog expire --expire=now --all\n$ git gc --prune=now\n$ git cat-file -t fbe993a\nfatal: Not a valid object name fbe993a\n--prune=now deletes unreachable objects of any age. Git's manual warns it can corrupt a repository if another process is writing to it at the same moment, so it belongs on a repository nobody is using. On GitHub you cannot run it. That is the job of the support request above.\nTry it\nThis is the same loop in six commands. The practice repository is the one we just built: the key is in an old commit, origin is set, and the key has already been rotated.\nPractice terminal · remove a leaked secret from history\nType these commands in order, in a fresh clone of the leaked repository:\nFind the commits that held the key. Try git log --oneline -S\"FAKE-not-a-real-key\". It lists Add settings and Stop tracking .env.\nMeasure before you change anything. Try git count-objects -vH. It reports 13 objects and 2.86 MiB.\nRewrite history without the two files. Try git filter-repo --sensitive-data-removal --invert-paths --path .env --path db-backup.dump. Every commit gets a new hash.\nRun the search from step 1 again. It prints nothing.\nMeasure again. Try git count-objects -vH. It reports 9 objects and 1.98 KiB.\nReplace the server's history. Try git push --force --mirror origin. A normal push is rejected.\nTry the interactive Git practice terminal on the canonical article (https://mnixblog.netlify.app/you-committed-a-secret-what-to-do-in-order/).\nTwo things to try in the terminal: leave out --invert-paths in step 3 and read the message, and run a plain git push origin main in step 6.\nQuestion: You pushed a key in .env, then deleted the file in your next commit and pushed again. Is the key still in the repository?\nYes, the older commit still holds it, and anyone can read it with git show <commit>:.env\nNo, the newest commit no longer contains the file\nOnly until the next git gc\nAnswer: 1. Yes, the older commit still holds it, and anyone can read it with git show <commit>:.env\nExplanation: The newest commit is clean, but history keeps every older commit. git log -S\"<text>\" lists the commits where the text was added and removed.\nQuestion: On a fresh clone, you run git filter-repo --path .env with no --invert-paths. What happens to the history?\nOnly the files you named are kept, so the commits that never touched .env are gone\nOnly .env is removed from history\nNothing, because filter-repo refuses to run without --invert-paths\nAnswer: 1. Only the files you named are kept, so the commits that never touched .env are gone\nExplanation: In our test, the repository kept two commits, the ones that touched .env, and the working tree was empty. --invert-paths flips the meaning to \"everything except these\".\nQuestion: After you force-push a rewritten history, a teammate who cloned earlier runs git pull and then git pull --allow-unrelated-histories. What does the second command do to the secret?\nIt merges the old history back in, so the secret is reachable again\nIt deletes the old commits from their clone\nIt does nothing, because the secret was already removed on the server\nAnswer: 1. It merges the old history back in, so the secret is reachable again\nExplanation: In our test the search listed the old commits again after that merge. A teammate with an old clone should re-clone, or rebase onto the new history, and never merge.\nChallenge: remove the key and keep the dump\nSometimes the file has to stay in history and only the text has to go. git filter-repo can replace text with --replace-text, which takes a file of replacements.\nBuild the leaked repository again in a new, empty folder (the box above does the whole thing). Make a fresh clone of its hub.git. Outside the clone, create expressions.txt with one line:\nFAKE-not-a-real-key-123==>REMOVED\nThen run git filter-repo --sensitive-data-removal with --replace-text pointing at that file.\nExpected result: all three of these hold in the clone.\n$ git log --oneline -S\"FAKE-not-a-real-key\"\n$ git log --oneline\n(four commits, with new hashes)\n$ git show HEAD~2:.env\nAPI_KEY=REMOVED\n<details>\n<summary>Answer</summary>\ngit clone hub.git fresh\ncd fresh\ngit filter-repo --sensitive-data-removal --replace-text ../expressions.txt\nThe text is replaced inside every old copy of .env, and all four commits are still there, with new hashes. The dump was never touched, so git count-objects -vH still shows about 2.86 MiB in size-pack. Replacing text hides the key. It does not shrink anything.\n</details>\nWhat we learned\nRevoke or rotate the key first. A pushed key is compromised even after history is rewritten.\nDeleting the file in a new commit leaves the key in the older commit. git log -S\"<text>\" finds every commit that added or removed it.\ngit filter-repo is a separate install that needs Python. It runs only in a fresh clone, and --sensitive-data-removal --invert-paths --path <file> removes a file from every commit.\ngit push --force --mirror origin replaces every branch and tag. Teammates re-clone, or rebase onto the new history. A merge brings the secret back.\nThe host may keep cached views and forks, and a plain bare repository keeps unreachable objects until a garbage collection prunes them. git maintenance alone does not do that within two weeks.\nRotating the key is what protects you. Rewriting history only removes the copies that anyone could still read.\nPortable export note: 15 raw HTML fragment(s) are shown as literal source because this Markdown export does not execute or preserve raw HTML. View the canonical article (https://mnixblog.netlify.app/you-committed-a-secret-what-to-do-in-order/).\nPortable fallback: The authored description of the demo is preserved; the live demonstration remains on the canonical article. Review: Review the authored description of the demo and keep the canonical link for the live version.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/you-committed-a-secret-what-to-do-in-order/#ob-discussion)",
      "date_published": "2026-10-27T00:07:00Z",
      "id": "https://mnixblog.netlify.app/you-committed-a-secret-what-to-do-in-order/",
      "image": "https://mnixblog.netlify.app/images/you-committed-a-secret-what-to-do-in-order/cover.jpg",
      "summary": "A key is already pushed in a commit. Revoke it first, then remove it from history with git filter-repo and get your team onto the new history.",
      "tags": [
        "Git",
        "GitHub",
        "Command Line"
      ],
      "title": "You Committed a Secret: What to Do, in Order.",
      "url": "https://mnixblog.netlify.app/you-committed-a-secret-what-to-do-in-order/"
    },
    {
      "content_html": "<p>You run <code>git pull</code> and Git answers like this:</p>\n<pre><code class=\"language-console\">$ git pull\nfatal: Need to specify how to reconcile divergent branches.\n</code></pre>\n<p>It also prints a dozen lines of hints. They tell you to set a config value, and they do not say where that value is kept or whose settings you are about to change.</p>\n<p>In this article, we are going to read Git's settings, work out which file each one comes from, and set a short list of defaults, each one tied to a message like the one above. By the end, one command will tell you where every setting lives:</p>\n<pre><code class=\"language-bash\">git config --list --show-origin\n</code></pre>\n<p>We will also write a few aliases and decide which ones are safe to keep.</p>\n<aside class=\"ob-callout ob-callout-note\" role=\"note\"><p class=\"ob-callout-title\"><strong>Note</strong></p>\n<p>Everything here ran on Git 2.45.2 on Windows 11, in Git Bash. Where a setting needs a newer Git, the text says so. In the output I replaced long scratch folder names with <code>...</code>.</p>\n</aside>\n<h2 id=\"where-a-setting-lives\">Where a setting lives<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/make-git-yours-config-and-aliases/#where-a-setting-lives\" aria-label=\"Link to this section\">#</a></h2>\n<p>Git reads settings from several files and layers them. The ones you will touch are these:</p>\n<table><thead><tr><th>Scope</th><th>Flag</th><th>File</th><th>Applies to</th></tr></thead><tbody>\n<tr><td>system</td><td><code>--system</code></td><td><code>etc/gitconfig</code> inside the Git install</td><td>every user on the machine</td></tr>\n<tr><td>global</td><td><code>--global</code></td><td><code>.gitconfig</code> in your home folder</td><td>every repository you use</td></tr>\n<tr><td>local</td><td><code>--local</code> (the default when you write)</td><td><code>.git/config</code> in the repository</td><td>that repository only</td></tr>\n</tbody></table>\n<p>When the same setting appears in more than one file, the narrower scope wins.</p>\n<p>To see that without touching your real settings, point Git at a throwaway global file. In Git Bash:</p>\n<pre><code class=\"language-bash\">export GIT_CONFIG_GLOBAL=\"$PWD/.gitconfig\"\nexport GIT_CONFIG_NOSYSTEM=1\n</code></pre>\n<p><code>GIT_CONFIG_GLOBAL</code> replaces your global file for this terminal window only. <code>GIT_CONFIG_NOSYSTEM=1</code> ignores the system file. Close the window and your own settings are back.</p>\n<p>Now set an email twice, once globally and once in a repository:</p>\n<pre><code class=\"language-bash\">git config --global user.name \"Ada Example\"\ngit config --global user.email \"ada@home.example\"\ngit init -q demo &amp;&amp; cd demo\ngit config user.email \"ada@work.example\"\n</code></pre>\n<p>Ask Git for everything it knows, and where it learned it. I left out the <code>core.*</code> lines that <code>git init</code> writes into <code>.git/config</code>:</p>\n<pre><code class=\"language-console\">$ git config --list --show-origin\nfile:C:/.../.gitconfig\tuser.name=Ada Example\nfile:C:/.../.gitconfig\tuser.email=ada@home.example\nfile:.git/config\tuser.email=ada@work.example\n</code></pre>\n<p>The left column is the file and the right column is the setting. The email appears twice, once from each file.</p>\n<p>Ask for the one that counts:</p>\n<pre><code class=\"language-console\">$ git config user.email\nada@work.example\n\n$ git config --show-scope --show-origin user.email\nlocal\tfile:.git/config\tada@work.example\n</code></pre>\n<p>The local file won. <code>--show-scope</code> adds the scope name, so you do not have to guess from the path.</p>\n<p>If a setting behaves strangely, this is the first command to run. A value you forgot you set three years ago shows up here with its file name next to it.</p>\n<p>Two more commands finish the picture. <code>git config --global --edit</code> opens the global file in your editor, and <code>git config --global --unset user.email</code> removes one setting.</p>\n<h2 id=\"six-defaults-that-each-stop-one-annoyance\">Six defaults that each stop one annoyance<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/make-git-yours-config-and-aliases/#six-defaults-that-each-stop-one-annoyance\" aria-label=\"Link to this section\">#</a></h2>\n<p>Every default below has a message it answers. We ran each one against a pretend remote, <code>origin.git</code>, with two clones standing in for two people.</p>\n<h3 id=\"pull-ff-stop-guessing-on-a-divergent-pull\">pull.ff: stop guessing on a divergent pull<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/make-git-yours-config-and-aliases/#pull-ff-stop-guessing-on-a-divergent-pull\" aria-label=\"Link to this section\">#</a></h3>\n<p>Alice pushes a commit. Bob commits locally without pulling first, then runs <code>git pull</code>. Here is Bob's screen, with the hints cut out:</p>\n<pre><code class=\"language-console\">$ git pull\nFrom .../origin\n   9afce9f..d953b3d  main       -&gt; origin/main\n...\nfatal: Need to specify how to reconcile divergent branches.\n</code></pre>\n<p>Git will not pick between a merge and a rebase for you. The hint lists three ways to answer. Two of them are useful defaults:</p>\n<pre><code class=\"language-console\">$ git -c pull.ff=only pull\nfatal: Not possible to fast-forward, aborting.\n\n$ git -c pull.rebase=true pull\nSuccessfully rebased and updated refs/heads/main.\n</code></pre>\n<p><code>-c name=value</code> applies a setting to one command and nothing else, which is a safe way to try a default before keeping it.</p>\n<p><code>pull.ff only</code> pulls only when the pull is a straight line, and stops otherwise. You then choose to merge or rebase yourself. <code>pull.rebase true</code> always replays your local commits on top of what arrived, which gave Bob this history:</p>\n<pre><code class=\"language-text\">* 751c288 bob change\n* d953b3d alice change\n* 9afce9f start\n</code></pre>\n<p>Either one removes the surprise. Pick one, and set it with <code>--global</code>:</p>\n<pre><code class=\"language-bash\">git config --global pull.ff only\n</code></pre>\n<h3 id=\"push-autosetupremote-stop-typing-set-upstream\">push.autoSetupRemote: stop typing --set-upstream<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/make-git-yours-config-and-aliases/#push-autosetupremote-stop-typing-set-upstream\" aria-label=\"Link to this section\">#</a></h3>\n<p>A new branch has no upstream yet, so the first plain <code>git push</code> fails:</p>\n<pre><code class=\"language-console\">$ git push\nfatal: The current branch feature/x has no upstream branch.\nTo push the current branch and set the remote as upstream, use\n\n    git push --set-upstream origin feature/x\n</code></pre>\n<p>Turn on the setting, and the same command works:</p>\n<pre><code class=\"language-console\">$ git config --global push.autoSetupRemote true\n$ git push\nbranch 'feature/x' set up to track 'origin/feature/x'.\n * [new branch]      feature/x -&gt; feature/x\n</code></pre>\n<p>The branch is now tracking <code>origin/feature/x</code>, so <code>git status -sb</code> starts with <code>## feature/x...origin/feature/x</code>. This setting needs Git 2.37 or newer. It assumes the branch keeps the same name on the remote, which is the usual case on GitHub.</p>\n<h3 id=\"fetch-prune-stop-seeing-branches-that-are-gone\">fetch.prune: stop seeing branches that are gone<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/make-git-yours-config-and-aliases/#fetch-prune-stop-seeing-branches-that-are-gone\" aria-label=\"Link to this section\">#</a></h3>\n<p>When a branch is deleted on GitHub, your copy of its pointer, <code>origin/feature/x</code>, stays behind. Alice's list after Bob deleted <code>feature/x</code> on the remote:</p>\n<pre><code class=\"language-console\">$ git fetch\n$ git branch -r\n  origin/feature/x\n  origin/main\n</code></pre>\n<p>A plain fetch does not remove it. With <code>--prune</code>, it does:</p>\n<pre><code class=\"language-console\">$ git fetch --prune\n - [deleted]         (none)     -&gt; origin/feature/x\n$ git branch -r\n  origin/main\n</code></pre>\n<p><code>fetch.prune true</code> makes every fetch behave that way. It only removes those remote pointers. A local branch of the same name stays, and Git marks it:</p>\n<pre><code class=\"language-console\">$ git branch -vv\n* main d953b3d [origin/main] alice change\n  y    ea11253 [origin/y: gone] y\n</code></pre>\n<p><code>gone</code> is your cue that the branch was merged or deleted upstream.</p>\n<h3 id=\"rebase-autosquash-fixups-land-where-they-belong\">rebase.autoSquash: fixups land where they belong<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/make-git-yours-config-and-aliases/#rebase-autosquash-fixups-land-where-they-belong\" aria-label=\"Link to this section\">#</a></h3>\n<p><code>git commit --fixup &lt;commit&gt;</code> creates a commit titled <code>fixup! &lt;that commit's title&gt;</code>. Without help, an interactive rebase leaves it at the end:</p>\n<pre><code class=\"language-console\">$ git rebase -i main\n$ git log --oneline\n3bb5134 fixup! Add b\n8ae88bd Add c\nd42c68b Add b\nc5773f4 Add a\n</code></pre>\n<p>With <code>rebase.autoSquash</code> on, the same command folds it into its target:</p>\n<pre><code class=\"language-console\">$ git config --global rebase.autoSquash true\n$ git rebase -i main\n$ git log --oneline\n4b423fe Add c\n883a054 Add b\nc5773f4 Add a\n</code></pre>\n<p>The Git manual says this setting applies to interactive mode. The full workflow is in <a href=\"https://mnixblog.netlify.app/interactive-rebase-done-properly/\">Interactive Rebase, Done Properly</a>.</p>\n<h3 id=\"rebase-updaterefs-stacked-branches-move-together\">rebase.updateRefs: stacked branches move together<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/make-git-yours-config-and-aliases/#rebase-updaterefs-stacked-branches-move-together\" aria-label=\"Link to this section\">#</a></h3>\n<p>Say <code>step2</code> is built on <code>step1</code>, and <code>main</code> moves. Rebase <code>step2</code> onto <code>main</code> and <code>step1</code> is left behind:</p>\n<pre><code class=\"language-console\">$ git rebase main\n$ git log --oneline --graph --all --decorate\n* c628fd7 (HEAD -&gt; step2) step 2\n* 2477d66 step 1\n* 2f96efb (main) main moves\n| * 545b7ce (step1) step 1\n|/\n* 2502ebe Add a\n</code></pre>\n<p>With <code>rebase.updateRefs true</code>, the same rebase also moves <code>step1</code>:</p>\n<pre><code class=\"language-text\">Successfully rebased and updated refs/heads/step2.\nUpdated the following refs with --update-refs:\n\trefs/heads/step1\n</code></pre>\n<p>This needs Git 2.38 or newer. On 2.45.2 it worked for a plain <code>git rebase main</code>, not only with <code>-i</code>.</p>\n<h3 id=\"merge-conflictstyle-a-conflict-that-shows-more\">merge.conflictStyle: a conflict that shows more<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/make-git-yours-config-and-aliases/#merge-conflictstyle-a-conflict-that-shows-more\" aria-label=\"Link to this section\">#</a></h3>\n<pre><code class=\"language-bash\">git config --global merge.conflictStyle zdiff3\n</code></pre>\n<p>This adds the original text to conflict markers and trims lines both sides share. It needs Git 2.35 or newer.</p>\n<h2 id=\"aliases\">Aliases<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/make-git-yours-config-and-aliases/#aliases\" aria-label=\"Link to this section\">#</a></h2>\n<p>An alias is a short name for a longer command, stored in config. They live in the same files, so <code>--show-origin</code> lists them too.</p>\n<pre><code class=\"language-bash\">git config --global alias.st \"status -sb\"\ngit config --global alias.lg \"log --oneline --graph --decorate\"\ngit config --global alias.cm \"commit -m\"\ngit config --global alias.unstage \"restore --staged\"\n</code></pre>\n<p>They behave like built-in commands:</p>\n<pre><code class=\"language-console\">$ git st\n## main\nM  a.txt\n\n$ git lg\n* 80e12e5 (HEAD -&gt; main) two\n* 69ce328 one\n\n$ git config --get-regexp '^alias\\.'\nalias.st status -sb\nalias.lg log --oneline --graph --decorate\nalias.cm commit -m\nalias.unstage restore --staged\n</code></pre>\n<h3 id=\"what-an-alias-cannot-do\">What an alias cannot do<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/make-git-yours-config-and-aliases/#what-an-alias-cannot-do\" aria-label=\"Link to this section\">#</a></h3>\n<p>Git does not let an alias replace a command it already has. We set one anyway:</p>\n<pre><code class=\"language-console\">$ git config --global alias.status \"log --oneline\"\n$ git status\nOn branch main\nnothing to commit, working tree clean\n</code></pre>\n<p>The built-in <code>git status</code> still ran. So an alias cannot hide a real command, and a new name is always safe to try.</p>\n<h3 id=\"a-shell-alias\">A shell alias<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/make-git-yours-config-and-aliases/#a-shell-alias\" aria-label=\"Link to this section\">#</a></h3>\n<p>A leading <code>!</code> runs the rest as a shell command:</p>\n<pre><code class=\"language-console\">$ git config --global alias.who '!git config user.name'\n$ git who\nAda Example\n</code></pre>\n<p>On Windows that shell is the one bundled with Git. Anything you put after <code>!</code> is a script you maintain, so keep those few.</p>\n<h3 id=\"aliases-to-think-twice-about\">Aliases to think twice about<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/make-git-yours-config-and-aliases/#aliases-to-think-twice-about\" aria-label=\"Link to this section\">#</a></h3>\n<p>The danger in an alias is not what it does. It is that it hides what it does, at the exact moment you stop reading.</p>\n<pre><code class=\"language-console\">$ git config --global alias.undo \"reset --hard HEAD~1\"\n$ git undo\nHEAD is now at 69ce328 one\n</code></pre>\n<p>That discarded the commit named <code>two</code>. It was recoverable only because the reflog remembers it:</p>\n<pre><code class=\"language-console\">$ git reflog\n69ce328 HEAD@{0}: reset: moving to HEAD~1\n80e12e5 HEAD@{1}: commit: two\n</code></pre>\n<p><code>git reset --hard 80e12e5</code> printed <code>HEAD is now at 80e12e5 two</code> and the commit was back. The lesson is about hiding <code>--hard</code> behind a friendly word.</p>\n<p>The same goes for <code>alias.pushf \"push --force\"</code>. It works, and <code>git help pushf</code> politely says <code>'pushf' is aliased to 'push --force'</code>. A name that sounds routine makes a force-push routine.</p>\n<p>If you want one, alias <code>--force-with-lease</code> instead. We amended a commit in one clone while someone else pushed to the same branch from another. The lease noticed:</p>\n<pre><code class=\"language-console\">$ git push --force-with-lease origin main\n ! [rejected]        main -&gt; main (stale info)\n</code></pre>\n<p>A plain <code>git push --force</code> in the same spot went through (<code>+ c3c5e08...2606a78 main -&gt; main (forced update)</code>) and replaced the other person's commit.</p>\n<p>A fair rule: alias commands that read (<code>status</code>, <code>log</code>, <code>diff</code>) freely, and think twice before aliasing anything that deletes or overwrites.</p>\n<h2 id=\"on-windows-line-endings-and-sign-in\">On Windows: line endings and sign-in<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/make-git-yours-config-and-aliases/#on-windows-line-endings-and-sign-in\" aria-label=\"Link to this section\">#</a></h2>\n<p>Two settings already sit in the system file. We can ask Git where they came from. These are from the Git for Windows install on this machine:</p>\n<pre><code class=\"language-console\">$ git config --system --show-origin --list\nfile:.../Git/etc/gitconfig\tcore.autocrlf=true\nfile:.../Git/etc/gitconfig\tcore.symlinks=true\nfile:.../Git/etc/gitconfig\tcredential.helper=manager\n</code></pre>\n<p>I filtered that to three lines.</p>\n<p><strong><code>core.autocrlf=true</code></strong> converts line endings in both directions: files get Windows endings (CRLF) in your folder and Unix endings (LF) in the repository. You see it as a warning:</p>\n<pre><code class=\"language-console\">$ git -c core.autocrlf=true add notes.txt\nwarning: in the working copy of 'notes.txt', LF will be replaced by CRLF the next time Git touches it\n</code></pre>\n<p>With <code>core.autocrlf=input</code> or <code>false</code>, the same <code>git add</code> prints nothing.</p>\n<p>To see what is really stored, <code>git ls-files --eol</code> lists the line endings in the index (<code>i/</code>) and in your folder (<code>w/</code>). For a Windows-style file in a repository that has a <code>.gitattributes</code> file containing <code>* text=auto</code>:</p>\n<pre><code class=\"language-console\">$ git ls-files --eol win.txt\ni/lf    w/crlf  attr/text=auto        \twin.txt\n</code></pre>\n<p>The index holds LF, the folder holds CRLF. A <code>.gitattributes</code> file is committed with the repository, so every clone sees the same rule. A config setting exists only on the machine that has it.</p>\n<p><strong><code>credential.helper=manager</code></strong> is Git Credential Manager, the helper that Git for Windows sets up, and it is what remembers your sign-in for HTTPS remotes. On this machine <code>git credential-manager --version</code> printed <code>2.5.0+d34930736e131ad80e5690e5634ced1808aff3e2</code>. If a push asks for a password every time, check this setting first, with <code>--show-origin</code> so you know which file overrides it.</p>\n<h2 id=\"try-it\">Try it<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/make-git-yours-config-and-aliases/#try-it\" aria-label=\"Link to this section\">#</a></h2>\n<p>Here is the tour in seven commands. Type them one at a time.</p>\n<figure class=\"ob-demo ob-portable-demo\" data-demo=\"git-practice\"><figcaption class=\"ob-code-title\">Practice terminal · config and aliases</figcaption><p>Type these commands in order, in a practice folder:</p>\n<ol>\n<li>List every setting and the file it comes from. Try <code>git config --list --show-origin</code>. The left column is the file; where a name repeats, the narrower scope wins.</li>\n<li>Find out where the user.email in effect comes from. Try <code>git config --show-origin user.email</code>. It prints the local file, <code>.git/config</code>.</li>\n<li>Make the first push of a new branch set its upstream. Try <code>git config --global push.autoSetupRemote true</code>.</li>\n<li>Remove remote branches that no longer exist on every fetch. Try <code>git config --global fetch.prune true</code>.</li>\n<li>Make a divergent pull stop instead of guessing. Try <code>git config --global pull.ff only</code>; <code>git config --global pull.rebase true</code> and <code>git config --global pull.rebase false</code> (merge) also answer Git's question.</li>\n<li>Add an alias named lg for <code>log --oneline --graph --decorate</code>. Try <code>git config --global alias.lg \"log --oneline --graph --decorate\"</code>.</li>\n<li>Remove that alias again. Try <code>git config --global --unset alias.lg</code>.</li>\n</ol>\n<p class=\"ob-portable-note\">Try the interactive Git practice terminal on <a href=\"https://mnixblog.netlify.app/make-git-yours-config-and-aliases/\">the original article</a>.</p></figure>\n<p>Two things worth trying: ask for a hint, and answer step 5 with the rebase setting.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong><code>user.email</code> is set in your global file and again in a repository's <code>.git/config</code>. In that repository, which value does <code>git config user.email</code> print?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>The one from <code>.git/config</code></li>\n<li>The one from the global file</li>\n<li>Both, one per line</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. The one from <code>.git/config</code></p>\n<p>The narrower scope wins. <code>git config --show-scope --show-origin user.email</code> prints <code>local file:.git/config</code> and the value.</p>\n</div>\n</div>\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>You run <code>git config --global alias.status \"log --oneline\"</code> and then <code>git status</code>. What happens?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Git runs the built-in <code>status</code> and ignores the alias</li>\n<li>Git runs <code>git log --oneline</code></li>\n<li>Git refuses with an error</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. Git runs the built-in <code>status</code> and ignores the alias</p>\n<p>Git does not let an alias replace an existing command. In our run, <code>git status</code> printed <code>On branch main</code> as usual.</p>\n</div>\n</div>\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>What does <code>fetch.prune true</code> remove?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Remote-tracking pointers such as <code>origin/feature/x</code>, when the branch is gone from the remote</li>\n<li>Local branches whose remote branch was deleted</li>\n<li>Commits that nobody references</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. Remote-tracking pointers such as <code>origin/feature/x</code>, when the branch is gone from the remote</p>\n<p>Only the <code>origin/...</code> pointers go. A local branch stays, and <code>git branch -vv</code> marks it <code>[origin/y: gone]</code>.</p>\n</div>\n</div>\n</section>\n<p><strong>Challenge: find where it lives</strong></p>\n<p>Inside any repository, set <code>pull.rebase</code> for that repository only, then find out which file holds it.</p>\n<p><strong>Expected result:</strong></p>\n<pre><code class=\"language-console\">$ git config --show-origin pull.rebase\nfile:.git/config\ttrue\n</code></pre>\n<p>If it printed a path outside <code>.git</code>, you set it with <code>--global</code>.</p>\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/make-git-yours-config-and-aliases/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<ul>\n<li>Settings live in system, global and local files. The narrower scope wins.</li>\n<li><code>git config --list --show-origin</code> shows every setting and the file behind it. <code>--show-scope</code> names the scope.</li>\n<li><code>-c name=value</code> tries a setting for one command only.</li>\n<li><code>pull.ff only</code> or <code>pull.rebase true</code> ends the divergent pull error. <code>push.autoSetupRemote</code> (2.37) sets upstream on the first push. <code>fetch.prune</code> removes pointers to deleted remote branches.</li>\n<li><code>rebase.autoSquash</code> places fixups. <code>rebase.updateRefs</code> (2.38) moves stacked branches. <code>merge.conflictStyle zdiff3</code> (2.35) shows the original text inside a conflict.</li>\n<li>An alias can add a name but cannot replace a command. Be careful with aliases that hide <code>--hard</code> or <code>--force</code>.</li>\n<li>On Windows, <code>core.autocrlf</code> and <code>credential.helper</code> come from the install. <code>.gitattributes</code> is the team's rule.</li>\n</ul>\n<p><code>git config --list --show-origin</code> now tells us where every setting lives, and which one wins.</p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/make-git-yours-config-and-aliases/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Make Git Yours.\nCanonical: https://mnixblog.netlify.app/make-git-yours-config-and-aliases/\nConfig Scopes, Safe Defaults, and Aliases Worth Having\nYou run git pull and Git answers like this:\n$ git pull\nfatal: Need to specify how to reconcile divergent branches.\nIt also prints a dozen lines of hints. They tell you to set a config value, and they do not say where that value is kept or whose settings you are about to change.\nIn this article, we are going to read Git's settings, work out which file each one comes from, and set a short list of defaults, each one tied to a message like the one above. By the end, one command will tell you where every setting lives:\ngit config --list --show-origin\nWe will also write a few aliases and decide which ones are safe to keep.\nNote:\nEverything here ran on Git 2.45.2 on Windows 11, in Git Bash. Where a setting needs a newer Git, the text says so. In the output I replaced long scratch folder names with ....\nWhere a setting lives\nGit reads settings from several files and layers them. The ones you will touch are these:\nScope\nFlag\nFile\nApplies to\nsystem\n--system\netc/gitconfig inside the Git install\nevery user on the machine\nglobal\n--global\n.gitconfig in your home folder\nevery repository you use\nlocal\n--local (the default when you write)\n.git/config in the repository\nthat repository only\nWhen the same setting appears in more than one file, the narrower scope wins.\nTo see that without touching your real settings, point Git at a throwaway global file. In Git Bash:\nexport GIT_CONFIG_GLOBAL=\"$PWD/.gitconfig\"\nexport GIT_CONFIG_NOSYSTEM=1\nGIT_CONFIG_GLOBAL replaces your global file for this terminal window only. GIT_CONFIG_NOSYSTEM=1 ignores the system file. Close the window and your own settings are back.\nNow set an email twice, once globally and once in a repository:\ngit config --global user.name \"Ada Example\"\ngit config --global user.email \"ada@home.example\"\ngit init -q demo && cd demo\ngit config user.email \"ada@work.example\"\nAsk Git for everything it knows, and where it learned it. I left out the core.* lines that git init writes into .git/config:\n$ git config --list --show-origin\nfile:C:/.../.gitconfig\tuser.name=Ada Example\nfile:C:/.../.gitconfig\tuser.email=ada@home.example\nfile:.git/config\tuser.email=ada@work.example\nThe left column is the file and the right column is the setting. The email appears twice, once from each file.\nAsk for the one that counts:\n$ git config user.email\nada@work.example\n$ git config --show-scope --show-origin user.email\nlocal\tfile:.git/config\tada@work.example\nThe local file won. --show-scope adds the scope name, so you do not have to guess from the path.\nIf a setting behaves strangely, this is the first command to run. A value you forgot you set three years ago shows up here with its file name next to it.\nTwo more commands finish the picture. git config --global --edit opens the global file in your editor, and git config --global --unset user.email removes one setting.\nSix defaults that each stop one annoyance\nEvery default below has a message it answers. We ran each one against a pretend remote, origin.git, with two clones standing in for two people.\npull.ff: stop guessing on a divergent pull\nAlice pushes a commit. Bob commits locally without pulling first, then runs git pull. Here is Bob's screen, with the hints cut out:\n$ git pull\nFrom .../origin\n9afce9f..d953b3d  main       -> origin/main\n...\nfatal: Need to specify how to reconcile divergent branches.\nGit will not pick between a merge and a rebase for you. The hint lists three ways to answer. Two of them are useful defaults:\n$ git -c pull.ff=only pull\nfatal: Not possible to fast-forward, aborting.\n$ git -c pull.rebase=true pull\nSuccessfully rebased and updated refs/heads/main.\n-c name=value applies a setting to one command and nothing else, which is a safe way to try a default before keeping it.\npull.ff only pulls only when the pull is a straight line, and stops otherwise. You then choose to merge or rebase yourself. pull.rebase true always replays your local commits on top of what arrived, which gave Bob this history:\n* 751c288 bob change\n* d953b3d alice change\n* 9afce9f start\nEither one removes the surprise. Pick one, and set it with --global:\ngit config --global pull.ff only\npush.autoSetupRemote: stop typing --set-upstream\nA new branch has no upstream yet, so the first plain git push fails:\n$ git push\nfatal: The current branch feature/x has no upstream branch.\nTo push the current branch and set the remote as upstream, use\ngit push --set-upstream origin feature/x\nTurn on the setting, and the same command works:\n$ git config --global push.autoSetupRemote true\n$ git push\nbranch 'feature/x' set up to track 'origin/feature/x'.\n* [new branch]      feature/x -> feature/x\nThe branch is now tracking origin/feature/x, so git status -sb starts with ## feature/x...origin/feature/x. This setting needs Git 2.37 or newer. It assumes the branch keeps the same name on the remote, which is the usual case on GitHub.\nfetch.prune: stop seeing branches that are gone\nWhen a branch is deleted on GitHub, your copy of its pointer, origin/feature/x, stays behind. Alice's list after Bob deleted feature/x on the remote:\n$ git fetch\n$ git branch -r\norigin/feature/x\norigin/main\nA plain fetch does not remove it. With --prune, it does:\n$ git fetch --prune\n- [deleted]         (none)     -> origin/feature/x\n$ git branch -r\norigin/main\nfetch.prune true makes every fetch behave that way. It only removes those remote pointers. A local branch of the same name stays, and Git marks it:\n$ git branch -vv\n* main d953b3d [origin/main] alice change\ny    ea11253 [origin/y: gone] y\ngone is your cue that the branch was merged or deleted upstream.\nrebase.autoSquash: fixups land where they belong\ngit commit --fixup <commit> creates a commit titled fixup! <that commit's title>. Without help, an interactive rebase leaves it at the end:\n$ git rebase -i main\n$ git log --oneline\n3bb5134 fixup! Add b\n8ae88bd Add c\nd42c68b Add b\nc5773f4 Add a\nWith rebase.autoSquash on, the same command folds it into its target:\n$ git config --global rebase.autoSquash true\n$ git rebase -i main\n$ git log --oneline\n4b423fe Add c\n883a054 Add b\nc5773f4 Add a\nThe Git manual says this setting applies to interactive mode. The full workflow is in Interactive Rebase, Done Properly (https://mnixblog.netlify.app/interactive-rebase-done-properly/).\nrebase.updateRefs: stacked branches move together\nSay step2 is built on step1, and main moves. Rebase step2 onto main and step1 is left behind:\n$ git rebase main\n$ git log --oneline --graph --all --decorate\n* c628fd7 (HEAD -> step2) step 2\n* 2477d66 step 1\n* 2f96efb (main) main moves\n| * 545b7ce (step1) step 1\n|/\n* 2502ebe Add a\nWith rebase.updateRefs true, the same rebase also moves step1:\nSuccessfully rebased and updated refs/heads/step2.\nUpdated the following refs with --update-refs:\nrefs/heads/step1\nThis needs Git 2.38 or newer. On 2.45.2 it worked for a plain git rebase main, not only with -i.\nmerge.conflictStyle: a conflict that shows more\ngit config --global merge.conflictStyle zdiff3\nThis adds the original text to conflict markers and trims lines both sides share. It needs Git 2.35 or newer.\nAliases\nAn alias is a short name for a longer command, stored in config. They live in the same files, so --show-origin lists them too.\ngit config --global alias.st \"status -sb\"\ngit config --global alias.lg \"log --oneline --graph --decorate\"\ngit config --global alias.cm \"commit -m\"\ngit config --global alias.unstage \"restore --staged\"\nThey behave like built-in commands:\n$ git st\n## main\nM  a.txt\n$ git lg\n* 80e12e5 (HEAD -> main) two\n* 69ce328 one\n$ git config --get-regexp '^alias\\.'\nalias.st status -sb\nalias.lg log --oneline --graph --decorate\nalias.cm commit -m\nalias.unstage restore --staged\nWhat an alias cannot do\nGit does not let an alias replace a command it already has. We set one anyway:\n$ git config --global alias.status \"log --oneline\"\n$ git status\nOn branch main\nnothing to commit, working tree clean\nThe built-in git status still ran. So an alias cannot hide a real command, and a new name is always safe to try.\nA shell alias\nA leading ! runs the rest as a shell command:\n$ git config --global alias.who '!git config user.name'\n$ git who\nAda Example\nOn Windows that shell is the one bundled with Git. Anything you put after ! is a script you maintain, so keep those few.\nAliases to think twice about\nThe danger in an alias is not what it does. It is that it hides what it does, at the exact moment you stop reading.\n$ git config --global alias.undo \"reset --hard HEAD~1\"\n$ git undo\nHEAD is now at 69ce328 one\nThat discarded the commit named two. It was recoverable only because the reflog remembers it:\n$ git reflog\n69ce328 HEAD@{0}: reset: moving to HEAD~1\n80e12e5 HEAD@{1}: commit: two\ngit reset --hard 80e12e5 printed HEAD is now at 80e12e5 two and the commit was back. The lesson is about hiding --hard behind a friendly word.\nThe same goes for alias.pushf \"push --force\". It works, and git help pushf politely says 'pushf' is aliased to 'push --force'. A name that sounds routine makes a force-push routine.\nIf you want one, alias --force-with-lease instead. We amended a commit in one clone while someone else pushed to the same branch from another. The lease noticed:\n$ git push --force-with-lease origin main\n! [rejected]        main -> main (stale info)\nA plain git push --force in the same spot went through (+ c3c5e08...2606a78 main -> main (forced update)) and replaced the other person's commit.\nA fair rule: alias commands that read (status, log, diff) freely, and think twice before aliasing anything that deletes or overwrites.\nOn Windows: line endings and sign-in\nTwo settings already sit in the system file. We can ask Git where they came from. These are from the Git for Windows install on this machine:\n$ git config --system --show-origin --list\nfile:.../Git/etc/gitconfig\tcore.autocrlf=true\nfile:.../Git/etc/gitconfig\tcore.symlinks=true\nfile:.../Git/etc/gitconfig\tcredential.helper=manager\nI filtered that to three lines.\ncore.autocrlf=true converts line endings in both directions: files get Windows endings (CRLF) in your folder and Unix endings (LF) in the repository. You see it as a warning:\n$ git -c core.autocrlf=true add notes.txt\nwarning: in the working copy of 'notes.txt', LF will be replaced by CRLF the next time Git touches it\nWith core.autocrlf=input or false, the same git add prints nothing.\nTo see what is really stored, git ls-files --eol lists the line endings in the index (i/) and in your folder (w/). For a Windows-style file in a repository that has a .gitattributes file containing * text=auto:\n$ git ls-files --eol win.txt\ni/lf    w/crlf  attr/text=auto        \twin.txt\nThe index holds LF, the folder holds CRLF. A .gitattributes file is committed with the repository, so every clone sees the same rule. A config setting exists only on the machine that has it.\ncredential.helper=manager is Git Credential Manager, the helper that Git for Windows sets up, and it is what remembers your sign-in for HTTPS remotes. On this machine git credential-manager --version printed 2.5.0+d34930736e131ad80e5690e5634ced1808aff3e2. If a push asks for a password every time, check this setting first, with --show-origin so you know which file overrides it.\nTry it\nHere is the tour in seven commands. Type them one at a time.\nPractice terminal · config and aliases\nType these commands in order, in a practice folder:\nList every setting and the file it comes from. Try git config --list --show-origin. The left column is the file; where a name repeats, the narrower scope wins.\nFind out where the user.email in effect comes from. Try git config --show-origin user.email. It prints the local file, .git/config.\nMake the first push of a new branch set its upstream. Try git config --global push.autoSetupRemote true.\nRemove remote branches that no longer exist on every fetch. Try git config --global fetch.prune true.\nMake a divergent pull stop instead of guessing. Try git config --global pull.ff only; git config --global pull.rebase true and git config --global pull.rebase false (merge) also answer Git's question.\nAdd an alias named lg for log --oneline --graph --decorate. Try git config --global alias.lg \"log --oneline --graph --decorate\".\nRemove that alias again. Try git config --global --unset alias.lg.\nTry the interactive Git practice terminal on the canonical article (https://mnixblog.netlify.app/make-git-yours-config-and-aliases/).\nTwo things worth trying: ask for a hint, and answer step 5 with the rebase setting.\nQuestion: user.email is set in your global file and again in a repository's .git/config. In that repository, which value does git config user.email print?\nThe one from .git/config\nThe one from the global file\nBoth, one per line\nAnswer: 1. The one from .git/config\nExplanation: The narrower scope wins. git config --show-scope --show-origin user.email prints local file:.git/config and the value.\nQuestion: You run git config --global alias.status \"log --oneline\" and then git status. What happens?\nGit runs the built-in status and ignores the alias\nGit runs git log --oneline\nGit refuses with an error\nAnswer: 1. Git runs the built-in status and ignores the alias\nExplanation: Git does not let an alias replace an existing command. In our run, git status printed On branch main as usual.\nQuestion: What does fetch.prune true remove?\nRemote-tracking pointers such as origin/feature/x, when the branch is gone from the remote\nLocal branches whose remote branch was deleted\nCommits that nobody references\nAnswer: 1. Remote-tracking pointers such as origin/feature/x, when the branch is gone from the remote\nExplanation: Only the origin/... pointers go. A local branch stays, and git branch -vv marks it [origin/y: gone].\nChallenge: find where it lives\nInside any repository, set pull.rebase for that repository only, then find out which file holds it.\nExpected result:\n$ git config --show-origin pull.rebase\nfile:.git/config\ttrue\nIf it printed a path outside .git, you set it with --global.\nWhat we learned\nSettings live in system, global and local files. The narrower scope wins.\ngit config --list --show-origin shows every setting and the file behind it. --show-scope names the scope.\n-c name=value tries a setting for one command only.\npull.ff only or pull.rebase true ends the divergent pull error. push.autoSetupRemote (2.37) sets upstream on the first push. fetch.prune removes pointers to deleted remote branches.\nrebase.autoSquash places fixups. rebase.updateRefs (2.38) moves stacked branches. merge.conflictStyle zdiff3 (2.35) shows the original text inside a conflict.\nAn alias can add a name but cannot replace a command. Be careful with aliases that hide --hard or --force.\nOn Windows, core.autocrlf and credential.helper come from the install. .gitattributes is the team's rule.\ngit config --list --show-origin now tells us where every setting lives, and which one wins.\nPortable fallback: The authored description of the demo is preserved; the live demonstration remains on the canonical article. Review: Review the authored description of the demo and keep the canonical link for the live version.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/make-git-yours-config-and-aliases/#ob-discussion)",
      "date_published": "2026-10-27T00:06:00Z",
      "id": "https://mnixblog.netlify.app/make-git-yours-config-and-aliases/",
      "image": "https://mnixblog.netlify.app/images/make-git-yours-config-and-aliases/cover.jpg",
      "summary": "Find where each Git setting comes from, turn on six defaults that each stop one annoyance, and write aliases that do not hide danger.",
      "tags": [
        "Git",
        "Command Line",
        "Configuration"
      ],
      "title": "Make Git Yours.",
      "url": "https://mnixblog.netlify.app/make-git-yours-config-and-aliases/"
    },
    {
      "content_html": "<p>Somebody commits a <code>.env</code> file with a real password in it. Nobody notices until it is pushed.</p>\n<p>You can write a rule for that in a README. You can mention it in review. Or you can make Git refuse the commit before it exists.</p>\n<p>A <strong>hook</strong> is a script that Git runs at a fixed moment, such as just before a commit is created. If the script exits with an error, Git stops.</p>\n<p>We will write two hooks, make them part of the repository so a teammate gets them, and then find out exactly how to get around them, because that decides what hooks are for. This is the result, and it does not promise more than it shows:</p>\n<pre><code class=\"language-console\">$ git commit -m \"Add env\"\npre-commit: refusing to commit a .env file.\n</code></pre>\n<aside class=\"ob-callout ob-callout-note\" role=\"note\"><p class=\"ob-callout-title\"><strong>Note</strong></p>\n<p>Every command and output in this article comes from Git 2.45.2 on Windows 11, run in Git Bash. Our stand-in for GitHub is a bare repository in a folder called <code>hub.git</code>.</p>\n</aside>\n<h2 id=\"what-a-hook-is-and-where-it-lives\">What a hook is, and where it lives<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/hooks-you-will-actually-use/#what-a-hook-is-and-where-it-lives\" aria-label=\"Link to this section\">#</a></h2>\n<p>Every repository starts with a <code>.git/hooks</code> folder full of examples:</p>\n<pre><code class=\"language-console\">$ ls .git/hooks\napplypatch-msg.sample\ncommit-msg.sample\nfsmonitor-watchman.sample\npost-update.sample\npre-applypatch.sample\npre-commit.sample\npre-merge-commit.sample\npre-push.sample\npre-rebase.sample\npre-receive.sample\nprepare-commit-msg.sample\npush-to-checkout.sample\nsendemail-validate.sample\nupdate.sample\n</code></pre>\n<p>Git ignores anything ending in <code>.sample</code>. To turn one on, you make a file with the hook's name and no extension: <code>pre-commit</code>, not <code>pre-commit.sh</code>.</p>\n<p>The two we will write:</p>\n<ul>\n<li><code>pre-commit</code> runs before Git asks for a commit message. It can look at what is staged. A non-zero exit stops the commit.</li>\n<li><code>commit-msg</code> runs after you write the message. Git gives it the path of the file that holds the message.</li>\n</ul>\n<h2 id=\"a-pre-commit-hook-that-checks-one-thing\">A pre-commit hook that checks one thing<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/hooks-you-will-actually-use/#a-pre-commit-hook-that-checks-one-thing\" aria-label=\"Link to this section\">#</a></h2>\n<p>A hook is just a script. This one stops a <code>.env</code> file from being added:</p>\n<pre><code class=\"language-bash\">#!/bin/sh\n# Stop a .env file from being added to a commit.\nif git diff --cached --name-only --diff-filter=AM | grep -q \"\\.env$\"; then\n  echo \"pre-commit: refusing to commit a .env file.\"\n  exit 1\nfi\n</code></pre>\n<p><code>git diff --cached --name-only</code> lists the files in the staging area. <code>--diff-filter=AM</code> keeps only those that are <strong>A</strong>dded or <strong>M</strong>odified. Without that part, the hook has a bug we hit in our test: a commit that removes <code>.env</code> from Git also lists <code>.env</code>, so the hook blocked the very commit that fixed the problem.</p>\n<pre><code class=\"language-console\">$ git commit -m \"Stop tracking env\"\npre-commit: refusing to commit a .env file.\n</code></pre>\n<p>With <code>--diff-filter=AM</code>, the same commit went through:</p>\n<pre><code class=\"language-text\">[main 6aa0a43] Stop tracking env\n 1 file changed, 1 deletion(-)\n delete mode 100644 .env\n</code></pre>\n<h2 id=\"windows-who-runs-it\">Windows: who runs it?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/hooks-you-will-actually-use/#windows-who-runs-it\" aria-label=\"Link to this section\">#</a></h2>\n<p>The first line, <code>#!/bin/sh</code>, is the shebang: it names the program that runs the file. Windows has no <code>/bin/sh</code>, and Git for Windows brings its own. Git runs hooks with that bundled shell, so the hook works no matter which terminal you start Git from. We committed from Git Bash and from PowerShell, and in both the hook ran and refused.</p>\n<p>The executable bit is the other thing everybody asks about. On Linux and macOS, a hook must be executable or Git skips it. On Windows, there is no such bit on the file system. In our test, <code>chmod -x</code> on a hook in <code>.git/hooks</code> changed nothing: <code>ls -l</code> still showed <code>-rwxr-xr-x</code>, and the hook still ran. So on Windows, you do not need <code>chmod</code> for the hook to run for you.</p>\n<p>It matters when a teammate on Linux or macOS gets your hook through the repository. That comes next.</p>\n<h2 id=\"make-everybody-get-the-hooks\">Make everybody get the hooks<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/hooks-you-will-actually-use/#make-everybody-get-the-hooks\" aria-label=\"Link to this section\">#</a></h2>\n<p><code>.git/hooks</code> is inside <code>.git</code>, and <code>.git</code> is never part of a clone. A fresh clone has the samples and none of your hooks. We check, in a clone of the same project:</p>\n<pre><code class=\"language-console\">$ git config core.hooksPath\n$ ls -A .git/hooks | grep -c sample\n14\n</code></pre>\n<p>The setting prints nothing, so it is not set. To share the hooks, we move them into a folder that is tracked, <code>.githooks</code>, and tell Git to look there:</p>\n<pre><code class=\"language-console\">$ git config core.hooksPath .githooks\n$ git config core.hooksPath\n.githooks\n</code></pre>\n<p>Two details:</p>\n<ol>\n<li><strong>The setting is per clone.</strong> It lives in <code>.git/config</code>, which is not shared either. Every person runs that one command once, or a setup script does. In our test, a teammate's fresh clone had the <code>.githooks</code> folder, and a commit with a <code>.env</code> in it went straight through, because the setting was not there yet.</li>\n<li><strong>Record the executable bit in Git.</strong> The files are <code>100644</code> by default on Windows. Make them <code>100755</code>, so a teammate on Linux or macOS gets runnable scripts:</li>\n</ol>\n<pre><code class=\"language-console\">$ git ls-files -s .githooks/pre-commit\n100644 d9a4c991b07dbd9b526df46c0956c7b5ac972df2 0\t.githooks/pre-commit\n$ git update-index --chmod=+x .githooks/pre-commit\n$ git ls-files -s .githooks/pre-commit\n100755 d9a4c991b07dbd9b526df46c0956c7b5ac972df2 0\t.githooks/pre-commit\n</code></pre>\n<p><code>git add --chmod=+x &lt;file&gt;</code> does the same thing when the file is new. After a commit and a push, the teammate runs the one config command, and:</p>\n<pre><code class=\"language-console\">$ git commit -m \"Add env from clone with hooksPath\"\npre-commit: refusing to commit a .env file.\n</code></pre>\n<p>To run a hook by hand without committing, <code>git hook run pre-commit</code> runs the same script and exits with its status.</p>\n<h2 id=\"a-commit-msg-hook\">A commit-msg hook<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/hooks-you-will-actually-use/#a-commit-msg-hook\" aria-label=\"Link to this section\">#</a></h2>\n<p>The second hook reads the message file Git passes as <code>$1</code>. This one keeps the first line short:</p>\n<pre><code class=\"language-bash\">#!/bin/sh\n# commit-msg gets the message file as $1. Keep the first line short.\nif [ \"$(head -n 1 \"$1\" | wc -c)\" -gt 51 ]; then\n  echo \"commit-msg: first line is longer than 50 characters.\"\n  exit 1\nfi\n</code></pre>\n<p><code>wc -c</code> counts the newline too, which is why the limit is 51.</p>\n<pre><code class=\"language-console\">$ git commit -m \"This subject line is much longer than fifty characters in total\"\ncommit-msg: first line is longer than 50 characters.\n$ git commit -m \"Short line\"\n[main 958c11f] Short line\n 1 file changed, 1 insertion(+)\n</code></pre>\n<p>Keep hooks like these quick and narrow. A hook that takes a minute will be skipped by everyone, and it will be right to be.</p>\n<h2 id=\"skip-it-knowingly\">Skip it, knowingly<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/hooks-you-will-actually-use/#skip-it-knowingly\" aria-label=\"Link to this section\">#</a></h2>\n<p>Sometimes the hook is wrong, or the file really is fine. <code>--no-verify</code> skips the <code>pre-commit</code> and <code>commit-msg</code> hooks for that one commit:</p>\n<pre><code class=\"language-console\">$ git commit --no-verify -m \"Add env anyway\"\n[main bf37352] Add env anyway\n 1 file changed, 1 insertion(+)\n create mode 100644 .env\n</code></pre>\n<p><code>-n</code> is the short form. Git did not ask for a reason.</p>\n<h2 id=\"what-hooks-cannot-enforce\">What hooks cannot enforce<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/hooks-you-will-actually-use/#what-hooks-cannot-enforce\" aria-label=\"Link to this section\">#</a></h2>\n<p>Keep this part in mind.</p>\n<ul>\n<li><strong>They are optional for the person committing.</strong> <code>--no-verify</code> is one flag, and Git does not ask why.</li>\n<li><strong>They are not delivered by <code>git clone</code>.</strong> Without the <code>core.hooksPath</code> command in each clone, they do not run. We saw a commit with a <code>.env</code> go through in a fresh clone.</li>\n<li><strong>They run on one machine.</strong> A hook cannot know what happens in another clone or on someone else's computer.</li>\n</ul>\n<p>So a hook is a <strong>courtesy to the person committing</strong>: it tells them early, while the fix is cheap. It is not a gate. A check that must hold for the whole team has to run on the server, in CI or in branch protection, where nobody can pass a flag to skip it. This article does not test either of those.</p>\n<p>The practical rule: put the fast, friendly checks in hooks and the checks you cannot live without in CI. Run the same script in both if you can.</p>\n<h2 id=\"try-it\">Try it<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/hooks-you-will-actually-use/#try-it\" aria-label=\"Link to this section\">#</a></h2>\n<p>Here is the whole loop in six commands. The <code>.githooks</code> folder is already in the repository, with a <code>pre-commit</code> hook that refuses a <code>.env</code> file, and one is already staged. Type them one at a time.</p>\n<figure class=\"ob-demo ob-portable-demo\" data-demo=\"git-practice\"><figcaption class=\"ob-code-title\">Practice terminal · share a hook with the team</figcaption><p>Type these commands in order, in a practice folder:</p>\n<ol>\n<li>Tell Git to run hooks from the tracked .githooks folder. Try <code>git config core.hooksPath .githooks</code>. This is stored in your clone's .git/config, so every clone needs it once.</li>\n<li>Check that the setting took. Try <code>git config core.hooksPath</code>. It prints <code>.githooks</code>.</li>\n<li>Commit with a .env file staged and watch the hook refuse. Try <code>git commit -m \"Add env\"</code>. Git prints <code>pre-commit: refusing to commit a .env file.</code> and creates no commit.</li>\n<li>Decide the file is fine and skip the hooks for this one commit. Try <code>git commit --no-verify -m \"Add env anyway\"</code>. <code>-n</code> is the short form. Git does not ask why.</li>\n<li>Record the pre-commit script as executable, so teammates on Linux and macOS can run it. Try <code>git update-index --chmod=+x .githooks/pre-commit</code>. <code>git add --chmod=+x .githooks/pre-commit</code> does the same.</li>\n<li>Confirm the mode. Try <code>git ls-files -s .githooks/pre-commit</code>. The mode column shows <code>100755</code>, where <code>100644</code> means not executable.</li>\n</ol>\n<p class=\"ob-portable-note\">Try the interactive Git practice terminal on <a href=\"https://mnixblog.netlify.app/hooks-you-will-actually-use/\">the original article</a>.</p></figure>\n<p>Two things worth trying in the terminal: run step 3 before step 1 and see what the commit does, and use <code>git commit -n</code> in step 4.</p>\n<p><strong>Challenge: one more file type</strong></p>\n<p>Change the <code>pre-commit</code> hook so it also refuses a <code>.pem</code> file, because a private key is no better than a password. Removing a <code>.pem</code> file from Git must still work.</p>\n<p><strong>Expected result:</strong> with <code>core.hooksPath</code> set, <code>git commit -m \"Add key\"</code> with <code>secret.pem</code> staged prints <code>pre-commit: refusing to commit a .env or .pem file.</code> and makes no commit. After <code>--no-verify</code> has let the file in, <code>git rm secret.pem</code> followed by <code>git commit -m \"Stop tracking key\"</code> goes through.</p>\n<details>\n<summary>Answer</summary>\n<pre><code class=\"language-bash\">#!/bin/sh\n# Stop a .env or .pem file from being added to a commit.\nif git diff --cached --name-only --diff-filter=AM | grep -Eq \"\\.(env|pem)$\"; then\n  echo \"pre-commit: refusing to commit a .env or .pem file.\"\n  exit 1\nfi\n</code></pre>\n<p><code>-E</code> turns on extended patterns, so <code>(env|pem)</code> works. Keep <code>--diff-filter=AM</code>, or the commit that removes the key is refused too.</p>\n</details>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>You write a <code>pre-commit</code> hook in <code>.git/hooks</code>, push your work, and a teammate clones the repository. Does the hook run for them?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>No, <code>.git</code> is not part of a clone, so they get only the sample hooks</li>\n<li>Yes, hooks are committed along with the code</li>\n<li>Yes, but only on Windows</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. No, <code>.git</code> is not part of a clone, so they get only the sample hooks</p>\n<p>In our test, a fresh clone had 14 <code>.sample</code> files and <code>core.hooksPath</code> was unset. A tracked folder plus <code>git config core.hooksPath</code> in each clone is how you share hooks.</p>\n</div>\n</div>\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>What does <code>git commit --no-verify</code> skip?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>The <code>pre-commit</code> and <code>commit-msg</code> hooks for that commit</li>\n<li>The commit message</li>\n<li>Every check, including the ones in CI</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. The <code>pre-commit</code> and <code>commit-msg</code> hooks for that commit</p>\n<p><code>--no-verify</code> only affects hooks on your machine. Checks that run on a server, such as CI, are not affected by a flag you pass locally.</p>\n</div>\n</div>\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>On Windows, you remove the executable bit from <code>.git/hooks/pre-commit</code> with <code>chmod -x</code>. What happened in our test?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Nothing. The hook still ran, because the file system keeps no executable bit</li>\n<li>Git skipped the hook and printed a warning</li>\n<li>Git deleted the hook</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. Nothing. The hook still ran, because the file system keeps no executable bit</p>\n<p><code>ls -l</code> still showed <code>-rwxr-xr-x</code> afterwards and the hook still refused the commit. The bit matters for teammates on Linux and macOS, so record it with <code>git update-index --chmod=+x</code>.</p>\n</div>\n</div>\n</section>\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/hooks-you-will-actually-use/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<ul>\n<li>A hook is a script named after a Git moment (<code>pre-commit</code>, <code>commit-msg</code>) with no extension. A non-zero exit stops the action.</li>\n<li>Git for Windows runs hooks with its bundled shell, from Git Bash or PowerShell. The executable bit does not exist on the file system, but record it with <code>git update-index --chmod=+x</code> for teammates elsewhere.</li>\n<li><code>.git/hooks</code> is not cloned. Put hooks in a tracked folder and run <code>git config core.hooksPath .githooks</code> once per clone.</li>\n<li><code>git commit --no-verify</code> (or <code>-n</code>) skips <code>pre-commit</code> and <code>commit-msg</code>.</li>\n<li>A hook is a friendly early check on one machine. Anything the whole team must obey belongs in CI or on the server.</li>\n</ul>\n<p>A hook that cannot be skipped is not a hook. It is a server.</p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/hooks-you-will-actually-use/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Hooks You Will Actually Use.\nCanonical: https://mnixblog.netlify.app/hooks-you-will-actually-use/\nStop a Bad Commit Before It Exists, and Know What a Hook Cannot Stop\nSomebody commits a .env file with a real password in it. Nobody notices until it is pushed.\nYou can write a rule for that in a README. You can mention it in review. Or you can make Git refuse the commit before it exists.\nA hook is a script that Git runs at a fixed moment, such as just before a commit is created. If the script exits with an error, Git stops.\nWe will write two hooks, make them part of the repository so a teammate gets them, and then find out exactly how to get around them, because that decides what hooks are for. This is the result, and it does not promise more than it shows:\n$ git commit -m \"Add env\"\npre-commit: refusing to commit a .env file.\nNote:\nEvery command and output in this article comes from Git 2.45.2 on Windows 11, run in Git Bash. Our stand-in for GitHub is a bare repository in a folder called hub.git.\nWhat a hook is, and where it lives\nEvery repository starts with a .git/hooks folder full of examples:\n$ ls .git/hooks\napplypatch-msg.sample\ncommit-msg.sample\nfsmonitor-watchman.sample\npost-update.sample\npre-applypatch.sample\npre-commit.sample\npre-merge-commit.sample\npre-push.sample\npre-rebase.sample\npre-receive.sample\nprepare-commit-msg.sample\npush-to-checkout.sample\nsendemail-validate.sample\nupdate.sample\nGit ignores anything ending in .sample. To turn one on, you make a file with the hook's name and no extension: pre-commit, not pre-commit.sh.\nThe two we will write:\npre-commit runs before Git asks for a commit message. It can look at what is staged. A non-zero exit stops the commit.\ncommit-msg runs after you write the message. Git gives it the path of the file that holds the message.\nA pre-commit hook that checks one thing\nA hook is just a script. This one stops a .env file from being added:\n#!/bin/sh\n# Stop a .env file from being added to a commit.\nif git diff --cached --name-only --diff-filter=AM | grep -q \"\\.env$\"; then\necho \"pre-commit: refusing to commit a .env file.\"\nexit 1\nfi\ngit diff --cached --name-only lists the files in the staging area. --diff-filter=AM keeps only those that are Added or Modified. Without that part, the hook has a bug we hit in our test: a commit that removes .env from Git also lists .env, so the hook blocked the very commit that fixed the problem.\n$ git commit -m \"Stop tracking env\"\npre-commit: refusing to commit a .env file.\nWith --diff-filter=AM, the same commit went through:\n[main 6aa0a43] Stop tracking env\n1 file changed, 1 deletion(-)\ndelete mode 100644 .env\nWindows: who runs it?\nThe first line, #!/bin/sh, is the shebang: it names the program that runs the file. Windows has no /bin/sh, and Git for Windows brings its own. Git runs hooks with that bundled shell, so the hook works no matter which terminal you start Git from. We committed from Git Bash and from PowerShell, and in both the hook ran and refused.\nThe executable bit is the other thing everybody asks about. On Linux and macOS, a hook must be executable or Git skips it. On Windows, there is no such bit on the file system. In our test, chmod -x on a hook in .git/hooks changed nothing: ls -l still showed -rwxr-xr-x, and the hook still ran. So on Windows, you do not need chmod for the hook to run for you.\nIt matters when a teammate on Linux or macOS gets your hook through the repository. That comes next.\nMake everybody get the hooks\n.git/hooks is inside .git, and .git is never part of a clone. A fresh clone has the samples and none of your hooks. We check, in a clone of the same project:\n$ git config core.hooksPath\n$ ls -A .git/hooks | grep -c sample\n14\nThe setting prints nothing, so it is not set. To share the hooks, we move them into a folder that is tracked, .githooks, and tell Git to look there:\n$ git config core.hooksPath .githooks\n$ git config core.hooksPath\n.githooks\nTwo details:\nThe setting is per clone. It lives in .git/config, which is not shared either. Every person runs that one command once, or a setup script does. In our test, a teammate's fresh clone had the .githooks folder, and a commit with a .env in it went straight through, because the setting was not there yet.\nRecord the executable bit in Git. The files are 100644 by default on Windows. Make them 100755, so a teammate on Linux or macOS gets runnable scripts:\n$ git ls-files -s .githooks/pre-commit\n100644 d9a4c991b07dbd9b526df46c0956c7b5ac972df2 0\t.githooks/pre-commit\n$ git update-index --chmod=+x .githooks/pre-commit\n$ git ls-files -s .githooks/pre-commit\n100755 d9a4c991b07dbd9b526df46c0956c7b5ac972df2 0\t.githooks/pre-commit\ngit add --chmod=+x <file> does the same thing when the file is new. After a commit and a push, the teammate runs the one config command, and:\n$ git commit -m \"Add env from clone with hooksPath\"\npre-commit: refusing to commit a .env file.\nTo run a hook by hand without committing, git hook run pre-commit runs the same script and exits with its status.\nA commit-msg hook\nThe second hook reads the message file Git passes as $1. This one keeps the first line short:\n#!/bin/sh\n# commit-msg gets the message file as $1. Keep the first line short.\nif [ \"$(head -n 1 \"$1\" | wc -c)\" -gt 51 ]; then\necho \"commit-msg: first line is longer than 50 characters.\"\nexit 1\nfi\nwc -c counts the newline too, which is why the limit is 51.\n$ git commit -m \"This subject line is much longer than fifty characters in total\"\ncommit-msg: first line is longer than 50 characters.\n$ git commit -m \"Short line\"\n[main 958c11f] Short line\n1 file changed, 1 insertion(+)\nKeep hooks like these quick and narrow. A hook that takes a minute will be skipped by everyone, and it will be right to be.\nSkip it, knowingly\nSometimes the hook is wrong, or the file really is fine. --no-verify skips the pre-commit and commit-msg hooks for that one commit:\n$ git commit --no-verify -m \"Add env anyway\"\n[main bf37352] Add env anyway\n1 file changed, 1 insertion(+)\ncreate mode 100644 .env\n-n is the short form. Git did not ask for a reason.\nWhat hooks cannot enforce\nKeep this part in mind.\nThey are optional for the person committing. --no-verify is one flag, and Git does not ask why.\nThey are not delivered by git clone. Without the core.hooksPath command in each clone, they do not run. We saw a commit with a .env go through in a fresh clone.\nThey run on one machine. A hook cannot know what happens in another clone or on someone else's computer.\nSo a hook is a courtesy to the person committing: it tells them early, while the fix is cheap. It is not a gate. A check that must hold for the whole team has to run on the server, in CI or in branch protection, where nobody can pass a flag to skip it. This article does not test either of those.\nThe practical rule: put the fast, friendly checks in hooks and the checks you cannot live without in CI. Run the same script in both if you can.\nTry it\nHere is the whole loop in six commands. The .githooks folder is already in the repository, with a pre-commit hook that refuses a .env file, and one is already staged. Type them one at a time.\nPractice terminal · share a hook with the team\nType these commands in order, in a practice folder:\nTell Git to run hooks from the tracked .githooks folder. Try git config core.hooksPath .githooks. This is stored in your clone's .git/config, so every clone needs it once.\nCheck that the setting took. Try git config core.hooksPath. It prints .githooks.\nCommit with a .env file staged and watch the hook refuse. Try git commit -m \"Add env\". Git prints pre-commit: refusing to commit a .env file. and creates no commit.\nDecide the file is fine and skip the hooks for this one commit. Try git commit --no-verify -m \"Add env anyway\". -n is the short form. Git does not ask why.\nRecord the pre-commit script as executable, so teammates on Linux and macOS can run it. Try git update-index --chmod=+x .githooks/pre-commit. git add --chmod=+x .githooks/pre-commit does the same.\nConfirm the mode. Try git ls-files -s .githooks/pre-commit. The mode column shows 100755, where 100644 means not executable.\nTry the interactive Git practice terminal on the canonical article (https://mnixblog.netlify.app/hooks-you-will-actually-use/).\nTwo things worth trying in the terminal: run step 3 before step 1 and see what the commit does, and use git commit -n in step 4.\nChallenge: one more file type\nChange the pre-commit hook so it also refuses a .pem file, because a private key is no better than a password. Removing a .pem file from Git must still work.\nExpected result: with core.hooksPath set, git commit -m \"Add key\" with secret.pem staged prints pre-commit: refusing to commit a .env or .pem file. and makes no commit. After --no-verify has let the file in, git rm secret.pem followed by git commit -m \"Stop tracking key\" goes through.\n<details>\n<summary>Answer</summary>\n#!/bin/sh\n# Stop a .env or .pem file from being added to a commit.\nif git diff --cached --name-only --diff-filter=AM | grep -Eq \"\\.(env|pem)$\"; then\necho \"pre-commit: refusing to commit a .env or .pem file.\"\nexit 1\nfi\n-E turns on extended patterns, so (env|pem) works. Keep --diff-filter=AM, or the commit that removes the key is refused too.\n</details>\nQuestion: You write a pre-commit hook in .git/hooks, push your work, and a teammate clones the repository. Does the hook run for them?\nNo, .git is not part of a clone, so they get only the sample hooks\nYes, hooks are committed along with the code\nYes, but only on Windows\nAnswer: 1. No, .git is not part of a clone, so they get only the sample hooks\nExplanation: In our test, a fresh clone had 14 .sample files and core.hooksPath was unset. A tracked folder plus git config core.hooksPath in each clone is how you share hooks.\nQuestion: What does git commit --no-verify skip?\nThe pre-commit and commit-msg hooks for that commit\nThe commit message\nEvery check, including the ones in CI\nAnswer: 1. The pre-commit and commit-msg hooks for that commit\nExplanation: --no-verify only affects hooks on your machine. Checks that run on a server, such as CI, are not affected by a flag you pass locally.\nQuestion: On Windows, you remove the executable bit from .git/hooks/pre-commit with chmod -x. What happened in our test?\nNothing. The hook still ran, because the file system keeps no executable bit\nGit skipped the hook and printed a warning\nGit deleted the hook\nAnswer: 1. Nothing. The hook still ran, because the file system keeps no executable bit\nExplanation: ls -l still showed -rwxr-xr-x afterwards and the hook still refused the commit. The bit matters for teammates on Linux and macOS, so record it with git update-index --chmod=+x.\nWhat we learned\nA hook is a script named after a Git moment (pre-commit, commit-msg) with no extension. A non-zero exit stops the action.\nGit for Windows runs hooks with its bundled shell, from Git Bash or PowerShell. The executable bit does not exist on the file system, but record it with git update-index --chmod=+x for teammates elsewhere.\n.git/hooks is not cloned. Put hooks in a tracked folder and run git config core.hooksPath .githooks once per clone.\ngit commit --no-verify (or -n) skips pre-commit and commit-msg.\nA hook is a friendly early check on one machine. Anything the whole team must obey belongs in CI or on the server.\nA hook that cannot be skipped is not a hook. It is a server.\nPortable export note: 3 raw HTML fragment(s) are shown as literal source because this Markdown export does not execute or preserve raw HTML. View the canonical article (https://mnixblog.netlify.app/hooks-you-will-actually-use/).\nPortable fallback: The authored description of the demo is preserved; the live demonstration remains on the canonical article. Review: Review the authored description of the demo and keep the canonical link for the live version.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/hooks-you-will-actually-use/#ob-discussion)",
      "date_published": "2026-10-27T00:05:00Z",
      "id": "https://mnixblog.netlify.app/hooks-you-will-actually-use/",
      "image": "https://mnixblog.netlify.app/images/hooks-you-will-actually-use/cover.jpg",
      "summary": "Write a pre-commit and a commit-msg hook that run on Windows, share them with the team, and know what a hook can never enforce.",
      "tags": [
        "Git",
        "Hooks",
        "Automation"
      ],
      "title": "Hooks You Will Actually Use.",
      "url": "https://mnixblog.netlify.app/hooks-you-will-actually-use/"
    },
    {
      "content_html": "<p>You need to fix a typo in one documentation page. The repository is large, and <code>git clone</code> is going to download every version of every file anyone ever committed.</p>\n<p>Git has three ways to take less. They sound alike, and they cut different things.</p>\n<ul>\n<li>A <strong>shallow</strong> clone takes fewer commits.</li>\n<li>A <strong>partial</strong> clone takes the commits but leaves out file contents until you need them.</li>\n<li>A <strong>sparse</strong> checkout takes everything, but only puts some folders on your disk.</li>\n</ul>\n<p>We will build a small repository with real history and clone it all three ways. That gives us a table of what each one cost on disk, and the commands that undo each one:</p>\n<pre><code class=\"language-bash\">git clone --depth 1 &lt;url&gt;\ngit clone --filter=blob:none --sparse &lt;url&gt;\ngit sparse-checkout set docs\n</code></pre>\n<aside class=\"ob-callout ob-callout-note\" role=\"note\"><p class=\"ob-callout-title\"><strong>Note</strong></p>\n<p>Every command and output in this article comes from Git 2.45.2 on Windows 11, run in Git Bash. The minimum Git version for each flag was not checked, so if one is missing on your machine, run <code>git --version</code> first. Our stand-in for GitHub is a bare repository in a folder, with 29 commits in which the same 150 KB data file is replaced every time, plus a few text files. Folder paths in the output are shortened to <code>...</code>.</p>\n</aside>\n<h2 id=\"the-cost-of-the-full-clone\">The cost of the full clone<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/big-repositories-shallow-partial-and-sparse/#the-cost-of-the-full-clone\" aria-label=\"Link to this section\">#</a></h2>\n<p>This is the baseline. A normal clone of our stand-in remote:</p>\n<pre><code class=\"language-console\">$ git clone file:///C:/.../hub.git full\n$ git rev-list --count main\n29\n$ git count-objects -vH\n...\nsize-pack: 4.29 MiB\n</code></pre>\n<p><code>size-pack</code> is the size of the packed history on disk. 4.29 MiB for a small test repository is the number to beat.</p>\n<aside class=\"ob-callout ob-callout-warning\" role=\"note\"><p class=\"ob-callout-title\"><strong>Warning</strong></p>\n<p>Notice the <code>file:///</code> at the start of the URL. If you clone a plain folder path, Git ignores <code>--depth</code>, and says so: <code>warning: --depth is ignored in local clones; use file:// instead.</code> You only meet this when practicing with folders on your own disk. A GitHub URL needs nothing special. On Windows, the form is <code>file:///C:/path/to/repo.git</code>.</p>\n</aside>\n<h2 id=\"shallow-fewer-commits\">Shallow: fewer commits<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/big-repositories-shallow-partial-and-sparse/#shallow-fewer-commits\" aria-label=\"Link to this section\">#</a></h2>\n<p><code>--depth 1</code> asks for the latest commit and nothing before it.</p>\n<pre><code class=\"language-console\">$ git clone --depth 1 file:///C:/.../hub.git shallow\nCloning into 'shallow'...\n$ git log --oneline\ne9769cd Change 30\n$ git rev-parse --is-shallow-repository\ntrue\n$ git count-objects -vH\n...\nsize-pack: 154.67 KiB\n</code></pre>\n<p>154.67 KiB instead of 4.29 MiB. The cost is in that <code>git log</code> output: there is one commit. Git remembers where it stopped in a file, <code>.git/shallow</code>, which holds the id of the commit that has its parents cut off.</p>\n<p>That is fine for a build server that only needs to compile the latest version. It is not fine when you want <code>git blame</code>, <code>git log</code> on a file, or <code>git bisect</code>, because the history they need is not there.</p>\n<p>You can get more, or all of it, later:</p>\n<pre><code class=\"language-console\">$ git fetch --depth=5\n$ git log --oneline | wc -l\n5\n$ git fetch --unshallow\n$ git log --oneline | wc -l\n29\n$ git rev-parse --is-shallow-repository\nfalse\n</code></pre>\n<p><code>--depth=5</code> grows the history to five commits. <code>--unshallow</code> fetches the rest and turns the repository back into a normal one.</p>\n<h2 id=\"partial-all-commits-no-file-contents\">Partial: all commits, no file contents<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/big-repositories-shallow-partial-and-sparse/#partial-all-commits-no-file-contents\" aria-label=\"Link to this section\">#</a></h2>\n<p>A <strong>partial clone</strong> keeps the whole commit history but leaves out file contents, which Git calls blobs. <code>--filter=blob:none</code> says: no blobs yet.</p>\n<pre><code class=\"language-console\">$ git clone --filter=blob:none file:///C:/.../hub.git part2\nCloning into 'part2'...\n$ git rev-list --count main\n29\n$ git count-objects -vH\n...\nsize-pack: 176.46 KiB\n</code></pre>\n<p>All 29 commits, and 176.46 KiB on disk. A shallow clone with a single commit and this one are about the same size here, and this one still has the history.</p>\n<p>Where did the files go? Git fetches a blob when something needs it. Checking out <code>main</code> needed the current files, so those came with the clone. An old version of the data file is a different matter:</p>\n<pre><code class=\"language-console\">$ git show HEAD~20:assets/data.txt | head -c 60\n/X/SMgBvhT7YnWhpJhZn6SgfWrgGUQ6Dooe1EmkpO0fQImxUNquFAriLInCn\n$ git count-objects -vH\n...\nsize-pack: 328.08 KiB\n</code></pre>\n<p>Reading a file from 20 commits ago made Git go back to the remote and fetch that one blob. <code>size-pack</code> grew by about 150 KB, which is one copy of the data file. Nothing printed, so you will not see it happen.</p>\n<p>Two things to know before you rely on this:</p>\n<ul>\n<li>The server has to allow filters. With a plain bare repository, Git warned <code>filtering not recognized by server, ignoring</code> and sent everything. After <code>git config uploadpack.allowFilter true</code> on that repository, the filter worked. GitHub supports it.</li>\n<li>The repository now depends on the remote. Our test clone shows why in its config:</li>\n</ul>\n<pre><code class=\"language-console\">$ git config --get-regexp \"remote.origin.(promisor|partialclonefilter)\"\nremote.origin.promisor true\nremote.origin.partialclonefilter blob:none\n</code></pre>\n<p><code>promisor</code> means the remote has promised to supply what is missing. Offline, with an old file you never opened, that promise cannot be kept.</p>\n<h2 id=\"sparse-everything-but-only-some-folders\">Sparse: everything, but only some folders<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/big-repositories-shallow-partial-and-sparse/#sparse-everything-but-only-some-folders\" aria-label=\"Link to this section\">#</a></h2>\n<p>A <strong>sparse checkout</strong> is about your working folder, not about what you downloaded. You tell Git to put only the folders you name on disk.</p>\n<p>The two ideas combine well. <code>--sparse</code> on a clone starts with only the top-level files:</p>\n<pre><code class=\"language-console\">$ git clone --filter=blob:none --sparse file:///C:/.../hub.git sp\n$ ls\nREADME.md\n$ git count-objects -vH\n...\nsize-pack: 24.40 KiB\n</code></pre>\n<p>24.40 KiB. Now add the folder we work in:</p>\n<pre><code class=\"language-console\">$ git sparse-checkout set docs\n$ ls\nREADME.md\ndocs\n$ git sparse-checkout list\ndocs\n</code></pre>\n<p>Git fetched the files in <code>docs</code>, and <code>size-pack</code> rose to 26.89 KiB. The <code>assets</code> and <code>src</code> folders are not on disk.</p>\n<p><code>set</code> replaces the list. <code>add</code> extends it:</p>\n<pre><code class=\"language-console\">$ git sparse-checkout add src\n$ ls\nREADME.md\ndocs\nsrc\n</code></pre>\n<p>In Git 2.45.2, <code>set</code> works in what Git calls <strong>cone mode</strong>: you name whole folders, and everything inside comes with them. <code>git config core.sparseCheckoutCone</code> printed <code>true</code> here.</p>\n<p>Finally, to go back to the whole tree:</p>\n<pre><code class=\"language-console\">$ git sparse-checkout disable\n$ ls\nREADME.md\nassets\ndocs\nsrc\n$ git count-objects -vH\n...\nsize-pack: 179.69 KiB\n</code></pre>\n<p>Git fetched the files it had skipped, and the pack grew from 26.89 KiB to 179.69 KiB.</p>\n<details>\n<summary>What the three left behind, side by side (optional)</summary>\n<p>All from the same remote.</p>\n<table><thead><tr><th>Clone</th><th>Commits</th><th><code>size-pack</code></th><th>Missing</th></tr></thead><tbody>\n<tr><td>Full</td><td>29</td><td>4.29 MiB</td><td>nothing</td></tr>\n<tr><td><code>--depth 1</code></td><td>1</td><td>154.67 KiB</td><td>all older history</td></tr>\n<tr><td><code>--filter=blob:none</code></td><td>29</td><td>176.46 KiB</td><td>old file versions, until read</td></tr>\n<tr><td><code>--filter=blob:none --sparse</code></td><td>29</td><td>24.40 KiB</td><td>files outside the sparse folders</td></tr>\n</tbody></table>\n</details>\n<h2 id=\"what-about-git-lfs\">What about Git LFS?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/big-repositories-shallow-partial-and-sparse/#what-about-git-lfs\" aria-label=\"Link to this section\">#</a></h2>\n<p>Git LFS is a different tool for a different problem: a few very large files, such as models or design files, that you do not want in history at all. It stores a small text pointer in Git and the file itself elsewhere.</p>\n<p>We ran it in a scratch repository, with the file type tracked before the file was added:</p>\n<pre><code class=\"language-console\">$ git lfs install --local\nUpdated Git hooks.\nGit LFS initialized.\n$ git lfs track \"*.bin\"\nTracking \"*.bin\"\n$ cat .gitattributes\n*.bin filter=lfs diff=lfs merge=lfs -text\n$ git lfs ls-files\n52ca684f48 * model.bin\n$ git cat-file -p HEAD:model.bin\nversion https://git-lfs.github.com/spec/v1\noid sha256:52ca684f4830a78b5a958d39f26e5bd276931ae753f2e3ea8476dd9f742be4ee\nsize 100000\n</code></pre>\n<p>The commit holds three lines of text. The 100,000 bytes live in LFS.</p>\n<p>It does not help with a file you already committed. In a second test, we added <code>old.bin</code>, committed it, and only then ran <code>git lfs track \"*.bin\"</code>. <code>git lfs ls-files</code> printed nothing, and <code>git cat-file -s HEAD:old.bin</code> printed <code>100000</code>, the real file. Tracking applies to new commits. History that already has the big file keeps it.</p>\n<p><code>git lfs migrate</code>, the tool for rewriting history, and GitHub's LFS storage limits were not tested here. Look those up before you rely on them.</p>\n<h2 id=\"try-it\">Try it<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/big-repositories-shallow-partial-and-sparse/#try-it\" aria-label=\"Link to this section\">#</a></h2>\n<p>Here is the whole tour in seven commands. The remote is <code>https://github.com/you/big.git</code>. Type them one at a time.</p>\n<figure class=\"ob-demo ob-portable-demo\" data-demo=\"git-practice\"><figcaption class=\"ob-code-title\">Practice terminal · take less of a big repository</figcaption><p>Type these commands in order, in a practice folder:</p>\n<ol>\n<li>Clone only the latest commit of https://github.com/you/big.git. Try <code>git clone --depth 1 https://github.com/you/big.git</code>. A plain <code>git clone</code> downloads all of the history.</li>\n<li>Ask Git whether this clone is shallow. Try <code>git rev-parse --is-shallow-repository</code>. It prints <code>true</code>.</li>\n<li>Get the rest of the history. Try <code>git fetch --unshallow</code>. The repository stops being shallow.</li>\n<li>Clone again, this time with no file contents until needed, and with only the top-level files checked out. Try <code>git clone --filter=blob:none --sparse https://github.com/you/big.git lean</code>. The commits come with it; the folders do not.</li>\n<li>Check out only the docs folder. Try <code>git sparse-checkout set docs</code>. Git fetches the files in docs.</li>\n<li>See which folders are on your disk. Try <code>git sparse-checkout list</code>. It prints <code>docs</code>.</li>\n<li>Go back to the whole tree. Try <code>git sparse-checkout disable</code>. Git fetches what it skipped.</li>\n</ol>\n<p class=\"ob-portable-note\">Try the interactive Git practice terminal on <a href=\"https://mnixblog.netlify.app/big-repositories-shallow-partial-and-sparse/\">the original article</a>.</p></figure>\n<p>Two things worth trying in the terminal: ask for a hint when you are stuck, and add <code>src</code> with <code>git sparse-checkout add src</code> before disabling.</p>\n<p><strong>Challenge: all the history, two folders</strong></p>\n<p>Make a new clone that has every commit but only <code>docs</code> and <code>src</code> on disk. Do it with a filter, not with <code>--depth</code>. Then prove that <code>assets</code> is missing from the disk and not from Git.</p>\n<p><strong>Expected result:</strong> <code>git rev-list --count main</code> prints the same number as in a full clone, <code>ls</code> shows <code>README.md</code>, <code>docs</code> and <code>src</code>, <code>ls assets</code> says <code>No such file or directory</code>, and <code>git ls-files</code> still lists <code>assets/data.txt</code>.</p>\n<details>\n<summary>Answer</summary>\n<pre><code class=\"language-console\">$ git clone --filter=blob:none --sparse file:///C:/.../hub.git lean\n$ cd lean\n$ git sparse-checkout set docs src\n$ git rev-list --count main\n29\n$ ls assets\nls: cannot access 'assets': No such file or directory\n$ git ls-files\nREADME.md\nassets/data.txt\ndocs/guide.md\nsrc/main.c\n</code></pre>\n<p>We ran this on a rebuilt stand-in remote, so the file names are ours. The point is the last command: the index knows <code>assets/data.txt</code>, the disk does not. This clone needed <code>git config uploadpack.allowFilter true</code> on the remote first.</p>\n</details>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>You clone with <code>--depth 1</code> and later run <code>git blame</code> on an old file. What is the problem?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>The older commits it needs were not downloaded</li>\n<li><code>--depth 1</code> also leaves out file contents</li>\n<li>Shallow clones are read-only</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. The older commits it needs were not downloaded</p>\n<p>A shallow clone has the latest commit and no history behind it. <code>git fetch --unshallow</code> brings the history back.</p>\n</div>\n</div>\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>You made a partial clone with <code>--filter=blob:none</code>, and you read an old version of a file for the first time. What does Git do?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>It fetches that one file version from the remote at that moment</li>\n<li>It reports the file as missing</li>\n<li>It had already downloaded every file in the first place</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. It fetches that one file version from the remote at that moment</p>\n<p>In our test, <code>git show HEAD~20:assets/data.txt</code> fetched one blob, and <code>size-pack</code> rose by about 150 KB. That is why the repository keeps the remote marked as <code>promisor</code>.</p>\n</div>\n</div>\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>You track <code>*.bin</code> with Git LFS after <code>old.bin</code> was already committed. Where is <code>old.bin</code> stored?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>In Git history, as before. Tracking applies to new commits</li>\n<li>In LFS, because the pattern now matches</li>\n<li>It is deleted from history</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. In Git history, as before. Tracking applies to new commits</p>\n<p><code>git lfs ls-files</code> printed nothing in our test, and <code>git cat-file -s HEAD:old.bin</code> showed the full 100000 bytes.</p>\n</div>\n</div>\n</section>\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/big-repositories-shallow-partial-and-sparse/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<ul>\n<li><code>git clone --depth 1</code> takes only the latest commit. <code>git fetch --unshallow</code> takes the rest. With local folders, use <code>file:///</code> or Git ignores <code>--depth</code>.</li>\n<li><code>--filter=blob:none</code> takes every commit and fetches file contents when something needs them. The server has to allow it, and the repository depends on the remote afterwards.</li>\n<li><code>git sparse-checkout set &lt;folders&gt;</code> puts only those folders on your disk. <code>add</code> extends the list, <code>list</code> shows it, <code>disable</code> brings back the whole tree.</li>\n<li>A shallow clone cuts history, a partial clone cuts file contents, a sparse checkout cuts what is on disk. They can be combined.</li>\n<li>Git LFS keeps large files out of history from the moment you track them, and does nothing for files already committed.</li>\n</ul>\n<p>Next time you only need one folder, you will not wait for the rest.</p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/big-repositories-shallow-partial-and-sparse/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Big Repositories, Shallow, Partial and Sparse.\nCanonical: https://mnixblog.netlify.app/big-repositories-shallow-partial-and-sparse/\nClone Only the Part You Need, and Know What You Gave Up\nYou need to fix a typo in one documentation page. The repository is large, and git clone is going to download every version of every file anyone ever committed.\nGit has three ways to take less. They sound alike, and they cut different things.\nA shallow clone takes fewer commits.\nA partial clone takes the commits but leaves out file contents until you need them.\nA sparse checkout takes everything, but only puts some folders on your disk.\nWe will build a small repository with real history and clone it all three ways. That gives us a table of what each one cost on disk, and the commands that undo each one:\ngit clone --depth 1 <url>\ngit clone --filter=blob:none --sparse <url>\ngit sparse-checkout set docs\nNote:\nEvery command and output in this article comes from Git 2.45.2 on Windows 11, run in Git Bash. The minimum Git version for each flag was not checked, so if one is missing on your machine, run git --version first. Our stand-in for GitHub is a bare repository in a folder, with 29 commits in which the same 150 KB data file is replaced every time, plus a few text files. Folder paths in the output are shortened to ....\nThe cost of the full clone\nThis is the baseline. A normal clone of our stand-in remote:\n$ git clone file:///C:/.../hub.git full\n$ git rev-list --count main\n29\n$ git count-objects -vH\n...\nsize-pack: 4.29 MiB\nsize-pack is the size of the packed history on disk. 4.29 MiB for a small test repository is the number to beat.\nWarning:\nNotice the file:/// at the start of the URL. If you clone a plain folder path, Git ignores --depth, and says so: warning: --depth is ignored in local clones; use file:// instead. You only meet this when practicing with folders on your own disk. A GitHub URL needs nothing special. On Windows, the form is file:///C:/path/to/repo.git.\nShallow: fewer commits\n--depth 1 asks for the latest commit and nothing before it.\n$ git clone --depth 1 file:///C:/.../hub.git shallow\nCloning into 'shallow'...\n$ git log --oneline\ne9769cd Change 30\n$ git rev-parse --is-shallow-repository\ntrue\n$ git count-objects -vH\n...\nsize-pack: 154.67 KiB\n154.67 KiB instead of 4.29 MiB. The cost is in that git log output: there is one commit. Git remembers where it stopped in a file, .git/shallow, which holds the id of the commit that has its parents cut off.\nThat is fine for a build server that only needs to compile the latest version. It is not fine when you want git blame, git log on a file, or git bisect, because the history they need is not there.\nYou can get more, or all of it, later:\n$ git fetch --depth=5\n$ git log --oneline | wc -l\n5\n$ git fetch --unshallow\n$ git log --oneline | wc -l\n29\n$ git rev-parse --is-shallow-repository\nfalse\n--depth=5 grows the history to five commits. --unshallow fetches the rest and turns the repository back into a normal one.\nPartial: all commits, no file contents\nA partial clone keeps the whole commit history but leaves out file contents, which Git calls blobs. --filter=blob:none says: no blobs yet.\n$ git clone --filter=blob:none file:///C:/.../hub.git part2\nCloning into 'part2'...\n$ git rev-list --count main\n29\n$ git count-objects -vH\n...\nsize-pack: 176.46 KiB\nAll 29 commits, and 176.46 KiB on disk. A shallow clone with a single commit and this one are about the same size here, and this one still has the history.\nWhere did the files go? Git fetches a blob when something needs it. Checking out main needed the current files, so those came with the clone. An old version of the data file is a different matter:\n$ git show HEAD~20:assets/data.txt | head -c 60\n/X/SMgBvhT7YnWhpJhZn6SgfWrgGUQ6Dooe1EmkpO0fQImxUNquFAriLInCn\n$ git count-objects -vH\n...\nsize-pack: 328.08 KiB\nReading a file from 20 commits ago made Git go back to the remote and fetch that one blob. size-pack grew by about 150 KB, which is one copy of the data file. Nothing printed, so you will not see it happen.\nTwo things to know before you rely on this:\nThe server has to allow filters. With a plain bare repository, Git warned filtering not recognized by server, ignoring and sent everything. After git config uploadpack.allowFilter true on that repository, the filter worked. GitHub supports it.\nThe repository now depends on the remote. Our test clone shows why in its config:\n$ git config --get-regexp \"remote.origin.(promisor|partialclonefilter)\"\nremote.origin.promisor true\nremote.origin.partialclonefilter blob:none\npromisor means the remote has promised to supply what is missing. Offline, with an old file you never opened, that promise cannot be kept.\nSparse: everything, but only some folders\nA sparse checkout is about your working folder, not about what you downloaded. You tell Git to put only the folders you name on disk.\nThe two ideas combine well. --sparse on a clone starts with only the top-level files:\n$ git clone --filter=blob:none --sparse file:///C:/.../hub.git sp\n$ ls\nREADME.md\n$ git count-objects -vH\n...\nsize-pack: 24.40 KiB\n24.40 KiB. Now add the folder we work in:\n$ git sparse-checkout set docs\n$ ls\nREADME.md\ndocs\n$ git sparse-checkout list\ndocs\nGit fetched the files in docs, and size-pack rose to 26.89 KiB. The assets and src folders are not on disk.\nset replaces the list. add extends it:\n$ git sparse-checkout add src\n$ ls\nREADME.md\ndocs\nsrc\nIn Git 2.45.2, set works in what Git calls cone mode: you name whole folders, and everything inside comes with them. git config core.sparseCheckoutCone printed true here.\nFinally, to go back to the whole tree:\n$ git sparse-checkout disable\n$ ls\nREADME.md\nassets\ndocs\nsrc\n$ git count-objects -vH\n...\nsize-pack: 179.69 KiB\nGit fetched the files it had skipped, and the pack grew from 26.89 KiB to 179.69 KiB.\n<details>\n<summary>What the three left behind, side by side (optional)</summary>\nAll from the same remote.\nClone\nCommits\nsize-pack\nMissing\nFull\n29\n4.29 MiB\nnothing\n--depth 1\n1\n154.67 KiB\nall older history\n--filter=blob:none\n29\n176.46 KiB\nold file versions, until read\n--filter=blob:none --sparse\n29\n24.40 KiB\nfiles outside the sparse folders\n</details>\nWhat about Git LFS?\nGit LFS is a different tool for a different problem: a few very large files, such as models or design files, that you do not want in history at all. It stores a small text pointer in Git and the file itself elsewhere.\nWe ran it in a scratch repository, with the file type tracked before the file was added:\n$ git lfs install --local\nUpdated Git hooks.\nGit LFS initialized.\n$ git lfs track \"*.bin\"\nTracking \"*.bin\"\n$ cat .gitattributes\n*.bin filter=lfs diff=lfs merge=lfs -text\n$ git lfs ls-files\n52ca684f48 * model.bin\n$ git cat-file -p HEAD:model.bin\nversion https://git-lfs.github.com/spec/v1\noid sha256:52ca684f4830a78b5a958d39f26e5bd276931ae753f2e3ea8476dd9f742be4ee\nsize 100000\nThe commit holds three lines of text. The 100,000 bytes live in LFS.\nIt does not help with a file you already committed. In a second test, we added old.bin, committed it, and only then ran git lfs track \"*.bin\". git lfs ls-files printed nothing, and git cat-file -s HEAD:old.bin printed 100000, the real file. Tracking applies to new commits. History that already has the big file keeps it.\ngit lfs migrate, the tool for rewriting history, and GitHub's LFS storage limits were not tested here. Look those up before you rely on them.\nTry it\nHere is the whole tour in seven commands. The remote is https://github.com/you/big.git. Type them one at a time.\nPractice terminal · take less of a big repository\nType these commands in order, in a practice folder:\nClone only the latest commit of https://github.com/you/big.git. Try git clone --depth 1 https://github.com/you/big.git. A plain git clone downloads all of the history.\nAsk Git whether this clone is shallow. Try git rev-parse --is-shallow-repository. It prints true.\nGet the rest of the history. Try git fetch --unshallow. The repository stops being shallow.\nClone again, this time with no file contents until needed, and with only the top-level files checked out. Try git clone --filter=blob:none --sparse https://github.com/you/big.git lean. The commits come with it; the folders do not.\nCheck out only the docs folder. Try git sparse-checkout set docs. Git fetches the files in docs.\nSee which folders are on your disk. Try git sparse-checkout list. It prints docs.\nGo back to the whole tree. Try git sparse-checkout disable. Git fetches what it skipped.\nTry the interactive Git practice terminal on the canonical article (https://mnixblog.netlify.app/big-repositories-shallow-partial-and-sparse/).\nTwo things worth trying in the terminal: ask for a hint when you are stuck, and add src with git sparse-checkout add src before disabling.\nChallenge: all the history, two folders\nMake a new clone that has every commit but only docs and src on disk. Do it with a filter, not with --depth. Then prove that assets is missing from the disk and not from Git.\nExpected result: git rev-list --count main prints the same number as in a full clone, ls shows README.md, docs and src, ls assets says No such file or directory, and git ls-files still lists assets/data.txt.\n<details>\n<summary>Answer</summary>\n$ git clone --filter=blob:none --sparse file:///C:/.../hub.git lean\n$ cd lean\n$ git sparse-checkout set docs src\n$ git rev-list --count main\n29\n$ ls assets\nls: cannot access 'assets': No such file or directory\n$ git ls-files\nREADME.md\nassets/data.txt\ndocs/guide.md\nsrc/main.c\nWe ran this on a rebuilt stand-in remote, so the file names are ours. The point is the last command: the index knows assets/data.txt, the disk does not. This clone needed git config uploadpack.allowFilter true on the remote first.\n</details>\nQuestion: You clone with --depth 1 and later run git blame on an old file. What is the problem?\nThe older commits it needs were not downloaded\n--depth 1 also leaves out file contents\nShallow clones are read-only\nAnswer: 1. The older commits it needs were not downloaded\nExplanation: A shallow clone has the latest commit and no history behind it. git fetch --unshallow brings the history back.\nQuestion: You made a partial clone with --filter=blob:none, and you read an old version of a file for the first time. What does Git do?\nIt fetches that one file version from the remote at that moment\nIt reports the file as missing\nIt had already downloaded every file in the first place\nAnswer: 1. It fetches that one file version from the remote at that moment\nExplanation: In our test, git show HEAD~20:assets/data.txt fetched one blob, and size-pack rose by about 150 KB. That is why the repository keeps the remote marked as promisor.\nQuestion: You track *.bin with Git LFS after old.bin was already committed. Where is old.bin stored?\nIn Git history, as before. Tracking applies to new commits\nIn LFS, because the pattern now matches\nIt is deleted from history\nAnswer: 1. In Git history, as before. Tracking applies to new commits\nExplanation: git lfs ls-files printed nothing in our test, and git cat-file -s HEAD:old.bin showed the full 100000 bytes.\nWhat we learned\ngit clone --depth 1 takes only the latest commit. git fetch --unshallow takes the rest. With local folders, use file:/// or Git ignores --depth.\n--filter=blob:none takes every commit and fetches file contents when something needs them. The server has to allow it, and the repository depends on the remote afterwards.\ngit sparse-checkout set <folders> puts only those folders on your disk. add extends the list, list shows it, disable brings back the whole tree.\nA shallow clone cuts history, a partial clone cuts file contents, a sparse checkout cuts what is on disk. They can be combined.\nGit LFS keeps large files out of history from the moment you track them, and does nothing for files already committed.\nNext time you only need one folder, you will not wait for the rest.\nPortable export note: 6 raw HTML fragment(s) are shown as literal source because this Markdown export does not execute or preserve raw HTML. View the canonical article (https://mnixblog.netlify.app/big-repositories-shallow-partial-and-sparse/).\nPortable fallback: The authored description of the demo is preserved; the live demonstration remains on the canonical article. Review: Review the authored description of the demo and keep the canonical link for the live version.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/big-repositories-shallow-partial-and-sparse/#ob-discussion)",
      "date_published": "2026-10-27T00:04:00Z",
      "id": "https://mnixblog.netlify.app/big-repositories-shallow-partial-and-sparse/",
      "image": "https://mnixblog.netlify.app/images/big-repositories-shallow-partial-and-sparse/cover.jpg",
      "summary": "Clone a large repository with only its latest commit, without file contents, or with only the folders you use, and know what each costs.",
      "tags": [
        "Git",
        "Performance",
        "GitHub"
      ],
      "title": "Big Repositories, Shallow, Partial and Sparse.",
      "url": "https://mnixblog.netlify.app/big-repositories-shallow-partial-and-sparse/"
    },
    {
      "content_html": "<p>You are reading <code>checkout.js</code>, and one line puzzles you.</p>\n<pre><code class=\"language-js\">return post(order, TIMEOUT_MS);\n</code></pre>\n<p>Why is there no retry here? You ask Git who wrote it, and <code>git blame</code> says Ravi, on April 20. His commit is called <code>Reformat with 2-space indent</code>. He did not write that line. He re-indented it.</p>\n<p>Old code raises four questions: who wrote this line, when did this word appear, when did it vanish, and how did this function change. Git can answer all four, and it gets the right answer for that line:</p>\n<pre><code class=\"language-text\">9dcdb28d (Ada  2026-04-06 10:00:00 -0400 8)   return post(order, TIMEOUT_MS);\n</code></pre>\n<h2 id=\"a-history-to-dig-through\">A history to dig through<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/dig-through-history/#a-history-to-dig-through\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our scratch project is one file with seven commits from two made-up authors, Ada and Ravi. The output in this article comes from Git 2.45.2 on Windows 11, run in Git Bash.</p>\n<pre><code class=\"language-console\">$ git log --oneline\nd28ffe2 Round tax\n5c04fdc Reformat with 2-space indent\n9dcdb28 Stop retrying orders\nbf249d9 Give slow carriers more time\n4a90109 Add tax\n7142a9f Retry failed orders\n5c64e3c Add checkout total\n</code></pre>\n<p>Here is the file at the end:</p>\n<pre><code class=\"language-js\">const TIMEOUT_MS = 60000;\n\nfunction total(items) {\n  return items.reduce((sum, item) =&gt; sum + item.price, 0);\n}\n\nfunction send(order) {\n  return post(order, TIMEOUT_MS);\n}\n\nfunction addTax(amount) {\n  return Math.round(amount * 1.2);\n}\n</code></pre>\n<details>\n<summary>Build the history yourself (Git Bash, optional)</summary>\n<p>Your hashes may differ from the ones printed here, because the dates use your time zone, and so will the time zone offsets in the blame output. The authors, messages and dates are set on purpose.</p>\n<pre><code class=\"language-bash\">git init -b main digging &amp;&amp; cd digging\nc() {\n  git add .\n  GIT_AUTHOR_NAME=\"$2\" GIT_AUTHOR_EMAIL=\"${2,,}@example.com\" \\\n  GIT_COMMITTER_NAME=\"$2\" GIT_COMMITTER_EMAIL=\"${2,,}@example.com\" \\\n  GIT_AUTHOR_DATE=\"$3\" GIT_COMMITTER_DATE=\"$3\" git commit -m \"$1\"\n}\ncat &gt; checkout.js &lt;&lt;'EOF'\nconst TIMEOUT_MS = 30000;\n\nfunction total(items) {\n    return items.reduce((sum, item) =&gt; sum + item.price, 0);\n}\nEOF\nc \"Add checkout total\" Ada 2026-02-02T10:00:00\ncat &gt; checkout.js &lt;&lt;'EOF'\nconst TIMEOUT_MS = 30000;\nconst RETRY_LIMIT = 3;\n\nfunction total(items) {\n    return items.reduce((sum, item) =&gt; sum + item.price, 0);\n}\n\nfunction send(order) {\n    for (let i = 0; i &lt; RETRY_LIMIT; i++) {\n        if (post(order, TIMEOUT_MS)) return true;\n    }\n    return false;\n}\nEOF\nc \"Retry failed orders\" Ravi 2026-02-16T10:00:00\ncat &gt;&gt; checkout.js &lt;&lt;'EOF'\n\nfunction addTax(amount) {\n    return amount * 1.2;\n}\nEOF\nc \"Add tax\" Ada 2026-03-02T10:00:00\nsed -i 's/TIMEOUT_MS = 30000/TIMEOUT_MS = 60000/' checkout.js\nc \"Give slow carriers more time\" Ravi 2026-03-16T10:00:00\ncat &gt; checkout.js &lt;&lt;'EOF'\nconst TIMEOUT_MS = 60000;\n\nfunction total(items) {\n    return items.reduce((sum, item) =&gt; sum + item.price, 0);\n}\n\nfunction send(order) {\n    return post(order, TIMEOUT_MS);\n}\n\nfunction addTax(amount) {\n    return amount * 1.2;\n}\nEOF\nc \"Stop retrying orders\" Ada 2026-04-06T10:00:00\nsed -i 's/^    /  /' checkout.js\nc \"Reformat with 2-space indent\" Ravi 2026-04-20T10:00:00\nsed -i 's/amount \\* 1.2/Math.round(amount * 1.2)/' checkout.js\nc \"Round tax\" Ada 2026-05-04T10:00:00\n</code></pre>\n</details>\n<h2 id=\"who-changed-this-line\">Who changed this line<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/dig-through-history/#who-changed-this-line\" aria-label=\"Link to this section\">#</a></h2>\n<p><code>git blame</code> prints each line of a file next to the commit that last changed it. The whole file is too much, so <code>-L</code> limits it to a range. Lines 7 to 9 hold <code>send</code>:</p>\n<pre><code class=\"language-console\">$ git blame -L 7,9 checkout.js\n7142a9f1 (Ravi 2026-02-16 10:00:00 -0500 7) function send(order) {\n5c04fdc4 (Ravi 2026-04-20 10:00:00 -0400 8)   return post(order, TIMEOUT_MS);\n7142a9f1 (Ravi 2026-02-16 10:00:00 -0500 9) }\n</code></pre>\n<p>Each row has the start of the commit hash, the author, the date and the line number, then the line itself.</p>\n<p>Line 8 belongs to <code>5c04fdc</code>, the reformat. Blame names the <strong>last</strong> commit that touched the line. It does not look through whitespace or style changes unless we ask.</p>\n<p>We can also give a function name instead of numbers:</p>\n<pre><code class=\"language-console\">$ git blame -L :send checkout.js\n7142a9f1 (Ravi 2026-02-16 10:00:00 -0500  7) function send(order) {\n5c04fdc4 (Ravi 2026-04-20 10:00:00 -0400  8)   return post(order, TIMEOUT_MS);\n7142a9f1 (Ravi 2026-02-16 10:00:00 -0500  9) }\n4a901098 (Ada  2026-03-02 10:00:00 -0500 10) \n</code></pre>\n<p>Git found <code>send</code> by looking for lines that look like the start of a function, so this works best on languages it recognizes well. It printed lines 7 to 10, which includes the blank line after the function.</p>\n<h2 id=\"look-past-the-reformat\">Look past the reformat<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/dig-through-history/#look-past-the-reformat\" aria-label=\"Link to this section\">#</a></h2>\n<p>There are two ways to get to the real author.</p>\n<p>If the commit only changed whitespace, <code>-w</code> tells blame to ignore whitespace when it compares lines:</p>\n<pre><code class=\"language-console\">$ git blame -w -L 7,9 checkout.js\n7142a9f1 (Ravi 2026-02-16 10:00:00 -0500 7) function send(order) {\n9dcdb28d (Ada  2026-04-06 10:00:00 -0400 8)   return post(order, TIMEOUT_MS);\n7142a9f1 (Ravi 2026-02-16 10:00:00 -0500 9) }\n</code></pre>\n<p>If the commit changed more than whitespace, such as a code formatter that rewrote quotes and wrapped lines, name the commit with <code>--ignore-rev</code>. Blame then acts as if that commit never happened:</p>\n<pre><code class=\"language-console\">$ git blame --ignore-rev 5c04fdc -L 7,9 checkout.js\n7142a9f1 (Ravi 2026-02-16 10:00:00 -0500 7) function send(order) {\n9dcdb28d (Ada  2026-04-06 10:00:00 -0400 8)   return post(order, TIMEOUT_MS);\n7142a9f1 (Ravi 2026-02-16 10:00:00 -0500 9) }\n</code></pre>\n<p>Line 8 is Ada's, from <code>Stop retrying orders</code>. That answers our question.</p>\n<h3 id=\"write-it-down-for-the-team\">Write it down for the team<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/dig-through-history/#write-it-down-for-the-team\" aria-label=\"Link to this section\">#</a></h3>\n<p>Nobody wants to retype that. Put the commit in a file, one <strong>full</strong> hash per line, and point Git at it:</p>\n<pre><code class=\"language-bash\">git log --format=%H --grep=Reformat &gt; .git-blame-ignore-revs\ngit config blame.ignoreRevsFile .git-blame-ignore-revs\n</code></pre>\n<p>Now plain <code>git blame -L 7,9 checkout.js</code> gives the answer above. The file takes full hashes. We first tried the short one, <code>5c04fdc</code>, and Git answered:</p>\n<pre><code class=\"language-text\">fatal: invalid object name: 5c04fdc\n</code></pre>\n<p>The manual says the same: \"one unabbreviated object name per line.\" Lines beginning with <code>#</code> are ignored, so a comment saying what each commit was is a good habit.</p>\n<p>Commit <code>.git-blame-ignore-revs</code> so everyone gets the list. The <code>git config</code> line is not shared by cloning, so each person runs it once.</p>\n<h2 id=\"when-did-this-string-appear-or-vanish\">When did this string appear, or vanish<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/dig-through-history/#when-did-this-string-appear-or-vanish\" aria-label=\"Link to this section\">#</a></h2>\n<p>Blame is about lines that exist now. For a thing that is gone, we search the changes instead. <code>-S</code> takes a string and lists the commits that changed <strong>how many times it appears</strong> in the code:</p>\n<pre><code class=\"language-console\">$ git log -S RETRY_LIMIT --oneline\n9dcdb28 Stop retrying orders\n7142a9f Retry failed orders\n</code></pre>\n<p>The oldest line is the commit that added <code>RETRY_LIMIT</code>, and the newest is the one that removed it. Add <code>-p</code> to see the change itself. This is the removal:</p>\n<pre><code class=\"language-console\">$ git log -S RETRY_LIMIT --oneline -p -1\n9dcdb28 Stop retrying orders\ndiff --git a/checkout.js b/checkout.js\nindex e14e8d1..08bde68 100644\n--- a/checkout.js\n+++ b/checkout.js\n@@ -1,15 +1,11 @@\n const TIMEOUT_MS = 60000;\n-const RETRY_LIMIT = 3;\n \n function total(items) {\n     return items.reduce((sum, item) =&gt; sum + item.price, 0);\n }\n \n function send(order) {\n-    for (let i = 0; i &lt; RETRY_LIMIT; i++) {\n-        if (post(order, TIMEOUT_MS)) return true;\n-    }\n-    return false;\n+    return post(order, TIMEOUT_MS);\n }\n \n function addTax(amount) {\n</code></pre>\n<p>That is the commit we wanted all along, found from a different direction.</p>\n<h3 id=\"s-counts-g-reads\">-S counts, -G reads<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/dig-through-history/#s-counts-g-reads\" aria-label=\"Link to this section\">#</a></h3>\n<p><code>TIMEOUT_MS</code> went from 30000 to 60000 in <code>bf249d9</code>. Search for it with <code>-S</code>:</p>\n<pre><code class=\"language-console\">$ git log -S TIMEOUT_MS --oneline\n7142a9f Retry failed orders\n5c64e3c Add checkout total\n</code></pre>\n<p><code>bf249d9</code> is missing. That commit changed the value, but the word <code>TIMEOUT_MS</code> appears the same number of times before and after, so <code>-S</code> does not count it. <code>-G</code> takes a regular expression and looks at the changed lines instead:</p>\n<pre><code class=\"language-console\">$ git log -G \"TIMEOUT_MS = [0-9]+\" --oneline\nbf249d9 Give slow carriers more time\n5c64e3c Add checkout total\n</code></pre>\n<p>There it is. Use <code>-S</code> when you want the commit where something was added or removed, and <code>-G</code> when you want any commit that touched a line matching a pattern. To search for a specific value, search for the value: <code>git log -S\"TIMEOUT_MS = 30000\" --oneline</code> printed <code>bf249d9</code> and <code>5c64e3c</code>.</p>\n<h2 id=\"how-did-this-function-change\">How did this function change<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/dig-through-history/#how-did-this-function-change\" aria-label=\"Link to this section\">#</a></h2>\n<p><code>git log -L</code> follows a range of lines, or a function, through every commit that touched it. It prints each change:</p>\n<pre><code class=\"language-console\">$ git log -L :addTax:checkout.js\ncommit d28ffe22c9294f7a36ffc43e640fdc1ba515ffe1\nAuthor: Ada &lt;ada@example.com&gt;\nDate:   Mon May 4 10:00:00 2026 -0400\n\n    Round tax\n\ndiff --git a/checkout.js b/checkout.js\n--- a/checkout.js\n+++ b/checkout.js\n@@ -11,3 +11,3 @@\n function addTax(amount) {\n-  return amount * 1.2;\n+  return Math.round(amount * 1.2);\n }\n\ncommit 5c04fdc495ac4dca9de5ef4fcaf59f427dc2fd92\nAuthor: Ravi &lt;ravi@example.com&gt;\nDate:   Mon Apr 20 10:00:00 2026 -0400\n\n    Reformat with 2-space indent\n...\n</code></pre>\n<p>The third commit, <code>Add tax</code>, is the original. The middle one is the reformat again, which <code>-L</code> does not skip. That is the whole history of one function, in one command.</p>\n<h2 id=\"shape-the-output\">Shape the output<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/dig-through-history/#shape-the-output\" aria-label=\"Link to this section\">#</a></h2>\n<p>The default <code>git log</code> is a lot of text. <code>--format</code> prints what we choose. These placeholders are the ones used most:</p>\n<table><thead><tr><th>Placeholder</th><th>Prints</th></tr></thead><tbody>\n<tr><td><code>%h</code></td><td>Short hash</td></tr>\n<tr><td><code>%H</code></td><td>Full hash</td></tr>\n<tr><td><code>%an</code>, <code>%ae</code></td><td>Author name and email</td></tr>\n<tr><td><code>%ad</code></td><td>Author date, in the style <code>--date</code> sets</td></tr>\n<tr><td><code>%s</code></td><td>Subject line</td></tr>\n<tr><td><code>%b</code></td><td>Body of the message</td></tr>\n<tr><td><code>%d</code></td><td>Branch and tag names</td></tr>\n<tr><td><code>%n</code></td><td>A new line</td></tr>\n</tbody></table>\n<pre><code class=\"language-console\">$ git log --format=\"%h %an %ad %s\" --date=short\nd28ffe2 Ada 2026-05-04 Round tax\n5c04fdc Ravi 2026-04-20 Reformat with 2-space indent\n9dcdb28 Ada 2026-04-06 Stop retrying orders\nbf249d9 Ravi 2026-03-16 Give slow carriers more time\n4a90109 Ada 2026-03-02 Add tax\n7142a9f Ravi 2026-02-16 Retry failed orders\n5c64e3c Ada 2026-02-02 Add checkout total\n</code></pre>\n<p>Filters narrow the list before it prints:</p>\n<table><thead><tr><th>Filter</th><th>Example</th><th>Result in our history</th></tr></thead><tbody>\n<tr><td><code>--since</code></td><td><code>git log --since=2026-04-01 --oneline</code></td><td><code>d28ffe2</code>, <code>5c04fdc</code>, <code>9dcdb28</code></td></tr>\n<tr><td><code>--author</code></td><td><code>git log --author=Ravi --oneline</code></td><td><code>5c04fdc</code>, <code>bf249d9</code>, <code>7142a9f</code></td></tr>\n<tr><td><code>--grep</code></td><td><code>git log --grep=tax -i --oneline</code></td><td><code>d28ffe2</code>, <code>4a90109</code></td></tr>\n</tbody></table>\n<p><code>--grep</code> searches commit messages, and <code>-i</code> ignores case. For comparison, <code>-S</code> and <code>-G</code> search the changes. And <code>git log --oneline --graph --decorate</code> draws the branches. Ours is a straight line, so it is seven stars in a column.</p>\n<h2 id=\"try-it\">Try it<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/dig-through-history/#try-it\" aria-label=\"Link to this section\">#</a></h2>\n<p>The whole hunt takes six commands.</p>\n<figure class=\"ob-demo ob-portable-demo\" data-demo=\"git-practice\"><figcaption class=\"ob-code-title\">Practice terminal · dig through history</figcaption><p>Type these commands in order, in a practice folder:</p>\n<ol>\n<li>Blame lines 7 to 9 of checkout.js. Try <code>git blame -L 7,9 checkout.js</code>. Line 8 is blamed on the reformat, <code>5c04fdc</code>.</li>\n<li>Blame the same lines, ignoring the reformat commit. Try <code>git blame --ignore-rev 5c04fdc -L 7,9 checkout.js</code>. <code>git blame -w -L 7,9 checkout.js</code> works too, because that commit only changed whitespace. Line 8 now belongs to Ada, <code>9dcdb28</code>.</li>\n<li>Find the commits that added or removed RETRY_LIMIT. Try <code>git log -S RETRY_LIMIT --oneline</code>.</li>\n<li>Find the commit that changed the value of TIMEOUT_MS, which <code>-S</code> misses. Try <code>git log -G \"TIMEOUT_MS = [0-9]+\" --oneline</code>.</li>\n<li>Follow the addTax function through history. Try <code>git log -L :addTax:checkout.js</code>.</li>\n<li>Print the hash, author, date and subject of every commit. Try <code>git log --format=\"%h %an %ad %s\" --date=short</code>.</li>\n</ol>\n<p class=\"ob-portable-note\">Try the interactive Git practice terminal on <a href=\"https://mnixblog.netlify.app/dig-through-history/\">the original article</a>.</p></figure>\n<p>Two things worth trying: type <code>git log -S TIMEOUT_MS --oneline</code> at step 4 and see why it is not the answer, and ask for a hint at step 2.</p>\n<p><strong>Challenge: one command, one line</strong></p>\n<p>The retry loop ended with <code>return false;</code>, and <code>Stop retrying orders</code> removed it. Print who removed that line and on what date, as one line in the form <code>Ada 2026-04-06</code>. Use one <code>git log</code> command, and do not type a commit hash.</p>\n<p><strong>Expected result:</strong> exactly <code>Ada 2026-04-06</code>, and nothing else.</p>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p><code>-S</code> lists the commit that added the string and the commit that removed it. Git prints the newest first.</p>\n</aside>\n<details>\n<summary>Answer</summary>\n<pre><code class=\"language-console\">$ git log -S\"return false;\" -1 --format=\"%an %ad\" --date=short\nAda 2026-04-06\n</code></pre>\n<p>Without <code>-1</code>, the same command also prints Ravi's <code>Retry failed orders</code>, the commit that added the line.</p>\n</details>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong><code>TIMEOUT_MS</code> changes from 30000 to 60000 in one commit. The word <code>TIMEOUT_MS</code> appears the same number of times before and after. Which command lists that commit?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>git log -G \"TIMEOUT_MS = [0-9]+\" --oneline</code></li>\n<li><code>git log -S TIMEOUT_MS --oneline</code></li>\n<li>Both of them</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. <code>git log -G \"TIMEOUT_MS = [0-9]+\" --oneline</code></p>\n<p><code>-S</code> lists commits that change the number of times a string appears, and here it did not change. <code>-G</code> looks at the changed lines themselves.</p>\n</div>\n</div>\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>A <code>.git-blame-ignore-revs</code> file lists the commits that blame should skip. How must each commit be written?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>As its full hash, one per line</li>\n<li>As a short hash of seven characters</li>\n<li>As the commit message</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. As its full hash, one per line</p>\n<p>Git's manual asks for one unabbreviated object name per line. In our run, the seven-character hash gave <code>fatal: invalid object name</code>.</p>\n</div>\n</div>\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong><code>git blame</code> shows a line next to a commit. Which commit is it?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>The most recent one that changed that line</li>\n<li>The first one that ever added that line</li>\n<li>The one that last touched the file</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. The most recent one that changed that line</p>\n<p>Line 8 was written by Ada but blamed on Ravi's reformat, because his was the last commit to change it.</p>\n</div>\n</div>\n</section>\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/dig-through-history/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<ul>\n<li><code>git blame -L 7,9 file</code> shows who last changed those lines. <code>-L :send</code> takes a function name.</li>\n<li>Blame names the last commit that touched a line, so a reformat hides the author.</li>\n<li><code>git blame -w</code> ignores whitespace changes. <code>--ignore-rev &lt;commit&gt;</code> ignores a whole commit.</li>\n<li><code>.git-blame-ignore-revs</code> lists full hashes, and <code>git config blame.ignoreRevsFile</code> points Git at it. The config is per clone.</li>\n<li><code>git log -S text</code> finds commits that change how many times the text appears. <code>-G regex</code> finds commits whose changed lines match.</li>\n<li><code>git log -L :function:file</code> shows every change to a function.</li>\n<li><code>--format</code>, <code>--date</code>, <code>--since</code>, <code>--author</code> and <code>--grep</code> shape what <code>git log</code> prints.</li>\n</ul>\n<p>Blame, the pickaxe and <code>-L</code> tell us which commit to look at. The commit message and the diff tell us why, and <code>git show 9dcdb28</code> prints both.</p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/dig-through-history/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Dig Through History.\nCanonical: https://mnixblog.netlify.app/dig-through-history/\nBlame, the Pickaxe, and the Reformat That Hid the Author\nYou are reading checkout.js, and one line puzzles you.\nreturn post(order, TIMEOUT_MS);\nWhy is there no retry here? You ask Git who wrote it, and git blame says Ravi, on April 20. His commit is called Reformat with 2-space indent. He did not write that line. He re-indented it.\nOld code raises four questions: who wrote this line, when did this word appear, when did it vanish, and how did this function change. Git can answer all four, and it gets the right answer for that line:\n9dcdb28d (Ada  2026-04-06 10:00:00 -0400 8)   return post(order, TIMEOUT_MS);\nA history to dig through\nOur scratch project is one file with seven commits from two made-up authors, Ada and Ravi. The output in this article comes from Git 2.45.2 on Windows 11, run in Git Bash.\n$ git log --oneline\nd28ffe2 Round tax\n5c04fdc Reformat with 2-space indent\n9dcdb28 Stop retrying orders\nbf249d9 Give slow carriers more time\n4a90109 Add tax\n7142a9f Retry failed orders\n5c64e3c Add checkout total\nHere is the file at the end:\nconst TIMEOUT_MS = 60000;\nfunction total(items) {\nreturn items.reduce((sum, item) => sum + item.price, 0);\n}\nfunction send(order) {\nreturn post(order, TIMEOUT_MS);\n}\nfunction addTax(amount) {\nreturn Math.round(amount * 1.2);\n}\n<details>\n<summary>Build the history yourself (Git Bash, optional)</summary>\nYour hashes may differ from the ones printed here, because the dates use your time zone, and so will the time zone offsets in the blame output. The authors, messages and dates are set on purpose.\ngit init -b main digging && cd digging\nc() {\ngit add .\nGIT_AUTHOR_NAME=\"$2\" GIT_AUTHOR_EMAIL=\"${2,,}@example.com\" \\\nGIT_COMMITTER_NAME=\"$2\" GIT_COMMITTER_EMAIL=\"${2,,}@example.com\" \\\nGIT_AUTHOR_DATE=\"$3\" GIT_COMMITTER_DATE=\"$3\" git commit -m \"$1\"\n}\ncat > checkout.js <<'EOF'\nconst TIMEOUT_MS = 30000;\nfunction total(items) {\nreturn items.reduce((sum, item) => sum + item.price, 0);\n}\nEOF\nc \"Add checkout total\" Ada 2026-02-02T10:00:00\ncat > checkout.js <<'EOF'\nconst TIMEOUT_MS = 30000;\nconst RETRY_LIMIT = 3;\nfunction total(items) {\nreturn items.reduce((sum, item) => sum + item.price, 0);\n}\nfunction send(order) {\nfor (let i = 0; i < RETRY_LIMIT; i++) {\nif (post(order, TIMEOUT_MS)) return true;\n}\nreturn false;\n}\nEOF\nc \"Retry failed orders\" Ravi 2026-02-16T10:00:00\ncat >> checkout.js <<'EOF'\nfunction addTax(amount) {\nreturn amount * 1.2;\n}\nEOF\nc \"Add tax\" Ada 2026-03-02T10:00:00\nsed -i 's/TIMEOUT_MS = 30000/TIMEOUT_MS = 60000/' checkout.js\nc \"Give slow carriers more time\" Ravi 2026-03-16T10:00:00\ncat > checkout.js <<'EOF'\nconst TIMEOUT_MS = 60000;\nfunction total(items) {\nreturn items.reduce((sum, item) => sum + item.price, 0);\n}\nfunction send(order) {\nreturn post(order, TIMEOUT_MS);\n}\nfunction addTax(amount) {\nreturn amount * 1.2;\n}\nEOF\nc \"Stop retrying orders\" Ada 2026-04-06T10:00:00\nsed -i 's/^    /  /' checkout.js\nc \"Reformat with 2-space indent\" Ravi 2026-04-20T10:00:00\nsed -i 's/amount \\* 1.2/Math.round(amount * 1.2)/' checkout.js\nc \"Round tax\" Ada 2026-05-04T10:00:00\n</details>\nWho changed this line\ngit blame prints each line of a file next to the commit that last changed it. The whole file is too much, so -L limits it to a range. Lines 7 to 9 hold send:\n$ git blame -L 7,9 checkout.js\n7142a9f1 (Ravi 2026-02-16 10:00:00 -0500 7) function send(order) {\n5c04fdc4 (Ravi 2026-04-20 10:00:00 -0400 8)   return post(order, TIMEOUT_MS);\n7142a9f1 (Ravi 2026-02-16 10:00:00 -0500 9) }\nEach row has the start of the commit hash, the author, the date and the line number, then the line itself.\nLine 8 belongs to 5c04fdc, the reformat. Blame names the last commit that touched the line. It does not look through whitespace or style changes unless we ask.\nWe can also give a function name instead of numbers:\n$ git blame -L :send checkout.js\n7142a9f1 (Ravi 2026-02-16 10:00:00 -0500  7) function send(order) {\n5c04fdc4 (Ravi 2026-04-20 10:00:00 -0400  8)   return post(order, TIMEOUT_MS);\n7142a9f1 (Ravi 2026-02-16 10:00:00 -0500  9) }\n4a901098 (Ada  2026-03-02 10:00:00 -0500 10)\nGit found send by looking for lines that look like the start of a function, so this works best on languages it recognizes well. It printed lines 7 to 10, which includes the blank line after the function.\nLook past the reformat\nThere are two ways to get to the real author.\nIf the commit only changed whitespace, -w tells blame to ignore whitespace when it compares lines:\n$ git blame -w -L 7,9 checkout.js\n7142a9f1 (Ravi 2026-02-16 10:00:00 -0500 7) function send(order) {\n9dcdb28d (Ada  2026-04-06 10:00:00 -0400 8)   return post(order, TIMEOUT_MS);\n7142a9f1 (Ravi 2026-02-16 10:00:00 -0500 9) }\nIf the commit changed more than whitespace, such as a code formatter that rewrote quotes and wrapped lines, name the commit with --ignore-rev. Blame then acts as if that commit never happened:\n$ git blame --ignore-rev 5c04fdc -L 7,9 checkout.js\n7142a9f1 (Ravi 2026-02-16 10:00:00 -0500 7) function send(order) {\n9dcdb28d (Ada  2026-04-06 10:00:00 -0400 8)   return post(order, TIMEOUT_MS);\n7142a9f1 (Ravi 2026-02-16 10:00:00 -0500 9) }\nLine 8 is Ada's, from Stop retrying orders. That answers our question.\nWrite it down for the team\nNobody wants to retype that. Put the commit in a file, one full hash per line, and point Git at it:\ngit log --format=%H --grep=Reformat > .git-blame-ignore-revs\ngit config blame.ignoreRevsFile .git-blame-ignore-revs\nNow plain git blame -L 7,9 checkout.js gives the answer above. The file takes full hashes. We first tried the short one, 5c04fdc, and Git answered:\nfatal: invalid object name: 5c04fdc\nThe manual says the same: \"one unabbreviated object name per line.\" Lines beginning with # are ignored, so a comment saying what each commit was is a good habit.\nCommit .git-blame-ignore-revs so everyone gets the list. The git config line is not shared by cloning, so each person runs it once.\nWhen did this string appear, or vanish\nBlame is about lines that exist now. For a thing that is gone, we search the changes instead. -S takes a string and lists the commits that changed how many times it appears in the code:\n$ git log -S RETRY_LIMIT --oneline\n9dcdb28 Stop retrying orders\n7142a9f Retry failed orders\nThe oldest line is the commit that added RETRY_LIMIT, and the newest is the one that removed it. Add -p to see the change itself. This is the removal:\n$ git log -S RETRY_LIMIT --oneline -p -1\n9dcdb28 Stop retrying orders\ndiff --git a/checkout.js b/checkout.js\nindex e14e8d1..08bde68 100644\n--- a/checkout.js\n+++ b/checkout.js\n@@ -1,15 +1,11 @@\nconst TIMEOUT_MS = 60000;\n-const RETRY_LIMIT = 3;\nfunction total(items) {\nreturn items.reduce((sum, item) => sum + item.price, 0);\n}\nfunction send(order) {\n-    for (let i = 0; i < RETRY_LIMIT; i++) {\n-        if (post(order, TIMEOUT_MS)) return true;\n-    }\n-    return false;\n+    return post(order, TIMEOUT_MS);\n}\nfunction addTax(amount) {\nThat is the commit we wanted all along, found from a different direction.\n-S counts, -G reads\nTIMEOUT_MS went from 30000 to 60000 in bf249d9. Search for it with -S:\n$ git log -S TIMEOUT_MS --oneline\n7142a9f Retry failed orders\n5c64e3c Add checkout total\nbf249d9 is missing. That commit changed the value, but the word TIMEOUT_MS appears the same number of times before and after, so -S does not count it. -G takes a regular expression and looks at the changed lines instead:\n$ git log -G \"TIMEOUT_MS = [0-9]+\" --oneline\nbf249d9 Give slow carriers more time\n5c64e3c Add checkout total\nThere it is. Use -S when you want the commit where something was added or removed, and -G when you want any commit that touched a line matching a pattern. To search for a specific value, search for the value: git log -S\"TIMEOUT_MS = 30000\" --oneline printed bf249d9 and 5c64e3c.\nHow did this function change\ngit log -L follows a range of lines, or a function, through every commit that touched it. It prints each change:\n$ git log -L :addTax:checkout.js\ncommit d28ffe22c9294f7a36ffc43e640fdc1ba515ffe1\nAuthor: Ada <ada@example.com>\nDate:   Mon May 4 10:00:00 2026 -0400\nRound tax\ndiff --git a/checkout.js b/checkout.js\n--- a/checkout.js\n+++ b/checkout.js\n@@ -11,3 +11,3 @@\nfunction addTax(amount) {\n-  return amount * 1.2;\n+  return Math.round(amount * 1.2);\n}\ncommit 5c04fdc495ac4dca9de5ef4fcaf59f427dc2fd92\nAuthor: Ravi <ravi@example.com>\nDate:   Mon Apr 20 10:00:00 2026 -0400\nReformat with 2-space indent\n...\nThe third commit, Add tax, is the original. The middle one is the reformat again, which -L does not skip. That is the whole history of one function, in one command.\nShape the output\nThe default git log is a lot of text. --format prints what we choose. These placeholders are the ones used most:\nPlaceholder\nPrints\n%h\nShort hash\n%H\nFull hash\n%an, %ae\nAuthor name and email\n%ad\nAuthor date, in the style --date sets\n%s\nSubject line\n%b\nBody of the message\n%d\nBranch and tag names\n%n\nA new line\n$ git log --format=\"%h %an %ad %s\" --date=short\nd28ffe2 Ada 2026-05-04 Round tax\n5c04fdc Ravi 2026-04-20 Reformat with 2-space indent\n9dcdb28 Ada 2026-04-06 Stop retrying orders\nbf249d9 Ravi 2026-03-16 Give slow carriers more time\n4a90109 Ada 2026-03-02 Add tax\n7142a9f Ravi 2026-02-16 Retry failed orders\n5c64e3c Ada 2026-02-02 Add checkout total\nFilters narrow the list before it prints:\nFilter\nExample\nResult in our history\n--since\ngit log --since=2026-04-01 --oneline\nd28ffe2, 5c04fdc, 9dcdb28\n--author\ngit log --author=Ravi --oneline\n5c04fdc, bf249d9, 7142a9f\n--grep\ngit log --grep=tax -i --oneline\nd28ffe2, 4a90109\n--grep searches commit messages, and -i ignores case. For comparison, -S and -G search the changes. And git log --oneline --graph --decorate draws the branches. Ours is a straight line, so it is seven stars in a column.\nTry it\nThe whole hunt takes six commands.\nPractice terminal · dig through history\nType these commands in order, in a practice folder:\nBlame lines 7 to 9 of checkout.js. Try git blame -L 7,9 checkout.js. Line 8 is blamed on the reformat, 5c04fdc.\nBlame the same lines, ignoring the reformat commit. Try git blame --ignore-rev 5c04fdc -L 7,9 checkout.js. git blame -w -L 7,9 checkout.js works too, because that commit only changed whitespace. Line 8 now belongs to Ada, 9dcdb28.\nFind the commits that added or removed RETRY_LIMIT. Try git log -S RETRY_LIMIT --oneline.\nFind the commit that changed the value of TIMEOUT_MS, which -S misses. Try git log -G \"TIMEOUT_MS = [0-9]+\" --oneline.\nFollow the addTax function through history. Try git log -L :addTax:checkout.js.\nPrint the hash, author, date and subject of every commit. Try git log --format=\"%h %an %ad %s\" --date=short.\nTry the interactive Git practice terminal on the canonical article (https://mnixblog.netlify.app/dig-through-history/).\nTwo things worth trying: type git log -S TIMEOUT_MS --oneline at step 4 and see why it is not the answer, and ask for a hint at step 2.\nChallenge: one command, one line\nThe retry loop ended with return false;, and Stop retrying orders removed it. Print who removed that line and on what date, as one line in the form Ada 2026-04-06. Use one git log command, and do not type a commit hash.\nExpected result: exactly Ada 2026-04-06, and nothing else.\nTip:\n-S lists the commit that added the string and the commit that removed it. Git prints the newest first.\n<details>\n<summary>Answer</summary>\n$ git log -S\"return false;\" -1 --format=\"%an %ad\" --date=short\nAda 2026-04-06\nWithout -1, the same command also prints Ravi's Retry failed orders, the commit that added the line.\n</details>\nQuestion: TIMEOUT_MS changes from 30000 to 60000 in one commit. The word TIMEOUT_MS appears the same number of times before and after. Which command lists that commit?\ngit log -G \"TIMEOUT_MS = [0-9]+\" --oneline\ngit log -S TIMEOUT_MS --oneline\nBoth of them\nAnswer: 1. git log -G \"TIMEOUT_MS = [0-9]+\" --oneline\nExplanation: -S lists commits that change the number of times a string appears, and here it did not change. -G looks at the changed lines themselves.\nQuestion: A .git-blame-ignore-revs file lists the commits that blame should skip. How must each commit be written?\nAs its full hash, one per line\nAs a short hash of seven characters\nAs the commit message\nAnswer: 1. As its full hash, one per line\nExplanation: Git's manual asks for one unabbreviated object name per line. In our run, the seven-character hash gave fatal: invalid object name.\nQuestion: git blame shows a line next to a commit. Which commit is it?\nThe most recent one that changed that line\nThe first one that ever added that line\nThe one that last touched the file\nAnswer: 1. The most recent one that changed that line\nExplanation: Line 8 was written by Ada but blamed on Ravi's reformat, because his was the last commit to change it.\nWhat we learned\ngit blame -L 7,9 file shows who last changed those lines. -L :send takes a function name.\nBlame names the last commit that touched a line, so a reformat hides the author.\ngit blame -w ignores whitespace changes. --ignore-rev <commit> ignores a whole commit.\n.git-blame-ignore-revs lists full hashes, and git config blame.ignoreRevsFile points Git at it. The config is per clone.\ngit log -S text finds commits that change how many times the text appears. -G regex finds commits whose changed lines match.\ngit log -L :function:file shows every change to a function.\n--format, --date, --since, --author and --grep shape what git log prints.\nBlame, the pickaxe and -L tell us which commit to look at. The commit message and the diff tell us why, and git show 9dcdb28 prints both.\nPortable export note: 6 raw HTML fragment(s) are shown as literal source because this Markdown export does not execute or preserve raw HTML. View the canonical article (https://mnixblog.netlify.app/dig-through-history/).\nPortable fallback: The authored description of the demo is preserved; the live demonstration remains on the canonical article. Review: Review the authored description of the demo and keep the canonical link for the live version.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/dig-through-history/#ob-discussion)",
      "date_published": "2026-10-27T00:03:00Z",
      "id": "https://mnixblog.netlify.app/dig-through-history/",
      "image": "https://mnixblog.netlify.app/images/dig-through-history/cover.jpg",
      "summary": "Find who changed a line, when a string appeared or vanished, and how a function changed, using git blame, log -S and log -L.",
      "tags": [
        "Git",
        "Command Line",
        "Debugging"
      ],
      "title": "Dig Through History.",
      "url": "https://mnixblog.netlify.app/dig-through-history/"
    },
    {
      "content_html": "<p>A check that passed at <code>v1.0</code> fails today. In between there are thirteen commits.</p>\n<pre><code class=\"language-console\">$ sh check.sh; echo $?\n1\n</code></pre>\n<p>The hashes in this article, such as <code>e2b28b4</code>, come from our own run. Yours will differ, so match commits by their messages.</p>\n<p>You could read thirteen diffs. You could check out each commit and run the check. Or you can let Git do the searching, and tell it only whether a commit is good or bad.</p>\n<p><code>git bisect</code> finds the commit that broke the check. One command answers the question without any help from us:</p>\n<pre><code class=\"language-console\">$ git bisect run sh check.sh\n...\ne2b28b48490fa9fca263b1139911de5315b6778c is the first bad commit\n</code></pre>\n<h2 id=\"a-project-with-one-broken-commit\">A project with one broken commit<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/find-the-commit-that-broke-it/#a-project-with-one-broken-commit\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our scratch project is a single script, <code>bill.sh</code>, which multiplies a quantity by a price. <code>check.sh</code> tests it:</p>\n<pre><code class=\"language-sh\">#!/bin/sh\n# exits 0 when 3 items at 4 each total 12\n[ \"$(sh bill.sh 3 4)\" = \"12\" ]\n</code></pre>\n<p>The exit code is the whole result. <code>0</code> means the check passed, anything else means it failed. That is the contract bisect uses, so keep it in mind.</p>\n<p>The history has sixteen commits, and <code>v1.0</code> is a tag on the third:</p>\n<pre><code class=\"language-console\">$ git log --oneline\naa1dcd3 Invite contributions\n0f1c2ac Add plums\n9a951ee Note whole-number prices\ne1eb7c8 Explain item list\n977c5b3 Add cherries\ne2b28b4 Add loyalty discount\n78bad62 Mention check.sh\n189fe51 Comment the formula\n97330b4 Add bananas\n1bf0dc7 Add license\n500813b Ignore log files\n32b8a71 Add pears\nab70081 Add item list\nf81a834 Document usage\nfa1d2e4 Add README\n60d1b2a Add bill script\n</code></pre>\n<p>Most of these commits touch a README or a list of fruit. One of them changed the arithmetic. The output in this article comes from Git 2.45.2 on Windows 11, run in Git Bash.</p>\n<details>\n<summary>Build the project yourself (Git Bash, optional)</summary>\n<p>Your hashes will differ from the ones printed here. The history, the tag and the results will match.</p>\n<pre><code class=\"language-bash\">git init -b main bill &amp;&amp; cd bill\nc() { git add . &amp;&amp; git commit -m \"$1\"; }\nprintf '#!/bin/sh\\necho $(( $1 * $2 ))\\n' &gt; bill.sh\nprintf '#!/bin/sh\\n# exits 0 when 3 items at 4 each total 12\\n[ \"$(sh bill.sh 3 4)\" = \"12\" ]\\n' &gt; check.sh\nc \"Add bill script\"\necho \"# Bill\" &gt; README.md; c \"Add README\"\necho \"Usage: sh bill.sh QUANTITY PRICE\" &gt;&gt; README.md; c \"Document usage\"\ngit tag v1.0\necho \"apple 4\" &gt; items.txt; c \"Add item list\"\necho \"pear 5\" &gt;&gt; items.txt; c \"Add pears\"\necho \"*.log\" &gt; .gitignore; c \"Ignore log files\"\necho \"MIT\" &gt; LICENSE; c \"Add license\"\necho \"banana 2\" &gt;&gt; items.txt; c \"Add bananas\"\nprintf '#!/bin/sh\\n# total = quantity * price\\necho $(( $1 * $2 ))\\n' &gt; bill.sh; c \"Comment the formula\"\necho \"Run check.sh to test.\" &gt;&gt; README.md; c \"Mention check.sh\"\nprintf '#!/bin/sh\\n# total = quantity * price, less a loyalty discount\\necho $(( $1 * $2 - 1 ))\\n' &gt; bill.sh; c \"Add loyalty discount\"\necho \"cherry 9\" &gt;&gt; items.txt; c \"Add cherries\"\necho \"Items live in items.txt.\" &gt;&gt; README.md; c \"Explain item list\"\nprintf '#!/bin/sh\\n# total = quantity * price, less a loyalty discount\\n# prices are whole numbers\\necho $(( $1 * $2 - 1 ))\\n' &gt; bill.sh; c \"Note whole-number prices\"\necho \"plum 3\" &gt;&gt; items.txt; c \"Add plums\"\necho \"Contributions welcome.\" &gt;&gt; README.md; c \"Invite contributions\"\n</code></pre>\n</details>\n<h2 id=\"search-do-not-read\">Search, do not read<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/find-the-commit-that-broke-it/#search-do-not-read\" aria-label=\"Link to this section\">#</a></h2>\n<p>Bisect is a binary search. We tell Git one commit that is bad and one that is good. Git checks out the commit in the middle, and we say whether it is good or bad. Whatever we answer throws away half of the commits that are left.</p>\n<pre><code class=\"language-text\">good ● ● ● ● ● ● ● ● ● ● ● ● ● ● bad\n              test here\n</code></pre>\n<p>Thirteen commits take about four tests. Sixty-four take about six, and a thousand take about ten. Every extra test doubles the number of commits we can cover.</p>\n<h2 id=\"by-hand\">By hand<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/find-the-commit-that-broke-it/#by-hand\" aria-label=\"Link to this section\">#</a></h2>\n<p>We start the session, then tell Git which end is which. The commit we are standing on is bad, and the <code>v1.0</code> tag was good:</p>\n<pre><code class=\"language-console\">$ git bisect start\nstatus: waiting for both good and bad commits\n$ git bisect bad\nstatus: waiting for good commit(s), bad commit known\n$ git bisect good v1.0\nBisecting: 6 revisions left to test after this (roughly 3 steps)\n[189fe51b563dc16a0f96744518ba87072c7a5e36] Comment the formula\n</code></pre>\n<p>Git has now checked out <code>189fe51</code>, in the middle. Our working folder holds that commit. We run the check:</p>\n<pre><code class=\"language-console\">$ sh check.sh; echo $?\n0\n$ git bisect good\nBisecting: 3 revisions left to test after this (roughly 2 steps)\n[977c5b326b651d1920eaeb0f82eaec1458d313c1] Add cherries\n</code></pre>\n<p>It passed, so everything up to and including <code>189fe51</code> is good. Git jumped to the middle of what is left.</p>\n<pre><code class=\"language-console\">$ sh check.sh; echo $?\n1\n$ git bisect bad\nBisecting: 0 revisions left to test after this (roughly 1 step)\n[e2b28b48490fa9fca263b1139911de5315b6778c] Add loyalty discount\n$ sh check.sh; echo $?\n1\n$ git bisect bad\nBisecting: 0 revisions left to test after this (roughly 0 steps)\n[78bad623c1471205a79ced65f082805a7e706ddd] Mention check.sh\n</code></pre>\n<p>Git's estimates say \"roughly\", and ours was off by one: it predicted three steps and we answered four times. The last commit it asks about is the one just before the suspect.</p>\n<pre><code class=\"language-console\">$ sh check.sh; echo $?\n0\n$ git bisect good\ne2b28b48490fa9fca263b1139911de5315b6778c is the first bad commit\ncommit e2b28b48490fa9fca263b1139911de5315b6778c\nAuthor: Test User &lt;test@example.com&gt;\nDate:   Mon Oct 26 19:23:19 2026 -0400\n\n    Add loyalty discount\n\n bill.sh | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n</code></pre>\n<p>Four tests, and the answer is <code>Add loyalty discount</code>. Here is what it did:</p>\n<pre><code class=\"language-console\">$ git show e2b28b4\n...\n-# total = quantity * price\n-echo $(( $1 * $2 ))\n+# total = quantity * price, less a loyalty discount\n+echo $(( $1 * $2 - 1 ))\n</code></pre>\n<p>A discount of one unit, subtracted from every total.</p>\n<h3 id=\"get-back-to-your-branch\">Get back to your branch<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/find-the-commit-that-broke-it/#get-back-to-your-branch\" aria-label=\"Link to this section\">#</a></h3>\n<p>While bisecting, we are not on a branch:</p>\n<pre><code class=\"language-console\">$ git status\nHEAD detached at 189fe51\nYou are currently bisecting, started from branch 'main'.\n  (use \"git bisect reset\" to get back to the original branch)\n</code></pre>\n<p>That is expected. When we are done, whether we found the answer or gave up, we leave:</p>\n<pre><code class=\"language-console\">$ git bisect reset\nPrevious HEAD position was 78bad62 Mention check.sh\nSwitched to branch 'main'\n</code></pre>\n<p>Start with a clean working folder. Bisect checks out other commits, so uncommitted work is in its way. <a href=\"https://mnixblog.netlify.app/advanced-git-features/\">Stash</a> it first.</p>\n<h2 id=\"by-script\">By script<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/find-the-commit-that-broke-it/#by-script\" aria-label=\"Link to this section\">#</a></h2>\n<p>We answered <code>good</code> or <code>bad</code> four times by running the same command and reading the exit code. That is a job for a script. <code>git bisect run</code> does exactly that loop:</p>\n<pre><code class=\"language-console\">$ git bisect start HEAD v1.0\nBisecting: 6 revisions left to test after this (roughly 3 steps)\n[189fe51b563dc16a0f96744518ba87072c7a5e36] Comment the formula\n$ git bisect run sh check.sh\nrunning 'sh' 'check.sh'\nBisecting: 3 revisions left to test after this (roughly 2 steps)\n[977c5b326b651d1920eaeb0f82eaec1458d313c1] Add cherries\nrunning 'sh' 'check.sh'\nBisecting: 0 revisions left to test after this (roughly 1 step)\n[e2b28b48490fa9fca263b1139911de5315b6778c] Add loyalty discount\nrunning 'sh' 'check.sh'\nBisecting: 0 revisions left to test after this (roughly 0 steps)\n[78bad623c1471205a79ced65f082805a7e706ddd] Mention check.sh\nrunning 'sh' 'check.sh'\ne2b28b48490fa9fca263b1139911de5315b6778c is the first bad commit\n...\nbisect found first bad commit\n</code></pre>\n<p><code>git bisect start HEAD v1.0</code> is the shorthand for the first three commands: bad first, then good. Same commit as by hand, and we did not type an answer.</p>\n<p>Git reads the exit code of the command after <code>run</code>. The manual defines it:</p>\n<table><thead><tr><th>Exit code</th><th>Meaning</th></tr></thead><tbody>\n<tr><td><code>0</code></td><td>The commit is good</td></tr>\n<tr><td><code>1</code> to <code>127</code>, except <code>125</code></td><td>The commit is bad</td></tr>\n<tr><td><code>125</code></td><td>The commit cannot be tested, so skip it</td></tr>\n<tr><td>Anything else, such as <code>255</code></td><td>Stop the whole bisect</td></tr>\n</tbody></table>\n<p>That means a script that cannot start looks like a bad commit. Exit code <code>127</code> is the shell's \"command not found\", and bisect counts it as bad. To see it, we ran a script name that does not exist:</p>\n<pre><code class=\"language-console\">$ git bisect run sh nothere.sh\nrunning 'sh' 'nothere.sh'\nsh: nothere.sh: No such file or directory\n[f81a83459eb425d69352d2232cb15b127a31236d] Document usage\nrunning 'sh' 'nothere.sh'\nsh: nothere.sh: No such file or directory\nerror: bogus exit code 127 for good revision\n</code></pre>\n<p>Git moved down to <code>v1.0</code>, which we said was good, got a failure there too, and stopped. This is what you would see if <code>check.sh</code> was added to the repository after <code>v1.0</code>: older commits do not have it. Keep a test script outside the repository, or untracked, so it survives every checkout.</p>\n<p>When a commit fails for a reason that is not our bug, such as a build that does not compile, the script should say so with <code>125</code>:</p>\n<pre><code class=\"language-sh\">make || exit 125\nsh check.sh\n</code></pre>\n<p>Git skips that commit and picks another. By hand, <code>git bisect skip</code> does the same.</p>\n<p>And we reset when finished:</p>\n<pre><code class=\"language-console\">$ git bisect reset\nPrevious HEAD position was 78bad62 Mention check.sh\nSwitched to branch 'main'\n</code></pre>\n<h2 id=\"what-bisect-cannot-tell-you\">What bisect cannot tell you<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/find-the-commit-that-broke-it/#what-bisect-cannot-tell-you\" aria-label=\"Link to this section\">#</a></h2>\n<p>Bisect finds the first commit where the check changed from passing to failing. It does not explain why that change broke the check. Reading the diff is still our job. And it trusts our answers. If the check is flaky, or we mark one commit wrong, the search narrows on the wrong half and ends with a confident wrong commit. We have not tested what Git does on branches with many merges, so check that case on your own history before relying on it.</p>\n<h2 id=\"try-it\">Try it<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/find-the-commit-that-broke-it/#try-it\" aria-label=\"Link to this section\">#</a></h2>\n<p>Here is the same hunt as ten commands. Match each commit by its message, not its hash.</p>\n<figure class=\"ob-demo ob-portable-demo\" data-demo=\"git-practice\"><figcaption class=\"ob-code-title\">Practice terminal · hunt a bad commit</figcaption><p>Type these commands in order, in a practice folder:</p>\n<ol>\n<li>Start the session. Try <code>git bisect start</code>.</li>\n<li>Mark the commit you are on as bad. Try <code>git bisect bad</code>.</li>\n<li>Mark the <code>v1.0</code> tag as good. Try <code>git bisect good v1.0</code>. Git checks out the commit in the middle, <code>Comment the formula</code>.</li>\n<li><code>sh check.sh</code> printed 0 at <code>Comment the formula</code>. Tell Git: <code>git bisect good</code>.</li>\n<li>At <code>Add cherries</code> it printed 1. Tell Git: <code>git bisect bad</code>.</li>\n<li>At <code>Add loyalty discount</code> it printed 1. Tell Git: <code>git bisect bad</code>.</li>\n<li>At <code>Mention check.sh</code> it printed 0. Tell Git: <code>git bisect good</code>. Git names the first bad commit, <code>Add loyalty discount</code>.</li>\n<li>Get back to your branch. Try <code>git bisect reset</code>.</li>\n<li>Start again, with both ends in one command. Try <code>git bisect start HEAD v1.0</code>.</li>\n<li>Let a script do the answering. Try <code>git bisect run sh check.sh</code>.</li>\n</ol>\n<p class=\"ob-portable-note\">Try the interactive Git practice terminal on <a href=\"https://mnixblog.netlify.app/find-the-commit-that-broke-it/\">the original article</a>.</p></figure>\n<p>Two things worth trying: answer <code>bad</code> at step 4 and read what the terminal says about the exit code, and finish the lesson and count how many times the script ran.</p>\n<p><strong>Challenge: ask a different question</strong></p>\n<p><code>check.sh</code> tests arithmetic, so it is no use here. In the same project, find the first commit after <code>v1.0</code> where <code>items.txt</code> contains the word <code>cherry</code>. Use <code>git bisect run</code> with one inline command, and no script file.</p>\n<p><strong>Expected result:</strong> Git prints <code>... is the first bad commit</code>, and the commit is <code>Add cherries</code>. Run <code>git bisect reset</code> afterwards.</p>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>Here, \"bad\" only means \"has the thing we are looking for\". So the command must exit <code>0</code> on commits that do not have it. <code>sh -c '...'</code> runs a one-line command. <code>!</code> flips an exit code, so a pass becomes a fail and the other way round. <code>grep -q</code> searches without printing anything, and exits <code>0</code> when it finds a match.</p>\n</aside>\n<details>\n<summary>Answer</summary>\n<pre><code class=\"language-console\">$ git bisect start HEAD v1.0\n$ git bisect run sh -c '! grep -q cherry items.txt'\n...\nAdd cherries\n...\nbisect found first bad commit\n</code></pre>\n<p>At <code>v1.0</code> there is no <code>items.txt</code>, so <code>grep</code> fails and <code>!</code> turns that into a pass. That is the right answer for a commit that has no cherries. The quotes are for Git Bash.</p>\n</details>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>A script run by <code>git bisect run</code> exits with code 125. What does Git do with that commit?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>It skips the commit, because it cannot be tested</li>\n<li>It marks the commit as bad</li>\n<li>It marks the commit as good</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. It skips the commit, because it cannot be tested</p>\n<p><code>125</code> is the code reserved for \"cannot be tested\". The commit is skipped, and Git picks another to try.</p>\n</div>\n</div>\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Roughly how many commits must bisect test to find the first bad one among 64 commits?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>About 6</li>\n<li>About 32</li>\n<li>All 64</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. About 6</p>\n<p>Every test halves the commits that are left. 64 halves to 32, 16, 8, 4, 2, 1, which is six tests.</p>\n</div>\n</div>\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>What does <code>git bisect reset</code> do?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Ends the session and checks out the branch you started from</li>\n<li>Marks the current commit as good</li>\n<li>Reverts the bad commit</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. Ends the session and checks out the branch you started from</p>\n<p><code>reset</code> only ends the search and takes you back. In our run it printed <code>Switched to branch 'main'</code>. It does not change any commit.</p>\n</div>\n</div>\n</section>\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/find-the-commit-that-broke-it/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<ul>\n<li><code>git bisect</code> is a binary search over history. We name one good and one bad commit, and Git checks out the middle.</li>\n<li><code>git bisect start</code>, <code>git bisect bad</code>, <code>git bisect good v1.0</code>, then <code>good</code> or <code>bad</code> after each check.</li>\n<li>Git says \"roughly\" about the number of steps, and it is only a guide.</li>\n<li><code>git bisect start HEAD v1.0</code> followed by <code>git bisect run sh check.sh</code> does the whole search from one script.</li>\n<li>The script's exit code is the answer: <code>0</code> good, <code>1</code> to <code>127</code> bad except <code>125</code>, which skips the commit.</li>\n<li>Keep the test script outside the repository or untracked, so every commit can run it.</li>\n<li><code>git bisect reset</code> always ends the session and returns to the branch.</li>\n</ul>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/find-the-commit-that-broke-it/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Find the Commit That Broke It.\nCanonical: https://mnixblog.netlify.app/find-the-commit-that-broke-it/\ngit bisect, by Hand and by Script\nA check that passed at v1.0 fails today. In between there are thirteen commits.\n$ sh check.sh; echo $?\n1\nThe hashes in this article, such as e2b28b4, come from our own run. Yours will differ, so match commits by their messages.\nYou could read thirteen diffs. You could check out each commit and run the check. Or you can let Git do the searching, and tell it only whether a commit is good or bad.\ngit bisect finds the commit that broke the check. One command answers the question without any help from us:\n$ git bisect run sh check.sh\n...\ne2b28b48490fa9fca263b1139911de5315b6778c is the first bad commit\nA project with one broken commit\nOur scratch project is a single script, bill.sh, which multiplies a quantity by a price. check.sh tests it:\n#!/bin/sh\n# exits 0 when 3 items at 4 each total 12\n[ \"$(sh bill.sh 3 4)\" = \"12\" ]\nThe exit code is the whole result. 0 means the check passed, anything else means it failed. That is the contract bisect uses, so keep it in mind.\nThe history has sixteen commits, and v1.0 is a tag on the third:\n$ git log --oneline\naa1dcd3 Invite contributions\n0f1c2ac Add plums\n9a951ee Note whole-number prices\ne1eb7c8 Explain item list\n977c5b3 Add cherries\ne2b28b4 Add loyalty discount\n78bad62 Mention check.sh\n189fe51 Comment the formula\n97330b4 Add bananas\n1bf0dc7 Add license\n500813b Ignore log files\n32b8a71 Add pears\nab70081 Add item list\nf81a834 Document usage\nfa1d2e4 Add README\n60d1b2a Add bill script\nMost of these commits touch a README or a list of fruit. One of them changed the arithmetic. The output in this article comes from Git 2.45.2 on Windows 11, run in Git Bash.\n<details>\n<summary>Build the project yourself (Git Bash, optional)</summary>\nYour hashes will differ from the ones printed here. The history, the tag and the results will match.\ngit init -b main bill && cd bill\nc() { git add . && git commit -m \"$1\"; }\nprintf '#!/bin/sh\\necho $(( $1 * $2 ))\\n' > bill.sh\nprintf '#!/bin/sh\\n# exits 0 when 3 items at 4 each total 12\\n[ \"$(sh bill.sh 3 4)\" = \"12\" ]\\n' > check.sh\nc \"Add bill script\"\necho \"# Bill\" > README.md; c \"Add README\"\necho \"Usage: sh bill.sh QUANTITY PRICE\" >> README.md; c \"Document usage\"\ngit tag v1.0\necho \"apple 4\" > items.txt; c \"Add item list\"\necho \"pear 5\" >> items.txt; c \"Add pears\"\necho \"*.log\" > .gitignore; c \"Ignore log files\"\necho \"MIT\" > LICENSE; c \"Add license\"\necho \"banana 2\" >> items.txt; c \"Add bananas\"\nprintf '#!/bin/sh\\n# total = quantity * price\\necho $(( $1 * $2 ))\\n' > bill.sh; c \"Comment the formula\"\necho \"Run check.sh to test.\" >> README.md; c \"Mention check.sh\"\nprintf '#!/bin/sh\\n# total = quantity * price, less a loyalty discount\\necho $(( $1 * $2 - 1 ))\\n' > bill.sh; c \"Add loyalty discount\"\necho \"cherry 9\" >> items.txt; c \"Add cherries\"\necho \"Items live in items.txt.\" >> README.md; c \"Explain item list\"\nprintf '#!/bin/sh\\n# total = quantity * price, less a loyalty discount\\n# prices are whole numbers\\necho $(( $1 * $2 - 1 ))\\n' > bill.sh; c \"Note whole-number prices\"\necho \"plum 3\" >> items.txt; c \"Add plums\"\necho \"Contributions welcome.\" >> README.md; c \"Invite contributions\"\n</details>\nSearch, do not read\nBisect is a binary search. We tell Git one commit that is bad and one that is good. Git checks out the commit in the middle, and we say whether it is good or bad. Whatever we answer throws away half of the commits that are left.\ngood ● ● ● ● ● ● ● ● ● ● ● ● ● ● bad\ntest here\nThirteen commits take about four tests. Sixty-four take about six, and a thousand take about ten. Every extra test doubles the number of commits we can cover.\nBy hand\nWe start the session, then tell Git which end is which. The commit we are standing on is bad, and the v1.0 tag was good:\n$ git bisect start\nstatus: waiting for both good and bad commits\n$ git bisect bad\nstatus: waiting for good commit(s), bad commit known\n$ git bisect good v1.0\nBisecting: 6 revisions left to test after this (roughly 3 steps)\n[189fe51b563dc16a0f96744518ba87072c7a5e36] Comment the formula\nGit has now checked out 189fe51, in the middle. Our working folder holds that commit. We run the check:\n$ sh check.sh; echo $?\n0\n$ git bisect good\nBisecting: 3 revisions left to test after this (roughly 2 steps)\n[977c5b326b651d1920eaeb0f82eaec1458d313c1] Add cherries\nIt passed, so everything up to and including 189fe51 is good. Git jumped to the middle of what is left.\n$ sh check.sh; echo $?\n1\n$ git bisect bad\nBisecting: 0 revisions left to test after this (roughly 1 step)\n[e2b28b48490fa9fca263b1139911de5315b6778c] Add loyalty discount\n$ sh check.sh; echo $?\n1\n$ git bisect bad\nBisecting: 0 revisions left to test after this (roughly 0 steps)\n[78bad623c1471205a79ced65f082805a7e706ddd] Mention check.sh\nGit's estimates say \"roughly\", and ours was off by one: it predicted three steps and we answered four times. The last commit it asks about is the one just before the suspect.\n$ sh check.sh; echo $?\n0\n$ git bisect good\ne2b28b48490fa9fca263b1139911de5315b6778c is the first bad commit\ncommit e2b28b48490fa9fca263b1139911de5315b6778c\nAuthor: Test User <test@example.com>\nDate:   Mon Oct 26 19:23:19 2026 -0400\nAdd loyalty discount\nbill.sh | 4 ++--\n1 file changed, 2 insertions(+), 2 deletions(-)\nFour tests, and the answer is Add loyalty discount. Here is what it did:\n$ git show e2b28b4\n...\n-# total = quantity * price\n-echo $(( $1 * $2 ))\n+# total = quantity * price, less a loyalty discount\n+echo $(( $1 * $2 - 1 ))\nA discount of one unit, subtracted from every total.\nGet back to your branch\nWhile bisecting, we are not on a branch:\n$ git status\nHEAD detached at 189fe51\nYou are currently bisecting, started from branch 'main'.\n(use \"git bisect reset\" to get back to the original branch)\nThat is expected. When we are done, whether we found the answer or gave up, we leave:\n$ git bisect reset\nPrevious HEAD position was 78bad62 Mention check.sh\nSwitched to branch 'main'\nStart with a clean working folder. Bisect checks out other commits, so uncommitted work is in its way. Stash (https://mnixblog.netlify.app/advanced-git-features/) it first.\nBy script\nWe answered good or bad four times by running the same command and reading the exit code. That is a job for a script. git bisect run does exactly that loop:\n$ git bisect start HEAD v1.0\nBisecting: 6 revisions left to test after this (roughly 3 steps)\n[189fe51b563dc16a0f96744518ba87072c7a5e36] Comment the formula\n$ git bisect run sh check.sh\nrunning 'sh' 'check.sh'\nBisecting: 3 revisions left to test after this (roughly 2 steps)\n[977c5b326b651d1920eaeb0f82eaec1458d313c1] Add cherries\nrunning 'sh' 'check.sh'\nBisecting: 0 revisions left to test after this (roughly 1 step)\n[e2b28b48490fa9fca263b1139911de5315b6778c] Add loyalty discount\nrunning 'sh' 'check.sh'\nBisecting: 0 revisions left to test after this (roughly 0 steps)\n[78bad623c1471205a79ced65f082805a7e706ddd] Mention check.sh\nrunning 'sh' 'check.sh'\ne2b28b48490fa9fca263b1139911de5315b6778c is the first bad commit\n...\nbisect found first bad commit\ngit bisect start HEAD v1.0 is the shorthand for the first three commands: bad first, then good. Same commit as by hand, and we did not type an answer.\nGit reads the exit code of the command after run. The manual defines it:\nExit code\nMeaning\n0\nThe commit is good\n1 to 127, except 125\nThe commit is bad\n125\nThe commit cannot be tested, so skip it\nAnything else, such as 255\nStop the whole bisect\nThat means a script that cannot start looks like a bad commit. Exit code 127 is the shell's \"command not found\", and bisect counts it as bad. To see it, we ran a script name that does not exist:\n$ git bisect run sh nothere.sh\nrunning 'sh' 'nothere.sh'\nsh: nothere.sh: No such file or directory\n[f81a83459eb425d69352d2232cb15b127a31236d] Document usage\nrunning 'sh' 'nothere.sh'\nsh: nothere.sh: No such file or directory\nerror: bogus exit code 127 for good revision\nGit moved down to v1.0, which we said was good, got a failure there too, and stopped. This is what you would see if check.sh was added to the repository after v1.0: older commits do not have it. Keep a test script outside the repository, or untracked, so it survives every checkout.\nWhen a commit fails for a reason that is not our bug, such as a build that does not compile, the script should say so with 125:\nmake || exit 125\nsh check.sh\nGit skips that commit and picks another. By hand, git bisect skip does the same.\nAnd we reset when finished:\n$ git bisect reset\nPrevious HEAD position was 78bad62 Mention check.sh\nSwitched to branch 'main'\nWhat bisect cannot tell you\nBisect finds the first commit where the check changed from passing to failing. It does not explain why that change broke the check. Reading the diff is still our job. And it trusts our answers. If the check is flaky, or we mark one commit wrong, the search narrows on the wrong half and ends with a confident wrong commit. We have not tested what Git does on branches with many merges, so check that case on your own history before relying on it.\nTry it\nHere is the same hunt as ten commands. Match each commit by its message, not its hash.\nPractice terminal · hunt a bad commit\nType these commands in order, in a practice folder:\nStart the session. Try git bisect start.\nMark the commit you are on as bad. Try git bisect bad.\nMark the v1.0 tag as good. Try git bisect good v1.0. Git checks out the commit in the middle, Comment the formula.\nsh check.sh printed 0 at Comment the formula. Tell Git: git bisect good.\nAt Add cherries it printed 1. Tell Git: git bisect bad.\nAt Add loyalty discount it printed 1. Tell Git: git bisect bad.\nAt Mention check.sh it printed 0. Tell Git: git bisect good. Git names the first bad commit, Add loyalty discount.\nGet back to your branch. Try git bisect reset.\nStart again, with both ends in one command. Try git bisect start HEAD v1.0.\nLet a script do the answering. Try git bisect run sh check.sh.\nTry the interactive Git practice terminal on the canonical article (https://mnixblog.netlify.app/find-the-commit-that-broke-it/).\nTwo things worth trying: answer bad at step 4 and read what the terminal says about the exit code, and finish the lesson and count how many times the script ran.\nChallenge: ask a different question\ncheck.sh tests arithmetic, so it is no use here. In the same project, find the first commit after v1.0 where items.txt contains the word cherry. Use git bisect run with one inline command, and no script file.\nExpected result: Git prints ... is the first bad commit, and the commit is Add cherries. Run git bisect reset afterwards.\nTip:\nHere, \"bad\" only means \"has the thing we are looking for\". So the command must exit 0 on commits that do not have it. sh -c '...' runs a one-line command. ! flips an exit code, so a pass becomes a fail and the other way round. grep -q searches without printing anything, and exits 0 when it finds a match.\n<details>\n<summary>Answer</summary>\n$ git bisect start HEAD v1.0\n$ git bisect run sh -c '! grep -q cherry items.txt'\n...\nAdd cherries\n...\nbisect found first bad commit\nAt v1.0 there is no items.txt, so grep fails and ! turns that into a pass. That is the right answer for a commit that has no cherries. The quotes are for Git Bash.\n</details>\nQuestion: A script run by git bisect run exits with code 125. What does Git do with that commit?\nIt skips the commit, because it cannot be tested\nIt marks the commit as bad\nIt marks the commit as good\nAnswer: 1. It skips the commit, because it cannot be tested\nExplanation: 125 is the code reserved for \"cannot be tested\". The commit is skipped, and Git picks another to try.\nQuestion: Roughly how many commits must bisect test to find the first bad one among 64 commits?\nAbout 6\nAbout 32\nAll 64\nAnswer: 1. About 6\nExplanation: Every test halves the commits that are left. 64 halves to 32, 16, 8, 4, 2, 1, which is six tests.\nQuestion: What does git bisect reset do?\nEnds the session and checks out the branch you started from\nMarks the current commit as good\nReverts the bad commit\nAnswer: 1. Ends the session and checks out the branch you started from\nExplanation: reset only ends the search and takes you back. In our run it printed Switched to branch 'main'. It does not change any commit.\nWhat we learned\ngit bisect is a binary search over history. We name one good and one bad commit, and Git checks out the middle.\ngit bisect start, git bisect bad, git bisect good v1.0, then good or bad after each check.\nGit says \"roughly\" about the number of steps, and it is only a guide.\ngit bisect start HEAD v1.0 followed by git bisect run sh check.sh does the whole search from one script.\nThe script's exit code is the answer: 0 good, 1 to 127 bad except 125, which skips the commit.\nKeep the test script outside the repository or untracked, so every commit can run it.\ngit bisect reset always ends the session and returns to the branch.\nPortable export note: 6 raw HTML fragment(s) are shown as literal source because this Markdown export does not execute or preserve raw HTML. View the canonical article (https://mnixblog.netlify.app/find-the-commit-that-broke-it/).\nPortable fallback: The authored description of the demo is preserved; the live demonstration remains on the canonical article. Review: Review the authored description of the demo and keep the canonical link for the live version.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/find-the-commit-that-broke-it/#ob-discussion)",
      "date_published": "2026-10-27T00:02:00Z",
      "id": "https://mnixblog.netlify.app/find-the-commit-that-broke-it/",
      "image": "https://mnixblog.netlify.app/images/find-the-commit-that-broke-it/cover.jpg",
      "summary": "Find the exact commit that introduced a bug with git bisect, first by hand and then by handing the job to a script.",
      "tags": [
        "Git",
        "Debugging",
        "Command Line"
      ],
      "title": "Find the Commit That Broke It.",
      "url": "https://mnixblog.netlify.app/find-the-commit-that-broke-it/"
    },
    {
      "content_html": "<p>Your branch is ready for review. Then you run <code>git log --oneline main..</code> and read it out loud:</p>\n<pre><code class=\"language-console\">$ git log --oneline main..\n96eed00 Debug output\nda9c2a6 wip\n872747b Fix typo in search box\n736ca7c Add search results\n038a3d4 wip\nb4b3152 Add search box\n</code></pre>\n<p>The <code>main..</code> part means \"the commits on this branch that are not on <code>main</code>\".</p>\n<p>The hashes on the left (<code>96eed00</code> and friends) differ for every reader, because a hash comes from the commit's content, author and time. Run <code>git log --oneline main..</code> in your own repository and use the hashes it prints wherever we type one.</p>\n<p>Two commits called <code>wip</code>. One that fixes a typo you made three commits earlier. One that prints debug output to the console. A reviewer has to read all of it.</p>\n<p>In this article, we are going to rewrite that list before anyone else sees it. We will use <code>git rebase -i</code>, which stands for interactive rebase, and by the end the same command will print this:</p>\n<pre><code class=\"language-text\">87dadb7 Add search results\n41d82a3 Style the search box\n23b3420 Add a search box\n</code></pre>\n<p>Three commits, each with a message that says what it does. We will also look at the ways out when a rebase goes wrong, and the one case where we should not start it at all.</p>\n<p>Three small commands come up here. <code>git commit --amend</code> rewrites the last commit, <code>git cherry-pick</code> copies one commit onto another branch, and <code>git stash</code> parks uncommitted changes. That is all you need from them. <a href=\"https://mnixblog.netlify.app/advanced-git-features/\">Advanced Git Features</a> teaches each one properly, but you can skip it for now.</p>\n<h2 id=\"the-scratch-repository\">The scratch repository<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/interactive-rebase-done-properly/#the-scratch-repository\" aria-label=\"Link to this section\">#</a></h2>\n<p>Everything below runs in a throwaway folder. The commands and the output in this article come from Git 2.45.2 on Windows 11, run in Git Bash.</p>\n<details>\n<summary>Build the messy branch yourself (Git Bash, optional)</summary>\n<p>Your hashes will differ from the ones printed here. Everything else will match.</p>\n<pre><code class=\"language-bash\">git init -b main notes &amp;&amp; cd notes\necho \"&lt;h1&gt;Home&lt;/h1&gt;\" &gt; index.html\ngit add . &amp;&amp; git commit -m \"Add home page\"\necho \"body { margin: 0; }\" &gt; style.css\ngit add . &amp;&amp; git commit -m \"Add base styles\"\ngit switch -c feature/search\necho '&lt;input name=\"q\" placeholder=\"Seach\"&gt;' &gt; search.html\ngit add . &amp;&amp; git commit -m \"Add search box\"\necho \".search { width: 20em; }\" &gt;&gt; style.css\ngit add . &amp;&amp; git commit -m \"wip\"\necho \"&lt;ul id=results&gt;&lt;/ul&gt;\" &gt; results.html\ngit add . &amp;&amp; git commit -m \"Add search results\"\necho '&lt;input name=\"q\" placeholder=\"Search\"&gt;' &gt; search.html\ngit add . &amp;&amp; git commit -m \"Fix typo in search box\"\necho \".search:focus { outline: 2px solid; }\" &gt;&gt; style.css\ngit add . &amp;&amp; git commit -m \"wip\"\necho \"&lt;script&gt;console.log('results')&lt;/script&gt;\" &gt;&gt; results.html\ngit add . &amp;&amp; git commit -m \"Debug output\"\n</code></pre>\n</details>\n<h2 id=\"what-the-editor-shows-you\">What the editor shows you<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/interactive-rebase-done-properly/#what-the-editor-shows-you\" aria-label=\"Link to this section\">#</a></h2>\n<p>Think of a messy first draft. Before you hand it to anyone, you reorder the pages, merge two of them and tear one out. Rebase does that to commits. In Git's words, rebase means \"take these commits and replay them on a different base.\" Interactive means \"show me the list first, so I can change it.\"</p>\n<pre><code class=\"language-bash\">git rebase -i main\n</code></pre>\n<p>Git opens your editor with one line per commit on the branch. This is the top of the file:</p>\n<pre><code class=\"language-text\">pick b4b3152 Add search box\npick 038a3d4 wip\npick 736ca7c Add search results\npick 872747b Fix typo in search box\npick da9c2a6 wip\npick 96eed00 Debug output\n</code></pre>\n<p>Below the list, Git prints its own cheat sheet in comment lines.</p>\n<p>If the editor that opens is Vim, you only need four moves. Press <code>i</code> to start typing, and <code>Esc</code> when you are done. To move a whole line, put the cursor on it, press <code>dd</code> to cut it, move to the line above where it should go, and press <code>p</code> to paste it below. Then type <code>:wq</code> and press Enter to save and close.</p>\n<p>To use another editor instead, set it once. For VS Code that is <code>git config --global core.editor \"code --wait\"</code>. The <code>--wait</code> makes Git hold on until you close the tab.</p>\n<p>Notice the order. <code>git log</code> shows the newest commit first. This list shows the <strong>oldest</strong> first, and Git replays it from the top down.</p>\n<p>Each line starts with a command. These five do nearly everything:</p>\n<table><thead><tr><th>Command</th><th>What it does</th></tr></thead><tbody>\n<tr><td><code>pick</code></td><td>Keep the commit as it is</td></tr>\n<tr><td><code>reword</code></td><td>Keep the commit, and open the editor to change its message</td></tr>\n<tr><td><code>squash</code></td><td>Meld the commit into the one above it, and combine the two messages</td></tr>\n<tr><td><code>fixup</code></td><td>Meld the commit into the one above it, and throw its message away</td></tr>\n<tr><td><code>drop</code></td><td>Remove the commit</td></tr>\n</tbody></table>\n<p>You can also reorder lines. If you delete every line, Git aborts the rebase.</p>\n<h2 id=\"our-edit\">Our edit<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/interactive-rebase-done-properly/#our-edit\" aria-label=\"Link to this section\">#</a></h2>\n<p>Here is what we want:</p>\n<ul>\n<li>The first commit gets a better message, and absorbs the typo fix.</li>\n<li>The first <code>wip</code> becomes <code>Style the search box</code>, and absorbs the second <code>wip</code>.</li>\n<li><code>Add search results</code> stays.</li>\n<li><code>Debug output</code> goes.</li>\n</ul>\n<p>Move the lines, change the words at the start of each, and save. Use the hashes from your own list:</p>\n<pre><code class=\"language-text\">reword b4b3152 Add search box\nfixup 872747b Fix typo in search box\nreword 038a3d4 wip\nfixup da9c2a6 wip\npick 736ca7c Add search results\ndrop 96eed00 Debug output\n</code></pre>\n<p>Dropping a commit can conflict with the ones after it. If we had dropped <code>Add search results</code> instead, <code>Debug output</code> would have failed to apply, because it edits <code>results.html</code>, which that commit creates. Git stops with <code>CONFLICT (modify/delete)</code>. If that happens, run <code>git rebase --abort</code>, and the branch is back as it was.</p>\n<p>Git works down the list. At each <code>reword</code>, it stops and opens the editor for the message. We typed <code>Add a search box</code>, then <code>Style the search box</code>. When the last line is done:</p>\n<pre><code class=\"language-text\">Successfully rebased and updated refs/heads/feature/search.\n</code></pre>\n<pre><code class=\"language-console\">$ git log --oneline main..\n87dadb7 Add search results\n41d82a3 Style the search box\n23b3420 Add a search box\n</code></pre>\n<aside class=\"ob-callout ob-callout-note\" role=\"note\"><p class=\"ob-callout-title\"><strong>Note</strong></p>\n<p>The editor steps in this article were run with a script standing in for the editor, so the text we saved is exactly what is shown. In your terminal, you will see the editor open instead. The path of the remote is shortened to <code>origin</code> in the push output further down.</p>\n</aside>\n<p>Every hash changed, including <code>Add search results</code>, which we did not touch. A commit's hash depends on its parent, so replaying on a new parent makes a new commit. That is why rewriting history has consequences, and we will come back to it.</p>\n<h3 id=\"squash-keeps-both-messages\">Squash keeps both messages<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/interactive-rebase-done-properly/#squash-keeps-both-messages\" aria-label=\"Link to this section\">#</a></h3>\n<p>We used <code>fixup</code>, which keeps the message from the top commit only. <code>squash</code> keeps both and opens the editor so we can combine them. In another scratch branch, we changed <code>Add search results</code> to <code>squash</code>:</p>\n<pre><code class=\"language-text\">pick 1631805 Add search box\nsquash 1a2b9b0 Add search results\npick f557164 Style the search box\n</code></pre>\n<p>The editor opens with this at the top:</p>\n<pre><code class=\"language-text\"># This is a combination of 2 commits.\n# This is the 1st commit message:\n\nAdd search box\n\n# This is the commit message #2:\n\nAdd search results\n</code></pre>\n<p>Edit the text above the <code>#</code> lines into the one message you want, and save. Use <code>fixup</code> when the second commit is a correction and <code>squash</code> when its message is worth reading.</p>\n<h2 id=\"folding-in-a-fix-with-autosquash\">Folding in a fix with autosquash<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/interactive-rebase-done-properly/#folding-in-a-fix-with-autosquash\" aria-label=\"Link to this section\">#</a></h2>\n<p>The reviewer comes back: the placeholder text should say \"Search notes\". That belongs in <code>Add a search box</code>, which is two commits down. We could amend, but only the latest commit is easy to amend. Instead, with the hash of <code>Add a search box</code> from your own <code>git log --oneline main..</code>:</p>\n<pre><code class=\"language-console\">$ echo '&lt;input name=\"q\" placeholder=\"Search notes\"&gt;' &gt; search.html\n$ git add search.html\n$ git commit --fixup 23b3420\n[feature/search 9c64964] fixup! Add a search box\n 1 file changed, 1 insertion(+), 1 deletion(-)\n</code></pre>\n<p><code>--fixup</code> takes the hash of the commit the change belongs to, and writes a commit called <code>fixup! Add a search box</code>. Now we ask rebase to put it in the right place:</p>\n<pre><code class=\"language-bash\">git rebase -i --autosquash main\n</code></pre>\n<p>Git writes the todo for us. The first lines:</p>\n<pre><code class=\"language-text\">pick 23b3420 Add a search box\nfixup 9c64964 fixup! Add a search box\npick 41d82a3 Style the search box\npick 87dadb7 Add search results\n</code></pre>\n<p>Save without changing anything, and the fix is inside the first commit. We typed no <code>fixup</code> ourselves.</p>\n<p>To make <code>--autosquash</code> the default for <code>git rebase -i</code>, set it once:</p>\n<pre><code class=\"language-bash\">git config rebase.autoSquash true\n</code></pre>\n<p>Add <code>--global</code> to set it for every repository. We ran it without <code>--global</code> in a scratch repository, and plain <code>git rebase -i main</code> produced the same todo.</p>\n<h2 id=\"moving-a-stack-of-branches\">Moving a stack of branches<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/interactive-rebase-done-properly/#moving-a-stack-of-branches\" aria-label=\"Link to this section\">#</a></h2>\n<p>If this is your first rebase, skip ahead to \"When the rebase goes wrong\". This section is for when one branch sits on top of another.</p>\n<p>Sometimes one branch sits on top of another. Here <code>search-history</code> is built on <code>search</code>, and then <code>main</code> moves on:</p>\n<pre><code class=\"language-text\">* 93e0fe4 (main) Add footer\n| * 7038177 (HEAD -&gt; search-history) Style recent searches\n| * f634c73 Add recent searches\n| * e5efeec (search) Add search results\n| * 4f084fb Add search box\n|/\n* 215815f Add home page\n</code></pre>\n<p>Rebase <code>search-history</code> onto <code>main</code> and look at the branch called <code>search</code>:</p>\n<pre><code class=\"language-text\">* 09fb68a (HEAD -&gt; search-history) Style recent searches\n* 8281a38 Add recent searches\n* 6e5c6a3 Add search results\n* 397f169 Add search box\n* 93e0fe4 (main) Add footer\n| * e5efeec (search) Add search results\n| * 4f084fb Add search box\n|/\n* 215815f Add home page\n</code></pre>\n<p><code>search</code> stayed behind. Its commits were replayed, but the label did not follow them. We put everything back with <code>git reset --hard ORIG_HEAD</code>, and ran it again with one flag:</p>\n<pre><code class=\"language-bash\">git rebase --update-refs main\n</code></pre>\n<pre><code class=\"language-text\">Successfully rebased and updated refs/heads/search-history.\nUpdated the following refs with --update-refs:\n\trefs/heads/search\n* 7e4602a (HEAD -&gt; search-history) Style recent searches\n* e5caf30 Add recent searches\n* 682f791 (search) Add search results\n* 30bb4f9 Add search box\n* 93e0fe4 (main) Add footer\n* 215815f Add home page\n</code></pre>\n<p>Both branch names moved. <code>--update-refs</code> is available from Git 2.38, and <code>git config rebase.updateRefs true</code> turns it on for every rebase. Git will not move a branch that is checked out in another worktree.</p>\n<h2 id=\"when-the-rebase-goes-wrong\">When the rebase goes wrong<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/interactive-rebase-done-properly/#when-the-rebase-goes-wrong\" aria-label=\"Link to this section\">#</a></h2>\n<p>Reordering commits is the easiest way to cause a conflict. Suppose we move <code>Debug output</code> to the top of the todo. That commit edits <code>results.html</code>, which does not exist yet at that point in the replay:</p>\n<pre><code class=\"language-console\">$ git rebase -i main\nCONFLICT (modify/delete): results.html deleted in HEAD and modified in 96eed00 (Debug output).  Version 96eed00 (Debug output) of results.html left in tree.\nRebasing (1/6)error: could not apply 96eed00... Debug output\nhint: Resolve all conflicts manually, mark them as resolved with\nhint: \"git add/rm &lt;conflicted_files&gt;\", then run \"git rebase --continue\".\nhint: You can instead skip this commit: run \"git rebase --skip\".\nhint: To abort and get back to the state before \"git rebase\", run \"git rebase --abort\".\n</code></pre>\n<pre><code class=\"language-console\">$ git status --short\nDU results.html\n</code></pre>\n<p>Git stopped in the middle. We have three choices:</p>\n<ul>\n<li>Fix the file, <code>git add</code> it, and run <code>git rebase --continue</code>.</li>\n<li><code>git rebase --skip</code> drops the commit it could not apply.</li>\n<li><code>git rebase --abort</code> puts everything back.</li>\n</ul>\n<p><code>--abort</code> is the one to remember. After it, the branch is exactly as it was before we typed <code>git rebase</code>:</p>\n<pre><code class=\"language-console\">$ git rebase --abort\n$ git status\nOn branch feature/search\nnothing to commit, working tree clean\n$ git log --oneline main..\n96eed00 Debug output\nda9c2a6 wip\n872747b Fix typo in search box\n736ca7c Add search results\n038a3d4 wip\nb4b3152 Add search box\n</code></pre>\n<h3 id=\"after-the-rebase-has-finished\">After the rebase has finished<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/interactive-rebase-done-properly/#after-the-rebase-has-finished\" aria-label=\"Link to this section\">#</a></h3>\n<p>Maybe the rebase succeeded and the result is wrong. We dropped a commit that we needed. Git saves where the branch pointed before a rebase in <code>ORIG_HEAD</code>:</p>\n<pre><code class=\"language-console\">$ git reset --hard ORIG_HEAD\nHEAD is now at 96eed00 Debug output\n</code></pre>\n<p>All six original commits are back. Other commands can overwrite <code>ORIG_HEAD</code>, so do this straight away. If you wait, <a href=\"https://mnixblog.netlify.app/git-troubleshooting-and-recovery/\">the reflog</a> still has the old position.</p>\n<h2 id=\"when-not-to-rebase\">When not to rebase<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/interactive-rebase-done-properly/#when-not-to-rebase\" aria-label=\"Link to this section\">#</a></h2>\n<p>If you have never pushed a branch to a shared remote, you can skip the force-push part for now. The first paragraph is the one to keep.</p>\n<p>Rebasing replaces commits with new ones. That is fine for commits only you have. It is a problem for commits somebody else already has.</p>\n<p>We pushed our branch, then reworded the last commit and pushed again:</p>\n<pre><code class=\"language-console\">$ git push\n ! [rejected]        feature/search -&gt; feature/search (non-fast-forward)\nerror: failed to push some refs to 'origin'\nhint: Updates were rejected because the tip of your current branch is behind\nhint: its remote counterpart. If you want to integrate the remote changes,\nhint: use 'git pull' before pushing again.\n</code></pre>\n<p>The remote still has the old commit and we have a new one, so Git sees two histories that diverged. To replace the remote branch we have to force it. <code>--force-with-lease</code> is the careful way to do that:</p>\n<pre><code class=\"language-console\">$ git push --force-with-lease\n + eb42e06...9adb2e6 feature/search -&gt; feature/search (forced update)\n</code></pre>\n<p>It overwrites the remote branch only if the remote still points where we last saw it. To test that, a second clone pushed a commit to <code>feature/search</code>. Then we rewrote our commit again and pushed:</p>\n<pre><code class=\"language-console\">$ git push --force-with-lease\n ! [rejected]        feature/search -&gt; feature/search (stale info)\n</code></pre>\n<p>Git refused. Plain <code>git push --force</code> in the same situation did not:</p>\n<pre><code class=\"language-console\">$ git push --force\n + 2c44758...7d8835e feature/search -&gt; feature/search (forced update)\n</code></pre>\n<p>That commit from the second clone is no longer on the remote branch.</p>\n<p>So the rule has two parts:</p>\n<ul>\n<li>Rebase freely before you push, and on a branch nobody else builds on.</li>\n<li>Once other people have your commits, merge instead, or agree on the rewrite first.</li>\n</ul>\n<p>Rewriting a branch that is already in a pull request is common, and <code>--force-with-lease</code> is how it is done safely. The risk is a teammate who started work on top of those commits. Their copy still has the old ones.</p>\n<h2 id=\"try-it\">Try it<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/interactive-rebase-done-properly/#try-it\" aria-label=\"Link to this section\">#</a></h2>\n<p>The whole workflow takes six commands. The terminal knows the scene for each step.</p>\n<figure class=\"ob-demo ob-portable-demo\" data-demo=\"git-practice\"><figcaption class=\"ob-code-title\">Practice terminal · tidy a branch with rebase</figcaption><p>Type these commands in order, in a practice folder:</p>\n<ol>\n<li>List the commits on your branch that are not on main. Try <code>git log --oneline main..</code>. It shows six commits, including two called <code>wip</code>.</li>\n<li>Record the typo fix as a fixup of the first commit, b4b3152. Try <code>git commit --fixup b4b3152</code>. Git names the new commit <code>fixup! Add search box</code>.</li>\n<li>Replay the branch with the fixup folded in. Try <code>git rebase -i --autosquash main</code>. Without <code>--autosquash</code>, the fixup stays at the bottom of the list.</li>\n<li>Rebase a stacked branch onto main and take the branch below it along. Try <code>git rebase --update-refs main</code>.</li>\n<li>A rebase stopped on a conflict, and you want to start over. Try <code>git rebase --abort</code>. The branch goes back to how it was before the rebase.</li>\n<li>A finished rebase dropped a commit you needed. Try <code>git reset --hard ORIG_HEAD</code>. <code>ORIG_HEAD</code> is where the branch pointed before the rebase.</li>\n</ol>\n<p class=\"ob-portable-note\">Try the interactive Git practice terminal on <a href=\"https://mnixblog.netlify.app/interactive-rebase-done-properly/\">the original article</a>.</p></figure>\n<p>Two things worth trying: type <code>git rebase -i main</code> at step 3 and read what the terminal says is missing, and run <code>git reflog</code> at step 6.</p>\n<p><strong>Challenge: fold it in the right place</strong></p>\n<p>Take the six-commit branch and rewrite it so that <code>git log --oneline main..</code> prints exactly two commits, <code>Add search results</code> on top of <code>Add search box</code>. Both <code>wip</code> commits and the typo fix must end up inside <code>Add search box</code>, and <code>Debug output</code> must be gone.</p>\n<p><strong>Expected result:</strong> <code>git show HEAD~:search.html</code> prints <code>&lt;input name=\"q\" placeholder=\"Search\"&gt;</code>, spelled correctly.</p>\n<p>The two-line log is the easy part. The typo fix is the trap: if it stays where it was, it folds into <code>Add search results</code>, and the log looks right while the first commit still says <code>Seach</code>.</p>\n<details>\n<summary>Answer</summary>\n<p>Move the lines so every commit that belongs in <code>Add search box</code> sits directly under it:</p>\n<pre><code class=\"language-text\">pick b4b3152 Add search box\nfixup 038a3d4 wip\nfixup 872747b Fix typo in search box\nfixup da9c2a6 wip\npick 736ca7c Add search results\ndrop 96eed00 Debug output\n</code></pre>\n<p>We ran this one, and <code>style.css</code> kept both <code>wip</code> rules while <code>results.html</code> lost its <code>&lt;script&gt;</code> line.</p>\n</details>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>In the todo list that <code>git rebase -i</code> opens, which commit runs first?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>The one on the top line, which is the oldest</li>\n<li>The one on the top line, which is the newest</li>\n<li>The one on the bottom line, which is the newest</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. The one on the top line, which is the oldest</p>\n<p>The list is the reverse of <code>git log</code>. Git replays it from the top down, so the oldest commit is first.</p>\n</div>\n</div>\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>You mark a commit <code>fixup</code> in the todo list. What happens to its message?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>It is discarded and the commit above keeps its message</li>\n<li>It replaces the message of the commit above</li>\n<li>Git opens the editor so you can combine both messages</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. It is discarded and the commit above keeps its message</p>\n<p><code>fixup</code> throws the message away. <code>squash</code> is the one that keeps both and opens the editor.</p>\n</div>\n</div>\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>In our scratch repository, why did Git stop when <code>Debug output</code> was moved to the top of the list?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>It edits results.html, which no earlier commit had created yet</li>\n<li>A commit called <code>Debug output</code> cannot be reordered</li>\n<li>Reordering is only allowed after a <code>reword</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. It edits results.html, which no earlier commit had created yet</p>\n<p>The conflict was <code>modify/delete</code>: <code>results.html</code> is created by <code>Add search results</code>, which now came later. Moving a commit above the one it depends on is a common cause of conflicts.</p>\n</div>\n</div>\n</section>\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/interactive-rebase-done-properly/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<ul>\n<li><code>git rebase -i main</code> lists the branch's commits, oldest first, and replays them from the top down.</li>\n<li><code>pick</code>, <code>reword</code>, <code>squash</code>, <code>fixup</code> and <code>drop</code> change what each commit becomes. Lines can be reordered.</li>\n<li>Every replayed commit gets a new hash, because its parent changed.</li>\n<li><code>git commit --fixup &lt;id&gt;</code> followed by <code>git rebase -i --autosquash main</code> puts a late fix inside the commit it belongs to.</li>\n<li><code>git rebase --update-refs main</code> (Git 2.38 or newer) moves the branches stacked below along with it.</li>\n<li><code>git rebase --abort</code> returns to the state before the rebase. <code>git reset --hard ORIG_HEAD</code> does the same after one has finished.</li>\n<li>Rebase commits only you have. After a push, use <code>--force-with-lease</code>, never plain <code>--force</code>, and tell anyone building on the branch.</li>\n</ul>\n<p>Our branch is tidy and ready for review. Sooner or later, a commit that looked fine in review turns out to have broken something, and nobody remembers which one. Next, in <a href=\"https://mnixblog.netlify.app/find-the-commit-that-broke-it/\">Find the Commit That Broke It</a>, we find it without reading the whole history.</p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/interactive-rebase-done-properly/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Interactive Rebase, Done Properly.\nCanonical: https://mnixblog.netlify.app/interactive-rebase-done-properly/\nReword, Squash, Fixup, Drop, and the Ways Back Out\nYour branch is ready for review. Then you run git log --oneline main.. and read it out loud:\n$ git log --oneline main..\n96eed00 Debug output\nda9c2a6 wip\n872747b Fix typo in search box\n736ca7c Add search results\n038a3d4 wip\nb4b3152 Add search box\nThe main.. part means \"the commits on this branch that are not on main\".\nThe hashes on the left (96eed00 and friends) differ for every reader, because a hash comes from the commit's content, author and time. Run git log --oneline main.. in your own repository and use the hashes it prints wherever we type one.\nTwo commits called wip. One that fixes a typo you made three commits earlier. One that prints debug output to the console. A reviewer has to read all of it.\nIn this article, we are going to rewrite that list before anyone else sees it. We will use git rebase -i, which stands for interactive rebase, and by the end the same command will print this:\n87dadb7 Add search results\n41d82a3 Style the search box\n23b3420 Add a search box\nThree commits, each with a message that says what it does. We will also look at the ways out when a rebase goes wrong, and the one case where we should not start it at all.\nThree small commands come up here. git commit --amend rewrites the last commit, git cherry-pick copies one commit onto another branch, and git stash parks uncommitted changes. That is all you need from them. Advanced Git Features (https://mnixblog.netlify.app/advanced-git-features/) teaches each one properly, but you can skip it for now.\nThe scratch repository\nEverything below runs in a throwaway folder. The commands and the output in this article come from Git 2.45.2 on Windows 11, run in Git Bash.\n<details>\n<summary>Build the messy branch yourself (Git Bash, optional)</summary>\nYour hashes will differ from the ones printed here. Everything else will match.\ngit init -b main notes && cd notes\necho \"<h1>Home</h1>\" > index.html\ngit add . && git commit -m \"Add home page\"\necho \"body { margin: 0; }\" > style.css\ngit add . && git commit -m \"Add base styles\"\ngit switch -c feature/search\necho '<input name=\"q\" placeholder=\"Seach\">' > search.html\ngit add . && git commit -m \"Add search box\"\necho \".search { width: 20em; }\" >> style.css\ngit add . && git commit -m \"wip\"\necho \"<ul id=results></ul>\" > results.html\ngit add . && git commit -m \"Add search results\"\necho '<input name=\"q\" placeholder=\"Search\">' > search.html\ngit add . && git commit -m \"Fix typo in search box\"\necho \".search:focus { outline: 2px solid; }\" >> style.css\ngit add . && git commit -m \"wip\"\necho \"<script>console.log('results')</script>\" >> results.html\ngit add . && git commit -m \"Debug output\"\n</details>\nWhat the editor shows you\nThink of a messy first draft. Before you hand it to anyone, you reorder the pages, merge two of them and tear one out. Rebase does that to commits. In Git's words, rebase means \"take these commits and replay them on a different base.\" Interactive means \"show me the list first, so I can change it.\"\ngit rebase -i main\nGit opens your editor with one line per commit on the branch. This is the top of the file:\npick b4b3152 Add search box\npick 038a3d4 wip\npick 736ca7c Add search results\npick 872747b Fix typo in search box\npick da9c2a6 wip\npick 96eed00 Debug output\nBelow the list, Git prints its own cheat sheet in comment lines.\nIf the editor that opens is Vim, you only need four moves. Press i to start typing, and Esc when you are done. To move a whole line, put the cursor on it, press dd to cut it, move to the line above where it should go, and press p to paste it below. Then type :wq and press Enter to save and close.\nTo use another editor instead, set it once. For VS Code that is git config --global core.editor \"code --wait\". The --wait makes Git hold on until you close the tab.\nNotice the order. git log shows the newest commit first. This list shows the oldest first, and Git replays it from the top down.\nEach line starts with a command. These five do nearly everything:\nCommand\nWhat it does\npick\nKeep the commit as it is\nreword\nKeep the commit, and open the editor to change its message\nsquash\nMeld the commit into the one above it, and combine the two messages\nfixup\nMeld the commit into the one above it, and throw its message away\ndrop\nRemove the commit\nYou can also reorder lines. If you delete every line, Git aborts the rebase.\nOur edit\nHere is what we want:\nThe first commit gets a better message, and absorbs the typo fix.\nThe first wip becomes Style the search box, and absorbs the second wip.\nAdd search results stays.\nDebug output goes.\nMove the lines, change the words at the start of each, and save. Use the hashes from your own list:\nreword b4b3152 Add search box\nfixup 872747b Fix typo in search box\nreword 038a3d4 wip\nfixup da9c2a6 wip\npick 736ca7c Add search results\ndrop 96eed00 Debug output\nDropping a commit can conflict with the ones after it. If we had dropped Add search results instead, Debug output would have failed to apply, because it edits results.html, which that commit creates. Git stops with CONFLICT (modify/delete). If that happens, run git rebase --abort, and the branch is back as it was.\nGit works down the list. At each reword, it stops and opens the editor for the message. We typed Add a search box, then Style the search box. When the last line is done:\nSuccessfully rebased and updated refs/heads/feature/search.\n$ git log --oneline main..\n87dadb7 Add search results\n41d82a3 Style the search box\n23b3420 Add a search box\nNote:\nThe editor steps in this article were run with a script standing in for the editor, so the text we saved is exactly what is shown. In your terminal, you will see the editor open instead. The path of the remote is shortened to origin in the push output further down.\nEvery hash changed, including Add search results, which we did not touch. A commit's hash depends on its parent, so replaying on a new parent makes a new commit. That is why rewriting history has consequences, and we will come back to it.\nSquash keeps both messages\nWe used fixup, which keeps the message from the top commit only. squash keeps both and opens the editor so we can combine them. In another scratch branch, we changed Add search results to squash:\npick 1631805 Add search box\nsquash 1a2b9b0 Add search results\npick f557164 Style the search box\nThe editor opens with this at the top:\n# This is a combination of 2 commits.\n# This is the 1st commit message:\nAdd search box\n# This is the commit message #2:\nAdd search results\nEdit the text above the # lines into the one message you want, and save. Use fixup when the second commit is a correction and squash when its message is worth reading.\nFolding in a fix with autosquash\nThe reviewer comes back: the placeholder text should say \"Search notes\". That belongs in Add a search box, which is two commits down. We could amend, but only the latest commit is easy to amend. Instead, with the hash of Add a search box from your own git log --oneline main..:\n$ echo '<input name=\"q\" placeholder=\"Search notes\">' > search.html\n$ git add search.html\n$ git commit --fixup 23b3420\n[feature/search 9c64964] fixup! Add a search box\n1 file changed, 1 insertion(+), 1 deletion(-)\n--fixup takes the hash of the commit the change belongs to, and writes a commit called fixup! Add a search box. Now we ask rebase to put it in the right place:\ngit rebase -i --autosquash main\nGit writes the todo for us. The first lines:\npick 23b3420 Add a search box\nfixup 9c64964 fixup! Add a search box\npick 41d82a3 Style the search box\npick 87dadb7 Add search results\nSave without changing anything, and the fix is inside the first commit. We typed no fixup ourselves.\nTo make --autosquash the default for git rebase -i, set it once:\ngit config rebase.autoSquash true\nAdd --global to set it for every repository. We ran it without --global in a scratch repository, and plain git rebase -i main produced the same todo.\nMoving a stack of branches\nIf this is your first rebase, skip ahead to \"When the rebase goes wrong\". This section is for when one branch sits on top of another.\nSometimes one branch sits on top of another. Here search-history is built on search, and then main moves on:\n* 93e0fe4 (main) Add footer\n| * 7038177 (HEAD -> search-history) Style recent searches\n| * f634c73 Add recent searches\n| * e5efeec (search) Add search results\n| * 4f084fb Add search box\n|/\n* 215815f Add home page\nRebase search-history onto main and look at the branch called search:\n* 09fb68a (HEAD -> search-history) Style recent searches\n* 8281a38 Add recent searches\n* 6e5c6a3 Add search results\n* 397f169 Add search box\n* 93e0fe4 (main) Add footer\n| * e5efeec (search) Add search results\n| * 4f084fb Add search box\n|/\n* 215815f Add home page\nsearch stayed behind. Its commits were replayed, but the label did not follow them. We put everything back with git reset --hard ORIG_HEAD, and ran it again with one flag:\ngit rebase --update-refs main\nSuccessfully rebased and updated refs/heads/search-history.\nUpdated the following refs with --update-refs:\nrefs/heads/search\n* 7e4602a (HEAD -> search-history) Style recent searches\n* e5caf30 Add recent searches\n* 682f791 (search) Add search results\n* 30bb4f9 Add search box\n* 93e0fe4 (main) Add footer\n* 215815f Add home page\nBoth branch names moved. --update-refs is available from Git 2.38, and git config rebase.updateRefs true turns it on for every rebase. Git will not move a branch that is checked out in another worktree.\nWhen the rebase goes wrong\nReordering commits is the easiest way to cause a conflict. Suppose we move Debug output to the top of the todo. That commit edits results.html, which does not exist yet at that point in the replay:\n$ git rebase -i main\nCONFLICT (modify/delete): results.html deleted in HEAD and modified in 96eed00 (Debug output).  Version 96eed00 (Debug output) of results.html left in tree.\nRebasing (1/6)error: could not apply 96eed00... Debug output\nhint: Resolve all conflicts manually, mark them as resolved with\nhint: \"git add/rm <conflicted_files>\", then run \"git rebase --continue\".\nhint: You can instead skip this commit: run \"git rebase --skip\".\nhint: To abort and get back to the state before \"git rebase\", run \"git rebase --abort\".\n$ git status --short\nDU results.html\nGit stopped in the middle. We have three choices:\nFix the file, git add it, and run git rebase --continue.\ngit rebase --skip drops the commit it could not apply.\ngit rebase --abort puts everything back.\n--abort is the one to remember. After it, the branch is exactly as it was before we typed git rebase:\n$ git rebase --abort\n$ git status\nOn branch feature/search\nnothing to commit, working tree clean\n$ git log --oneline main..\n96eed00 Debug output\nda9c2a6 wip\n872747b Fix typo in search box\n736ca7c Add search results\n038a3d4 wip\nb4b3152 Add search box\nAfter the rebase has finished\nMaybe the rebase succeeded and the result is wrong. We dropped a commit that we needed. Git saves where the branch pointed before a rebase in ORIG_HEAD:\n$ git reset --hard ORIG_HEAD\nHEAD is now at 96eed00 Debug output\nAll six original commits are back. Other commands can overwrite ORIG_HEAD, so do this straight away. If you wait, the reflog (https://mnixblog.netlify.app/git-troubleshooting-and-recovery/) still has the old position.\nWhen not to rebase\nIf you have never pushed a branch to a shared remote, you can skip the force-push part for now. The first paragraph is the one to keep.\nRebasing replaces commits with new ones. That is fine for commits only you have. It is a problem for commits somebody else already has.\nWe pushed our branch, then reworded the last commit and pushed again:\n$ git push\n! [rejected]        feature/search -> feature/search (non-fast-forward)\nerror: failed to push some refs to 'origin'\nhint: Updates were rejected because the tip of your current branch is behind\nhint: its remote counterpart. If you want to integrate the remote changes,\nhint: use 'git pull' before pushing again.\nThe remote still has the old commit and we have a new one, so Git sees two histories that diverged. To replace the remote branch we have to force it. --force-with-lease is the careful way to do that:\n$ git push --force-with-lease\n+ eb42e06...9adb2e6 feature/search -> feature/search (forced update)\nIt overwrites the remote branch only if the remote still points where we last saw it. To test that, a second clone pushed a commit to feature/search. Then we rewrote our commit again and pushed:\n$ git push --force-with-lease\n! [rejected]        feature/search -> feature/search (stale info)\nGit refused. Plain git push --force in the same situation did not:\n$ git push --force\n+ 2c44758...7d8835e feature/search -> feature/search (forced update)\nThat commit from the second clone is no longer on the remote branch.\nSo the rule has two parts:\nRebase freely before you push, and on a branch nobody else builds on.\nOnce other people have your commits, merge instead, or agree on the rewrite first.\nRewriting a branch that is already in a pull request is common, and --force-with-lease is how it is done safely. The risk is a teammate who started work on top of those commits. Their copy still has the old ones.\nTry it\nThe whole workflow takes six commands. The terminal knows the scene for each step.\nPractice terminal · tidy a branch with rebase\nType these commands in order, in a practice folder:\nList the commits on your branch that are not on main. Try git log --oneline main... It shows six commits, including two called wip.\nRecord the typo fix as a fixup of the first commit, b4b3152. Try git commit --fixup b4b3152. Git names the new commit fixup! Add search box.\nReplay the branch with the fixup folded in. Try git rebase -i --autosquash main. Without --autosquash, the fixup stays at the bottom of the list.\nRebase a stacked branch onto main and take the branch below it along. Try git rebase --update-refs main.\nA rebase stopped on a conflict, and you want to start over. Try git rebase --abort. The branch goes back to how it was before the rebase.\nA finished rebase dropped a commit you needed. Try git reset --hard ORIG_HEAD. ORIG_HEAD is where the branch pointed before the rebase.\nTry the interactive Git practice terminal on the canonical article (https://mnixblog.netlify.app/interactive-rebase-done-properly/).\nTwo things worth trying: type git rebase -i main at step 3 and read what the terminal says is missing, and run git reflog at step 6.\nChallenge: fold it in the right place\nTake the six-commit branch and rewrite it so that git log --oneline main.. prints exactly two commits, Add search results on top of Add search box. Both wip commits and the typo fix must end up inside Add search box, and Debug output must be gone.\nExpected result: git show HEAD~:search.html prints <input name=\"q\" placeholder=\"Search\">, spelled correctly.\nThe two-line log is the easy part. The typo fix is the trap: if it stays where it was, it folds into Add search results, and the log looks right while the first commit still says Seach.\n<details>\n<summary>Answer</summary>\nMove the lines so every commit that belongs in Add search box sits directly under it:\npick b4b3152 Add search box\nfixup 038a3d4 wip\nfixup 872747b Fix typo in search box\nfixup da9c2a6 wip\npick 736ca7c Add search results\ndrop 96eed00 Debug output\nWe ran this one, and style.css kept both wip rules while results.html lost its <script> line.\n</details>\nQuestion: In the todo list that git rebase -i opens, which commit runs first?\nThe one on the top line, which is the oldest\nThe one on the top line, which is the newest\nThe one on the bottom line, which is the newest\nAnswer: 1. The one on the top line, which is the oldest\nExplanation: The list is the reverse of git log. Git replays it from the top down, so the oldest commit is first.\nQuestion: You mark a commit fixup in the todo list. What happens to its message?\nIt is discarded and the commit above keeps its message\nIt replaces the message of the commit above\nGit opens the editor so you can combine both messages\nAnswer: 1. It is discarded and the commit above keeps its message\nExplanation: fixup throws the message away. squash is the one that keeps both and opens the editor.\nQuestion: In our scratch repository, why did Git stop when Debug output was moved to the top of the list?\nIt edits results.html, which no earlier commit had created yet\nA commit called Debug output cannot be reordered\nReordering is only allowed after a reword\nAnswer: 1. It edits results.html, which no earlier commit had created yet\nExplanation: The conflict was modify/delete: results.html is created by Add search results, which now came later. Moving a commit above the one it depends on is a common cause of conflicts.\nWhat we learned\ngit rebase -i main lists the branch's commits, oldest first, and replays them from the top down.\npick, reword, squash, fixup and drop change what each commit becomes. Lines can be reordered.\nEvery replayed commit gets a new hash, because its parent changed.\ngit commit --fixup <id> followed by git rebase -i --autosquash main puts a late fix inside the commit it belongs to.\ngit rebase --update-refs main (Git 2.38 or newer) moves the branches stacked below along with it.\ngit rebase --abort returns to the state before the rebase. git reset --hard ORIG_HEAD does the same after one has finished.\nRebase commits only you have. After a push, use --force-with-lease, never plain --force, and tell anyone building on the branch.\nOur branch is tidy and ready for review. Sooner or later, a commit that looked fine in review turns out to have broken something, and nobody remembers which one. Next, in Find the Commit That Broke It (https://mnixblog.netlify.app/find-the-commit-that-broke-it/), we find it without reading the whole history.\nPortable export note: 6 raw HTML fragment(s) are shown as literal source because this Markdown export does not execute or preserve raw HTML. View the canonical article (https://mnixblog.netlify.app/interactive-rebase-done-properly/).\nPortable fallback: The authored description of the demo is preserved; the live demonstration remains on the canonical article. Review: Review the authored description of the demo and keep the canonical link for the live version.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/interactive-rebase-done-properly/#ob-discussion)",
      "date_published": "2026-10-27T00:01:00Z",
      "id": "https://mnixblog.netlify.app/interactive-rebase-done-properly/",
      "image": "https://mnixblog.netlify.app/images/interactive-rebase-done-properly/cover.jpg",
      "summary": "Tidy a branch before review with git rebase -i. Squash, reword and fixup commits, fold in a late fix, and recover from a bad rebase.",
      "tags": [
        "Git",
        "GitHub",
        "Command Line"
      ],
      "title": "Interactive Rebase, Done Properly.",
      "url": "https://mnixblog.netlify.app/interactive-rebase-done-properly/"
    },
    {
      "content_html": "<p>Our game works, but its rules and its screen are tangled together in one file of more than four hundred lines. Today we pull them apart, so the same game can run in a terminal now and in a browser later.</p>\n<p>Nothing about the game changes. We will not touch a rule.</p>\n<p>Nobody planned that file. Each article needed one more thing, and each thing went in the next free spot. But look at what is in there: everything above <code>main()</code> knows nothing about a terminal or a screen. Only <code>main()</code> does.</p>\n<p>So we put a wall between them. When we are done, <code>main.rs</code> is fifty-six lines: it creates a game, clicks, and prints. The game itself lives in a library that has never heard of a terminal.</p>\n<p>A library like that, made of rules and state that do not know how they are being displayed, is the heart of what people call a game engine.</p>\n<p>A small one.</p>\n<p>Ours will have a front door and nothing else.</p>\n<p>The program still prints the same thing, a few stages cleared and then an hour of waiting:</p>\n<pre><code class=\"language-text\">One hour away.\nStage 4 -&gt; 131  Level 2 -&gt; 48  Gold 11 -&gt; 17410\n</code></pre>\n<p>Same output.</p>\n<p>Same twenty-one tests, plus one new one.</p>\n<p>Different house.</p>\n<hr />\n<h2 id=\"our-starting-point\">Our starting point<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#our-starting-point\" aria-label=\"Link to this section\">#</a></h2>\n<p>This series builds a small idle game: a player clicks to fight slimes, earns gold and XP, and keeps progressing while you are away.</p>\n<p>If you are only reading along, skip past this code to \"The groups are already visible.\"</p>\n<p>If you are building along, create a project with <code>cargo new rustbound-idle</code> if you do not already have one. The name matters later.</p>\n<p>At this point, <code>src/main.rs</code> holds three structs, an enum, three <code>impl</code> blocks, twelve small functions, and twenty-one tests:</p>\n<pre><code class=\"language-rust\">struct Player {\n    gold: u32,\n    xp: u32,\n    level: u32,\n    click_upgrade_level: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n    gold_reward: u32,\n    xp_reward: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\n#[derive(Debug, PartialEq)]\nenum PurchaseOutcome {\n    Purchased,\n    NotEnoughGold,\n}\n\nimpl Player {\n    fn new() -&gt; Player {\n        Player {\n            gold: 0,\n            xp: 0,\n            level: 1,\n            click_upgrade_level: 0,\n        }\n    }\n\n    fn add_gold(&amp;mut self, amount: u32) {\n        self.gold = self.gold.saturating_add(amount);\n    }\n\n    fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= xp_to_next_level(self.level) {\n            self.xp -= xp_to_next_level(self.level);\n            self.level = self.level.saturating_add(1);\n        }\n    }\n\n    fn base_damage(&amp;self) -&gt; u32 {\n        base_damage_for_level(self.level)\n    }\n\n    fn bonus_damage(&amp;self) -&gt; u32 {\n        click_upgrade_bonus(self.click_upgrade_level)\n    }\n\n    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        let cost = click_upgrade_cost(self.click_upgrade_level);\n\n        if self.gold &lt; cost {\n            return PurchaseOutcome::NotEnoughGold;\n        }\n\n        self.gold -= cost;\n        self.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\n\n        PurchaseOutcome::Purchased\n    }\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n            gold_reward: gold_reward_for_stage(stage),\n            xp_reward: xp_reward_for_stage(stage),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player::new(),\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n        self.resolve_hit();\n    }\n\n    fn tick(&amp;mut self, seconds: u32) {\n        for _ in 0..seconds {\n            let damage = idle_damage_for_level(self.player.level);\n\n            self.enemy.health = health_after_hit(self.enemy.health, damage);\n            self.resolve_hit();\n        }\n    }\n\n    fn resolve_hit(&amp;mut self) {\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.player.add_gold(self.enemy.gold_reward);\n        self.player.add_xp(self.enemy.xp_reward);\n\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n\n    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        self.player.buy_click_upgrade()\n    }\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn gold_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(3)\n}\n\nfn xp_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(4)\n}\n\nfn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n\nfn base_damage_for_level(level: u32) -&gt; u32 {\n    level.saturating_add(4)\n}\n\nfn idle_damage_for_level(level: u32) -&gt; u32 {\n    level\n}\n\nfn click_upgrade_bonus(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_mul(2)\n}\n\nfn click_upgrade_cost(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_add(1).saturating_mul(10)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage(), player.bonus_damage());\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 3 {\n        let stage = game.stage;\n        let level = game.player.level;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\n                \"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\n                game.player.gold,\n                game.player.xp,\n                xp_to_next_level(game.player.level)\n            );\n\n            if game.player.level &gt; level {\n                println!(\n                    \"Level up! Now level {}. Click damage: {}\",\n                    game.player.level,\n                    click_damage(game.player.base_damage(), game.player.bonus_damage())\n                );\n            }\n\n            println!(\n                \"  Next up: {} with {} health.\",\n                game.enemy.name, game.enemy.health\n            );\n        }\n\n        match game.buy_click_upgrade() {\n            PurchaseOutcome::Purchased =&gt; println!(\n                \"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\n                game.player.click_upgrade_level,\n                click_damage(game.player.base_damage(), game.player.bonus_damage()),\n                game.player.gold\n            ),\n            PurchaseOutcome::NotEnoughGold =&gt; {}\n        }\n    }\n\n    let stage = game.stage;\n    let level = game.player.level;\n    let gold = game.player.gold;\n\n    game.tick(3600);\n\n    println!(\"One hour away.\");\n    println!(\n        \"Stage {} -&gt; {}  Level {} -&gt; {}  Gold {} -&gt; {}\",\n        stage, game.stage, level, game.player.level, gold, game.player.gold\n    );\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player::new();\n\n        let mut enemy = Enemy::for_stage(1);\n        enemy.health = 10;\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 5);\n    }\n\n    #[test]\n    fn later_stages_have_tougher_enemies() {\n        assert_eq!(enemy_health_for_stage(1), 20);\n        assert_eq!(enemy_health_for_stage(2), 30);\n    }\n\n    #[test]\n    fn later_stages_pay_better() {\n        assert_eq!(gold_reward_for_stage(1), 5);\n        assert_eq!(gold_reward_for_stage(2), 7);\n        assert_eq!(xp_reward_for_stage(1), 6);\n        assert_eq!(xp_reward_for_stage(2), 8);\n    }\n\n    #[test]\n    fn levels_need_more_xp_as_they_go() {\n        assert_eq!(xp_to_next_level(1), 20);\n        assert_eq!(xp_to_next_level(2), 35);\n    }\n\n    #[test]\n    fn leveling_up_keeps_leftover_xp() {\n        let mut player = Player::new();\n\n        player.add_xp(25);\n\n        assert_eq!(player.level, 2);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn a_big_reward_can_skip_levels() {\n        let mut player = Player::new();\n\n        player.add_xp(60);\n\n        assert_eq!(player.level, 3);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn levels_make_the_player_hit_harder() {\n        assert_eq!(base_damage_for_level(1), 5);\n        assert_eq!(base_damage_for_level(2), 6);\n    }\n\n    #[test]\n    fn upgrades_cost_more_each_time() {\n        assert_eq!(click_upgrade_cost(0), 10);\n        assert_eq!(click_upgrade_cost(1), 20);\n    }\n\n    #[test]\n    fn buying_an_upgrade_needs_gold() {\n        let mut player = Player::new();\n\n        assert_eq!(\n            player.buy_click_upgrade(),\n            PurchaseOutcome::NotEnoughGold\n        );\n        assert_eq!(player.click_upgrade_level, 0);\n        assert_eq!(player.bonus_damage(), 0);\n    }\n\n    #[test]\n    fn buying_an_upgrade_spends_gold() {\n        let mut player = Player::new();\n        player.gold = 12;\n\n        assert_eq!(\n            player.buy_click_upgrade(),\n            PurchaseOutcome::Purchased\n        );\n        assert_eq!(player.gold, 2);\n        assert_eq!(player.click_upgrade_level, 1);\n        assert_eq!(player.bonus_damage(), 2);\n    }\n\n    #[test]\n    fn a_new_game_starts_at_stage_one() {\n        let game = Game::new();\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n        assert_eq!(game.player.gold, 0);\n    }\n\n    #[test]\n    fn clicking_damages_the_enemy() {\n        let mut game = Game::new();\n\n        game.click();\n\n        assert_eq!(game.enemy.health, 15);\n    }\n\n    #[test]\n    fn defeating_an_enemy_pays_the_player() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n\n    #[test]\n    fn idle_damage_grows_with_level() {\n        assert_eq!(idle_damage_for_level(1), 1);\n        assert_eq!(idle_damage_for_level(2), 2);\n    }\n\n    #[test]\n    fn enough_time_defeats_the_enemy() {\n        let mut game = Game::new();\n\n        game.tick(20);\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n\n    #[test]\n    fn one_second_is_one_idle_hit() {\n        let mut game = Game::new();\n\n        game.tick(1);\n\n        assert_eq!(game.enemy.health, 19);\n    }\n\n    #[test]\n    fn zero_seconds_changes_nothing() {\n        let mut game = Game::new();\n\n        game.tick(0);\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n    }\n\n    #[test]\n    fn one_long_tick_matches_many_short_ones() {\n        let mut long = Game::new();\n        let mut short = Game::new();\n\n        long.tick(45);\n\n        for _ in 0..45 {\n            short.tick(1);\n        }\n\n        assert_eq!(long.stage, short.stage);\n        assert_eq!(long.enemy.health, short.enemy.health);\n        assert_eq!(long.player.gold, short.player.gold);\n        assert_eq!(long.player.xp, short.player.xp);\n        assert_eq!(long.player.level, short.player.level);\n    }\n}\n</code></pre>\n<p>Run it, and the player clears three stages, buys an upgrade, and spends an hour away.</p>\n<p>Run <code>cargo test</code>, and all twenty-one tests pass.</p>\n<p>You need a recent Rust version. This project uses edition 2024, which requires Rust 1.85 or newer.</p>\n<p>Hold on to that test count.</p>\n<p>Everything we do today is a reorganization, and the tests are how we prove we did not break anything.</p>\n<hr />\n<h2 id=\"the-groups-are-already-visible\">The groups are already visible<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#the-groups-are-already-visible\" aria-label=\"Link to this section\">#</a></h2>\n<p>Nobody has to invent categories for this code.</p>\n<p>It has been sorting itself since the first article:</p>\n<pre><code class=\"language-text\">rules    click_damage, health_after_hit, is_defeated,\n         enemy_health_for_stage, gold_reward_for_stage,\n         xp_reward_for_stage, xp_to_next_level,\n         base_damage_for_level, idle_damage_for_level,\n         click_upgrade_bonus, click_upgrade_cost\n\nenemy    Enemy\nplayer   Player, PurchaseOutcome\ngame     Game, attack\nmain     everything that prints\n</code></pre>\n<p>Look at the first group.</p>\n<p>Those eleven functions take plain values and return plain values.</p>\n<p>They never touch a <code>Player</code>, an <code>Enemy</code>, or a <code>Game</code>.</p>\n<p>They are the rules of the game, and every other group leans on them.</p>\n<p>Rust has a name for a named group of code: a <strong>module</strong>.</p>\n<p>For this project, we are going to give each top-level module its own file, one at a time, and run the tests after every move.</p>\n<hr />\n<h2 id=\"give-the-rules-their-own-module\">Give the rules their own module<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#give-the-rules-their-own-module\" aria-label=\"Link to this section\">#</a></h2>\n<p>Create a new file next to <code>main.rs</code>:</p>\n<pre><code class=\"language-text\">src/\n├── main.rs\n└── rules.rs\n</code></pre>\n<p>Cut the eleven rule functions out of <code>main.rs</code> and paste them into <code>rules.rs</code>.</p>\n<p>Leave <code>attack()</code> where it is.</p>\n<p>It takes a <code>Player</code> and an <code>Enemy</code>, so it is not one of our plain-value rules. It belongs with the game.</p>\n<p>The tests for those rules should move too.</p>\n<p>Nine of them check nothing but rules:</p>\n<pre><code class=\"language-text\">click_damage_adds_base_and_bonus\nhealth_never_drops_below_zero\nzero_health_is_defeated\nlater_stages_have_tougher_enemies\nlater_stages_pay_better\nlevels_need_more_xp_as_they_go\nlevels_make_the_player_hit_harder\nidle_damage_grows_with_level\nupgrades_cost_more_each_time\n</code></pre>\n<p>Cut each one out of the <code>tests</code> module in <code>main.rs</code>, including the <code>#[test]</code> line above it, and give <code>rules.rs</code> a <code>tests</code> module of its own at the bottom:</p>\n<pre><code class=\"language-rust\">#[cfg(test)]\nmod tests {\n    use super::*;\n\n    // the nine rule tests go here\n}\n</code></pre>\n<p>The <code>use super::*;</code> line means:</p>\n<blockquote>\n<p>Bring in everything from the module above me.</p>\n</blockquote>\n<p>Now <code>main.rs</code> needs to find its rules again.</p>\n<p>Add this to the very top of <code>main.rs</code>, above everything else:</p>\n<pre><code class=\"language-rust\">mod rules;\n\nuse rules::{\n    base_damage_for_level, click_damage,\n    click_upgrade_bonus, click_upgrade_cost,\n    enemy_health_for_stage, gold_reward_for_stage,\n    health_after_hit, idle_damage_for_level,\n    is_defeated, xp_reward_for_stage,\n    xp_to_next_level,\n};\n</code></pre>\n<p><code>mod rules;</code> says:</p>\n<blockquote>\n<p>There is a module called <code>rules</code>, and its code lives in <code>src/rules.rs</code>.</p>\n</blockquote>\n<p>The <code>use</code> line brings its functions into <code>main.rs</code> by name, so the rest of the file can keep calling <code>click_damage()</code> exactly as before.</p>\n<p>Build it:</p>\n<pre><code class=\"language-bash\">cargo build\n</code></pre>\n<p>Rust refuses:</p>\n<pre><code class=\"language-text\">error[E0603]: function `base_damage_for_level` is private\n --&gt; src/main.rs:4:5\n  |\n4 |     base_damage_for_level,\n  |     ^^^^^^^^^^^^^^^^^^^^^ private function\n  |\nnote: the function `base_damage_for_level` is defined here\n --&gt; src/rules.rs:32:1\n  |\n32 | fn base_damage_for_level(level: u32) -&gt; u32 {\n  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n</code></pre>\n<p>You will get one of these for every function.</p>\n<p>That is not a bug in what we did.</p>\n<p>It is our first encounter with one of Rust's module boundaries:</p>\n<blockquote>\n<p><strong>By default, an item is visible inside its module and the modules beneath it. Sibling modules and outside code cannot use it unless we open that boundary.</strong></p>\n</blockquote>\n<p><code>main.rs</code> sits outside the <code>rules</code> module.</p>\n<p>It cannot reach those functions yet.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>After we move the eleven functions into <code>rules.rs</code>, why does <code>main.rs</code> fail with <code>E0603</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>use</code> can only import structs, never functions</li>\n<li>Items are private by default, and <code>main.rs</code> sits outside the <code>rules</code> module</li>\n<li>A module can only be declared inside <code>lib.rs</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Items are private by default, and <code>main.rs</code> sits outside the <code>rules</code> module</p>\n<p>Privacy is the default. An item is visible inside its own module (and the modules beneath it) until we open it up with <code>pub</code>.</p>\n</div>\n</div>\n</section>\n<p>The simplest way to open that boundary is <code>pub</code>, short for <em>public</em>.</p>\n<p>Put it in front of each of the eleven functions:</p>\n<pre><code class=\"language-rust\">pub fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n</code></pre>\n<p>Run both:</p>\n<pre><code class=\"language-bash\">cargo test\ncargo run\n</code></pre>\n<p>Twenty-one tests pass, and the output is the same as before.</p>\n<p>The count should stay at twenty-one after every move in this article.</p>\n<p>We moved eleven functions and nine tests into another file, and nothing changed.</p>\n<p>That is what a good move looks like.</p>\n<p>Try it: a real project puts <code>rules</code> in its own file, but a live example has only one file, so here the module is written inline with <code>mod rules { ... }</code>. It behaves the same way. Run it and you should see <code>Damage: 7</code> and <code>Health left: 23</code>. Then delete the <code>pub</code> in front of <code>click_damage</code> and run it again. That is the same <code>E0603</code> error we just fixed, on purpose this time. Put the <code>pub</code> back when you have read it.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try making it private</figcaption><pre><code class=\"language-rust\">mod rules {\n    pub fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n        base.saturating_add(bonus)\n    }\n\n    pub fn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n        health.saturating_sub(damage)\n    }\n}\n\nuse rules::{click_damage, health_after_hit};\n\nfn main() {\n    let damage = click_damage(5, 2);\n    let health = health_after_hit(30, damage);\n\n    println!(&quot;Damage: {damage}&quot;);\n    println!(&quot;Health left: {health}&quot;);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/\">the original article</a>.</p></figure>\n<hr />\n<h2 id=\"the-enemy\">The enemy<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#the-enemy\" aria-label=\"Link to this section\">#</a></h2>\n<p><code>Enemy</code> is next because it is the smallest.</p>\n<p>Create <code>src/enemy.rs</code>, and move the <code>Enemy</code> struct and its <code>impl</code> block into it.</p>\n<p>The new file needs to find the rules it uses, so it starts like this:</p>\n<pre><code class=\"language-rust\">use crate::rules::{\n    enemy_health_for_stage,\n    gold_reward_for_stage,\n    xp_reward_for_stage,\n};\n</code></pre>\n<p><code>crate</code> means the top of the crate this file belongs to.</p>\n<p>A path beginning with <code>crate::</code> starts there no matter which module we are currently inside.</p>\n<p>For now, all of these modules still belong to the executable crate.</p>\n<p>In a moment we are going to move them into a library crate, and that distinction will matter.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Inside <code>enemy.rs</code>, what does the path <code>crate::rules::xp_reward_for_stage</code> mean?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Start from <code>enemy.rs</code> and look for a nested <code>rules</code> module inside it</li>\n<li>Start from the top of the crate and go to <code>rules</code>, no matter which module we are in</li>\n<li>Look for a <code>rules</code> crate that we installed from outside</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Start from the top of the crate and go to <code>rules</code>, no matter which module we are in</p>\n<p><code>crate::</code> always starts at the root of the crate the file belongs to, so the same path works from any module.</p>\n</div>\n</div>\n</section>\n<p>Back in <code>main.rs</code>, add:</p>\n<pre><code class=\"language-rust\">mod enemy;\n</code></pre>\n<p>under <code>mod rules;</code>, and:</p>\n<pre><code class=\"language-rust\">use enemy::Enemy;\n</code></pre>\n<p>under the rules import.</p>\n<p>Build again.</p>\n<p>Rust has an opinion:</p>\n<pre><code class=\"language-text\">error[E0603]: struct `Enemy` is private\n --&gt; src/main.rs:4:12\n  |\n4 | use enemy::Enemy;\n  |            ^^^^^ private struct\n</code></pre>\n<p>Same boundary, same first fix:</p>\n<pre><code class=\"language-rust\">pub struct Enemy\n</code></pre>\n<p>Build again, and Rust has a few more opinions.</p>\n<p>Making the struct public does not automatically make all of its fields public.</p>\n<p>Each field has its own visibility.</p>\n<p>So does <code>Enemy::for_stage()</code>.</p>\n<p>For now, here is the finished file:</p>\n<pre><code class=\"language-rust\">use crate::rules::{\n    enemy_health_for_stage,\n    gold_reward_for_stage,\n    xp_reward_for_stage,\n};\n\npub struct Enemy {\n    pub name: String,\n    pub health: u32,\n    pub gold_reward: u32,\n    pub xp_reward: u32,\n}\n\nimpl Enemy {\n    pub fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n            gold_reward: gold_reward_for_stage(stage),\n            xp_reward: xp_reward_for_stage(stage),\n        }\n    }\n}\n</code></pre>\n<p>Everything is <code>pub</code> for now.</p>\n<p>That is too generous.</p>\n<p>We are going to fix it on purpose once the boundaries are in place.</p>\n<p>Run:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>Twenty-one pass.</p>\n<p>You may also see warnings that some imports in <code>main.rs</code> are unused.</p>\n<p>That is true.</p>\n<p><code>Enemy</code> was the only caller of <code>enemy_health_for_stage()</code>, <code>gold_reward_for_stage()</code>, and <code>xp_reward_for_stage()</code>, and it just moved.</p>\n<p>Delete those names from the <code>use rules::{...}</code> list in <code>main.rs</code>.</p>\n<hr />\n<h2 id=\"the-player\">The player<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#the-player\" aria-label=\"Link to this section\">#</a></h2>\n<p>Same move, bigger file.</p>\n<p>Create <code>src/player.rs</code>.</p>\n<p>Move in:</p>\n<ul>\n<li><code>Player</code></li>\n<li><code>PurchaseOutcome</code></li>\n<li>the <code>impl Player</code> block</li>\n</ul>\n<p><code>PurchaseOutcome</code> goes with <code>Player</code> because it is the player's vocabulary.</p>\n<p>It is how a purchase answers.</p>\n<p>At the top of the file, import the rules <code>Player</code> uses:</p>\n<pre><code class=\"language-rust\">use crate::rules::{\n    base_damage_for_level,\n    click_upgrade_bonus,\n    click_upgrade_cost,\n    xp_to_next_level,\n};\n</code></pre>\n<p>Then add <code>pub</code> to the struct, its four fields, the enum, and every function in <code>impl Player</code>.</p>\n<p>An enum's variants follow the enum's visibility.</p>\n<p>Make <code>PurchaseOutcome</code> public, and its <code>Purchased</code> and <code>NotEnoughGold</code> variants are available with it.</p>\n<p>Four tests are about the player and nothing else:</p>\n<pre><code class=\"language-text\">leveling_up_keeps_leftover_xp\na_big_reward_can_skip_levels\nbuying_an_upgrade_needs_gold\nbuying_an_upgrade_spends_gold\n</code></pre>\n<p>Move those into a <code>tests</code> module at the bottom of <code>player.rs</code>, the same way we did with the rules.</p>\n<p>In <code>main.rs</code>, add:</p>\n<pre><code class=\"language-rust\">mod player;\n</code></pre>\n<p>and:</p>\n<pre><code class=\"language-rust\">use player::{Player, PurchaseOutcome};\n</code></pre>\n<p><code>Game</code> and <code>attack()</code> still live in <code>main.rs</code> for now, so <code>main.rs</code> still needs <code>Player</code>.</p>\n<p>That import drops out again in the next section.</p>\n<p>Run:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>Twenty-one pass.</p>\n<p>Clean up the new unused-import warnings the same way: remove <code>base_damage_for_level</code>, <code>click_upgrade_bonus</code>, and <code>click_upgrade_cost</code> from the <code>use rules::{...}</code> list in <code>main.rs</code>.</p>\n<hr />\n<h2 id=\"the-game\">The game<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#the-game\" aria-label=\"Link to this section\">#</a></h2>\n<p>Last one.</p>\n<p>Create <code>src/game.rs</code> and move in:</p>\n<ul>\n<li><code>Game</code></li>\n<li>its <code>impl</code> block</li>\n<li><code>attack()</code></li>\n</ul>\n<p>It needs three things from the other modules:</p>\n<pre><code class=\"language-rust\">use crate::enemy::Enemy;\nuse crate::player::{Player, PurchaseOutcome};\nuse crate::rules::{\n    click_damage,\n    health_after_hit,\n    idle_damage_for_level,\n    is_defeated,\n};\n</code></pre>\n<p>Add <code>pub</code> to the struct and its three fields.</p>\n<p>Then add it to the four functions the outside world is going to call:</p>\n<pre><code class=\"language-rust\">pub fn new() -&gt; Game\npub fn click(&amp;mut self)\npub fn tick(&amp;mut self, seconds: u32)\npub fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome\n</code></pre>\n<p>Now look at what is <strong>not</strong> on that list.</p>\n<p><code>resolve_hit()</code> stays private.</p>\n<p><code>defeat_enemy()</code> stays private.</p>\n<p>So does <code>attack()</code>.</p>\n<p>Nothing outside <code>game.rs</code> has a reason to call them.</p>\n<p>When a slime dies, the game decides what that means.</p>\n<p>A caller should not get to say:</p>\n<blockquote>\n<p>Pretend that enemy just died.</p>\n</blockquote>\n<p>We will close the field loophole shortly.</p>\n<p>That is what privacy is for.</p>\n<p>It is not about hiding code for the sake of hiding it.</p>\n<p>It is about deciding what the rest of the program is allowed to depend on.</p>\n<p>Eight tests are about the game, including the one for <code>attack()</code>:</p>\n<pre><code class=\"language-text\">attack_reduces_enemy_health\na_new_game_starts_at_stage_one\nclicking_damages_the_enemy\ndefeating_an_enemy_pays_the_player\none_second_is_one_idle_hit\nzero_seconds_changes_nothing\nenough_time_defeats_the_enemy\none_long_tick_matches_many_short_ones\n</code></pre>\n<p>Move them to the bottom of <code>game.rs</code>.</p>\n<p>In <code>main.rs</code>, add:</p>\n<pre><code class=\"language-rust\">mod game;\n</code></pre>\n<p>and:</p>\n<pre><code class=\"language-rust\">use game::Game;\n</code></pre>\n<p>At this point, the <code>tests</code> module in <code>main.rs</code> is empty.</p>\n<p>Delete it.</p>\n<p>You can also remove <code>Player</code> from:</p>\n<pre><code class=\"language-rust\">use player::{Player, PurchaseOutcome};\n</code></pre>\n<p>because only <code>Game</code> needs it now.</p>\n<p>Clear out any remaining unused imports.</p>\n<p>The top of <code>main.rs</code> should look like this:</p>\n<pre><code class=\"language-rust\">mod enemy;\nmod game;\nmod player;\nmod rules;\n\nuse game::Game;\nuse player::PurchaseOutcome;\nuse rules::{click_damage, xp_to_next_level};\n</code></pre>\n<p>Under that, only <code>main()</code> is left.</p>\n<p>Run both:</p>\n<pre><code class=\"language-bash\">cargo test\ncargo run\n</code></pre>\n<p>Twenty-one tests.</p>\n<p>Same output.</p>\n<p>Four files hold the game.</p>\n<p>One holds the printing.</p>\n<p>But we have not built the wall yet.</p>\n<p><code>main.rs</code> still declares all four modules.</p>\n<p>They are part of the executable crate.</p>\n<p>Nothing outside that executable could use them as a library.</p>\n<hr />\n<h2 id=\"build-the-wall\">Build the wall<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#build-the-wall\" aria-label=\"Link to this section\">#</a></h2>\n<p>A Cargo package can contain both a library target and a binary target:</p>\n<pre><code class=\"language-text\">src/lib.rs    library\nsrc/main.rs   executable\n</code></pre>\n<p>Rust compiles them as separate crates.</p>\n<p>The binary can depend on the library.</p>\n<p>That gives us the wall we have been looking for.</p>\n<p>A <strong>crate</strong> is one compiled unit of Rust code.</p>\n<p>Until now, our project has had one crate: the executable.</p>\n<p>Now our package will contain two:</p>\n<pre><code class=\"language-text\">library crate\n    ↑\nbinary crate\n</code></pre>\n<p>They live in the same project, but Rust treats them as separate pieces of code.</p>\n<p>Create <code>src/lib.rs</code>, and move the four <code>mod</code> lines out of <code>main.rs</code> and into it:</p>\n<pre><code class=\"language-rust\">mod enemy;\nmod game;\nmod player;\nmod rules;\n</code></pre>\n<p>The project is named:</p>\n<pre><code class=\"language-text\">rustbound-idle\n</code></pre>\n<p>Rust turns the hyphen into an underscore when we refer to the library in code:</p>\n<pre><code class=\"language-text\">rustbound_idle\n</code></pre>\n<p>At the top of <code>main.rs</code>, replace all the old <code>mod</code> and <code>use</code> lines with:</p>\n<pre><code class=\"language-rust\">use rustbound_idle::{Game, PurchaseOutcome};\n</code></pre>\n<p>Build it:</p>\n<pre><code class=\"language-bash\">cargo build\n</code></pre>\n<p>Rust complains.</p>\n<p>The first problem looks like this:</p>\n<pre><code class=\"language-text\">error[E0432]: unresolved imports\n  `rustbound_idle::Game`,\n  `rustbound_idle::PurchaseOutcome`\n --&gt; src/main.rs:1:22\n  |\n  |   no `Game` in the root\n  |   no `PurchaseOutcome` in the root\n</code></pre>\n<p>The library has a <code>game</code> module.</p>\n<p>That module has a <code>Game</code>.</p>\n<p>But <code>mod game;</code> is private.</p>\n<p>From outside the library, there is no public path to <code>Game</code>.</p>\n<p>As far as the binary can tell, the library's front door is empty.</p>\n<p>We choose what appears there.</p>\n<p>That is what <code>pub use</code> is for:</p>\n<pre><code class=\"language-rust\">mod enemy;\nmod game;\nmod player;\nmod rules;\n\npub use enemy::Enemy;\npub use game::Game;\npub use player::{Player, PurchaseOutcome};\n</code></pre>\n<p><code>pub use</code> means:</p>\n<blockquote>\n<p>Take this item from inside the library and make it available through this public path.</p>\n</blockquote>\n<p>Now code using the library can write:</p>\n<pre><code class=\"language-rust\">rustbound_idle::Game\n</code></pre>\n<p>Where <code>Game</code> actually lives inside the library is no longer the caller's problem.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong><code>mod game;</code> in <code>lib.rs</code> contains a <code>pub struct Game</code>, yet <code>use rustbound_idle::Game;</code> fails. What is missing?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>A <code>pub</code> in front of every field of <code>Game</code></li>\n<li>A public path to <code>Game</code>, such as <code>pub use game::Game;</code>, because <code>mod game;</code> is private</li>\n<li>A second <code>Game</code> inside <code>main.rs</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. A public path to <code>Game</code>, such as <code>pub use game::Game;</code>, because <code>mod game;</code> is private</p>\n<p>A <code>pub</code> item inside a private module is still unreachable from outside. <code>pub use</code> puts it on the library's front door.</p>\n</div>\n</div>\n</section>\n<p><strong>Coding challenge: Open the front door</strong></p>\n<p>The <code>engine</code> module below hides its <code>enemy</code> module, so <code>main</code> cannot find <code>Enemy</code>. Add one <code>pub use</code> line where the <code>// TODO</code> is.</p>\n<p>Keep the <code>engine</code>, <code>rules</code> and <code>enemy</code> modules and the <code>Enemy</code> struct as they are, because the checks reach for <code>engine::Enemy</code>. <strong>Check my code</strong> will tell you how you are doing; until the line is right it will show the compiler's error instead.</p>\n<p><strong>Expected result:</strong> the starter stops with <code>error[E0432]: unresolved import</code> (the same front-door error as above). With your line added, it prints <code>Training Slime has 50 health</code>.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Open the front door</figcaption><pre><code class=\"language-rust\">mod engine {\n    mod rules {\n        pub(crate) fn enemy_health_for_stage(stage: u32) -&gt; u32 {\n            stage.saturating_mul(10).saturating_add(10)\n        }\n    }\n\n    mod enemy {\n        use crate::engine::rules::enemy_health_for_stage;\n\n        pub struct Enemy {\n            pub name: String,\n            pub health: u32,\n        }\n\n        impl Enemy {\n            pub fn for_stage(stage: u32) -&gt; Enemy {\n                Enemy {\n                    name: String::from(&quot;Training Slime&quot;),\n                    health: enemy_health_for_stage(stage),\n                }\n            }\n        }\n    }\n\n    // TODO: put Enemy on the front door with one `pub use` line\n}\n\nuse engine::Enemy;\n\nfn main() {\n    let enemy = Enemy::for_stage(4);\n\n    println!(&quot;{} has {} health&quot;, enemy.name, enemy.health);\n}\n</code></pre><details class=\"ob-spoiler\"><summary>Spoiler: the checks for this challenge</summary><p>These tests run when you press Check my code on the original article. Reading them first gives away the answer.</p><pre><code class=\"language-rust\">/// From outside `engine`, we should be able to write `engine::Enemy` and build one. Look at who can see that name.\n#[test]\nfn enemy_is_reachable_through_engine() {\n    let enemy = crate::engine::Enemy::for_stage(4);\n    assert_eq!(enemy.health, 50);\n    assert_eq!(enemy.name, &quot;Training Slime&quot;);\n}\n\n/// Calling `main()` should work once the door is open. Every import in the file has to resolve.\n#[test]\nfn main_runs() {\n    main();\n}\n</code></pre></details><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>Inside <code>engine</code>, the <code>enemy</code> module is right beside you, so the path is short: <code>pub use enemy::Enemy;</code>.</p>\n</aside>\n<p>Build again.</p>\n<p>Now the remaining errors are about rules:</p>\n<pre><code class=\"language-text\">error[E0425]: cannot find function `xp_to_next_level` in this scope\nerror[E0425]: cannot find function `click_damage` in this scope\nerror[E0425]: cannot find function `click_damage` in this scope\n</code></pre>\n<p>Those calls used to work because <code>main.rs</code> imported the rules directly.</p>\n<p>Now <code>rules</code> is private inside the library.</p>\n<p>There are two ways we could fix that.</p>\n<p>We could put <code>rules</code> on the front door too:</p>\n<pre><code class=\"language-rust\">pub mod rules;\n</code></pre>\n<p>Then <code>main.rs</code> could import those formulas and keep doing this:</p>\n<pre><code class=\"language-rust\">click_damage(\n    game.player.base_damage(),\n    game.player.bonus_damage()\n)\n</code></pre>\n<p>That would compile.</p>\n<p>But think about what it means.</p>\n<p>Every formula in the game becomes part of the library's public API.</p>\n<p>And the screen keeps doing game arithmetic.</p>\n<p>Today the screen is a terminal.</p>\n<p>Later it will be a browser.</p>\n<p>If both of them know how to calculate damage, we now have multiple callers depending on an internal game rule.</p>\n<p>The rules should live in one place.</p>\n<p>The screen should ask for answers.</p>\n<p>So we take the second path.</p>\n<p>Leave <code>rules</code> private.</p>\n<p>Let <code>Player</code> provide the information the outside world needs.</p>\n<p>Add two methods inside <code>impl Player</code>, after <code>add_xp()</code>:</p>\n<pre><code class=\"language-rust\">pub fn xp_needed(&amp;self) -&gt; u32 {\n    xp_to_next_level(self.level)\n}\n\npub fn click_damage(&amp;self) -&gt; u32 {\n    click_damage(\n        self.base_damage(),\n        self.bonus_damage()\n    )\n}\n</code></pre>\n<p>The method and the rule share a name.</p>\n<p>That is fine.</p>\n<p>Inside the method:</p>\n<pre><code class=\"language-rust\">click_damage(...)\n</code></pre>\n<p>with no <code>self.</code> in front of it means the imported rule.</p>\n<p>The method is the player asking that rule a question about itself.</p>\n<p>First, add <code>click_damage</code> to the <code>use crate::rules::{...}</code> list at the top of <code>player.rs</code>.</p>\n<p>It was not there before because <code>Player</code> used to leave that arithmetic to <code>attack()</code>.</p>\n<p>While we are in there, <code>add_xp()</code> has been calling:</p>\n<pre><code class=\"language-rust\">xp_to_next_level(self.level)\n</code></pre>\n<p>twice.</p>\n<p>Now that the player has a method for that question, replace its loop with:</p>\n<pre><code class=\"language-rust\">while self.xp &gt;= self.xp_needed() {\n    self.xp -= self.xp_needed();\n    self.level = self.level.saturating_add(1);\n}\n</code></pre>\n<p>The loop is still the same loop.</p>\n<p><code>xp_to_next_level</code> stays imported because <code>xp_needed()</code> uses it.</p>\n<p><code>attack()</code> in <code>game.rs</code> can lean on the other new method too:</p>\n<pre><code class=\"language-rust\">fn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    enemy.health =\n        health_after_hit(\n            enemy.health,\n            player.click_damage()\n        );\n}\n</code></pre>\n<p>It no longer needs the <code>click_damage</code> rule, so remove that name from the <code>use</code> statement at the top of <code>game.rs</code>.</p>\n<p>Last, <code>main()</code> gets simpler.</p>\n<p>Where it said:</p>\n<pre><code class=\"language-rust\">xp_to_next_level(game.player.level)\n</code></pre>\n<p>it now says:</p>\n<pre><code class=\"language-rust\">game.player.xp_needed()\n</code></pre>\n<p>And where it said:</p>\n<pre><code class=\"language-rust\">click_damage(\n    game.player.base_damage(),\n    game.player.bonus_damage()\n)\n</code></pre>\n<p>it now says:</p>\n<pre><code class=\"language-rust\">game.player.click_damage()\n</code></pre>\n<p>That second replacement happens twice in <code>main()</code>.</p>\n<p>Build, test, and run:</p>\n<pre><code class=\"language-bash\">cargo test\ncargo run\n</code></pre>\n<p>Same twenty-one tests.</p>\n<p>Same output.</p>\n<p>Cargo also prints test blocks for <code>main.rs</code> and doc-tests that say:</p>\n<pre><code class=\"language-text\">running 0 tests\n</code></pre>\n<p>That is normal.</p>\n<p>And <code>main.rs</code> no longer knows a single game formula.</p>\n<hr />\n<h2 id=\"make-the-front-door-smaller\">Make the front door smaller<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#make-the-front-door-smaller\" aria-label=\"Link to this section\">#</a></h2>\n<p>We said everything was too <code>pub</code>.</p>\n<p>Time to fix that.</p>\n<p>Right now <code>main()</code> can see far more than it needs.</p>\n<p>Try it.</p>\n<p>Add this line just after:</p>\n<pre><code class=\"language-rust\">let mut game = Game::new();\n</code></pre>\n<pre><code class=\"language-rust\">game.player.add_gold(1_000_000);\n</code></pre>\n<p>It compiles.</p>\n<p>A screen can hand the player a million gold, and the game never gets a say.</p>\n<p>Let us close that off.</p>\n<p>Rust has a visibility setting between fully private and fully public:</p>\n<pre><code class=\"language-rust\">pub(crate)\n</code></pre>\n<p>It means:</p>\n<blockquote>\n<p>Visible anywhere inside this crate, but not outside it.</p>\n</blockquote>\n<p>Our library can use the item freely.</p>\n<p><code>main.rs</code> belongs to a different crate now, so it cannot.</p>\n<p>Go through the files and tighten everything <code>main()</code> does not need.</p>\n<p>The rule for now is:</p>\n<blockquote>\n<p><code>pub</code> for what outside code needs. <code>pub(crate)</code> or private for everything else.</p>\n</blockquote>\n<p>In <code>rules.rs</code>, change all eleven:</p>\n<pre><code class=\"language-rust\">pub fn\n</code></pre>\n<p>to:</p>\n<pre><code class=\"language-rust\">pub(crate) fn\n</code></pre>\n<p>In <code>enemy.rs</code>:</p>\n<ul>\n<li>change <code>for_stage()</code> to <code>pub(crate)</code></li>\n<li>make <code>gold_reward</code> and <code>xp_reward</code> <code>pub(crate)</code></li>\n<li>leave <code>name</code> and <code>health</code> public because <code>main()</code> displays them</li>\n</ul>\n<p>In <code>player.rs</code>, change:</p>\n<ul>\n<li><code>new</code></li>\n<li><code>add_gold</code></li>\n<li><code>add_xp</code></li>\n<li><code>buy_click_upgrade</code></li>\n</ul>\n<p>to <code>pub(crate)</code>.</p>\n<p>Take <code>pub</code> off <code>base_damage()</code> and <code>bonus_damage()</code> entirely.</p>\n<p>Only <code>Player</code> needs those now.</p>\n<p>What remains fully public is what the current screen needs:</p>\n<pre><code class=\"language-text\">Game\n  new\n  click\n  tick\n  buy_click_upgrade\n  stage\n  player\n  enemy\n\nPlayer\n  gold\n  xp\n  level\n  click_upgrade_level\n  xp_needed\n  click_damage\n\nEnemy\n  name\n  health\n\nPurchaseOutcome\n</code></pre>\n<p>Now try this again:</p>\n<pre><code class=\"language-rust\">game.player.add_gold(1_000_000);\n</code></pre>\n<p>It fails.</p>\n<p><code>add_gold()</code> is <code>pub(crate)</code>, and <code>main.rs</code> is outside the library crate.</p>\n<p>Delete that line.</p>\n<p>Run:</p>\n<pre><code class=\"language-bash\">cargo test\ncargo run\n</code></pre>\n<p>Nothing changes.</p>\n<p>That is the point.</p>\n<p>If tightening the boundary had broken <code>main()</code>, it would mean <code>main()</code> depended on something we did not actually want to promise publicly.</p>\n<p>Now try to cheat another way.</p>\n<p>In <code>main()</code>, add:</p>\n<pre><code class=\"language-rust\">let secret = game.enemy.gold_reward;\n</code></pre>\n<p>Rust says no:</p>\n<pre><code class=\"language-text\">error[E0616]: field `gold_reward` of struct `Enemy` is private\n</code></pre>\n<p>Delete it.</p>\n<p>Now try reaching for an internal damage detail.</p>\n<p>Replace:</p>\n<pre><code class=\"language-rust\">game.player.click_damage()\n</code></pre>\n<p>with:</p>\n<pre><code class=\"language-rust\">game.player.base_damage()\n</code></pre>\n<p>Rust refuses again:</p>\n<pre><code class=\"language-text\">error[E0624]: method `base_damage` is private\n</code></pre>\n<p>Put it back.</p>\n<p>Those errors are the wall doing its job.</p>\n<p>Try it: here is the same idea in one file. <code>base_damage</code> is private to the <code>player</code> module, and <code>click_damage</code> is the public question the outside world is allowed to ask. Run it and you should see <code>Click damage: 7</code>. Then remove the <code>//</code> in front of the last <code>println!</code> in <code>main</code> and run again. You should get <code>E0624</code>, <code>method base_damage is private</code>, which is exactly the error above.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · reach for the private method</figcaption><pre><code class=\"language-rust\">mod player {\n    pub struct Player {\n        pub level: u32,\n    }\n\n    impl Player {\n        fn base_damage(&amp;self) -&gt; u32 {\n            self.level + 4\n        }\n\n        pub fn click_damage(&amp;self) -&gt; u32 {\n            self.base_damage()\n        }\n    }\n}\n\nuse player::Player;\n\nfn main() {\n    let player = Player { level: 3 };\n\n    println!(&quot;Click damage: {}&quot;, player.click_damage());\n    // println!(&quot;Base damage: {}&quot;, player.base_damage());\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/\">the original article</a>.</p></figure>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Once <code>add_gold()</code> is <code>pub(crate)</code>, why can <code>main()</code> no longer call it?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>pub(crate)</code> is the same as private, so only <code>Player</code> itself can call it</li>\n<li><code>main.rs</code> is now a different crate from the library, and <code>pub(crate)</code> only reaches code inside the same crate</li>\n<li>Methods can never be called on a field like <code>game.player</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. <code>main.rs</code> is now a different crate from the library, and <code>pub(crate)</code> only reaches code inside the same crate</p>\n<p>The rest of the library can still call <code>add_gold()</code>. Only code outside the crate, like our binary, is shut out.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"a-test-from-the-outside\">A test from the outside<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#a-test-from-the-outside\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our twenty-one tests all live <strong>inside</strong> the library, next to the code they check.</p>\n<p>That is useful.</p>\n<p>Those tests can work closely with the modules they belong to.</p>\n<p>But it leaves another question:</p>\n<blockquote>\n<p>Does the library work for code that only gets to use the public front door?</p>\n</blockquote>\n<p>Cargo has a place for that.</p>\n<p>Create a folder called <code>tests</code> next to <code>src</code>, and put a file in it:</p>\n<pre><code class=\"language-text\">rustbound-idle/\n├── src/\n└── tests/\n    └── idle_hour.rs\n</code></pre>\n<p>Each Rust file directly inside <code>tests/</code> is compiled as a separate <strong>integration test crate</strong>.</p>\n<p>It uses the library the same way outside code does: through the public API.</p>\n<p>This is the first code in the project that sees the game the way a stranger would.</p>\n<p>Put this in <code>tests/idle_hour.rs</code>:</p>\n<pre><code class=\"language-rust\">use rustbound_idle::Game;\n\n#[test]\nfn an_hour_away_moves_the_game_forward() {\n    let mut game = Game::new();\n\n    game.tick(3600);\n\n    assert_eq!(game.stage, 128);\n    assert_eq!(game.player.level, 47);\n}\n</code></pre>\n<p>The terminal program clicked through three stages before it waited an hour.</p>\n<p>This test waits an hour from a brand-new game, so the numbers are different:</p>\n<pre><code class=\"language-text\">stage 128\nlevel 47\n</code></pre>\n<p>Run everything:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>Cargo prints a block for each piece it tests.</p>\n<p>You should see:</p>\n<ul>\n<li>the twenty-one tests inside the library</li>\n<li>the new integration test in <code>tests/idle_hour.rs</code></li>\n<li>blocks for <code>main.rs</code> and doc-tests that say <code>running 0 tests</code></li>\n</ul>\n<p>That is fine.</p>\n<p>Twenty-two tests in all.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why can <code>tests/idle_hour.rs</code> not call a <code>pub(crate)</code> function from the library?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Integration tests cannot call any functions at all</li>\n<li>Each file in <code>tests/</code> is its own crate and sees only the public API, like any outside code</li>\n<li><code>pub(crate)</code> items are removed when tests are compiled</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Each file in <code>tests/</code> is its own crate and sees only the public API, like any outside code</p>\n<p>That is what makes it a test from the outside: if it compiles there, a stranger could write the same code.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"one-last-complaint\">One last complaint<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#one-last-complaint\" aria-label=\"Link to this section\">#</a></h2>\n<p>There is one more thing, and it has been waiting for us.</p>\n<p>Clippy is Rust's linter.</p>\n<p>It looks for patterns that compile but could be clearer, more idiomatic, or less error-prone.</p>\n<p>If:</p>\n<pre><code class=\"language-bash\">cargo clippy\n</code></pre>\n<p>is not available, install the component first:</p>\n<pre><code class=\"language-bash\">rustup component add clippy\n</code></pre>\n<p>Then run:</p>\n<pre><code class=\"language-bash\">cargo clippy\n</code></pre>\n<p>Clippy has a suggestion:</p>\n<pre><code class=\"language-text\">warning: you should consider adding a `Default` implementation for `Game`\n</code></pre>\n<p>It did not complain before because <code>Game::new()</code> was not part of a public API.</p>\n<p>Now <code>Game</code> is public, and so is its zero-argument <code>new()</code>.</p>\n<p>Clippy notices that callers may reasonably expect the standard Rust <code>Default</code> trait too.</p>\n<p><code>Default</code> describes a type's standard starting value.</p>\n<p>We already have one:</p>\n<pre><code class=\"language-rust\">Game::new()\n</code></pre>\n<p>So we can say that explicitly:</p>\n<pre><code class=\"language-rust\">impl Default for Game {\n    fn default() -&gt; Game {\n        Game::new()\n    }\n}\n</code></pre>\n<p>Put it in <code>game.rs</code>, between the <code>impl Game</code> block and <code>attack()</code>.</p>\n<p>Now:</p>\n<pre><code class=\"language-rust\">Game::new()\n</code></pre>\n<p>and:</p>\n<pre><code class=\"language-rust\">Game::default()\n</code></pre>\n<p>produce the same starting game.</p>\n<p>Run Clippy again.</p>\n<p>It should be quiet.</p>\n<hr />\n<h2 id=\"the-complete-program\">The complete program<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#the-complete-program\" aria-label=\"Link to this section\">#</a></h2>\n<p>If you are only reading along, skip to the last two sections.</p>\n<p>If you have been following along in your editor, here is everything in one place.</p>\n<p>Your project should now look like this:</p>\n<pre><code class=\"language-text\">rustbound-idle/\n├── Cargo.toml\n├── src/\n│   ├── lib.rs\n│   ├── main.rs\n│   ├── rules.rs\n│   ├── enemy.rs\n│   ├── player.rs\n│   └── game.rs\n└── tests/\n    └── idle_hour.rs\n</code></pre>\n<h3 id=\"src-lib-rs\"><code>src/lib.rs</code><a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#src-lib-rs\" aria-label=\"Link to this section\">#</a></h3>\n<pre><code class=\"language-rust\">mod enemy;\nmod game;\nmod player;\nmod rules;\n\npub use enemy::Enemy;\npub use game::Game;\npub use player::{Player, PurchaseOutcome};\n</code></pre>\n<h3 id=\"src-main-rs\"><code>src/main.rs</code><a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#src-main-rs\" aria-label=\"Link to this section\">#</a></h3>\n<pre><code class=\"language-rust\">use rustbound_idle::{Game, PurchaseOutcome};\n\nfn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 3 {\n        let stage = game.stage;\n        let level = game.player.level;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\n                \"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\n                game.player.gold,\n                game.player.xp,\n                game.player.xp_needed()\n            );\n\n            if game.player.level &gt; level {\n                println!(\n                    \"Level up! Now level {}. Click damage: {}\",\n                    game.player.level,\n                    game.player.click_damage()\n                );\n            }\n\n            println!(\n                \"  Next up: {} with {} health.\",\n                game.enemy.name,\n                game.enemy.health\n            );\n        }\n\n        match game.buy_click_upgrade() {\n            PurchaseOutcome::Purchased =&gt; println!(\n                \"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\n                game.player.click_upgrade_level,\n                game.player.click_damage(),\n                game.player.gold\n            ),\n            PurchaseOutcome::NotEnoughGold =&gt; {}\n        }\n    }\n\n    let stage = game.stage;\n    let level = game.player.level;\n    let gold = game.player.gold;\n\n    game.tick(3600);\n\n    println!(\"One hour away.\");\n    println!(\n        \"Stage {} -&gt; {}  Level {} -&gt; {}  Gold {} -&gt; {}\",\n        stage,\n        game.stage,\n        level,\n        game.player.level,\n        gold,\n        game.player.gold\n    );\n}\n</code></pre>\n<h3 id=\"src-rules-rs\"><code>src/rules.rs</code><a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#src-rules-rs\" aria-label=\"Link to this section\">#</a></h3>\n<pre><code class=\"language-rust\">pub(crate) fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\npub(crate) fn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\npub(crate) fn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\npub(crate) fn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\npub(crate) fn gold_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(3)\n}\n\npub(crate) fn xp_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(4)\n}\n\npub(crate) fn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n\npub(crate) fn base_damage_for_level(level: u32) -&gt; u32 {\n    level.saturating_add(4)\n}\n\npub(crate) fn idle_damage_for_level(level: u32) -&gt; u32 {\n    level\n}\n\npub(crate) fn click_upgrade_bonus(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_mul(2)\n}\n\npub(crate) fn click_upgrade_cost(upgrades: u32) -&gt; u32 {\n    upgrades\n        .saturating_add(1)\n        .saturating_mul(10)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn later_stages_have_tougher_enemies() {\n        assert_eq!(enemy_health_for_stage(1), 20);\n        assert_eq!(enemy_health_for_stage(2), 30);\n    }\n\n    #[test]\n    fn later_stages_pay_better() {\n        assert_eq!(gold_reward_for_stage(1), 5);\n        assert_eq!(gold_reward_for_stage(2), 7);\n        assert_eq!(xp_reward_for_stage(1), 6);\n        assert_eq!(xp_reward_for_stage(2), 8);\n    }\n\n    #[test]\n    fn levels_need_more_xp_as_they_go() {\n        assert_eq!(xp_to_next_level(1), 20);\n        assert_eq!(xp_to_next_level(2), 35);\n    }\n\n    #[test]\n    fn levels_make_the_player_hit_harder() {\n        assert_eq!(base_damage_for_level(1), 5);\n        assert_eq!(base_damage_for_level(2), 6);\n    }\n\n    #[test]\n    fn idle_damage_grows_with_level() {\n        assert_eq!(idle_damage_for_level(1), 1);\n        assert_eq!(idle_damage_for_level(2), 2);\n    }\n\n    #[test]\n    fn upgrades_cost_more_each_time() {\n        assert_eq!(click_upgrade_cost(0), 10);\n        assert_eq!(click_upgrade_cost(1), 20);\n    }\n}\n</code></pre>\n<h3 id=\"src-enemy-rs\"><code>src/enemy.rs</code><a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#src-enemy-rs\" aria-label=\"Link to this section\">#</a></h3>\n<pre><code class=\"language-rust\">use crate::rules::{\n    enemy_health_for_stage,\n    gold_reward_for_stage,\n    xp_reward_for_stage,\n};\n\npub struct Enemy {\n    pub name: String,\n    pub health: u32,\n    pub(crate) gold_reward: u32,\n    pub(crate) xp_reward: u32,\n}\n\nimpl Enemy {\n    pub(crate) fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n            gold_reward: gold_reward_for_stage(stage),\n            xp_reward: xp_reward_for_stage(stage),\n        }\n    }\n}\n</code></pre>\n<h3 id=\"src-player-rs\"><code>src/player.rs</code><a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#src-player-rs\" aria-label=\"Link to this section\">#</a></h3>\n<pre><code class=\"language-rust\">use crate::rules::{\n    base_damage_for_level,\n    click_damage,\n    click_upgrade_bonus,\n    click_upgrade_cost,\n    xp_to_next_level,\n};\n\npub struct Player {\n    pub gold: u32,\n    pub xp: u32,\n    pub level: u32,\n    pub click_upgrade_level: u32,\n}\n\n#[derive(Debug, PartialEq)]\npub enum PurchaseOutcome {\n    Purchased,\n    NotEnoughGold,\n}\n\nimpl Player {\n    pub(crate) fn new() -&gt; Player {\n        Player {\n            gold: 0,\n            xp: 0,\n            level: 1,\n            click_upgrade_level: 0,\n        }\n    }\n\n    pub(crate) fn add_gold(&amp;mut self, amount: u32) {\n        self.gold = self.gold.saturating_add(amount);\n    }\n\n    pub(crate) fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= self.xp_needed() {\n            self.xp -= self.xp_needed();\n            self.level = self.level.saturating_add(1);\n        }\n    }\n\n    pub fn xp_needed(&amp;self) -&gt; u32 {\n        xp_to_next_level(self.level)\n    }\n\n    pub fn click_damage(&amp;self) -&gt; u32 {\n        click_damage(\n            self.base_damage(),\n            self.bonus_damage()\n        )\n    }\n\n    fn base_damage(&amp;self) -&gt; u32 {\n        base_damage_for_level(self.level)\n    }\n\n    fn bonus_damage(&amp;self) -&gt; u32 {\n        click_upgrade_bonus(self.click_upgrade_level)\n    }\n\n    pub(crate) fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        let cost =\n            click_upgrade_cost(self.click_upgrade_level);\n\n        if self.gold &lt; cost {\n            return PurchaseOutcome::NotEnoughGold;\n        }\n\n        self.gold -= cost;\n        self.click_upgrade_level =\n            self.click_upgrade_level.saturating_add(1);\n\n        PurchaseOutcome::Purchased\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn leveling_up_keeps_leftover_xp() {\n        let mut player = Player::new();\n\n        player.add_xp(25);\n\n        assert_eq!(player.level, 2);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn a_big_reward_can_skip_levels() {\n        let mut player = Player::new();\n\n        player.add_xp(60);\n\n        assert_eq!(player.level, 3);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn buying_an_upgrade_needs_gold() {\n        let mut player = Player::new();\n\n        assert_eq!(\n            player.buy_click_upgrade(),\n            PurchaseOutcome::NotEnoughGold\n        );\n        assert_eq!(player.click_upgrade_level, 0);\n        assert_eq!(player.bonus_damage(), 0);\n    }\n\n    #[test]\n    fn buying_an_upgrade_spends_gold() {\n        let mut player = Player::new();\n        player.gold = 12;\n\n        assert_eq!(\n            player.buy_click_upgrade(),\n            PurchaseOutcome::Purchased\n        );\n        assert_eq!(player.gold, 2);\n        assert_eq!(player.click_upgrade_level, 1);\n        assert_eq!(player.bonus_damage(), 2);\n    }\n}\n</code></pre>\n<h3 id=\"src-game-rs\"><code>src/game.rs</code><a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#src-game-rs\" aria-label=\"Link to this section\">#</a></h3>\n<pre><code class=\"language-rust\">use crate::enemy::Enemy;\nuse crate::player::{Player, PurchaseOutcome};\nuse crate::rules::{\n    health_after_hit,\n    idle_damage_for_level,\n    is_defeated,\n};\n\npub struct Game {\n    pub stage: u32,\n    pub player: Player,\n    pub enemy: Enemy,\n}\n\nimpl Game {\n    pub fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player::new(),\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    pub fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n        self.resolve_hit();\n    }\n\n    pub fn tick(&amp;mut self, seconds: u32) {\n        for _ in 0..seconds {\n            let damage =\n                idle_damage_for_level(self.player.level);\n\n            self.enemy.health =\n                health_after_hit(\n                    self.enemy.health,\n                    damage\n                );\n\n            self.resolve_hit();\n        }\n    }\n\n    fn resolve_hit(&amp;mut self) {\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.player.add_gold(self.enemy.gold_reward);\n        self.player.add_xp(self.enemy.xp_reward);\n\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n\n    pub fn buy_click_upgrade(\n        &amp;mut self\n    ) -&gt; PurchaseOutcome {\n        self.player.buy_click_upgrade()\n    }\n}\n\nimpl Default for Game {\n    fn default() -&gt; Game {\n        Game::new()\n    }\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    enemy.health =\n        health_after_hit(\n            enemy.health,\n            player.click_damage()\n        );\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player::new();\n\n        let mut enemy = Enemy::for_stage(1);\n        enemy.health = 10;\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 5);\n    }\n\n    #[test]\n    fn a_new_game_starts_at_stage_one() {\n        let game = Game::new();\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n        assert_eq!(game.player.gold, 0);\n    }\n\n    #[test]\n    fn clicking_damages_the_enemy() {\n        let mut game = Game::new();\n\n        game.click();\n\n        assert_eq!(game.enemy.health, 15);\n    }\n\n    #[test]\n    fn defeating_an_enemy_pays_the_player() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n\n    #[test]\n    fn one_second_is_one_idle_hit() {\n        let mut game = Game::new();\n\n        game.tick(1);\n\n        assert_eq!(game.enemy.health, 19);\n    }\n\n    #[test]\n    fn zero_seconds_changes_nothing() {\n        let mut game = Game::new();\n\n        game.tick(0);\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n    }\n\n    #[test]\n    fn enough_time_defeats_the_enemy() {\n        let mut game = Game::new();\n\n        game.tick(20);\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n\n    #[test]\n    fn one_long_tick_matches_many_short_ones() {\n        let mut long = Game::new();\n        let mut short = Game::new();\n\n        long.tick(45);\n\n        for _ in 0..45 {\n            short.tick(1);\n        }\n\n        assert_eq!(long.stage, short.stage);\n        assert_eq!(\n            long.enemy.health,\n            short.enemy.health\n        );\n        assert_eq!(\n            long.player.gold,\n            short.player.gold\n        );\n        assert_eq!(\n            long.player.xp,\n            short.player.xp\n        );\n        assert_eq!(\n            long.player.level,\n            short.player.level\n        );\n    }\n}\n</code></pre>\n<h3 id=\"tests-idle-hour-rs\"><code>tests/idle_hour.rs</code><a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#tests-idle-hour-rs\" aria-label=\"Link to this section\">#</a></h3>\n<pre><code class=\"language-rust\">use rustbound_idle::Game;\n\n#[test]\nfn an_hour_away_moves_the_game_forward() {\n    let mut game = Game::new();\n\n    game.tick(3600);\n\n    assert_eq!(game.stage, 128);\n    assert_eq!(game.player.level, 47);\n}\n</code></pre>\n<p>Run everything:</p>\n<pre><code class=\"language-bash\">cargo run\ncargo test\ncargo clippy\n</code></pre>\n<p>The game should clear three stages, buy one upgrade, and spend an hour away.</p>\n<p>All twenty-two tests should pass.</p>\n<p>Clippy should be quiet.</p>\n<hr />\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Along the way, we encountered:</p>\n<ul>\n<li>modules, and <code>mod</code> to declare one</li>\n<li>organizing top-level modules into separate files</li>\n<li><code>use</code>, and <code>crate::</code> paths</li>\n<li>Rust's default privacy boundaries</li>\n<li><code>pub</code></li>\n<li><code>pub(crate)</code></li>\n<li><code>pub use</code>, for choosing what appears on a library's front door</li>\n<li>a library crate and a binary crate in the same Cargo package</li>\n<li>tests that live beside their code</li>\n<li>integration tests in <code>tests/</code></li>\n<li><code>Default</code>, which Clippy asked us to provide</li>\n</ul>\n<p>Two ideas are worth keeping.</p>\n<p>The first is that privacy is not about hiding.</p>\n<p>It is about deciding what everyone else is allowed to depend on.</p>\n<p>A private function gives us freedom.</p>\n<p>We can rename it, reorganize it, or replace its implementation without outside code depending on that detail, as long as the public behavior stays the same.</p>\n<p><code>resolve_hit()</code> can move.</p>\n<p><code>base_damage()</code> can change.</p>\n<p>Code outside their visibility boundary does not get to build itself around those details.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>What does keeping <code>resolve_hit()</code> private buy us?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>It runs faster than a public function</li>\n<li>We can change or move it later without breaking any code outside its module</li>\n<li>It stops the compiler from warning about unused code</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. We can change or move it later without breaking any code outside its module</p>\n<p>A public item is a promise to everyone who uses it. A private one is a detail we are free to change.</p>\n</div>\n</div>\n</section>\n<p>The second idea is where the line went.</p>\n<p>We did not decide to build an engine.</p>\n<p>We sorted the code, and the sorting kept asking one question:</p>\n<blockquote>\n<p><em>Does this need to know about a screen?</em></p>\n</blockquote>\n<p>For everything except <code>main()</code>, the answer was no.</p>\n<p>That is the heart of what our little game engine is becoming.</p>\n<p>It is not a graphics library.</p>\n<p>It does not open a window.</p>\n<p>It does not print to a terminal.</p>\n<p>It does not know whether a browser even exists.</p>\n<p>It owns rules and state.</p>\n<p>The game's four main commands are:</p>\n<pre><code class=\"language-text\">new\nclick\ntick\nbuy_click_upgrade\n</code></pre>\n<p>We still expose some state so <code>main()</code> can display it.</p>\n<p>That part of the front door is wider than I want.</p>\n<p><code>Game</code>, <code>Player</code>, and <code>Enemy</code> still have public fields, which means <code>main()</code> could write:</p>\n<pre><code class=\"language-rust\">game.enemy.health = 0;\n</code></pre>\n<p>and the compiler would allow it.</p>\n<p>That is a loose end.</p>\n<p>We are going to lean on it soon.</p>\n<hr />\n<h2 id=\"it-cannot-remember\">It cannot remember<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#it-cannot-remember\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our game can simulate an hour instantly.</p>\n<p>It cannot survive the program ending.</p>\n<p>Close the terminal and everything is gone:</p>\n<pre><code class=\"language-text\">stage\ngold\nXP\nlevel\nupgrades\n</code></pre>\n<p>An idle game that forgets you the moment you leave is not much of an idle game.</p>\n<p>Next time, the game learns to save itself.</p>\n<p>Not to a file.</p>\n<p>Not to a browser.</p>\n<p>Not to a server.</p>\n<p>The library will not know where saves go, and it should not.</p>\n<p>It will only know how to say, in a form that can be written down:</p>\n<blockquote>\n<p>This is everything about me.</p>\n</blockquote>\n<p>And later, given that same form:</p>\n<blockquote>\n<p>I remember.</p>\n</blockquote>\n<p><strong>Small function. Bigger game.</strong></p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "We Accidentally Built a Game Engine.\nCanonical: https://mnixblog.netlify.app/rust-modules-library-crate-split-game/\nSplit Your Rust Game Into Modules and a Library\nOur game works, but its rules and its screen are tangled together in one file of more than four hundred lines. Today we pull them apart, so the same game can run in a terminal now and in a browser later.\nNothing about the game changes. We will not touch a rule.\nNobody planned that file. Each article needed one more thing, and each thing went in the next free spot. But look at what is in there: everything above main() knows nothing about a terminal or a screen. Only main() does.\nSo we put a wall between them. When we are done, main.rs is fifty-six lines: it creates a game, clicks, and prints. The game itself lives in a library that has never heard of a terminal.\nA library like that, made of rules and state that do not know how they are being displayed, is the heart of what people call a game engine.\nA small one.\nOurs will have a front door and nothing else.\nThe program still prints the same thing, a few stages cleared and then an hour of waiting:\nOne hour away.\nStage 4 -> 131  Level 2 -> 48  Gold 11 -> 17410\nSame output.\nSame twenty-one tests, plus one new one.\nDifferent house.\nOur starting point\nThis series builds a small idle game: a player clicks to fight slimes, earns gold and XP, and keeps progressing while you are away.\nIf you are only reading along, skip past this code to \"The groups are already visible.\"\nIf you are building along, create a project with cargo new rustbound-idle if you do not already have one. The name matters later.\nAt this point, src/main.rs holds three structs, an enum, three impl blocks, twelve small functions, and twenty-one tests:\nstruct Player {\ngold: u32,\nxp: u32,\nlevel: u32,\nclick_upgrade_level: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\ngold_reward: u32,\nxp_reward: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\n#[derive(Debug, PartialEq)]\nenum PurchaseOutcome {\nPurchased,\nNotEnoughGold,\n}\nimpl Player {\nfn new() -> Player {\nPlayer {\ngold: 0,\nxp: 0,\nlevel: 1,\nclick_upgrade_level: 0,\n}\n}\nfn add_gold(&mut self, amount: u32) {\nself.gold = self.gold.saturating_add(amount);\n}\nfn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= xp_to_next_level(self.level) {\nself.xp -= xp_to_next_level(self.level);\nself.level = self.level.saturating_add(1);\n}\n}\nfn base_damage(&self) -> u32 {\nbase_damage_for_level(self.level)\n}\nfn bonus_damage(&self) -> u32 {\nclick_upgrade_bonus(self.click_upgrade_level)\n}\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nlet cost = click_upgrade_cost(self.click_upgrade_level);\nif self.gold < cost {\nreturn PurchaseOutcome::NotEnoughGold;\n}\nself.gold -= cost;\nself.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\nPurchaseOutcome::Purchased\n}\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\ngold_reward: gold_reward_for_stage(stage),\nxp_reward: xp_reward_for_stage(stage),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player::new(),\nenemy: Enemy::for_stage(1),\n}\n}\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nself.resolve_hit();\n}\nfn tick(&mut self, seconds: u32) {\nfor _ in 0..seconds {\nlet damage = idle_damage_for_level(self.player.level);\nself.enemy.health = health_after_hit(self.enemy.health, damage);\nself.resolve_hit();\n}\n}\nfn resolve_hit(&mut self) {\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.player.add_gold(self.enemy.gold_reward);\nself.player.add_xp(self.enemy.xp_reward);\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nself.player.buy_click_upgrade()\n}\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn gold_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(3)\n}\nfn xp_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(4)\n}\nfn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\nfn base_damage_for_level(level: u32) -> u32 {\nlevel.saturating_add(4)\n}\nfn idle_damage_for_level(level: u32) -> u32 {\nlevel\n}\nfn click_upgrade_bonus(upgrades: u32) -> u32 {\nupgrades.saturating_mul(2)\n}\nfn click_upgrade_cost(upgrades: u32) -> u32 {\nupgrades.saturating_add(1).saturating_mul(10)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage(), player.bonus_damage());\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 3 {\nlet stage = game.stage;\nlet level = game.player.level;\ngame.click();\nif game.stage > stage {\nprintln!(\n\"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\ngame.player.gold,\ngame.player.xp,\nxp_to_next_level(game.player.level)\n);\nif game.player.level > level {\nprintln!(\n\"Level up! Now level {}. Click damage: {}\",\ngame.player.level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage())\n);\n}\nprintln!(\n\"  Next up: {} with {} health.\",\ngame.enemy.name, game.enemy.health\n);\n}\nmatch game.buy_click_upgrade() {\nPurchaseOutcome::Purchased => println!(\n\"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\ngame.player.click_upgrade_level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage()),\ngame.player.gold\n),\nPurchaseOutcome::NotEnoughGold => {}\n}\n}\nlet stage = game.stage;\nlet level = game.player.level;\nlet gold = game.player.gold;\ngame.tick(3600);\nprintln!(\"One hour away.\");\nprintln!(\n\"Stage {} -> {}  Level {} -> {}  Gold {} -> {}\",\nstage, game.stage, level, game.player.level, gold, game.player.gold\n);\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player::new();\nlet mut enemy = Enemy::for_stage(1);\nenemy.health = 10;\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 5);\n}\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\n#[test]\nfn later_stages_pay_better() {\nassert_eq!(gold_reward_for_stage(1), 5);\nassert_eq!(gold_reward_for_stage(2), 7);\nassert_eq!(xp_reward_for_stage(1), 6);\nassert_eq!(xp_reward_for_stage(2), 8);\n}\n#[test]\nfn levels_need_more_xp_as_they_go() {\nassert_eq!(xp_to_next_level(1), 20);\nassert_eq!(xp_to_next_level(2), 35);\n}\n#[test]\nfn leveling_up_keeps_leftover_xp() {\nlet mut player = Player::new();\nplayer.add_xp(25);\nassert_eq!(player.level, 2);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn a_big_reward_can_skip_levels() {\nlet mut player = Player::new();\nplayer.add_xp(60);\nassert_eq!(player.level, 3);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn levels_make_the_player_hit_harder() {\nassert_eq!(base_damage_for_level(1), 5);\nassert_eq!(base_damage_for_level(2), 6);\n}\n#[test]\nfn upgrades_cost_more_each_time() {\nassert_eq!(click_upgrade_cost(0), 10);\nassert_eq!(click_upgrade_cost(1), 20);\n}\n#[test]\nfn buying_an_upgrade_needs_gold() {\nlet mut player = Player::new();\nassert_eq!(\nplayer.buy_click_upgrade(),\nPurchaseOutcome::NotEnoughGold\n);\nassert_eq!(player.click_upgrade_level, 0);\nassert_eq!(player.bonus_damage(), 0);\n}\n#[test]\nfn buying_an_upgrade_spends_gold() {\nlet mut player = Player::new();\nplayer.gold = 12;\nassert_eq!(\nplayer.buy_click_upgrade(),\nPurchaseOutcome::Purchased\n);\nassert_eq!(player.gold, 2);\nassert_eq!(player.click_upgrade_level, 1);\nassert_eq!(player.bonus_damage(), 2);\n}\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\nassert_eq!(game.player.gold, 0);\n}\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 15);\n}\n#[test]\nfn defeating_an_enemy_pays_the_player() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n#[test]\nfn idle_damage_grows_with_level() {\nassert_eq!(idle_damage_for_level(1), 1);\nassert_eq!(idle_damage_for_level(2), 2);\n}\n#[test]\nfn enough_time_defeats_the_enemy() {\nlet mut game = Game::new();\ngame.tick(20);\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n#[test]\nfn one_second_is_one_idle_hit() {\nlet mut game = Game::new();\ngame.tick(1);\nassert_eq!(game.enemy.health, 19);\n}\n#[test]\nfn zero_seconds_changes_nothing() {\nlet mut game = Game::new();\ngame.tick(0);\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\n}\n#[test]\nfn one_long_tick_matches_many_short_ones() {\nlet mut long = Game::new();\nlet mut short = Game::new();\nlong.tick(45);\nfor _ in 0..45 {\nshort.tick(1);\n}\nassert_eq!(long.stage, short.stage);\nassert_eq!(long.enemy.health, short.enemy.health);\nassert_eq!(long.player.gold, short.player.gold);\nassert_eq!(long.player.xp, short.player.xp);\nassert_eq!(long.player.level, short.player.level);\n}\n}\nRun it, and the player clears three stages, buys an upgrade, and spends an hour away.\nRun cargo test, and all twenty-one tests pass.\nYou need a recent Rust version. This project uses edition 2024, which requires Rust 1.85 or newer.\nHold on to that test count.\nEverything we do today is a reorganization, and the tests are how we prove we did not break anything.\nThe groups are already visible\nNobody has to invent categories for this code.\nIt has been sorting itself since the first article:\nrules    click_damage, health_after_hit, is_defeated,\nenemy_health_for_stage, gold_reward_for_stage,\nxp_reward_for_stage, xp_to_next_level,\nbase_damage_for_level, idle_damage_for_level,\nclick_upgrade_bonus, click_upgrade_cost\nenemy    Enemy\nplayer   Player, PurchaseOutcome\ngame     Game, attack\nmain     everything that prints\nLook at the first group.\nThose eleven functions take plain values and return plain values.\nThey never touch a Player, an Enemy, or a Game.\nThey are the rules of the game, and every other group leans on them.\nRust has a name for a named group of code: a module.\nFor this project, we are going to give each top-level module its own file, one at a time, and run the tests after every move.\nGive the rules their own module\nCreate a new file next to main.rs:\nsrc/\n├── main.rs\n└── rules.rs\nCut the eleven rule functions out of main.rs and paste them into rules.rs.\nLeave attack() where it is.\nIt takes a Player and an Enemy, so it is not one of our plain-value rules. It belongs with the game.\nThe tests for those rules should move too.\nNine of them check nothing but rules:\nclick_damage_adds_base_and_bonus\nhealth_never_drops_below_zero\nzero_health_is_defeated\nlater_stages_have_tougher_enemies\nlater_stages_pay_better\nlevels_need_more_xp_as_they_go\nlevels_make_the_player_hit_harder\nidle_damage_grows_with_level\nupgrades_cost_more_each_time\nCut each one out of the tests module in main.rs, including the #[test] line above it, and give rules.rs a tests module of its own at the bottom:\n#[cfg(test)]\nmod tests {\nuse super::*;\n// the nine rule tests go here\n}\nThe use super::*; line means:\nBring in everything from the module above me.\nNow main.rs needs to find its rules again.\nAdd this to the very top of main.rs, above everything else:\nmod rules;\nuse rules::{\nbase_damage_for_level, click_damage,\nclick_upgrade_bonus, click_upgrade_cost,\nenemy_health_for_stage, gold_reward_for_stage,\nhealth_after_hit, idle_damage_for_level,\nis_defeated, xp_reward_for_stage,\nxp_to_next_level,\n};\nmod rules; says:\nThere is a module called rules, and its code lives in src/rules.rs.\nThe use line brings its functions into main.rs by name, so the rest of the file can keep calling click_damage() exactly as before.\nBuild it:\ncargo build\nRust refuses:\nerror[E0603]: function `base_damage_for_level` is private\n--> src/main.rs:4:5\n|\n4 |     base_damage_for_level,\n|     ^^^^^^^^^^^^^^^^^^^^^ private function\n|\nnote: the function `base_damage_for_level` is defined here\n--> src/rules.rs:32:1\n|\n32 | fn base_damage_for_level(level: u32) -> u32 {\n| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nYou will get one of these for every function.\nThat is not a bug in what we did.\nIt is our first encounter with one of Rust's module boundaries:\nBy default, an item is visible inside its module and the modules beneath it. Sibling modules and outside code cannot use it unless we open that boundary.\nmain.rs sits outside the rules module.\nIt cannot reach those functions yet.\nQuestion: After we move the eleven functions into rules.rs, why does main.rs fail with E0603?\nuse can only import structs, never functions\nItems are private by default, and main.rs sits outside the rules module\nA module can only be declared inside lib.rs\nAnswer: 2. Items are private by default, and main.rs sits outside the rules module\nExplanation: Privacy is the default. An item is visible inside its own module (and the modules beneath it) until we open it up with pub.\nThe simplest way to open that boundary is pub, short for public.\nPut it in front of each of the eleven functions:\npub fn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nRun both:\ncargo test\ncargo run\nTwenty-one tests pass, and the output is the same as before.\nThe count should stay at twenty-one after every move in this article.\nWe moved eleven functions and nine tests into another file, and nothing changed.\nThat is what a good move looks like.\nTry it: a real project puts rules in its own file, but a live example has only one file, so here the module is written inline with mod rules { ... }. It behaves the same way. Run it and you should see Damage: 7 and Health left: 23. Then delete the pub in front of click_damage and run it again. That is the same E0603 error we just fixed, on purpose this time. Put the pub back when you have read it.\nLive example · try making it private\nmod rules {\npub fn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\npub fn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\n}\nuse rules::{click_damage, health_after_hit};\nfn main() {\nlet damage = click_damage(5, 2);\nlet health = health_after_hit(30, damage);\nprintln!(\"Damage: {damage}\");\nprintln!(\"Health left: {health}\");\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/rust-modules-library-crate-split-game/).\nThe enemy\nEnemy is next because it is the smallest.\nCreate src/enemy.rs, and move the Enemy struct and its impl block into it.\nThe new file needs to find the rules it uses, so it starts like this:\nuse crate::rules::{\nenemy_health_for_stage,\ngold_reward_for_stage,\nxp_reward_for_stage,\n};\ncrate means the top of the crate this file belongs to.\nA path beginning with crate:: starts there no matter which module we are currently inside.\nFor now, all of these modules still belong to the executable crate.\nIn a moment we are going to move them into a library crate, and that distinction will matter.\nQuestion: Inside enemy.rs, what does the path crate::rules::xp_reward_for_stage mean?\nStart from enemy.rs and look for a nested rules module inside it\nStart from the top of the crate and go to rules, no matter which module we are in\nLook for a rules crate that we installed from outside\nAnswer: 2. Start from the top of the crate and go to rules, no matter which module we are in\nExplanation: crate:: always starts at the root of the crate the file belongs to, so the same path works from any module.\nBack in main.rs, add:\nmod enemy;\nunder mod rules;, and:\nuse enemy::Enemy;\nunder the rules import.\nBuild again.\nRust has an opinion:\nerror[E0603]: struct `Enemy` is private\n--> src/main.rs:4:12\n|\n4 | use enemy::Enemy;\n|            ^^^^^ private struct\nSame boundary, same first fix:\npub struct Enemy\nBuild again, and Rust has a few more opinions.\nMaking the struct public does not automatically make all of its fields public.\nEach field has its own visibility.\nSo does Enemy::for_stage().\nFor now, here is the finished file:\nuse crate::rules::{\nenemy_health_for_stage,\ngold_reward_for_stage,\nxp_reward_for_stage,\n};\npub struct Enemy {\npub name: String,\npub health: u32,\npub gold_reward: u32,\npub xp_reward: u32,\n}\nimpl Enemy {\npub fn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\ngold_reward: gold_reward_for_stage(stage),\nxp_reward: xp_reward_for_stage(stage),\n}\n}\n}\nEverything is pub for now.\nThat is too generous.\nWe are going to fix it on purpose once the boundaries are in place.\nRun:\ncargo test\nTwenty-one pass.\nYou may also see warnings that some imports in main.rs are unused.\nThat is true.\nEnemy was the only caller of enemy_health_for_stage(), gold_reward_for_stage(), and xp_reward_for_stage(), and it just moved.\nDelete those names from the use rules::{...} list in main.rs.\nThe player\nSame move, bigger file.\nCreate src/player.rs.\nMove in:\nPlayer\nPurchaseOutcome\nthe impl Player block\nPurchaseOutcome goes with Player because it is the player's vocabulary.\nIt is how a purchase answers.\nAt the top of the file, import the rules Player uses:\nuse crate::rules::{\nbase_damage_for_level,\nclick_upgrade_bonus,\nclick_upgrade_cost,\nxp_to_next_level,\n};\nThen add pub to the struct, its four fields, the enum, and every function in impl Player.\nAn enum's variants follow the enum's visibility.\nMake PurchaseOutcome public, and its Purchased and NotEnoughGold variants are available with it.\nFour tests are about the player and nothing else:\nleveling_up_keeps_leftover_xp\na_big_reward_can_skip_levels\nbuying_an_upgrade_needs_gold\nbuying_an_upgrade_spends_gold\nMove those into a tests module at the bottom of player.rs, the same way we did with the rules.\nIn main.rs, add:\nmod player;\nand:\nuse player::{Player, PurchaseOutcome};\nGame and attack() still live in main.rs for now, so main.rs still needs Player.\nThat import drops out again in the next section.\nRun:\ncargo test\nTwenty-one pass.\nClean up the new unused-import warnings the same way: remove base_damage_for_level, click_upgrade_bonus, and click_upgrade_cost from the use rules::{...} list in main.rs.\nThe game\nLast one.\nCreate src/game.rs and move in:\nGame\nits impl block\nattack()\nIt needs three things from the other modules:\nuse crate::enemy::Enemy;\nuse crate::player::{Player, PurchaseOutcome};\nuse crate::rules::{\nclick_damage,\nhealth_after_hit,\nidle_damage_for_level,\nis_defeated,\n};\nAdd pub to the struct and its three fields.\nThen add it to the four functions the outside world is going to call:\npub fn new() -> Game\npub fn click(&mut self)\npub fn tick(&mut self, seconds: u32)\npub fn buy_click_upgrade(&mut self) -> PurchaseOutcome\nNow look at what is not on that list.\nresolve_hit() stays private.\ndefeat_enemy() stays private.\nSo does attack().\nNothing outside game.rs has a reason to call them.\nWhen a slime dies, the game decides what that means.\nA caller should not get to say:\nPretend that enemy just died.\nWe will close the field loophole shortly.\nThat is what privacy is for.\nIt is not about hiding code for the sake of hiding it.\nIt is about deciding what the rest of the program is allowed to depend on.\nEight tests are about the game, including the one for attack():\nattack_reduces_enemy_health\na_new_game_starts_at_stage_one\nclicking_damages_the_enemy\ndefeating_an_enemy_pays_the_player\none_second_is_one_idle_hit\nzero_seconds_changes_nothing\nenough_time_defeats_the_enemy\none_long_tick_matches_many_short_ones\nMove them to the bottom of game.rs.\nIn main.rs, add:\nmod game;\nand:\nuse game::Game;\nAt this point, the tests module in main.rs is empty.\nDelete it.\nYou can also remove Player from:\nuse player::{Player, PurchaseOutcome};\nbecause only Game needs it now.\nClear out any remaining unused imports.\nThe top of main.rs should look like this:\nmod enemy;\nmod game;\nmod player;\nmod rules;\nuse game::Game;\nuse player::PurchaseOutcome;\nuse rules::{click_damage, xp_to_next_level};\nUnder that, only main() is left.\nRun both:\ncargo test\ncargo run\nTwenty-one tests.\nSame output.\nFour files hold the game.\nOne holds the printing.\nBut we have not built the wall yet.\nmain.rs still declares all four modules.\nThey are part of the executable crate.\nNothing outside that executable could use them as a library.\nBuild the wall\nA Cargo package can contain both a library target and a binary target:\nsrc/lib.rs    library\nsrc/main.rs   executable\nRust compiles them as separate crates.\nThe binary can depend on the library.\nThat gives us the wall we have been looking for.\nA crate is one compiled unit of Rust code.\nUntil now, our project has had one crate: the executable.\nNow our package will contain two:\nlibrary crate\n↑\nbinary crate\nThey live in the same project, but Rust treats them as separate pieces of code.\nCreate src/lib.rs, and move the four mod lines out of main.rs and into it:\nmod enemy;\nmod game;\nmod player;\nmod rules;\nThe project is named:\nrustbound-idle\nRust turns the hyphen into an underscore when we refer to the library in code:\nrustbound_idle\nAt the top of main.rs, replace all the old mod and use lines with:\nuse rustbound_idle::{Game, PurchaseOutcome};\nBuild it:\ncargo build\nRust complains.\nThe first problem looks like this:\nerror[E0432]: unresolved imports\n`rustbound_idle::Game`,\n`rustbound_idle::PurchaseOutcome`\n--> src/main.rs:1:22\n|\n|   no `Game` in the root\n|   no `PurchaseOutcome` in the root\nThe library has a game module.\nThat module has a Game.\nBut mod game; is private.\nFrom outside the library, there is no public path to Game.\nAs far as the binary can tell, the library's front door is empty.\nWe choose what appears there.\nThat is what pub use is for:\nmod enemy;\nmod game;\nmod player;\nmod rules;\npub use enemy::Enemy;\npub use game::Game;\npub use player::{Player, PurchaseOutcome};\npub use means:\nTake this item from inside the library and make it available through this public path.\nNow code using the library can write:\nrustbound_idle::Game\nWhere Game actually lives inside the library is no longer the caller's problem.\nQuestion: mod game; in lib.rs contains a pub struct Game, yet use rustbound_idle::Game; fails. What is missing?\nA pub in front of every field of Game\nA public path to Game, such as pub use game::Game;, because mod game; is private\nA second Game inside main.rs\nAnswer: 2. A public path to Game, such as pub use game::Game;, because mod game; is private\nExplanation: A pub item inside a private module is still unreachable from outside. pub use puts it on the library's front door.\nCoding challenge: Open the front door\nThe engine module below hides its enemy module, so main cannot find Enemy. Add one pub use line where the // TODO is.\nKeep the engine, rules and enemy modules and the Enemy struct as they are, because the checks reach for engine::Enemy. Check my code will tell you how you are doing; until the line is right it will show the compiler's error instead.\nExpected result: the starter stops with error[E0432]: unresolved import (the same front-door error as above). With your line added, it prints Training Slime has 50 health.\nOpen the front door\nmod engine {\nmod rules {\npub(crate) fn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\n}\nmod enemy {\nuse crate::engine::rules::enemy_health_for_stage;\npub struct Enemy {\npub name: String,\npub health: u32,\n}\nimpl Enemy {\npub fn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\n}\n}\n}\n}\n// TODO: put Enemy on the front door with one `pub use` line\n}\nuse engine::Enemy;\nfn main() {\nlet enemy = Enemy::for_stage(4);\nprintln!(\"{} has {} health\", enemy.name, enemy.health);\n}\n<details>\n<summary>Spoiler: the checks for this challenge</summary>\nThese tests run when you press Check my code on the original article. Reading them first gives away the answer.\n/// From outside `engine`, we should be able to write `engine::Enemy` and build one. Look at who can see that name.\n#[test]\nfn enemy_is_reachable_through_engine() {\nlet enemy = crate::engine::Enemy::for_stage(4);\nassert_eq!(enemy.health, 50);\nassert_eq!(enemy.name, \"Training Slime\");\n}\n/// Calling `main()` should work once the door is open. Every import in the file has to resolve.\n#[test]\nfn main_runs() {\nmain();\n}\n</details>\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/rust-modules-library-crate-split-game/).\nTip:\nInside engine, the enemy module is right beside you, so the path is short: pub use enemy::Enemy;.\nBuild again.\nNow the remaining errors are about rules:\nerror[E0425]: cannot find function `xp_to_next_level` in this scope\nerror[E0425]: cannot find function `click_damage` in this scope\nerror[E0425]: cannot find function `click_damage` in this scope\nThose calls used to work because main.rs imported the rules directly.\nNow rules is private inside the library.\nThere are two ways we could fix that.\nWe could put rules on the front door too:\npub mod rules;\nThen main.rs could import those formulas and keep doing this:\nclick_damage(\ngame.player.base_damage(),\ngame.player.bonus_damage()\n)\nThat would compile.\nBut think about what it means.\nEvery formula in the game becomes part of the library's public API.\nAnd the screen keeps doing game arithmetic.\nToday the screen is a terminal.\nLater it will be a browser.\nIf both of them know how to calculate damage, we now have multiple callers depending on an internal game rule.\nThe rules should live in one place.\nThe screen should ask for answers.\nSo we take the second path.\nLeave rules private.\nLet Player provide the information the outside world needs.\nAdd two methods inside impl Player, after add_xp():\npub fn xp_needed(&self) -> u32 {\nxp_to_next_level(self.level)\n}\npub fn click_damage(&self) -> u32 {\nclick_damage(\nself.base_damage(),\nself.bonus_damage()\n)\n}\nThe method and the rule share a name.\nThat is fine.\nInside the method:\nclick_damage(...)\nwith no self. in front of it means the imported rule.\nThe method is the player asking that rule a question about itself.\nFirst, add click_damage to the use crate::rules::{...} list at the top of player.rs.\nIt was not there before because Player used to leave that arithmetic to attack().\nWhile we are in there, add_xp() has been calling:\nxp_to_next_level(self.level)\ntwice.\nNow that the player has a method for that question, replace its loop with:\nwhile self.xp >= self.xp_needed() {\nself.xp -= self.xp_needed();\nself.level = self.level.saturating_add(1);\n}\nThe loop is still the same loop.\nxp_to_next_level stays imported because xp_needed() uses it.\nattack() in game.rs can lean on the other new method too:\nfn attack(player: &Player, enemy: &mut Enemy) {\nenemy.health =\nhealth_after_hit(\nenemy.health,\nplayer.click_damage()\n);\n}\nIt no longer needs the click_damage rule, so remove that name from the use statement at the top of game.rs.\nLast, main() gets simpler.\nWhere it said:\nxp_to_next_level(game.player.level)\nit now says:\ngame.player.xp_needed()\nAnd where it said:\nclick_damage(\ngame.player.base_damage(),\ngame.player.bonus_damage()\n)\nit now says:\ngame.player.click_damage()\nThat second replacement happens twice in main().\nBuild, test, and run:\ncargo test\ncargo run\nSame twenty-one tests.\nSame output.\nCargo also prints test blocks for main.rs and doc-tests that say:\nrunning 0 tests\nThat is normal.\nAnd main.rs no longer knows a single game formula.\nMake the front door smaller\nWe said everything was too pub.\nTime to fix that.\nRight now main() can see far more than it needs.\nTry it.\nAdd this line just after:\nlet mut game = Game::new();\ngame.player.add_gold(1_000_000);\nIt compiles.\nA screen can hand the player a million gold, and the game never gets a say.\nLet us close that off.\nRust has a visibility setting between fully private and fully public:\npub(crate)\nIt means:\nVisible anywhere inside this crate, but not outside it.\nOur library can use the item freely.\nmain.rs belongs to a different crate now, so it cannot.\nGo through the files and tighten everything main() does not need.\nThe rule for now is:\npub for what outside code needs. pub(crate) or private for everything else.\nIn rules.rs, change all eleven:\npub fn\nto:\npub(crate) fn\nIn enemy.rs:\nchange for_stage() to pub(crate)\nmake gold_reward and xp_reward pub(crate)\nleave name and health public because main() displays them\nIn player.rs, change:\nnew\nadd_gold\nadd_xp\nbuy_click_upgrade\nto pub(crate).\nTake pub off base_damage() and bonus_damage() entirely.\nOnly Player needs those now.\nWhat remains fully public is what the current screen needs:\nGame\nnew\nclick\ntick\nbuy_click_upgrade\nstage\nplayer\nenemy\nPlayer\ngold\nxp\nlevel\nclick_upgrade_level\nxp_needed\nclick_damage\nEnemy\nname\nhealth\nPurchaseOutcome\nNow try this again:\ngame.player.add_gold(1_000_000);\nIt fails.\nadd_gold() is pub(crate), and main.rs is outside the library crate.\nDelete that line.\nRun:\ncargo test\ncargo run\nNothing changes.\nThat is the point.\nIf tightening the boundary had broken main(), it would mean main() depended on something we did not actually want to promise publicly.\nNow try to cheat another way.\nIn main(), add:\nlet secret = game.enemy.gold_reward;\nRust says no:\nerror[E0616]: field `gold_reward` of struct `Enemy` is private\nDelete it.\nNow try reaching for an internal damage detail.\nReplace:\ngame.player.click_damage()\nwith:\ngame.player.base_damage()\nRust refuses again:\nerror[E0624]: method `base_damage` is private\nPut it back.\nThose errors are the wall doing its job.\nTry it: here is the same idea in one file. base_damage is private to the player module, and click_damage is the public question the outside world is allowed to ask. Run it and you should see Click damage: 7. Then remove the // in front of the last println! in main and run again. You should get E0624, method base_damage is private, which is exactly the error above.\nLive example · reach for the private method\nmod player {\npub struct Player {\npub level: u32,\n}\nimpl Player {\nfn base_damage(&self) -> u32 {\nself.level + 4\n}\npub fn click_damage(&self) -> u32 {\nself.base_damage()\n}\n}\n}\nuse player::Player;\nfn main() {\nlet player = Player { level: 3 };\nprintln!(\"Click damage: {}\", player.click_damage());\n// println!(\"Base damage: {}\", player.base_damage());\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/rust-modules-library-crate-split-game/).\nQuestion: Once add_gold() is pub(crate), why can main() no longer call it?\npub(crate) is the same as private, so only Player itself can call it\nmain.rs is now a different crate from the library, and pub(crate) only reaches code inside the same crate\nMethods can never be called on a field like game.player\nAnswer: 2. main.rs is now a different crate from the library, and pub(crate) only reaches code inside the same crate\nExplanation: The rest of the library can still call add_gold(). Only code outside the crate, like our binary, is shut out.\nA test from the outside\nOur twenty-one tests all live inside the library, next to the code they check.\nThat is useful.\nThose tests can work closely with the modules they belong to.\nBut it leaves another question:\nDoes the library work for code that only gets to use the public front door?\nCargo has a place for that.\nCreate a folder called tests next to src, and put a file in it:\nrustbound-idle/\n├── src/\n└── tests/\n└── idle_hour.rs\nEach Rust file directly inside tests/ is compiled as a separate integration test crate.\nIt uses the library the same way outside code does: through the public API.\nThis is the first code in the project that sees the game the way a stranger would.\nPut this in tests/idle_hour.rs:\nuse rustbound_idle::Game;\n#[test]\nfn an_hour_away_moves_the_game_forward() {\nlet mut game = Game::new();\ngame.tick(3600);\nassert_eq!(game.stage, 128);\nassert_eq!(game.player.level, 47);\n}\nThe terminal program clicked through three stages before it waited an hour.\nThis test waits an hour from a brand-new game, so the numbers are different:\nstage 128\nlevel 47\nRun everything:\ncargo test\nCargo prints a block for each piece it tests.\nYou should see:\nthe twenty-one tests inside the library\nthe new integration test in tests/idle_hour.rs\nblocks for main.rs and doc-tests that say running 0 tests\nThat is fine.\nTwenty-two tests in all.\nQuestion: Why can tests/idle_hour.rs not call a pub(crate) function from the library?\nIntegration tests cannot call any functions at all\nEach file in tests/ is its own crate and sees only the public API, like any outside code\npub(crate) items are removed when tests are compiled\nAnswer: 2. Each file in tests/ is its own crate and sees only the public API, like any outside code\nExplanation: That is what makes it a test from the outside: if it compiles there, a stranger could write the same code.\nOne last complaint\nThere is one more thing, and it has been waiting for us.\nClippy is Rust's linter.\nIt looks for patterns that compile but could be clearer, more idiomatic, or less error-prone.\nIf:\ncargo clippy\nis not available, install the component first:\nrustup component add clippy\nThen run:\ncargo clippy\nClippy has a suggestion:\nwarning: you should consider adding a `Default` implementation for `Game`\nIt did not complain before because Game::new() was not part of a public API.\nNow Game is public, and so is its zero-argument new().\nClippy notices that callers may reasonably expect the standard Rust Default trait too.\nDefault describes a type's standard starting value.\nWe already have one:\nGame::new()\nSo we can say that explicitly:\nimpl Default for Game {\nfn default() -> Game {\nGame::new()\n}\n}\nPut it in game.rs, between the impl Game block and attack().\nNow:\nGame::new()\nand:\nGame::default()\nproduce the same starting game.\nRun Clippy again.\nIt should be quiet.\nThe complete program\nIf you are only reading along, skip to the last two sections.\nIf you have been following along in your editor, here is everything in one place.\nYour project should now look like this:\nrustbound-idle/\n├── Cargo.toml\n├── src/\n│   ├── lib.rs\n│   ├── main.rs\n│   ├── rules.rs\n│   ├── enemy.rs\n│   ├── player.rs\n│   └── game.rs\n└── tests/\n└── idle_hour.rs\nsrc/lib.rs\nmod enemy;\nmod game;\nmod player;\nmod rules;\npub use enemy::Enemy;\npub use game::Game;\npub use player::{Player, PurchaseOutcome};\nsrc/main.rs\nuse rustbound_idle::{Game, PurchaseOutcome};\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 3 {\nlet stage = game.stage;\nlet level = game.player.level;\ngame.click();\nif game.stage > stage {\nprintln!(\n\"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\ngame.player.gold,\ngame.player.xp,\ngame.player.xp_needed()\n);\nif game.player.level > level {\nprintln!(\n\"Level up! Now level {}. Click damage: {}\",\ngame.player.level,\ngame.player.click_damage()\n);\n}\nprintln!(\n\"  Next up: {} with {} health.\",\ngame.enemy.name,\ngame.enemy.health\n);\n}\nmatch game.buy_click_upgrade() {\nPurchaseOutcome::Purchased => println!(\n\"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\ngame.player.click_upgrade_level,\ngame.player.click_damage(),\ngame.player.gold\n),\nPurchaseOutcome::NotEnoughGold => {}\n}\n}\nlet stage = game.stage;\nlet level = game.player.level;\nlet gold = game.player.gold;\ngame.tick(3600);\nprintln!(\"One hour away.\");\nprintln!(\n\"Stage {} -> {}  Level {} -> {}  Gold {} -> {}\",\nstage,\ngame.stage,\nlevel,\ngame.player.level,\ngold,\ngame.player.gold\n);\n}\nsrc/rules.rs\npub(crate) fn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\npub(crate) fn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\npub(crate) fn is_defeated(health: u32) -> bool {\nhealth == 0\n}\npub(crate) fn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\npub(crate) fn gold_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(3)\n}\npub(crate) fn xp_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(4)\n}\npub(crate) fn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\npub(crate) fn base_damage_for_level(level: u32) -> u32 {\nlevel.saturating_add(4)\n}\npub(crate) fn idle_damage_for_level(level: u32) -> u32 {\nlevel\n}\npub(crate) fn click_upgrade_bonus(upgrades: u32) -> u32 {\nupgrades.saturating_mul(2)\n}\npub(crate) fn click_upgrade_cost(upgrades: u32) -> u32 {\nupgrades\n.saturating_add(1)\n.saturating_mul(10)\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\n#[test]\nfn later_stages_pay_better() {\nassert_eq!(gold_reward_for_stage(1), 5);\nassert_eq!(gold_reward_for_stage(2), 7);\nassert_eq!(xp_reward_for_stage(1), 6);\nassert_eq!(xp_reward_for_stage(2), 8);\n}\n#[test]\nfn levels_need_more_xp_as_they_go() {\nassert_eq!(xp_to_next_level(1), 20);\nassert_eq!(xp_to_next_level(2), 35);\n}\n#[test]\nfn levels_make_the_player_hit_harder() {\nassert_eq!(base_damage_for_level(1), 5);\nassert_eq!(base_damage_for_level(2), 6);\n}\n#[test]\nfn idle_damage_grows_with_level() {\nassert_eq!(idle_damage_for_level(1), 1);\nassert_eq!(idle_damage_for_level(2), 2);\n}\n#[test]\nfn upgrades_cost_more_each_time() {\nassert_eq!(click_upgrade_cost(0), 10);\nassert_eq!(click_upgrade_cost(1), 20);\n}\n}\nsrc/enemy.rs\nuse crate::rules::{\nenemy_health_for_stage,\ngold_reward_for_stage,\nxp_reward_for_stage,\n};\npub struct Enemy {\npub name: String,\npub health: u32,\npub(crate) gold_reward: u32,\npub(crate) xp_reward: u32,\n}\nimpl Enemy {\npub(crate) fn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\ngold_reward: gold_reward_for_stage(stage),\nxp_reward: xp_reward_for_stage(stage),\n}\n}\n}\nsrc/player.rs\nuse crate::rules::{\nbase_damage_for_level,\nclick_damage,\nclick_upgrade_bonus,\nclick_upgrade_cost,\nxp_to_next_level,\n};\npub struct Player {\npub gold: u32,\npub xp: u32,\npub level: u32,\npub click_upgrade_level: u32,\n}\n#[derive(Debug, PartialEq)]\npub enum PurchaseOutcome {\nPurchased,\nNotEnoughGold,\n}\nimpl Player {\npub(crate) fn new() -> Player {\nPlayer {\ngold: 0,\nxp: 0,\nlevel: 1,\nclick_upgrade_level: 0,\n}\n}\npub(crate) fn add_gold(&mut self, amount: u32) {\nself.gold = self.gold.saturating_add(amount);\n}\npub(crate) fn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= self.xp_needed() {\nself.xp -= self.xp_needed();\nself.level = self.level.saturating_add(1);\n}\n}\npub fn xp_needed(&self) -> u32 {\nxp_to_next_level(self.level)\n}\npub fn click_damage(&self) -> u32 {\nclick_damage(\nself.base_damage(),\nself.bonus_damage()\n)\n}\nfn base_damage(&self) -> u32 {\nbase_damage_for_level(self.level)\n}\nfn bonus_damage(&self) -> u32 {\nclick_upgrade_bonus(self.click_upgrade_level)\n}\npub(crate) fn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nlet cost =\nclick_upgrade_cost(self.click_upgrade_level);\nif self.gold < cost {\nreturn PurchaseOutcome::NotEnoughGold;\n}\nself.gold -= cost;\nself.click_upgrade_level =\nself.click_upgrade_level.saturating_add(1);\nPurchaseOutcome::Purchased\n}\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn leveling_up_keeps_leftover_xp() {\nlet mut player = Player::new();\nplayer.add_xp(25);\nassert_eq!(player.level, 2);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn a_big_reward_can_skip_levels() {\nlet mut player = Player::new();\nplayer.add_xp(60);\nassert_eq!(player.level, 3);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn buying_an_upgrade_needs_gold() {\nlet mut player = Player::new();\nassert_eq!(\nplayer.buy_click_upgrade(),\nPurchaseOutcome::NotEnoughGold\n);\nassert_eq!(player.click_upgrade_level, 0);\nassert_eq!(player.bonus_damage(), 0);\n}\n#[test]\nfn buying_an_upgrade_spends_gold() {\nlet mut player = Player::new();\nplayer.gold = 12;\nassert_eq!(\nplayer.buy_click_upgrade(),\nPurchaseOutcome::Purchased\n);\nassert_eq!(player.gold, 2);\nassert_eq!(player.click_upgrade_level, 1);\nassert_eq!(player.bonus_damage(), 2);\n}\n}\nsrc/game.rs\nuse crate::enemy::Enemy;\nuse crate::player::{Player, PurchaseOutcome};\nuse crate::rules::{\nhealth_after_hit,\nidle_damage_for_level,\nis_defeated,\n};\npub struct Game {\npub stage: u32,\npub player: Player,\npub enemy: Enemy,\n}\nimpl Game {\npub fn new() -> Game {\nGame {\nstage: 1,\nplayer: Player::new(),\nenemy: Enemy::for_stage(1),\n}\n}\npub fn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nself.resolve_hit();\n}\npub fn tick(&mut self, seconds: u32) {\nfor _ in 0..seconds {\nlet damage =\nidle_damage_for_level(self.player.level);\nself.enemy.health =\nhealth_after_hit(\nself.enemy.health,\ndamage\n);\nself.resolve_hit();\n}\n}\nfn resolve_hit(&mut self) {\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.player.add_gold(self.enemy.gold_reward);\nself.player.add_xp(self.enemy.xp_reward);\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\npub fn buy_click_upgrade(\n&mut self\n) -> PurchaseOutcome {\nself.player.buy_click_upgrade()\n}\n}\nimpl Default for Game {\nfn default() -> Game {\nGame::new()\n}\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nenemy.health =\nhealth_after_hit(\nenemy.health,\nplayer.click_damage()\n);\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player::new();\nlet mut enemy = Enemy::for_stage(1);\nenemy.health = 10;\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 5);\n}\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\nassert_eq!(game.player.gold, 0);\n}\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 15);\n}\n#[test]\nfn defeating_an_enemy_pays_the_player() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n#[test]\nfn one_second_is_one_idle_hit() {\nlet mut game = Game::new();\ngame.tick(1);\nassert_eq!(game.enemy.health, 19);\n}\n#[test]\nfn zero_seconds_changes_nothing() {\nlet mut game = Game::new();\ngame.tick(0);\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\n}\n#[test]\nfn enough_time_defeats_the_enemy() {\nlet mut game = Game::new();\ngame.tick(20);\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n#[test]\nfn one_long_tick_matches_many_short_ones() {\nlet mut long = Game::new();\nlet mut short = Game::new();\nlong.tick(45);\nfor _ in 0..45 {\nshort.tick(1);\n}\nassert_eq!(long.stage, short.stage);\nassert_eq!(\nlong.enemy.health,\nshort.enemy.health\n);\nassert_eq!(\nlong.player.gold,\nshort.player.gold\n);\nassert_eq!(\nlong.player.xp,\nshort.player.xp\n);\nassert_eq!(\nlong.player.level,\nshort.player.level\n);\n}\n}\ntests/idle_hour.rs\nuse rustbound_idle::Game;\n#[test]\nfn an_hour_away_moves_the_game_forward() {\nlet mut game = Game::new();\ngame.tick(3600);\nassert_eq!(game.stage, 128);\nassert_eq!(game.player.level, 47);\n}\nRun everything:\ncargo run\ncargo test\ncargo clippy\nThe game should clear three stages, buy one upgrade, and spend an hour away.\nAll twenty-two tests should pass.\nClippy should be quiet.\nWhat we learned\nAlong the way, we encountered:\nmodules, and mod to declare one\norganizing top-level modules into separate files\nuse, and crate:: paths\nRust's default privacy boundaries\npub\npub(crate)\npub use, for choosing what appears on a library's front door\na library crate and a binary crate in the same Cargo package\ntests that live beside their code\nintegration tests in tests/\nDefault, which Clippy asked us to provide\nTwo ideas are worth keeping.\nThe first is that privacy is not about hiding.\nIt is about deciding what everyone else is allowed to depend on.\nA private function gives us freedom.\nWe can rename it, reorganize it, or replace its implementation without outside code depending on that detail, as long as the public behavior stays the same.\nresolve_hit() can move.\nbase_damage() can change.\nCode outside their visibility boundary does not get to build itself around those details.\nQuestion: What does keeping resolve_hit() private buy us?\nIt runs faster than a public function\nWe can change or move it later without breaking any code outside its module\nIt stops the compiler from warning about unused code\nAnswer: 2. We can change or move it later without breaking any code outside its module\nExplanation: A public item is a promise to everyone who uses it. A private one is a detail we are free to change.\nThe second idea is where the line went.\nWe did not decide to build an engine.\nWe sorted the code, and the sorting kept asking one question:\nDoes this need to know about a screen?\nFor everything except main(), the answer was no.\nThat is the heart of what our little game engine is becoming.\nIt is not a graphics library.\nIt does not open a window.\nIt does not print to a terminal.\nIt does not know whether a browser even exists.\nIt owns rules and state.\nThe game's four main commands are:\nnew\nclick\ntick\nbuy_click_upgrade\nWe still expose some state so main() can display it.\nThat part of the front door is wider than I want.\nGame, Player, and Enemy still have public fields, which means main() could write:\ngame.enemy.health = 0;\nand the compiler would allow it.\nThat is a loose end.\nWe are going to lean on it soon.\nIt cannot remember\nOur game can simulate an hour instantly.\nIt cannot survive the program ending.\nClose the terminal and everything is gone:\nstage\ngold\nXP\nlevel\nupgrades\nAn idle game that forgets you the moment you leave is not much of an idle game.\nNext time, the game learns to save itself.\nNot to a file.\nNot to a browser.\nNot to a server.\nThe library will not know where saves go, and it should not.\nIt will only know how to say, in a form that can be written down:\nThis is everything about me.\nAnd later, given that same form:\nI remember.\nSmall function. Bigger game.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2026-10-25T15:09:41Z",
      "id": "https://mnixblog.netlify.app/rust-modules-library-crate-split-game/",
      "image": "https://mnixblog.netlify.app/images/rust-modules-library-crate-split-game/cover.jpg",
      "summary": "Split a Rust game into modules and a library crate, step by step, with tests passing after every move. Learn mod, pub, and pub use.",
      "tags": [
        "Rust",
        "WebAssembly",
        "Game Development",
        "Beginners"
      ],
      "title": "We Accidentally Built a Game Engine.",
      "url": "https://mnixblog.netlify.app/rust-modules-library-crate-split-game/"
    },
    {
      "content_html": "<p>Our game has a progression loop now.</p>\n<p>Kill a slime, get paid, level up, buy damage, kill a tougher slime.</p>\n<p>It still stops the moment you stop clicking.</p>\n<p>So today we are going to make time part of the game.</p>\n<p>Not a timer. Not a clock. Not <code>sleep</code>.</p>\n<p>Just a number.</p>\n<p>Because the game will never look at a clock, we can test a full hour away in a fraction of a second.</p>\n<p>By the end, an hour away from the game is one line:</p>\n<pre><code class=\"language-rust\">game.tick(3600);\n</code></pre>\n<p>And here is what the game looks like when you come back:</p>\n<pre><code class=\"language-text\">One hour away.\nStage 4 -&gt; 131  Level 2 -&gt; 48  Gold 11 -&gt; 17410\n</code></pre>\n<hr />\n<h2 id=\"our-starting-point\">Our starting point<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#our-starting-point\" aria-label=\"Link to this section\">#</a></h2>\n<p>If you are only reading along, you can skip past this code to \"Time is just a number.\"</p>\n<p>If you are building along, you need three structs, an enum, three <code>impl</code> blocks, eleven small functions, and sixteen tests:</p>\n<pre><code class=\"language-rust\">struct Player {\n    gold: u32,\n    xp: u32,\n    level: u32,\n    click_upgrade_level: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n    gold_reward: u32,\n    xp_reward: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\n#[derive(Debug, PartialEq)]\nenum PurchaseOutcome {\n    Purchased,\n    NotEnoughGold,\n}\n\nimpl Player {\n    fn new() -&gt; Player {\n        Player {\n            gold: 0,\n            xp: 0,\n            level: 1,\n            click_upgrade_level: 0,\n        }\n    }\n\n    fn add_gold(&amp;mut self, amount: u32) {\n        self.gold = self.gold.saturating_add(amount);\n    }\n\n    fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= xp_to_next_level(self.level) {\n            self.xp -= xp_to_next_level(self.level);\n            self.level = self.level.saturating_add(1);\n        }\n    }\n\n    fn base_damage(&amp;self) -&gt; u32 {\n        base_damage_for_level(self.level)\n    }\n\n    fn bonus_damage(&amp;self) -&gt; u32 {\n        click_upgrade_bonus(self.click_upgrade_level)\n    }\n\n    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        let cost = click_upgrade_cost(self.click_upgrade_level);\n\n        if self.gold &lt; cost {\n            return PurchaseOutcome::NotEnoughGold;\n        }\n\n        self.gold -= cost;\n        self.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\n\n        PurchaseOutcome::Purchased\n    }\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n            gold_reward: gold_reward_for_stage(stage),\n            xp_reward: xp_reward_for_stage(stage),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player::new(),\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.player.add_gold(self.enemy.gold_reward);\n        self.player.add_xp(self.enemy.xp_reward);\n\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n\n    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        self.player.buy_click_upgrade()\n    }\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn gold_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(3)\n}\n\nfn xp_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(4)\n}\n\nfn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n\nfn base_damage_for_level(level: u32) -&gt; u32 {\n    level.saturating_add(4)\n}\n\nfn click_upgrade_bonus(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_mul(2)\n}\n\nfn click_upgrade_cost(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_add(1).saturating_mul(10)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage(), player.bonus_damage());\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 8 {\n        let stage = game.stage;\n        let level = game.player.level;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\n                \"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\n                game.player.gold,\n                game.player.xp,\n                xp_to_next_level(game.player.level)\n            );\n\n            if game.player.level &gt; level {\n                println!(\n                    \"Level up! Now level {}. Click damage: {}\",\n                    game.player.level,\n                    click_damage(game.player.base_damage(), game.player.bonus_damage())\n                );\n            }\n\n            println!(\n                \"  Next up: {} with {} health.\",\n                game.enemy.name, game.enemy.health\n            );\n        }\n\n        match game.buy_click_upgrade() {\n            PurchaseOutcome::Purchased =&gt; println!(\n                \"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\n                game.player.click_upgrade_level,\n                click_damage(game.player.base_damage(), game.player.bonus_damage()),\n                game.player.gold\n            ),\n            PurchaseOutcome::NotEnoughGold =&gt; {}\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player::new();\n\n        let mut enemy = Enemy::for_stage(1);\n        enemy.health = 10;\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 5);\n    }\n\n    #[test]\n    fn later_stages_have_tougher_enemies() {\n        assert_eq!(enemy_health_for_stage(1), 20);\n        assert_eq!(enemy_health_for_stage(2), 30);\n    }\n\n    #[test]\n    fn later_stages_pay_better() {\n        assert_eq!(gold_reward_for_stage(1), 5);\n        assert_eq!(gold_reward_for_stage(2), 7);\n        assert_eq!(xp_reward_for_stage(1), 6);\n        assert_eq!(xp_reward_for_stage(2), 8);\n    }\n\n    #[test]\n    fn levels_need_more_xp_as_they_go() {\n        assert_eq!(xp_to_next_level(1), 20);\n        assert_eq!(xp_to_next_level(2), 35);\n    }\n\n    #[test]\n    fn leveling_up_keeps_leftover_xp() {\n        let mut player = Player::new();\n\n        player.add_xp(25);\n\n        assert_eq!(player.level, 2);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn a_big_reward_can_skip_levels() {\n        let mut player = Player::new();\n\n        player.add_xp(60);\n\n        assert_eq!(player.level, 3);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn levels_make_the_player_hit_harder() {\n        assert_eq!(base_damage_for_level(1), 5);\n        assert_eq!(base_damage_for_level(2), 6);\n    }\n\n    #[test]\n    fn upgrades_cost_more_each_time() {\n        assert_eq!(click_upgrade_cost(0), 10);\n        assert_eq!(click_upgrade_cost(1), 20);\n    }\n\n    #[test]\n    fn buying_an_upgrade_needs_gold() {\n        let mut player = Player::new();\n\n        assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::NotEnoughGold);\n        assert_eq!(player.click_upgrade_level, 0);\n        assert_eq!(player.bonus_damage(), 0);\n    }\n\n    #[test]\n    fn buying_an_upgrade_spends_gold() {\n        let mut player = Player::new();\n        player.gold = 12;\n\n        assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased);\n        assert_eq!(player.gold, 2);\n        assert_eq!(player.click_upgrade_level, 1);\n        assert_eq!(player.bonus_damage(), 2);\n    }\n\n    #[test]\n    fn a_new_game_starts_at_stage_one() {\n        let game = Game::new();\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n        assert_eq!(game.player.gold, 0);\n    }\n\n    #[test]\n    fn clicking_damages_the_enemy() {\n        let mut game = Game::new();\n\n        game.click();\n\n        assert_eq!(game.enemy.health, 15);\n    }\n\n    #[test]\n    fn defeating_an_enemy_pays_the_player() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n}\n</code></pre>\n<p>Run it, and the player clears eight stages and buys three upgrades.</p>\n<p>Then the program is over, and so is all the progress, because nothing in it ever happens on its own.</p>\n<hr />\n<h2 id=\"time-is-just-a-number\">Time is just a number<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#time-is-just-a-number\" aria-label=\"Link to this section\">#</a></h2>\n<p>An idle game has to keep going while nobody is looking.</p>\n<p>The obvious way to build that is to give the game a clock.</p>\n<p>Start a timer. Every second, do some work. When the player leaves, keep the timer running.</p>\n<p>That sounds simple, and it is a trap.</p>\n<p>A game that watches a clock is a game that has to be tested by waiting, unless you build a fake clock, and then you have rebuilt the idea we are about to use anyway.</p>\n<p>Want to know what happens after an hour away? Wait an hour.</p>\n<p>So let's turn it around.</p>\n<p>The game will not ask what time it is.</p>\n<p>Whoever is running the game will tell it how much time went by:</p>\n<pre><code class=\"language-rust\">game.tick(10);\n</code></pre>\n<p>Ten seconds passed. What happened?</p>\n<p>Game code often uses the word <em>tick</em> for advancing simulation time, so we will borrow the name.</p>\n<p>The contract is one sentence:</p>\n<blockquote>\n<p>Tell me how many seconds passed. I will tell you what the game became.</p>\n</blockquote>\n<p><code>tick</code> will be a method on <code>Game</code>, and the only thing it takes is that number.</p>\n<p>The number is the only thing about time that Rust ever sees.</p>\n<p>Later in this series, a web page will measure real time and hand the game that number.</p>\n<p>For now, our tests can hand it any number they like.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does <code>tick(seconds)</code> take a number instead of reading a clock?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>A test can hand it an hour of time and finish instantly</li>\n<li>Rust has no way to read a clock at all</li>\n<li>A <code>u32</code> automatically counts real seconds</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. A test can hand it an hour of time and finish instantly</p>\n<p>The game only sees a number, so whoever calls it decides how much time went by. A test can say <code>3600</code> without waiting.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"what-does-a-player-do-while-nobody-is-clicking\">What does a player do while nobody is clicking?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#what-does-a-player-do-while-nobody-is-clicking\" aria-label=\"Link to this section\">#</a></h2>\n<p>Before the game can work out what happened, it needs to know what the player does with spare seconds.</p>\n<p>Our player is going to keep fighting.</p>\n<p>One hit per second, and the harder the player has leveled up, the harder it hits.</p>\n<p>That is a game rule, and we know what to do with game rules by now.</p>\n<p>Give it a small function, next to the others:</p>\n<pre><code class=\"language-rust\">fn idle_damage_for_level(level: u32) -&gt; u32 {\n    level\n}\n</code></pre>\n<p>One damage per second at level 1.</p>\n<p>Two at level 2.</p>\n<p>It is deliberately small. It is not balanced and it does not need to be yet.</p>\n<p>It also ignores the click upgrades on purpose. Those are for clicking.</p>\n<p>What matters is that the rule has a name and a test.</p>\n<p>Add it at the bottom of the <code>tests</code> module:</p>\n<pre><code class=\"language-rust\">#[test]\nfn idle_damage_grows_with_level() {\n    assert_eq!(idle_damage_for_level(1), 1);\n    assert_eq!(idle_damage_for_level(2), 2);\n}\n</code></pre>\n<p>Notice what the function does not take.</p>\n<p>It does not take a <code>Player</code>. It does not take a <code>Game</code>.</p>\n<p>It takes a level and returns a number. It is the same kind of function as <code>base_damage_for_level()</code>, and it asks the same question: what does this level of progress get you?</p>\n<p>If you run <code>cargo build</code> now, Rust warns that <code>idle_damage_for_level</code> is never used.</p>\n<p>We are about to use it.</p>\n<hr />\n<h2 id=\"one-second-at-a-time\">One second at a time<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#one-second-at-a-time\" aria-label=\"Link to this section\">#</a></h2>\n<p>Now for <code>tick()</code>.</p>\n<p>The player hits once per second, so ten seconds is ten hits.</p>\n<p>We know exactly how many times to repeat: once per second that passed.</p>\n<p>That is not a job for <code>while</code>.</p>\n<p>A <code>while</code> loop keeps going until a condition stops being true, like our level-up loop, which cannot know how many levels one reward will cross.</p>\n<p>When you know the count in advance, Rust has a loop for that:</p>\n<pre><code class=\"language-rust\">for _ in 0..seconds {\n    // one second of work\n}\n</code></pre>\n<p><code>0..seconds</code> is a range.</p>\n<p>It counts from 0 up to, but not including, <code>seconds</code>.</p>\n<p>So <code>0..3</code> gives you 0, 1, and 2. That is three trips through the loop, which is exactly what we want for three seconds.</p>\n<p>Try it: run this, then change <code>seconds</code> to <code>0</code> and to <code>5</code>, and count the trips.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try changing the seconds away</figcaption><pre><code class=\"language-rust\">fn main() {\n    let seconds: u32 = 3;\n    let mut trips = 0;\n\n    for second in 0..seconds {\n        println!(&quot;trip with range value {second}&quot;);\n        trips += 1;\n    }\n\n    println!(&quot;{seconds} seconds away, {trips} trips through the loop&quot;);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/\">the original article</a>.</p></figure>\n<p>With <code>3</code> you should see range values <code>0</code>, <code>1</code> and <code>2</code>, then <code>3 seconds away, 3 trips through the loop</code>. With <code>0</code> the loop never runs, and the last line says <code>0 trips</code>.</p>\n<p>The <code>_</code> is a placeholder that means:</p>\n<blockquote>\n<p>I do not need this value.</p>\n</blockquote>\n<p>A <code>for</code> loop hands us the current number from the range on every trip.</p>\n<p>We do not care which second we are on. We only care that one passed.</p>\n<p>If we gave the number a name like <code>second</code>, Rust would warn that we never use it.</p>\n<p>The <code>_</code> says that is on purpose.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why is <code>tick()</code> a <code>for</code> loop instead of a <code>while</code> loop?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>It is given the number of seconds, so the count is known in advance</li>\n<li>A <code>while</code> loop can only run once</li>\n<li>A <code>for</code> loop skips the enemy health check</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. It is given the number of seconds, so the count is known in advance</p>\n<p>A <code>while</code> loop fits when you cannot know how many trips you need, like the level-up loop. Here the caller hands us the count.</p>\n</div>\n</div>\n</section>\n<p>Write the first version inside the <code>impl Game</code> block, right under <code>click()</code>. It is indented because it lives inside that block:</p>\n<pre><code class=\"language-rust\">    fn tick(&amp;mut self, seconds: u32) {\n        for _ in 0..seconds {\n            let damage = idle_damage_for_level(self.player.level);\n\n            self.enemy.health = health_after_hit(self.enemy.health, damage);\n        }\n    }\n</code></pre>\n<p>Each trip through the loop works out the damage for the player's current level and hits the enemy with it.</p>\n<p>It uses <code>health_after_hit()</code>, the same small rule that has been protecting enemy health since the first article.</p>\n<p>That looks finished.</p>\n<p>It is not, and we should have a test to say so.</p>\n<hr />\n<h2 id=\"the-slime-that-died-and-kept-being-hit\">The slime that died and kept being hit<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#the-slime-that-died-and-kept-being-hit\" aria-label=\"Link to this section\">#</a></h2>\n<p>We have a rule for how fast the player hits.</p>\n<p>What we need next is a test for the thing a player cares about: if I wait long enough, does the slime go down?</p>\n<p>At level 1, our player hits for 1 per second, and the first slime has 20 health.</p>\n<p>Twenty seconds should do it:</p>\n<pre><code class=\"language-rust\">#[test]\nfn enough_time_defeats_the_enemy() {\n    let mut game = Game::new();\n\n    game.tick(20);\n\n    assert_eq!(game.stage, 2);\n    assert_eq!(game.enemy.health, 30);\n    assert_eq!(game.player.gold, 5);\n    assert_eq!(game.player.xp, 6);\n}\n</code></pre>\n<p>Those last numbers should look familiar.</p>\n<p>They are the same ones the click version of this test checks: stage 2, a 30 health enemy, 5 gold, 6 XP.</p>\n<p>It does not matter whether a slime dies from a click or from a wait.</p>\n<p>The player should get paid the same either way.</p>\n<p>Run it:</p>\n<pre><code class=\"language-bash\">cargo test enough_time\n</code></pre>\n<p>and it fails:</p>\n<pre><code class=\"language-text\">assertion `left == right` failed\n  left: 1\n right: 2\n</code></pre>\n<p>(Rust prints more than that, including a line number and a note about backtraces. This is the part that matters.)</p>\n<p><code>left</code> is what the game did. <code>right</code> is what the test expected.</p>\n<p>The stage is still 1.</p>\n<p>Twenty seconds passed. The slime's health reached zero on the twentieth hit.</p>\n<p>And then nothing happened.</p>\n<p>Our loop never asks whether the enemy is defeated.</p>\n<p>That check lives in <code>click()</code>:</p>\n<pre><code class=\"language-rust\">        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n</code></pre>\n<p><code>tick()</code> took the hit from <code>click()</code> but left the aftermath behind.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why was the stage still 1 after the first version of <code>tick(20)</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>The slime's health reached zero, but <code>tick()</code> never checked <code>is_defeated</code></li>\n<li><code>0..20</code> only runs 19 times, so the last hit never landed</li>\n<li><code>saturating_sub</code> wrapped around below zero</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. The slime's health reached zero, but <code>tick()</code> never checked <code>is_defeated</code></p>\n<p>The loop landed all twenty hits. Only <code>click()</code> knew what to do afterward, so <code>tick()</code> needed the same check.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"two-ways-to-land-a-hit\">Two ways to land a hit<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#two-ways-to-land-a-hit\" aria-label=\"Link to this section\">#</a></h2>\n<p>There are two ways to hurt an enemy now: a click, and a second of waiting.</p>\n<p>Both of them need the same thing to happen afterward.</p>\n<p>If we paste the <code>if</code> into <code>tick()</code>, we have the same three lines in two places.</p>\n<p>Then the next time defeating an enemy needs to change, we have to remember both.</p>\n<p>We have seen this before.</p>\n<p>When the same enemy was being built in three different places, we gave the building a name, and <code>Enemy::for_stage()</code> became the one place that builds an enemy.</p>\n<p>Same fix here.</p>\n<p>Add a method inside the same <code>impl Game</code> block, under <code>tick()</code>:</p>\n<pre><code class=\"language-rust\">    fn resolve_hit(&amp;mut self) {\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n</code></pre>\n<p>Read it out loud:</p>\n<blockquote>\n<p>A hit just landed. If the enemy went down, handle the defeat.</p>\n</blockquote>\n<p>Now both ways of landing a hit can finish the same way.</p>\n<p>Replace the whole <code>click()</code> method with this one. The old <code>if</code> is gone, and a call to <code>resolve_hit()</code> takes its place:</p>\n<pre><code class=\"language-rust\">    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n        self.resolve_hit();\n    }\n</code></pre>\n<p>And replace <code>tick()</code> with this. Only the last line inside the loop is new:</p>\n<pre><code class=\"language-rust\">    fn tick(&amp;mut self, seconds: u32) {\n        for _ in 0..seconds {\n            let damage = idle_damage_for_level(self.player.level);\n\n            self.enemy.health = health_after_hit(self.enemy.health, damage);\n            self.resolve_hit();\n        }\n    }\n</code></pre>\n<p>Run the tests again:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>All eighteen pass, including the clicking tests we did not touch.</p>\n<p>That is what a good extraction looks like.</p>\n<p><code>click()</code> does the same thing it did before.</p>\n<p>It just does it by calling a method that <code>tick()</code> can call too.</p>\n<p>Now follow the path of the twentieth second:</p>\n<pre><code class=\"language-text\">tick()\n    ↓\nhealth_after_hit() → 0\n    ↓\nresolve_hit()\n    ↓\ndefeat_enemy()\n    ↓\ngold, XP, next stage, next enemy\n</code></pre>\n<p>That bottom half is the same code a click reaches.</p>\n<p>The slime can die from a click or from a second of waiting.</p>\n<p>Either way, it dies the same way.</p>\n<hr />\n<h2 id=\"test-time-without-waiting\">Test time without waiting<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#test-time-without-waiting\" aria-label=\"Link to this section\">#</a></h2>\n<p>Look at what we just tested.</p>\n<p>Twenty seconds of game time ran, and the test finished instantly.</p>\n<p>Nothing slept. Nothing waited.</p>\n<p>The game does not know what a second is.</p>\n<p>It was handed the number 20 and did twenty steps of work.</p>\n<p>That is the payoff for the design.</p>\n<p>Time is an input, so a test can supply as much of it as it likes.</p>\n<p>Three more tests are worth adding. Put each one at the bottom of the <code>tests</code> module.</p>\n<p>One second should be one hit:</p>\n<pre><code class=\"language-rust\">#[test]\nfn one_second_is_one_idle_hit() {\n    let mut game = Game::new();\n\n    game.tick(1);\n\n    assert_eq!(game.enemy.health, 19);\n}\n</code></pre>\n<p>Zero seconds should change nothing:</p>\n<pre><code class=\"language-rust\">#[test]\nfn zero_seconds_changes_nothing() {\n    let mut game = Game::new();\n\n    game.tick(0);\n\n    assert_eq!(game.stage, 1);\n    assert_eq!(game.enemy.health, 20);\n}\n</code></pre>\n<p>We did not write any code to handle zero.</p>\n<p>We did not need to.</p>\n<p><code>0..0</code> is an empty range, so the loop runs zero times and the game stays exactly as it was.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does <code>game.tick(0)</code> leave the game unchanged without any special code for zero?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>0..0</code> is an empty range, so the loop body never runs</li>\n<li><code>health_after_hit</code> refuses a damage of zero</li>\n<li>Rust skips any method called with <code>0</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. <code>0..0</code> is an empty range, so the loop body never runs</p>\n<p>No trips through the loop means no damage, no defeat, and no payout.</p>\n</div>\n</div>\n</section>\n<p>The last test is the one that matters most.</p>\n<p>Waiting forty-five seconds in one go should give the same game as waiting one second forty-five times:</p>\n<pre><code class=\"language-rust\">#[test]\nfn one_long_tick_matches_many_short_ones() {\n    let mut long = Game::new();\n    let mut short = Game::new();\n\n    long.tick(45);\n\n    for _ in 0..45 {\n        short.tick(1);\n    }\n\n    assert_eq!(long.stage, short.stage);\n    assert_eq!(long.enemy.health, short.enemy.health);\n    assert_eq!(long.player.gold, short.player.gold);\n    assert_eq!(long.player.xp, short.player.xp);\n    assert_eq!(long.player.level, short.player.level);\n}\n</code></pre>\n<p>This one passes by construction today, because <code>tick(45)</code> literally is forty-five single steps.</p>\n<p>Today it almost looks redundant. Later, when we replace thousands of tiny steps with arithmetic, it becomes the test that proves the optimization did not change the game.</p>\n<p>It is a promise to our future selves.</p>\n<p>Later, a browser will sometimes call <code>tick(1)</code> every second.</p>\n<p>Other times, it will come back after a long absence and call <code>tick(7200)</code> once.</p>\n<p>Those two have to be the same game.</p>\n<p>If a change ever makes them different, this test will say so.</p>\n<p>Try it: here is a trimmed copy of our game with just <code>tick()</code>, <code>resolve_hit()</code> and the defeat rules. Change <code>seconds</code> and run it. Then try <code>3600</code>.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try changing the seconds away</figcaption><pre><code class=\"language-rust\">struct Player {\n    gold: u32,\n    xp: u32,\n    level: u32,\n}\n\nstruct Enemy {\n    health: u32,\n    gold_reward: u32,\n    xp_reward: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\nimpl Player {\n    fn new() -&gt; Player {\n        Player { gold: 0, xp: 0, level: 1 }\n    }\n\n    fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= xp_to_next_level(self.level) {\n            self.xp -= xp_to_next_level(self.level);\n            self.level = self.level.saturating_add(1);\n        }\n    }\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            health: stage.saturating_mul(10).saturating_add(10),\n            gold_reward: stage.saturating_mul(2).saturating_add(3),\n            xp_reward: stage.saturating_mul(2).saturating_add(4),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game { stage: 1, player: Player::new(), enemy: Enemy::for_stage(1) }\n    }\n\n    fn tick(&amp;mut self, seconds: u32) {\n        for _ in 0..seconds {\n            let damage = idle_damage_for_level(self.player.level);\n\n            self.enemy.health = self.enemy.health.saturating_sub(damage);\n            self.resolve_hit();\n        }\n    }\n\n    fn resolve_hit(&amp;mut self) {\n        if self.enemy.health == 0 {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.player.gold = self.player.gold.saturating_add(self.enemy.gold_reward);\n        self.player.add_xp(self.enemy.xp_reward);\n\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n}\n\nfn xp_to_next_level(level: u32) -&gt; u32 {\n    level.saturating_sub(1).saturating_mul(15).saturating_add(20)\n}\n\nfn idle_damage_for_level(level: u32) -&gt; u32 {\n    level\n}\n\nfn main() {\n    let seconds = 45;\n\n    let mut long = Game::new();\n    let mut short = Game::new();\n\n    long.tick(seconds);\n\n    for _ in 0..seconds {\n        short.tick(1);\n    }\n\n    println!(\n        &quot;One long tick:   stage {} health {} gold {} xp {} level {}&quot;,\n        long.stage, long.enemy.health, long.player.gold, long.player.xp, long.player.level\n    );\n    println!(\n        &quot;Many short ones: stage {} health {} gold {} xp {} level {}&quot;,\n        short.stage, short.enemy.health, short.player.gold, short.player.xp, short.player.level\n    );\n\n    assert_eq!(long.stage, short.stage);\n    assert_eq!(long.enemy.health, short.enemy.health);\n    assert_eq!(long.player.gold, short.player.gold);\n    assert_eq!(long.player.xp, short.player.xp);\n    assert_eq!(long.player.level, short.player.level);\n\n    println!(&quot;Same game.&quot;);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/\">the original article</a>.</p></figure>\n<p>With <code>45</code> it prints the same line twice, <code>stage 2 health 5 gold 5 xp 6 level 1</code>, then <code>Same game.</code> The <code>assert_eq!</code> lines in <code>main</code> are our test; if the two games ever differ, the program stops with a panic instead.</p>\n<p>Run them all:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>Twenty-one tests should pass.</p>\n<hr />\n<h2 id=\"leave-for-an-hour\">Leave for an hour<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#leave-for-an-hour\" aria-label=\"Link to this section\">#</a></h2>\n<p>Time to try it for real.</p>\n<p>We are going to leave the loop in <code>main()</code> exactly as it is, with one change: stop at stage 3, so the output is short.</p>\n<pre><code class=\"language-rust\">    while game.stage &lt;= 3 {\n</code></pre>\n<p>Everything inside the loop stays the same.</p>\n<p>Then, after the loop, the player leaves.</p>\n<p>Add this after the closing brace of the <code>while</code> loop, still inside <code>main()</code>:</p>\n<pre><code class=\"language-rust\">    let stage = game.stage;\n    let level = game.player.level;\n    let gold = game.player.gold;\n\n    game.tick(3600);\n\n    println!(\"One hour away.\");\n    println!(\n        \"Stage {} -&gt; {}  Level {} -&gt; {}  Gold {} -&gt; {}\",\n        stage, game.stage, level, game.player.level, gold, game.player.gold\n    );\n</code></pre>\n<p>Three thousand six hundred is the number of seconds in an hour.</p>\n<p>Run it:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>You should see:</p>\n<pre><code class=\"language-text\">Stage 1 cleared!  Gold: 5  XP: 6/20\n  Next up: Training Slime with 30 health.\nStage 2 cleared!  Gold: 12  XP: 14/20\n  Next up: Training Slime with 40 health.\nBought click upgrade 1! Click damage: 7  Gold left: 2\nStage 3 cleared!  Gold: 11  XP: 4/35\nLevel up! Now level 2. Click damage: 8\n  Next up: Training Slime with 50 health.\nOne hour away.\nStage 4 -&gt; 131  Level 2 -&gt; 48  Gold 11 -&gt; 17410\n</code></pre>\n<p>The first half is the player, clicking their way through three stages.</p>\n<p>The last two lines are the hour.</p>\n<p>One hour, with nobody at the keyboard, took the game from stage 4 to stage 131.</p>\n<p>The player also came home with 17,410 gold and nothing spent. Noted.</p>\n<p>Look at the level.</p>\n<p>It went from 2 to 48.</p>\n<p>That was not a bug.</p>\n<p>Every trip through the <code>for</code> loop asks for the damage at the player's <em>current</em> level.</p>\n<p>When a slime pays out enough XP, the player levels up in the middle of the hour, and every second after that hits a little harder.</p>\n<p>Waiting makes the player stronger, which makes waiting faster.</p>\n<p>That is the shape of an idle game.</p>\n<p>Is it balanced? No.</p>\n<p>A stage 131 slime after an hour is a lot.</p>\n<p>We will tune that later. We have a rule with a name and a test, so tuning it is changing one function.</p>\n<p><strong>Coding challenge: Tune the idle hit</strong></p>\n<p>Change <code>idle_damage_for_level</code> so that level 1 still hits for 1, but every level after that adds 2 more damage per second.</p>\n<p>Keep the function named <code>idle_damage_for_level</code>, taking a <code>u32</code> level and returning a <code>u32</code>, because the checks call it. <strong>Check my code</strong> will tell you how you are doing.</p>\n<p><strong>Expected result:</strong> the program prints <code>Level 1: 1 damage per second</code>, <code>Level 2: 3 damage per second</code> and <code>Level 3: 5 damage per second</code>.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Finish the idle damage rule</figcaption><pre><code class=\"language-rust\">fn idle_damage_for_level(level: u32) -&gt; u32 {\n    // TODO: level 1 hits for 1, and every level after that adds 2 more\n    level\n}\n\nfn main() {\n    for level in 1..=3 {\n        println!(&quot;Level {level}: {} damage per second&quot;, idle_damage_for_level(level));\n    }\n}\n</code></pre><details class=\"ob-spoiler\"><summary>Spoiler: the checks for this challenge</summary><p>These tests run when you press Check my code on the original article. Reading them first gives away the answer.</p><pre><code class=\"language-rust\">/// Level 1 is the base hit and gets nothing extra. Look at what your rule does with the first level.\n#[test]\nfn level_one_is_the_base_hit() {\n    assert_eq!(idle_damage_for_level(1), 1);\n}\n\n/// Levels 2 and 3 should each step up from the level before. Check the size of the step.\n#[test]\nfn each_level_adds_two() {\n    assert_eq!(idle_damage_for_level(2), 3);\n    assert_eq!(idle_damage_for_level(3), 5);\n}\n\n/// The step should stay the same as levels climb. Try your rule on a level further out.\n#[test]\nfn later_levels_keep_the_step() {\n    assert_eq!(idle_damage_for_level(10), 19);\n    assert_eq!(idle_damage_for_level(100), 199);\n}\n\n/// Level 0 should not crash the game. Unsigned numbers do not like going below zero.\n#[test]\nfn level_zero_does_not_panic() {\n    let _ = idle_damage_for_level(0);\n}\n</code></pre></details><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>Start from <code>level.saturating_sub(1)</code>, like <code>xp_to_next_level</code> does, so level 1 gets no extra. Multiply that by 2 and add the 1 back.</p>\n</aside>\n<p>What matters today is that the engine can answer a question it could not answer before:</p>\n<blockquote>\n<p>What happened while I was away?</p>\n</blockquote>\n<hr />\n<h2 id=\"who-owns-the-clock\">Who owns the clock?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#who-owns-the-clock\" aria-label=\"Link to this section\">#</a></h2>\n<p>The game never asked what time it was.</p>\n<p>It was handed 3600, and it did 3600 seconds of work.</p>\n<p>That is going to become one of the most important lines in this series:</p>\n<pre><code class=\"language-text\">Rust owns the rules.\nWhatever runs the game owns time.\n</code></pre>\n<p>Today, that caller is a test or <code>main()</code>. Later, it will be a web page.</p>\n<p>The caller measures how much real time passed and hands that fact to the game.</p>\n<p>Rust decides what that time means for gameplay.</p>\n<p>That distinction matters. A browser might report that 28,800 seconds passed while the player was away. The game can still decide that only part of that time should count.</p>\n<p>In other words:</p>\n<pre><code class=\"language-text\">caller measures elapsed time\n        ↓\nRust receives the number\n        ↓\nRust decides how much time counts\n        ↓\nRust applies the game rules\n</code></pre>\n<p>That keeps balance decisions inside the game instead of hiding them in whichever environment happens to run it.</p>\n<p>There is one honest cost.</p>\n<p><code>tick()</code> does one trip through the loop for every second.</p>\n<p>An hour is 3,600 trips. A day is 86,400.</p>\n<p>That is still manageable for this tiny program, but the cost grows directly with the number of seconds we simulate.</p>\n<p>A number like <code>u32::MAX</code> would be a very long walk.</p>\n<p>Later, we can cap credited away-time or replace repeated steps with arithmetic. The caller will still report elapsed time; Rust will still own the policy and the rules.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>A browser reports 28,800 seconds away. Why should the game, not the caller, decide how many of them count?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Balance rules belong in Rust, so every caller gets the same policy</li>\n<li>The caller cannot measure elapsed time</li>\n<li><code>tick</code> rejects any number above 3600</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. Balance rules belong in Rust, so every caller gets the same policy</p>\n<p>The caller measures; Rust decides what that time means. Capping away-time is a game rule, so it lives with the other rules.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"the-complete-program\">The complete program<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#the-complete-program\" aria-label=\"Link to this section\">#</a></h2>\n<p>If you are only reading along, skip to the last two sections.</p>\n<p>If you have been following along in your editor, here is everything in one place.</p>\n<p>At this point, <code>src/main.rs</code> should look like this:</p>\n<pre><code class=\"language-rust\">struct Player {\n    gold: u32,\n    xp: u32,\n    level: u32,\n    click_upgrade_level: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n    gold_reward: u32,\n    xp_reward: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\n#[derive(Debug, PartialEq)]\nenum PurchaseOutcome {\n    Purchased,\n    NotEnoughGold,\n}\n\nimpl Player {\n    fn new() -&gt; Player {\n        Player {\n            gold: 0,\n            xp: 0,\n            level: 1,\n            click_upgrade_level: 0,\n        }\n    }\n\n    fn add_gold(&amp;mut self, amount: u32) {\n        self.gold = self.gold.saturating_add(amount);\n    }\n\n    fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= xp_to_next_level(self.level) {\n            self.xp -= xp_to_next_level(self.level);\n            self.level = self.level.saturating_add(1);\n        }\n    }\n\n    fn base_damage(&amp;self) -&gt; u32 {\n        base_damage_for_level(self.level)\n    }\n\n    fn bonus_damage(&amp;self) -&gt; u32 {\n        click_upgrade_bonus(self.click_upgrade_level)\n    }\n\n    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        let cost = click_upgrade_cost(self.click_upgrade_level);\n\n        if self.gold &lt; cost {\n            return PurchaseOutcome::NotEnoughGold;\n        }\n\n        self.gold -= cost;\n        self.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\n\n        PurchaseOutcome::Purchased\n    }\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n            gold_reward: gold_reward_for_stage(stage),\n            xp_reward: xp_reward_for_stage(stage),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player::new(),\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n        self.resolve_hit();\n    }\n\n    fn tick(&amp;mut self, seconds: u32) {\n        for _ in 0..seconds {\n            let damage = idle_damage_for_level(self.player.level);\n\n            self.enemy.health = health_after_hit(self.enemy.health, damage);\n            self.resolve_hit();\n        }\n    }\n\n    fn resolve_hit(&amp;mut self) {\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.player.add_gold(self.enemy.gold_reward);\n        self.player.add_xp(self.enemy.xp_reward);\n\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n\n    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        self.player.buy_click_upgrade()\n    }\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn gold_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(3)\n}\n\nfn xp_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(4)\n}\n\nfn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n\nfn base_damage_for_level(level: u32) -&gt; u32 {\n    level.saturating_add(4)\n}\n\nfn idle_damage_for_level(level: u32) -&gt; u32 {\n    level\n}\n\nfn click_upgrade_bonus(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_mul(2)\n}\n\nfn click_upgrade_cost(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_add(1).saturating_mul(10)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage(), player.bonus_damage());\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 3 {\n        let stage = game.stage;\n        let level = game.player.level;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\n                \"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\n                game.player.gold,\n                game.player.xp,\n                xp_to_next_level(game.player.level)\n            );\n\n            if game.player.level &gt; level {\n                println!(\n                    \"Level up! Now level {}. Click damage: {}\",\n                    game.player.level,\n                    click_damage(game.player.base_damage(), game.player.bonus_damage())\n                );\n            }\n\n            println!(\n                \"  Next up: {} with {} health.\",\n                game.enemy.name, game.enemy.health\n            );\n        }\n\n        match game.buy_click_upgrade() {\n            PurchaseOutcome::Purchased =&gt; println!(\n                \"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\n                game.player.click_upgrade_level,\n                click_damage(game.player.base_damage(), game.player.bonus_damage()),\n                game.player.gold\n            ),\n            PurchaseOutcome::NotEnoughGold =&gt; {}\n        }\n    }\n\n    let stage = game.stage;\n    let level = game.player.level;\n    let gold = game.player.gold;\n\n    game.tick(3600);\n\n    println!(\"One hour away.\");\n    println!(\n        \"Stage {} -&gt; {}  Level {} -&gt; {}  Gold {} -&gt; {}\",\n        stage, game.stage, level, game.player.level, gold, game.player.gold\n    );\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player::new();\n\n        let mut enemy = Enemy::for_stage(1);\n        enemy.health = 10;\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 5);\n    }\n\n    #[test]\n    fn later_stages_have_tougher_enemies() {\n        assert_eq!(enemy_health_for_stage(1), 20);\n        assert_eq!(enemy_health_for_stage(2), 30);\n    }\n\n    #[test]\n    fn later_stages_pay_better() {\n        assert_eq!(gold_reward_for_stage(1), 5);\n        assert_eq!(gold_reward_for_stage(2), 7);\n        assert_eq!(xp_reward_for_stage(1), 6);\n        assert_eq!(xp_reward_for_stage(2), 8);\n    }\n\n    #[test]\n    fn levels_need_more_xp_as_they_go() {\n        assert_eq!(xp_to_next_level(1), 20);\n        assert_eq!(xp_to_next_level(2), 35);\n    }\n\n    #[test]\n    fn leveling_up_keeps_leftover_xp() {\n        let mut player = Player::new();\n\n        player.add_xp(25);\n\n        assert_eq!(player.level, 2);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn a_big_reward_can_skip_levels() {\n        let mut player = Player::new();\n\n        player.add_xp(60);\n\n        assert_eq!(player.level, 3);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn levels_make_the_player_hit_harder() {\n        assert_eq!(base_damage_for_level(1), 5);\n        assert_eq!(base_damage_for_level(2), 6);\n    }\n\n    #[test]\n    fn upgrades_cost_more_each_time() {\n        assert_eq!(click_upgrade_cost(0), 10);\n        assert_eq!(click_upgrade_cost(1), 20);\n    }\n\n    #[test]\n    fn buying_an_upgrade_needs_gold() {\n        let mut player = Player::new();\n\n        assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::NotEnoughGold);\n        assert_eq!(player.click_upgrade_level, 0);\n        assert_eq!(player.bonus_damage(), 0);\n    }\n\n    #[test]\n    fn buying_an_upgrade_spends_gold() {\n        let mut player = Player::new();\n        player.gold = 12;\n\n        assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased);\n        assert_eq!(player.gold, 2);\n        assert_eq!(player.click_upgrade_level, 1);\n        assert_eq!(player.bonus_damage(), 2);\n    }\n\n    #[test]\n    fn a_new_game_starts_at_stage_one() {\n        let game = Game::new();\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n        assert_eq!(game.player.gold, 0);\n    }\n\n    #[test]\n    fn clicking_damages_the_enemy() {\n        let mut game = Game::new();\n\n        game.click();\n\n        assert_eq!(game.enemy.health, 15);\n    }\n\n    #[test]\n    fn defeating_an_enemy_pays_the_player() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n\n    #[test]\n    fn idle_damage_grows_with_level() {\n        assert_eq!(idle_damage_for_level(1), 1);\n        assert_eq!(idle_damage_for_level(2), 2);\n    }\n\n    #[test]\n    fn enough_time_defeats_the_enemy() {\n        let mut game = Game::new();\n\n        game.tick(20);\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n\n    #[test]\n    fn one_second_is_one_idle_hit() {\n        let mut game = Game::new();\n\n        game.tick(1);\n\n        assert_eq!(game.enemy.health, 19);\n    }\n\n    #[test]\n    fn zero_seconds_changes_nothing() {\n        let mut game = Game::new();\n\n        game.tick(0);\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n    }\n\n    #[test]\n    fn one_long_tick_matches_many_short_ones() {\n        let mut long = Game::new();\n        let mut short = Game::new();\n\n        long.tick(45);\n\n        for _ in 0..45 {\n            short.tick(1);\n        }\n\n        assert_eq!(long.stage, short.stage);\n        assert_eq!(long.enemy.health, short.enemy.health);\n        assert_eq!(long.player.gold, short.player.gold);\n        assert_eq!(long.player.xp, short.player.xp);\n        assert_eq!(long.player.level, short.player.level);\n    }\n}\n</code></pre>\n<p>Run both:</p>\n<pre><code class=\"language-bash\">cargo run\ncargo test\n</code></pre>\n<p>The game should clear three stages, buy one upgrade, spend an hour away, and all twenty-one tests should pass.</p>\n<hr />\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Along the way, we encountered:</p>\n<ul>\n<li>time as a plain number, passed in by the caller</li>\n<li>the <code>tick</code> method</li>\n<li><code>for</code> loops and ranges like <code>0..seconds</code></li>\n<li><code>_</code> for a value we do not need</li>\n<li>extracting a shared method so two paths finish the same way</li>\n<li>tests that run an hour without waiting</li>\n</ul>\n<p>Two ideas are worth keeping.</p>\n<p>The first is how to choose between the two loops we now know.</p>\n<p>Ask whether you know the count.</p>\n<p>The level-up loop could not know how many levels one reward would cross, so it was a <code>while</code>.</p>\n<p><code>tick()</code> knows exactly how many seconds it was given, so it is a <code>for</code>.</p>\n<p>The second is <code>resolve_hit()</code>.</p>\n<p>The bug was not in the loop. The loop did what we told it.</p>\n<p>The bug was that two ways of hurting an enemy had to finish the same way, and only one of them did.</p>\n<p>Giving the finish a name fixed it, and now there is one place where \"the enemy went down\" turns into gold, XP, and a new slime.</p>\n<hr />\n<h2 id=\"you-accidentally-built-a-game-engine\">You accidentally built a game engine<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#you-accidentally-built-a-game-engine\" aria-label=\"Link to this section\">#</a></h2>\n<p>Open <code>src/main.rs</code> and scroll.</p>\n<p>Three structs. An enum. Three <code>impl</code> blocks. Twelve small rules. A click, a purchase, a clock that is not a clock, and twenty-one tests.</p>\n<p>All of it is in one file, and the file is getting long.</p>\n<p>Nobody planned that.</p>\n<p>Each article needed one more thing, and each thing went in the next free spot.</p>\n<p>But look at what is in there.</p>\n<p>Everything above <code>main()</code> knows nothing about a terminal, a screen, or a clock.</p>\n<p>Only <code>main()</code> does.</p>\n<p>Today they share a file. Next time, we put a wall between them, and find out that the part on the far side of the wall is already a small game engine.</p>\n<p><strong>Small function. Bigger game.</strong></p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Idle Is Not a Timer.\nCanonical: https://mnixblog.netlify.app/idle-is-not-a-timer/\nLet Your Rust Game Progress While You Are Away\nOur game has a progression loop now.\nKill a slime, get paid, level up, buy damage, kill a tougher slime.\nIt still stops the moment you stop clicking.\nSo today we are going to make time part of the game.\nNot a timer. Not a clock. Not sleep.\nJust a number.\nBecause the game will never look at a clock, we can test a full hour away in a fraction of a second.\nBy the end, an hour away from the game is one line:\ngame.tick(3600);\nAnd here is what the game looks like when you come back:\nOne hour away.\nStage 4 -> 131  Level 2 -> 48  Gold 11 -> 17410\nOur starting point\nIf you are only reading along, you can skip past this code to \"Time is just a number.\"\nIf you are building along, you need three structs, an enum, three impl blocks, eleven small functions, and sixteen tests:\nstruct Player {\ngold: u32,\nxp: u32,\nlevel: u32,\nclick_upgrade_level: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\ngold_reward: u32,\nxp_reward: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\n#[derive(Debug, PartialEq)]\nenum PurchaseOutcome {\nPurchased,\nNotEnoughGold,\n}\nimpl Player {\nfn new() -> Player {\nPlayer {\ngold: 0,\nxp: 0,\nlevel: 1,\nclick_upgrade_level: 0,\n}\n}\nfn add_gold(&mut self, amount: u32) {\nself.gold = self.gold.saturating_add(amount);\n}\nfn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= xp_to_next_level(self.level) {\nself.xp -= xp_to_next_level(self.level);\nself.level = self.level.saturating_add(1);\n}\n}\nfn base_damage(&self) -> u32 {\nbase_damage_for_level(self.level)\n}\nfn bonus_damage(&self) -> u32 {\nclick_upgrade_bonus(self.click_upgrade_level)\n}\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nlet cost = click_upgrade_cost(self.click_upgrade_level);\nif self.gold < cost {\nreturn PurchaseOutcome::NotEnoughGold;\n}\nself.gold -= cost;\nself.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\nPurchaseOutcome::Purchased\n}\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\ngold_reward: gold_reward_for_stage(stage),\nxp_reward: xp_reward_for_stage(stage),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player::new(),\nenemy: Enemy::for_stage(1),\n}\n}\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.player.add_gold(self.enemy.gold_reward);\nself.player.add_xp(self.enemy.xp_reward);\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nself.player.buy_click_upgrade()\n}\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn gold_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(3)\n}\nfn xp_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(4)\n}\nfn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\nfn base_damage_for_level(level: u32) -> u32 {\nlevel.saturating_add(4)\n}\nfn click_upgrade_bonus(upgrades: u32) -> u32 {\nupgrades.saturating_mul(2)\n}\nfn click_upgrade_cost(upgrades: u32) -> u32 {\nupgrades.saturating_add(1).saturating_mul(10)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage(), player.bonus_damage());\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 8 {\nlet stage = game.stage;\nlet level = game.player.level;\ngame.click();\nif game.stage > stage {\nprintln!(\n\"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\ngame.player.gold,\ngame.player.xp,\nxp_to_next_level(game.player.level)\n);\nif game.player.level > level {\nprintln!(\n\"Level up! Now level {}. Click damage: {}\",\ngame.player.level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage())\n);\n}\nprintln!(\n\"  Next up: {} with {} health.\",\ngame.enemy.name, game.enemy.health\n);\n}\nmatch game.buy_click_upgrade() {\nPurchaseOutcome::Purchased => println!(\n\"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\ngame.player.click_upgrade_level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage()),\ngame.player.gold\n),\nPurchaseOutcome::NotEnoughGold => {}\n}\n}\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player::new();\nlet mut enemy = Enemy::for_stage(1);\nenemy.health = 10;\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 5);\n}\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\n#[test]\nfn later_stages_pay_better() {\nassert_eq!(gold_reward_for_stage(1), 5);\nassert_eq!(gold_reward_for_stage(2), 7);\nassert_eq!(xp_reward_for_stage(1), 6);\nassert_eq!(xp_reward_for_stage(2), 8);\n}\n#[test]\nfn levels_need_more_xp_as_they_go() {\nassert_eq!(xp_to_next_level(1), 20);\nassert_eq!(xp_to_next_level(2), 35);\n}\n#[test]\nfn leveling_up_keeps_leftover_xp() {\nlet mut player = Player::new();\nplayer.add_xp(25);\nassert_eq!(player.level, 2);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn a_big_reward_can_skip_levels() {\nlet mut player = Player::new();\nplayer.add_xp(60);\nassert_eq!(player.level, 3);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn levels_make_the_player_hit_harder() {\nassert_eq!(base_damage_for_level(1), 5);\nassert_eq!(base_damage_for_level(2), 6);\n}\n#[test]\nfn upgrades_cost_more_each_time() {\nassert_eq!(click_upgrade_cost(0), 10);\nassert_eq!(click_upgrade_cost(1), 20);\n}\n#[test]\nfn buying_an_upgrade_needs_gold() {\nlet mut player = Player::new();\nassert_eq!(player.buy_click_upgrade(), PurchaseOutcome::NotEnoughGold);\nassert_eq!(player.click_upgrade_level, 0);\nassert_eq!(player.bonus_damage(), 0);\n}\n#[test]\nfn buying_an_upgrade_spends_gold() {\nlet mut player = Player::new();\nplayer.gold = 12;\nassert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased);\nassert_eq!(player.gold, 2);\nassert_eq!(player.click_upgrade_level, 1);\nassert_eq!(player.bonus_damage(), 2);\n}\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\nassert_eq!(game.player.gold, 0);\n}\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 15);\n}\n#[test]\nfn defeating_an_enemy_pays_the_player() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n}\nRun it, and the player clears eight stages and buys three upgrades.\nThen the program is over, and so is all the progress, because nothing in it ever happens on its own.\nTime is just a number\nAn idle game has to keep going while nobody is looking.\nThe obvious way to build that is to give the game a clock.\nStart a timer. Every second, do some work. When the player leaves, keep the timer running.\nThat sounds simple, and it is a trap.\nA game that watches a clock is a game that has to be tested by waiting, unless you build a fake clock, and then you have rebuilt the idea we are about to use anyway.\nWant to know what happens after an hour away? Wait an hour.\nSo let's turn it around.\nThe game will not ask what time it is.\nWhoever is running the game will tell it how much time went by:\ngame.tick(10);\nTen seconds passed. What happened?\nGame code often uses the word tick for advancing simulation time, so we will borrow the name.\nThe contract is one sentence:\nTell me how many seconds passed. I will tell you what the game became.\ntick will be a method on Game, and the only thing it takes is that number.\nThe number is the only thing about time that Rust ever sees.\nLater in this series, a web page will measure real time and hand the game that number.\nFor now, our tests can hand it any number they like.\nQuestion: Why does tick(seconds) take a number instead of reading a clock?\nA test can hand it an hour of time and finish instantly\nRust has no way to read a clock at all\nA u32 automatically counts real seconds\nAnswer: 1. A test can hand it an hour of time and finish instantly\nExplanation: The game only sees a number, so whoever calls it decides how much time went by. A test can say 3600 without waiting.\nWhat does a player do while nobody is clicking?\nBefore the game can work out what happened, it needs to know what the player does with spare seconds.\nOur player is going to keep fighting.\nOne hit per second, and the harder the player has leveled up, the harder it hits.\nThat is a game rule, and we know what to do with game rules by now.\nGive it a small function, next to the others:\nfn idle_damage_for_level(level: u32) -> u32 {\nlevel\n}\nOne damage per second at level 1.\nTwo at level 2.\nIt is deliberately small. It is not balanced and it does not need to be yet.\nIt also ignores the click upgrades on purpose. Those are for clicking.\nWhat matters is that the rule has a name and a test.\nAdd it at the bottom of the tests module:\n#[test]\nfn idle_damage_grows_with_level() {\nassert_eq!(idle_damage_for_level(1), 1);\nassert_eq!(idle_damage_for_level(2), 2);\n}\nNotice what the function does not take.\nIt does not take a Player. It does not take a Game.\nIt takes a level and returns a number. It is the same kind of function as base_damage_for_level(), and it asks the same question: what does this level of progress get you?\nIf you run cargo build now, Rust warns that idle_damage_for_level is never used.\nWe are about to use it.\nOne second at a time\nNow for tick().\nThe player hits once per second, so ten seconds is ten hits.\nWe know exactly how many times to repeat: once per second that passed.\nThat is not a job for while.\nA while loop keeps going until a condition stops being true, like our level-up loop, which cannot know how many levels one reward will cross.\nWhen you know the count in advance, Rust has a loop for that:\nfor _ in 0..seconds {\n// one second of work\n}\n0..seconds is a range.\nIt counts from 0 up to, but not including, seconds.\nSo 0..3 gives you 0, 1, and 2. That is three trips through the loop, which is exactly what we want for three seconds.\nTry it: run this, then change seconds to 0 and to 5, and count the trips.\nLive example · try changing the seconds away\nfn main() {\nlet seconds: u32 = 3;\nlet mut trips = 0;\nfor second in 0..seconds {\nprintln!(\"trip with range value {second}\");\ntrips += 1;\n}\nprintln!(\"{seconds} seconds away, {trips} trips through the loop\");\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/idle-is-not-a-timer/).\nWith 3 you should see range values 0, 1 and 2, then 3 seconds away, 3 trips through the loop. With 0 the loop never runs, and the last line says 0 trips.\nThe _ is a placeholder that means:\nI do not need this value.\nA for loop hands us the current number from the range on every trip.\nWe do not care which second we are on. We only care that one passed.\nIf we gave the number a name like second, Rust would warn that we never use it.\nThe _ says that is on purpose.\nQuestion: Why is tick() a for loop instead of a while loop?\nIt is given the number of seconds, so the count is known in advance\nA while loop can only run once\nA for loop skips the enemy health check\nAnswer: 1. It is given the number of seconds, so the count is known in advance\nExplanation: A while loop fits when you cannot know how many trips you need, like the level-up loop. Here the caller hands us the count.\nWrite the first version inside the impl Game block, right under click(). It is indented because it lives inside that block:\nfn tick(&mut self, seconds: u32) {\nfor _ in 0..seconds {\nlet damage = idle_damage_for_level(self.player.level);\nself.enemy.health = health_after_hit(self.enemy.health, damage);\n}\n}\nEach trip through the loop works out the damage for the player's current level and hits the enemy with it.\nIt uses health_after_hit(), the same small rule that has been protecting enemy health since the first article.\nThat looks finished.\nIt is not, and we should have a test to say so.\nThe slime that died and kept being hit\nWe have a rule for how fast the player hits.\nWhat we need next is a test for the thing a player cares about: if I wait long enough, does the slime go down?\nAt level 1, our player hits for 1 per second, and the first slime has 20 health.\nTwenty seconds should do it:\n#[test]\nfn enough_time_defeats_the_enemy() {\nlet mut game = Game::new();\ngame.tick(20);\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\nThose last numbers should look familiar.\nThey are the same ones the click version of this test checks: stage 2, a 30 health enemy, 5 gold, 6 XP.\nIt does not matter whether a slime dies from a click or from a wait.\nThe player should get paid the same either way.\nRun it:\ncargo test enough_time\nand it fails:\nassertion `left == right` failed\nleft: 1\nright: 2\n(Rust prints more than that, including a line number and a note about backtraces. This is the part that matters.)\nleft is what the game did. right is what the test expected.\nThe stage is still 1.\nTwenty seconds passed. The slime's health reached zero on the twentieth hit.\nAnd then nothing happened.\nOur loop never asks whether the enemy is defeated.\nThat check lives in click():\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\ntick() took the hit from click() but left the aftermath behind.\nQuestion: Why was the stage still 1 after the first version of tick(20)?\nThe slime's health reached zero, but tick() never checked is_defeated\n0..20 only runs 19 times, so the last hit never landed\nsaturating_sub wrapped around below zero\nAnswer: 1. The slime's health reached zero, but tick() never checked is_defeated\nExplanation: The loop landed all twenty hits. Only click() knew what to do afterward, so tick() needed the same check.\nTwo ways to land a hit\nThere are two ways to hurt an enemy now: a click, and a second of waiting.\nBoth of them need the same thing to happen afterward.\nIf we paste the if into tick(), we have the same three lines in two places.\nThen the next time defeating an enemy needs to change, we have to remember both.\nWe have seen this before.\nWhen the same enemy was being built in three different places, we gave the building a name, and Enemy::for_stage() became the one place that builds an enemy.\nSame fix here.\nAdd a method inside the same impl Game block, under tick():\nfn resolve_hit(&mut self) {\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nRead it out loud:\nA hit just landed. If the enemy went down, handle the defeat.\nNow both ways of landing a hit can finish the same way.\nReplace the whole click() method with this one. The old if is gone, and a call to resolve_hit() takes its place:\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nself.resolve_hit();\n}\nAnd replace tick() with this. Only the last line inside the loop is new:\nfn tick(&mut self, seconds: u32) {\nfor _ in 0..seconds {\nlet damage = idle_damage_for_level(self.player.level);\nself.enemy.health = health_after_hit(self.enemy.health, damage);\nself.resolve_hit();\n}\n}\nRun the tests again:\ncargo test\nAll eighteen pass, including the clicking tests we did not touch.\nThat is what a good extraction looks like.\nclick() does the same thing it did before.\nIt just does it by calling a method that tick() can call too.\nNow follow the path of the twentieth second:\ntick()\n↓\nhealth_after_hit() → 0\n↓\nresolve_hit()\n↓\ndefeat_enemy()\n↓\ngold, XP, next stage, next enemy\nThat bottom half is the same code a click reaches.\nThe slime can die from a click or from a second of waiting.\nEither way, it dies the same way.\nTest time without waiting\nLook at what we just tested.\nTwenty seconds of game time ran, and the test finished instantly.\nNothing slept. Nothing waited.\nThe game does not know what a second is.\nIt was handed the number 20 and did twenty steps of work.\nThat is the payoff for the design.\nTime is an input, so a test can supply as much of it as it likes.\nThree more tests are worth adding. Put each one at the bottom of the tests module.\nOne second should be one hit:\n#[test]\nfn one_second_is_one_idle_hit() {\nlet mut game = Game::new();\ngame.tick(1);\nassert_eq!(game.enemy.health, 19);\n}\nZero seconds should change nothing:\n#[test]\nfn zero_seconds_changes_nothing() {\nlet mut game = Game::new();\ngame.tick(0);\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\n}\nWe did not write any code to handle zero.\nWe did not need to.\n0..0 is an empty range, so the loop runs zero times and the game stays exactly as it was.\nQuestion: Why does game.tick(0) leave the game unchanged without any special code for zero?\n0..0 is an empty range, so the loop body never runs\nhealth_after_hit refuses a damage of zero\nRust skips any method called with 0\nAnswer: 1. 0..0 is an empty range, so the loop body never runs\nExplanation: No trips through the loop means no damage, no defeat, and no payout.\nThe last test is the one that matters most.\nWaiting forty-five seconds in one go should give the same game as waiting one second forty-five times:\n#[test]\nfn one_long_tick_matches_many_short_ones() {\nlet mut long = Game::new();\nlet mut short = Game::new();\nlong.tick(45);\nfor _ in 0..45 {\nshort.tick(1);\n}\nassert_eq!(long.stage, short.stage);\nassert_eq!(long.enemy.health, short.enemy.health);\nassert_eq!(long.player.gold, short.player.gold);\nassert_eq!(long.player.xp, short.player.xp);\nassert_eq!(long.player.level, short.player.level);\n}\nThis one passes by construction today, because tick(45) literally is forty-five single steps.\nToday it almost looks redundant. Later, when we replace thousands of tiny steps with arithmetic, it becomes the test that proves the optimization did not change the game.\nIt is a promise to our future selves.\nLater, a browser will sometimes call tick(1) every second.\nOther times, it will come back after a long absence and call tick(7200) once.\nThose two have to be the same game.\nIf a change ever makes them different, this test will say so.\nTry it: here is a trimmed copy of our game with just tick(), resolve_hit() and the defeat rules. Change seconds and run it. Then try 3600.\nLive example · try changing the seconds away\nstruct Player {\ngold: u32,\nxp: u32,\nlevel: u32,\n}\nstruct Enemy {\nhealth: u32,\ngold_reward: u32,\nxp_reward: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\nimpl Player {\nfn new() -> Player {\nPlayer { gold: 0, xp: 0, level: 1 }\n}\nfn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= xp_to_next_level(self.level) {\nself.xp -= xp_to_next_level(self.level);\nself.level = self.level.saturating_add(1);\n}\n}\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nhealth: stage.saturating_mul(10).saturating_add(10),\ngold_reward: stage.saturating_mul(2).saturating_add(3),\nxp_reward: stage.saturating_mul(2).saturating_add(4),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame { stage: 1, player: Player::new(), enemy: Enemy::for_stage(1) }\n}\nfn tick(&mut self, seconds: u32) {\nfor _ in 0..seconds {\nlet damage = idle_damage_for_level(self.player.level);\nself.enemy.health = self.enemy.health.saturating_sub(damage);\nself.resolve_hit();\n}\n}\nfn resolve_hit(&mut self) {\nif self.enemy.health == 0 {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.player.gold = self.player.gold.saturating_add(self.enemy.gold_reward);\nself.player.add_xp(self.enemy.xp_reward);\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\n}\nfn xp_to_next_level(level: u32) -> u32 {\nlevel.saturating_sub(1).saturating_mul(15).saturating_add(20)\n}\nfn idle_damage_for_level(level: u32) -> u32 {\nlevel\n}\nfn main() {\nlet seconds = 45;\nlet mut long = Game::new();\nlet mut short = Game::new();\nlong.tick(seconds);\nfor _ in 0..seconds {\nshort.tick(1);\n}\nprintln!(\n\"One long tick:   stage {} health {} gold {} xp {} level {}\",\nlong.stage, long.enemy.health, long.player.gold, long.player.xp, long.player.level\n);\nprintln!(\n\"Many short ones: stage {} health {} gold {} xp {} level {}\",\nshort.stage, short.enemy.health, short.player.gold, short.player.xp, short.player.level\n);\nassert_eq!(long.stage, short.stage);\nassert_eq!(long.enemy.health, short.enemy.health);\nassert_eq!(long.player.gold, short.player.gold);\nassert_eq!(long.player.xp, short.player.xp);\nassert_eq!(long.player.level, short.player.level);\nprintln!(\"Same game.\");\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/idle-is-not-a-timer/).\nWith 45 it prints the same line twice, stage 2 health 5 gold 5 xp 6 level 1, then Same game. The assert_eq! lines in main are our test; if the two games ever differ, the program stops with a panic instead.\nRun them all:\ncargo test\nTwenty-one tests should pass.\nLeave for an hour\nTime to try it for real.\nWe are going to leave the loop in main() exactly as it is, with one change: stop at stage 3, so the output is short.\nwhile game.stage <= 3 {\nEverything inside the loop stays the same.\nThen, after the loop, the player leaves.\nAdd this after the closing brace of the while loop, still inside main():\nlet stage = game.stage;\nlet level = game.player.level;\nlet gold = game.player.gold;\ngame.tick(3600);\nprintln!(\"One hour away.\");\nprintln!(\n\"Stage {} -> {}  Level {} -> {}  Gold {} -> {}\",\nstage, game.stage, level, game.player.level, gold, game.player.gold\n);\nThree thousand six hundred is the number of seconds in an hour.\nRun it:\ncargo run\nYou should see:\nStage 1 cleared!  Gold: 5  XP: 6/20\nNext up: Training Slime with 30 health.\nStage 2 cleared!  Gold: 12  XP: 14/20\nNext up: Training Slime with 40 health.\nBought click upgrade 1! Click damage: 7  Gold left: 2\nStage 3 cleared!  Gold: 11  XP: 4/35\nLevel up! Now level 2. Click damage: 8\nNext up: Training Slime with 50 health.\nOne hour away.\nStage 4 -> 131  Level 2 -> 48  Gold 11 -> 17410\nThe first half is the player, clicking their way through three stages.\nThe last two lines are the hour.\nOne hour, with nobody at the keyboard, took the game from stage 4 to stage 131.\nThe player also came home with 17,410 gold and nothing spent. Noted.\nLook at the level.\nIt went from 2 to 48.\nThat was not a bug.\nEvery trip through the for loop asks for the damage at the player's current level.\nWhen a slime pays out enough XP, the player levels up in the middle of the hour, and every second after that hits a little harder.\nWaiting makes the player stronger, which makes waiting faster.\nThat is the shape of an idle game.\nIs it balanced? No.\nA stage 131 slime after an hour is a lot.\nWe will tune that later. We have a rule with a name and a test, so tuning it is changing one function.\nCoding challenge: Tune the idle hit\nChange idle_damage_for_level so that level 1 still hits for 1, but every level after that adds 2 more damage per second.\nKeep the function named idle_damage_for_level, taking a u32 level and returning a u32, because the checks call it. Check my code will tell you how you are doing.\nExpected result: the program prints Level 1: 1 damage per second, Level 2: 3 damage per second and Level 3: 5 damage per second.\nFinish the idle damage rule\nfn idle_damage_for_level(level: u32) -> u32 {\n// TODO: level 1 hits for 1, and every level after that adds 2 more\nlevel\n}\nfn main() {\nfor level in 1..=3 {\nprintln!(\"Level {level}: {} damage per second\", idle_damage_for_level(level));\n}\n}\n<details>\n<summary>Spoiler: the checks for this challenge</summary>\nThese tests run when you press Check my code on the original article. Reading them first gives away the answer.\n/// Level 1 is the base hit and gets nothing extra. Look at what your rule does with the first level.\n#[test]\nfn level_one_is_the_base_hit() {\nassert_eq!(idle_damage_for_level(1), 1);\n}\n/// Levels 2 and 3 should each step up from the level before. Check the size of the step.\n#[test]\nfn each_level_adds_two() {\nassert_eq!(idle_damage_for_level(2), 3);\nassert_eq!(idle_damage_for_level(3), 5);\n}\n/// The step should stay the same as levels climb. Try your rule on a level further out.\n#[test]\nfn later_levels_keep_the_step() {\nassert_eq!(idle_damage_for_level(10), 19);\nassert_eq!(idle_damage_for_level(100), 199);\n}\n/// Level 0 should not crash the game. Unsigned numbers do not like going below zero.\n#[test]\nfn level_zero_does_not_panic() {\nlet _ = idle_damage_for_level(0);\n}\n</details>\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/idle-is-not-a-timer/).\nTip:\nStart from level.saturating_sub(1), like xp_to_next_level does, so level 1 gets no extra. Multiply that by 2 and add the 1 back.\nWhat matters today is that the engine can answer a question it could not answer before:\nWhat happened while I was away?\nWho owns the clock?\nThe game never asked what time it was.\nIt was handed 3600, and it did 3600 seconds of work.\nThat is going to become one of the most important lines in this series:\nRust owns the rules.\nWhatever runs the game owns time.\nToday, that caller is a test or main(). Later, it will be a web page.\nThe caller measures how much real time passed and hands that fact to the game.\nRust decides what that time means for gameplay.\nThat distinction matters. A browser might report that 28,800 seconds passed while the player was away. The game can still decide that only part of that time should count.\nIn other words:\ncaller measures elapsed time\n↓\nRust receives the number\n↓\nRust decides how much time counts\n↓\nRust applies the game rules\nThat keeps balance decisions inside the game instead of hiding them in whichever environment happens to run it.\nThere is one honest cost.\ntick() does one trip through the loop for every second.\nAn hour is 3,600 trips. A day is 86,400.\nThat is still manageable for this tiny program, but the cost grows directly with the number of seconds we simulate.\nA number like u32::MAX would be a very long walk.\nLater, we can cap credited away-time or replace repeated steps with arithmetic. The caller will still report elapsed time; Rust will still own the policy and the rules.\nQuestion: A browser reports 28,800 seconds away. Why should the game, not the caller, decide how many of them count?\nBalance rules belong in Rust, so every caller gets the same policy\nThe caller cannot measure elapsed time\ntick rejects any number above 3600\nAnswer: 1. Balance rules belong in Rust, so every caller gets the same policy\nExplanation: The caller measures; Rust decides what that time means. Capping away-time is a game rule, so it lives with the other rules.\nThe complete program\nIf you are only reading along, skip to the last two sections.\nIf you have been following along in your editor, here is everything in one place.\nAt this point, src/main.rs should look like this:\nstruct Player {\ngold: u32,\nxp: u32,\nlevel: u32,\nclick_upgrade_level: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\ngold_reward: u32,\nxp_reward: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\n#[derive(Debug, PartialEq)]\nenum PurchaseOutcome {\nPurchased,\nNotEnoughGold,\n}\nimpl Player {\nfn new() -> Player {\nPlayer {\ngold: 0,\nxp: 0,\nlevel: 1,\nclick_upgrade_level: 0,\n}\n}\nfn add_gold(&mut self, amount: u32) {\nself.gold = self.gold.saturating_add(amount);\n}\nfn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= xp_to_next_level(self.level) {\nself.xp -= xp_to_next_level(self.level);\nself.level = self.level.saturating_add(1);\n}\n}\nfn base_damage(&self) -> u32 {\nbase_damage_for_level(self.level)\n}\nfn bonus_damage(&self) -> u32 {\nclick_upgrade_bonus(self.click_upgrade_level)\n}\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nlet cost = click_upgrade_cost(self.click_upgrade_level);\nif self.gold < cost {\nreturn PurchaseOutcome::NotEnoughGold;\n}\nself.gold -= cost;\nself.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\nPurchaseOutcome::Purchased\n}\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\ngold_reward: gold_reward_for_stage(stage),\nxp_reward: xp_reward_for_stage(stage),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player::new(),\nenemy: Enemy::for_stage(1),\n}\n}\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nself.resolve_hit();\n}\nfn tick(&mut self, seconds: u32) {\nfor _ in 0..seconds {\nlet damage = idle_damage_for_level(self.player.level);\nself.enemy.health = health_after_hit(self.enemy.health, damage);\nself.resolve_hit();\n}\n}\nfn resolve_hit(&mut self) {\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.player.add_gold(self.enemy.gold_reward);\nself.player.add_xp(self.enemy.xp_reward);\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nself.player.buy_click_upgrade()\n}\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn gold_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(3)\n}\nfn xp_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(4)\n}\nfn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\nfn base_damage_for_level(level: u32) -> u32 {\nlevel.saturating_add(4)\n}\nfn idle_damage_for_level(level: u32) -> u32 {\nlevel\n}\nfn click_upgrade_bonus(upgrades: u32) -> u32 {\nupgrades.saturating_mul(2)\n}\nfn click_upgrade_cost(upgrades: u32) -> u32 {\nupgrades.saturating_add(1).saturating_mul(10)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage(), player.bonus_damage());\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 3 {\nlet stage = game.stage;\nlet level = game.player.level;\ngame.click();\nif game.stage > stage {\nprintln!(\n\"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\ngame.player.gold,\ngame.player.xp,\nxp_to_next_level(game.player.level)\n);\nif game.player.level > level {\nprintln!(\n\"Level up! Now level {}. Click damage: {}\",\ngame.player.level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage())\n);\n}\nprintln!(\n\"  Next up: {} with {} health.\",\ngame.enemy.name, game.enemy.health\n);\n}\nmatch game.buy_click_upgrade() {\nPurchaseOutcome::Purchased => println!(\n\"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\ngame.player.click_upgrade_level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage()),\ngame.player.gold\n),\nPurchaseOutcome::NotEnoughGold => {}\n}\n}\nlet stage = game.stage;\nlet level = game.player.level;\nlet gold = game.player.gold;\ngame.tick(3600);\nprintln!(\"One hour away.\");\nprintln!(\n\"Stage {} -> {}  Level {} -> {}  Gold {} -> {}\",\nstage, game.stage, level, game.player.level, gold, game.player.gold\n);\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player::new();\nlet mut enemy = Enemy::for_stage(1);\nenemy.health = 10;\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 5);\n}\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\n#[test]\nfn later_stages_pay_better() {\nassert_eq!(gold_reward_for_stage(1), 5);\nassert_eq!(gold_reward_for_stage(2), 7);\nassert_eq!(xp_reward_for_stage(1), 6);\nassert_eq!(xp_reward_for_stage(2), 8);\n}\n#[test]\nfn levels_need_more_xp_as_they_go() {\nassert_eq!(xp_to_next_level(1), 20);\nassert_eq!(xp_to_next_level(2), 35);\n}\n#[test]\nfn leveling_up_keeps_leftover_xp() {\nlet mut player = Player::new();\nplayer.add_xp(25);\nassert_eq!(player.level, 2);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn a_big_reward_can_skip_levels() {\nlet mut player = Player::new();\nplayer.add_xp(60);\nassert_eq!(player.level, 3);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn levels_make_the_player_hit_harder() {\nassert_eq!(base_damage_for_level(1), 5);\nassert_eq!(base_damage_for_level(2), 6);\n}\n#[test]\nfn upgrades_cost_more_each_time() {\nassert_eq!(click_upgrade_cost(0), 10);\nassert_eq!(click_upgrade_cost(1), 20);\n}\n#[test]\nfn buying_an_upgrade_needs_gold() {\nlet mut player = Player::new();\nassert_eq!(player.buy_click_upgrade(), PurchaseOutcome::NotEnoughGold);\nassert_eq!(player.click_upgrade_level, 0);\nassert_eq!(player.bonus_damage(), 0);\n}\n#[test]\nfn buying_an_upgrade_spends_gold() {\nlet mut player = Player::new();\nplayer.gold = 12;\nassert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased);\nassert_eq!(player.gold, 2);\nassert_eq!(player.click_upgrade_level, 1);\nassert_eq!(player.bonus_damage(), 2);\n}\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\nassert_eq!(game.player.gold, 0);\n}\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 15);\n}\n#[test]\nfn defeating_an_enemy_pays_the_player() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n#[test]\nfn idle_damage_grows_with_level() {\nassert_eq!(idle_damage_for_level(1), 1);\nassert_eq!(idle_damage_for_level(2), 2);\n}\n#[test]\nfn enough_time_defeats_the_enemy() {\nlet mut game = Game::new();\ngame.tick(20);\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n#[test]\nfn one_second_is_one_idle_hit() {\nlet mut game = Game::new();\ngame.tick(1);\nassert_eq!(game.enemy.health, 19);\n}\n#[test]\nfn zero_seconds_changes_nothing() {\nlet mut game = Game::new();\ngame.tick(0);\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\n}\n#[test]\nfn one_long_tick_matches_many_short_ones() {\nlet mut long = Game::new();\nlet mut short = Game::new();\nlong.tick(45);\nfor _ in 0..45 {\nshort.tick(1);\n}\nassert_eq!(long.stage, short.stage);\nassert_eq!(long.enemy.health, short.enemy.health);\nassert_eq!(long.player.gold, short.player.gold);\nassert_eq!(long.player.xp, short.player.xp);\nassert_eq!(long.player.level, short.player.level);\n}\n}\nRun both:\ncargo run\ncargo test\nThe game should clear three stages, buy one upgrade, spend an hour away, and all twenty-one tests should pass.\nWhat we learned\nAlong the way, we encountered:\ntime as a plain number, passed in by the caller\nthe tick method\nfor loops and ranges like 0..seconds\n_ for a value we do not need\nextracting a shared method so two paths finish the same way\ntests that run an hour without waiting\nTwo ideas are worth keeping.\nThe first is how to choose between the two loops we now know.\nAsk whether you know the count.\nThe level-up loop could not know how many levels one reward would cross, so it was a while.\ntick() knows exactly how many seconds it was given, so it is a for.\nThe second is resolve_hit().\nThe bug was not in the loop. The loop did what we told it.\nThe bug was that two ways of hurting an enemy had to finish the same way, and only one of them did.\nGiving the finish a name fixed it, and now there is one place where \"the enemy went down\" turns into gold, XP, and a new slime.\nYou accidentally built a game engine\nOpen src/main.rs and scroll.\nThree structs. An enum. Three impl blocks. Twelve small rules. A click, a purchase, a clock that is not a clock, and twenty-one tests.\nAll of it is in one file, and the file is getting long.\nNobody planned that.\nEach article needed one more thing, and each thing went in the next free spot.\nBut look at what is in there.\nEverything above main() knows nothing about a terminal, a screen, or a clock.\nOnly main() does.\nToday they share a file. Next time, we put a wall between them, and find out that the part on the far side of the wall is already a small game engine.\nSmall function. Bigger game.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/idle-is-not-a-timer/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2026-10-23T16:25:37Z",
      "id": "https://mnixblog.netlify.app/idle-is-not-a-timer/",
      "image": "https://mnixblog.netlify.app/images/idle-is-not-a-timer/cover.jpg",
      "summary": "Make your Rust idle game progress while you are away. Add tick(seconds), a for loop, and tests that run an hour of game time instantly.",
      "tags": [
        "Rust",
        "WebAssembly",
        "Game Development",
        "Beginners"
      ],
      "title": "Idle Is Not a Timer.",
      "url": "https://mnixblog.netlify.app/idle-is-not-a-timer/"
    },
    {
      "content_html": "<p>Our player earns gold now. After eight stages, they have ninety-six of it and nothing to spend it on.</p>\n<p>That is not a reward. That is a high score.</p>\n<p>So today we are going to sell them something: more damage per click, at a price that goes up every time they buy it.</p>\n<p>The shopping is the easy part.</p>\n<p>The interesting part is what happens when we try to tell the rest of the program whether the purchase worked.</p>\n<p>Because the obvious answer, <code>true</code> or <code>false</code>, turns out to be not quite an answer at all.</p>\n<pre><code class=\"language-text\">Bought click upgrade 1! Click damage: 7  Gold left: 2\n</code></pre>\n<p>Along the way: four quick knowledge checks, two live examples you can edit and run in the page, and a coding challenge at the end.</p>\n<hr />\n<h2 id=\"our-starting-point\">Our starting point<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#our-starting-point\" aria-label=\"Link to this section\">#</a></h2>\n<p>If you are only reading along, <a href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#the-free-upgrade-nobody-earned\">skip past this code to \"The free upgrade nobody earned.\"</a></p>\n<p>If you are building along, you need three structs, three <code>impl</code> blocks, nine small functions, and thirteen tests:</p>\n<pre><code class=\"language-rust\">struct Player {\n    bonus_damage: u32,\n    gold: u32,\n    xp: u32,\n    level: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n    gold_reward: u32,\n    xp_reward: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\nimpl Player {\n    fn new() -&gt; Player {\n        Player {\n            bonus_damage: 2,\n            gold: 0,\n            xp: 0,\n            level: 1,\n        }\n    }\n\n    fn add_gold(&amp;mut self, amount: u32) {\n        self.gold = self.gold.saturating_add(amount);\n    }\n\n    fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= xp_to_next_level(self.level) {\n            self.xp -= xp_to_next_level(self.level);\n            self.level = self.level.saturating_add(1);\n        }\n    }\n\n    fn base_damage(&amp;self) -&gt; u32 {\n        base_damage_for_level(self.level)\n    }\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n            gold_reward: gold_reward_for_stage(stage),\n            xp_reward: xp_reward_for_stage(stage),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player::new(),\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.player.add_gold(self.enemy.gold_reward);\n        self.player.add_xp(self.enemy.xp_reward);\n\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn gold_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(3)\n}\n\nfn xp_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(4)\n}\n\nfn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n\nfn base_damage_for_level(level: u32) -&gt; u32 {\n    level.saturating_add(4)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage(), player.bonus_damage);\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 8 {\n        let stage = game.stage;\n        let level = game.player.level;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\n                \"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\n                game.player.gold,\n                game.player.xp,\n                xp_to_next_level(game.player.level)\n            );\n\n            if game.player.level &gt; level {\n                println!(\n                    \"Level up! Now level {}. Click damage: {}\",\n                    game.player.level,\n                    click_damage(game.player.base_damage(), game.player.bonus_damage)\n                );\n            }\n\n            println!(\n                \"  Next up: {} with {} health.\",\n                game.enemy.name, game.enemy.health\n            );\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player::new();\n\n        let mut enemy = Enemy::for_stage(1);\n        enemy.health = 10;\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 3);\n    }\n\n    #[test]\n    fn later_stages_have_tougher_enemies() {\n        assert_eq!(enemy_health_for_stage(1), 20);\n        assert_eq!(enemy_health_for_stage(2), 30);\n    }\n\n    #[test]\n    fn later_stages_pay_better() {\n        assert_eq!(gold_reward_for_stage(1), 5);\n        assert_eq!(gold_reward_for_stage(2), 7);\n        assert_eq!(xp_reward_for_stage(1), 6);\n        assert_eq!(xp_reward_for_stage(2), 8);\n    }\n\n    #[test]\n    fn levels_need_more_xp_as_they_go() {\n        assert_eq!(xp_to_next_level(1), 20);\n        assert_eq!(xp_to_next_level(2), 35);\n    }\n\n    #[test]\n    fn leveling_up_keeps_leftover_xp() {\n        let mut player = Player::new();\n\n        player.add_xp(25);\n\n        assert_eq!(player.level, 2);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn a_big_reward_can_skip_levels() {\n        let mut player = Player::new();\n\n        player.add_xp(60);\n\n        assert_eq!(player.level, 3);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn levels_make_the_player_hit_harder() {\n        assert_eq!(base_damage_for_level(1), 5);\n        assert_eq!(base_damage_for_level(2), 6);\n    }\n\n    #[test]\n    fn a_new_game_starts_at_stage_one() {\n        let game = Game::new();\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n        assert_eq!(game.player.gold, 0);\n    }\n\n    #[test]\n    fn clicking_damages_the_enemy() {\n        let mut game = Game::new();\n\n        game.click();\n\n        assert_eq!(game.enemy.health, 13);\n    }\n\n    #[test]\n    fn defeating_an_enemy_pays_the_player() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n}\n</code></pre>\n<p><code>Player</code> stores what the player has earned: gold, XP, and a level. Defeating an enemy pays gold and XP, and enough XP levels the player up.</p>\n<p>Click damage comes from two places. <code>base_damage()</code> works it out from the player's level. <code>bonus_damage</code> is just a number stored in the struct.</p>\n<p>Run <code>cargo test</code> and all thirteen should pass.</p>\n<hr />\n<h2 id=\"the-free-upgrade-nobody-earned\">The free upgrade nobody earned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#the-free-upgrade-nobody-earned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Look at that <code>Player</code> again.</p>\n<pre><code class=\"language-rust\">    bonus_damage: u32,\n</code></pre>\n<p>Every new player starts with 2 of it. For free. Since the first time we gave the player a second damage number.</p>\n<p><code>base_damage</code> is already a method, not a field. It is worked out from the player's level, because it is a consequence of that level, not a fact to store.</p>\n<p><code>bonus_damage</code> has the same problem, with a different cause.</p>\n<p>Bonus damage should be a consequence of how many upgrades the player has bought.</p>\n<p>So the player does not need to store the bonus. The player needs to store the shopping.</p>\n<p>Two rules, under <code>base_damage_for_level()</code>:</p>\n<pre><code class=\"language-rust\">fn click_upgrade_bonus(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_mul(2)\n}\n\nfn click_upgrade_cost(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_add(1).saturating_mul(10)\n}\n</code></pre>\n<p>Each upgrade adds 2 damage:</p>\n<pre><code class=\"language-text\">0 upgrades → 0 bonus\n1 upgrade  → 2 bonus\n2 upgrades → 4 bonus\n</code></pre>\n<p>And each one costs more than the last:</p>\n<pre><code class=\"language-text\">first upgrade  → 10 gold\nsecond upgrade → 20 gold\nthird upgrade  → 30 gold\n</code></pre>\n<p>Both rules take the number of upgrades already bought. Neither one knows a <code>Player</code> exists.</p>\n<p>If you build now, Rust warns that both are never used. The next few steps use them.</p>\n<p>Want to feel the price curve before you commit to it? Edit the numbers and press Run.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · tune the shop</figcaption><pre><code class=\"language-rust\">fn click_upgrade_bonus(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_mul(2)\n}\n\nfn click_upgrade_cost(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_add(1).saturating_mul(10)\n}\n\nfn main() {\n    for upgrades in 0..5 {\n        println!(\n            &quot;{upgrades} bought: +{} damage, next one costs {} gold&quot;,\n            click_upgrade_bonus(upgrades),\n            click_upgrade_cost(upgrades)\n        );\n    }\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/\">the original article</a>.</p></figure>\n<p>Try <code>saturating_mul(3)</code> for a stronger upgrade, or <code>saturating_mul(25)</code> for a pricier one. Keep the original numbers in your project, though. The run output later in this article depends on them.</p>\n<p>Now replace the field with a count of purchases:</p>\n<pre><code class=\"language-rust\">struct Player {\n    gold: u32,\n    xp: u32,\n    level: u32,\n    click_upgrade_level: u32,\n}\n</code></pre>\n<p>Rust immediately complains about every place that still mentions <code>bonus_damage</code>. The next few steps clear those errors one at a time.</p>\n<p>A new player has bought nothing:</p>\n<pre><code class=\"language-rust\">            click_upgrade_level: 0,\n</code></pre>\n<p>And just like base damage, bonus damage becomes a method that asks the rule. Put it under <code>base_damage()</code>:</p>\n<pre><code class=\"language-rust\">    fn bonus_damage(&amp;self) -&gt; u32 {\n        click_upgrade_bonus(self.click_upgrade_level)\n    }\n</code></pre>\n<p><code>attack()</code> gets its second pair of parentheses:</p>\n<pre><code class=\"language-rust\">    let damage = click_damage(player.base_damage(), player.bonus_damage());\n</code></pre>\n<p>One more place still reads the old field: the \"Level up!\" line in <code>main()</code>. Rust points straight at it (the line is wrapped here to fit):</p>\n<pre><code class=\"language-text\">error[E0615]: attempted to take value of method\n`bonus_damage` on type `Player`\n</code></pre>\n<p>It even suggests the fix: use parentheses to call the method.</p>\n<pre><code class=\"language-rust\">                    click_damage(game.player.base_damage(), game.player.bonus_damage())\n</code></pre>\n<p>Now it builds again.</p>\n<p>Notice what <code>Player</code> is made of now.</p>\n<pre><code class=\"language-text\">Player\n├── gold\n├── xp\n├── level\n└── click_upgrade_level\n</code></pre>\n<p>Not a single damage number.</p>\n<p>Every field is a record of <strong>progress</strong>: what the player has earned, and what they have done with it.</p>\n<p>The damage numbers are worked out from that progress, by rules, on demand.</p>\n<p>That is going to matter a great deal in a few articles, when we save the game.</p>\n<p>We will only have to save what the player earned. Everything else can be recalculated.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does <code>Player</code> now store <code>click_upgrade_level</code> instead of <code>bonus_damage</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Rust does not allow a struct to store a damage number</li>\n<li>The bonus is a consequence of what the player bought, so a rule can work it out</li>\n<li>Calling a method is faster than reading a field</li>\n<li>So the player can never buy more than one upgrade</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. The bonus is a consequence of what the player bought, so a rule can work it out</p>\n<p>Store the progress, compute the rest. The number of purchases is the fact, and <code>click_upgrade_bonus()</code> turns it into damage whenever we ask.</p>\n</div>\n</div>\n</section>\n<h3 id=\"the-first-fight-changes\">The first fight changes<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#the-first-fight-changes\" aria-label=\"Link to this section\">#</a></h3>\n<p>One honest side effect.</p>\n<p>A new player used to hit for 7: 5 base plus 2 free bonus.</p>\n<p>Now they hit for 5, because the bonus has to be bought.</p>\n<p>The stage 1 slime still has 20 health, so it takes four clicks instead of three.</p>\n<p>Two of our tests notice.</p>\n<p><code>clicking_damages_the_enemy</code> expected 13 health after one click. It is 15 now.</p>\n<p><code>attack_reduces_enemy_health</code> expected 3 health left from 10. It is 5.</p>\n<p>Update both numbers and run <code>cargo test</code>.</p>\n<p>Those tests were not broken. The game changed, and the tests were telling the truth about it.</p>\n<hr />\n<h2 id=\"buying-the-upgrade\">Buying the upgrade<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#buying-the-upgrade\" aria-label=\"Link to this section\">#</a></h2>\n<p>Here is the first version of the purchase. Add it to <code>impl Player</code>, under <code>bonus_damage()</code>:</p>\n<pre><code class=\"language-rust\">    fn buy_click_upgrade(&amp;mut self) -&gt; bool {\n        let cost = click_upgrade_cost(self.click_upgrade_level);\n\n        if self.gold &lt; cost {\n            return false;\n        }\n\n        self.gold -= cost;\n        self.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\n\n        true\n    }\n</code></pre>\n<p>Work out the price.</p>\n<p>If the player cannot afford it, stop, and say so.</p>\n<p>Otherwise, take the money, add the upgrade, and say so.</p>\n<p>Two things to notice.</p>\n<p><code>return false;</code> leaves the method early.</p>\n<p>Everything after it only runs if the player could afford the upgrade.</p>\n<p>That lets the rest of the method read without an <code>else</code> wrapped around it.</p>\n<p>The <code>true</code> on the last line needs no <code>return</code>. It is the same rule every function so far has used: the last expression, with no semicolon, is the value the function gives back. <code>return</code> is only for leaving from somewhere in the middle.</p>\n<p>And <code>self.gold -= cost;</code> is a plain minus sign, like the one in <code>add_xp()</code>.</p>\n<p>The check right above it guarantees <code>self.gold</code> is at least <code>cost</code>, so the subtraction cannot go below zero.</p>\n<p>Then <code>Game</code> passes the request along, inside <code>impl Game</code>:</p>\n<pre><code class=\"language-rust\">    fn buy_click_upgrade(&amp;mut self) -&gt; bool {\n        self.player.buy_click_upgrade()\n    }\n</code></pre>\n<p>The game does not know the price of an upgrade.</p>\n<p>It does not know what happens when you buy one.</p>\n<p>It knows the player handles that.</p>\n<p>Nothing calls either method yet, so Rust warns that they are never used, and <code>click_upgrade_cost()</code> along with them. <code>main()</code> will fix that shortly.</p>\n<hr />\n<h2 id=\"false-is-not-an-answer\"><code>false</code> is not an answer<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#false-is-not-an-answer\" aria-label=\"Link to this section\">#</a></h2>\n<p>This works.</p>\n<p>Now imagine using it:</p>\n<pre><code class=\"language-rust\">if game.buy_click_upgrade() {\n    println!(\"Bought it!\");\n} else {\n    println!(\"???\");\n}\n</code></pre>\n<p>What goes in the <code>else</code>?</p>\n<p><code>false</code> means the purchase did not happen.</p>\n<p>It does not say <em>why</em>.</p>\n<p>Today there is only one reason: not enough gold.</p>\n<p>But look at this from the other side.</p>\n<p>The code calling <code>buy_click_upgrade()</code> has to <em>remember</em> that <code>false</code> means \"not enough gold.\"</p>\n<p>Nothing in the code says so.</p>\n<p>Add a second reason to fail someday, say a maximum upgrade level, and <code>false</code> starts meaning two different things.</p>\n<p>Every place that checked it is now quietly wrong, and Rust will not say a word.</p>\n<p>We want an answer that says what happened in plain words, and that Rust can check for us.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong><code>buy_click_upgrade()</code> returns <code>false</code>. What does the calling code actually know?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>That the player did not have enough gold</li>\n<li>Only that the purchase did not happen</li>\n<li>That the method crashed</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Only that the purchase did not happen</p>\n<p><code>false</code> says no, and nothing else. The reason lives in the programmer's memory, not in the code, and a second way to fail would hide behind the same <code>false</code>.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"give-the-outcome-a-name\">Give the outcome a name<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#give-the-outcome-a-name\" aria-label=\"Link to this section\">#</a></h2>\n<p>Rust has a type for \"one of these specific things\":</p>\n<pre><code class=\"language-rust\">#[derive(Debug, PartialEq)]\nenum PurchaseOutcome {\n    Purchased,\n    NotEnoughGold,\n}\n</code></pre>\n<p>Put it with the structs, after <code>Game</code>. It is a type, like they are. Until something uses it, Rust adds one more never-used warning to the pile.</p>\n<p><code>enum</code> is short for <em>enumeration</em>: a type whose values are listed out, one by one.</p>\n<p>A <code>PurchaseOutcome</code> is either <code>Purchased</code> or <code>NotEnoughGold</code>.</p>\n<p>It cannot be anything else.</p>\n<p>It cannot be both.</p>\n<p>The two options are called <strong>variants</strong>, and you name one the same way you reach into a type for a function:</p>\n<pre><code class=\"language-rust\">PurchaseOutcome::NotEnoughGold\n</code></pre>\n<p>You can think about <code>bool</code> the same way for this purpose: it also stands for one of exactly two possibilities, <code>true</code> or <code>false</code>.</p>\n<p>The difference is that our enum lets us give those possibilities names that belong to our game.</p>\n<h3 id=\"the-line-above-the-enum\">The line above the enum<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#the-line-above-the-enum\" aria-label=\"Link to this section\">#</a></h3>\n<pre><code class=\"language-rust\">#[derive(Debug, PartialEq)]\n</code></pre>\n<p><code>PartialEq</code> is what lets <code>==</code> compare two outcomes. Without it, Rust has no idea whether <code>Purchased == Purchased</code>.</p>\n<p><code>Debug</code> is what lets Rust print an outcome for a programmer to read, so a failing test can show <code>NotEnoughGold</code> in its message.</p>\n<p>Numbers like <code>u32</code> come with both abilities built in. Our brand-new enum starts with neither.</p>\n<p>Rust calls abilities like these <strong>traits</strong>. That is all you need about traits today: a trait is a named ability a type can have, like \"can be compared with <code>==</code>.\"</p>\n<p><code>#[derive(...)]</code> is the one-line way to ask Rust for the standard version of a trait, instead of writing it by hand.</p>\n<p>Our tests will need both.</p>\n<h3 id=\"use-it\">Use it<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#use-it\" aria-label=\"Link to this section\">#</a></h3>\n<p>Now the purchase can say what happened:</p>\n<pre><code class=\"language-rust\">    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        let cost = click_upgrade_cost(self.click_upgrade_level);\n\n        if self.gold &lt; cost {\n            return PurchaseOutcome::NotEnoughGold;\n        }\n\n        self.gold -= cost;\n        self.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\n\n        PurchaseOutcome::Purchased\n    }\n</code></pre>\n<p>Same shape. Same early return.</p>\n<p>Only the answers changed, and now they read like the game talking.</p>\n<p><code>Game::buy_click_upgrade()</code> changes its return type to match:</p>\n<pre><code class=\"language-rust\">    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        self.player.buy_click_upgrade()\n    }\n</code></pre>\n<hr />\n<h2 id=\"match-one-branch-per-answer\"><code>match</code>: one branch per answer<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#match-one-branch-per-answer\" aria-label=\"Link to this section\">#</a></h2>\n<p>Rust's <code>match</code> expression gives us a way to handle each possible variant explicitly.</p>\n<p>Here is how <code>main()</code> will react to a purchase:</p>\n<pre><code class=\"language-rust\">match game.buy_click_upgrade() {\n    PurchaseOutcome::Purchased =&gt; println!(\"Bought it!\"),\n    PurchaseOutcome::NotEnoughGold =&gt; {}\n}\n</code></pre>\n<p><code>match</code> looks at the value and runs the branch that fits.</p>\n<p>Each line is called an <strong>arm</strong>: a pattern, an arrow, and what to do.</p>\n<p>That arrow is <code>=&gt;</code>, a fat arrow. It is not the <code>-&gt;</code> from function signatures, and your fingers will try to type that one at least once.</p>\n<p>Arms end with a comma, like items in a list. An arm whose body is a block in braces can leave the comma off.</p>\n<p>The <code>{}</code> on the second branch means: do nothing.</p>\n<p>That is allowed. What is not allowed is leaving a variant out.</p>\n<p>Delete the <code>NotEnoughGold</code> line and Rust refuses to compile:</p>\n<pre><code class=\"language-text\">error[E0004]: non-exhaustive patterns:\n`PurchaseOutcome::NotEnoughGold` not covered\n</code></pre>\n<p>This is the payoff.</p>\n<p>With our <code>bool</code> version, the caller had to remember what <code>false</code> meant.</p>\n<p>With an enum and <code>match</code>, the possible outcomes are named, and Rust can make us handle every one.</p>\n<p>If we add a third variant next month, every <code>match</code> that names the variants will stop compiling until it says what to do about it.</p>\n<p>Rust is keeping a list for us.</p>\n<p>The full error message also offers to fill the gap for you, with a <em>wildcard pattern</em> or a <code>todo!()</code> placeholder that crashes if it ever runs. A wildcard (<code>_ =&gt;</code>, meaning \"anything else\") would compile, but it switches this check off for every variant you add later. Naming each variant is what keeps the list.</p>\n<p>Try it. Run this, then delete one arm and run it again. Then put the arm back, add a third variant to the enum, and run it without touching the <code>match</code>.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · break the match on purpose</figcaption><pre><code class=\"language-rust\">enum PurchaseOutcome {\n    Purchased,\n    NotEnoughGold,\n}\n\nfn try_to_buy(gold: u32, cost: u32) -&gt; PurchaseOutcome {\n    if gold &lt; cost {\n        return PurchaseOutcome::NotEnoughGold;\n    }\n\n    PurchaseOutcome::Purchased\n}\n\nfn main() {\n    for gold in [5, 10, 15] {\n        match try_to_buy(gold, 10) {\n            PurchaseOutcome::Purchased =&gt; println!(&quot;{gold} gold: bought it!&quot;),\n            PurchaseOutcome::NotEnoughGold =&gt; println!(&quot;{gold} gold: not enough.&quot;),\n        }\n    }\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/\">the original article</a>.</p></figure>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>You add a <code>MaxLevelReached</code> variant to <code>PurchaseOutcome</code>. What does Rust do the next time you build?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Nothing, until a purchase actually returns <code>MaxLevelReached</code></li>\n<li>It refuses to compile the enum itself</li>\n<li>Every <code>match</code> that names the variants, but not the new one, stops compiling</li>\n<li>Every <code>match</code> quietly skips the new variant</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: C. Every <code>match</code> that names the variants, but not the new one, stops compiling</p>\n<p>The check happens when the program is built, not when it runs. The <code>match</code> in <code>main()</code> gets an <code>E0004</code> error until it handles the new variant.</p>\n</div>\n</div>\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Rust reports <code>E0004</code> for a missing arm, and you silence it by adding <code>_ =&gt; {}</code>. What have you given up?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>The existing arms stop running</li>\n<li>Rust will no longer tell you when a variant added later needs handling</li>\n<li>The <code>match</code> no longer compiles</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Rust will no longer tell you when a variant added later needs handling</p>\n<p><code>_</code> means \"anything else\", so a future variant lands in it silently. Naming every variant is what keeps Rust's list.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"main-goes-shopping\"><code>main()</code> goes shopping<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#main-goes-shopping\" aria-label=\"Link to this section\">#</a></h2>\n<p>Add the purchase to the loop, after the stage-cleared block:</p>\n<pre><code class=\"language-rust\">        match game.buy_click_upgrade() {\n            PurchaseOutcome::Purchased =&gt; println!(\n                \"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\n                game.player.click_upgrade_level,\n                click_damage(game.player.base_damage(), game.player.bonus_damage()),\n                game.player.gold\n            ),\n            PurchaseOutcome::NotEnoughGold =&gt; {}\n        }\n</code></pre>\n<p>Every turn, <code>main()</code> tries to buy an upgrade, and only says something when it works.</p>\n<p>Worth being clear about what that means: in this terminal version, the game buys an upgrade the moment the player can afford one. Nobody is deciding anything.</p>\n<p>Later, when this game has a button, the player will choose when to spend. The purchasing rule does not change when that happens. Only who calls it.</p>\n<p>Run it:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<pre><code class=\"language-text\">Stage 1 cleared!  Gold: 5  XP: 6/20\n  Next up: Training Slime with 30 health.\nStage 2 cleared!  Gold: 12  XP: 14/20\n  Next up: Training Slime with 40 health.\nBought click upgrade 1! Click damage: 7  Gold left: 2\nStage 3 cleared!  Gold: 11  XP: 4/35\nLevel up! Now level 2. Click damage: 8\n  Next up: Training Slime with 50 health.\nStage 4 cleared!  Gold: 22  XP: 16/35\n  Next up: Training Slime with 60 health.\nBought click upgrade 2! Click damage: 10  Gold left: 2\nStage 5 cleared!  Gold: 15  XP: 30/35\n  Next up: Training Slime with 70 health.\nStage 6 cleared!  Gold: 30  XP: 11/50\nLevel up! Now level 3. Click damage: 11\n  Next up: Training Slime with 80 health.\nBought click upgrade 3! Click damage: 13  Gold left: 0\nStage 7 cleared!  Gold: 17  XP: 29/50\n  Next up: Training Slime with 90 health.\nStage 8 cleared!  Gold: 36  XP: 49/50\n  Next up: Training Slime with 100 health.\n</code></pre>\n<p>Now gold goes somewhere.</p>\n<p>It climbs, and then it drops to almost nothing, because something got bought.</p>\n<p>Click damage goes 5 → 7 → 8 → 10 → 11 → 13, pushed up from two directions at once: levels the player earns, and upgrades the player buys.</p>\n<p>Compare the last line to where we started this article. Ninety-six gold and nothing to do with it, versus thirty-six gold and three upgrades already paid for.</p>\n<hr />\n<h2 id=\"testing-the-purchase\">Testing the purchase<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#testing-the-purchase\" aria-label=\"Link to this section\">#</a></h2>\n<p>The pricing rule gets a test for its numbers. Put the new tests in <code>mod tests</code>, after <code>levels_make_the_player_hit_harder</code>:</p>\n<pre><code class=\"language-rust\">    #[test]\n    fn upgrades_cost_more_each_time() {\n        assert_eq!(click_upgrade_cost(0), 10);\n        assert_eq!(click_upgrade_cost(1), 20);\n    }\n</code></pre>\n<p>Then the two things that can happen.</p>\n<p>A broke player cannot buy anything, and a failed purchase changes nothing:</p>\n<pre><code class=\"language-rust\">    #[test]\n    fn buying_an_upgrade_needs_gold() {\n        let mut player = Player::new();\n\n        assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::NotEnoughGold);\n        assert_eq!(player.click_upgrade_level, 0);\n        assert_eq!(player.bonus_damage(), 0);\n    }\n\n    #[test]\n    fn buying_an_upgrade_spends_gold() {\n        let mut player = Player::new();\n        player.gold = 12;\n\n        assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased);\n        assert_eq!(player.gold, 2);\n        assert_eq!(player.click_upgrade_level, 1);\n        assert_eq!(player.bonus_damage(), 2);\n    }\n</code></pre>\n<p>Those <code>assert_eq!</code> lines comparing outcomes are exactly why we asked for <code>PartialEq</code> and <code>Debug</code>.</p>\n<p><code>assert_eq!</code> needs both. <code>PartialEq</code> does the comparison. <code>Debug</code> prints the two sides if they do not match. Leave either one out of the <code>derive</code> line and these tests will not even compile.</p>\n<p><code>cargo run</code> would still build without them, because <code>match</code> needs neither. It is the tests that ask.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>You delete <code>Debug</code> from <code>#[derive(Debug, PartialEq)]</code>. What happens to the test that runs <code>assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased)</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>It compiles, but a failing test prints an empty message</li>\n<li>The test code no longer compiles</li>\n<li>Nothing changes</li>\n<li>It panics when the test runs</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. The test code no longer compiles</p>\n<p><code>assert_eq!</code> has to be able to print both sides in case they differ, so it needs <code>Debug</code> before the test can even be built.</p>\n</div>\n</div>\n</section>\n<p>Run them:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>All sixteen should pass.</p>\n<hr />\n<h2 id=\"the-complete-program\">The complete program<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#the-complete-program\" aria-label=\"Link to this section\">#</a></h2>\n<p>If you are only reading along, <a href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#your-turn-cap-the-upgrades\">skip ahead to the coding challenge</a>.</p>\n<p>If you have been following along in your editor, here is everything in one place.</p>\n<p>At this point, <code>src/main.rs</code> should look like this:</p>\n<pre><code class=\"language-rust\">struct Player {\n    gold: u32,\n    xp: u32,\n    level: u32,\n    click_upgrade_level: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n    gold_reward: u32,\n    xp_reward: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\n#[derive(Debug, PartialEq)]\nenum PurchaseOutcome {\n    Purchased,\n    NotEnoughGold,\n}\n\nimpl Player {\n    fn new() -&gt; Player {\n        Player {\n            gold: 0,\n            xp: 0,\n            level: 1,\n            click_upgrade_level: 0,\n        }\n    }\n\n    fn add_gold(&amp;mut self, amount: u32) {\n        self.gold = self.gold.saturating_add(amount);\n    }\n\n    fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= xp_to_next_level(self.level) {\n            self.xp -= xp_to_next_level(self.level);\n            self.level = self.level.saturating_add(1);\n        }\n    }\n\n    fn base_damage(&amp;self) -&gt; u32 {\n        base_damage_for_level(self.level)\n    }\n\n    fn bonus_damage(&amp;self) -&gt; u32 {\n        click_upgrade_bonus(self.click_upgrade_level)\n    }\n\n    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        let cost = click_upgrade_cost(self.click_upgrade_level);\n\n        if self.gold &lt; cost {\n            return PurchaseOutcome::NotEnoughGold;\n        }\n\n        self.gold -= cost;\n        self.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\n\n        PurchaseOutcome::Purchased\n    }\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n            gold_reward: gold_reward_for_stage(stage),\n            xp_reward: xp_reward_for_stage(stage),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player::new(),\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.player.add_gold(self.enemy.gold_reward);\n        self.player.add_xp(self.enemy.xp_reward);\n\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n\n    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        self.player.buy_click_upgrade()\n    }\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn gold_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(3)\n}\n\nfn xp_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(4)\n}\n\nfn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n\nfn base_damage_for_level(level: u32) -&gt; u32 {\n    level.saturating_add(4)\n}\n\nfn click_upgrade_bonus(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_mul(2)\n}\n\nfn click_upgrade_cost(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_add(1).saturating_mul(10)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage(), player.bonus_damage());\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 8 {\n        let stage = game.stage;\n        let level = game.player.level;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\n                \"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\n                game.player.gold,\n                game.player.xp,\n                xp_to_next_level(game.player.level)\n            );\n\n            if game.player.level &gt; level {\n                println!(\n                    \"Level up! Now level {}. Click damage: {}\",\n                    game.player.level,\n                    click_damage(game.player.base_damage(), game.player.bonus_damage())\n                );\n            }\n\n            println!(\n                \"  Next up: {} with {} health.\",\n                game.enemy.name, game.enemy.health\n            );\n        }\n\n        match game.buy_click_upgrade() {\n            PurchaseOutcome::Purchased =&gt; println!(\n                \"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\n                game.player.click_upgrade_level,\n                click_damage(game.player.base_damage(), game.player.bonus_damage()),\n                game.player.gold\n            ),\n            PurchaseOutcome::NotEnoughGold =&gt; {}\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player::new();\n\n        let mut enemy = Enemy::for_stage(1);\n        enemy.health = 10;\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 5);\n    }\n\n    #[test]\n    fn later_stages_have_tougher_enemies() {\n        assert_eq!(enemy_health_for_stage(1), 20);\n        assert_eq!(enemy_health_for_stage(2), 30);\n    }\n\n    #[test]\n    fn later_stages_pay_better() {\n        assert_eq!(gold_reward_for_stage(1), 5);\n        assert_eq!(gold_reward_for_stage(2), 7);\n        assert_eq!(xp_reward_for_stage(1), 6);\n        assert_eq!(xp_reward_for_stage(2), 8);\n    }\n\n    #[test]\n    fn levels_need_more_xp_as_they_go() {\n        assert_eq!(xp_to_next_level(1), 20);\n        assert_eq!(xp_to_next_level(2), 35);\n    }\n\n    #[test]\n    fn leveling_up_keeps_leftover_xp() {\n        let mut player = Player::new();\n\n        player.add_xp(25);\n\n        assert_eq!(player.level, 2);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn a_big_reward_can_skip_levels() {\n        let mut player = Player::new();\n\n        player.add_xp(60);\n\n        assert_eq!(player.level, 3);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn levels_make_the_player_hit_harder() {\n        assert_eq!(base_damage_for_level(1), 5);\n        assert_eq!(base_damage_for_level(2), 6);\n    }\n\n    #[test]\n    fn upgrades_cost_more_each_time() {\n        assert_eq!(click_upgrade_cost(0), 10);\n        assert_eq!(click_upgrade_cost(1), 20);\n    }\n\n    #[test]\n    fn buying_an_upgrade_needs_gold() {\n        let mut player = Player::new();\n\n        assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::NotEnoughGold);\n        assert_eq!(player.click_upgrade_level, 0);\n        assert_eq!(player.bonus_damage(), 0);\n    }\n\n    #[test]\n    fn buying_an_upgrade_spends_gold() {\n        let mut player = Player::new();\n        player.gold = 12;\n\n        assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased);\n        assert_eq!(player.gold, 2);\n        assert_eq!(player.click_upgrade_level, 1);\n        assert_eq!(player.bonus_damage(), 2);\n    }\n\n    #[test]\n    fn a_new_game_starts_at_stage_one() {\n        let game = Game::new();\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n        assert_eq!(game.player.gold, 0);\n    }\n\n    #[test]\n    fn clicking_damages_the_enemy() {\n        let mut game = Game::new();\n\n        game.click();\n\n        assert_eq!(game.enemy.health, 15);\n    }\n\n    #[test]\n    fn defeating_an_enemy_pays_the_player() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n}\n</code></pre>\n<p>Run both:</p>\n<pre><code class=\"language-bash\">cargo run\ncargo test\n</code></pre>\n<p>The game should clear eight stages, buying three upgrades along the way, and all sixteen tests should pass.</p>\n<hr />\n<h2 id=\"your-turn-cap-the-upgrades\">Your turn: cap the upgrades<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#your-turn-cap-the-upgrades\" aria-label=\"Link to this section\">#</a></h2>\n<p>A third way for a purchase to fail is exactly what the enum was built for. This is your chance to watch Rust walk you to every place that needs to know.</p>\n<p><strong>Coding challenge: a maxed-out upgrade</strong></p>\n<p>Stop selling click upgrades after the fifth one.</p>\n<ol>\n<li>Add a third variant to <code>PurchaseOutcome</code>, for an upgrade that is maxed out.</li>\n<li>In <code>buy_click_upgrade()</code>, return that variant when the player already has 5 upgrades. Check this before the price.</li>\n<li>Press Run. Rust will refuse to compile until the <code>match</code> in <code>main()</code> handles your new variant. Add that arm.</li>\n</ol>\n<p>Keep <code>Player</code>, its two fields and <code>buy_click_upgrade()</code> as they are, because the checks call them. <strong>Check my code</strong> will tell you how you are doing.</p>\n<p><strong>Expected result:</strong> five \"Bought upgrade\" lines, then two lines saying the upgrades are maxed out. Gold left stops at 850.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Coding challenge · cap the upgrades</figcaption><pre><code class=\"language-rust\">struct Player {\n    gold: u32,\n    click_upgrade_level: u32,\n}\n\nenum PurchaseOutcome {\n    Purchased,\n    NotEnoughGold,\n    // TODO 1: add a third variant for &quot;this upgrade is maxed out&quot;\n}\n\nimpl Player {\n    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        // TODO 2: stop at 5 upgrades, before checking the price\n\n        let cost = click_upgrade_cost(self.click_upgrade_level);\n\n        if self.gold &lt; cost {\n            return PurchaseOutcome::NotEnoughGold;\n        }\n\n        self.gold -= cost;\n        self.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\n\n        PurchaseOutcome::Purchased\n    }\n}\n\nfn click_upgrade_cost(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_add(1).saturating_mul(10)\n}\n\nfn main() {\n    let mut player = Player {\n        gold: 1000,\n        click_upgrade_level: 0,\n    };\n\n    for _ in 0..7 {\n        match player.buy_click_upgrade() {\n            PurchaseOutcome::Purchased =&gt; println!(\n                &quot;Bought upgrade {}. Gold left: {}&quot;,\n                player.click_upgrade_level, player.gold\n            ),\n            PurchaseOutcome::NotEnoughGold =&gt; println!(&quot;Not enough gold.&quot;),\n            // TODO 3: tell the player the upgrade is maxed out\n        }\n    }\n}\n</code></pre><details class=\"ob-spoiler\"><summary>Spoiler: the checks for this challenge</summary><p>These tests run when you press Check my code on the original article. Reading them first gives away the answer.</p><pre><code class=\"language-rust\">/// Once a player holds 5 upgrades, buying again should leave their gold and level alone. Look at what your function does at the cap.\n#[test]\nfn sixth_upgrade_is_refused() {\n    let mut player = Player {\n        gold: 1000,\n        click_upgrade_level: 5,\n    };\n    player.buy_click_upgrade();\n    assert_eq!(player.gold, 1000);\n    assert_eq!(player.click_upgrade_level, 5);\n}\n\n/// A player at the cap with an empty purse is maxed out, not broke. Which check comes first?\n#[test]\nfn cap_is_checked_before_price() {\n    let mut player = Player {\n        gold: 0,\n        click_upgrade_level: 5,\n    };\n    let outcome = player.buy_click_upgrade();\n    assert!(!matches!(outcome, PurchaseOutcome::NotEnoughGold));\n    assert!(!matches!(outcome, PurchaseOutcome::Purchased));\n}\n\n/// The fifth upgrade still has to sell. Look at where your cap starts.\n#[test]\nfn fifth_upgrade_still_sells() {\n    let mut player = Player {\n        gold: 1000,\n        click_upgrade_level: 4,\n    };\n    let outcome = player.buy_click_upgrade();\n    assert!(matches!(outcome, PurchaseOutcome::Purchased));\n    assert_eq!(player.click_upgrade_level, 5);\n    assert_eq!(player.gold, 950);\n}\n</code></pre></details><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>Step 2 is an early <code>return</code>, just like the <code>NotEnoughGold</code> check below it. Compare <code>self.click_upgrade_level</code> with 5 using <code>&gt;=</code>.</p>\n</aside>\n<details>\n<summary>Show one solution</summary>\n<p>The new variant:</p>\n<pre><code class=\"language-rust\">    MaxedOut,\n</code></pre>\n<p>The early return, at the top of <code>buy_click_upgrade()</code>:</p>\n<pre><code class=\"language-rust\">        if self.click_upgrade_level &gt;= 5 {\n            return PurchaseOutcome::MaxedOut;\n        }\n</code></pre>\n<p>The new arm in <code>main()</code>:</p>\n<pre><code class=\"language-rust\">            PurchaseOutcome::MaxedOut =&gt; println!(\"Click upgrades are maxed out.\"),\n</code></pre>\n</details>\n<p>The challenge is its own small program, so your <code>src/main.rs</code> stays exactly as it is above.</p>\n<hr />\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Along the way, we encountered:</p>\n<ul>\n<li>a second rule-derived value, replacing a stored one</li>\n<li>early <code>return</code></li>\n<li><code>enum</code> and variants</li>\n<li>traits, in one sentence, and <code>#[derive(Debug, PartialEq)]</code></li>\n<li><code>match</code>, and the compiler refusing to let us forget a case</li>\n</ul>\n<p>Two ideas are worth keeping.</p>\n<p>The first finishes the job <code>base_damage()</code> started. <code>Player</code> has no damage numbers in it at all now. It stores progress, and damage is a rule applied to that progress.</p>\n<p>The second is <code>PurchaseOutcome</code>.</p>\n<p>When a <code>bool</code> would have made the caller remember what <code>false</code> meant, we gave the answers names instead, and Rust started checking that every answer gets handled.</p>\n<p>That pattern shows up everywhere in Rust. Two common examples you will eventually meet are <code>Option</code>, for a value that might not be there, and <code>Result</code>, for an operation that might have failed.</p>\n<p>This is the first time we needed our own. It will not be the last.</p>\n<hr />\n<h2 id=\"nothing-happens-unless-you-click\">Nothing happens unless you click<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#nothing-happens-unless-you-click\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our game finally has a progression loop.</p>\n<p>Kill a slime, get paid, level up, buy damage, kill a tougher slime.</p>\n<p>There is just one problem with calling it an idle game.</p>\n<pre><code class=\"language-text\">clicks per second while you are playing: a lot\nclicks per second while you are not:    zero\n</code></pre>\n<p>Nothing happens unless you click.</p>\n<p>Walk away, come back an hour later, and the slime is exactly where you left it.</p>\n<p>An idle game is supposed to keep working while you do not.</p>\n<p>Next time, we are going to give the game time.</p>\n<p>Not a timer. Not a clock. Just a number: how many seconds went by.</p>\n<p>The game will take that number and work out everything that should have happened.</p>\n<p><strong>Small function. Bigger game.</strong></p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "False Is Not an Answer.\nCanonical: https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/\nBuild Click Upgrades With Rust Enums and Match\nOur player earns gold now. After eight stages, they have ninety-six of it and nothing to spend it on.\nThat is not a reward. That is a high score.\nSo today we are going to sell them something: more damage per click, at a price that goes up every time they buy it.\nThe shopping is the easy part.\nThe interesting part is what happens when we try to tell the rest of the program whether the purchase worked.\nBecause the obvious answer, true or false, turns out to be not quite an answer at all.\nBought click upgrade 1! Click damage: 7  Gold left: 2\nAlong the way: four quick knowledge checks, two live examples you can edit and run in the page, and a coding challenge at the end.\nOur starting point\nIf you are only reading along, skip past this code to \"The free upgrade nobody earned.\" (https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#the-free-upgrade-nobody-earned)\nIf you are building along, you need three structs, three impl blocks, nine small functions, and thirteen tests:\nstruct Player {\nbonus_damage: u32,\ngold: u32,\nxp: u32,\nlevel: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\ngold_reward: u32,\nxp_reward: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\nimpl Player {\nfn new() -> Player {\nPlayer {\nbonus_damage: 2,\ngold: 0,\nxp: 0,\nlevel: 1,\n}\n}\nfn add_gold(&mut self, amount: u32) {\nself.gold = self.gold.saturating_add(amount);\n}\nfn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= xp_to_next_level(self.level) {\nself.xp -= xp_to_next_level(self.level);\nself.level = self.level.saturating_add(1);\n}\n}\nfn base_damage(&self) -> u32 {\nbase_damage_for_level(self.level)\n}\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\ngold_reward: gold_reward_for_stage(stage),\nxp_reward: xp_reward_for_stage(stage),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player::new(),\nenemy: Enemy::for_stage(1),\n}\n}\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.player.add_gold(self.enemy.gold_reward);\nself.player.add_xp(self.enemy.xp_reward);\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn gold_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(3)\n}\nfn xp_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(4)\n}\nfn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\nfn base_damage_for_level(level: u32) -> u32 {\nlevel.saturating_add(4)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage(), player.bonus_damage);\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 8 {\nlet stage = game.stage;\nlet level = game.player.level;\ngame.click();\nif game.stage > stage {\nprintln!(\n\"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\ngame.player.gold,\ngame.player.xp,\nxp_to_next_level(game.player.level)\n);\nif game.player.level > level {\nprintln!(\n\"Level up! Now level {}. Click damage: {}\",\ngame.player.level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage)\n);\n}\nprintln!(\n\"  Next up: {} with {} health.\",\ngame.enemy.name, game.enemy.health\n);\n}\n}\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player::new();\nlet mut enemy = Enemy::for_stage(1);\nenemy.health = 10;\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 3);\n}\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\n#[test]\nfn later_stages_pay_better() {\nassert_eq!(gold_reward_for_stage(1), 5);\nassert_eq!(gold_reward_for_stage(2), 7);\nassert_eq!(xp_reward_for_stage(1), 6);\nassert_eq!(xp_reward_for_stage(2), 8);\n}\n#[test]\nfn levels_need_more_xp_as_they_go() {\nassert_eq!(xp_to_next_level(1), 20);\nassert_eq!(xp_to_next_level(2), 35);\n}\n#[test]\nfn leveling_up_keeps_leftover_xp() {\nlet mut player = Player::new();\nplayer.add_xp(25);\nassert_eq!(player.level, 2);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn a_big_reward_can_skip_levels() {\nlet mut player = Player::new();\nplayer.add_xp(60);\nassert_eq!(player.level, 3);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn levels_make_the_player_hit_harder() {\nassert_eq!(base_damage_for_level(1), 5);\nassert_eq!(base_damage_for_level(2), 6);\n}\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\nassert_eq!(game.player.gold, 0);\n}\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 13);\n}\n#[test]\nfn defeating_an_enemy_pays_the_player() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n}\nPlayer stores what the player has earned: gold, XP, and a level. Defeating an enemy pays gold and XP, and enough XP levels the player up.\nClick damage comes from two places. base_damage() works it out from the player's level. bonus_damage is just a number stored in the struct.\nRun cargo test and all thirteen should pass.\nThe free upgrade nobody earned\nLook at that Player again.\nbonus_damage: u32,\nEvery new player starts with 2 of it. For free. Since the first time we gave the player a second damage number.\nbase_damage is already a method, not a field. It is worked out from the player's level, because it is a consequence of that level, not a fact to store.\nbonus_damage has the same problem, with a different cause.\nBonus damage should be a consequence of how many upgrades the player has bought.\nSo the player does not need to store the bonus. The player needs to store the shopping.\nTwo rules, under base_damage_for_level():\nfn click_upgrade_bonus(upgrades: u32) -> u32 {\nupgrades.saturating_mul(2)\n}\nfn click_upgrade_cost(upgrades: u32) -> u32 {\nupgrades.saturating_add(1).saturating_mul(10)\n}\nEach upgrade adds 2 damage:\n0 upgrades → 0 bonus\n1 upgrade  → 2 bonus\n2 upgrades → 4 bonus\nAnd each one costs more than the last:\nfirst upgrade  → 10 gold\nsecond upgrade → 20 gold\nthird upgrade  → 30 gold\nBoth rules take the number of upgrades already bought. Neither one knows a Player exists.\nIf you build now, Rust warns that both are never used. The next few steps use them.\nWant to feel the price curve before you commit to it? Edit the numbers and press Run.\nLive example · tune the shop\nfn click_upgrade_bonus(upgrades: u32) -> u32 {\nupgrades.saturating_mul(2)\n}\nfn click_upgrade_cost(upgrades: u32) -> u32 {\nupgrades.saturating_add(1).saturating_mul(10)\n}\nfn main() {\nfor upgrades in 0..5 {\nprintln!(\n\"{upgrades} bought: +{} damage, next one costs {} gold\",\nclick_upgrade_bonus(upgrades),\nclick_upgrade_cost(upgrades)\n);\n}\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/).\nTry saturating_mul(3) for a stronger upgrade, or saturating_mul(25) for a pricier one. Keep the original numbers in your project, though. The run output later in this article depends on them.\nNow replace the field with a count of purchases:\nstruct Player {\ngold: u32,\nxp: u32,\nlevel: u32,\nclick_upgrade_level: u32,\n}\nRust immediately complains about every place that still mentions bonus_damage. The next few steps clear those errors one at a time.\nA new player has bought nothing:\nclick_upgrade_level: 0,\nAnd just like base damage, bonus damage becomes a method that asks the rule. Put it under base_damage():\nfn bonus_damage(&self) -> u32 {\nclick_upgrade_bonus(self.click_upgrade_level)\n}\nattack() gets its second pair of parentheses:\nlet damage = click_damage(player.base_damage(), player.bonus_damage());\nOne more place still reads the old field: the \"Level up!\" line in main(). Rust points straight at it (the line is wrapped here to fit):\nerror[E0615]: attempted to take value of method\n`bonus_damage` on type `Player`\nIt even suggests the fix: use parentheses to call the method.\nclick_damage(game.player.base_damage(), game.player.bonus_damage())\nNow it builds again.\nNotice what Player is made of now.\nPlayer\n├── gold\n├── xp\n├── level\n└── click_upgrade_level\nNot a single damage number.\nEvery field is a record of progress: what the player has earned, and what they have done with it.\nThe damage numbers are worked out from that progress, by rules, on demand.\nThat is going to matter a great deal in a few articles, when we save the game.\nWe will only have to save what the player earned. Everything else can be recalculated.\nQuestion: Why does Player now store click_upgrade_level instead of bonus_damage?\nRust does not allow a struct to store a damage number\nThe bonus is a consequence of what the player bought, so a rule can work it out\nCalling a method is faster than reading a field\nSo the player can never buy more than one upgrade\nAnswer: 2. The bonus is a consequence of what the player bought, so a rule can work it out\nExplanation: Store the progress, compute the rest. The number of purchases is the fact, and click_upgrade_bonus() turns it into damage whenever we ask.\nThe first fight changes\nOne honest side effect.\nA new player used to hit for 7: 5 base plus 2 free bonus.\nNow they hit for 5, because the bonus has to be bought.\nThe stage 1 slime still has 20 health, so it takes four clicks instead of three.\nTwo of our tests notice.\nclicking_damages_the_enemy expected 13 health after one click. It is 15 now.\nattack_reduces_enemy_health expected 3 health left from 10. It is 5.\nUpdate both numbers and run cargo test.\nThose tests were not broken. The game changed, and the tests were telling the truth about it.\nBuying the upgrade\nHere is the first version of the purchase. Add it to impl Player, under bonus_damage():\nfn buy_click_upgrade(&mut self) -> bool {\nlet cost = click_upgrade_cost(self.click_upgrade_level);\nif self.gold < cost {\nreturn false;\n}\nself.gold -= cost;\nself.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\ntrue\n}\nWork out the price.\nIf the player cannot afford it, stop, and say so.\nOtherwise, take the money, add the upgrade, and say so.\nTwo things to notice.\nreturn false; leaves the method early.\nEverything after it only runs if the player could afford the upgrade.\nThat lets the rest of the method read without an else wrapped around it.\nThe true on the last line needs no return. It is the same rule every function so far has used: the last expression, with no semicolon, is the value the function gives back. return is only for leaving from somewhere in the middle.\nAnd self.gold -= cost; is a plain minus sign, like the one in add_xp().\nThe check right above it guarantees self.gold is at least cost, so the subtraction cannot go below zero.\nThen Game passes the request along, inside impl Game:\nfn buy_click_upgrade(&mut self) -> bool {\nself.player.buy_click_upgrade()\n}\nThe game does not know the price of an upgrade.\nIt does not know what happens when you buy one.\nIt knows the player handles that.\nNothing calls either method yet, so Rust warns that they are never used, and click_upgrade_cost() along with them. main() will fix that shortly.\nfalse is not an answer\nThis works.\nNow imagine using it:\nif game.buy_click_upgrade() {\nprintln!(\"Bought it!\");\n} else {\nprintln!(\"???\");\n}\nWhat goes in the else?\nfalse means the purchase did not happen.\nIt does not say why.\nToday there is only one reason: not enough gold.\nBut look at this from the other side.\nThe code calling buy_click_upgrade() has to remember that false means \"not enough gold.\"\nNothing in the code says so.\nAdd a second reason to fail someday, say a maximum upgrade level, and false starts meaning two different things.\nEvery place that checked it is now quietly wrong, and Rust will not say a word.\nWe want an answer that says what happened in plain words, and that Rust can check for us.\nQuestion: buy_click_upgrade() returns false. What does the calling code actually know?\nThat the player did not have enough gold\nOnly that the purchase did not happen\nThat the method crashed\nAnswer: 2. Only that the purchase did not happen\nExplanation: false says no, and nothing else. The reason lives in the programmer's memory, not in the code, and a second way to fail would hide behind the same false.\nGive the outcome a name\nRust has a type for \"one of these specific things\":\n#[derive(Debug, PartialEq)]\nenum PurchaseOutcome {\nPurchased,\nNotEnoughGold,\n}\nPut it with the structs, after Game. It is a type, like they are. Until something uses it, Rust adds one more never-used warning to the pile.\nenum is short for enumeration: a type whose values are listed out, one by one.\nA PurchaseOutcome is either Purchased or NotEnoughGold.\nIt cannot be anything else.\nIt cannot be both.\nThe two options are called variants, and you name one the same way you reach into a type for a function:\nPurchaseOutcome::NotEnoughGold\nYou can think about bool the same way for this purpose: it also stands for one of exactly two possibilities, true or false.\nThe difference is that our enum lets us give those possibilities names that belong to our game.\nThe line above the enum\n#[derive(Debug, PartialEq)]\nPartialEq is what lets == compare two outcomes. Without it, Rust has no idea whether Purchased == Purchased.\nDebug is what lets Rust print an outcome for a programmer to read, so a failing test can show NotEnoughGold in its message.\nNumbers like u32 come with both abilities built in. Our brand-new enum starts with neither.\nRust calls abilities like these traits. That is all you need about traits today: a trait is a named ability a type can have, like \"can be compared with ==.\"\n#[derive(...)] is the one-line way to ask Rust for the standard version of a trait, instead of writing it by hand.\nOur tests will need both.\nUse it\nNow the purchase can say what happened:\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nlet cost = click_upgrade_cost(self.click_upgrade_level);\nif self.gold < cost {\nreturn PurchaseOutcome::NotEnoughGold;\n}\nself.gold -= cost;\nself.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\nPurchaseOutcome::Purchased\n}\nSame shape. Same early return.\nOnly the answers changed, and now they read like the game talking.\nGame::buy_click_upgrade() changes its return type to match:\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nself.player.buy_click_upgrade()\n}\nmatch: one branch per answer\nRust's match expression gives us a way to handle each possible variant explicitly.\nHere is how main() will react to a purchase:\nmatch game.buy_click_upgrade() {\nPurchaseOutcome::Purchased => println!(\"Bought it!\"),\nPurchaseOutcome::NotEnoughGold => {}\n}\nmatch looks at the value and runs the branch that fits.\nEach line is called an arm: a pattern, an arrow, and what to do.\nThat arrow is =>, a fat arrow. It is not the -> from function signatures, and your fingers will try to type that one at least once.\nArms end with a comma, like items in a list. An arm whose body is a block in braces can leave the comma off.\nThe {} on the second branch means: do nothing.\nThat is allowed. What is not allowed is leaving a variant out.\nDelete the NotEnoughGold line and Rust refuses to compile:\nerror[E0004]: non-exhaustive patterns:\n`PurchaseOutcome::NotEnoughGold` not covered\nThis is the payoff.\nWith our bool version, the caller had to remember what false meant.\nWith an enum and match, the possible outcomes are named, and Rust can make us handle every one.\nIf we add a third variant next month, every match that names the variants will stop compiling until it says what to do about it.\nRust is keeping a list for us.\nThe full error message also offers to fill the gap for you, with a wildcard pattern or a todo!() placeholder that crashes if it ever runs. A wildcard (_ =>, meaning \"anything else\") would compile, but it switches this check off for every variant you add later. Naming each variant is what keeps the list.\nTry it. Run this, then delete one arm and run it again. Then put the arm back, add a third variant to the enum, and run it without touching the match.\nLive example · break the match on purpose\nenum PurchaseOutcome {\nPurchased,\nNotEnoughGold,\n}\nfn try_to_buy(gold: u32, cost: u32) -> PurchaseOutcome {\nif gold < cost {\nreturn PurchaseOutcome::NotEnoughGold;\n}\nPurchaseOutcome::Purchased\n}\nfn main() {\nfor gold in [5, 10, 15] {\nmatch try_to_buy(gold, 10) {\nPurchaseOutcome::Purchased => println!(\"{gold} gold: bought it!\"),\nPurchaseOutcome::NotEnoughGold => println!(\"{gold} gold: not enough.\"),\n}\n}\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/).\nQuestion: You add a MaxLevelReached variant to PurchaseOutcome. What does Rust do the next time you build?\nNothing, until a purchase actually returns MaxLevelReached\nIt refuses to compile the enum itself\nEvery match that names the variants, but not the new one, stops compiling\nEvery match quietly skips the new variant\nAnswer: 3. Every match that names the variants, but not the new one, stops compiling\nExplanation: The check happens when the program is built, not when it runs. The match in main() gets an E0004 error until it handles the new variant.\nQuestion: Rust reports E0004 for a missing arm, and you silence it by adding _ => {}. What have you given up?\nThe existing arms stop running\nRust will no longer tell you when a variant added later needs handling\nThe match no longer compiles\nAnswer: 2. Rust will no longer tell you when a variant added later needs handling\nExplanation: _ means \"anything else\", so a future variant lands in it silently. Naming every variant is what keeps Rust's list.\nmain() goes shopping\nAdd the purchase to the loop, after the stage-cleared block:\nmatch game.buy_click_upgrade() {\nPurchaseOutcome::Purchased => println!(\n\"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\ngame.player.click_upgrade_level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage()),\ngame.player.gold\n),\nPurchaseOutcome::NotEnoughGold => {}\n}\nEvery turn, main() tries to buy an upgrade, and only says something when it works.\nWorth being clear about what that means: in this terminal version, the game buys an upgrade the moment the player can afford one. Nobody is deciding anything.\nLater, when this game has a button, the player will choose when to spend. The purchasing rule does not change when that happens. Only who calls it.\nRun it:\ncargo run\nStage 1 cleared!  Gold: 5  XP: 6/20\nNext up: Training Slime with 30 health.\nStage 2 cleared!  Gold: 12  XP: 14/20\nNext up: Training Slime with 40 health.\nBought click upgrade 1! Click damage: 7  Gold left: 2\nStage 3 cleared!  Gold: 11  XP: 4/35\nLevel up! Now level 2. Click damage: 8\nNext up: Training Slime with 50 health.\nStage 4 cleared!  Gold: 22  XP: 16/35\nNext up: Training Slime with 60 health.\nBought click upgrade 2! Click damage: 10  Gold left: 2\nStage 5 cleared!  Gold: 15  XP: 30/35\nNext up: Training Slime with 70 health.\nStage 6 cleared!  Gold: 30  XP: 11/50\nLevel up! Now level 3. Click damage: 11\nNext up: Training Slime with 80 health.\nBought click upgrade 3! Click damage: 13  Gold left: 0\nStage 7 cleared!  Gold: 17  XP: 29/50\nNext up: Training Slime with 90 health.\nStage 8 cleared!  Gold: 36  XP: 49/50\nNext up: Training Slime with 100 health.\nNow gold goes somewhere.\nIt climbs, and then it drops to almost nothing, because something got bought.\nClick damage goes 5 → 7 → 8 → 10 → 11 → 13, pushed up from two directions at once: levels the player earns, and upgrades the player buys.\nCompare the last line to where we started this article. Ninety-six gold and nothing to do with it, versus thirty-six gold and three upgrades already paid for.\nTesting the purchase\nThe pricing rule gets a test for its numbers. Put the new tests in mod tests, after levels_make_the_player_hit_harder:\n#[test]\nfn upgrades_cost_more_each_time() {\nassert_eq!(click_upgrade_cost(0), 10);\nassert_eq!(click_upgrade_cost(1), 20);\n}\nThen the two things that can happen.\nA broke player cannot buy anything, and a failed purchase changes nothing:\n#[test]\nfn buying_an_upgrade_needs_gold() {\nlet mut player = Player::new();\nassert_eq!(player.buy_click_upgrade(), PurchaseOutcome::NotEnoughGold);\nassert_eq!(player.click_upgrade_level, 0);\nassert_eq!(player.bonus_damage(), 0);\n}\n#[test]\nfn buying_an_upgrade_spends_gold() {\nlet mut player = Player::new();\nplayer.gold = 12;\nassert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased);\nassert_eq!(player.gold, 2);\nassert_eq!(player.click_upgrade_level, 1);\nassert_eq!(player.bonus_damage(), 2);\n}\nThose assert_eq! lines comparing outcomes are exactly why we asked for PartialEq and Debug.\nassert_eq! needs both. PartialEq does the comparison. Debug prints the two sides if they do not match. Leave either one out of the derive line and these tests will not even compile.\ncargo run would still build without them, because match needs neither. It is the tests that ask.\nQuestion: You delete Debug from #[derive(Debug, PartialEq)]. What happens to the test that runs assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased)?\nIt compiles, but a failing test prints an empty message\nThe test code no longer compiles\nNothing changes\nIt panics when the test runs\nAnswer: 2. The test code no longer compiles\nExplanation: assert_eq! has to be able to print both sides in case they differ, so it needs Debug before the test can even be built.\nRun them:\ncargo test\nAll sixteen should pass.\nThe complete program\nIf you are only reading along, skip ahead to the coding challenge (https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#your-turn-cap-the-upgrades).\nIf you have been following along in your editor, here is everything in one place.\nAt this point, src/main.rs should look like this:\nstruct Player {\ngold: u32,\nxp: u32,\nlevel: u32,\nclick_upgrade_level: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\ngold_reward: u32,\nxp_reward: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\n#[derive(Debug, PartialEq)]\nenum PurchaseOutcome {\nPurchased,\nNotEnoughGold,\n}\nimpl Player {\nfn new() -> Player {\nPlayer {\ngold: 0,\nxp: 0,\nlevel: 1,\nclick_upgrade_level: 0,\n}\n}\nfn add_gold(&mut self, amount: u32) {\nself.gold = self.gold.saturating_add(amount);\n}\nfn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= xp_to_next_level(self.level) {\nself.xp -= xp_to_next_level(self.level);\nself.level = self.level.saturating_add(1);\n}\n}\nfn base_damage(&self) -> u32 {\nbase_damage_for_level(self.level)\n}\nfn bonus_damage(&self) -> u32 {\nclick_upgrade_bonus(self.click_upgrade_level)\n}\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nlet cost = click_upgrade_cost(self.click_upgrade_level);\nif self.gold < cost {\nreturn PurchaseOutcome::NotEnoughGold;\n}\nself.gold -= cost;\nself.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\nPurchaseOutcome::Purchased\n}\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\ngold_reward: gold_reward_for_stage(stage),\nxp_reward: xp_reward_for_stage(stage),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player::new(),\nenemy: Enemy::for_stage(1),\n}\n}\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.player.add_gold(self.enemy.gold_reward);\nself.player.add_xp(self.enemy.xp_reward);\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nself.player.buy_click_upgrade()\n}\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn gold_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(3)\n}\nfn xp_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(4)\n}\nfn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\nfn base_damage_for_level(level: u32) -> u32 {\nlevel.saturating_add(4)\n}\nfn click_upgrade_bonus(upgrades: u32) -> u32 {\nupgrades.saturating_mul(2)\n}\nfn click_upgrade_cost(upgrades: u32) -> u32 {\nupgrades.saturating_add(1).saturating_mul(10)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage(), player.bonus_damage());\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 8 {\nlet stage = game.stage;\nlet level = game.player.level;\ngame.click();\nif game.stage > stage {\nprintln!(\n\"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\ngame.player.gold,\ngame.player.xp,\nxp_to_next_level(game.player.level)\n);\nif game.player.level > level {\nprintln!(\n\"Level up! Now level {}. Click damage: {}\",\ngame.player.level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage())\n);\n}\nprintln!(\n\"  Next up: {} with {} health.\",\ngame.enemy.name, game.enemy.health\n);\n}\nmatch game.buy_click_upgrade() {\nPurchaseOutcome::Purchased => println!(\n\"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\ngame.player.click_upgrade_level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage()),\ngame.player.gold\n),\nPurchaseOutcome::NotEnoughGold => {}\n}\n}\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player::new();\nlet mut enemy = Enemy::for_stage(1);\nenemy.health = 10;\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 5);\n}\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\n#[test]\nfn later_stages_pay_better() {\nassert_eq!(gold_reward_for_stage(1), 5);\nassert_eq!(gold_reward_for_stage(2), 7);\nassert_eq!(xp_reward_for_stage(1), 6);\nassert_eq!(xp_reward_for_stage(2), 8);\n}\n#[test]\nfn levels_need_more_xp_as_they_go() {\nassert_eq!(xp_to_next_level(1), 20);\nassert_eq!(xp_to_next_level(2), 35);\n}\n#[test]\nfn leveling_up_keeps_leftover_xp() {\nlet mut player = Player::new();\nplayer.add_xp(25);\nassert_eq!(player.level, 2);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn a_big_reward_can_skip_levels() {\nlet mut player = Player::new();\nplayer.add_xp(60);\nassert_eq!(player.level, 3);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn levels_make_the_player_hit_harder() {\nassert_eq!(base_damage_for_level(1), 5);\nassert_eq!(base_damage_for_level(2), 6);\n}\n#[test]\nfn upgrades_cost_more_each_time() {\nassert_eq!(click_upgrade_cost(0), 10);\nassert_eq!(click_upgrade_cost(1), 20);\n}\n#[test]\nfn buying_an_upgrade_needs_gold() {\nlet mut player = Player::new();\nassert_eq!(player.buy_click_upgrade(), PurchaseOutcome::NotEnoughGold);\nassert_eq!(player.click_upgrade_level, 0);\nassert_eq!(player.bonus_damage(), 0);\n}\n#[test]\nfn buying_an_upgrade_spends_gold() {\nlet mut player = Player::new();\nplayer.gold = 12;\nassert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased);\nassert_eq!(player.gold, 2);\nassert_eq!(player.click_upgrade_level, 1);\nassert_eq!(player.bonus_damage(), 2);\n}\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\nassert_eq!(game.player.gold, 0);\n}\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 15);\n}\n#[test]\nfn defeating_an_enemy_pays_the_player() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n}\nRun both:\ncargo run\ncargo test\nThe game should clear eight stages, buying three upgrades along the way, and all sixteen tests should pass.\nYour turn: cap the upgrades\nA third way for a purchase to fail is exactly what the enum was built for. This is your chance to watch Rust walk you to every place that needs to know.\nCoding challenge: a maxed-out upgrade\nStop selling click upgrades after the fifth one.\nAdd a third variant to PurchaseOutcome, for an upgrade that is maxed out.\nIn buy_click_upgrade(), return that variant when the player already has 5 upgrades. Check this before the price.\nPress Run. Rust will refuse to compile until the match in main() handles your new variant. Add that arm.\nKeep Player, its two fields and buy_click_upgrade() as they are, because the checks call them. Check my code will tell you how you are doing.\nExpected result: five \"Bought upgrade\" lines, then two lines saying the upgrades are maxed out. Gold left stops at 850.\nCoding challenge · cap the upgrades\nstruct Player {\ngold: u32,\nclick_upgrade_level: u32,\n}\nenum PurchaseOutcome {\nPurchased,\nNotEnoughGold,\n// TODO 1: add a third variant for \"this upgrade is maxed out\"\n}\nimpl Player {\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\n// TODO 2: stop at 5 upgrades, before checking the price\nlet cost = click_upgrade_cost(self.click_upgrade_level);\nif self.gold < cost {\nreturn PurchaseOutcome::NotEnoughGold;\n}\nself.gold -= cost;\nself.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\nPurchaseOutcome::Purchased\n}\n}\nfn click_upgrade_cost(upgrades: u32) -> u32 {\nupgrades.saturating_add(1).saturating_mul(10)\n}\nfn main() {\nlet mut player = Player {\ngold: 1000,\nclick_upgrade_level: 0,\n};\nfor _ in 0..7 {\nmatch player.buy_click_upgrade() {\nPurchaseOutcome::Purchased => println!(\n\"Bought upgrade {}. Gold left: {}\",\nplayer.click_upgrade_level, player.gold\n),\nPurchaseOutcome::NotEnoughGold => println!(\"Not enough gold.\"),\n// TODO 3: tell the player the upgrade is maxed out\n}\n}\n}\n<details>\n<summary>Spoiler: the checks for this challenge</summary>\nThese tests run when you press Check my code on the original article. Reading them first gives away the answer.\n/// Once a player holds 5 upgrades, buying again should leave their gold and level alone. Look at what your function does at the cap.\n#[test]\nfn sixth_upgrade_is_refused() {\nlet mut player = Player {\ngold: 1000,\nclick_upgrade_level: 5,\n};\nplayer.buy_click_upgrade();\nassert_eq!(player.gold, 1000);\nassert_eq!(player.click_upgrade_level, 5);\n}\n/// A player at the cap with an empty purse is maxed out, not broke. Which check comes first?\n#[test]\nfn cap_is_checked_before_price() {\nlet mut player = Player {\ngold: 0,\nclick_upgrade_level: 5,\n};\nlet outcome = player.buy_click_upgrade();\nassert!(!matches!(outcome, PurchaseOutcome::NotEnoughGold));\nassert!(!matches!(outcome, PurchaseOutcome::Purchased));\n}\n/// The fifth upgrade still has to sell. Look at where your cap starts.\n#[test]\nfn fifth_upgrade_still_sells() {\nlet mut player = Player {\ngold: 1000,\nclick_upgrade_level: 4,\n};\nlet outcome = player.buy_click_upgrade();\nassert!(matches!(outcome, PurchaseOutcome::Purchased));\nassert_eq!(player.click_upgrade_level, 5);\nassert_eq!(player.gold, 950);\n}\n</details>\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/).\nTip:\nStep 2 is an early return, just like the NotEnoughGold check below it. Compare self.click_upgrade_level with 5 using >=.\n<details>\n<summary>Show one solution</summary>\nThe new variant:\nMaxedOut,\nThe early return, at the top of buy_click_upgrade():\nif self.click_upgrade_level >= 5 {\nreturn PurchaseOutcome::MaxedOut;\n}\nThe new arm in main():\nPurchaseOutcome::MaxedOut => println!(\"Click upgrades are maxed out.\"),\n</details>\nThe challenge is its own small program, so your src/main.rs stays exactly as it is above.\nWhat we learned\nAlong the way, we encountered:\na second rule-derived value, replacing a stored one\nearly return\nenum and variants\ntraits, in one sentence, and #[derive(Debug, PartialEq)]\nmatch, and the compiler refusing to let us forget a case\nTwo ideas are worth keeping.\nThe first finishes the job base_damage() started. Player has no damage numbers in it at all now. It stores progress, and damage is a rule applied to that progress.\nThe second is PurchaseOutcome.\nWhen a bool would have made the caller remember what false meant, we gave the answers names instead, and Rust started checking that every answer gets handled.\nThat pattern shows up everywhere in Rust. Two common examples you will eventually meet are Option, for a value that might not be there, and Result, for an operation that might have failed.\nThis is the first time we needed our own. It will not be the last.\nNothing happens unless you click\nOur game finally has a progression loop.\nKill a slime, get paid, level up, buy damage, kill a tougher slime.\nThere is just one problem with calling it an idle game.\nclicks per second while you are playing: a lot\nclicks per second while you are not:    zero\nNothing happens unless you click.\nWalk away, come back an hour later, and the slime is exactly where you left it.\nAn idle game is supposed to keep working while you do not.\nNext time, we are going to give the game time.\nNot a timer. Not a clock. Just a number: how many seconds went by.\nThe game will take that number and work out everything that should have happened.\nSmall function. Bigger game.\nPortable export note: 3 raw HTML fragment(s) are shown as literal source because this Markdown export does not execute or preserve raw HTML. View the canonical article (https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/).\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2026-10-21T04:15:10Z",
      "id": "https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/",
      "image": "https://mnixblog.netlify.app/images/rust-enums-and-match-click-upgrades/cover.jpg",
      "summary": "Our player has ninety-six gold and nothing to buy. Build a shop in Rust, then replace a vague true or false with an enum and let match handle every outcome.",
      "tags": [
        "Rust",
        "WebAssembly",
        "Game Development",
        "Beginners"
      ],
      "title": "False Is Not an Answer.",
      "url": "https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/"
    },
    {
      "content_html": "<p>Our game can fight its way through stage after stage now.</p>\n<p>Click. The slime falls. A tougher one oozes in. Click.</p>\n<p>That is a loop.</p>\n<p>It is also a treadmill.</p>\n<p>Every slime gets stronger. Our player never does.</p>\n<p>In this article, winning starts to pay: gold and experience for every kill, and levels that make the player hit harder.</p>\n<p>By the end, clearing a stage prints this:</p>\n<pre><code class=\"language-text\">Stage 3 cleared!  Gold: 21  XP: 4/35\nLevel up! Now level 2. Click damage: 8\n  Next up: Training Slime with 50 health.\n</code></pre>\n<hr />\n<h2 id=\"our-starting-point\">Our starting point<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#our-starting-point\" aria-label=\"Link to this section\">#</a></h2>\n<p>If you are only reading along, you can skip past this code to \"Where does the gold go?\"</p>\n<p>If you are building along, you need three <code>struct</code>s, two <code>impl</code> blocks, five small functions, and eight tests:</p>\n<pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player {\n                base_damage: 5,\n                bonus_damage: 2,\n            },\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage, player.bonus_damage);\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 3 {\n        let stage = game.stage;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\"Stage {stage} cleared!\");\n        } else {\n            println!(\"{} health: {}\", game.enemy.name, game.enemy.health);\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player {\n            base_damage: 5,\n            bonus_damage: 2,\n        };\n\n        let mut enemy = Enemy {\n            name: String::from(\"Training Slime\"),\n            health: 10,\n        };\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 3);\n    }\n\n    #[test]\n    fn later_stages_have_tougher_enemies() {\n        assert_eq!(enemy_health_for_stage(1), 20);\n        assert_eq!(enemy_health_for_stage(2), 30);\n    }\n\n    #[test]\n    fn a_new_game_starts_at_stage_one() {\n        let game = Game::new();\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n    }\n\n    #[test]\n    fn clicking_damages_the_enemy() {\n        let mut game = Game::new();\n\n        game.click();\n\n        assert_eq!(game.enemy.health, 13);\n    }\n\n    #[test]\n    fn defeating_an_enemy_advances_the_stage() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n    }\n}\n</code></pre>\n<p><code>game.click()</code> attacks the enemy. If that attack kills it, <code>defeat_enemy()</code> advances the stage and sends in the next one.</p>\n<p>The tests hold those rules in place. Three of them are about to change, so it helps to see them as they are now.</p>\n<p>Everything today builds on that.</p>\n<hr />\n<h2 id=\"where-does-the-gold-go\">Where does the gold go?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#where-does-the-gold-go\" aria-label=\"Link to this section\">#</a></h2>\n<p>Defeating an enemy should pay.</p>\n<p>Before we can pay the player, the player needs somewhere to put the money.</p>\n<p>Right now, our <code>Player</code> has two fields, and neither of them is a wallet.</p>\n<p>Add one:</p>\n<pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n    gold: u32,\n}\n</code></pre>\n<p>Rust immediately complains.</p>\n<p>Every place we build a <code>Player</code> now has to say how much gold it starts with.</p>\n<p>There are two of those: <code>Game::new()</code> and the <code>attack_reduces_enemy_health</code> test. (<code>cargo run</code> only finds the first one, because it does not compile the tests. <code>cargo test</code> finds both.)</p>\n<p>We are about to change <code>Player</code> a few more times today, and we do not want to chase every field through every place a player gets built.</p>\n<p><code>Enemy::for_stage()</code> already solves this exact problem for enemies.</p>\n<p>Give <code>Player</code> the same treatment:</p>\n<pre><code class=\"language-rust\">impl Player {\n    fn new() -&gt; Player {\n        Player {\n            base_damage: 5,\n            bonus_damage: 2,\n            gold: 0,\n        }\n    }\n}\n</code></pre>\n<p>Put it right after the structs, next to <code>impl Enemy</code>.</p>\n<p>Same convention as <code>Game::new()</code>: <code>new</code> is not a Rust keyword, just the name Rust programmers use for \"make me a fresh one of these.\"</p>\n<p>Then use it in <code>Game::new()</code>:</p>\n<pre><code class=\"language-rust\">            player: Player::new(),\n</code></pre>\n<p>and in the test:</p>\n<pre><code class=\"language-rust\">        let player = Player::new();\n</code></pre>\n<p>Now there is exactly one place that knows what a new player looks like.</p>\n<p>When <code>Player</code> changes again, and it will, that is the only place we touch.</p>\n<p>While you are in the tests, <code>a_new_game_starts_at_stage_one</code> can check that the wallet starts empty:</p>\n<pre><code class=\"language-rust\">    assert_eq!(game.player.gold, 0);\n</code></pre>\n<p>Run <code>cargo test</code>.</p>\n<p>All eight should still pass.</p>\n<p>If you <code>cargo run</code> instead, Rust warns that <code>gold</code> is never read. A warning, not an error. The game is about to read it.</p>\n<hr />\n<h2 id=\"how-much-is-a-slime-worth\">How much is a slime worth?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#how-much-is-a-slime-worth\" aria-label=\"Link to this section\">#</a></h2>\n<p>How much gold should a defeated enemy drop?</p>\n<p>That is a game rule.</p>\n<p>Game rules get their own small function, like <code>enemy_health_for_stage()</code>.</p>\n<p>Give this one the same treatment, right next to it:</p>\n<pre><code class=\"language-rust\">fn gold_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(3)\n}\n</code></pre>\n<pre><code class=\"language-text\">stage 1 → 5 gold\nstage 2 → 7 gold\nstage 3 → 9 gold\n</code></pre>\n<p>Try it: press Run to see those three numbers, plus two more stages. The last line asks for the reward at the biggest stage a <code>u32</code> can hold. Rewrite the body as <code>stage * 2 + 3</code> and run it again. In this debug build, Rust stops with an \"attempt to multiply with overflow\" panic instead of printing a number.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · saturating or not</figcaption><pre><code class=\"language-rust\">fn gold_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(3)\n}\n\nfn main() {\n    for stage in 1..=5 {\n        println!(&quot;stage {stage} -&gt; {} gold&quot;, gold_reward_for_stage(stage));\n    }\n\n    println!(&quot;stage {} -&gt; {} gold&quot;, u32::MAX, gold_reward_for_stage(u32::MAX));\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/\">the original article</a>.</p></figure>\n<p>As written, the last line prints <code>4294967295 gold</code>: the biggest <code>u32</code>, because the arithmetic stops at the top instead of going past it.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does <code>gold_reward_for_stage()</code> use <code>saturating_mul</code> and <code>saturating_add</code> instead of <code>*</code> and <code>+</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>They make the reward bigger at high stages</li>\n<li>If the result would be too big for a <code>u32</code>, they stop at the largest value instead of overflowing</li>\n<li>Plain <code>*</code> and <code>+</code> do not work on <code>u32</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. If the result would be too big for a <code>u32</code>, they stop at the largest value instead of overflowing</p>\n<p>Plain <code>*</code> and <code>+</code> work, but an overflow panics in a debug build and wraps around in a release build. Saturating arithmetic gives the same safe answer in both.</p>\n</div>\n</div>\n</section>\n<p>Later enemies are tougher.</p>\n<p>They should also pay better.</p>\n<p>Now, where does that reward live?</p>\n<p>It belongs to the enemy.</p>\n<p>A slime knows how much health it has.</p>\n<p>It should also know what it is worth:</p>\n<pre><code class=\"language-rust\">struct Enemy {\n    name: String,\n    health: u32,\n    gold_reward: u32,\n}\n</code></pre>\n<p>And <code>Enemy::for_stage()</code> fills it in:</p>\n<pre><code class=\"language-rust\">impl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n            gold_reward: gold_reward_for_stage(stage),\n        }\n    }\n}\n</code></pre>\n<p>Because every enemy is built through <code>for_stage()</code>, this is the only place that needed to change.</p>\n<p>Now run <code>cargo test</code> again.</p>\n<p><code>cargo run</code> would build, with one more never-read warning, because the game only builds enemies through <code>for_stage()</code>.</p>\n<p>The tests do not build. <code>attack_reduces_enemy_health</code> builds an <code>Enemy</code> by hand, with a name and 10 health, and Rust wants a <code>gold_reward</code> there too.</p>\n<p>That is the compiler finding the last place for us.</p>\n<p>Instead of making that test learn about rewards, let it start from a real enemy and adjust the one thing it cares about:</p>\n<pre><code class=\"language-rust\">        let mut enemy = Enemy::for_stage(1);\n        enemy.health = 10;\n</code></pre>\n<p>The test still checks what it always checked: an attack takes health off the enemy.</p>\n<hr />\n<h2 id=\"paying-out\">Paying out<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#paying-out\" aria-label=\"Link to this section\">#</a></h2>\n<p>The player has a wallet. The enemy has a price on its head.</p>\n<p>Handing one to the other is the player's job.</p>\n<p>Add a method to <code>impl Player</code>, under <code>new()</code>:</p>\n<pre><code class=\"language-rust\">    fn add_gold(&amp;mut self, amount: u32) {\n        self.gold = self.gold.saturating_add(amount);\n    }\n</code></pre>\n<p><code>&amp;mut self</code> is the same <code>&amp;mut self</code> from <code>click()</code>.</p>\n<p>The method borrows the player, changes it, and gives it back.</p>\n<p>Which brings us to <code>defeat_enemy()</code>, the one place in the game that already knows an enemy just died.</p>\n<p>It grows:</p>\n<pre><code class=\"language-rust\">    fn defeat_enemy(&amp;mut self) {\n        self.player.add_gold(self.enemy.gold_reward);\n\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n</code></pre>\n<p>The order matters.</p>\n<p>We pay the player <strong>before</strong> the enemy is replaced.</p>\n<p>On the next line, <code>self.enemy</code> becomes a brand new slime with a brand new reward.</p>\n<p>If we paid afterward, the player would get paid for an enemy they have not fought yet.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>In <code>defeat_enemy()</code>, why does <code>add_gold()</code> come before <code>self.enemy = Enemy::for_stage(self.stage)</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Rust requires method calls to come before assignments</li>\n<li>After the assignment, <code>self.enemy.gold_reward</code> belongs to the next enemy, not the one just defeated</li>\n<li><code>add_gold()</code> would not compile after the enemy is replaced</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. After the assignment, <code>self.enemy.gold_reward</code> belongs to the next enemy, not the one just defeated</p>\n<p>The code would still compile. It would just pay the wrong reward, which is why the order is a game rule and not something Rust can catch.</p>\n</div>\n</div>\n</section>\n<p>Read <code>defeat_enemy()</code> out loud now:</p>\n<blockquote>\n<p>Pay the player. Go to the next stage. Bring in the next enemy.</p>\n</blockquote>\n<p>No arithmetic to decode.</p>\n<hr />\n<h2 id=\"experience-and-levels\">Experience and levels<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#experience-and-levels\" aria-label=\"Link to this section\">#</a></h2>\n<p>Gold is something to spend.</p>\n<p>Experience, or XP, is something to keep.</p>\n<p>Every defeated enemy should also give the player experience, and when they have enough, they level up.</p>\n<p>That is two rules, so we need two numbers:</p>\n<pre><code class=\"language-text\">how much XP does an enemy give?\nhow much XP does the next level take?\n</code></pre>\n<p>The first one looks like the gold rule.</p>\n<p>Put it right under <code>gold_reward_for_stage()</code>:</p>\n<pre><code class=\"language-rust\">fn xp_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(4)\n}\n</code></pre>\n<p>The second one is new.</p>\n<p>Each level should take a little more than the last:</p>\n<pre><code class=\"language-rust\">fn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n</code></pre>\n<pre><code class=\"language-text\">level 1 → 20 XP to reach level 2\nlevel 2 → 35 XP to reach level 3\nlevel 3 → 50 XP to reach level 4\n</code></pre>\n<p>That chain is split across lines so each step is easy to read. A line starting with <code>.</code> just continues the line above it.</p>\n<p>Level 1 is the starting line. The <code>saturating_sub(1)</code> makes it count as zero steps taken, so level 1 needs the plain 20 and every level after that adds 15. Without it, the first level would already cost 35.</p>\n<p>The exact numbers do not matter yet.</p>\n<p>They have names and they will have tests.</p>\n<p>The enemy carries its XP reward the same way it carries gold:</p>\n<pre><code class=\"language-rust\">struct Enemy {\n    name: String,\n    health: u32,\n    gold_reward: u32,\n    xp_reward: u32,\n}\n</code></pre>\n<p>Add it to <code>Enemy::for_stage()</code>, under <code>gold_reward</code>:</p>\n<pre><code class=\"language-rust\">            xp_reward: xp_reward_for_stage(stage),\n</code></pre>\n<p>And the player needs two more fields: how much XP they have, and what level they are:</p>\n<pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n    gold: u32,\n    xp: u32,\n    level: u32,\n}\n</code></pre>\n<p>In <code>Player::new()</code>:</p>\n<pre><code class=\"language-rust\">            xp: 0,\n            level: 1,\n</code></pre>\n<p>That is now the normal place a fresh player is built, so future changes stay concentrated there.</p>\n<p>Run <code>cargo test</code>. Still eight, plus a few warnings that the new fields and <code>xp_to_next_level()</code> are never used. The next section uses them.</p>\n<hr />\n<h2 id=\"leveling-up\">Leveling up<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#leveling-up\" aria-label=\"Link to this section\">#</a></h2>\n<p>Now the interesting part.</p>\n<p>Adding XP is not like adding gold.</p>\n<p>Gold just piles up.</p>\n<p>XP piles up until it crosses a line, and then something happens:</p>\n<pre><code class=\"language-rust\">    fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= xp_to_next_level(self.level) {\n            self.xp -= xp_to_next_level(self.level);\n            self.level = self.level.saturating_add(1);\n        }\n    }\n</code></pre>\n<p>Add it to <code>impl Player</code>, right under <code>add_gold()</code>.</p>\n<p>Walk through it with a level 1 player who has 0 XP and just earned 25.</p>\n<p>The XP becomes 25.</p>\n<p>Level 1 needs 20. We have 25. That is enough, so we go into the loop.</p>\n<p>Spend 20 of it. XP is now 5. Level is now 2.</p>\n<p>Level 2 needs 35. We have 5. The loop stops.</p>\n<p>The player is level 2 with 5 XP toward the next one.</p>\n<p>Why a <code>while</code> and not an <code>if</code>?</p>\n<p>Because a reward might be big enough to cross two lines at once.</p>\n<p>Give that same level 1 player 60 XP.</p>\n<p>Spend 20 → level 2, 40 left.</p>\n<p>Level 2 needs 35. 40 is enough. Spend 35 → level 3, 5 left.</p>\n<p>Level 3 needs 50. The loop stops.</p>\n<p>An <code>if</code> would have stopped at level 2 and left 40 XP sitting there, more than enough for another level the player never got.</p>\n<p>A <code>while</code> keeps going until the XP really is below the line.</p>\n<p>Try it: press Run and the 60 XP walkthrough plays out as <code>level 3 with 5 XP left over</code>. Then change <code>while</code> to <code>if</code> and run it again. The output becomes <code>level 2 with 40 XP left over</code>, which is the stuck player from above.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · swap the while for an if</figcaption><pre><code class=\"language-rust\">struct Player {\n    xp: u32,\n    level: u32,\n}\n\nfn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n\nimpl Player {\n    fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= xp_to_next_level(self.level) {\n            self.xp -= xp_to_next_level(self.level);\n            self.level = self.level.saturating_add(1);\n        }\n    }\n}\n\nfn main() {\n    let mut player = Player { xp: 0, level: 1 };\n\n    player.add_xp(60);\n\n    println!(&quot;level {} with {} XP left over&quot;, player.level, player.xp);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/\">the original article</a>.</p></figure>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>A level 1 player earns 60 XP at once. What does <code>if</code> instead of <code>while</code> leave behind?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>A player at level 3 with 5 XP</li>\n<li>A player at level 2 holding 40 XP, enough for another level they never got</li>\n<li>A compile error, because <code>if</code> cannot be used there</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. A player at level 2 holding 40 XP, enough for another level they never got</p>\n<p><code>if</code> checks the line once. <code>while</code> keeps checking until the XP really is below it.</p>\n</div>\n</div>\n</section>\n<p>You might notice that <code>xp_to_next_level(self.level)</code> gets called twice in a row. That is fine. It is a tiny calculation, and for the same level it always gives the same answer.</p>\n<p>That matters more than it looks right now. Later in this series, a player is going to come back after a few hours away and collect a very large pile of XP all at once.</p>\n<h3 id=\"a-plain-minus-sign\">A plain minus sign<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#a-plain-minus-sign\" aria-label=\"Link to this section\">#</a></h3>\n<p>Look at this line:</p>\n<pre><code class=\"language-rust\">self.xp -= xp_to_next_level(self.level);\n</code></pre>\n<p><code>-=</code> is shorthand: <code>self.xp -= 20</code> means <code>self.xp = self.xp - 20</code>.</p>\n<p>That is ordinary subtraction, not <code>saturating_sub()</code>.</p>\n<p>Everywhere else in this program, subtraction goes through <code>saturating_sub()</code>.</p>\n<p>Here, it is safe, and the line right above it is the reason.</p>\n<p>The <code>while</code> only lets us in when <code>self.xp</code> is at least as big as the amount we are about to subtract.</p>\n<p>The result can never drop below zero.</p>\n<p><code>saturating_sub()</code> is for when a number <em>might</em> go somewhere it should not.</p>\n<p>When the code right next to it proves that it cannot, plain subtraction says exactly what it means.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why is the plain <code>-=</code> in <code>add_xp()</code> safe?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>-=</code> quietly stops at zero, the same as <code>saturating_sub()</code></li>\n<li>The <code>while</code> condition only lets us in when <code>self.xp</code> is at least the amount being subtracted</li>\n<li><code>xp</code> is a <code>u32</code>, and <code>u32</code> can never go below zero</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. The <code>while</code> condition only lets us in when <code>self.xp</code> is at least the amount being subtracted</p>\n<p>A <code>u32</code> cannot hold a negative number, so a plain subtraction that went below zero would be a bug, not a clamp. Here the loop condition rules that out.</p>\n</div>\n</div>\n</section>\n<p>Now <code>defeat_enemy()</code> pays both:</p>\n<pre><code class=\"language-rust\">        self.player.add_gold(self.enemy.gold_reward);\n        self.player.add_xp(self.enemy.xp_reward);\n</code></pre>\n<hr />\n<h2 id=\"what-does-a-level-actually-do\">What does a level actually do?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#what-does-a-level-actually-do\" aria-label=\"Link to this section\">#</a></h2>\n<p>Right now, nothing.</p>\n<p>The player levels up, and hits exactly as hard as before.</p>\n<p>A level should make the player stronger.</p>\n<p>Look at <code>Player</code> again:</p>\n<pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n    gold: u32,\n    xp: u32,\n    level: u32,\n}\n</code></pre>\n<p><code>base_damage</code> is a number we wrote down once, when the player was created, and never touched again.</p>\n<p>But base damage should not be a fact about the player anymore.</p>\n<p>It should be a <em>consequence</em> of the player's level.</p>\n<p>That makes it a rule. Put it under <code>xp_to_next_level()</code>:</p>\n<pre><code class=\"language-rust\">fn base_damage_for_level(level: u32) -&gt; u32 {\n    level.saturating_add(4)\n}\n</code></pre>\n<pre><code class=\"language-text\">level 1 → 5 damage\nlevel 2 → 6 damage\nlevel 3 → 7 damage\n</code></pre>\n<p>Level 1 gives 5, which is exactly the <code>base_damage</code> we have had since the beginning.</p>\n<p>Nothing about the first fight changes.</p>\n<p>Now the field can go.</p>\n<p>Remove <code>base_damage</code> from the struct and from <code>Player::new()</code>.</p>\n<p>Rust will complain about one more place that still uses it. We will get to that in a second.</p>\n<p>In its place, give <code>Player</code> a method that asks the rule, right under <code>add_xp()</code>:</p>\n<pre><code class=\"language-rust\">    fn base_damage(&amp;self) -&gt; u32 {\n        base_damage_for_level(self.level)\n    }\n</code></pre>\n<p>There is something new in that signature.</p>\n<p><code>&amp;self</code>, without the <code>mut</code>.</p>\n<p>So far, our methods have needed <code>&amp;mut self</code> because they changed something.</p>\n<p>A method that only needs to <strong>read</strong> takes <code>&amp;self</code> instead. This is our first one.</p>\n<p><code>base_damage()</code> looks at the player's level and reports a number.</p>\n<p>It does not change anything, so it does not ask for permission to.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does <code>base_damage()</code> take <code>&amp;self</code> and <code>add_xp()</code> take <code>&amp;mut self</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>&amp;self</code> methods run faster</li>\n<li><code>add_xp()</code> changes the player, and <code>base_damage()</code> only reads it</li>\n<li>Only the first method in an <code>impl</code> block may change the player</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. <code>add_xp()</code> changes the player, and <code>base_damage()</code> only reads it</p>\n<p><code>&amp;mut self</code> is the borrow that allows changes. A method that only reports a value asks for the smaller <code>&amp;self</code>.</p>\n</div>\n</div>\n</section>\n<p>One thing still uses the old field: <code>attack()</code>.</p>\n<p>It reads <code>player.base_damage</code>, and that field no longer exists.</p>\n<p>Rust says so (the line is wrapped here to fit):</p>\n<pre><code class=\"language-text\">error[E0615]: attempted to take value of method\n`base_damage` on type `&amp;Player`\n</code></pre>\n<p>and even suggests the fix: use parentheses to call the method.</p>\n<pre><code class=\"language-rust\">fn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage(), player.bonus_damage);\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n</code></pre>\n<p>The parentheses are the only difference.</p>\n<p><code>player.base_damage</code> was a number stored in the player.</p>\n<p><code>player.base_damage()</code> is a number the player works out from its level.</p>\n<p><code>attack()</code> did not have to learn anything about levels.</p>\n<p>It asked for base damage. It got base damage.</p>\n<p>Run <code>cargo test</code>. All eight still pass, and that is the point: a level 1 player still deals 5 base damage, so nothing a test checks has changed.</p>\n<hr />\n<h2 id=\"main-shows-the-progress\"><code>main()</code> shows the progress<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#main-shows-the-progress\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our <code>main()</code> has been printing the enemy's health after every click.</p>\n<p>That was useful when a fight took three clicks. Now there are rewards to watch, so let us print when something happens instead:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 8 {\n        let stage = game.stage;\n        let level = game.player.level;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\n                \"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\n                game.player.gold,\n                game.player.xp,\n                xp_to_next_level(game.player.level)\n            );\n\n            if game.player.level &gt; level {\n                println!(\n                    \"Level up! Now level {}. Click damage: {}\",\n                    game.player.level,\n                    click_damage(game.player.base_damage(), game.player.bonus_damage)\n                );\n            }\n\n            println!(\n                \"  Next up: {} with {} health.\",\n                game.enemy.name, game.enemy.health\n            );\n        }\n    }\n}\n</code></pre>\n<p>Before each click, <code>main()</code> remembers the stage and the level.</p>\n<p>After the click, it compares.</p>\n<p>Stage went up? The enemy died, so say so, show the wallet, and introduce the next one.</p>\n<p>Level went up? Say that too.</p>\n<p>Notice that the level check sits <em>inside</em> the stage check. It can, because XP is only ever handed out in <code>defeat_enemy()</code>. No enemy died, no XP, no level up.</p>\n<p>One small thing about that first <code>println!</code>. <code>{stage}</code> works because <code>stage</code> is a plain variable. <code>game.player.gold</code> has dots in it, so it goes in the list after the string and a bare <code>{}</code> picks it up.</p>\n<p><code>main()</code> still does not know how leveling works.</p>\n<p>It does borrow one rule, <code>xp_to_next_level()</code>, so it can show how far away the next level is. That is display, not a decision.</p>\n<p>It clicks, it asks, and it prints what it was told.</p>\n<p>Run it:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<pre><code class=\"language-text\">Stage 1 cleared!  Gold: 5  XP: 6/20\n  Next up: Training Slime with 30 health.\nStage 2 cleared!  Gold: 12  XP: 14/20\n  Next up: Training Slime with 40 health.\nStage 3 cleared!  Gold: 21  XP: 4/35\nLevel up! Now level 2. Click damage: 8\n  Next up: Training Slime with 50 health.\nStage 4 cleared!  Gold: 32  XP: 16/35\n  Next up: Training Slime with 60 health.\nStage 5 cleared!  Gold: 45  XP: 30/35\n  Next up: Training Slime with 70 health.\nStage 6 cleared!  Gold: 60  XP: 11/50\nLevel up! Now level 3. Click damage: 9\n  Next up: Training Slime with 80 health.\nStage 7 cleared!  Gold: 77  XP: 29/50\n  Next up: Training Slime with 90 health.\nStage 8 cleared!  Gold: 96  XP: 49/50\n  Next up: Training Slime with 100 health.\n</code></pre>\n<p>Read that top to bottom.</p>\n<p>Gold climbs. XP climbs toward a line, crosses it, and resets with the leftover carried over. Click damage starts at 7 and climbs to 8, then 9.</p>\n<p>Compare that to where we started this article: 5 base damage plus 2 bonus, on stage 1 and on stage 30 alike.</p>\n<p>The slimes still get tougher every stage. The difference is that now, so does the player.</p>\n<hr />\n<h2 id=\"testing-the-rules\">Testing the rules<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#testing-the-rules\" aria-label=\"Link to this section\">#</a></h2>\n<p>Each new rule gets a test for its numbers. Add these to the <code>tests</code> module:</p>\n<pre><code class=\"language-rust\">    #[test]\n    fn later_stages_pay_better() {\n        assert_eq!(gold_reward_for_stage(1), 5);\n        assert_eq!(gold_reward_for_stage(2), 7);\n        assert_eq!(xp_reward_for_stage(1), 6);\n        assert_eq!(xp_reward_for_stage(2), 8);\n    }\n\n    #[test]\n    fn levels_need_more_xp_as_they_go() {\n        assert_eq!(xp_to_next_level(1), 20);\n        assert_eq!(xp_to_next_level(2), 35);\n    }\n\n    #[test]\n    fn levels_make_the_player_hit_harder() {\n        assert_eq!(base_damage_for_level(1), 5);\n        assert_eq!(base_damage_for_level(2), 6);\n    }\n</code></pre>\n<p>Then the behaviors.</p>\n<p>The two walkthroughs from <code>add_xp()</code> become tests, so the explanation and the code can never drift apart:</p>\n<pre><code class=\"language-rust\">    #[test]\n    fn leveling_up_keeps_leftover_xp() {\n        let mut player = Player::new();\n\n        player.add_xp(25);\n\n        assert_eq!(player.level, 2);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn a_big_reward_can_skip_levels() {\n        let mut player = Player::new();\n\n        player.add_xp(60);\n\n        assert_eq!(player.level, 3);\n        assert_eq!(player.xp, 5);\n    }\n</code></pre>\n<p>And the big one.</p>\n<p>The last test in our starting point, <code>defeating_an_enemy_advances_the_stage</code>, checked that a win moved the game forward.</p>\n<p>Now a win should also pay.</p>\n<p>Rename it and add two checks at the end:</p>\n<pre><code class=\"language-rust\">    #[test]\n    fn defeating_an_enemy_pays_the_player() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n</code></pre>\n<p>This test does not care how damage is calculated, or how XP crosses a threshold.</p>\n<p>Those rules have their own tests now.</p>\n<p>It asks the bigger question:</p>\n<blockquote>\n<p>When a click kills something, does the player get paid for it?</p>\n</blockquote>\n<p>Run them:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>All thirteen should pass.</p>\n<hr />\n<h2 id=\"the-complete-program\">The complete program<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#the-complete-program\" aria-label=\"Link to this section\">#</a></h2>\n<p>If you are only reading along, skip to the last two sections.</p>\n<p>If you have been following along in your editor, here is everything in one place.</p>\n<p>At this point, <code>src/main.rs</code> should look like this:</p>\n<pre><code class=\"language-rust\">struct Player {\n    bonus_damage: u32,\n    gold: u32,\n    xp: u32,\n    level: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n    gold_reward: u32,\n    xp_reward: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\nimpl Player {\n    fn new() -&gt; Player {\n        Player {\n            bonus_damage: 2,\n            gold: 0,\n            xp: 0,\n            level: 1,\n        }\n    }\n\n    fn add_gold(&amp;mut self, amount: u32) {\n        self.gold = self.gold.saturating_add(amount);\n    }\n\n    fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= xp_to_next_level(self.level) {\n            self.xp -= xp_to_next_level(self.level);\n            self.level = self.level.saturating_add(1);\n        }\n    }\n\n    fn base_damage(&amp;self) -&gt; u32 {\n        base_damage_for_level(self.level)\n    }\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n            gold_reward: gold_reward_for_stage(stage),\n            xp_reward: xp_reward_for_stage(stage),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player::new(),\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.player.add_gold(self.enemy.gold_reward);\n        self.player.add_xp(self.enemy.xp_reward);\n\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn gold_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(3)\n}\n\nfn xp_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(4)\n}\n\nfn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n\nfn base_damage_for_level(level: u32) -&gt; u32 {\n    level.saturating_add(4)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage(), player.bonus_damage);\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 8 {\n        let stage = game.stage;\n        let level = game.player.level;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\n                \"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\n                game.player.gold,\n                game.player.xp,\n                xp_to_next_level(game.player.level)\n            );\n\n            if game.player.level &gt; level {\n                println!(\n                    \"Level up! Now level {}. Click damage: {}\",\n                    game.player.level,\n                    click_damage(game.player.base_damage(), game.player.bonus_damage)\n                );\n            }\n\n            println!(\n                \"  Next up: {} with {} health.\",\n                game.enemy.name, game.enemy.health\n            );\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player::new();\n\n        let mut enemy = Enemy::for_stage(1);\n        enemy.health = 10;\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 3);\n    }\n\n    #[test]\n    fn later_stages_have_tougher_enemies() {\n        assert_eq!(enemy_health_for_stage(1), 20);\n        assert_eq!(enemy_health_for_stage(2), 30);\n    }\n\n    #[test]\n    fn later_stages_pay_better() {\n        assert_eq!(gold_reward_for_stage(1), 5);\n        assert_eq!(gold_reward_for_stage(2), 7);\n        assert_eq!(xp_reward_for_stage(1), 6);\n        assert_eq!(xp_reward_for_stage(2), 8);\n    }\n\n    #[test]\n    fn levels_need_more_xp_as_they_go() {\n        assert_eq!(xp_to_next_level(1), 20);\n        assert_eq!(xp_to_next_level(2), 35);\n    }\n\n    #[test]\n    fn leveling_up_keeps_leftover_xp() {\n        let mut player = Player::new();\n\n        player.add_xp(25);\n\n        assert_eq!(player.level, 2);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn a_big_reward_can_skip_levels() {\n        let mut player = Player::new();\n\n        player.add_xp(60);\n\n        assert_eq!(player.level, 3);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn levels_make_the_player_hit_harder() {\n        assert_eq!(base_damage_for_level(1), 5);\n        assert_eq!(base_damage_for_level(2), 6);\n    }\n\n    #[test]\n    fn a_new_game_starts_at_stage_one() {\n        let game = Game::new();\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n        assert_eq!(game.player.gold, 0);\n    }\n\n    #[test]\n    fn clicking_damages_the_enemy() {\n        let mut game = Game::new();\n\n        game.click();\n\n        assert_eq!(game.enemy.health, 13);\n    }\n\n    #[test]\n    fn defeating_an_enemy_pays_the_player() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n}\n</code></pre>\n<p>Run both:</p>\n<pre><code class=\"language-bash\">cargo run\ncargo test\n</code></pre>\n<p>The game should clear eight stages, and all thirteen tests should pass.</p>\n<p>Want to practice the <code>while</code> loop on its own? Here is a small one to finish.</p>\n<p><strong>Coding challenge: total XP to reach a level</strong></p>\n<p>Write <code>total_xp_to_reach(level)</code>: the XP a player has spent in total by the time they arrive at <code>level</code>. Level 1 is where everyone starts, so it costs nothing. Keep the name <code>total_xp_to_reach</code> and its <code>level</code> argument, because the checks call it. Press <strong>Check my code</strong> and it will tell you how you are doing.</p>\n<p><strong>Expected result:</strong> it prints <code>0</code>, <code>20</code>, <code>55</code> and <code>105</code> for levels 1 to 4. Right now it prints <code>0</code> four times.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Coding challenge · total XP to reach a level</figcaption><pre><code class=\"language-rust\">fn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n\nfn total_xp_to_reach(level: u32) -&gt; u32 {\n    // TODO: add up xp_to_next_level for level 1, 2, ... up to (not including) `level`\n    0\n}\n\nfn main() {\n    for level in 1..=4 {\n        println!(&quot;level {level}: {} total XP&quot;, total_xp_to_reach(level));\n    }\n}\n</code></pre><details class=\"ob-spoiler\"><summary>Spoiler: the checks for this challenge</summary><p>These tests run when you press Check my code on the original article. Reading them first gives away the answer.</p><pre><code class=\"language-rust\">/// Level 1 costs nothing, and arriving at level 2 costs what level 1 asks for. Does the loop stop before `level`?\n#[test]\nfn first_steps() {\n    assert_eq!(total_xp_to_reach(1), 0);\n    assert_eq!(total_xp_to_reach(2), 20);\n}\n\n/// Every level on the way has to be added, one at a time. The total grows with each step.\n#[test]\nfn adds_every_step() {\n    assert_eq!(total_xp_to_reach(3), 55);\n    assert_eq!(total_xp_to_reach(4), 105);\n}\n\n/// A huge level should not panic or wrap around. The hint names a method that stops at the ceiling.\n#[test]\nfn stops_at_the_ceiling() {\n    assert_eq!(total_xp_to_reach(100_000), u32::MAX);\n}\n</code></pre></details><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>Keep a running <code>total</code> (start it as <code>let mut total: u32 = 0;</code>) and a <code>current</code> level that starts at 1. While <code>current</code> is below <code>level</code>, add <code>xp_to_next_level(current)</code> to the total with <code>saturating_add</code>, then move <code>current</code> up by one.</p>\n</aside>\n<p>The challenge is its own small program, so your <code>src/main.rs</code> stays exactly as it is above.</p>\n<hr />\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Along the way, we encountered:</p>\n<ul>\n<li>\n<p><code>Player::new()</code>, so a player is built in one place</p>\n</li>\n<li>\n<p><code>&amp;self</code> methods that read without changing</p>\n</li>\n<li>\n<p>a <code>while</code> loop that keeps going until a condition stops being true</p>\n</li>\n<li>\n<p><code>-=</code>, and plain subtraction where the code above it proves the number cannot go below zero</p>\n</li>\n<li>\n<p>rules that calculate a value instead of storing it</p>\n</li>\n</ul>\n<p>That is a short list, and the last item is the one worth keeping.</p>\n<p><code>base_damage</code> used to be a number sitting in the player.</p>\n<p>Now it is a rule applied to the player's level.</p>\n<p>The player got smaller, and the game got bigger.</p>\n<p>Hold onto that idea. We are going to do it again next time, to the other half of that damage calculation.</p>\n<hr />\n<h2 id=\"gold-with-nothing-to-buy\">Gold with nothing to buy<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#gold-with-nothing-to-buy\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our player finally gets stronger by winning.</p>\n<p>There is just one problem.</p>\n<p>Look at the last line of that run again:</p>\n<pre><code class=\"language-text\">Stage 8 cleared!  Gold: 96  XP: 49/50\n</code></pre>\n<p>Ninety-six gold.</p>\n<p>We call it money, but there is nothing in this game to spend it on.</p>\n<pre><code class=\"language-text\">gold earned: 96\ngold spent:  0\n</code></pre>\n<p>Right now it is not a reward.</p>\n<p>It is a high score.</p>\n<p>Next time, we are going to give the player something to buy, and discover that the obvious way to report whether a purchase worked is not good enough.</p>\n<p><strong>Small function. Bigger game.</strong></p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Winning Is Not Progress.\nCanonical: https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/\nAdd Gold, XP, and Levels to Your Rust Game\nOur game can fight its way through stage after stage now.\nClick. The slime falls. A tougher one oozes in. Click.\nThat is a loop.\nIt is also a treadmill.\nEvery slime gets stronger. Our player never does.\nIn this article, winning starts to pay: gold and experience for every kill, and levels that make the player hit harder.\nBy the end, clearing a stage prints this:\nStage 3 cleared!  Gold: 21  XP: 4/35\nLevel up! Now level 2. Click damage: 8\nNext up: Training Slime with 50 health.\nOur starting point\nIf you are only reading along, you can skip past this code to \"Where does the gold go?\"\nIf you are building along, you need three structs, two impl blocks, five small functions, and eight tests:\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player {\nbase_damage: 5,\nbonus_damage: 2,\n},\nenemy: Enemy::for_stage(1),\n}\n}\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage, player.bonus_damage);\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 3 {\nlet stage = game.stage;\ngame.click();\nif game.stage > stage {\nprintln!(\"Stage {stage} cleared!\");\n} else {\nprintln!(\"{} health: {}\", game.enemy.name, game.enemy.health);\n}\n}\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 10,\n};\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 3);\n}\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\n}\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 13);\n}\n#[test]\nfn defeating_an_enemy_advances_the_stage() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\n}\n}\ngame.click() attacks the enemy. If that attack kills it, defeat_enemy() advances the stage and sends in the next one.\nThe tests hold those rules in place. Three of them are about to change, so it helps to see them as they are now.\nEverything today builds on that.\nWhere does the gold go?\nDefeating an enemy should pay.\nBefore we can pay the player, the player needs somewhere to put the money.\nRight now, our Player has two fields, and neither of them is a wallet.\nAdd one:\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\ngold: u32,\n}\nRust immediately complains.\nEvery place we build a Player now has to say how much gold it starts with.\nThere are two of those: Game::new() and the attack_reduces_enemy_health test. (cargo run only finds the first one, because it does not compile the tests. cargo test finds both.)\nWe are about to change Player a few more times today, and we do not want to chase every field through every place a player gets built.\nEnemy::for_stage() already solves this exact problem for enemies.\nGive Player the same treatment:\nimpl Player {\nfn new() -> Player {\nPlayer {\nbase_damage: 5,\nbonus_damage: 2,\ngold: 0,\n}\n}\n}\nPut it right after the structs, next to impl Enemy.\nSame convention as Game::new(): new is not a Rust keyword, just the name Rust programmers use for \"make me a fresh one of these.\"\nThen use it in Game::new():\nplayer: Player::new(),\nand in the test:\nlet player = Player::new();\nNow there is exactly one place that knows what a new player looks like.\nWhen Player changes again, and it will, that is the only place we touch.\nWhile you are in the tests, a_new_game_starts_at_stage_one can check that the wallet starts empty:\nassert_eq!(game.player.gold, 0);\nRun cargo test.\nAll eight should still pass.\nIf you cargo run instead, Rust warns that gold is never read. A warning, not an error. The game is about to read it.\nHow much is a slime worth?\nHow much gold should a defeated enemy drop?\nThat is a game rule.\nGame rules get their own small function, like enemy_health_for_stage().\nGive this one the same treatment, right next to it:\nfn gold_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(3)\n}\nstage 1 → 5 gold\nstage 2 → 7 gold\nstage 3 → 9 gold\nTry it: press Run to see those three numbers, plus two more stages. The last line asks for the reward at the biggest stage a u32 can hold. Rewrite the body as stage * 2 + 3 and run it again. In this debug build, Rust stops with an \"attempt to multiply with overflow\" panic instead of printing a number.\nLive example · saturating or not\nfn gold_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(3)\n}\nfn main() {\nfor stage in 1..=5 {\nprintln!(\"stage {stage} -> {} gold\", gold_reward_for_stage(stage));\n}\nprintln!(\"stage {} -> {} gold\", u32::MAX, gold_reward_for_stage(u32::MAX));\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/).\nAs written, the last line prints 4294967295 gold: the biggest u32, because the arithmetic stops at the top instead of going past it.\nQuestion: Why does gold_reward_for_stage() use saturating_mul and saturating_add instead of * and +?\nThey make the reward bigger at high stages\nIf the result would be too big for a u32, they stop at the largest value instead of overflowing\nPlain * and + do not work on u32\nAnswer: 2. If the result would be too big for a u32, they stop at the largest value instead of overflowing\nExplanation: Plain * and + work, but an overflow panics in a debug build and wraps around in a release build. Saturating arithmetic gives the same safe answer in both.\nLater enemies are tougher.\nThey should also pay better.\nNow, where does that reward live?\nIt belongs to the enemy.\nA slime knows how much health it has.\nIt should also know what it is worth:\nstruct Enemy {\nname: String,\nhealth: u32,\ngold_reward: u32,\n}\nAnd Enemy::for_stage() fills it in:\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\ngold_reward: gold_reward_for_stage(stage),\n}\n}\n}\nBecause every enemy is built through for_stage(), this is the only place that needed to change.\nNow run cargo test again.\ncargo run would build, with one more never-read warning, because the game only builds enemies through for_stage().\nThe tests do not build. attack_reduces_enemy_health builds an Enemy by hand, with a name and 10 health, and Rust wants a gold_reward there too.\nThat is the compiler finding the last place for us.\nInstead of making that test learn about rewards, let it start from a real enemy and adjust the one thing it cares about:\nlet mut enemy = Enemy::for_stage(1);\nenemy.health = 10;\nThe test still checks what it always checked: an attack takes health off the enemy.\nPaying out\nThe player has a wallet. The enemy has a price on its head.\nHanding one to the other is the player's job.\nAdd a method to impl Player, under new():\nfn add_gold(&mut self, amount: u32) {\nself.gold = self.gold.saturating_add(amount);\n}\n&mut self is the same &mut self from click().\nThe method borrows the player, changes it, and gives it back.\nWhich brings us to defeat_enemy(), the one place in the game that already knows an enemy just died.\nIt grows:\nfn defeat_enemy(&mut self) {\nself.player.add_gold(self.enemy.gold_reward);\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\nThe order matters.\nWe pay the player before the enemy is replaced.\nOn the next line, self.enemy becomes a brand new slime with a brand new reward.\nIf we paid afterward, the player would get paid for an enemy they have not fought yet.\nQuestion: In defeat_enemy(), why does add_gold() come before self.enemy = Enemy::for_stage(self.stage)?\nRust requires method calls to come before assignments\nAfter the assignment, self.enemy.gold_reward belongs to the next enemy, not the one just defeated\nadd_gold() would not compile after the enemy is replaced\nAnswer: 2. After the assignment, self.enemy.gold_reward belongs to the next enemy, not the one just defeated\nExplanation: The code would still compile. It would just pay the wrong reward, which is why the order is a game rule and not something Rust can catch.\nRead defeat_enemy() out loud now:\nPay the player. Go to the next stage. Bring in the next enemy.\nNo arithmetic to decode.\nExperience and levels\nGold is something to spend.\nExperience, or XP, is something to keep.\nEvery defeated enemy should also give the player experience, and when they have enough, they level up.\nThat is two rules, so we need two numbers:\nhow much XP does an enemy give?\nhow much XP does the next level take?\nThe first one looks like the gold rule.\nPut it right under gold_reward_for_stage():\nfn xp_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(4)\n}\nThe second one is new.\nEach level should take a little more than the last:\nfn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\nlevel 1 → 20 XP to reach level 2\nlevel 2 → 35 XP to reach level 3\nlevel 3 → 50 XP to reach level 4\nThat chain is split across lines so each step is easy to read. A line starting with . just continues the line above it.\nLevel 1 is the starting line. The saturating_sub(1) makes it count as zero steps taken, so level 1 needs the plain 20 and every level after that adds 15. Without it, the first level would already cost 35.\nThe exact numbers do not matter yet.\nThey have names and they will have tests.\nThe enemy carries its XP reward the same way it carries gold:\nstruct Enemy {\nname: String,\nhealth: u32,\ngold_reward: u32,\nxp_reward: u32,\n}\nAdd it to Enemy::for_stage(), under gold_reward:\nxp_reward: xp_reward_for_stage(stage),\nAnd the player needs two more fields: how much XP they have, and what level they are:\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\ngold: u32,\nxp: u32,\nlevel: u32,\n}\nIn Player::new():\nxp: 0,\nlevel: 1,\nThat is now the normal place a fresh player is built, so future changes stay concentrated there.\nRun cargo test. Still eight, plus a few warnings that the new fields and xp_to_next_level() are never used. The next section uses them.\nLeveling up\nNow the interesting part.\nAdding XP is not like adding gold.\nGold just piles up.\nXP piles up until it crosses a line, and then something happens:\nfn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= xp_to_next_level(self.level) {\nself.xp -= xp_to_next_level(self.level);\nself.level = self.level.saturating_add(1);\n}\n}\nAdd it to impl Player, right under add_gold().\nWalk through it with a level 1 player who has 0 XP and just earned 25.\nThe XP becomes 25.\nLevel 1 needs 20. We have 25. That is enough, so we go into the loop.\nSpend 20 of it. XP is now 5. Level is now 2.\nLevel 2 needs 35. We have 5. The loop stops.\nThe player is level 2 with 5 XP toward the next one.\nWhy a while and not an if?\nBecause a reward might be big enough to cross two lines at once.\nGive that same level 1 player 60 XP.\nSpend 20 → level 2, 40 left.\nLevel 2 needs 35. 40 is enough. Spend 35 → level 3, 5 left.\nLevel 3 needs 50. The loop stops.\nAn if would have stopped at level 2 and left 40 XP sitting there, more than enough for another level the player never got.\nA while keeps going until the XP really is below the line.\nTry it: press Run and the 60 XP walkthrough plays out as level 3 with 5 XP left over. Then change while to if and run it again. The output becomes level 2 with 40 XP left over, which is the stuck player from above.\nLive example · swap the while for an if\nstruct Player {\nxp: u32,\nlevel: u32,\n}\nfn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\nimpl Player {\nfn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= xp_to_next_level(self.level) {\nself.xp -= xp_to_next_level(self.level);\nself.level = self.level.saturating_add(1);\n}\n}\n}\nfn main() {\nlet mut player = Player { xp: 0, level: 1 };\nplayer.add_xp(60);\nprintln!(\"level {} with {} XP left over\", player.level, player.xp);\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/).\nQuestion: A level 1 player earns 60 XP at once. What does if instead of while leave behind?\nA player at level 3 with 5 XP\nA player at level 2 holding 40 XP, enough for another level they never got\nA compile error, because if cannot be used there\nAnswer: 2. A player at level 2 holding 40 XP, enough for another level they never got\nExplanation: if checks the line once. while keeps checking until the XP really is below it.\nYou might notice that xp_to_next_level(self.level) gets called twice in a row. That is fine. It is a tiny calculation, and for the same level it always gives the same answer.\nThat matters more than it looks right now. Later in this series, a player is going to come back after a few hours away and collect a very large pile of XP all at once.\nA plain minus sign\nLook at this line:\nself.xp -= xp_to_next_level(self.level);\n-= is shorthand: self.xp -= 20 means self.xp = self.xp - 20.\nThat is ordinary subtraction, not saturating_sub().\nEverywhere else in this program, subtraction goes through saturating_sub().\nHere, it is safe, and the line right above it is the reason.\nThe while only lets us in when self.xp is at least as big as the amount we are about to subtract.\nThe result can never drop below zero.\nsaturating_sub() is for when a number might go somewhere it should not.\nWhen the code right next to it proves that it cannot, plain subtraction says exactly what it means.\nQuestion: Why is the plain -= in add_xp() safe?\n-= quietly stops at zero, the same as saturating_sub()\nThe while condition only lets us in when self.xp is at least the amount being subtracted\nxp is a u32, and u32 can never go below zero\nAnswer: 2. The while condition only lets us in when self.xp is at least the amount being subtracted\nExplanation: A u32 cannot hold a negative number, so a plain subtraction that went below zero would be a bug, not a clamp. Here the loop condition rules that out.\nNow defeat_enemy() pays both:\nself.player.add_gold(self.enemy.gold_reward);\nself.player.add_xp(self.enemy.xp_reward);\nWhat does a level actually do?\nRight now, nothing.\nThe player levels up, and hits exactly as hard as before.\nA level should make the player stronger.\nLook at Player again:\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\ngold: u32,\nxp: u32,\nlevel: u32,\n}\nbase_damage is a number we wrote down once, when the player was created, and never touched again.\nBut base damage should not be a fact about the player anymore.\nIt should be a consequence of the player's level.\nThat makes it a rule. Put it under xp_to_next_level():\nfn base_damage_for_level(level: u32) -> u32 {\nlevel.saturating_add(4)\n}\nlevel 1 → 5 damage\nlevel 2 → 6 damage\nlevel 3 → 7 damage\nLevel 1 gives 5, which is exactly the base_damage we have had since the beginning.\nNothing about the first fight changes.\nNow the field can go.\nRemove base_damage from the struct and from Player::new().\nRust will complain about one more place that still uses it. We will get to that in a second.\nIn its place, give Player a method that asks the rule, right under add_xp():\nfn base_damage(&self) -> u32 {\nbase_damage_for_level(self.level)\n}\nThere is something new in that signature.\n&self, without the mut.\nSo far, our methods have needed &mut self because they changed something.\nA method that only needs to read takes &self instead. This is our first one.\nbase_damage() looks at the player's level and reports a number.\nIt does not change anything, so it does not ask for permission to.\nQuestion: Why does base_damage() take &self and add_xp() take &mut self?\n&self methods run faster\nadd_xp() changes the player, and base_damage() only reads it\nOnly the first method in an impl block may change the player\nAnswer: 2. add_xp() changes the player, and base_damage() only reads it\nExplanation: &mut self is the borrow that allows changes. A method that only reports a value asks for the smaller &self.\nOne thing still uses the old field: attack().\nIt reads player.base_damage, and that field no longer exists.\nRust says so (the line is wrapped here to fit):\nerror[E0615]: attempted to take value of method\n`base_damage` on type `&Player`\nand even suggests the fix: use parentheses to call the method.\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage(), player.bonus_damage);\nenemy.health = health_after_hit(enemy.health, damage);\n}\nThe parentheses are the only difference.\nplayer.base_damage was a number stored in the player.\nplayer.base_damage() is a number the player works out from its level.\nattack() did not have to learn anything about levels.\nIt asked for base damage. It got base damage.\nRun cargo test. All eight still pass, and that is the point: a level 1 player still deals 5 base damage, so nothing a test checks has changed.\nmain() shows the progress\nOur main() has been printing the enemy's health after every click.\nThat was useful when a fight took three clicks. Now there are rewards to watch, so let us print when something happens instead:\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 8 {\nlet stage = game.stage;\nlet level = game.player.level;\ngame.click();\nif game.stage > stage {\nprintln!(\n\"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\ngame.player.gold,\ngame.player.xp,\nxp_to_next_level(game.player.level)\n);\nif game.player.level > level {\nprintln!(\n\"Level up! Now level {}. Click damage: {}\",\ngame.player.level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage)\n);\n}\nprintln!(\n\"  Next up: {} with {} health.\",\ngame.enemy.name, game.enemy.health\n);\n}\n}\n}\nBefore each click, main() remembers the stage and the level.\nAfter the click, it compares.\nStage went up? The enemy died, so say so, show the wallet, and introduce the next one.\nLevel went up? Say that too.\nNotice that the level check sits inside the stage check. It can, because XP is only ever handed out in defeat_enemy(). No enemy died, no XP, no level up.\nOne small thing about that first println!. {stage} works because stage is a plain variable. game.player.gold has dots in it, so it goes in the list after the string and a bare {} picks it up.\nmain() still does not know how leveling works.\nIt does borrow one rule, xp_to_next_level(), so it can show how far away the next level is. That is display, not a decision.\nIt clicks, it asks, and it prints what it was told.\nRun it:\ncargo run\nStage 1 cleared!  Gold: 5  XP: 6/20\nNext up: Training Slime with 30 health.\nStage 2 cleared!  Gold: 12  XP: 14/20\nNext up: Training Slime with 40 health.\nStage 3 cleared!  Gold: 21  XP: 4/35\nLevel up! Now level 2. Click damage: 8\nNext up: Training Slime with 50 health.\nStage 4 cleared!  Gold: 32  XP: 16/35\nNext up: Training Slime with 60 health.\nStage 5 cleared!  Gold: 45  XP: 30/35\nNext up: Training Slime with 70 health.\nStage 6 cleared!  Gold: 60  XP: 11/50\nLevel up! Now level 3. Click damage: 9\nNext up: Training Slime with 80 health.\nStage 7 cleared!  Gold: 77  XP: 29/50\nNext up: Training Slime with 90 health.\nStage 8 cleared!  Gold: 96  XP: 49/50\nNext up: Training Slime with 100 health.\nRead that top to bottom.\nGold climbs. XP climbs toward a line, crosses it, and resets with the leftover carried over. Click damage starts at 7 and climbs to 8, then 9.\nCompare that to where we started this article: 5 base damage plus 2 bonus, on stage 1 and on stage 30 alike.\nThe slimes still get tougher every stage. The difference is that now, so does the player.\nTesting the rules\nEach new rule gets a test for its numbers. Add these to the tests module:\n#[test]\nfn later_stages_pay_better() {\nassert_eq!(gold_reward_for_stage(1), 5);\nassert_eq!(gold_reward_for_stage(2), 7);\nassert_eq!(xp_reward_for_stage(1), 6);\nassert_eq!(xp_reward_for_stage(2), 8);\n}\n#[test]\nfn levels_need_more_xp_as_they_go() {\nassert_eq!(xp_to_next_level(1), 20);\nassert_eq!(xp_to_next_level(2), 35);\n}\n#[test]\nfn levels_make_the_player_hit_harder() {\nassert_eq!(base_damage_for_level(1), 5);\nassert_eq!(base_damage_for_level(2), 6);\n}\nThen the behaviors.\nThe two walkthroughs from add_xp() become tests, so the explanation and the code can never drift apart:\n#[test]\nfn leveling_up_keeps_leftover_xp() {\nlet mut player = Player::new();\nplayer.add_xp(25);\nassert_eq!(player.level, 2);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn a_big_reward_can_skip_levels() {\nlet mut player = Player::new();\nplayer.add_xp(60);\nassert_eq!(player.level, 3);\nassert_eq!(player.xp, 5);\n}\nAnd the big one.\nThe last test in our starting point, defeating_an_enemy_advances_the_stage, checked that a win moved the game forward.\nNow a win should also pay.\nRename it and add two checks at the end:\n#[test]\nfn defeating_an_enemy_pays_the_player() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\nThis test does not care how damage is calculated, or how XP crosses a threshold.\nThose rules have their own tests now.\nIt asks the bigger question:\nWhen a click kills something, does the player get paid for it?\nRun them:\ncargo test\nAll thirteen should pass.\nThe complete program\nIf you are only reading along, skip to the last two sections.\nIf you have been following along in your editor, here is everything in one place.\nAt this point, src/main.rs should look like this:\nstruct Player {\nbonus_damage: u32,\ngold: u32,\nxp: u32,\nlevel: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\ngold_reward: u32,\nxp_reward: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\nimpl Player {\nfn new() -> Player {\nPlayer {\nbonus_damage: 2,\ngold: 0,\nxp: 0,\nlevel: 1,\n}\n}\nfn add_gold(&mut self, amount: u32) {\nself.gold = self.gold.saturating_add(amount);\n}\nfn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= xp_to_next_level(self.level) {\nself.xp -= xp_to_next_level(self.level);\nself.level = self.level.saturating_add(1);\n}\n}\nfn base_damage(&self) -> u32 {\nbase_damage_for_level(self.level)\n}\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\ngold_reward: gold_reward_for_stage(stage),\nxp_reward: xp_reward_for_stage(stage),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player::new(),\nenemy: Enemy::for_stage(1),\n}\n}\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.player.add_gold(self.enemy.gold_reward);\nself.player.add_xp(self.enemy.xp_reward);\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn gold_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(3)\n}\nfn xp_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(4)\n}\nfn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\nfn base_damage_for_level(level: u32) -> u32 {\nlevel.saturating_add(4)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage(), player.bonus_damage);\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 8 {\nlet stage = game.stage;\nlet level = game.player.level;\ngame.click();\nif game.stage > stage {\nprintln!(\n\"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\ngame.player.gold,\ngame.player.xp,\nxp_to_next_level(game.player.level)\n);\nif game.player.level > level {\nprintln!(\n\"Level up! Now level {}. Click damage: {}\",\ngame.player.level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage)\n);\n}\nprintln!(\n\"  Next up: {} with {} health.\",\ngame.enemy.name, game.enemy.health\n);\n}\n}\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player::new();\nlet mut enemy = Enemy::for_stage(1);\nenemy.health = 10;\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 3);\n}\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\n#[test]\nfn later_stages_pay_better() {\nassert_eq!(gold_reward_for_stage(1), 5);\nassert_eq!(gold_reward_for_stage(2), 7);\nassert_eq!(xp_reward_for_stage(1), 6);\nassert_eq!(xp_reward_for_stage(2), 8);\n}\n#[test]\nfn levels_need_more_xp_as_they_go() {\nassert_eq!(xp_to_next_level(1), 20);\nassert_eq!(xp_to_next_level(2), 35);\n}\n#[test]\nfn leveling_up_keeps_leftover_xp() {\nlet mut player = Player::new();\nplayer.add_xp(25);\nassert_eq!(player.level, 2);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn a_big_reward_can_skip_levels() {\nlet mut player = Player::new();\nplayer.add_xp(60);\nassert_eq!(player.level, 3);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn levels_make_the_player_hit_harder() {\nassert_eq!(base_damage_for_level(1), 5);\nassert_eq!(base_damage_for_level(2), 6);\n}\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\nassert_eq!(game.player.gold, 0);\n}\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 13);\n}\n#[test]\nfn defeating_an_enemy_pays_the_player() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n}\nRun both:\ncargo run\ncargo test\nThe game should clear eight stages, and all thirteen tests should pass.\nWant to practice the while loop on its own? Here is a small one to finish.\nCoding challenge: total XP to reach a level\nWrite total_xp_to_reach(level): the XP a player has spent in total by the time they arrive at level. Level 1 is where everyone starts, so it costs nothing. Keep the name total_xp_to_reach and its level argument, because the checks call it. Press Check my code and it will tell you how you are doing.\nExpected result: it prints 0, 20, 55 and 105 for levels 1 to 4. Right now it prints 0 four times.\nCoding challenge · total XP to reach a level\nfn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\nfn total_xp_to_reach(level: u32) -> u32 {\n// TODO: add up xp_to_next_level for level 1, 2, ... up to (not including) `level`\n0\n}\nfn main() {\nfor level in 1..=4 {\nprintln!(\"level {level}: {} total XP\", total_xp_to_reach(level));\n}\n}\n<details>\n<summary>Spoiler: the checks for this challenge</summary>\nThese tests run when you press Check my code on the original article. Reading them first gives away the answer.\n/// Level 1 costs nothing, and arriving at level 2 costs what level 1 asks for. Does the loop stop before `level`?\n#[test]\nfn first_steps() {\nassert_eq!(total_xp_to_reach(1), 0);\nassert_eq!(total_xp_to_reach(2), 20);\n}\n/// Every level on the way has to be added, one at a time. The total grows with each step.\n#[test]\nfn adds_every_step() {\nassert_eq!(total_xp_to_reach(3), 55);\nassert_eq!(total_xp_to_reach(4), 105);\n}\n/// A huge level should not panic or wrap around. The hint names a method that stops at the ceiling.\n#[test]\nfn stops_at_the_ceiling() {\nassert_eq!(total_xp_to_reach(100_000), u32::MAX);\n}\n</details>\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/).\nTip:\nKeep a running total (start it as let mut total: u32 = 0;) and a current level that starts at 1. While current is below level, add xp_to_next_level(current) to the total with saturating_add, then move current up by one.\nThe challenge is its own small program, so your src/main.rs stays exactly as it is above.\nWhat we learned\nAlong the way, we encountered:\nPlayer::new(), so a player is built in one place\n&self methods that read without changing\na while loop that keeps going until a condition stops being true\n-=, and plain subtraction where the code above it proves the number cannot go below zero\nrules that calculate a value instead of storing it\nThat is a short list, and the last item is the one worth keeping.\nbase_damage used to be a number sitting in the player.\nNow it is a rule applied to the player's level.\nThe player got smaller, and the game got bigger.\nHold onto that idea. We are going to do it again next time, to the other half of that damage calculation.\nGold with nothing to buy\nOur player finally gets stronger by winning.\nThere is just one problem.\nLook at the last line of that run again:\nStage 8 cleared!  Gold: 96  XP: 49/50\nNinety-six gold.\nWe call it money, but there is nothing in this game to spend it on.\ngold earned: 96\ngold spent:  0\nRight now it is not a reward.\nIt is a high score.\nNext time, we are going to give the player something to buy, and discover that the obvious way to report whether a purchase worked is not good enough.\nSmall function. Bigger game.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2026-10-19T11:03:11Z",
      "id": "https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/",
      "image": "https://mnixblog.netlify.app/images/add-gold-xp-and-levels-in-rust/cover.jpg",
      "summary": "Learn Rust by making winning pay. Add gold, XP, and levels to an idle game, level up with a while loop, and calculate damage with a &self method.",
      "tags": [
        "Rust",
        "WebAssembly",
        "Game Development",
        "Beginners"
      ],
      "title": "Winning Is Not Progress.",
      "url": "https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/"
    },
    {
      "content_html": "<p>Every fight in our game ends the same way.</p>\n<p>The slime dies.</p>\n<p>Then the program dies with it.</p>\n<p>So far, winning has been how our game ends.</p>\n<p>In an idle game, winning is how the game keeps going.</p>\n<p>One enemy falls, and the next one walks in.</p>\n<p>In this article, we are going to give our game a next stage, a tougher enemy waiting there, and one place to keep all of it.</p>\n<p>By the end, a whole turn of our game will fit in one line:</p>\n<pre><code class=\"language-rust\">game.click();\n</code></pre>\n<p>That one call will attack the enemy, notice when it dies, advance the stage, and send in the next one.</p>\n<hr />\n<h2 id=\"our-starting-point\">Our starting point<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#our-starting-point\" aria-label=\"Link to this section\">#</a></h2>\n<p>So far, we have a player, a slime, and a way for one to hit the other.</p>\n<p>You need two structs and four small functions to follow along:</p>\n<pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(\n    health: u32,\n    damage: u32,\n) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn attack(\n    player: &amp;Player,\n    enemy: &amp;mut Enemy,\n) {\n    let damage = click_damage(\n        player.base_damage,\n        player.bonus_damage,\n    );\n\n    enemy.health =\n        health_after_hit(enemy.health, damage);\n}\n</code></pre>\n<p><code>attack()</code> borrows the player to read it and the enemy to change it.</p>\n<p>Our combat loop looks like this:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let mut enemy = Enemy {\n        name: String::from(\"Training Slime\"),\n        health: 20,\n    };\n\n    while !is_defeated(enemy.health) {\n        attack(&amp;player, &amp;mut enemy);\n\n        println!(\n            \"{} health: {}\",\n            enemy.name,\n            enemy.health\n        );\n    }\n\n    println!(\"{} defeated!\", enemy.name);\n}\n</code></pre>\n<p>Run it and the Training Slime goes down in three hits.</p>\n<p>Then the program is over.</p>\n<hr />\n<h2 id=\"the-next-enemy-needs-to-be-tougher\">The next enemy needs to be tougher<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#the-next-enemy-needs-to-be-tougher\" aria-label=\"Link to this section\">#</a></h2>\n<p>Before we worry about where the next enemy comes from, we need to decide what it looks like.</p>\n<p>If every enemy had 20 health, stage 50 would feel exactly like stage 1.</p>\n<p>The next enemy should be tougher.</p>\n<p>That is a game rule, and we know what to do with game rules by now.</p>\n<p>Give it a small function:</p>\n<pre><code class=\"language-rust\">fn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n</code></pre>\n<p>That gives us:</p>\n<pre><code class=\"language-text\">stage 1 → 20 health\nstage 2 → 30 health\nstage 3 → 40 health\n</code></pre>\n<p><code>saturating_mul()</code> is the multiplication version of the saturating arithmetic we already use.</p>\n<p>The exact balance does not matter yet.</p>\n<p>What matters is that the rule has a name and a test:</p>\n<pre><code class=\"language-rust\">#[test]\nfn later_stages_have_tougher_enemies() {\n    assert_eq!(enemy_health_for_stage(1), 20);\n    assert_eq!(enemy_health_for_stage(2), 30);\n}\n</code></pre>\n<p>Try it: change the numbers in the rule, or ask for a much later stage, and run it again.</p>\n<p>You should see 20, 30 and 40 health, then <code>Checks passed.</code></p>\n<p>The Playground only runs <code>#[test]</code> functions in its Test mode, so these checks use <code>assert_eq!</code> inside <code>main()</code> instead.</p>\n<p>If you change the formula, an <code>assert_eq!</code> fails and tells you which numbers disagreed.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try changing the formula</figcaption><pre><code class=\"language-rust\">fn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn main() {\n    println!(&quot;stage 1 -&gt; {} health&quot;, enemy_health_for_stage(1));\n    println!(&quot;stage 2 -&gt; {} health&quot;, enemy_health_for_stage(2));\n    println!(&quot;stage 3 -&gt; {} health&quot;, enemy_health_for_stage(3));\n\n    assert_eq!(enemy_health_for_stage(1), 20);\n    assert_eq!(enemy_health_for_stage(2), 30);\n    println!(&quot;Checks passed.&quot;);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/\">the original article</a>.</p></figure>\n<p>If Rust warns that the function is never used yet, that is fine.</p>\n<p>We are about to use it.</p>\n<hr />\n<h2 id=\"try-it-in-main-first\">Try it in <code>main()</code> first<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#try-it-in-main-first\" aria-label=\"Link to this section\">#</a></h2>\n<p>The quickest way to make stages work is to put everything straight into <code>main()</code>:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let mut stage = 1;\n\n    let mut enemy = Enemy {\n        name: String::from(\"Training Slime\"),\n        health: enemy_health_for_stage(stage),\n    };\n\n    while stage &lt;= 3 {\n        attack(&amp;player, &amp;mut enemy);\n\n        if is_defeated(enemy.health) {\n            println!(\"Stage {stage} cleared!\");\n\n            stage = stage.saturating_add(1);\n\n            enemy = Enemy {\n                name: String::from(\"Training Slime\"),\n                health: enemy_health_for_stage(stage),\n            };\n        } else {\n            println!(\n                \"{} health: {}\",\n                enemy.name,\n                enemy.health\n            );\n        }\n    }\n}\n</code></pre>\n<p>We stop after stage 3 so the terminal does not scroll forever.</p>\n<p>Run it.</p>\n<p>It works.</p>\n<p>The slime falls, a tougher one takes its place, and the stages keep coming.</p>\n<p>It is also a mess.</p>\n<p>Look at what <code>main()</code> is doing now:</p>\n<pre><code class=\"language-text\">owns the player\nowns the enemy\nowns the stage\ndecides when a stage is cleared\nbuilds the next enemy\nprints everything\n</code></pre>\n<p>Only that last job really belongs in <code>main()</code>.</p>\n<p>Everything else is the game.</p>\n<p>Soon, defeating an enemy will also hand out rewards.</p>\n<p>If all of those rules stay here, <code>main()</code> becomes the game engine and the terminal UI at the same time.</p>\n<p>There is another reason to fix that now.</p>\n<p>Later, the browser will replace <code>main()</code>.</p>\n<p>The game rules need somewhere else to live.</p>\n<hr />\n<h2 id=\"give-the-game-a-shape\">Give the game a shape<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#give-the-game-a-shape\" aria-label=\"Link to this section\">#</a></h2>\n<p>We have solved this kind of problem before.</p>\n<p>Three loose numbers were not a player, so we built a <code>Player</code>.</p>\n<p>A stage, a player, and an enemy sitting loose in <code>main()</code> are not a game.</p>\n<p>So we build a <code>Game</code>:</p>\n<pre><code class=\"language-rust\">struct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n</code></pre>\n<p>This struct contains other structs:</p>\n<pre><code class=\"language-text\">Game\n├── stage: u32\n├── player: Player\n└── enemy: Enemy\n</code></pre>\n<p>A struct can hold types we made ourselves.</p>\n<p>The <code>Game</code> owns its <code>Player</code>.</p>\n<p>It owns its <code>Enemy</code>.</p>\n<p>There is now one value we can point at and say:</p>\n<blockquote>\n<p>This is the current state of the game.</p>\n</blockquote>\n<hr />\n<h2 id=\"where-does-a-new-enemy-come-from\">Where does a new enemy come from?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#where-does-a-new-enemy-come-from\" aria-label=\"Link to this section\">#</a></h2>\n<p>We could create a game by spelling out every field manually:</p>\n<pre><code class=\"language-rust\">let mut game = Game {\n    stage: 1,\n    player: Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    },\n    enemy: Enemy {\n        name: String::from(\"Training Slime\"),\n        health: enemy_health_for_stage(1),\n    },\n};\n</code></pre>\n<p>That works.</p>\n<p>But we have already written this enemy more than once:</p>\n<pre><code class=\"language-rust\">Enemy {\n    name: String::from(\"Training Slime\"),\n    health: enemy_health_for_stage(stage),\n}\n</code></pre>\n<p>What we really want to say is:</p>\n<blockquote>\n<p>Give me the enemy for this stage.</p>\n</blockquote>\n<p>That sounds like a function.</p>\n<p>And because the function is specifically about creating an <code>Enemy</code>, it belongs with <code>Enemy</code>.</p>\n<hr />\n<h2 id=\"attach-functions-to-a-type-with-impl\">Attach functions to a type with <code>impl</code><a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#attach-functions-to-a-type-with-impl\" aria-label=\"Link to this section\">#</a></h2>\n<p>Rust lets us attach functions to a type with an <code>impl</code> block:</p>\n<pre><code class=\"language-rust\">impl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n        }\n    }\n}\n</code></pre>\n<p><code>impl Enemy</code> means:</p>\n<blockquote>\n<p>here are functions that belong to <code>Enemy</code></p>\n</blockquote>\n<p>Now we can write:</p>\n<pre><code class=\"language-rust\">Enemy::for_stage(1)\n</code></pre>\n<p>You have already seen this style of call:</p>\n<pre><code class=\"language-rust\">String::from(\"Training Slime\")\n</code></pre>\n<p>A function attached to a type that does not need an existing value is called an <strong>associated function</strong>.</p>\n<p><code>Enemy::for_stage()</code> does not replace our small health rule.</p>\n<p>It uses it:</p>\n<pre><code class=\"language-rust\">health: enemy_health_for_stage(stage),\n</code></pre>\n<p>One function decides how tough the enemy is.</p>\n<p>The other decides how to build one.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>What separates <code>Enemy::for_stage(2)</code> from a method like <code>game.click()</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>for_stage</code> needs no existing value to run, while <code>click</code> works on a specific <code>Game</code> through <code>self</code></li>\n<li><code>for_stage</code> is a method, and <code>click</code> is an associated function</li>\n<li><code>click</code> can only be called from inside <code>main()</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. <code>for_stage</code> needs no existing value to run, while <code>click</code> works on a specific <code>Game</code> through <code>self</code></p>\n<p>An associated function belongs to the type and is called with <code>::</code>. A method takes <code>self</code> and is called on a value with a dot.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"a-new-game\">A new game<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#a-new-game\" aria-label=\"Link to this section\">#</a></h2>\n<p><code>Game</code> gets an <code>impl</code> block too:</p>\n<pre><code class=\"language-rust\">impl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player {\n                base_damage: 5,\n                bonus_damage: 2,\n            },\n            enemy: Enemy::for_stage(1),\n        }\n    }\n}\n</code></pre>\n<p>There is nothing magical about <code>new</code>.</p>\n<p>Rust has no special constructor keyword.</p>\n<p><code>new</code> is simply a common name for an associated function that creates a fresh value.</p>\n<p>Starting the game is now:</p>\n<pre><code class=\"language-rust\">let mut game = Game::new();\n</code></pre>\n<p>Game setup has one home.</p>\n<p>And it is no longer <code>main()</code>.</p>\n<hr />\n<h2 id=\"one-click\">One click<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#one-click\" aria-label=\"Link to this section\">#</a></h2>\n<p>Now the part we have been building toward.</p>\n<p>What happens when the player clicks?</p>\n<p>Add this inside <code>impl Game</code>:</p>\n<pre><code class=\"language-rust\">fn click(&amp;mut self) {\n    attack(&amp;self.player, &amp;mut self.enemy);\n}\n</code></pre>\n<p>There is one new name in that signature:</p>\n<pre><code class=\"language-rust\">self\n</code></pre>\n<p><code>self</code> means:</p>\n<blockquote>\n<p>the value this method was called on</p>\n</blockquote>\n<p>When we write:</p>\n<pre><code class=\"language-rust\">game.click();\n</code></pre>\n<p>inside <code>click()</code>, <code>self</code> refers to <code>game</code>.</p>\n<p>That makes <code>click()</code> a <strong>method</strong>: a function attached to a type that works on a specific value of that type.</p>\n<p><code>Game::new()</code> does not take <code>self</code> because there is no game yet.</p>\n<p><code>click()</code> does because it works on a game that already exists.</p>\n<p>And <code>&amp;mut self</code> should look familiar.</p>\n<p>It means <code>click()</code> may temporarily change the game without taking ownership of it.</p>\n<p>That is why this needs:</p>\n<pre><code class=\"language-rust\">let mut game = Game::new();\n</code></pre>\n<p>Without <code>mut</code>, Rust refuses to let <code>click()</code> borrow the game mutably.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does <code>let game = Game::new(); game.click();</code> fail to compile?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>Game::new()</code> returns a borrowed value</li>\n<li><code>click</code> takes <code>&amp;mut self</code>, and <code>game</code> was not declared <code>mut</code></li>\n<li><code>click</code> takes ownership of <code>game</code>, so it can only be called once</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. <code>click</code> takes <code>&amp;mut self</code>, and <code>game</code> was not declared <code>mut</code></p>\n<p>The error is <code>E0596</code>: cannot borrow <code>game</code> as mutable, as it is not declared as mutable.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"two-borrows-one-game\">Two borrows, one game<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#two-borrows-one-game\" aria-label=\"Link to this section\">#</a></h2>\n<p>Look inside <code>click()</code>:</p>\n<pre><code class=\"language-rust\">attack(&amp;self.player, &amp;mut self.enemy);\n</code></pre>\n<p>Last time, we learned:</p>\n<blockquote>\n<p><strong>For the same data at the same time: many readers, or one writer, but not both.</strong></p>\n</blockquote>\n<p>So why can we borrow part of the game to read and another part to change?</p>\n<p>Because they are not the same data.</p>\n<p><code>self.player</code> and <code>self.enemy</code> are different fields.</p>\n<p>They do not overlap.</p>\n<p>Rust can see that reading the player does not interfere with changing the enemy.</p>\n<p>Our <code>attack()</code> did not need to change.</p>\n<p>The borrowing simply moved inside the game.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why can <code>click()</code> write <code>attack(&amp;self.player, &amp;mut self.enemy)</code> when the rule says many readers or one writer, never both?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Rust does not apply borrowing rules to <code>self</code></li>\n<li>The reader borrows <code>self.player</code> and the writer borrows <code>self.enemy</code>, different fields that do not overlap</li>\n<li>Any <code>&amp;</code> and <code>&amp;mut</code> are allowed together when they appear in the same call</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. The reader borrows <code>self.player</code> and the writer borrows <code>self.enemy</code>, different fields that do not overlap</p>\n<p>Borrowing <code>self.enemy</code> both ways at once, like <code>let p = &amp;self.enemy;</code> before the attack and using <code>p</code> after it, fails with <code>E0502</code>.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"what-happens-when-the-enemy-dies\">What happens when the enemy dies?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#what-happens-when-the-enemy-dies\" aria-label=\"Link to this section\">#</a></h2>\n<p>Right now, <code>click()</code> only attacks.</p>\n<p>When the enemy reaches zero health, the next click would attack a corpse.</p>\n<p>That is grim.</p>\n<p>After every attack, <code>click()</code> should check whether the enemy was defeated:</p>\n<pre><code class=\"language-rust\">fn click(&amp;mut self) {\n    attack(&amp;self.player, &amp;mut self.enemy);\n\n    if is_defeated(self.enemy.health) {\n        self.defeat_enemy();\n    }\n}\n</code></pre>\n<p>Now we need <code>defeat_enemy()</code>:</p>\n<pre><code class=\"language-rust\">fn defeat_enemy(&amp;mut self) {\n    self.stage = self.stage.saturating_add(1);\n    self.enemy = Enemy::for_stage(self.stage);\n}\n</code></pre>\n<p>The stage goes up.</p>\n<p>The defeated enemy is replaced with the enemy for the new stage.</p>\n<p>We could put both lines directly inside <code>click()</code>.</p>\n<p>But defeating an enemy is its own event, and that event is going to grow.</p>\n<p>Soon it will also hand out rewards.</p>\n<p>Keeping it named means <code>click()</code> can stay readable:</p>\n<blockquote>\n<p>Attack the enemy. If it went down, handle the defeat.</p>\n</blockquote>\n<p>And <code>defeat_enemy()</code> says:</p>\n<blockquote>\n<p>Go to the next stage. Bring in the next enemy.</p>\n</blockquote>\n<p>Try it: here is the game so far, clicked three times.</p>\n<p>You should see <code>Stage 1: Training Slime health: 13</code>, then <code>Stage 1: Training Slime health: 6</code>, then <code>Stage 2: Training Slime health: 30</code>.</p>\n<p>Then try two experiments:</p>\n<ul>\n<li>delete the <code>if</code> block in <code>click()</code> and the third click leaves the slime at <code>health: 0</code> on stage 1, because nothing handles the defeat</li>\n<li>put the <code>if</code> block back and remove <code>mut</code> from <code>let mut game</code>, and Rust refuses with <code>E0596</code></li>\n</ul>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try removing the defeat check</figcaption><pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(&quot;Training Slime&quot;),\n            health: enemy_health_for_stage(stage),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player {\n                base_damage: 5,\n                bonus_damage: 2,\n            },\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage, player.bonus_damage);\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let mut game = Game::new();\n\n    game.click();\n    println!(&quot;Stage {}: {} health: {}&quot;, game.stage, game.enemy.name, game.enemy.health);\n\n    game.click();\n    println!(&quot;Stage {}: {} health: {}&quot;, game.stage, game.enemy.name, game.enemy.health);\n\n    game.click();\n    println!(&quot;Stage {}: {} health: {}&quot;, game.stage, game.enemy.name, game.enemy.health);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/\">the original article</a>.</p></figure>\n<hr />\n<h2 id=\"main-gets-boring\"><code>main()</code> gets boring<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#main-gets-boring\" aria-label=\"Link to this section\">#</a></h2>\n<p>Here is the new <code>main()</code>:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 3 {\n        let stage = game.stage;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\"Stage {stage} cleared!\");\n        } else {\n            println!(\n                \"{} health: {}\",\n                game.enemy.name,\n                game.enemy.health\n            );\n        }\n    }\n}\n</code></pre>\n<p><code>main()</code> still reads the game so it can decide what to print.</p>\n<p>But it does not:</p>\n<ul>\n<li>build enemies</li>\n<li>calculate their health</li>\n<li>decide when a stage advances</li>\n<li>handle a defeat</li>\n</ul>\n<p>It creates a game, clicks, and prints.</p>\n<p>Boring is exactly what we want.</p>\n<p>Run:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>You should see:</p>\n<pre><code class=\"language-text\">Training Slime health: 13\nTraining Slime health: 6\nStage 1 cleared!\nTraining Slime health: 23\nTraining Slime health: 16\nTraining Slime health: 9\nTraining Slime health: 2\nStage 2 cleared!\nTraining Slime health: 33\nTraining Slime health: 26\nTraining Slime health: 19\nTraining Slime health: 12\nTraining Slime health: 5\nStage 3 cleared!\n</code></pre>\n<p>Three stages.</p>\n<p>Each slime tougher than the last.</p>\n<p>And the game, not <code>main()</code>, decided all of that.</p>\n<hr />\n<h2 id=\"test-what-a-player-experiences\">Test what a player experiences<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#test-what-a-player-experiences\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our first tests check individual rules.</p>\n<p><code>Game</code> lets us start testing something larger: what happens when those rules work together.</p>\n<p>A new game should start at stage 1:</p>\n<pre><code class=\"language-rust\">#[test]\nfn a_new_game_starts_at_stage_one() {\n    let game = Game::new();\n\n    assert_eq!(game.stage, 1);\n    assert_eq!(game.enemy.health, 20);\n}\n</code></pre>\n<p>A click should hurt the enemy:</p>\n<pre><code class=\"language-rust\">#[test]\nfn clicking_damages_the_enemy() {\n    let mut game = Game::new();\n\n    game.click();\n\n    assert_eq!(game.enemy.health, 13);\n}\n</code></pre>\n<p>And defeating an enemy should move the game forward:</p>\n<pre><code class=\"language-rust\">#[test]\nfn defeating_an_enemy_advances_the_stage() {\n    let mut game = Game::new();\n    game.enemy.health = 1;\n\n    game.click();\n\n    assert_eq!(game.stage, 2);\n    assert_eq!(game.enemy.health, 30);\n}\n</code></pre>\n<p>That last test does not care how damage is calculated.</p>\n<p>Those smaller rules already have their own tests.</p>\n<p>It asks a larger question:</p>\n<blockquote>\n<p>When a click defeats an enemy, does the game move on?</p>\n</blockquote>\n<p>Run:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>All eight should pass.</p>\n<p><strong>Coding challenge: Click many times</strong></p>\n<p>Add a method to <code>Game</code> called <code>click_many</code> that calls <code>self.click()</code> once for every click requested. Keep the name <code>click_many</code> and its <code>clicks</code> argument, because the checks call it. Press <strong>Check my code</strong> and it will tell you how you are doing.</p>\n<p><strong>Expected result:</strong> the starter prints <code>Stage 1 - Training Slime health: 20</code>, and your finished version prints <code>Stage 2 - Training Slime health: 16</code>.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Finish click_many</figcaption><pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(&quot;Training Slime&quot;),\n            health: enemy_health_for_stage(stage),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player {\n                base_damage: 5,\n                bonus_damage: 2,\n            },\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    // TODO: call self.click() once for every click requested\n    fn click_many(&amp;mut self, clicks: u32) {}\n\n    fn defeat_enemy(&amp;mut self) {\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage, player.bonus_damage);\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let mut game = Game::new();\n\n    game.click_many(5);\n\n    println!(&quot;Stage {} - {} health: {}&quot;, game.stage, game.enemy.name, game.enemy.health);\n}\n</code></pre><details class=\"ob-spoiler\"><summary>Spoiler: the checks for this challenge</summary><p>These tests run when you press Check my code on the original article. Reading them first gives away the answer.</p><pre><code class=\"language-rust\">/// One click should be exactly one hit. Has click_many touched the enemy yet?\n#[test]\nfn one_click_is_one_hit() {\n    let mut game = Game::new();\n\n    game.click_many(1);\n\n    assert_eq!(game.enemy.health, 13);\n    assert_eq!(game.stage, 1);\n}\n\n/// Count your clicks. Ask for two and the slime should feel two hits, no more and no fewer.\n#[test]\nfn every_click_lands() {\n    let mut game = Game::new();\n\n    game.click_many(2);\n\n    assert_eq!(game.enemy.health, 6);\n}\n\n/// The slime should still fall mid-loop and the next one oozes in. Is each click going through self.click()?\n#[test]\nfn defeat_still_happens() {\n    let mut game = Game::new();\n\n    game.click_many(3);\n\n    assert_eq!(game.stage, 2);\n    assert_eq!(game.enemy.health, enemy_health_for_stage(2));\n}\n</code></pre></details><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>Keep a counter that starts at 0, and use <code>while done &lt; clicks</code> to repeat. Add 1 to the counter with <code>saturating_add(1)</code> each time around.</p>\n</aside>\n<hr />\n<h2 id=\"the-complete-program\">The complete program<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#the-complete-program\" aria-label=\"Link to this section\">#</a></h2>\n<p>If you are only reading along, you can skip to the last two sections.</p>\n<p>If you are coding along, here is everything in one place:</p>\n<pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player {\n                base_damage: 5,\n                bonus_damage: 2,\n            },\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(\n    health: u32,\n    damage: u32,\n) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn attack(\n    player: &amp;Player,\n    enemy: &amp;mut Enemy,\n) {\n    let damage = click_damage(\n        player.base_damage,\n        player.bonus_damage,\n    );\n\n    enemy.health =\n        health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 3 {\n        let stage = game.stage;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\"Stage {stage} cleared!\");\n        } else {\n            println!(\n                \"{} health: {}\",\n                game.enemy.name,\n                game.enemy.health\n            );\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(\n            health_after_hit(3, 10),\n            0\n        );\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player {\n            base_damage: 5,\n            bonus_damage: 2,\n        };\n\n        let mut enemy = Enemy {\n            name: String::from(\"Training Slime\"),\n            health: 10,\n        };\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 3);\n    }\n\n    #[test]\n    fn later_stages_have_tougher_enemies() {\n        assert_eq!(enemy_health_for_stage(1), 20);\n        assert_eq!(enemy_health_for_stage(2), 30);\n    }\n\n    #[test]\n    fn a_new_game_starts_at_stage_one() {\n        let game = Game::new();\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n    }\n\n    #[test]\n    fn clicking_damages_the_enemy() {\n        let mut game = Game::new();\n\n        game.click();\n\n        assert_eq!(game.enemy.health, 13);\n    }\n\n    #[test]\n    fn defeating_an_enemy_advances_the_stage() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n    }\n}\n</code></pre>\n<p>Run both:</p>\n<pre><code class=\"language-bash\">cargo run\ncargo test\n</code></pre>\n<p>The game should clear three stages, and all eight tests should pass.</p>\n<hr />\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Along the way, we encountered:</p>\n<ul>\n<li>structs that own other structs</li>\n<li><code>impl</code> blocks</li>\n<li>associated functions like <code>Enemy::for_stage()</code></li>\n<li><code>new()</code> as a convention, not a keyword</li>\n<li>methods</li>\n<li><code>self</code> and <code>&amp;mut self</code></li>\n<li>borrowing different fields at the same time</li>\n<li><code>saturating_mul()</code></li>\n<li>tests that describe larger game behavior</li>\n</ul>\n<p>You do not need to memorize all of that yet.</p>\n<p>The important change is where the game lives.</p>\n<p>We started with a <code>main()</code> that owned everything and decided everything.</p>\n<p>We ended with:</p>\n<pre><code class=\"language-rust\">let mut game = Game::new();\n\ngame.click();\n</code></pre>\n<p>One click is now one full turn:</p>\n<pre><code class=\"language-text\">game.click()\n    ↓\nattack(&amp;self.player, &amp;mut self.enemy)\n    ↓\nclick_damage()\n    ↓\nhealth_after_hit()\n    ↓\nis_defeated()?\n    ↓\ndefeat_enemy()\n    ↓\nnext stage + Enemy::for_stage()\n</code></pre>\n<p>Look at the bottom of that chain.</p>\n<p><code>click_damage()</code>, <code>health_after_hit()</code>, and <code>is_defeated()</code> are the same small functions we started the series with.</p>\n<p>They have not changed.</p>\n<p>They are simply a few layers deeper now, inside something that finally looks like a game.</p>\n<hr />\n<h2 id=\"winning-does-not-pay-yet\">Winning does not pay yet<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#winning-does-not-pay-yet\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our game has a real loop now.</p>\n<p>Click. The slime falls. The next one walks in tougher than the last.</p>\n<p>But look at what the player gets for all that clicking:</p>\n<pre><code class=\"language-text\">gold:       nothing\nexperience: nothing\nlevels:     nothing\n</code></pre>\n<p>We just cleared three stages.</p>\n<p>Our player is exactly as strong as when the game started.</p>\n<p>Every slime gets tougher.</p>\n<p>Our player never does.</p>\n<p>Keep going long enough, and every new slime takes more clicks than the last one.</p>\n<p>Forever.</p>\n<p>That is not progression.</p>\n<p>That is a treadmill.</p>\n<p>An idle game needs a reason to keep clicking: rewards for winning, levels to reach, and something to spend those rewards on.</p>\n<p>And this time, we already know where those rewards will be handed out:</p>\n<pre><code class=\"language-rust\">fn defeat_enemy(&amp;mut self)\n</code></pre>\n<p>Next, we will make winning worth something.</p>\n<p><strong>Small function. Bigger game.</strong></p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "One Fight Is Not a Game.\nCanonical: https://mnixblog.netlify.app/rust-game-state-impl-methods/\nPut Your Rust Game in One Place With impl and Methods\nEvery fight in our game ends the same way.\nThe slime dies.\nThen the program dies with it.\nSo far, winning has been how our game ends.\nIn an idle game, winning is how the game keeps going.\nOne enemy falls, and the next one walks in.\nIn this article, we are going to give our game a next stage, a tougher enemy waiting there, and one place to keep all of it.\nBy the end, a whole turn of our game will fit in one line:\ngame.click();\nThat one call will attack the enemy, notice when it dies, advance the stage, and send in the next one.\nOur starting point\nSo far, we have a player, a slime, and a way for one to hit the other.\nYou need two structs and four small functions to follow along:\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(\nhealth: u32,\ndamage: u32,\n) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn attack(\nplayer: &Player,\nenemy: &mut Enemy,\n) {\nlet damage = click_damage(\nplayer.base_damage,\nplayer.bonus_damage,\n);\nenemy.health =\nhealth_after_hit(enemy.health, damage);\n}\nattack() borrows the player to read it and the enemy to change it.\nOur combat loop looks like this:\nfn main() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nwhile !is_defeated(enemy.health) {\nattack(&player, &mut enemy);\nprintln!(\n\"{} health: {}\",\nenemy.name,\nenemy.health\n);\n}\nprintln!(\"{} defeated!\", enemy.name);\n}\nRun it and the Training Slime goes down in three hits.\nThen the program is over.\nThe next enemy needs to be tougher\nBefore we worry about where the next enemy comes from, we need to decide what it looks like.\nIf every enemy had 20 health, stage 50 would feel exactly like stage 1.\nThe next enemy should be tougher.\nThat is a game rule, and we know what to do with game rules by now.\nGive it a small function:\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nThat gives us:\nstage 1 → 20 health\nstage 2 → 30 health\nstage 3 → 40 health\nsaturating_mul() is the multiplication version of the saturating arithmetic we already use.\nThe exact balance does not matter yet.\nWhat matters is that the rule has a name and a test:\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\nTry it: change the numbers in the rule, or ask for a much later stage, and run it again.\nYou should see 20, 30 and 40 health, then Checks passed.\nThe Playground only runs #[test] functions in its Test mode, so these checks use assert_eq! inside main() instead.\nIf you change the formula, an assert_eq! fails and tells you which numbers disagreed.\nLive example · try changing the formula\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn main() {\nprintln!(\"stage 1 -> {} health\", enemy_health_for_stage(1));\nprintln!(\"stage 2 -> {} health\", enemy_health_for_stage(2));\nprintln!(\"stage 3 -> {} health\", enemy_health_for_stage(3));\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\nprintln!(\"Checks passed.\");\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/rust-game-state-impl-methods/).\nIf Rust warns that the function is never used yet, that is fine.\nWe are about to use it.\nTry it in main() first\nThe quickest way to make stages work is to put everything straight into main():\nfn main() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut stage = 1;\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\n};\nwhile stage <= 3 {\nattack(&player, &mut enemy);\nif is_defeated(enemy.health) {\nprintln!(\"Stage {stage} cleared!\");\nstage = stage.saturating_add(1);\nenemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\n};\n} else {\nprintln!(\n\"{} health: {}\",\nenemy.name,\nenemy.health\n);\n}\n}\n}\nWe stop after stage 3 so the terminal does not scroll forever.\nRun it.\nIt works.\nThe slime falls, a tougher one takes its place, and the stages keep coming.\nIt is also a mess.\nLook at what main() is doing now:\nowns the player\nowns the enemy\nowns the stage\ndecides when a stage is cleared\nbuilds the next enemy\nprints everything\nOnly that last job really belongs in main().\nEverything else is the game.\nSoon, defeating an enemy will also hand out rewards.\nIf all of those rules stay here, main() becomes the game engine and the terminal UI at the same time.\nThere is another reason to fix that now.\nLater, the browser will replace main().\nThe game rules need somewhere else to live.\nGive the game a shape\nWe have solved this kind of problem before.\nThree loose numbers were not a player, so we built a Player.\nA stage, a player, and an enemy sitting loose in main() are not a game.\nSo we build a Game:\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\nThis struct contains other structs:\nGame\n├── stage: u32\n├── player: Player\n└── enemy: Enemy\nA struct can hold types we made ourselves.\nThe Game owns its Player.\nIt owns its Enemy.\nThere is now one value we can point at and say:\nThis is the current state of the game.\nWhere does a new enemy come from?\nWe could create a game by spelling out every field manually:\nlet mut game = Game {\nstage: 1,\nplayer: Player {\nbase_damage: 5,\nbonus_damage: 2,\n},\nenemy: Enemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(1),\n},\n};\nThat works.\nBut we have already written this enemy more than once:\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\n}\nWhat we really want to say is:\nGive me the enemy for this stage.\nThat sounds like a function.\nAnd because the function is specifically about creating an Enemy, it belongs with Enemy.\nAttach functions to a type with impl\nRust lets us attach functions to a type with an impl block:\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\n}\n}\n}\nimpl Enemy means:\nhere are functions that belong to Enemy\nNow we can write:\nEnemy::for_stage(1)\nYou have already seen this style of call:\nString::from(\"Training Slime\")\nA function attached to a type that does not need an existing value is called an associated function.\nEnemy::for_stage() does not replace our small health rule.\nIt uses it:\nhealth: enemy_health_for_stage(stage),\nOne function decides how tough the enemy is.\nThe other decides how to build one.\nQuestion: What separates Enemy::for_stage(2) from a method like game.click()?\nfor_stage needs no existing value to run, while click works on a specific Game through self\nfor_stage is a method, and click is an associated function\nclick can only be called from inside main()\nAnswer: 1. for_stage needs no existing value to run, while click works on a specific Game through self\nExplanation: An associated function belongs to the type and is called with ::. A method takes self and is called on a value with a dot.\nA new game\nGame gets an impl block too:\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player {\nbase_damage: 5,\nbonus_damage: 2,\n},\nenemy: Enemy::for_stage(1),\n}\n}\n}\nThere is nothing magical about new.\nRust has no special constructor keyword.\nnew is simply a common name for an associated function that creates a fresh value.\nStarting the game is now:\nlet mut game = Game::new();\nGame setup has one home.\nAnd it is no longer main().\nOne click\nNow the part we have been building toward.\nWhat happens when the player clicks?\nAdd this inside impl Game:\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\n}\nThere is one new name in that signature:\nself\nself means:\nthe value this method was called on\nWhen we write:\ngame.click();\ninside click(), self refers to game.\nThat makes click() a method: a function attached to a type that works on a specific value of that type.\nGame::new() does not take self because there is no game yet.\nclick() does because it works on a game that already exists.\nAnd &mut self should look familiar.\nIt means click() may temporarily change the game without taking ownership of it.\nThat is why this needs:\nlet mut game = Game::new();\nWithout mut, Rust refuses to let click() borrow the game mutably.\nQuestion: Why does let game = Game::new(); game.click(); fail to compile?\nGame::new() returns a borrowed value\nclick takes &mut self, and game was not declared mut\nclick takes ownership of game, so it can only be called once\nAnswer: 2. click takes &mut self, and game was not declared mut\nExplanation: The error is E0596: cannot borrow game as mutable, as it is not declared as mutable.\nTwo borrows, one game\nLook inside click():\nattack(&self.player, &mut self.enemy);\nLast time, we learned:\nFor the same data at the same time: many readers, or one writer, but not both.\nSo why can we borrow part of the game to read and another part to change?\nBecause they are not the same data.\nself.player and self.enemy are different fields.\nThey do not overlap.\nRust can see that reading the player does not interfere with changing the enemy.\nOur attack() did not need to change.\nThe borrowing simply moved inside the game.\nQuestion: Why can click() write attack(&self.player, &mut self.enemy) when the rule says many readers or one writer, never both?\nRust does not apply borrowing rules to self\nThe reader borrows self.player and the writer borrows self.enemy, different fields that do not overlap\nAny & and &mut are allowed together when they appear in the same call\nAnswer: 2. The reader borrows self.player and the writer borrows self.enemy, different fields that do not overlap\nExplanation: Borrowing self.enemy both ways at once, like let p = &self.enemy; before the attack and using p after it, fails with E0502.\nWhat happens when the enemy dies?\nRight now, click() only attacks.\nWhen the enemy reaches zero health, the next click would attack a corpse.\nThat is grim.\nAfter every attack, click() should check whether the enemy was defeated:\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nNow we need defeat_enemy():\nfn defeat_enemy(&mut self) {\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\nThe stage goes up.\nThe defeated enemy is replaced with the enemy for the new stage.\nWe could put both lines directly inside click().\nBut defeating an enemy is its own event, and that event is going to grow.\nSoon it will also hand out rewards.\nKeeping it named means click() can stay readable:\nAttack the enemy. If it went down, handle the defeat.\nAnd defeat_enemy() says:\nGo to the next stage. Bring in the next enemy.\nTry it: here is the game so far, clicked three times.\nYou should see Stage 1: Training Slime health: 13, then Stage 1: Training Slime health: 6, then Stage 2: Training Slime health: 30.\nThen try two experiments:\ndelete the if block in click() and the third click leaves the slime at health: 0 on stage 1, because nothing handles the defeat\nput the if block back and remove mut from let mut game, and Rust refuses with E0596\nLive example · try removing the defeat check\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player {\nbase_damage: 5,\nbonus_damage: 2,\n},\nenemy: Enemy::for_stage(1),\n}\n}\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage, player.bonus_damage);\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet mut game = Game::new();\ngame.click();\nprintln!(\"Stage {}: {} health: {}\", game.stage, game.enemy.name, game.enemy.health);\ngame.click();\nprintln!(\"Stage {}: {} health: {}\", game.stage, game.enemy.name, game.enemy.health);\ngame.click();\nprintln!(\"Stage {}: {} health: {}\", game.stage, game.enemy.name, game.enemy.health);\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/rust-game-state-impl-methods/).\nmain() gets boring\nHere is the new main():\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 3 {\nlet stage = game.stage;\ngame.click();\nif game.stage > stage {\nprintln!(\"Stage {stage} cleared!\");\n} else {\nprintln!(\n\"{} health: {}\",\ngame.enemy.name,\ngame.enemy.health\n);\n}\n}\n}\nmain() still reads the game so it can decide what to print.\nBut it does not:\nbuild enemies\ncalculate their health\ndecide when a stage advances\nhandle a defeat\nIt creates a game, clicks, and prints.\nBoring is exactly what we want.\nRun:\ncargo run\nYou should see:\nTraining Slime health: 13\nTraining Slime health: 6\nStage 1 cleared!\nTraining Slime health: 23\nTraining Slime health: 16\nTraining Slime health: 9\nTraining Slime health: 2\nStage 2 cleared!\nTraining Slime health: 33\nTraining Slime health: 26\nTraining Slime health: 19\nTraining Slime health: 12\nTraining Slime health: 5\nStage 3 cleared!\nThree stages.\nEach slime tougher than the last.\nAnd the game, not main(), decided all of that.\nTest what a player experiences\nOur first tests check individual rules.\nGame lets us start testing something larger: what happens when those rules work together.\nA new game should start at stage 1:\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\n}\nA click should hurt the enemy:\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 13);\n}\nAnd defeating an enemy should move the game forward:\n#[test]\nfn defeating_an_enemy_advances_the_stage() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\n}\nThat last test does not care how damage is calculated.\nThose smaller rules already have their own tests.\nIt asks a larger question:\nWhen a click defeats an enemy, does the game move on?\nRun:\ncargo test\nAll eight should pass.\nCoding challenge: Click many times\nAdd a method to Game called click_many that calls self.click() once for every click requested. Keep the name click_many and its clicks argument, because the checks call it. Press Check my code and it will tell you how you are doing.\nExpected result: the starter prints Stage 1 - Training Slime health: 20, and your finished version prints Stage 2 - Training Slime health: 16.\nFinish click_many\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player {\nbase_damage: 5,\nbonus_damage: 2,\n},\nenemy: Enemy::for_stage(1),\n}\n}\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\n// TODO: call self.click() once for every click requested\nfn click_many(&mut self, clicks: u32) {}\nfn defeat_enemy(&mut self) {\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage, player.bonus_damage);\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet mut game = Game::new();\ngame.click_many(5);\nprintln!(\"Stage {} - {} health: {}\", game.stage, game.enemy.name, game.enemy.health);\n}\n<details>\n<summary>Spoiler: the checks for this challenge</summary>\nThese tests run when you press Check my code on the original article. Reading them first gives away the answer.\n/// One click should be exactly one hit. Has click_many touched the enemy yet?\n#[test]\nfn one_click_is_one_hit() {\nlet mut game = Game::new();\ngame.click_many(1);\nassert_eq!(game.enemy.health, 13);\nassert_eq!(game.stage, 1);\n}\n/// Count your clicks. Ask for two and the slime should feel two hits, no more and no fewer.\n#[test]\nfn every_click_lands() {\nlet mut game = Game::new();\ngame.click_many(2);\nassert_eq!(game.enemy.health, 6);\n}\n/// The slime should still fall mid-loop and the next one oozes in. Is each click going through self.click()?\n#[test]\nfn defeat_still_happens() {\nlet mut game = Game::new();\ngame.click_many(3);\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, enemy_health_for_stage(2));\n}\n</details>\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/rust-game-state-impl-methods/).\nTip:\nKeep a counter that starts at 0, and use while done < clicks to repeat. Add 1 to the counter with saturating_add(1) each time around.\nThe complete program\nIf you are only reading along, you can skip to the last two sections.\nIf you are coding along, here is everything in one place:\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player {\nbase_damage: 5,\nbonus_damage: 2,\n},\nenemy: Enemy::for_stage(1),\n}\n}\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(\nhealth: u32,\ndamage: u32,\n) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn attack(\nplayer: &Player,\nenemy: &mut Enemy,\n) {\nlet damage = click_damage(\nplayer.base_damage,\nplayer.bonus_damage,\n);\nenemy.health =\nhealth_after_hit(enemy.health, damage);\n}\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 3 {\nlet stage = game.stage;\ngame.click();\nif game.stage > stage {\nprintln!(\"Stage {stage} cleared!\");\n} else {\nprintln!(\n\"{} health: {}\",\ngame.enemy.name,\ngame.enemy.health\n);\n}\n}\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(\nhealth_after_hit(3, 10),\n0\n);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 10,\n};\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 3);\n}\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\n}\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 13);\n}\n#[test]\nfn defeating_an_enemy_advances_the_stage() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\n}\n}\nRun both:\ncargo run\ncargo test\nThe game should clear three stages, and all eight tests should pass.\nWhat we learned\nAlong the way, we encountered:\nstructs that own other structs\nimpl blocks\nassociated functions like Enemy::for_stage()\nnew() as a convention, not a keyword\nmethods\nself and &mut self\nborrowing different fields at the same time\nsaturating_mul()\ntests that describe larger game behavior\nYou do not need to memorize all of that yet.\nThe important change is where the game lives.\nWe started with a main() that owned everything and decided everything.\nWe ended with:\nlet mut game = Game::new();\ngame.click();\nOne click is now one full turn:\ngame.click()\n↓\nattack(&self.player, &mut self.enemy)\n↓\nclick_damage()\n↓\nhealth_after_hit()\n↓\nis_defeated()?\n↓\ndefeat_enemy()\n↓\nnext stage + Enemy::for_stage()\nLook at the bottom of that chain.\nclick_damage(), health_after_hit(), and is_defeated() are the same small functions we started the series with.\nThey have not changed.\nThey are simply a few layers deeper now, inside something that finally looks like a game.\nWinning does not pay yet\nOur game has a real loop now.\nClick. The slime falls. The next one walks in tougher than the last.\nBut look at what the player gets for all that clicking:\ngold:       nothing\nexperience: nothing\nlevels:     nothing\nWe just cleared three stages.\nOur player is exactly as strong as when the game started.\nEvery slime gets tougher.\nOur player never does.\nKeep going long enough, and every new slime takes more clicks than the last one.\nForever.\nThat is not progression.\nThat is a treadmill.\nAn idle game needs a reason to keep clicking: rewards for winning, levels to reach, and something to spend those rewards on.\nAnd this time, we already know where those rewards will be handed out:\nfn defeat_enemy(&mut self)\nNext, we will make winning worth something.\nSmall function. Bigger game.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/rust-game-state-impl-methods/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2026-10-17T19:42:53Z",
      "id": "https://mnixblog.netlify.app/rust-game-state-impl-methods/",
      "image": "https://mnixblog.netlify.app/images/rust-game-state-impl-methods/cover.png",
      "summary": "Learn Rust impl blocks and methods by giving your idle game one Game struct, tougher enemies by stage, and a game.click() that runs a full turn.",
      "tags": [
        "Rust",
        "WebAssembly",
        "Game Development",
        "Beginners"
      ],
      "title": "One Fight Is Not a Game.",
      "url": "https://mnixblog.netlify.app/rust-game-state-impl-methods/"
    },
    {
      "content_html": "<p>Our game has a <code>Player</code> and an <code>Enemy</code></p>\n<p>Then, the moment they fight, we strip them back down for parts:</p>\n<pre><code class=\"language-rust\">enemy.health = attack(\n    enemy.health,\n    player.base_damage,\n    player.bonus_damage,\n);\n</code></pre>\n<p>We built two real game types.</p>\n<p>The moment they interact, we go back to passing loose numbers around.</p>\n<p>In this article, we are going to change <code>attack()</code> so it takes the player and the enemy themselves.</p>\n<p>Along the way, Rust will refuse to compile our code three times.</p>\n<p>Every time, it will be right.</p>\n<p>By the end, our combat loop will say what the game actually does:</p>\n<pre><code class=\"language-rust\">attack(&amp;player, &amp;mut enemy);\n</code></pre>\n<p>A player attacks an enemy.</p>\n<p>Nothing gets torn apart.</p>\n<hr />\n<h2 id=\"our-starting-point\">Our starting point<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#our-starting-point\" aria-label=\"Link to this section\">#</a></h2>\n<p>You need two structs and four small functions to follow along:</p>\n<pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(\n    health: u32,\n    damage: u32,\n) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn attack(\n    health: u32,\n    base_damage: u32,\n    bonus_damage: u32,\n) -&gt; u32 {\n    let damage =\n        click_damage(base_damage, bonus_damage);\n\n    health_after_hit(health, damage)\n}\n</code></pre>\n<p>At the bottom of <code>main.rs</code>, the <code>tests</code> module holds three tests for the small rules and one test that calls <code>attack()</code> directly:</p>\n<pre><code class=\"language-rust\">#[test]\nfn attack_reduces_enemy_health() {\n    assert_eq!(attack(10, 5, 2), 3);\n}\n</code></pre>\n<p>We will come back to that test once <code>attack()</code> changes.</p>\n<p>Our combat loop looks like this:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let mut enemy = Enemy {\n        name: String::from(\"Training Slime\"),\n        health: 20,\n    };\n\n    while !is_defeated(enemy.health) {\n        enemy.health = attack(\n            enemy.health,\n            player.base_damage,\n            player.bonus_damage,\n        );\n\n        println!(\n            \"{} health: {}\",\n            enemy.name,\n            enemy.health\n        );\n    }\n\n    println!(\"{} defeated!\", enemy.name);\n}\n</code></pre>\n<p>It works.</p>\n<p>But <code>attack()</code> does not know what a player is.</p>\n<p>It does not know what an enemy is.</p>\n<p>It takes three numbers and hands back a fourth, and <code>main()</code> has to put that number back where it belongs.</p>\n<p>What we want to write is closer to:</p>\n<pre><code class=\"language-rust\">attack(player, enemy);\n</code></pre>\n<p>So let us try that.</p>\n<hr />\n<h2 id=\"hand-the-player-to-attack\">Hand the player to <code>attack()</code><a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#hand-the-player-to-attack\" aria-label=\"Link to this section\">#</a></h2>\n<p>Let us start with the player and leave the enemy alone for a moment.</p>\n<p>Change <code>attack()</code> so it takes a whole <code>Player</code> instead of two loose damage values:</p>\n<pre><code class=\"language-rust\">fn attack(\n    player: Player,\n    health: u32,\n) -&gt; u32 {\n    let damage = click_damage(\n        player.base_damage,\n        player.bonus_damage,\n    );\n\n    health_after_hit(health, damage)\n}\n</code></pre>\n<p>The body barely changed.</p>\n<p>We read the damage values from the player with the same dot syntax we used in <code>main()</code>.</p>\n<p>Now update the call inside the loop:</p>\n<pre><code class=\"language-rust\">enemy.health =\n    attack(player, enemy.health);\n</code></pre>\n<p>That reads much better.</p>\n<p>Run it:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>Rust refuses to build the program.</p>\n<p>Trimmed down, the error says:</p>\n<pre><code class=\"language-text\">error[E0382]: use of moved value: `player`\n\nvalue moved here, in previous\niteration of loop\n</code></pre>\n<p>The full message is longer, and it suggests a few fixes, like cloning the player.</p>\n<p>Some of them would work.</p>\n<p>None of them is what we want, so let us figure out what actually happened.</p>\n<p>The first attack would have been fine.</p>\n<p>The second attack is the problem.</p>\n<p>Where did the player go?</p>\n<hr />\n<h2 id=\"the-player-moved\">The player moved<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#the-player-moved\" aria-label=\"Link to this section\">#</a></h2>\n<p>In Rust, every value has an owner.</p>\n<p>Right now, <code>main()</code> owns <code>player</code>.</p>\n<p>It created it, so it is responsible for it.</p>\n<p>When we write:</p>\n<pre><code class=\"language-rust\">attack(player, enemy.health)\n</code></pre>\n<p>and <code>attack()</code> asks for a <code>Player</code>, we are not lending the player to <code>attack()</code>.</p>\n<p>We are giving it away.</p>\n<p>Ownership moves into <code>attack()</code>.</p>\n<p>When <code>attack()</code> finishes, the <code>Player</code> it owns is no longer available back in <code>main()</code>.</p>\n<p>So on the next trip around the loop, <code>main()</code> tries to hand over a player it no longer owns.</p>\n<p>Rust catches that before the program ever runs.</p>\n<p>In game terms, our player attacked once and then vanished.</p>\n<p>That is a bold balancing decision.</p>\n<p>It is not ours.</p>\n<p>The attack was supposed to defeat the enemy.</p>\n<p>Not the player.</p>\n<hr />\n<h2 id=\"why-did-health-never-move\">Why did health never move?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#why-did-health-never-move\" aria-label=\"Link to this section\">#</a></h2>\n<p>We have been passing <code>enemy.health</code> into functions all along, and it never disappeared.</p>\n<p>That is because <code>u32</code> implements Rust's <code>Copy</code> trait.</p>\n<p>When we pass a <code>u32</code> by value, Rust can automatically make a copy, so the original remains available.</p>\n<p>Our <code>Player</code> does not implement <code>Copy</code>, so passing it by value moves ownership instead.</p>\n<p>You do not need to understand traits yet.</p>\n<p>For now, the useful difference is:</p>\n<pre><code class=\"language-text\">u32     → copied\nPlayer  → moved\n</code></pre>\n<p>A <code>u32</code> gets photocopied.</p>\n<p>A <code>Player</code> gets handed over.</p>\n<p>We do not want to give the player away.</p>\n<p>We want <code>attack()</code> to look at it.</p>\n<p>Try it: this is the program with <code>attack()</code> taking a whole <code>Player</code>, and it fails to compile on purpose.</p>\n<p>Run it and read the error for yourself.</p>\n<p>You should see error code <code>E0382</code>, \"use of moved value\", pointing at the call inside the loop.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · read the move error</figcaption><pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn attack(player: Player, health: u32) -&gt; u32 {\n    let damage = click_damage(player.base_damage, player.bonus_damage);\n\n    health_after_hit(health, damage)\n}\n\nfn main() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let mut enemy = Enemy {\n        name: String::from(&quot;Training Slime&quot;),\n        health: 20,\n    };\n\n    while !is_defeated(enemy.health) {\n        enemy.health = attack(player, enemy.health);\n\n        println!(&quot;{} health: {}&quot;, enemy.name, enemy.health);\n    }\n\n    println!(&quot;{} defeated!&quot;, enemy.name);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/\">the original article</a>.</p></figure>\n<p>The next section fixes it with one character in two places.</p>\n<p>If you want to fix it first, ask yourself what <code>attack()</code> needs to do with the player.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>After <code>attack(player, enemy.health)</code> runs once, why can <code>main()</code> still use <code>enemy.health</code> but not <code>player</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Fields of a struct are never moved</li>\n<li><code>u32</code> implements <code>Copy</code>, so a copy was passed, but <code>Player</code> does not, so it moved</li>\n<li>Rust only tracks moves for values that contain a <code>String</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. <code>u32</code> implements <code>Copy</code>, so a copy was passed, but <code>Player</code> does not, so it moved</p>\n<p>Passing a <code>u32</code> by value copies it. <code>Player</code> is not <code>Copy</code>, so passing it by value hands ownership to <code>attack()</code>.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"borrow-the-player-with\">Borrow the player with <code>&amp;</code><a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#borrow-the-player-with\" aria-label=\"Link to this section\">#</a></h2>\n<p>Rust lets a function borrow a value instead of owning it.</p>\n<p>Add one character to the parameter type:</p>\n<pre><code class=\"language-rust\">fn attack(\n    player: &amp;Player,\n    health: u32,\n) -&gt; u32 {\n    let damage = click_damage(\n        player.base_damage,\n        player.bonus_damage,\n    );\n\n    health_after_hit(health, damage)\n}\n</code></pre>\n<p><code>&amp;Player</code> means:</p>\n<blockquote>\n<p>a reference to a <code>Player</code> that someone else owns</p>\n</blockquote>\n<p>A reference lets <code>attack()</code> reach the player without taking ownership of it.</p>\n<p>When the call finishes, the player is still sitting in <code>main()</code> where we left it.</p>\n<p>Notice that the body did not change.</p>\n<p>The dot works the same way through a reference, so:</p>\n<pre><code class=\"language-rust\">player.base_damage\n</code></pre>\n<p>still reads the field.</p>\n<p>The call needs the same <code>&amp;</code>:</p>\n<pre><code class=\"language-rust\">enemy.health =\n    attack(&amp;player, enemy.health);\n</code></pre>\n<p><code>&amp;player</code> says:</p>\n<blockquote>\n<p>let <code>attack()</code> borrow the player for this call</p>\n</blockquote>\n<p>Run it:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>The Training Slime goes down in three hits again.</p>\n<p>The player survives every one of them.</p>\n<p>There is a bonus hiding in that <code>&amp;</code>.</p>\n<p>A plain <code>&amp;</code> reference gives <code>attack()</code> read-only access to the player's fields.</p>\n<p>If <code>attack()</code> tried to assign a new value to:</p>\n<pre><code class=\"language-rust\">player.base_damage\n</code></pre>\n<p>Rust would refuse to compile it.</p>\n<p>That is exactly the promise we want.</p>\n<p>An attack needs to read the player.</p>\n<p>It does not need to change the player.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Inside <code>fn attack(player: &amp;Player, ...)</code>, what does the line <code>player.base_damage = 9;</code> do?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Changes the caller's player, since a reference points at it</li>\n<li>Fails to compile, because a plain <code>&amp;</code> reference is read-only</li>\n<li>Compiles, but only changes a private copy</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Fails to compile, because a plain <code>&amp;</code> reference is read-only</p>\n<p>Rust rejects it with <code>E0594</code>. The reference lets <code>attack()</code> read the player, not write to it.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"now-the-enemy\">Now the enemy<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#now-the-enemy\" aria-label=\"Link to this section\">#</a></h2>\n<p>The player is solved.</p>\n<p>The enemy is still split apart:</p>\n<pre><code class=\"language-rust\">enemy.health =\n    attack(&amp;player, enemy.health);\n</code></pre>\n<p><code>attack()</code> gets a copy of the enemy's health, calculates a new number, and returns it.</p>\n<p>Then <code>main()</code> writes that number back into the enemy.</p>\n<p>What we really want is for <code>attack()</code> to change the enemy itself.</p>\n<p>We just learned that borrowing keeps a value where it is.</p>\n<p>Let us borrow the enemy too:</p>\n<pre><code class=\"language-rust\">fn attack(\n    player: &amp;Player,\n    enemy: &amp;Enemy,\n) {\n    let damage = click_damage(\n        player.base_damage,\n        player.bonus_damage,\n    );\n\n    enemy.health =\n        health_after_hit(enemy.health, damage);\n}\n</code></pre>\n<p>Two things changed besides the enemy parameter.</p>\n<p>There is no:</p>\n<pre><code class=\"language-rust\">-&gt; u32\n</code></pre>\n<p>anymore.</p>\n<p><code>attack()</code> is not handing back a number for someone else to store.</p>\n<p>The result lives in the enemy.</p>\n<p>Inside the function, we assign the new health directly to:</p>\n<pre><code class=\"language-rust\">enemy.health\n</code></pre>\n<p>Update the call in <code>main()</code>:</p>\n<pre><code class=\"language-rust\">attack(&amp;player, &amp;enemy);\n</code></pre>\n<p>Run it:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>Rust refuses again:</p>\n<pre><code class=\"language-text\">error[E0594]: cannot assign to\n`enemy.health`, which is behind\na `&amp;` reference\n</code></pre>\n<p>You may also see a warning that <code>enemy</code> does not need to be mutable.</p>\n<p>Ignore it for a moment.</p>\n<p>We are about to need that <code>mut</code> again.</p>\n<p>This is the promise from the last section working against us.</p>\n<p><code>&amp;Enemy</code> lets us look at the enemy.</p>\n<p>We did not come here to look.</p>\n<p>A plain <code>&amp;</code> reference is read-only.</p>\n<p>That was perfect for the player.</p>\n<p>It is wrong for the enemy because the entire point of an attack is that the enemy's health changes.</p>\n<hr />\n<h2 id=\"borrow-the-enemy-with-mut\">Borrow the enemy with <code>&amp;mut</code><a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#borrow-the-enemy-with-mut\" aria-label=\"Link to this section\">#</a></h2>\n<p>We have already met <code>mut</code>.</p>\n<p>It is how we told Rust that our enemy is allowed to change.</p>\n<p>References can express the same idea.</p>\n<p>Change:</p>\n<pre><code class=\"language-rust\">&amp;Enemy\n</code></pre>\n<p>to:</p>\n<pre><code class=\"language-rust\">&amp;mut Enemy\n</code></pre>\n<p>Our attack becomes:</p>\n<pre><code class=\"language-rust\">fn attack(\n    player: &amp;Player,\n    enemy: &amp;mut Enemy,\n) {\n    let damage = click_damage(\n        player.base_damage,\n        player.bonus_damage,\n    );\n\n    enemy.health =\n        health_after_hit(enemy.health, damage);\n}\n</code></pre>\n<p><code>&amp;mut Enemy</code> means:</p>\n<blockquote>\n<p>a mutable reference to an <code>Enemy</code> that someone else owns</p>\n</blockquote>\n<p>In game terms:</p>\n<blockquote>\n<p><code>attack()</code> may temporarily change this enemy, but it does not own the enemy.</p>\n</blockquote>\n<p>The call needs to ask for the same thing:</p>\n<pre><code class=\"language-rust\">attack(&amp;player, &amp;mut enemy);\n</code></pre>\n<p>This only works because <code>enemy</code> was already declared with:</p>\n<pre><code class=\"language-rust\">let mut enemy\n</code></pre>\n<p>Rust will not let us borrow something mutably if the owner never agreed that it could change.</p>\n<p>Run it:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>You should see:</p>\n<pre><code class=\"language-text\">Training Slime health: 13\nTraining Slime health: 6\nTraining Slime health: 0\nTraining Slime defeated!\n</code></pre>\n<p>The same fight as before.</p>\n<p>But now the signature tells the whole story:</p>\n<pre><code class=\"language-text\">&amp;Player     → attack reads the player\n&amp;mut Enemy  → attack changes the enemy\n</code></pre>\n<p>Nobody gets consumed.</p>\n<p>Nobody gets torn apart.</p>\n<p>Both are still in <code>main()</code>, ready for the next attack.</p>\n<p>Try it: here is a small version of the fixed program that attacks twice.</p>\n<p>Run it first.</p>\n<p>You should see <code>Training Slime health: 13</code>, then <code>Training Slime health: 6</code>.</p>\n<p>Then break it on purpose, one change at a time, and read each error:</p>\n<ul>\n<li>change <code>enemy: &amp;mut Enemy</code> to <code>enemy: &amp;Enemy</code> in <code>attack()</code> and you get <code>E0594</code>, the same error as before</li>\n<li>put <code>enemy: &amp;mut Enemy</code> back, then remove <code>mut</code> from <code>let mut enemy</code> and you get <code>E0596</code></li>\n</ul>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try breaking the borrow</figcaption><pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage, player.bonus_damage);\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let mut enemy = Enemy {\n        name: String::from(&quot;Training Slime&quot;),\n        health: 20,\n    };\n\n    attack(&amp;player, &amp;mut enemy);\n    println!(&quot;{} health: {}&quot;, enemy.name, enemy.health);\n\n    attack(&amp;player, &amp;mut enemy);\n    println!(&quot;{} health: {}&quot;, enemy.name, enemy.health);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/\">the original article</a>.</p></figure>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does <code>attack(&amp;player, &amp;mut enemy)</code> need <code>let mut enemy</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Because <code>&amp;mut</code> makes Rust copy the enemy first</li>\n<li>Because Rust will not lend out a mutable borrow of a value its owner never declared changeable</li>\n<li>It does not; <code>&amp;mut enemy</code> works on any variable</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Because Rust will not lend out a mutable borrow of a value its owner never declared changeable</p>\n<p>Without <code>mut</code> on the variable, the borrow fails with <code>E0596</code>.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"one-rule-worth-knowing-now\">One rule worth knowing now<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#one-rule-worth-knowing-now\" aria-label=\"Link to this section\">#</a></h2>\n<p>There is one borrowing rule you will run into early, so here it is in game terms.</p>\n<p>Suppose that, instead of the combat loop, <code>main()</code> grabs a reference to the enemy's name before attacking:</p>\n<pre><code class=\"language-rust\">let name = &amp;enemy.name;\n\nattack(&amp;player, &amp;mut enemy);\n\nprintln!(\"{name} took a hit!\");\n</code></pre>\n<p>Rust refuses for the third time:</p>\n<pre><code class=\"language-text\">error[E0502]: cannot borrow `enemy`\nas mutable because it is also\nborrowed as immutable\n</code></pre>\n<p>A borrow remains active until its last use.</p>\n<p><code>name</code> is used by the <code>println!()</code> after the attack, so that borrow is still active when <code>attack()</code> asks for permission to change the enemy.</p>\n<p>Rust will not hand out a mutable borrow while that reading borrow is still being used.</p>\n<p>Our combat loop never hits this problem because each borrow is finished before the next conflicting access begins:</p>\n<pre><code class=\"language-rust\">attack(&amp;player, &amp;mut enemy);\n\nprintln!(\n    \"{} health: {}\",\n    enemy.name,\n    enemy.health\n);\n</code></pre>\n<p>In this case, the mutable borrow is finished when <code>attack()</code> returns.</p>\n<p>Then <code>println!()</code> is free to read the enemy.</p>\n<p>That is the beginner version of the rule:</p>\n<blockquote>\n<p><strong>For the same data at the same time: many readers, or one writer, but not both.</strong></p>\n</blockquote>\n<p>You do not need more than that today.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does Rust reject reading <code>name = &amp;enemy.name</code> before <code>attack(&amp;player, &amp;mut enemy)</code> when <code>name</code> is printed afterward?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>A <code>&amp;</code> borrow can never be followed by a <code>&amp;mut</code> borrow of the same value</li>\n<li>The <code>&amp;</code> borrow is still in use when <code>attack()</code> asks for <code>&amp;mut</code>, so a reader and a writer overlap</li>\n<li><code>attack()</code> takes ownership of <code>enemy</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. The <code>&amp;</code> borrow is still in use when <code>attack()</code> asks for <code>&amp;mut</code>, so a reader and a writer overlap</p>\n<p>A borrow lasts until its last use. If nothing used <code>name</code> after the attack, the program would compile.</p>\n</div>\n</div>\n</section>\n<p><strong>Coding challenge: End the borrow in time</strong></p>\n<p>This starter fails to compile on purpose, because the lesson here is the error.</p>\n<p>Fix it without changing <code>attack()</code>. Keep the names <code>attack</code> and <code>main</code> and the shape of <code>Player</code> and <code>Enemy</code>, because the checks call them. Press <strong>Check my code</strong> and it will tell you how you are doing.</p>\n<p><strong>Expected result:</strong> it compiles and prints <code>Training Slime took a hit! Health: 13</code>.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Fix the borrow conflict</figcaption><pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage, player.bonus_damage);\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let mut enemy = Enemy {\n        name: String::from(&quot;Training Slime&quot;),\n        health: 20,\n    };\n\n    // TODO: this borrow is still in use after the attack. Make the program compile.\n    let name = &amp;enemy.name;\n\n    attack(&amp;player, &amp;mut enemy);\n\n    println!(&quot;{name} took a hit! Health: {}&quot;, enemy.health);\n}\n</code></pre><details class=\"ob-spoiler\"><summary>Spoiler: the checks for this challenge</summary><p>These tests run when you press Check my code on the original article. Reading them first gives away the answer.</p><pre><code class=\"language-rust\">/// It compiles now, and the program still runs from start to finish.\n#[test]\nfn main_still_runs() {\n    main();\n}\n\n/// It compiles, but attack() no longer does its job. We only wanted to move the borrow, not the damage.\n#[test]\nfn attack_still_lands() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n    let mut enemy = Enemy {\n        name: String::from(&quot;Training Slime&quot;),\n        health: 20,\n    };\n\n    attack(&amp;player, &amp;mut enemy);\n\n    assert_eq!(enemy.health, 13);\n}\n</code></pre></details><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>The error is <code>E0502</code>. A borrow ends at its last use, so ask where <code>name</code> needs to start borrowing.</p>\n</aside>\n<hr />\n<h2 id=\"update-the-test\">Update the test<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#update-the-test\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our <code>attack()</code> test still passes three loose numbers, so it needs the new signature:</p>\n<pre><code class=\"language-rust\">#[test]\nfn attack_reduces_enemy_health() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let mut enemy = Enemy {\n        name: String::from(\"Training Slime\"),\n        health: 10,\n    };\n\n    attack(&amp;player, &amp;mut enemy);\n\n    assert_eq!(enemy.health, 3);\n}\n</code></pre>\n<p>The rule it checks has not changed.</p>\n<p>An attack with five base damage and two bonus damage takes ten health down to three.</p>\n<p>What changed is how the test says it.</p>\n<p>It builds a player.</p>\n<p>It builds an enemy.</p>\n<p>It lets one attack the other.</p>\n<p>Then it checks the enemy afterward.</p>\n<p>That reads like the game.</p>\n<p>The other three tests do not touch <code>attack()</code>, so they stay exactly as they were.</p>\n<p>Run them:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>All four should pass.</p>\n<p>We do not need a separate test proving that <code>attack()</code> does not change the player.</p>\n<p>With our current <code>Player</code> type, an <code>&amp;Player</code> reference does not allow <code>attack()</code> to assign new values to the player's fields.</p>\n<p>The compiler checks that for us every time we build.</p>\n<hr />\n<h2 id=\"the-complete-program\">The complete program<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#the-complete-program\" aria-label=\"Link to this section\">#</a></h2>\n<p>If you have been following along in your editor, here is everything in one place.</p>\n<p>At this point, <code>src/main.rs</code> should look like this:</p>\n<pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(\n    health: u32,\n    damage: u32,\n) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn attack(\n    player: &amp;Player,\n    enemy: &amp;mut Enemy,\n) {\n    let damage = click_damage(\n        player.base_damage,\n        player.bonus_damage,\n    );\n\n    enemy.health =\n        health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let mut enemy = Enemy {\n        name: String::from(\"Training Slime\"),\n        health: 20,\n    };\n\n    while !is_defeated(enemy.health) {\n        attack(&amp;player, &amp;mut enemy);\n\n        println!(\n            \"{} health: {}\",\n            enemy.name,\n            enemy.health\n        );\n    }\n\n    println!(\"{} defeated!\", enemy.name);\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(\n            health_after_hit(3, 10),\n            0\n        );\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player {\n            base_damage: 5,\n            bonus_damage: 2,\n        };\n\n        let mut enemy = Enemy {\n            name: String::from(\"Training Slime\"),\n            health: 10,\n        };\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 3);\n    }\n}\n</code></pre>\n<p>Run both:</p>\n<pre><code class=\"language-bash\">cargo run\ncargo test\n</code></pre>\n<p>The Training Slime should fall in three hits, and all four tests should pass.</p>\n<hr />\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Along the way, we encountered:</p>\n<ul>\n<li>\n<p>ownership</p>\n</li>\n<li>\n<p>moves</p>\n</li>\n<li>\n<p><code>Copy</code> values</p>\n</li>\n<li>\n<p><code>&amp;T</code> references</p>\n</li>\n<li>\n<p><code>&amp;mut T</code> references</p>\n</li>\n<li>\n<p>functions that change a value instead of returning a new one</p>\n</li>\n<li>\n<p>when borrows stop being used</p>\n</li>\n<li>\n<p>why Rust prevents conflicting access</p>\n</li>\n</ul>\n<p>You do not need to memorize all of that yet.</p>\n<p>The important change is what our attack says.</p>\n<p>We started with:</p>\n<pre><code class=\"language-rust\">enemy.health = attack(\n    enemy.health,\n    player.base_damage,\n    player.bonus_damage,\n);\n</code></pre>\n<p>We ended with:</p>\n<pre><code class=\"language-rust\">attack(&amp;player, &amp;mut enemy);\n</code></pre>\n<p>The player is read.</p>\n<p>The enemy is changed.</p>\n<p>Both stay where they belong.</p>\n<p>And the compiler now checks that <code>attack()</code> keeps its side of that deal.</p>\n<hr />\n<h2 id=\"there-is-a-bigger-shape-missing\">There is a bigger shape missing<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#there-is-a-bigger-shape-missing\" aria-label=\"Link to this section\">#</a></h2>\n<p>Look at what <code>main()</code> is holding now:</p>\n<pre><code class=\"language-text\">player\nenemy\nthe combat loop\n</code></pre>\n<p>That is a fight.</p>\n<p>It is not a game.</p>\n<p>When the Training Slime dies, the program simply ends.</p>\n<p>There is no next enemy.</p>\n<p>There is no stage counter.</p>\n<p>Nobody earns anything for winning.</p>\n<p>And every one of those things would become another loose variable inside <code>main()</code>:</p>\n<pre><code class=\"language-text\">player\nenemy\nstage\ngold\nexperience\n</code></pre>\n<p>We have seen this problem before.</p>\n<p>Three numbers were not a player.</p>\n<p>A pile of variables in <code>main()</code> is not a game.</p>\n<p>The pieces that make up our game need one place to live, and one place that decides what happens when a player clicks.</p>\n<p>Next, we will put the game in one place.</p>\n<p><strong>Small function. Bigger game.</strong></p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Borrowing Without the Lecture.\nCanonical: https://mnixblog.netlify.app/borrowing-without-the-lecture/\nLet a Player Attack an Enemy in Rust\nOur game has a Player and an Enemy\nThen, the moment they fight, we strip them back down for parts:\nenemy.health = attack(\nenemy.health,\nplayer.base_damage,\nplayer.bonus_damage,\n);\nWe built two real game types.\nThe moment they interact, we go back to passing loose numbers around.\nIn this article, we are going to change attack() so it takes the player and the enemy themselves.\nAlong the way, Rust will refuse to compile our code three times.\nEvery time, it will be right.\nBy the end, our combat loop will say what the game actually does:\nattack(&player, &mut enemy);\nA player attacks an enemy.\nNothing gets torn apart.\nOur starting point\nYou need two structs and four small functions to follow along:\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(\nhealth: u32,\ndamage: u32,\n) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn attack(\nhealth: u32,\nbase_damage: u32,\nbonus_damage: u32,\n) -> u32 {\nlet damage =\nclick_damage(base_damage, bonus_damage);\nhealth_after_hit(health, damage)\n}\nAt the bottom of main.rs, the tests module holds three tests for the small rules and one test that calls attack() directly:\n#[test]\nfn attack_reduces_enemy_health() {\nassert_eq!(attack(10, 5, 2), 3);\n}\nWe will come back to that test once attack() changes.\nOur combat loop looks like this:\nfn main() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nwhile !is_defeated(enemy.health) {\nenemy.health = attack(\nenemy.health,\nplayer.base_damage,\nplayer.bonus_damage,\n);\nprintln!(\n\"{} health: {}\",\nenemy.name,\nenemy.health\n);\n}\nprintln!(\"{} defeated!\", enemy.name);\n}\nIt works.\nBut attack() does not know what a player is.\nIt does not know what an enemy is.\nIt takes three numbers and hands back a fourth, and main() has to put that number back where it belongs.\nWhat we want to write is closer to:\nattack(player, enemy);\nSo let us try that.\nHand the player to attack()\nLet us start with the player and leave the enemy alone for a moment.\nChange attack() so it takes a whole Player instead of two loose damage values:\nfn attack(\nplayer: Player,\nhealth: u32,\n) -> u32 {\nlet damage = click_damage(\nplayer.base_damage,\nplayer.bonus_damage,\n);\nhealth_after_hit(health, damage)\n}\nThe body barely changed.\nWe read the damage values from the player with the same dot syntax we used in main().\nNow update the call inside the loop:\nenemy.health =\nattack(player, enemy.health);\nThat reads much better.\nRun it:\ncargo run\nRust refuses to build the program.\nTrimmed down, the error says:\nerror[E0382]: use of moved value: `player`\nvalue moved here, in previous\niteration of loop\nThe full message is longer, and it suggests a few fixes, like cloning the player.\nSome of them would work.\nNone of them is what we want, so let us figure out what actually happened.\nThe first attack would have been fine.\nThe second attack is the problem.\nWhere did the player go?\nThe player moved\nIn Rust, every value has an owner.\nRight now, main() owns player.\nIt created it, so it is responsible for it.\nWhen we write:\nattack(player, enemy.health)\nand attack() asks for a Player, we are not lending the player to attack().\nWe are giving it away.\nOwnership moves into attack().\nWhen attack() finishes, the Player it owns is no longer available back in main().\nSo on the next trip around the loop, main() tries to hand over a player it no longer owns.\nRust catches that before the program ever runs.\nIn game terms, our player attacked once and then vanished.\nThat is a bold balancing decision.\nIt is not ours.\nThe attack was supposed to defeat the enemy.\nNot the player.\nWhy did health never move?\nWe have been passing enemy.health into functions all along, and it never disappeared.\nThat is because u32 implements Rust's Copy trait.\nWhen we pass a u32 by value, Rust can automatically make a copy, so the original remains available.\nOur Player does not implement Copy, so passing it by value moves ownership instead.\nYou do not need to understand traits yet.\nFor now, the useful difference is:\nu32     → copied\nPlayer  → moved\nA u32 gets photocopied.\nA Player gets handed over.\nWe do not want to give the player away.\nWe want attack() to look at it.\nTry it: this is the program with attack() taking a whole Player, and it fails to compile on purpose.\nRun it and read the error for yourself.\nYou should see error code E0382, \"use of moved value\", pointing at the call inside the loop.\nLive example · read the move error\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn attack(player: Player, health: u32) -> u32 {\nlet damage = click_damage(player.base_damage, player.bonus_damage);\nhealth_after_hit(health, damage)\n}\nfn main() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nwhile !is_defeated(enemy.health) {\nenemy.health = attack(player, enemy.health);\nprintln!(\"{} health: {}\", enemy.name, enemy.health);\n}\nprintln!(\"{} defeated!\", enemy.name);\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/borrowing-without-the-lecture/).\nThe next section fixes it with one character in two places.\nIf you want to fix it first, ask yourself what attack() needs to do with the player.\nQuestion: After attack(player, enemy.health) runs once, why can main() still use enemy.health but not player?\nFields of a struct are never moved\nu32 implements Copy, so a copy was passed, but Player does not, so it moved\nRust only tracks moves for values that contain a String\nAnswer: 2. u32 implements Copy, so a copy was passed, but Player does not, so it moved\nExplanation: Passing a u32 by value copies it. Player is not Copy, so passing it by value hands ownership to attack().\nBorrow the player with &\nRust lets a function borrow a value instead of owning it.\nAdd one character to the parameter type:\nfn attack(\nplayer: &Player,\nhealth: u32,\n) -> u32 {\nlet damage = click_damage(\nplayer.base_damage,\nplayer.bonus_damage,\n);\nhealth_after_hit(health, damage)\n}\n&Player means:\na reference to a Player that someone else owns\nA reference lets attack() reach the player without taking ownership of it.\nWhen the call finishes, the player is still sitting in main() where we left it.\nNotice that the body did not change.\nThe dot works the same way through a reference, so:\nplayer.base_damage\nstill reads the field.\nThe call needs the same &:\nenemy.health =\nattack(&player, enemy.health);\n&player says:\nlet attack() borrow the player for this call\nRun it:\ncargo run\nThe Training Slime goes down in three hits again.\nThe player survives every one of them.\nThere is a bonus hiding in that &.\nA plain & reference gives attack() read-only access to the player's fields.\nIf attack() tried to assign a new value to:\nplayer.base_damage\nRust would refuse to compile it.\nThat is exactly the promise we want.\nAn attack needs to read the player.\nIt does not need to change the player.\nQuestion: Inside fn attack(player: &Player, ...), what does the line player.base_damage = 9; do?\nChanges the caller's player, since a reference points at it\nFails to compile, because a plain & reference is read-only\nCompiles, but only changes a private copy\nAnswer: 2. Fails to compile, because a plain & reference is read-only\nExplanation: Rust rejects it with E0594. The reference lets attack() read the player, not write to it.\nNow the enemy\nThe player is solved.\nThe enemy is still split apart:\nenemy.health =\nattack(&player, enemy.health);\nattack() gets a copy of the enemy's health, calculates a new number, and returns it.\nThen main() writes that number back into the enemy.\nWhat we really want is for attack() to change the enemy itself.\nWe just learned that borrowing keeps a value where it is.\nLet us borrow the enemy too:\nfn attack(\nplayer: &Player,\nenemy: &Enemy,\n) {\nlet damage = click_damage(\nplayer.base_damage,\nplayer.bonus_damage,\n);\nenemy.health =\nhealth_after_hit(enemy.health, damage);\n}\nTwo things changed besides the enemy parameter.\nThere is no:\n-> u32\nanymore.\nattack() is not handing back a number for someone else to store.\nThe result lives in the enemy.\nInside the function, we assign the new health directly to:\nenemy.health\nUpdate the call in main():\nattack(&player, &enemy);\nRun it:\ncargo run\nRust refuses again:\nerror[E0594]: cannot assign to\n`enemy.health`, which is behind\na `&` reference\nYou may also see a warning that enemy does not need to be mutable.\nIgnore it for a moment.\nWe are about to need that mut again.\nThis is the promise from the last section working against us.\n&Enemy lets us look at the enemy.\nWe did not come here to look.\nA plain & reference is read-only.\nThat was perfect for the player.\nIt is wrong for the enemy because the entire point of an attack is that the enemy's health changes.\nBorrow the enemy with &mut\nWe have already met mut.\nIt is how we told Rust that our enemy is allowed to change.\nReferences can express the same idea.\nChange:\n&Enemy\nto:\n&mut Enemy\nOur attack becomes:\nfn attack(\nplayer: &Player,\nenemy: &mut Enemy,\n) {\nlet damage = click_damage(\nplayer.base_damage,\nplayer.bonus_damage,\n);\nenemy.health =\nhealth_after_hit(enemy.health, damage);\n}\n&mut Enemy means:\na mutable reference to an Enemy that someone else owns\nIn game terms:\nattack() may temporarily change this enemy, but it does not own the enemy.\nThe call needs to ask for the same thing:\nattack(&player, &mut enemy);\nThis only works because enemy was already declared with:\nlet mut enemy\nRust will not let us borrow something mutably if the owner never agreed that it could change.\nRun it:\ncargo run\nYou should see:\nTraining Slime health: 13\nTraining Slime health: 6\nTraining Slime health: 0\nTraining Slime defeated!\nThe same fight as before.\nBut now the signature tells the whole story:\n&Player     → attack reads the player\n&mut Enemy  → attack changes the enemy\nNobody gets consumed.\nNobody gets torn apart.\nBoth are still in main(), ready for the next attack.\nTry it: here is a small version of the fixed program that attacks twice.\nRun it first.\nYou should see Training Slime health: 13, then Training Slime health: 6.\nThen break it on purpose, one change at a time, and read each error:\nchange enemy: &mut Enemy to enemy: &Enemy in attack() and you get E0594, the same error as before\nput enemy: &mut Enemy back, then remove mut from let mut enemy and you get E0596\nLive example · try breaking the borrow\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage, player.bonus_damage);\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nattack(&player, &mut enemy);\nprintln!(\"{} health: {}\", enemy.name, enemy.health);\nattack(&player, &mut enemy);\nprintln!(\"{} health: {}\", enemy.name, enemy.health);\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/borrowing-without-the-lecture/).\nQuestion: Why does attack(&player, &mut enemy) need let mut enemy?\nBecause &mut makes Rust copy the enemy first\nBecause Rust will not lend out a mutable borrow of a value its owner never declared changeable\nIt does not; &mut enemy works on any variable\nAnswer: 2. Because Rust will not lend out a mutable borrow of a value its owner never declared changeable\nExplanation: Without mut on the variable, the borrow fails with E0596.\nOne rule worth knowing now\nThere is one borrowing rule you will run into early, so here it is in game terms.\nSuppose that, instead of the combat loop, main() grabs a reference to the enemy's name before attacking:\nlet name = &enemy.name;\nattack(&player, &mut enemy);\nprintln!(\"{name} took a hit!\");\nRust refuses for the third time:\nerror[E0502]: cannot borrow `enemy`\nas mutable because it is also\nborrowed as immutable\nA borrow remains active until its last use.\nname is used by the println!() after the attack, so that borrow is still active when attack() asks for permission to change the enemy.\nRust will not hand out a mutable borrow while that reading borrow is still being used.\nOur combat loop never hits this problem because each borrow is finished before the next conflicting access begins:\nattack(&player, &mut enemy);\nprintln!(\n\"{} health: {}\",\nenemy.name,\nenemy.health\n);\nIn this case, the mutable borrow is finished when attack() returns.\nThen println!() is free to read the enemy.\nThat is the beginner version of the rule:\nFor the same data at the same time: many readers, or one writer, but not both.\nYou do not need more than that today.\nQuestion: Why does Rust reject reading name = &enemy.name before attack(&player, &mut enemy) when name is printed afterward?\nA & borrow can never be followed by a &mut borrow of the same value\nThe & borrow is still in use when attack() asks for &mut, so a reader and a writer overlap\nattack() takes ownership of enemy\nAnswer: 2. The & borrow is still in use when attack() asks for &mut, so a reader and a writer overlap\nExplanation: A borrow lasts until its last use. If nothing used name after the attack, the program would compile.\nCoding challenge: End the borrow in time\nThis starter fails to compile on purpose, because the lesson here is the error.\nFix it without changing attack(). Keep the names attack and main and the shape of Player and Enemy, because the checks call them. Press Check my code and it will tell you how you are doing.\nExpected result: it compiles and prints Training Slime took a hit! Health: 13.\nFix the borrow conflict\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage, player.bonus_damage);\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\n// TODO: this borrow is still in use after the attack. Make the program compile.\nlet name = &enemy.name;\nattack(&player, &mut enemy);\nprintln!(\"{name} took a hit! Health: {}\", enemy.health);\n}\n<details>\n<summary>Spoiler: the checks for this challenge</summary>\nThese tests run when you press Check my code on the original article. Reading them first gives away the answer.\n/// It compiles now, and the program still runs from start to finish.\n#[test]\nfn main_still_runs() {\nmain();\n}\n/// It compiles, but attack() no longer does its job. We only wanted to move the borrow, not the damage.\n#[test]\nfn attack_still_lands() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 13);\n}\n</details>\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/borrowing-without-the-lecture/).\nTip:\nThe error is E0502. A borrow ends at its last use, so ask where name needs to start borrowing.\nUpdate the test\nOur attack() test still passes three loose numbers, so it needs the new signature:\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 10,\n};\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 3);\n}\nThe rule it checks has not changed.\nAn attack with five base damage and two bonus damage takes ten health down to three.\nWhat changed is how the test says it.\nIt builds a player.\nIt builds an enemy.\nIt lets one attack the other.\nThen it checks the enemy afterward.\nThat reads like the game.\nThe other three tests do not touch attack(), so they stay exactly as they were.\nRun them:\ncargo test\nAll four should pass.\nWe do not need a separate test proving that attack() does not change the player.\nWith our current Player type, an &Player reference does not allow attack() to assign new values to the player's fields.\nThe compiler checks that for us every time we build.\nThe complete program\nIf you have been following along in your editor, here is everything in one place.\nAt this point, src/main.rs should look like this:\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(\nhealth: u32,\ndamage: u32,\n) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn attack(\nplayer: &Player,\nenemy: &mut Enemy,\n) {\nlet damage = click_damage(\nplayer.base_damage,\nplayer.bonus_damage,\n);\nenemy.health =\nhealth_after_hit(enemy.health, damage);\n}\nfn main() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nwhile !is_defeated(enemy.health) {\nattack(&player, &mut enemy);\nprintln!(\n\"{} health: {}\",\nenemy.name,\nenemy.health\n);\n}\nprintln!(\"{} defeated!\", enemy.name);\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(\nhealth_after_hit(3, 10),\n0\n);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 10,\n};\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 3);\n}\n}\nRun both:\ncargo run\ncargo test\nThe Training Slime should fall in three hits, and all four tests should pass.\nWhat we learned\nAlong the way, we encountered:\nownership\nmoves\nCopy values\n&T references\n&mut T references\nfunctions that change a value instead of returning a new one\nwhen borrows stop being used\nwhy Rust prevents conflicting access\nYou do not need to memorize all of that yet.\nThe important change is what our attack says.\nWe started with:\nenemy.health = attack(\nenemy.health,\nplayer.base_damage,\nplayer.bonus_damage,\n);\nWe ended with:\nattack(&player, &mut enemy);\nThe player is read.\nThe enemy is changed.\nBoth stay where they belong.\nAnd the compiler now checks that attack() keeps its side of that deal.\nThere is a bigger shape missing\nLook at what main() is holding now:\nplayer\nenemy\nthe combat loop\nThat is a fight.\nIt is not a game.\nWhen the Training Slime dies, the program simply ends.\nThere is no next enemy.\nThere is no stage counter.\nNobody earns anything for winning.\nAnd every one of those things would become another loose variable inside main():\nplayer\nenemy\nstage\ngold\nexperience\nWe have seen this problem before.\nThree numbers were not a player.\nA pile of variables in main() is not a game.\nThe pieces that make up our game need one place to live, and one place that decides what happens when a player clicks.\nNext, we will put the game in one place.\nSmall function. Bigger game.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/borrowing-without-the-lecture/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2026-10-16T01:15:30Z",
      "id": "https://mnixblog.netlify.app/borrowing-without-the-lecture/",
      "image": "https://mnixblog.netlify.app/images/borrowing-without-the-lecture/cover.jpg",
      "summary": "Learn Rust ownership and borrowing by fixing three real compiler errors. Let a player attack an enemy with &Player and &mut Enemy, without a lecture.",
      "tags": [
        "Rust",
        "WebAssembly",
        "Game Development",
        "Beginners"
      ],
      "title": "Borrowing Without the Lecture.",
      "url": "https://mnixblog.netlify.app/borrowing-without-the-lecture/"
    },
    {
      "content_html": "<p>Look at this line:</p>\n<pre><code class=\"language-rust\">attack(enemy_health, 5, 2)\n</code></pre>\n<p>Three values. Two belong to the player. One belongs to the enemy.</p>\n<p>Nothing in the code says which is which.</p>\n<p>In this article, we are going to fix that by grouping those values into two new Rust types: a <code>Player</code> and an <code>Enemy</code>.</p>\n<p>By the end, our code will start describing the things that actually exist in our game instead of asking us to remember what those numbers represent.</p>\n<hr />\n<h2 id=\"our-starting-point\">Our starting point<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#our-starting-point\" aria-label=\"Link to this section\">#</a></h2>\n<p>You only need four small functions to follow along:</p>\n<pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(\n    health: u32,\n    damage: u32,\n) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn attack(\n    health: u32,\n    base_damage: u32,\n    bonus_damage: u32,\n) -&gt; u32 {\n    let damage =\n        click_damage(base_damage, bonus_damage);\n\n    health_after_hit(health, damage)\n}\n</code></pre>\n<p>Our combat loop currently looks like this:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let mut enemy_health = 20;\n\n    while !is_defeated(enemy_health) {\n        enemy_health =\n            attack(enemy_health, 5, 2);\n\n        println!(\n            \"Enemy health: {enemy_health}\"\n        );\n    }\n\n    println!(\"Enemy defeated!\");\n}\n</code></pre>\n<p>This code works.</p>\n<p>The problem is not what it does.</p>\n<p>The problem is what it says.</p>\n<hr />\n<h2 id=\"three-numbers-with-three-different-meanings\">Three numbers with three different meanings<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#three-numbers-with-three-different-meanings\" aria-label=\"Link to this section\">#</a></h2>\n<p>Look at the attack again:</p>\n<pre><code class=\"language-rust\">attack(enemy_health, 5, 2)\n</code></pre>\n<p>The first value represents the enemy.</p>\n<p>The other two belong to the player.</p>\n<p>But nothing in the function call makes those relationships clear.</p>\n<p>Someone reading this code has to remember:</p>\n<pre><code class=\"language-text\">enemy_health → the enemy's current health\n5            → the player's base damage\n2            → the player's bonus damage\n</code></pre>\n<p>That is manageable today.</p>\n<p>It will become harder when our player also has:</p>\n<ul>\n<li>\n<p>a level</p>\n</li>\n<li>\n<p>experience</p>\n</li>\n<li>\n<p>gold</p>\n</li>\n<li>\n<p>upgrade levels</p>\n</li>\n<li>\n<p>passive damage</p>\n</li>\n</ul>\n<p>The enemy will need more information too:</p>\n<ul>\n<li>\n<p>a name</p>\n</li>\n<li>\n<p>maximum health</p>\n</li>\n<li>\n<p>gold rewards</p>\n</li>\n<li>\n<p>experience rewards</p>\n</li>\n</ul>\n<p>Passing every single value as an individual number just isn't going to scale.</p>\n<p>We need a way to tell Rust which values belong together.</p>\n<hr />\n<h2 id=\"give-the-player-some-shape\">Give the player some shape<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#give-the-player-some-shape\" aria-label=\"Link to this section\">#</a></h2>\n<p>Rust lets us create our own types with <code>struct</code>.</p>\n<p>Add this near the top of <code>main.rs</code>:</p>\n<pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n</code></pre>\n<p>A <strong>struct</strong> is a type that groups related values together.</p>\n<p>Our new type is called:</p>\n<pre><code class=\"language-text\">Player\n</code></pre>\n<p>It contains two named values:</p>\n<pre><code class=\"language-text\">base_damage\nbonus_damage\n</code></pre>\n<p>The named values inside a struct are called <strong>fields</strong>.</p>\n<p>Each field has a type. The colon syntax is the same as a function parameter—it connects the name to the type:</p>\n<pre><code class=\"language-rust\">base_damage: u32\nbonus_damage: u32\n</code></pre>\n<p>So every <code>Player</code> has a field named <code>base_damage</code> that contains a <code>u32</code>, and a field named <code>bonus_damage</code> that also contains a <code>u32</code>.</p>\n<p>The struct definition describes the shape of a player.</p>\n<p>It does not create one yet.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>After <code>struct Player { base_damage: u32, bonus_damage: u32 }</code>, how many players exist in the running program?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>One, with both damage fields set to <code>0</code></li>\n<li>None, because the definition only describes the shape of a player</li>\n<li>One, and it is mutable</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. None, because the definition only describes the shape of a player</p>\n<p>A <code>struct</code> definition is a blueprint. A player exists only once we write a value such as <code>Player { base_damage: 5, bonus_damage: 2 }</code>.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"create-a-player\">Create a player<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#create-a-player\" aria-label=\"Link to this section\">#</a></h2>\n<p>Now that Rust knows what a <code>Player</code> looks like, we can create one:</p>\n<pre><code class=\"language-rust\">let player = Player {\n    base_damage: 5,\n    bonus_damage: 2,\n};\n</code></pre>\n<p>This creates a value with a type of: <code>Player</code>.</p>\n<p>The values line up with the fields from our definition:</p>\n<pre><code class=\"language-text\">Player\n├── base_damage: 5\n└── bonus_damage: 2\n</code></pre>\n<p>We can access a field with a dot:</p>\n<pre><code class=\"language-rust\">player.base_damage\n</code></pre>\n<p>That gives us the player's base damage.</p>\n<p>We can do the same for bonus damage:</p>\n<pre><code class=\"language-rust\">player.bonus_damage\n</code></pre>\n<p>Our attack call can now become:</p>\n<pre><code class=\"language-rust\">attack(\n    enemy_health,\n    player.base_damage,\n    player.bonus_damage,\n)\n</code></pre>\n<p>That does happen to be longer than:</p>\n<pre><code class=\"language-rust\">attack(enemy_health, 5, 2)\n</code></pre>\n<p>But it is also clearer.</p>\n<p>We no longer have to remember what <code>5</code> and <code>2</code> represent.</p>\n<p>The code tells us.</p>\n<p>Try it: this builds a <code>Player</code> and attacks an enemy with 20 health. It should print <code>Enemy health: 13</code>. Change <code>bonus_damage</code> to <code>10</code> and predict the new number, then add a third field of your own to <code>Player</code> and see what the compiler asks of you.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try changing the player</figcaption><pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn attack(health: u32, base_damage: u32, bonus_damage: u32) -&gt; u32 {\n    let damage = click_damage(base_damage, bonus_damage);\n\n    health_after_hit(health, damage)\n}\n\nfn main() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let enemy_health = 20;\n    let remaining = attack(\n        enemy_health,\n        player.base_damage,\n        player.bonus_damage,\n    );\n\n    println!(&quot;Enemy health: {remaining}&quot;);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/\">the original article</a>.</p></figure>\n<hr />\n<h2 id=\"give-the-enemy-some-shape\">Give the enemy some shape<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#give-the-enemy-some-shape\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our enemy is still represented by one loose variable:</p>\n<pre><code class=\"language-rust\">let mut enemy_health = 20;\n</code></pre>\n<p>Health is not the only thing an enemy needs.</p>\n<p>Even giving it a name creates another related value.</p>\n<p>Add another struct:</p>\n<pre><code class=\"language-rust\">struct Enemy {\n    name: String,\n    health: u32,\n}\n</code></pre>\n<p>Our new <code>Enemy</code> type contains two fields:</p>\n<pre><code class=\"language-text\">Enemy\n├── name\n└── health\n</code></pre>\n<p>We already know the type of <code>health</code>:</p>\n<pre><code class=\"language-rust\">health: u32\n</code></pre>\n<p>The name introduces a new type:</p>\n<pre><code class=\"language-rust\">name: String\n</code></pre>\n<hr />\n<h2 id=\"give-the-enemy-a-name\">Give the enemy a name<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#give-the-enemy-a-name\" aria-label=\"Link to this section\">#</a></h2>\n<p><code>String</code> is a Rust type for storing owned text.</p>\n<p>That fits our enemy: it should keep its own name for as long as it exists.</p>\n<p>We can create a <code>String</code> from text like this:</p>\n<pre><code class=\"language-rust\">String::from(\"Training Slime\")\n</code></pre>\n<p>For now, read that as:</p>\n<blockquote>\n<p>Create a <code>String</code> containing \"Training Slime.\"</p>\n</blockquote>\n<p>Our game only needs enough text to name an enemy.</p>\n<p>Now we can create one:</p>\n<pre><code class=\"language-rust\">let enemy = Enemy {\n    name: String::from(\"Training Slime\"),\n    health: 20,\n};\n</code></pre>\n<p>Rust now understands that the name and health belong to the same enemy.</p>\n<hr />\n<h2 id=\"the-enemy-still-needs-to-change\">The enemy still needs to change<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#the-enemy-still-needs-to-change\" aria-label=\"Link to this section\">#</a></h2>\n<p>There is one problem with our new enemy:</p>\n<pre><code class=\"language-rust\">let enemy = Enemy {\n    name: String::from(\"Training Slime\"),\n    health: 20,\n};\n</code></pre>\n<p>Combat changes health.</p>\n<p>But <code>enemy</code> is immutable by default.</p>\n<p>If we try to assign a new value to:</p>\n<pre><code class=\"language-rust\">enemy.health\n</code></pre>\n<p>Rust will refuse to compile the program.</p>\n<p>We have already solved this problem once.</p>\n<p>The enemy needs to be mutable:</p>\n<pre><code class=\"language-rust\">let mut enemy = Enemy {\n    name: String::from(\"Training Slime\"),\n    health: 20,\n};\n</code></pre>\n<p>The <code>mut</code> applies to the <code>enemy</code> value, which allows us to change its fields:</p>\n<pre><code class=\"language-rust\">enemy.health = 13;\n</code></pre>\n<p>We are not adding mutability everywhere.</p>\n<p>The player does not need to change yet:</p>\n<pre><code class=\"language-rust\">let player = Player {\n</code></pre>\n<p>The enemy does:</p>\n<pre><code class=\"language-rust\">let mut enemy = Enemy {\n</code></pre>\n<p>Once again, the game tells us which values need to be mutable.</p>\n<p>Try it: this program changes an enemy's health. It should print <code>Training Slime health: 13</code>. Now delete the word <code>mut</code> and run it again. Rust refuses to compile it and tells you it cannot assign to <code>enemy.health</code>, because <code>enemy</code> is not declared as mutable.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try removing mut</figcaption><pre><code class=\"language-rust\">struct Enemy {\n    name: String,\n    health: u32,\n}\n\nfn main() {\n    let mut enemy = Enemy {\n        name: String::from(&quot;Training Slime&quot;),\n        health: 20,\n    };\n\n    enemy.health = 13;\n\n    println!(&quot;{} health: {}&quot;, enemy.name, enemy.health);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/\">the original article</a>.</p></figure>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does the combat code write <code>let mut enemy</code> but plain <code>let player</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Rust requires <code>mut</code> on whichever struct is declared second</li>\n<li>Structs holding a <code>String</code> must always be <code>mut</code></li>\n<li>Combat changes <code>enemy.health</code>, while nothing changes the player</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: C. Combat changes <code>enemy.health</code>, while nothing changes the player</p>\n<p>Values are immutable by default. We add <code>mut</code> only where the game actually reassigns a field, which is the enemy's health.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"put-the-player-and-enemy-into-combat\">Put the player and enemy into combat<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#put-the-player-and-enemy-into-combat\" aria-label=\"Link to this section\">#</a></h2>\n<p>We can now update <code>main()</code>:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let mut enemy = Enemy {\n        name: String::from(\"Training Slime\"),\n        health: 20,\n    };\n\n    while !is_defeated(enemy.health) {\n        enemy.health = attack(\n            enemy.health,\n            player.base_damage,\n            player.bonus_damage,\n        );\n\n        println!(\n            \"{} health: {}\",\n            enemy.name,\n            enemy.health\n        );\n    }\n\n    println!(\"{} defeated!\", enemy.name);\n}\n</code></pre>\n<p>There is one small change in our <code>println!()</code> calls.</p>\n<p>The <code>{}</code> placeholders are filled from left to right by the values that follow the format string:</p>\n<pre><code class=\"language-rust\">println!(\n    \"{} health: {}\",\n    enemy.name,\n    enemy.health\n);\n</code></pre>\n<p>The first <code>{}</code> displays <code>enemy.name</code>.</p>\n<p>The second displays <code>enemy.health</code>.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>In <code>println!(\"{} health: {}\", enemy.name, enemy.health);</code>, which value fills the first <code>{}</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>enemy.name</code>, because the placeholders are filled left to right</li>\n<li><code>enemy.health</code>, because numbers are always printed first</li>\n<li>Whichever field is declared first in the <code>Enemy</code> struct</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. <code>enemy.name</code>, because the placeholders are filled left to right</p>\n<p>The values after the format string fill the <code>{}</code> placeholders in the order you write them, not in the order the struct declares its fields.</p>\n</div>\n</div>\n</section>\n<p>Run the game:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>You should see:</p>\n<pre><code class=\"language-text\">Training Slime health: 13\nTraining Slime health: 6\nTraining Slime health: 0\nTraining Slime defeated!\n</code></pre>\n<p>The combat still works.</p>\n<p>But the code now contains an actual player and enemy.</p>\n<p><strong>Coding challenge: Pay the player</strong></p>\n<p>Enemies should be worth something. Add a <code>gold_reward: u32</code> field to <code>Enemy</code>, give the Training Slime a reward of <code>5</code>, and print it using the field instead of the placeholder <code>0</code>. Keep the field name <code>gold_reward</code>, and press <strong>Check my code</strong> to see how you are doing.</p>\n<p><strong>Expected result:</strong> the program prints <code>Training Slime is worth 5 gold</code>. As written, the starter prints <code>Training Slime is worth 0 gold</code>.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Give the enemy a reward</figcaption><pre><code class=\"language-rust\">struct Enemy {\n    name: String,\n    health: u32,\n    // TODO: add a gold_reward field of type u32\n}\n\nfn main() {\n    let enemy = Enemy {\n        name: String::from(&quot;Training Slime&quot;),\n        health: 20,\n        // TODO: give the Training Slime a gold_reward of 5\n    };\n\n    // TODO: replace the 0 with enemy.gold_reward\n    println!(&quot;{} is worth {} gold&quot;, enemy.name, 0);\n    println!(&quot;It has {} health&quot;, enemy.health);\n}\n\n</code></pre><details class=\"ob-spoiler\"><summary>Spoiler: the checks for this challenge</summary><p>These tests run when you press Check my code on the original article. Reading them first gives away the answer.</p><pre><code class=\"language-rust\">/// `Enemy` needs a field called `gold_reward`, and it should hold a `u32`. Check the name and the type.\n#[test]\nfn enemy_has_a_u32_gold_reward() {\n    let enemy = Enemy {\n        name: String::from(&quot;Test Slime&quot;),\n        health: 1,\n        gold_reward: 7u32,\n    };\n    let reward: u32 = enemy.gold_reward;\n    assert_eq!(reward, 7);\n}\n\n/// Adding a field should not cost us the old ones. `name` and `health` still belong on `Enemy`.\n#[test]\nfn old_fields_survive() {\n    let enemy = Enemy {\n        name: String::from(&quot;Test Slime&quot;),\n        health: 9,\n        gold_reward: 1,\n    };\n    assert_eq!(enemy.name, &quot;Test Slime&quot;);\n    assert_eq!(enemy.health, 9);\n}\n</code></pre></details><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>A new field needs two edits: its name and type in the <code>struct</code> definition, and a value wherever you create an <code>Enemy</code>. Then read it with a dot, like <code>enemy.health</code>.</p>\n</aside>\n<hr />\n<h2 id=\"notice-what-the-structs-made-visible\">Notice what the structs made visible<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#notice-what-the-structs-made-visible\" aria-label=\"Link to this section\">#</a></h2>\n<p>Before, our game looked like this:</p>\n<pre><code class=\"language-rust\">let mut enemy_health = 20;\n\nenemy_health =\n    attack(enemy_health, 5, 2);\n</code></pre>\n<p>Now it looks like this:</p>\n<pre><code class=\"language-rust\">let player = Player {\n    base_damage: 5,\n    bonus_damage: 2,\n};\n\nlet mut enemy = Enemy {\n    name: String::from(\"Training Slime\"),\n    health: 20,\n};\n\nenemy.health = attack(\n    enemy.health,\n    player.base_damage,\n    player.bonus_damage,\n);\n</code></pre>\n<p>The behavior is the same.</p>\n<p>The meaning is not.</p>\n<p>Before:</p>\n<pre><code class=\"language-text\">20\n5\n2\n</code></pre>\n<p>Now:</p>\n<pre><code class=\"language-text\">Player\n├── base_damage\n└── bonus_damage\n\nEnemy\n├── name\n└── health\n</code></pre>\n<p>We gave related values names and grouped them into types.</p>\n<p>Rust can now help us keep player data with the player and enemy data with the enemy.</p>\n<hr />\n<h2 id=\"the-behavior-did-not-change\">The behavior did not change<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#the-behavior-did-not-change\" aria-label=\"Link to this section\">#</a></h2>\n<p>Notice what we did not touch:</p>\n<pre><code class=\"language-rust\">click_damage()\n</code></pre>\n<p>still calculates damage.</p>\n<pre><code class=\"language-rust\">health_after_hit()\n</code></pre>\n<p>still reduces health.</p>\n<pre><code class=\"language-rust\">is_defeated()\n</code></pre>\n<p>still checks for defeat.</p>\n<pre><code class=\"language-rust\">attack()\n</code></pre>\n<p>still combines the smaller rules.</p>\n<p>We changed how the application organizes its data.</p>\n<p>We did not rewrite the behavior we already trust.</p>\n<p>Run the tests:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>All four should still pass.</p>\n<p>No new combat rule was added in this article, so we do not need to invent a new behavior test just to prove that a struct can hold the values we typed into it.</p>\n<p>The compiler checks that our struct fields exist and contain the expected types.</p>\n<p>Our existing tests continue checking the game rules.</p>\n<hr />\n<h2 id=\"the-complete-program\">The complete program<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#the-complete-program\" aria-label=\"Link to this section\">#</a></h2>\n<p>At this point, <code>src/main.rs</code> should look like this:</p>\n<pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(\n    health: u32,\n    damage: u32,\n) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn attack(\n    health: u32,\n    base_damage: u32,\n    bonus_damage: u32,\n) -&gt; u32 {\n    let damage =\n        click_damage(base_damage, bonus_damage);\n\n    health_after_hit(health, damage)\n}\n\nfn main() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let mut enemy = Enemy {\n        name: String::from(\"Training Slime\"),\n        health: 20,\n    };\n\n    while !is_defeated(enemy.health) {\n        enemy.health = attack(\n            enemy.health,\n            player.base_damage,\n            player.bonus_damage,\n        );\n\n        println!(\n            \"{} health: {}\",\n            enemy.name,\n            enemy.health\n        );\n    }\n\n    println!(\"{} defeated!\", enemy.name);\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(\n            health_after_hit(3, 10),\n            0\n        );\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        assert_eq!(attack(10, 5, 2), 3);\n    }\n}\n</code></pre>\n<p>Run both:</p>\n<pre><code class=\"language-bash\">cargo run\ncargo test\n</code></pre>\n<p>The game should defeat the Training Slime, and all four tests should pass.</p>\n<hr />\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Along the way, we encountered:</p>\n<ul>\n<li>\n<p><code>struct</code></p>\n</li>\n<li>\n<p>custom Rust types</p>\n</li>\n<li>\n<p>struct fields</p>\n</li>\n<li>\n<p>creating struct values</p>\n</li>\n<li>\n<p>field access with <code>.</code></p>\n</li>\n<li>\n<p><code>String</code></p>\n</li>\n<li>\n<p>mutable struct values</p>\n</li>\n</ul>\n<p>You do not need to memorize all of that yet.</p>\n<p>The important change is what happened to our data.</p>\n<p>We started with:</p>\n<pre><code class=\"language-text\">enemy_health\n5\n2\n</code></pre>\n<p>We turned those values into:</p>\n<pre><code class=\"language-text\">Player\nEnemy\n</code></pre>\n<p>Our program now has a vocabulary that matches the game we are building.</p>\n<hr />\n<h2 id=\"there-is-still-something-awkward\">There is still something awkward<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#there-is-still-something-awkward\" aria-label=\"Link to this section\">#</a></h2>\n<p>We solved one problem.</p>\n<p>But look at our attack again:</p>\n<pre><code class=\"language-rust\">enemy.health = attack(\n    enemy.health,\n    player.base_damage,\n    player.bonus_damage,\n);\n</code></pre>\n<p>We created a <code>Player</code>.</p>\n<p>We created an <code>Enemy</code>.</p>\n<p>Then, the moment they need to interact, we pull them apart into individual fields.</p>\n<p><code>attack()</code> does not accept a player.</p>\n<p>It does not accept an enemy.</p>\n<p>It still accepts three numbers and returns another number that we manually assign back to the enemy.</p>\n<p>What we really want to express is:</p>\n<pre><code class=\"language-text\">player attacks enemy\n</code></pre>\n<p>That means our attack needs to:</p>\n<pre><code class=\"language-text\">read the player\nchange the enemy\nleave both available for the next attack\n</code></pre>\n<p>Rust has a way to let a function temporarily use values without taking them away from the code that already owns them.</p>\n<p>That is called <strong>borrowing</strong>.</p>\n<p>Next, we will let our player attack an enemy without consuming either one.</p>\n<p><strong>Small function. Bigger game.</strong></p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Three Numbers Are Not a Player.\nCanonical: https://mnixblog.netlify.app/three-numbers-are-not-a-player/\nGive Your Game Data Shape With Rust Structs\nLook at this line:\nattack(enemy_health, 5, 2)\nThree values. Two belong to the player. One belongs to the enemy.\nNothing in the code says which is which.\nIn this article, we are going to fix that by grouping those values into two new Rust types: a Player and an Enemy.\nBy the end, our code will start describing the things that actually exist in our game instead of asking us to remember what those numbers represent.\nOur starting point\nYou only need four small functions to follow along:\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(\nhealth: u32,\ndamage: u32,\n) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn attack(\nhealth: u32,\nbase_damage: u32,\nbonus_damage: u32,\n) -> u32 {\nlet damage =\nclick_damage(base_damage, bonus_damage);\nhealth_after_hit(health, damage)\n}\nOur combat loop currently looks like this:\nfn main() {\nlet mut enemy_health = 20;\nwhile !is_defeated(enemy_health) {\nenemy_health =\nattack(enemy_health, 5, 2);\nprintln!(\n\"Enemy health: {enemy_health}\"\n);\n}\nprintln!(\"Enemy defeated!\");\n}\nThis code works.\nThe problem is not what it does.\nThe problem is what it says.\nThree numbers with three different meanings\nLook at the attack again:\nattack(enemy_health, 5, 2)\nThe first value represents the enemy.\nThe other two belong to the player.\nBut nothing in the function call makes those relationships clear.\nSomeone reading this code has to remember:\nenemy_health → the enemy's current health\n5            → the player's base damage\n2            → the player's bonus damage\nThat is manageable today.\nIt will become harder when our player also has:\na level\nexperience\ngold\nupgrade levels\npassive damage\nThe enemy will need more information too:\na name\nmaximum health\ngold rewards\nexperience rewards\nPassing every single value as an individual number just isn't going to scale.\nWe need a way to tell Rust which values belong together.\nGive the player some shape\nRust lets us create our own types with struct.\nAdd this near the top of main.rs:\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nA struct is a type that groups related values together.\nOur new type is called:\nPlayer\nIt contains two named values:\nbase_damage\nbonus_damage\nThe named values inside a struct are called fields.\nEach field has a type. The colon syntax is the same as a function parameter—it connects the name to the type:\nbase_damage: u32\nbonus_damage: u32\nSo every Player has a field named base_damage that contains a u32, and a field named bonus_damage that also contains a u32.\nThe struct definition describes the shape of a player.\nIt does not create one yet.\nQuestion: After struct Player { base_damage: u32, bonus_damage: u32 }, how many players exist in the running program?\nOne, with both damage fields set to 0\nNone, because the definition only describes the shape of a player\nOne, and it is mutable\nAnswer: 2. None, because the definition only describes the shape of a player\nExplanation: A struct definition is a blueprint. A player exists only once we write a value such as Player { base_damage: 5, bonus_damage: 2 }.\nCreate a player\nNow that Rust knows what a Player looks like, we can create one:\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nThis creates a value with a type of: Player.\nThe values line up with the fields from our definition:\nPlayer\n├── base_damage: 5\n└── bonus_damage: 2\nWe can access a field with a dot:\nplayer.base_damage\nThat gives us the player's base damage.\nWe can do the same for bonus damage:\nplayer.bonus_damage\nOur attack call can now become:\nattack(\nenemy_health,\nplayer.base_damage,\nplayer.bonus_damage,\n)\nThat does happen to be longer than:\nattack(enemy_health, 5, 2)\nBut it is also clearer.\nWe no longer have to remember what 5 and 2 represent.\nThe code tells us.\nTry it: this builds a Player and attacks an enemy with 20 health. It should print Enemy health: 13. Change bonus_damage to 10 and predict the new number, then add a third field of your own to Player and see what the compiler asks of you.\nLive example · try changing the player\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn attack(health: u32, base_damage: u32, bonus_damage: u32) -> u32 {\nlet damage = click_damage(base_damage, bonus_damage);\nhealth_after_hit(health, damage)\n}\nfn main() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet enemy_health = 20;\nlet remaining = attack(\nenemy_health,\nplayer.base_damage,\nplayer.bonus_damage,\n);\nprintln!(\"Enemy health: {remaining}\");\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/three-numbers-are-not-a-player/).\nGive the enemy some shape\nOur enemy is still represented by one loose variable:\nlet mut enemy_health = 20;\nHealth is not the only thing an enemy needs.\nEven giving it a name creates another related value.\nAdd another struct:\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nOur new Enemy type contains two fields:\nEnemy\n├── name\n└── health\nWe already know the type of health:\nhealth: u32\nThe name introduces a new type:\nname: String\nGive the enemy a name\nString is a Rust type for storing owned text.\nThat fits our enemy: it should keep its own name for as long as it exists.\nWe can create a String from text like this:\nString::from(\"Training Slime\")\nFor now, read that as:\nCreate a String containing \"Training Slime.\"\nOur game only needs enough text to name an enemy.\nNow we can create one:\nlet enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nRust now understands that the name and health belong to the same enemy.\nThe enemy still needs to change\nThere is one problem with our new enemy:\nlet enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nCombat changes health.\nBut enemy is immutable by default.\nIf we try to assign a new value to:\nenemy.health\nRust will refuse to compile the program.\nWe have already solved this problem once.\nThe enemy needs to be mutable:\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nThe mut applies to the enemy value, which allows us to change its fields:\nenemy.health = 13;\nWe are not adding mutability everywhere.\nThe player does not need to change yet:\nlet player = Player {\nThe enemy does:\nlet mut enemy = Enemy {\nOnce again, the game tells us which values need to be mutable.\nTry it: this program changes an enemy's health. It should print Training Slime health: 13. Now delete the word mut and run it again. Rust refuses to compile it and tells you it cannot assign to enemy.health, because enemy is not declared as mutable.\nLive example · try removing mut\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nfn main() {\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nenemy.health = 13;\nprintln!(\"{} health: {}\", enemy.name, enemy.health);\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/three-numbers-are-not-a-player/).\nQuestion: Why does the combat code write let mut enemy but plain let player?\nRust requires mut on whichever struct is declared second\nStructs holding a String must always be mut\nCombat changes enemy.health, while nothing changes the player\nAnswer: 3. Combat changes enemy.health, while nothing changes the player\nExplanation: Values are immutable by default. We add mut only where the game actually reassigns a field, which is the enemy's health.\nPut the player and enemy into combat\nWe can now update main():\nfn main() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nwhile !is_defeated(enemy.health) {\nenemy.health = attack(\nenemy.health,\nplayer.base_damage,\nplayer.bonus_damage,\n);\nprintln!(\n\"{} health: {}\",\nenemy.name,\nenemy.health\n);\n}\nprintln!(\"{} defeated!\", enemy.name);\n}\nThere is one small change in our println!() calls.\nThe {} placeholders are filled from left to right by the values that follow the format string:\nprintln!(\n\"{} health: {}\",\nenemy.name,\nenemy.health\n);\nThe first {} displays enemy.name.\nThe second displays enemy.health.\nQuestion: In println!(\"{} health: {}\", enemy.name, enemy.health);, which value fills the first {}?\nenemy.name, because the placeholders are filled left to right\nenemy.health, because numbers are always printed first\nWhichever field is declared first in the Enemy struct\nAnswer: 1. enemy.name, because the placeholders are filled left to right\nExplanation: The values after the format string fill the {} placeholders in the order you write them, not in the order the struct declares its fields.\nRun the game:\ncargo run\nYou should see:\nTraining Slime health: 13\nTraining Slime health: 6\nTraining Slime health: 0\nTraining Slime defeated!\nThe combat still works.\nBut the code now contains an actual player and enemy.\nCoding challenge: Pay the player\nEnemies should be worth something. Add a gold_reward: u32 field to Enemy, give the Training Slime a reward of 5, and print it using the field instead of the placeholder 0. Keep the field name gold_reward, and press Check my code to see how you are doing.\nExpected result: the program prints Training Slime is worth 5 gold. As written, the starter prints Training Slime is worth 0 gold.\nGive the enemy a reward\nstruct Enemy {\nname: String,\nhealth: u32,\n// TODO: add a gold_reward field of type u32\n}\nfn main() {\nlet enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n// TODO: give the Training Slime a gold_reward of 5\n};\n// TODO: replace the 0 with enemy.gold_reward\nprintln!(\"{} is worth {} gold\", enemy.name, 0);\nprintln!(\"It has {} health\", enemy.health);\n}\n<details>\n<summary>Spoiler: the checks for this challenge</summary>\nThese tests run when you press Check my code on the original article. Reading them first gives away the answer.\n/// `Enemy` needs a field called `gold_reward`, and it should hold a `u32`. Check the name and the type.\n#[test]\nfn enemy_has_a_u32_gold_reward() {\nlet enemy = Enemy {\nname: String::from(\"Test Slime\"),\nhealth: 1,\ngold_reward: 7u32,\n};\nlet reward: u32 = enemy.gold_reward;\nassert_eq!(reward, 7);\n}\n/// Adding a field should not cost us the old ones. `name` and `health` still belong on `Enemy`.\n#[test]\nfn old_fields_survive() {\nlet enemy = Enemy {\nname: String::from(\"Test Slime\"),\nhealth: 9,\ngold_reward: 1,\n};\nassert_eq!(enemy.name, \"Test Slime\");\nassert_eq!(enemy.health, 9);\n}\n</details>\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/three-numbers-are-not-a-player/).\nTip:\nA new field needs two edits: its name and type in the struct definition, and a value wherever you create an Enemy. Then read it with a dot, like enemy.health.\nNotice what the structs made visible\nBefore, our game looked like this:\nlet mut enemy_health = 20;\nenemy_health =\nattack(enemy_health, 5, 2);\nNow it looks like this:\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nenemy.health = attack(\nenemy.health,\nplayer.base_damage,\nplayer.bonus_damage,\n);\nThe behavior is the same.\nThe meaning is not.\nBefore:\n20\n5\n2\nNow:\nPlayer\n├── base_damage\n└── bonus_damage\nEnemy\n├── name\n└── health\nWe gave related values names and grouped them into types.\nRust can now help us keep player data with the player and enemy data with the enemy.\nThe behavior did not change\nNotice what we did not touch:\nclick_damage()\nstill calculates damage.\nhealth_after_hit()\nstill reduces health.\nis_defeated()\nstill checks for defeat.\nattack()\nstill combines the smaller rules.\nWe changed how the application organizes its data.\nWe did not rewrite the behavior we already trust.\nRun the tests:\ncargo test\nAll four should still pass.\nNo new combat rule was added in this article, so we do not need to invent a new behavior test just to prove that a struct can hold the values we typed into it.\nThe compiler checks that our struct fields exist and contain the expected types.\nOur existing tests continue checking the game rules.\nThe complete program\nAt this point, src/main.rs should look like this:\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(\nhealth: u32,\ndamage: u32,\n) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn attack(\nhealth: u32,\nbase_damage: u32,\nbonus_damage: u32,\n) -> u32 {\nlet damage =\nclick_damage(base_damage, bonus_damage);\nhealth_after_hit(health, damage)\n}\nfn main() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nwhile !is_defeated(enemy.health) {\nenemy.health = attack(\nenemy.health,\nplayer.base_damage,\nplayer.bonus_damage,\n);\nprintln!(\n\"{} health: {}\",\nenemy.name,\nenemy.health\n);\n}\nprintln!(\"{} defeated!\", enemy.name);\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(\nhealth_after_hit(3, 10),\n0\n);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nassert_eq!(attack(10, 5, 2), 3);\n}\n}\nRun both:\ncargo run\ncargo test\nThe game should defeat the Training Slime, and all four tests should pass.\nWhat we learned\nAlong the way, we encountered:\nstruct\ncustom Rust types\nstruct fields\ncreating struct values\nfield access with .\nString\nmutable struct values\nYou do not need to memorize all of that yet.\nThe important change is what happened to our data.\nWe started with:\nenemy_health\n5\n2\nWe turned those values into:\nPlayer\nEnemy\nOur program now has a vocabulary that matches the game we are building.\nThere is still something awkward\nWe solved one problem.\nBut look at our attack again:\nenemy.health = attack(\nenemy.health,\nplayer.base_damage,\nplayer.bonus_damage,\n);\nWe created a Player.\nWe created an Enemy.\nThen, the moment they need to interact, we pull them apart into individual fields.\nattack() does not accept a player.\nIt does not accept an enemy.\nIt still accepts three numbers and returns another number that we manually assign back to the enemy.\nWhat we really want to express is:\nplayer attacks enemy\nThat means our attack needs to:\nread the player\nchange the enemy\nleave both available for the next attack\nRust has a way to let a function temporarily use values without taking them away from the code that already owns them.\nThat is called borrowing.\nNext, we will let our player attack an enemy without consuming either one.\nSmall function. Bigger game.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/three-numbers-are-not-a-player/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2026-10-14T02:06:53Z",
      "id": "https://mnixblog.netlify.app/three-numbers-are-not-a-player/",
      "image": "https://mnixblog.netlify.app/images/three-numbers-are-not-a-player/cover.jpg",
      "summary": "When your attack takes three loose u32 values, Rust has no idea which belong to the player. Learn how structs give your game data a name and a shape.",
      "tags": [
        "Rust",
        "WebAssembly",
        "Game Development",
        "Beginners"
      ],
      "title": "Three Numbers Are Not a Player.",
      "url": "https://mnixblog.netlify.app/three-numbers-are-not-a-player/"
    },
    {
      "content_html": "<p>Our game can now calculate damage, reduce health, and decide whether an enemy has been defeated.</p>\n<p>There is a problem, though.</p>\n<p>Those rules do not actually <strong>attack</strong> anything yet.</p>\n<p>In this article, we are going to combine our small Rust functions into a larger <code>attack()</code> function, let enemy health change during combat, and repeat attacks until the enemy is defeated.</p>\n<p>By the end, we will have our first actual game mechanic: a combat loop.</p>\n<hr />\n<h2 id=\"our-starting-point\">Our starting point<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#our-starting-point\" aria-label=\"Link to this section\">#</a></h2>\n<p>You only need three small functions to follow along:</p>\n<pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(\n    health: u32,\n    damage: u32,\n) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n</code></pre>\n<p>Each one has a single job.</p>\n<p><code>click_damage()</code> calculates how much damage the player deals.</p>\n<p><code>health_after_hit()</code> applies that damage to an enemy's health.</p>\n<p><code>is_defeated()</code> tells us whether the enemy has reached zero health.</p>\n<p>Right now, however, something else has to coordinate all three.</p>\n<p>That something is <code>main()</code>.</p>\n<hr />\n<h2 id=\"main-knows-too-much\"><code>main()</code> knows too much<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#main-knows-too-much\" aria-label=\"Link to this section\">#</a></h2>\n<p>We could attack an enemy like this:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let enemy_health = 10;\n\n    let damage = click_damage(5, 2);\n\n    let remaining_health =\n        health_after_hit(enemy_health, damage);\n\n    println!(\"Enemy health: {remaining_health}\");\n\n    if is_defeated(remaining_health) {\n        println!(\"Enemy defeated!\");\n    }\n}\n</code></pre>\n<p>Technically, this does work.</p>\n<p>But look at everything that <code>main()</code> knows about.</p>\n<p>It knows how damage is calculated.</p>\n<p>It knows how damage affects health.</p>\n<p>It knows how to determine whether the enemy has been defeated.</p>\n<p>Those first two steps describe one larger idea:</p>\n<p><strong>an attack.</strong></p>\n<p>So let us give that idea a function.</p>\n<hr />\n<h2 id=\"build-an-attack-from-smaller-functions\">Build an attack from smaller functions<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#build-an-attack-from-smaller-functions\" aria-label=\"Link to this section\">#</a></h2>\n<p>Add this beneath the functions we already have:</p>\n<pre><code class=\"language-rust\">fn attack(\n    health: u32,\n    base_damage: u32,\n    bonus_damage: u32,\n) -&gt; u32 {\n    let damage =\n        click_damage(base_damage, bonus_damage);\n\n    health_after_hit(health, damage)\n}\n</code></pre>\n<p>Notice how we are not adding new Rust ideas here.</p>\n<p>That is the point.</p>\n<p>We are taking functions we already understand and using them to build something more complex.</p>\n<p>The first line inside <code>attack()</code>:</p>\n<pre><code class=\"language-rust\">let damage =\n    click_damage(base_damage, bonus_damage);\n</code></pre>\n<p>uses our existing damage rule.</p>\n<p>Then:</p>\n<pre><code class=\"language-rust\">health_after_hit(health, damage)\n</code></pre>\n<p>uses the result to calculate the enemy's remaining health.</p>\n<p>Because it is the final expression and has no semicolon, that remaining health becomes the value returned by <code>attack()</code>.</p>\n<p>Our functions now fit together like this:</p>\n<pre><code class=\"language-text\">click_damage() ───────┐\n                      ├──→ attack()\nhealth_after_hit() ───┘\n</code></pre>\n<p>We did not replace our small functions.</p>\n<p><strong>We composed a larger behavior from them.</strong></p>\n<p>Instead of teaching <code>attack()</code> every rule from scratch, it delegates those jobs to functions that already know how to calculate damage and reduce health.</p>\n<p>We already know what each piece does, so understanding <code>attack()</code> does not require us to understand an entirely new system.</p>\n<p>Try it: this runs <code>attack()</code> against an enemy with ten health. It should print <code>Enemy health: 3</code>. Change the bonus damage from <code>2</code> to <code>1000</code> and predict the result before you run it.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try changing the bonus damage</figcaption><pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn attack(health: u32, base_damage: u32, bonus_damage: u32) -&gt; u32 {\n    let damage = click_damage(base_damage, bonus_damage);\n\n    health_after_hit(health, damage)\n}\n\nfn main() {\n    let remaining_health = attack(10, 5, 2);\n\n    println!(&quot;Enemy health: {remaining_health}&quot;);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/\">the original article</a>.</p></figure>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does <code>attack()</code> call <code>click_damage()</code> and <code>health_after_hit()</code> instead of repeating their arithmetic?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Rust does not allow arithmetic inside a function that returns <code>u32</code></li>\n<li>The damage and health rules stay in one place each, so a change there changes every attack</li>\n<li>Calling a function is always faster than writing the arithmetic inline</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. The damage and health rules stay in one place each, so a change there changes every attack</p>\n<p>Composition reuses the rules we already trust. Changing <code>click_damage()</code> later changes every attack without touching <code>attack()</code>.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"try-the-attack\">Try the attack<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#try-the-attack\" aria-label=\"Link to this section\">#</a></h2>\n<p>Replace <code>main()</code> with:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let enemy_health = 10;\n\n    let remaining_health =\n        attack(enemy_health, 5, 2);\n\n    println!(\"Enemy health: {remaining_health}\");\n}\n</code></pre>\n<p>Run the game:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>You should see:</p>\n<pre><code class=\"language-text\">Enemy health: 3\n</code></pre>\n<p>Our player dealt seven damage to an enemy with ten health.</p>\n<p>That means <code>attack()</code> works.</p>\n<p>We can test it too.</p>\n<p>Add this to the test module:</p>\n<pre><code class=\"language-rust\">#[test]\nfn attack_reduces_enemy_health() {\n    assert_eq!(attack(10, 5, 2), 3);\n}\n</code></pre>\n<p>Then run:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>We now have a test for behavior that depends on several smaller rules working together.</p>\n<hr />\n<h2 id=\"one-attack-is-not-much-of-a-fight\">One attack is not much of a fight<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#one-attack-is-not-much-of-a-fight\" aria-label=\"Link to this section\">#</a></h2>\n<p>We can attack an enemy once.</p>\n<p>An idle game probably needs a little more combat than that.</p>\n<p>Suppose our enemy starts with twenty health:</p>\n<pre><code class=\"language-rust\">let enemy_health = 20;\n</code></pre>\n<p>The first attack reduces it to thirteen.</p>\n<p>The next should reduce it to six.</p>\n<p>The next should reduce it to zero.</p>\n<p>There is a problem, though.</p>\n<p>This:</p>\n<pre><code class=\"language-rust\">let enemy_health = 20;\n</code></pre>\n<p>creates a variable that Rust does not allow us to change.</p>\n<p>Variables are <strong>immutable by default</strong> in Rust.</p>\n<p>That means this is not allowed:</p>\n<pre><code class=\"language-rust\">let enemy_health = 20;\n\nenemy_health = attack(enemy_health, 5, 2);\n</code></pre>\n<p>If you try this, Rust will refuse to compile it and tell us that we cannot assign to an immutable variable.</p>\n<p>That error is useful. We have to explicitly tell Rust which values we expect to change.</p>\n<p>For most values, that is a useful default.</p>\n<p>For health during combat, it is not.</p>\n<p>Health is supposed to change.</p>\n<hr />\n<h2 id=\"let-health-change-with-mut\">Let health change with <code>mut</code><a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#let-health-change-with-mut\" aria-label=\"Link to this section\">#</a></h2>\n<p>Rust uses the <code>mut</code> keyword when we want a variable to be mutable.</p>\n<p>Change:</p>\n<pre><code class=\"language-rust\">let enemy_health = 20;\n</code></pre>\n<p>to:</p>\n<pre><code class=\"language-rust\">let mut enemy_health = 20;\n</code></pre>\n<p>Now we can assign a new value to it:</p>\n<pre><code class=\"language-rust\">enemy_health =\n    attack(enemy_health, 5, 2);\n</code></pre>\n<p>The word <code>mut</code> is short for <strong>mutable</strong>, meaning the value bound to this variable may change.</p>\n<p>We are not making everything mutable just because we can.</p>\n<p>We are using mutability because enemy health represents something that changes as part of the game.</p>\n<p>That distinction will become increasingly useful as our state grows.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does the combat loop declare <code>let mut enemy_health = 20;</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>while</code> loops only work with variables declared <code>mut</code></li>\n<li><code>println!</code> needs a mutable variable to display it</li>\n<li>The loop assigns a new health value after every attack</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: C. The loop assigns a new health value after every attack</p>\n<p><code>mut</code> is needed because of the reassignment <code>enemy_health = attack(...)</code>, not because of the loop or the printing.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"attack-again\">Attack again<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#attack-again\" aria-label=\"Link to this section\">#</a></h2>\n<p>We can now do this:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let mut enemy_health = 20;\n\n    enemy_health =\n        attack(enemy_health, 5, 2);\n\n    println!(\"Enemy health: {enemy_health}\");\n\n    enemy_health =\n        attack(enemy_health, 5, 2);\n\n    println!(\"Enemy health: {enemy_health}\");\n\n    enemy_health =\n        attack(enemy_health, 5, 2);\n\n    println!(\"Enemy health: {enemy_health}\");\n}\n</code></pre>\n<p>Run it:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>and we get:</p>\n<pre><code class=\"language-text\">Enemy health: 13\nEnemy health: 6\nEnemy health: 0\n</code></pre>\n<p>It works.</p>\n<p>It is also repetitive.</p>\n<p>We know what should happen:</p>\n<blockquote>\n<p>Keep attacking while the enemy is still alive.</p>\n</blockquote>\n<p>Rust has a way to express exactly that.</p>\n<hr />\n<h2 id=\"repeat-with-while\">Repeat with <code>while</code><a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#repeat-with-while\" aria-label=\"Link to this section\">#</a></h2>\n<p>A <code>while</code> loop repeats a block of code as long as a condition remains <code>true</code>.</p>\n<p>For example:</p>\n<pre><code class=\"language-rust\">while enemy_health &gt; 0 {\n    // repeat something\n}\n</code></pre>\n<p>We could use that.</p>\n<p>But we already wrote a function that knows whether the enemy has been defeated:</p>\n<pre><code class=\"language-rust\">is_defeated(enemy_health)\n</code></pre>\n<p>We should use the rule we already have.</p>\n<p>What we really want to say is:</p>\n<blockquote>\n<p>Keep attacking while the enemy is <strong>not</strong> defeated.</p>\n</blockquote>\n<p>Rust uses <code>!</code> to mean <strong>not</strong>.</p>\n<p>So:</p>\n<pre><code class=\"language-rust\">!is_defeated(enemy_health)\n</code></pre>\n<p>means:</p>\n<blockquote>\n<p>the enemy has not been defeated</p>\n</blockquote>\n<p>Put that inside a <code>while</code> loop:</p>\n<pre><code class=\"language-rust\">while !is_defeated(enemy_health) {\n    enemy_health =\n        attack(enemy_health, 5, 2);\n\n    println!(\"Enemy health: {enemy_health}\");\n}\n</code></pre>\n<p>Now Rust keeps running the block until <code>is_defeated()</code> returns <code>true</code>.</p>\n<p>Try it: this is the whole loop. It should print <code>Enemy health: 13</code>, <code>Enemy health: 6</code>, <code>Enemy health: 0</code>, then <code>Enemy defeated!</code>. Change the starting health to <code>50</code> and predict how many lines of health you will see, then change it to <code>0</code> and predict again.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try changing the starting health</figcaption><pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn attack(health: u32, base_damage: u32, bonus_damage: u32) -&gt; u32 {\n    let damage = click_damage(base_damage, bonus_damage);\n\n    health_after_hit(health, damage)\n}\n\nfn main() {\n    let mut enemy_health = 20;\n\n    while !is_defeated(enemy_health) {\n        enemy_health = attack(enemy_health, 5, 2);\n\n        println!(&quot;Enemy health: {enemy_health}&quot;);\n    }\n\n    println!(&quot;Enemy defeated!&quot;);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/\">the original article</a>.</p></figure>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>What does the loop above print if <code>enemy_health</code> starts at <code>0</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>Enemy health: 0</code> and then <code>Enemy defeated!</code></li>\n<li>Only <code>Enemy defeated!</code></li>\n<li>Nothing, because the program fails to compile</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Only <code>Enemy defeated!</code></p>\n<p><code>while</code> checks its condition before each pass. <code>!is_defeated(0)</code> is already <code>false</code>, so the body never runs and only the final <code>println!</code> does.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"our-first-combat-loop\">Our first combat loop<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#our-first-combat-loop\" aria-label=\"Link to this section\">#</a></h2>\n<p>Update <code>main()</code>:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let mut enemy_health = 20;\n\n    while !is_defeated(enemy_health) {\n        enemy_health =\n            attack(enemy_health, 5, 2);\n\n        println!(\n            \"Enemy health: {enemy_health}\"\n        );\n    }\n\n    println!(\"Enemy defeated!\");\n}\n</code></pre>\n<p>Run:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>You should see:</p>\n<pre><code class=\"language-text\">Enemy health: 13\nEnemy health: 6\nEnemy health: 0\nEnemy defeated!\n</code></pre>\n<p>We have a combat loop.</p>\n<p>The player attacks.</p>\n<p>Health changes.</p>\n<p>The game checks whether combat should continue.</p>\n<p>Then it attacks again.</p>\n<p>Our little system now looks something like this:</p>\n<pre><code class=\"language-text\">click_damage() ───────┐\n                      ├──→ attack()\nhealth_after_hit() ───┘\n                      ↓\n                  repeat while\n          enemy has not been defeated\n</code></pre>\n<p>The individual pieces are still small.</p>\n<p>The behavior is getting larger.</p>\n<p><strong>Coding challenge: Count the swings</strong></p>\n<p>A fight is more fun when the game can say how long it took. Add a counter that goes up once for every attack, so the program reports how many swings it took to defeat the enemy. The loop now lives in a small <code>fight</code> function so the count can be handed back, and <strong>Check my code</strong> will tell you how you are doing. Keep the name and signature of <code>fight</code>.</p>\n<p><strong>Expected result:</strong> the program prints the three health lines, <code>Enemy defeated!</code>, and then <code>Swings: 3</code>. As written, the starter ends with <code>Swings: 0</code>.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Count the swings</figcaption><pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn attack(health: u32, base_damage: u32, bonus_damage: u32) -&gt; u32 {\n    let damage = click_damage(base_damage, bonus_damage);\n\n    health_after_hit(health, damage)\n}\n\nfn fight(mut enemy_health: u32, base_damage: u32, bonus_damage: u32) -&gt; u32 {\n    let mut swings = 0;\n\n    while !is_defeated(enemy_health) {\n        enemy_health = attack(enemy_health, base_damage, bonus_damage);\n        // TODO: add 1 to swings every time the player attacks\n\n        println!(&quot;Enemy health: {enemy_health}&quot;);\n    }\n\n    swings\n}\n\nfn main() {\n    let swings = fight(20, 5, 2);\n\n    println!(&quot;Enemy defeated!&quot;);\n    println!(&quot;Swings: {swings}&quot;);\n}\n\n</code></pre><details class=\"ob-spoiler\"><summary>Spoiler: the checks for this challenge</summary><p>These tests run when you press Check my code on the original article. Reading them first gives away the answer.</p><pre><code class=\"language-rust\">/// The fight should report how many attacks it took. Look at what `fight` hands back at the end.\n#[test]\nfn counts_every_swing() {\n    assert_eq!(fight(20, 5, 2), 3);\n}\n\n/// An enemy that falls to one hit still took a swing. Check what a short fight reports.\n#[test]\nfn one_hit_is_one_swing() {\n    assert_eq!(fight(7, 5, 2), 1);\n}\n\n/// A longer fight should report a longer count. The number has to come from the loop, not from us.\n#[test]\nfn long_fight_counts_more() {\n    assert_eq!(fight(70, 5, 2), 10);\n}\n</code></pre></details><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p><code>swings</code> is <code>mut</code> for the same reason <code>enemy_health</code> is. Reassign it inside the loop the way we reassign health, with the old value plus <code>1</code>.</p>\n</aside>\n<hr />\n<h2 id=\"notice-what-rust-made-us-say\">Notice what Rust made us say<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#notice-what-rust-made-us-say\" aria-label=\"Link to this section\">#</a></h2>\n<p>We encountered three small pieces of Rust syntax in this article:</p>\n<pre><code class=\"language-text\">mut    → health needed to change\nwhile  → combat needed to repeat\n!      → we needed to express \"not defeated\"\n</code></pre>\n<p>But we did not add them because they appeared next on a Rust checklist.</p>\n<p>The game gave us reasons to need them.</p>\n<p>Enemy health needed to change:</p>\n<pre><code class=\"language-rust\">let mut enemy_health = 20;\n</code></pre>\n<p>Combat needed to repeat until the enemy is defeated:</p>\n<pre><code class=\"language-rust\">while !is_defeated(enemy_health) {\n</code></pre>\n<p>That is how we are going to keep learning Rust throughout this project.</p>\n<p>When the game creates a problem, we will learn the Rust needed to solve it.</p>\n<hr />\n<h2 id=\"testing-the-larger-rule\">Testing the larger rule<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#testing-the-larger-rule\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our existing tests still protect the smaller rules:</p>\n<pre><code class=\"language-rust\">#[test]\nfn click_damage_adds_base_and_bonus() {\n    assert_eq!(click_damage(3, 4), 7);\n}\n\n#[test]\nfn health_never_drops_below_zero() {\n    assert_eq!(health_after_hit(3, 10), 0);\n}\n\n#[test]\nfn zero_health_is_defeated() {\n    assert!(is_defeated(0));\n    assert!(!is_defeated(1));\n}\n</code></pre>\n<p>Now we also have:</p>\n<pre><code class=\"language-rust\">#[test]\nfn attack_reduces_enemy_health() {\n    assert_eq!(attack(10, 5, 2), 3);\n}\n</code></pre>\n<p>This test is slightly different.</p>\n<p><code>attack()</code> is not doing one tiny calculation by itself.</p>\n<p>It relies on other functions.</p>\n<p>That means we are beginning to test not only individual rules, but also the larger behavior created when those rules work together.</p>\n<hr />\n<h2 id=\"the-complete-program\">The complete program<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#the-complete-program\" aria-label=\"Link to this section\">#</a></h2>\n<p>At this point, <code>src/main.rs</code> should look like this:</p>\n<pre><code class=\"language-rust\">fn click_damage(\n    base: u32,\n    bonus: u32,\n) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(\n    health: u32,\n    damage: u32,\n) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn attack(\n    health: u32,\n    base_damage: u32,\n    bonus_damage: u32,\n) -&gt; u32 {\n    let damage =\n        click_damage(base_damage, bonus_damage);\n\n    health_after_hit(health, damage)\n}\n\nfn main() {\n    let mut enemy_health = 20;\n\n    while !is_defeated(enemy_health) {\n        enemy_health =\n            attack(enemy_health, 5, 2);\n\n        println!(\n            \"Enemy health: {enemy_health}\"\n        );\n    }\n\n    println!(\"Enemy defeated!\");\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(\n            health_after_hit(3, 10),\n            0\n        );\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        assert_eq!(attack(10, 5, 2), 3);\n    }\n}\n</code></pre>\n<p>Run both:</p>\n<pre><code class=\"language-bash\">cargo run\ncargo test\n</code></pre>\n<p>Our game now fights until the enemy is defeated, and all four functions we have written are covered by tests.</p>\n<hr />\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Along the way, we encountered:</p>\n<ul>\n<li>functions calling other functions</li>\n<li>local variables inside functions</li>\n<li><code>mut</code></li>\n<li>reassignment</li>\n<li><code>while</code></li>\n<li><code>!</code></li>\n<li>tests for composed behavior</li>\n</ul>\n<p>You do not need to memorize all of that yet.</p>\n<p>The important change is what happened to our game.</p>\n<p>We started with three independent rules:</p>\n<pre><code class=\"language-text\">damage\nhealth\ndefeat\n</code></pre>\n<p>We combined them into:</p>\n<pre><code class=\"language-text\">attack\n</code></pre>\n<p>Then repeated that attack until we had:</p>\n<pre><code class=\"language-text\">combat\n</code></pre>\n<p>Our program became more capable without any individual function becoming particularly complicated.</p>\n<hr />\n<h2 id=\"there-is-another-problem-hiding-in-our-code\">There is another problem hiding in our code<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#there-is-another-problem-hiding-in-our-code\" aria-label=\"Link to this section\">#</a></h2>\n<p>Look at this:</p>\n<pre><code class=\"language-rust\">attack(enemy_health, 5, 2)\n</code></pre>\n<p>We know what those values mean because we just wrote them.</p>\n<p><code>enemy_health</code> belongs to the enemy.</p>\n<p><code>5</code> is the player's base damage.</p>\n<p><code>2</code> is the player's bonus damage.</p>\n<p>Rust does not know any of that.</p>\n<p>As far as Rust is concerned, they are just three <code>u32</code> values being passed into a function.</p>\n<p>And our game is about to need more information:</p>\n<pre><code class=\"language-text\">player level\nplayer gold\nenemy name\nenemy maximum health\nenemy rewards\n</code></pre>\n<p>Passing more and more unrelated-looking numbers around is not going to age well.</p>\n<p>The next problem is no longer about making our functions stronger.</p>\n<p>It is about giving our data some shape.</p>\n<p>Next, we will turn the player and enemy into real Rust types of their own.</p>\n<p><strong>Small function. Bigger game.</strong></p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "One Attack Is Not a Fight.\nCanonical: https://mnixblog.netlify.app/build-a-combat-loop-in-rust/\nBuild a Combat Loop From Small Rust Functions\nOur game can now calculate damage, reduce health, and decide whether an enemy has been defeated.\nThere is a problem, though.\nThose rules do not actually attack anything yet.\nIn this article, we are going to combine our small Rust functions into a larger attack() function, let enemy health change during combat, and repeat attacks until the enemy is defeated.\nBy the end, we will have our first actual game mechanic: a combat loop.\nOur starting point\nYou only need three small functions to follow along:\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(\nhealth: u32,\ndamage: u32,\n) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nEach one has a single job.\nclick_damage() calculates how much damage the player deals.\nhealth_after_hit() applies that damage to an enemy's health.\nis_defeated() tells us whether the enemy has reached zero health.\nRight now, however, something else has to coordinate all three.\nThat something is main().\nmain() knows too much\nWe could attack an enemy like this:\nfn main() {\nlet enemy_health = 10;\nlet damage = click_damage(5, 2);\nlet remaining_health =\nhealth_after_hit(enemy_health, damage);\nprintln!(\"Enemy health: {remaining_health}\");\nif is_defeated(remaining_health) {\nprintln!(\"Enemy defeated!\");\n}\n}\nTechnically, this does work.\nBut look at everything that main() knows about.\nIt knows how damage is calculated.\nIt knows how damage affects health.\nIt knows how to determine whether the enemy has been defeated.\nThose first two steps describe one larger idea:\nan attack.\nSo let us give that idea a function.\nBuild an attack from smaller functions\nAdd this beneath the functions we already have:\nfn attack(\nhealth: u32,\nbase_damage: u32,\nbonus_damage: u32,\n) -> u32 {\nlet damage =\nclick_damage(base_damage, bonus_damage);\nhealth_after_hit(health, damage)\n}\nNotice how we are not adding new Rust ideas here.\nThat is the point.\nWe are taking functions we already understand and using them to build something more complex.\nThe first line inside attack():\nlet damage =\nclick_damage(base_damage, bonus_damage);\nuses our existing damage rule.\nThen:\nhealth_after_hit(health, damage)\nuses the result to calculate the enemy's remaining health.\nBecause it is the final expression and has no semicolon, that remaining health becomes the value returned by attack().\nOur functions now fit together like this:\nclick_damage() ───────┐\n├──→ attack()\nhealth_after_hit() ───┘\nWe did not replace our small functions.\nWe composed a larger behavior from them.\nInstead of teaching attack() every rule from scratch, it delegates those jobs to functions that already know how to calculate damage and reduce health.\nWe already know what each piece does, so understanding attack() does not require us to understand an entirely new system.\nTry it: this runs attack() against an enemy with ten health. It should print Enemy health: 3. Change the bonus damage from 2 to 1000 and predict the result before you run it.\nLive example · try changing the bonus damage\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn attack(health: u32, base_damage: u32, bonus_damage: u32) -> u32 {\nlet damage = click_damage(base_damage, bonus_damage);\nhealth_after_hit(health, damage)\n}\nfn main() {\nlet remaining_health = attack(10, 5, 2);\nprintln!(\"Enemy health: {remaining_health}\");\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/build-a-combat-loop-in-rust/).\nQuestion: Why does attack() call click_damage() and health_after_hit() instead of repeating their arithmetic?\nRust does not allow arithmetic inside a function that returns u32\nThe damage and health rules stay in one place each, so a change there changes every attack\nCalling a function is always faster than writing the arithmetic inline\nAnswer: 2. The damage and health rules stay in one place each, so a change there changes every attack\nExplanation: Composition reuses the rules we already trust. Changing click_damage() later changes every attack without touching attack().\nTry the attack\nReplace main() with:\nfn main() {\nlet enemy_health = 10;\nlet remaining_health =\nattack(enemy_health, 5, 2);\nprintln!(\"Enemy health: {remaining_health}\");\n}\nRun the game:\ncargo run\nYou should see:\nEnemy health: 3\nOur player dealt seven damage to an enemy with ten health.\nThat means attack() works.\nWe can test it too.\nAdd this to the test module:\n#[test]\nfn attack_reduces_enemy_health() {\nassert_eq!(attack(10, 5, 2), 3);\n}\nThen run:\ncargo test\nWe now have a test for behavior that depends on several smaller rules working together.\nOne attack is not much of a fight\nWe can attack an enemy once.\nAn idle game probably needs a little more combat than that.\nSuppose our enemy starts with twenty health:\nlet enemy_health = 20;\nThe first attack reduces it to thirteen.\nThe next should reduce it to six.\nThe next should reduce it to zero.\nThere is a problem, though.\nThis:\nlet enemy_health = 20;\ncreates a variable that Rust does not allow us to change.\nVariables are immutable by default in Rust.\nThat means this is not allowed:\nlet enemy_health = 20;\nenemy_health = attack(enemy_health, 5, 2);\nIf you try this, Rust will refuse to compile it and tell us that we cannot assign to an immutable variable.\nThat error is useful. We have to explicitly tell Rust which values we expect to change.\nFor most values, that is a useful default.\nFor health during combat, it is not.\nHealth is supposed to change.\nLet health change with mut\nRust uses the mut keyword when we want a variable to be mutable.\nChange:\nlet enemy_health = 20;\nto:\nlet mut enemy_health = 20;\nNow we can assign a new value to it:\nenemy_health =\nattack(enemy_health, 5, 2);\nThe word mut is short for mutable, meaning the value bound to this variable may change.\nWe are not making everything mutable just because we can.\nWe are using mutability because enemy health represents something that changes as part of the game.\nThat distinction will become increasingly useful as our state grows.\nQuestion: Why does the combat loop declare let mut enemy_health = 20;?\nwhile loops only work with variables declared mut\nprintln! needs a mutable variable to display it\nThe loop assigns a new health value after every attack\nAnswer: 3. The loop assigns a new health value after every attack\nExplanation: mut is needed because of the reassignment enemy_health = attack(...), not because of the loop or the printing.\nAttack again\nWe can now do this:\nfn main() {\nlet mut enemy_health = 20;\nenemy_health =\nattack(enemy_health, 5, 2);\nprintln!(\"Enemy health: {enemy_health}\");\nenemy_health =\nattack(enemy_health, 5, 2);\nprintln!(\"Enemy health: {enemy_health}\");\nenemy_health =\nattack(enemy_health, 5, 2);\nprintln!(\"Enemy health: {enemy_health}\");\n}\nRun it:\ncargo run\nand we get:\nEnemy health: 13\nEnemy health: 6\nEnemy health: 0\nIt works.\nIt is also repetitive.\nWe know what should happen:\nKeep attacking while the enemy is still alive.\nRust has a way to express exactly that.\nRepeat with while\nA while loop repeats a block of code as long as a condition remains true.\nFor example:\nwhile enemy_health > 0 {\n// repeat something\n}\nWe could use that.\nBut we already wrote a function that knows whether the enemy has been defeated:\nis_defeated(enemy_health)\nWe should use the rule we already have.\nWhat we really want to say is:\nKeep attacking while the enemy is not defeated.\nRust uses ! to mean not.\nSo:\n!is_defeated(enemy_health)\nmeans:\nthe enemy has not been defeated\nPut that inside a while loop:\nwhile !is_defeated(enemy_health) {\nenemy_health =\nattack(enemy_health, 5, 2);\nprintln!(\"Enemy health: {enemy_health}\");\n}\nNow Rust keeps running the block until is_defeated() returns true.\nTry it: this is the whole loop. It should print Enemy health: 13, Enemy health: 6, Enemy health: 0, then Enemy defeated!. Change the starting health to 50 and predict how many lines of health you will see, then change it to 0 and predict again.\nLive example · try changing the starting health\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn attack(health: u32, base_damage: u32, bonus_damage: u32) -> u32 {\nlet damage = click_damage(base_damage, bonus_damage);\nhealth_after_hit(health, damage)\n}\nfn main() {\nlet mut enemy_health = 20;\nwhile !is_defeated(enemy_health) {\nenemy_health = attack(enemy_health, 5, 2);\nprintln!(\"Enemy health: {enemy_health}\");\n}\nprintln!(\"Enemy defeated!\");\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/build-a-combat-loop-in-rust/).\nQuestion: What does the loop above print if enemy_health starts at 0?\nEnemy health: 0 and then Enemy defeated!\nOnly Enemy defeated!\nNothing, because the program fails to compile\nAnswer: 2. Only Enemy defeated!\nExplanation: while checks its condition before each pass. !is_defeated(0) is already false, so the body never runs and only the final println! does.\nOur first combat loop\nUpdate main():\nfn main() {\nlet mut enemy_health = 20;\nwhile !is_defeated(enemy_health) {\nenemy_health =\nattack(enemy_health, 5, 2);\nprintln!(\n\"Enemy health: {enemy_health}\"\n);\n}\nprintln!(\"Enemy defeated!\");\n}\nRun:\ncargo run\nYou should see:\nEnemy health: 13\nEnemy health: 6\nEnemy health: 0\nEnemy defeated!\nWe have a combat loop.\nThe player attacks.\nHealth changes.\nThe game checks whether combat should continue.\nThen it attacks again.\nOur little system now looks something like this:\nclick_damage() ───────┐\n├──→ attack()\nhealth_after_hit() ───┘\n↓\nrepeat while\nenemy has not been defeated\nThe individual pieces are still small.\nThe behavior is getting larger.\nCoding challenge: Count the swings\nA fight is more fun when the game can say how long it took. Add a counter that goes up once for every attack, so the program reports how many swings it took to defeat the enemy. The loop now lives in a small fight function so the count can be handed back, and Check my code will tell you how you are doing. Keep the name and signature of fight.\nExpected result: the program prints the three health lines, Enemy defeated!, and then Swings: 3. As written, the starter ends with Swings: 0.\nCount the swings\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn attack(health: u32, base_damage: u32, bonus_damage: u32) -> u32 {\nlet damage = click_damage(base_damage, bonus_damage);\nhealth_after_hit(health, damage)\n}\nfn fight(mut enemy_health: u32, base_damage: u32, bonus_damage: u32) -> u32 {\nlet mut swings = 0;\nwhile !is_defeated(enemy_health) {\nenemy_health = attack(enemy_health, base_damage, bonus_damage);\n// TODO: add 1 to swings every time the player attacks\nprintln!(\"Enemy health: {enemy_health}\");\n}\nswings\n}\nfn main() {\nlet swings = fight(20, 5, 2);\nprintln!(\"Enemy defeated!\");\nprintln!(\"Swings: {swings}\");\n}\n<details>\n<summary>Spoiler: the checks for this challenge</summary>\nThese tests run when you press Check my code on the original article. Reading them first gives away the answer.\n/// The fight should report how many attacks it took. Look at what `fight` hands back at the end.\n#[test]\nfn counts_every_swing() {\nassert_eq!(fight(20, 5, 2), 3);\n}\n/// An enemy that falls to one hit still took a swing. Check what a short fight reports.\n#[test]\nfn one_hit_is_one_swing() {\nassert_eq!(fight(7, 5, 2), 1);\n}\n/// A longer fight should report a longer count. The number has to come from the loop, not from us.\n#[test]\nfn long_fight_counts_more() {\nassert_eq!(fight(70, 5, 2), 10);\n}\n</details>\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/build-a-combat-loop-in-rust/).\nTip:\nswings is mut for the same reason enemy_health is. Reassign it inside the loop the way we reassign health, with the old value plus 1.\nNotice what Rust made us say\nWe encountered three small pieces of Rust syntax in this article:\nmut    → health needed to change\nwhile  → combat needed to repeat\n!      → we needed to express \"not defeated\"\nBut we did not add them because they appeared next on a Rust checklist.\nThe game gave us reasons to need them.\nEnemy health needed to change:\nlet mut enemy_health = 20;\nCombat needed to repeat until the enemy is defeated:\nwhile !is_defeated(enemy_health) {\nThat is how we are going to keep learning Rust throughout this project.\nWhen the game creates a problem, we will learn the Rust needed to solve it.\nTesting the larger rule\nOur existing tests still protect the smaller rules:\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\nNow we also have:\n#[test]\nfn attack_reduces_enemy_health() {\nassert_eq!(attack(10, 5, 2), 3);\n}\nThis test is slightly different.\nattack() is not doing one tiny calculation by itself.\nIt relies on other functions.\nThat means we are beginning to test not only individual rules, but also the larger behavior created when those rules work together.\nThe complete program\nAt this point, src/main.rs should look like this:\nfn click_damage(\nbase: u32,\nbonus: u32,\n) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(\nhealth: u32,\ndamage: u32,\n) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn attack(\nhealth: u32,\nbase_damage: u32,\nbonus_damage: u32,\n) -> u32 {\nlet damage =\nclick_damage(base_damage, bonus_damage);\nhealth_after_hit(health, damage)\n}\nfn main() {\nlet mut enemy_health = 20;\nwhile !is_defeated(enemy_health) {\nenemy_health =\nattack(enemy_health, 5, 2);\nprintln!(\n\"Enemy health: {enemy_health}\"\n);\n}\nprintln!(\"Enemy defeated!\");\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(\nhealth_after_hit(3, 10),\n0\n);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nassert_eq!(attack(10, 5, 2), 3);\n}\n}\nRun both:\ncargo run\ncargo test\nOur game now fights until the enemy is defeated, and all four functions we have written are covered by tests.\nWhat we learned\nAlong the way, we encountered:\nfunctions calling other functions\nlocal variables inside functions\nmut\nreassignment\nwhile\n!\ntests for composed behavior\nYou do not need to memorize all of that yet.\nThe important change is what happened to our game.\nWe started with three independent rules:\ndamage\nhealth\ndefeat\nWe combined them into:\nattack\nThen repeated that attack until we had:\ncombat\nOur program became more capable without any individual function becoming particularly complicated.\nThere is another problem hiding in our code\nLook at this:\nattack(enemy_health, 5, 2)\nWe know what those values mean because we just wrote them.\nenemy_health belongs to the enemy.\n5 is the player's base damage.\n2 is the player's bonus damage.\nRust does not know any of that.\nAs far as Rust is concerned, they are just three u32 values being passed into a function.\nAnd our game is about to need more information:\nplayer level\nplayer gold\nenemy name\nenemy maximum health\nenemy rewards\nPassing more and more unrelated-looking numbers around is not going to age well.\nThe next problem is no longer about making our functions stronger.\nIt is about giving our data some shape.\nNext, we will turn the player and enemy into real Rust types of their own.\nSmall function. Bigger game.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2026-10-11T23:14:06Z",
      "id": "https://mnixblog.netlify.app/build-a-combat-loop-in-rust/",
      "image": "https://mnixblog.netlify.app/images/build-a-combat-loop-in-rust/cover.jpg",
      "summary": "Learn Rust by turning small functions into a combat loop. Build an attack() function, use mut and while, and create your first real game mechanic.",
      "tags": [
        "Rust",
        "WebAssembly",
        "Game Development",
        "Beginners"
      ],
      "title": "One Attack Is Not a Fight.",
      "url": "https://mnixblog.netlify.app/build-a-combat-loop-in-rust/"
    },
    {
      "content_html": "<p>Are you curious about diving into Rust, but are tired of tutorials that stop at <code>Hello World!</code>?</p>\n<p>In this article, I am going to walk you through writing and testing three tiny Rust functions that do something slightly more interesting: calculate attack damage, reduce an enemy's health, and decide whether the enemy has been defeated.</p>\n<p>You do not need any previous Rust experience to follow along.</p>\n<p>If you are able to edit a file and run a command in your terminal, you can jump right in.</p>\n<p>By the end, we will have the first real rules of an idle game, and these functions will not be throwaway tutorial code. We will keep building with them until they are running inside a complete browser game powered by Rust compiled to WebAssembly.</p>\n<p>If you are getting excited, then without further ado, it is time to start dealing damage!</p>\n<hr />\n<h2 id=\"create-the-project\">Create the project<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#create-the-project\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our first step is ensuring that Rust is installed on our local machine.</p>\n<blockquote>\n<p><strong>Need Rust installed first?</strong><br />\nRust is installed and managed through <code>rustup</code>, the official Rust toolchain installer. Follow the <a href=\"https://rust-lang.org/install.html\">official Rust installation instructions</a> for your operating system, then open a new terminal when the installation finishes.</p>\n</blockquote>\n<p>You can check that everything is ready with:</p>\n<pre><code class=\"language-bash\">rustc --version\ncargo --version\n</code></pre>\n<p>If both commands print version numbers, we are all set.</p>\n<p>We can create a new project by typing:</p>\n<pre><code class=\"language-bash\">cargo new rustbound-idle\ncd rustbound-idle\n</code></pre>\n<p>Cargo will create a small Rust application for us:</p>\n<pre><code class=\"language-text\">rustbound-idle/\n├── Cargo.toml\n└── src/\n    └── main.rs\n</code></pre>\n<p>Open <code>src/main.rs</code>.</p>\n<p>Notice the traditional Rust greeting:</p>\n<pre><code class=\"language-rust\">fn main() {\n    println!(\"Hello, world!\");\n}\n</code></pre>\n<p>If you want, you can run it once:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>Then chuck it in the bin.</p>\n<p>We are not here to say hello to enemies.</p>\n<p>We are here to say goodbye to them.</p>\n<hr />\n<h2 id=\"our-first-game-rule\">Our first game rule<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#our-first-game-rule\" aria-label=\"Link to this section\">#</a></h2>\n<p>Imagine our player has two sources of attack power:</p>\n<ul>\n<li>\n<p>base damage</p>\n</li>\n<li>\n<p>bonus damage from upgrades</p>\n</li>\n</ul>\n<p>Calculating total click damage is just addition.</p>\n<p>Add this to <code>main.rs</code>:</p>\n<pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n</code></pre>\n<p>This is our first Rust function.</p>\n<p>Breaking it apart into bits and pieces:</p>\n<pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32\n</code></pre>\n<p><code>fn</code> means we are defining a function.</p>\n<p>The function name is:</p>\n<pre><code class=\"language-text\">click_damage\n</code></pre>\n<p>It takes two values:</p>\n<pre><code class=\"language-rust\">base: u32\nbonus: u32\n</code></pre>\n<p><code>u32</code> is the type of both values. A type tells Rust what kind of data a value represents and what operations make sense for it.</p>\n<p>The colon connects the parameter name to its type, so:</p>\n<pre><code class=\"language-rust\">base: u32\n</code></pre>\n<p>means that <code>base</code> is a value with a type of <code>u32</code>.</p>\n<p>For now, think of <code>u32</code> as a non-negative whole number. More specifically, it is an unsigned 32-bit integer.</p>\n<p>So these are valid <code>u32</code> values:</p>\n<pre><code class=\"language-text\">0\n1\n25\n1000\n</code></pre>\n<p>But this is not:</p>\n<pre><code class=\"language-text\">-5\n</code></pre>\n<p>After the parameters, we have:</p>\n<pre><code class=\"language-rust\">-&gt; u32\n</code></pre>\n<p>This tells Rust that the function returns a value whose type is also <code>u32</code>.</p>\n<p>Then:</p>\n<pre><code class=\"language-rust\">{\n    base.saturating_add(bonus)\n}\n</code></pre>\n<p>is the value we return.</p>\n<p>Rust lets the last expression in a function become its return value by leaving off the semicolon.</p>\n<p>So:</p>\n<pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n</code></pre>\n<p>means:</p>\n<blockquote>\n<p>Take the player's base damage and bonus damage, add them together, and return the result.</p>\n</blockquote>\n<p>Quick check before we move on:</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong><code>click_damage</code> has no <code>return</code> keyword. Why does it still hand back a <code>u32</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Its last line is an expression with no semicolon, so that value becomes the result</li>\n<li>Rust returns the first parameter when a function has no <code>return</code></li>\n<li><code>saturating_add</code> always sends its result back to <code>main()</code> on its own</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. Its last line is an expression with no semicolon, so that value becomes the result</p>\n<p>Leaving off the semicolon makes <code>base.saturating_add(bonus)</code> the function's final expression, and the final expression is what the function returns.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"why-not-just-use-base-bonus\">Why not just use <code>base + bonus</code>?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#why-not-just-use-base-bonus\" aria-label=\"Link to this section\">#</a></h2>\n<p>We could. For most ordinary values, both versions give us the same answer. However, <code>u32</code> has a maximum value.</p>\n<p><code>saturating_add()</code> gives our game explicit behavior at that boundary: damage stops at the largest value <code>u32</code> can represent instead of leaving overflow behavior to the build configuration.</p>\n<p>This is a <strong>game-design choice</strong>, not a rule that Rust programs should always use saturating arithmetic.</p>\n<p>We are choosing it because damage is a quantity where hitting the numeric ceiling makes more sense than allowing overflow.</p>\n<p>Try it: run this, then push the ceiling yourself. It should print <code>7</code>, then <code>4294967295</code>, which is the largest <code>u32</code>. Change the second call to <code>click_damage(u32::MAX, 1000)</code> and predict the result before you run it.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try pushing past the ceiling</figcaption><pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn main() {\n    println!(&quot;{}&quot;, click_damage(5, 2));\n    println!(&quot;{}&quot;, click_damage(u32::MAX, 1));\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/skip-hello-world/\">the original article</a>.</p></figure>\n<p>Now swap <code>base.saturating_add(bonus)</code> for <code>base + bonus</code> and run it again. The Playground builds in debug mode, so the second line stops with an overflow panic instead of printing a number.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>In this game, what does <code>saturating_add</code> do when the true total is larger than the biggest <code>u32</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>It stops at the largest <code>u32</code> value</li>\n<li>It wraps around and starts again from <code>0</code></li>\n<li>It turns the result into a negative number</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. It stops at the largest <code>u32</code> value</p>\n<p>\"Saturating\" means the value sticks at the ceiling. A <code>u32</code> can never be negative, and <code>saturating_add</code> never wraps.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"call-the-function\">Call the function<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#call-the-function\" aria-label=\"Link to this section\">#</a></h2>\n<p>Now we can give <code>main()</code> something to do:</p>\n<pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn main() {\n    let damage = click_damage(5, 2);\n\n    println!(\"You dealt {damage} damage!\");\n}\n</code></pre>\n<p>Run:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>You should see:</p>\n<pre><code class=\"language-text\">You dealt 7 damage!\n</code></pre>\n<p>Technically, we did not escape <code>println!()</code> after all.</p>\n<p>But at least we are using it to deal some damage as promised.</p>\n<hr />\n<h2 id=\"give-the-enemy-some-health\">Give the enemy some health<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#give-the-enemy-some-health\" aria-label=\"Link to this section\">#</a></h2>\n<p>Damage is not useful if our enemies do not have a health pool.</p>\n<p>Our second rule will calculate how much health remains after an attack:</p>\n<pre><code class=\"language-rust\">fn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n</code></pre>\n<p>Notice how this looks a lot like our first function.</p>\n<p>It takes:</p>\n<pre><code class=\"language-rust\">health\ndamage\n</code></pre>\n<p>and returns the remaining health.</p>\n<p>Again, we are using saturating arithmetic.</p>\n<p>Why?</p>\n<p>Imagine the enemy has:</p>\n<pre><code class=\"language-text\">3 health\n</code></pre>\n<p>and we deal:</p>\n<pre><code class=\"language-text\">10 damage\n</code></pre>\n<p>For our game, we want the result to be:</p>\n<pre><code class=\"language-text\">0\n</code></pre>\n<p>not:</p>\n<pre><code class=\"language-text\">-7\n</code></pre>\n<p>Health cannot drop below zero.</p>\n<p>So:</p>\n<pre><code class=\"language-rust\">health.saturating_sub(damage)\n</code></pre>\n<p>expresses this game rule clearly.</p>\n<p>Try it: this program should print <code>0</code> twice, once for an enemy with 3 health taking 10 damage and once for exactly 3 damage. Change the first call to <code>health_after_hit(30, 10)</code> and predict the new first line.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try changing the damage</figcaption><pre><code class=\"language-rust\">fn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn main() {\n    println!(&quot;{}&quot;, health_after_hit(3, 10));\n    println!(&quot;{}&quot;, health_after_hit(3, 3));\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/skip-hello-world/\">the original article</a>.</p></figure>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does <code>health_after_hit(3, 10)</code> give <code>0</code> instead of <code>-7</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Rust rounds negative answers up to zero for every subtraction</li>\n<li>A <code>u32</code> cannot be negative, and <code>saturating_sub</code> stops at <code>0</code></li>\n<li>The call fails to compile because <code>10</code> is bigger than <code>3</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. A <code>u32</code> cannot be negative, and <code>saturating_sub</code> stops at <code>0</code></p>\n<p>The rule is a deliberate choice of <code>saturating_sub</code>. Plain subtraction on a <code>u32</code> does not clamp; it would overflow instead.</p>\n</div>\n</div>\n</section>\n<p>Try it:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let damage = click_damage(5, 2);\n    let remaining_health = health_after_hit(10, damage);\n\n    println!(\"You dealt {damage} damage!\");\n    println!(\"Enemy health: {remaining_health}\");\n}\n</code></pre>\n<p>Run again:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>You should see:</p>\n<pre><code class=\"language-text\">You dealt 7 damage!\nEnemy health: 3\n</code></pre>\n<p>Now we are starting to have something that resembles combat.</p>\n<p>Barely. But it counts.</p>\n<hr />\n<h2 id=\"is-the-enemy-defeated\">Is the enemy defeated?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#is-the-enemy-defeated\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our third rule is even smaller:</p>\n<pre><code class=\"language-rust\">fn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n</code></pre>\n<p>This introduces another Rust type:</p>\n<pre><code class=\"language-text\">bool\n</code></pre>\n<p>A boolean has two possible values:</p>\n<pre><code class=\"language-text\">true\nfalse\n</code></pre>\n<p>Our function answers one question:</p>\n<blockquote>\n<p>Has the enemy's health reached zero?</p>\n</blockquote>\n<p>If we call:</p>\n<pre><code class=\"language-rust\">is_defeated(0)\n</code></pre>\n<p>we get:</p>\n<pre><code class=\"language-text\">true\n</code></pre>\n<p>If we call:</p>\n<pre><code class=\"language-rust\">is_defeated(10)\n</code></pre>\n<p>we get:</p>\n<pre><code class=\"language-text\">false\n</code></pre>\n<p>It is time to use it.</p>\n<pre><code class=\"language-rust\">fn main() {\n    let enemy_health = 5;\n\n    let damage = click_damage(5, 2);\n    let remaining_health =\n        health_after_hit(enemy_health, damage);\n\n    println!(\"You dealt {damage} damage!\");\n    println!(\"Enemy health: {remaining_health}\");\n\n    if is_defeated(remaining_health) {\n        println!(\"Enemy defeated!\");\n    }\n}\n</code></pre>\n<p>Run it:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>and now we get:</p>\n<pre><code class=\"language-text\">You dealt 7 damage!\nEnemy health: 0\nEnemy defeated!\n</code></pre>\n<p>Now we have combat rules!</p>\n<p>Tiny ones. But real ones.</p>\n<p><strong>Coding challenge: Armor</strong></p>\n<p>Enemies in an idle game eventually wear armor. Finish <code>damage_after_armor</code> so it subtracts the armor from the damage and never goes below zero, just like <code>health_after_hit</code> does for health. Keep the name and signature of <code>damage_after_armor</code>, and press <strong>Check my code</strong> to see how you are doing.</p>\n<p><strong>Expected result:</strong> the program prints <code>4</code> and then <code>0</code>. As written, the starter prints <code>7</code> and then <code>2</code>.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Finish the armor rule</figcaption><pre><code class=\"language-rust\">fn damage_after_armor(damage: u32, armor: u32) -&gt; u32 {\n    // TODO: subtract the armor from the damage, but never go below 0\n    damage\n}\n\nfn main() {\n    println!(&quot;{}&quot;, damage_after_armor(7, 3));\n    println!(&quot;{}&quot;, damage_after_armor(2, 5));\n}\n\n</code></pre><details class=\"ob-spoiler\"><summary>Spoiler: the checks for this challenge</summary><p>These tests run when you press Check my code on the original article. Reading them first gives away the answer.</p><pre><code class=\"language-rust\">/// The armor should come off the damage. Look at what the function hands back.\n#[test]\nfn armor_reduces_damage() {\n    assert_eq!(damage_after_armor(10, 4), 6);\n}\n\n/// A weak hit against heavy armor is still only a weak hit. Look at what happens when the armor is the bigger number.\n#[test]\nfn never_goes_below_zero() {\n    assert_eq!(damage_after_armor(2, 5), 0);\n}\n\n/// Armor that matches the damage exactly is the edge. Check which side of it your rule lands on.\n#[test]\nfn armor_equal_to_damage() {\n    assert_eq!(damage_after_armor(6, 6), 0);\n}\n</code></pre></details><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/skip-hello-world/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>This is the same shape as <code>health_after_hit</code>. Which method did we use there to subtract without going below zero?</p>\n</aside>\n<hr />\n<h2 id=\"why-make-these-separate-functions\">Why make these separate functions?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#why-make-these-separate-functions\" aria-label=\"Link to this section\">#</a></h2>\n<p>We could have written everything directly inside <code>main()</code>:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let health = 5;\n    let base_damage = 5;\n    let bonus_damage = 2;\n\n    let damage = base_damage + bonus_damage;\n    let remaining_health =\n        health.saturating_sub(damage);\n\n    if remaining_health == 0 {\n        println!(\"Enemy defeated!\");\n    }\n}\n</code></pre>\n<p>That works. Especially for a program this small, and it may even look easier.</p>\n<p>But this game is not going to stay this small.</p>\n<p>Eventually we will need to answer questions like:</p>\n<ul>\n<li>How much damage does the player deal?</li>\n<li>Did the enemy die?</li>\n<li>How much gold should the player receive?</li>\n<li>How much XP did they earn?</li>\n<li>Did they level up?</li>\n<li>How strong should the next enemy be?</li>\n<li>How much passive damage happened while the player was away?</li>\n</ul>\n<p>If all of those rules live inside one giant function, the program gets harder to understand very quickly.</p>\n<p>Instead, we are starting with small pieces that have clear jobs:</p>\n<pre><code class=\"language-text\">click_damage()\nhealth_after_hit()\nis_defeated()\n</code></pre>\n<p>Later, bigger functions will use these functions.</p>\n<p>Then even bigger behavior will use those.</p>\n<p>That is how we are going to build the entire game.</p>\n<p>Not by writing one giant clever function.</p>\n<p>By combining small, boring ones until the result is no longer boring.</p>\n<hr />\n<h2 id=\"testing-the-rules\">Testing the rules<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#testing-the-rules\" aria-label=\"Link to this section\">#</a></h2>\n<p>These functions have another useful property:</p>\n<p>They are easy to test.</p>\n<p>We give them input.</p>\n<p>We check the output.</p>\n<p>At the bottom of <code>main.rs</code>, add:</p>\n<pre><code class=\"language-rust\">#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n}\n</code></pre>\n<p>Run:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>You should see three passing tests.</p>\n<p>We are not going to turn this into a giant testing lesson yet.</p>\n<p>For now, notice how easy it is to verify a small function with a clear input and output.</p>\n<p>That will matter more as the game grows.</p>\n<hr />\n<h2 id=\"the-complete-program\">The complete program<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#the-complete-program\" aria-label=\"Link to this section\">#</a></h2>\n<p>At this point, <code>src/main.rs</code> should look like this:</p>\n<pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(\n    health: u32,\n    damage: u32,\n) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn main() {\n    let enemy_health = 5;\n\n    let damage = click_damage(5, 2);\n\n    let remaining_health =\n        health_after_hit(enemy_health, damage);\n\n    println!(\"You dealt {damage} damage!\");\n    println!(\"Enemy health: {remaining_health}\");\n\n    if is_defeated(remaining_health) {\n        println!(\"Enemy defeated!\");\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n}\n</code></pre>\n<p>This is not much of a game yet.</p>\n<p>And that is okay.</p>\n<p>We now have something more useful than a big pile of code, and we have three rules that we understand.</p>\n<hr />\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Along the way, we have already encountered:</p>\n<ul>\n<li>\n<p><code>fn</code></p>\n</li>\n<li>\n<p>function parameters</p>\n</li>\n<li>\n<p>return types</p>\n</li>\n<li>\n<p><code>u32</code></p>\n</li>\n<li>\n<p><code>bool</code></p>\n</li>\n<li>\n<p><code>let</code></p>\n</li>\n<li>\n<p>expressions</p>\n</li>\n<li>\n<p><code>if</code></p>\n</li>\n<li>\n<p>function calls</p>\n</li>\n<li>\n<p>Cargo</p>\n</li>\n<li>\n<p>basic Rust tests</p>\n</li>\n</ul>\n<p>You do not need to memorize all of that yet. More importantly, we can now describe combat with three small rules:</p>\n<pre><code class=\"language-text\">calculate damage\n       ↓\nreduce health\n       ↓\ncheck for defeat\n</code></pre>\n<p>That is our first little game system.</p>\n<p>And we are keeping it.</p>\n<hr />\n<h2 id=\"where-we-will-take-it-next\">Where we will take it next<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#where-we-will-take-it-next\" aria-label=\"Link to this section\">#</a></h2>\n<p>Right now <code>main()</code> is doing everything — calculating damage, applying it, checking for defeat. Those three steps together represent a single larger idea: an attack.</p>\n<p>Next, we will build that concept as its own function, using the three small functions we already wrote. That is where this project starts to show its real shape.</p>\n<p>Eventually a click in the browser will travel through a Web Component, into WebAssembly, and down into Rust — all the way to:</p>\n<pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n</code></pre>\n<p>Small function. Bigger game.</p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/skip-hello-world/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Skip Hello World.\nCanonical: https://mnixblog.netlify.app/skip-hello-world/\nWrite the First Rules of an Idle Game\nAre you curious about diving into Rust, but are tired of tutorials that stop at Hello World!?\nIn this article, I am going to walk you through writing and testing three tiny Rust functions that do something slightly more interesting: calculate attack damage, reduce an enemy's health, and decide whether the enemy has been defeated.\nYou do not need any previous Rust experience to follow along.\nIf you are able to edit a file and run a command in your terminal, you can jump right in.\nBy the end, we will have the first real rules of an idle game, and these functions will not be throwaway tutorial code. We will keep building with them until they are running inside a complete browser game powered by Rust compiled to WebAssembly.\nIf you are getting excited, then without further ado, it is time to start dealing damage!\nCreate the project\nOur first step is ensuring that Rust is installed on our local machine.\nNeed Rust installed first?\nRust is installed and managed through rustup, the official Rust toolchain installer. Follow the official Rust installation instructions (https://rust-lang.org/install.html) for your operating system, then open a new terminal when the installation finishes.\nYou can check that everything is ready with:\nrustc --version\ncargo --version\nIf both commands print version numbers, we are all set.\nWe can create a new project by typing:\ncargo new rustbound-idle\ncd rustbound-idle\nCargo will create a small Rust application for us:\nrustbound-idle/\n├── Cargo.toml\n└── src/\n└── main.rs\nOpen src/main.rs.\nNotice the traditional Rust greeting:\nfn main() {\nprintln!(\"Hello, world!\");\n}\nIf you want, you can run it once:\ncargo run\nThen chuck it in the bin.\nWe are not here to say hello to enemies.\nWe are here to say goodbye to them.\nOur first game rule\nImagine our player has two sources of attack power:\nbase damage\nbonus damage from upgrades\nCalculating total click damage is just addition.\nAdd this to main.rs:\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nThis is our first Rust function.\nBreaking it apart into bits and pieces:\nfn click_damage(base: u32, bonus: u32) -> u32\nfn means we are defining a function.\nThe function name is:\nclick_damage\nIt takes two values:\nbase: u32\nbonus: u32\nu32 is the type of both values. A type tells Rust what kind of data a value represents and what operations make sense for it.\nThe colon connects the parameter name to its type, so:\nbase: u32\nmeans that base is a value with a type of u32.\nFor now, think of u32 as a non-negative whole number. More specifically, it is an unsigned 32-bit integer.\nSo these are valid u32 values:\n0\n1\n25\n1000\nBut this is not:\n-5\nAfter the parameters, we have:\n-> u32\nThis tells Rust that the function returns a value whose type is also u32.\nThen:\n{\nbase.saturating_add(bonus)\n}\nis the value we return.\nRust lets the last expression in a function become its return value by leaving off the semicolon.\nSo:\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nmeans:\nTake the player's base damage and bonus damage, add them together, and return the result.\nQuick check before we move on:\nQuestion: click_damage has no return keyword. Why does it still hand back a u32?\nIts last line is an expression with no semicolon, so that value becomes the result\nRust returns the first parameter when a function has no return\nsaturating_add always sends its result back to main() on its own\nAnswer: 1. Its last line is an expression with no semicolon, so that value becomes the result\nExplanation: Leaving off the semicolon makes base.saturating_add(bonus) the function's final expression, and the final expression is what the function returns.\nWhy not just use base + bonus?\nWe could. For most ordinary values, both versions give us the same answer. However, u32 has a maximum value.\nsaturating_add() gives our game explicit behavior at that boundary: damage stops at the largest value u32 can represent instead of leaving overflow behavior to the build configuration.\nThis is a game-design choice, not a rule that Rust programs should always use saturating arithmetic.\nWe are choosing it because damage is a quantity where hitting the numeric ceiling makes more sense than allowing overflow.\nTry it: run this, then push the ceiling yourself. It should print 7, then 4294967295, which is the largest u32. Change the second call to click_damage(u32::MAX, 1000) and predict the result before you run it.\nLive example · try pushing past the ceiling\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn main() {\nprintln!(\"{}\", click_damage(5, 2));\nprintln!(\"{}\", click_damage(u32::MAX, 1));\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/skip-hello-world/).\nNow swap base.saturating_add(bonus) for base + bonus and run it again. The Playground builds in debug mode, so the second line stops with an overflow panic instead of printing a number.\nQuestion: In this game, what does saturating_add do when the true total is larger than the biggest u32?\nIt stops at the largest u32 value\nIt wraps around and starts again from 0\nIt turns the result into a negative number\nAnswer: 1. It stops at the largest u32 value\nExplanation: \"Saturating\" means the value sticks at the ceiling. A u32 can never be negative, and saturating_add never wraps.\nCall the function\nNow we can give main() something to do:\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn main() {\nlet damage = click_damage(5, 2);\nprintln!(\"You dealt {damage} damage!\");\n}\nRun:\ncargo run\nYou should see:\nYou dealt 7 damage!\nTechnically, we did not escape println!() after all.\nBut at least we are using it to deal some damage as promised.\nGive the enemy some health\nDamage is not useful if our enemies do not have a health pool.\nOur second rule will calculate how much health remains after an attack:\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nNotice how this looks a lot like our first function.\nIt takes:\nhealth\ndamage\nand returns the remaining health.\nAgain, we are using saturating arithmetic.\nWhy?\nImagine the enemy has:\n3 health\nand we deal:\n10 damage\nFor our game, we want the result to be:\n0\nnot:\n-7\nHealth cannot drop below zero.\nSo:\nhealth.saturating_sub(damage)\nexpresses this game rule clearly.\nTry it: this program should print 0 twice, once for an enemy with 3 health taking 10 damage and once for exactly 3 damage. Change the first call to health_after_hit(30, 10) and predict the new first line.\nLive example · try changing the damage\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn main() {\nprintln!(\"{}\", health_after_hit(3, 10));\nprintln!(\"{}\", health_after_hit(3, 3));\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/skip-hello-world/).\nQuestion: Why does health_after_hit(3, 10) give 0 instead of -7?\nRust rounds negative answers up to zero for every subtraction\nA u32 cannot be negative, and saturating_sub stops at 0\nThe call fails to compile because 10 is bigger than 3\nAnswer: 2. A u32 cannot be negative, and saturating_sub stops at 0\nExplanation: The rule is a deliberate choice of saturating_sub. Plain subtraction on a u32 does not clamp; it would overflow instead.\nTry it:\nfn main() {\nlet damage = click_damage(5, 2);\nlet remaining_health = health_after_hit(10, damage);\nprintln!(\"You dealt {damage} damage!\");\nprintln!(\"Enemy health: {remaining_health}\");\n}\nRun again:\ncargo run\nYou should see:\nYou dealt 7 damage!\nEnemy health: 3\nNow we are starting to have something that resembles combat.\nBarely. But it counts.\nIs the enemy defeated?\nOur third rule is even smaller:\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nThis introduces another Rust type:\nbool\nA boolean has two possible values:\ntrue\nfalse\nOur function answers one question:\nHas the enemy's health reached zero?\nIf we call:\nis_defeated(0)\nwe get:\ntrue\nIf we call:\nis_defeated(10)\nwe get:\nfalse\nIt is time to use it.\nfn main() {\nlet enemy_health = 5;\nlet damage = click_damage(5, 2);\nlet remaining_health =\nhealth_after_hit(enemy_health, damage);\nprintln!(\"You dealt {damage} damage!\");\nprintln!(\"Enemy health: {remaining_health}\");\nif is_defeated(remaining_health) {\nprintln!(\"Enemy defeated!\");\n}\n}\nRun it:\ncargo run\nand now we get:\nYou dealt 7 damage!\nEnemy health: 0\nEnemy defeated!\nNow we have combat rules!\nTiny ones. But real ones.\nCoding challenge: Armor\nEnemies in an idle game eventually wear armor. Finish damage_after_armor so it subtracts the armor from the damage and never goes below zero, just like health_after_hit does for health. Keep the name and signature of damage_after_armor, and press Check my code to see how you are doing.\nExpected result: the program prints 4 and then 0. As written, the starter prints 7 and then 2.\nFinish the armor rule\nfn damage_after_armor(damage: u32, armor: u32) -> u32 {\n// TODO: subtract the armor from the damage, but never go below 0\ndamage\n}\nfn main() {\nprintln!(\"{}\", damage_after_armor(7, 3));\nprintln!(\"{}\", damage_after_armor(2, 5));\n}\n<details>\n<summary>Spoiler: the checks for this challenge</summary>\nThese tests run when you press Check my code on the original article. Reading them first gives away the answer.\n/// The armor should come off the damage. Look at what the function hands back.\n#[test]\nfn armor_reduces_damage() {\nassert_eq!(damage_after_armor(10, 4), 6);\n}\n/// A weak hit against heavy armor is still only a weak hit. Look at what happens when the armor is the bigger number.\n#[test]\nfn never_goes_below_zero() {\nassert_eq!(damage_after_armor(2, 5), 0);\n}\n/// Armor that matches the damage exactly is the edge. Check which side of it your rule lands on.\n#[test]\nfn armor_equal_to_damage() {\nassert_eq!(damage_after_armor(6, 6), 0);\n}\n</details>\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/skip-hello-world/).\nTip:\nThis is the same shape as health_after_hit. Which method did we use there to subtract without going below zero?\nWhy make these separate functions?\nWe could have written everything directly inside main():\nfn main() {\nlet health = 5;\nlet base_damage = 5;\nlet bonus_damage = 2;\nlet damage = base_damage + bonus_damage;\nlet remaining_health =\nhealth.saturating_sub(damage);\nif remaining_health == 0 {\nprintln!(\"Enemy defeated!\");\n}\n}\nThat works. Especially for a program this small, and it may even look easier.\nBut this game is not going to stay this small.\nEventually we will need to answer questions like:\nHow much damage does the player deal?\nDid the enemy die?\nHow much gold should the player receive?\nHow much XP did they earn?\nDid they level up?\nHow strong should the next enemy be?\nHow much passive damage happened while the player was away?\nIf all of those rules live inside one giant function, the program gets harder to understand very quickly.\nInstead, we are starting with small pieces that have clear jobs:\nclick_damage()\nhealth_after_hit()\nis_defeated()\nLater, bigger functions will use these functions.\nThen even bigger behavior will use those.\nThat is how we are going to build the entire game.\nNot by writing one giant clever function.\nBy combining small, boring ones until the result is no longer boring.\nTesting the rules\nThese functions have another useful property:\nThey are easy to test.\nWe give them input.\nWe check the output.\nAt the bottom of main.rs, add:\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n}\nRun:\ncargo test\nYou should see three passing tests.\nWe are not going to turn this into a giant testing lesson yet.\nFor now, notice how easy it is to verify a small function with a clear input and output.\nThat will matter more as the game grows.\nThe complete program\nAt this point, src/main.rs should look like this:\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(\nhealth: u32,\ndamage: u32,\n) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn main() {\nlet enemy_health = 5;\nlet damage = click_damage(5, 2);\nlet remaining_health =\nhealth_after_hit(enemy_health, damage);\nprintln!(\"You dealt {damage} damage!\");\nprintln!(\"Enemy health: {remaining_health}\");\nif is_defeated(remaining_health) {\nprintln!(\"Enemy defeated!\");\n}\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n}\nThis is not much of a game yet.\nAnd that is okay.\nWe now have something more useful than a big pile of code, and we have three rules that we understand.\nWhat we learned\nAlong the way, we have already encountered:\nfn\nfunction parameters\nreturn types\nu32\nbool\nlet\nexpressions\nif\nfunction calls\nCargo\nbasic Rust tests\nYou do not need to memorize all of that yet. More importantly, we can now describe combat with three small rules:\ncalculate damage\n↓\nreduce health\n↓\ncheck for defeat\nThat is our first little game system.\nAnd we are keeping it.\nWhere we will take it next\nRight now main() is doing everything — calculating damage, applying it, checking for defeat. Those three steps together represent a single larger idea: an attack.\nNext, we will build that concept as its own function, using the three small functions we already wrote. That is where this project starts to show its real shape.\nEventually a click in the browser will travel through a Web Component, into WebAssembly, and down into Rust — all the way to:\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nSmall function. Bigger game.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/skip-hello-world/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2026-10-08T22:03:28Z",
      "id": "https://mnixblog.netlify.app/skip-hello-world/",
      "image": "https://mnixblog.netlify.app/images/skip-hello-world/cover.jpg",
      "summary": "Learn Rust by building real game logic instead of stopping at Hello World. Write and test three small functions for damage, health, and enemy defeat.",
      "tags": [
        "Rust",
        "WebAssembly",
        "Game Development",
        "Beginners"
      ],
      "title": "Skip Hello World.",
      "url": "https://mnixblog.netlify.app/skip-hello-world/"
    },
    {
      "content_html": "<hr />\n<p>The last time I posted was on April 28, 2025. I know the exact date because it was the same day I got married.</p>\n<p>Looking back, that might explain why this blog settled down soon after.</p>\n<p>Not because I stopped programming. I was still building, learning, and working through plenty of code. I just quit writing <em>about</em> it.</p>\n<p>As time passed, the blog seemed to drift further down the backlog. When I found myself with some free time, I was more likely to spend it squashing bugs, working on a project, or learning something new than writing about any of it.</p>\n<p>So before I start writing about what comes next, I should probably explain what has happened since then.</p>\n<p>Because it was a year.</p>\n<h2 id=\"first-a-tiny-detail-i-forgot-to-mention\">First, a tiny detail I forgot to mention<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#first-a-tiny-detail-i-forgot-to-mention\" aria-label=\"Link to this section\">#</a></h2>\n<p>While participating in a <a href=\"https://chingu.io\">Chingu</a> Voyage, I met my wife.</p>\n<p>Yes, <em>that</em> Chingu.</p>\n<p>The online platform that put together teams of software engineers and agile leaders to build projects suitable to their abilities, argue politely about Git branches, and every once in a while spend twenty minutes figuring out who forgot to pull before opening a PR.</p>\n<p>Before this past year, I had completed several Chingu Voyages, working as a developer and a product owner, and even as a mentor. One of those Voyages just happened to work out particularly well.</p>\n<p>I met her.</p>\n<p>As far as I am aware, almost nobody over there knows about it. It never really came up, and there never seemed to be a natural moment in a Discord channel to mention it. Plus it all escalated well after that Voyage ended.</p>\n<p>This blog has always been about my experience writing code and sharing the lessons I've learned in order to help other developers grow and tap into their potential.</p>\n<p>So this may seem like it comes out of left field, but Chingu gave me teammates, portfolio projects, experience working on software teams...</p>\n<p>...and a wife.</p>\n<p>Maybe not in that order.</p>\n<p>You might say that it was a successful Voyage.</p>\n<h2 id=\"before-the-baby-there-was-baby-book\">Before the Baby, There Was Baby Book<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#before-the-baby-there-was-baby-book\" aria-label=\"Link to this section\">#</a></h2>\n<p>Before summer could even arrive, we found out our lives were about to change again!</p>\n<p>We found out we were expecting a son, due November 7. We began to think about all of the little things that come with two people becoming parents.</p>\n<p>Naturally, we both looked at each other and said:</p>\n<blockquote>\n<p>We could probably build an app for this.</p>\n</blockquote>\n<p>So we began working on an idea we called <strong>Baby Book</strong>.</p>\n<p>In the first phase, it was only a household task and grocery list tracking app. The long-term plan was to track milestones, achievements, appointments, and eventually the little things one parent might miss while the other was there—so we could stay on the same page.</p>\n<p>More or less, we set out to build a shared record of our new favorite person's life as it began to unfold.</p>\n<p>We never did polish the application to the original design plan, but for a little while it was actually useful.</p>\n<p>When she went into labor, I threw together a quick feature for helping her with breathing and contractions. When our son was born, Baby Book shifted toward the realities of parenthood. We started tracking feeding times and details, diaper changes, and little notes.</p>\n<p>It did not take long before we did what most new parents probably do.</p>\n<p>We fell into a rhythm and stopped stressing over those details as much.</p>\n<p>Our need for this app faded, and Baby Book quietly moved into the category titled:</p>\n<blockquote>\n<p>I should really come back to this someday.</p>\n</blockquote>\n<p>And I still may.</p>\n<p>Some of the original idea could still be useful today, especially around milestones and giving us an easy way to share the little things the other parent might miss.</p>\n<p>But it is not exactly at the top of the backlog right now.</p>\n<h2 id=\"a-wild-son-has-appeared\">A Wild Son Has Appeared<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#a-wild-son-has-appeared\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our son was supposed to arrive on <strong>November 7</strong>.</p>\n<p>Apparently, he had his own release schedule. He arrived <strong>October 12</strong>, about four weeks early.</p>\n<p>Anybody who has had an infant can probably relate to what happened to my schedule next. If you haven't, imagine handing your calendar to a tiny version of yourself that only speaks babble, uses his mouth to read, and thinks 3:17 in the morning is the perfect time to discuss politics.</p>\n<p>That is roughly the system, right?</p>\n<p>The funny part is that another fairly important date had happened only five days earlier.</p>\n<p>On <strong>October 7</strong>, I was accepted into Code the Dream's React course.</p>\n<h2 id=\"code-the-dream-was-already-in-motion\">Code the Dream Was Already in Motion<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#code-the-dream-was-already-in-motion\" aria-label=\"Link to this section\">#</a></h2>\n<p>I had actually applied to <a href=\"https://codethedream.org/\"><strong>Code the Dream</strong></a> months earlier, around April 2025.</p>\n<p>The timing of the whole thing could have been a little better. But it is not like I brought the baby home, learned a thing or two about our new tiny boss, and decided:</p>\n<blockquote>\n<p>You know what this situation needs?</p>\n</blockquote>\n<blockquote>\n<p>Another commitment.</p>\n</blockquote>\n<p>The decision had already been made.</p>\n<p>I had tested out of the Intro to Programming course, which is how I ended up going directly into React.</p>\n<p>By this point, I knew that it was going to be a challenge, but it seemed reasonable enough.</p>\n<p>Strangely enough, his early arrival may actually have made the start of the course easier. I lost some preparation time, but I gained a few weeks to settle into being a parent before React started. And that made a big difference.</p>\n<p>By the time the React cohort started, I was not deciding whether I wanted to begin another programming course while raising a newborn. I had already committed to the course.</p>\n<p>Now I just had to figure out what continuing looked like under completely different circumstances.</p>\n<p>The answer was very different from the way I had worked before.</p>\n<p>Long, uninterrupted coding sessions became harder to find. Development started happening in smaller windows: thirty minutes here, an hour there, whatever time happened to exist between everything else.</p>\n<p>If twenty minutes of a thirty-five-minute session were spent trying to remember what I had been doing the last time I opened the project, that was a problem.</p>\n<p>So I started leaving myself better notes. I broke problems into smaller pieces. I became more deliberate about commits and less interested in keeping an entire feature in my head.</p>\n<p>Without really planning to, becoming a parent forced me to get better at stopping halfway through something while leaving enough breadcrumbs that future me could figure out what past me had been doing.</p>\n<p>And then React started.</p>\n<h2 id=\"react-the-second-time-around\">React, the Second Time Around<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#react-the-second-time-around\" aria-label=\"Link to this section\">#</a></h2>\n<p>I joined Code the Dream's React v3 course as part of the <strong>Lark Cohort</strong>, which ran from November through February.</p>\n<p>Code the Dream was not where I first learned React, or where I first built a full-stack application. I had already built MERN applications, worked with APIs, Express, and MongoDB, and completed several development projects with other people through Chingu.</p>\n<p>What CTD gave me was something different.</p>\n<p>It gave me several months to go back through technologies I already knew and find the places where my understanding was thinner than I realized.</p>\n<p>One of those places was <strong>Context</strong>.</p>\n<p>I had used Context before and understood what it did, but there is a difference between knowing that Context exists and getting comfortable designing state around it, combining it with reducers, deciding what actually belongs there, and thinking about what happens as your component tree gets larger.</p>\n<p>Sometimes learning something for the second time is a lot more useful than learning it the first time.</p>\n<p>The first time, you are mostly trying to make it work.</p>\n<p>The second time, you start asking why it works that way.</p>\n<p>That became a recurring theme during the course. It was less:</p>\n<blockquote>\n<p>Here is React.</p>\n</blockquote>\n<p>and more:</p>\n<blockquote>\n<p>You know React. Now let's find the parts you have been getting away with not understanding deeply enough.</p>\n</blockquote>\n<p>My guided project grew throughout the course. Reducers came in, Context became more important, routing got more complicated, external data got involved, state crossed more boundaries, and testing became a larger part of the way I worked.</p>\n<p>Eventually something I had built myself six weeks earlier could surprise me, which is always reassuring.</p>\n<p>Somewhere during that process, the question I asked after completing something changed.</p>\n<p>It used to be:</p>\n<blockquote>\n<p>Does it work?</p>\n</blockquote>\n<p>I would click around a few times, see that everything looked fine, and move on.</p>\n<p>Now I am much more likely to ask:</p>\n<blockquote>\n<p>Okay, it works. What breaks it?</p>\n</blockquote>\n<p>What happens if the request fails? What happens if the user refreshes here? What if this state has not hydrated yet? What happens if the component disappears before an asynchronous operation finishes? What happens if somebody clicks the same button five times while the first request is still running?</p>\n<p>The happy path is still important.</p>\n<p>I just trust it a lot less than I used to.</p>\n<h2 id=\"then-i-went-back-through-the-backend\">Then I Went Back Through the Backend<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#then-i-went-back-through-the-backend\" aria-label=\"Link to this section\">#</a></h2>\n<p>After React, I moved into Code the Dream's <strong>Node 26.2 Cohort</strong>, which ran from late March through June.</p>\n<p>Again, Node and Express were not new territory. I had already built full-stack applications and worked with the MERN stack, but going through a dedicated backend course gave me an opportunity to spend more time on the details instead of treating the backend as whatever needed to exist so the frontend could work.</p>\n<p>We spent time with routes, middleware, authentication, validation, error handling, HTTP semantics, database access, and application structure.</p>\n<p>One technology I got a lot more practice with was <strong>Prisma</strong>.</p>\n<p>I had used Prisma before CTD, but not enough that I would have called myself particularly comfortable with it. CTD gave me more opportunities to use it deliberately instead of simply following the happy path through an ORM.</p>\n<p>That distinction mattered.</p>\n<p>There are a lot of technologies I have <em>used</em>. That does not necessarily mean I understand all of the decisions they are making for me.</p>\n<p>Knowing how to use a tool is one level.</p>\n<p>Knowing what that tool is abstracting away is another.</p>\n<h2 id=\"then-came-practicum\">Then Came Practicum<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#then-came-practicum\" aria-label=\"Link to this section\">#</a></h2>\n<p>After finishing the coursework, I joined Code the Dream's <strong>Summer 2026 Practicum</strong>.</p>\n<p>This part felt familiar.</p>\n<p>I had already done several team projects through Chingu, so practicum was not my introduction to collaboration, Git, pull requests, requirements, product decisions, or the occasional joy of discovering two developers had spent an afternoon independently solving the same problem.</p>\n<p>What made practicum interesting was the amount of structure we had to build around the development itself.</p>\n<p>Our team built <a href=\"https://github.com/Code-the-Dream-School/summer-26-js-practicum-team2\"><strong>Sprout</strong></a>, a financial literacy application.</p>\n<p>The easiest way I have found to describe it is something like Duolingo pointed at personal finance: short lessons, quizzes, progress tracking, onboarding, and a guided learning path.</p>\n<p>The idea was to make a subject people know they <em>should</em> understand feel less like sitting down with a textbook, because almost nobody wakes up Saturday morning thinking:</p>\n<blockquote>\n<p>You know what would really make this weekend? Budgeting terminology.</p>\n</blockquote>\n<p>Sprout was not the project I would have chosen on my own, and I think that made the experience more interesting.</p>\n<p>When something is your personal project, you can build whatever you want. On a team, the product does not belong entirely to you. You have to build the thing the group agreed to build.</p>\n<p>That is a different skill.</p>\n<h2 id=\"before-we-built-much-of-anything-we-built-a-backlog\">Before We Built Much of Anything, We Built a Backlog<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#before-we-built-much-of-anything-we-built-a-backlog\" aria-label=\"Link to this section\">#</a></h2>\n<p>One of the first things I did was start planning.</p>\n<p>And then I planned some more.</p>\n<p>And then apparently I decided Jira had not suffered enough, because I built a pretty enormous backlog.</p>\n<p>There are still plenty of tickets sitting in it that we never touched, and that was intentional. A backlog is not a promise that everything gets built. It is a record of things that <em>could</em> get built.</p>\n<p>We split the core work across sprint weeks and broke larger ideas into epics, user stories, and smaller tasks. A Jira ticket would often become a GitHub issue. That issue gave us something concrete to build around and could then map to a branch.</p>\n<p>Eventually there was a clear line connecting:</p>\n<p><strong>idea → epic → story → issue → branch → pull request → application</strong></p>\n<p>That process became one of the more valuable parts of practicum for me because a large application does not get built by somebody opening an editor and thinking:</p>\n<blockquote>\n<p>Today I shall implement the application.</p>\n</blockquote>\n<p>You build a small piece. Then another. Then another.</p>\n<p>Sometimes several developers were contributing to the same larger feature, so we would use what amounted to a parent or integration branch. Instead of everybody immediately merging their work into <code>development</code>, we could combine related work there first, get the pieces cooperating, fix whatever exploded, and then merge the larger feature.</p>\n<p>It added another layer to the Git workflow, but it also prevented <code>development</code> from becoming quite as adventurous.</p>\n<h2 id=\"we-never-really-finished-an-epic\">We Never Really \"Finished\" an Epic<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#we-never-really-finished-an-epic\" aria-label=\"Link to this section\">#</a></h2>\n<p>Something else became apparent pretty quickly: epics are big.</p>\n<p>Really big.</p>\n<p>When you only have a limited number of weeks, \"complete the epic\" is often the wrong goal. So we sliced useful pieces out of them instead.</p>\n<p>One week we might improve onboarding. Later, we would come back and add something else. The learning path grew incrementally. Admin tools grew incrementally. Accounts grew incrementally. Testing grew incrementally.</p>\n<p>Even now, I do not think I could point at many of our larger epics and say:</p>\n<blockquote>\n<p>Yep. Finished forever.</p>\n</blockquote>\n<p>There are still stories sitting in the backlog, but the application works.</p>\n<p>That taught me something useful about MVP development. An epic does not have to be complete for part of it to create value. Sometimes the right feature is simply the smallest useful slice of a much larger idea.</p>\n<p>In a strange way, I suppose Baby Book had already taught me a smaller version of that lesson.</p>\n<p>It never became the application I originally imagined, but for a brief period the pieces we actually needed were useful.</p>\n<p>Sometimes software has already done its job before you finish everything you once imagined for it.</p>\n<h2 id=\"then-sprout-became-complicated\">Then Sprout Became Complicated<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#then-sprout-became-complicated\" aria-label=\"Link to this section\">#</a></h2>\n<p>Sprout used React on the frontend, Node and Express on the backend, and MongoDB underneath. We used Tailwind CSS and eventually created a collection of shared components so the UI could stay relatively consistent while different developers worked on different screens.</p>\n<p>That became important very quickly.</p>\n<p>Without shared components, five developers can somehow invent seven buttons. Each one is slightly different; none of them are necessarily wrong, and together they make the application look like it was assembled by committee.</p>\n<p>Because it was.</p>\n<p>Shared components gave us some guardrails, but the more interesting architectural decisions happened around the curriculum.</p>\n<p>Several members of the team worked together to build our learning material into structured JSON, and a huge part of the application eventually revolved around that structure: lessons, sections, quizzes, questions, answers, and progress.</p>\n<p>The JSON became something close to a content engine for the application.</p>\n<p>For a while, that worked really well.</p>\n<p>Then we started asking uncomfortable questions. Questions like:</p>\n<blockquote>\n<p>Should the correct answers to a quiz really be sitting somewhere the browser can inspect?</p>\n</blockquote>\n<p>Probably not.</p>\n<h2 id=\"one-architecture-decision-created-a-whole-new-category-of-bugs\">One Architecture Decision Created a Whole New Category of Bugs<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#one-architecture-decision-created-a-whole-new-category-of-bugs\" aria-label=\"Link to this section\">#</a></h2>\n<p>Eventually we started integrating more of the curriculum data with MongoDB.</p>\n<p>Part of the motivation was protecting quiz answers. If the browser receives everything—including which answer is correct—then somebody who knows their way around developer tools can potentially skip the whole \"learning\" portion of the learning application.</p>\n<p>Which feels slightly against the spirit of the thing.</p>\n<p>Moving more responsibility to the backend made sense, but architectural decisions rarely arrive alone. This one brought friends: more requests, more synchronization, more state, more opportunities for the frontend and backend to disagree, performance concerns, and some really annoying bugs.</p>\n<p>We solved one problem and introduced an entirely different class of problems.</p>\n<p>That does not necessarily mean the decision was wrong. It means the decision had a cost, and that might be one of the most useful things Sprout reinforced for me.</p>\n<p>Architecture is usually not:</p>\n<blockquote>\n<p>Which solution is correct?</p>\n</blockquote>\n<p>It is more often:</p>\n<blockquote>\n<p>Which set of problems would you rather have?</p>\n</blockquote>\n<p>I may revisit that part of Sprout in the future because I think there are better ways we could protect assessment data without making parts of the experience as expensive or fragile as they became.</p>\n<p>But that is also why I am glad we tried it.</p>\n<p>You learn a lot more from an architectural decision that creates unexpected problems than one that only exists in a diagram.</p>\n<h2 id=\"testing-was-only-half-of-what-i-enjoyed\">Testing Was Only Half of What I Enjoyed<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#testing-was-only-half-of-what-i-enjoyed\" aria-label=\"Link to this section\">#</a></h2>\n<p>Testing became a major part of my role on the project. I worked with <strong>Jest, Vitest, React Testing Library, and Playwright</strong>, and I liked the way testing forced us to think beyond the exact sequence of events we pictured when writing a feature.</p>\n<p>What happens if the component unmounts? What happens if the network request fails? What if authentication has not finished hydrating? What if somebody double-clicks? What if the frontend thinks a lesson is complete and the backend disagrees?</p>\n<p>We found all of those.</p>\n<p>But something else ended up being just as interesting to me:</p>\n<p><strong>CI.</strong></p>\n<p>I really enjoyed working with <strong>GitHub Actions</strong> and getting our tests running automatically as part of the development workflow.</p>\n<p>There is something deeply satisfying about turning:</p>\n<blockquote>\n<p>Somebody should probably run the tests before we merge this.</p>\n</blockquote>\n<p>into:</p>\n<blockquote>\n<p>The repository literally will not let us forget.</p>\n</blockquote>\n<p>That felt like another step beyond simply knowing how to write tests.</p>\n<p>A test suite sitting on somebody's laptop is useful. A test suite that runs automatically against changes is infrastructure.</p>\n<p>Once I started working on that side of the project, I found myself liking it quite a bit: tests, coverage, automated checks, GitHub Actions, deployment workflows, and all the little pieces that make it harder for a team to accidentally ship something broken.</p>\n<p>None of those things are particularly flashy.</p>\n<p>But they make everything else easier to trust.</p>\n<h2 id=\"some-bugs-deserve-their-own-zip-code\">Some Bugs Deserve Their Own ZIP Code<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#some-bugs-deserve-their-own-zip-code\" aria-label=\"Link to this section\">#</a></h2>\n<p>We still had plenty of bugs.</p>\n<p>Some were straightforward, friendly bugs. The sort that basically introduce themselves:</p>\n<blockquote>\n<p>Hello. I am on line 47.</p>\n</blockquote>\n<p>Those are wonderful.</p>\n<p>Others required the application to be in exactly the wrong state at exactly the right time.</p>\n<p>We had authentication behavior where the frontend could decide where a user should be redirected before it had finished determining whether the user was authenticated. Everything worked unless the timing changed, which is always comforting.</p>\n<p>We had asynchronous quiz operations where work could finish after the component that started it no longer existed. We had buttons users could click multiple times while a request was still running. We had lesson progression and quiz completion logic with occasionally incompatible definitions of reality.</p>\n<p>Some of those bugs looked completely fine when you clicked through the application manually.</p>\n<p>Then an automated test created the right sequence of events and everything fell apart.</p>\n<p>Beautiful.</p>\n<p>Those are the problems I enjoy the most now: the ones where the solution requires understanding the system rather than simply finding a typo.</p>\n<h2 id=\"git-got-more-interesting-too\">Git Got More Interesting Too<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#git-got-more-interesting-too\" aria-label=\"Link to this section\">#</a></h2>\n<p>Working by yourself gives you a very misleading impression of Git.</p>\n<p>You make a branch, write some code, commit it, merge it, and suddenly you feel pretty good about version control.</p>\n<p>Working with several developers changes the situation, especially once branches start depending on other branches.</p>\n<p>Suddenly Git becomes less:</p>\n<blockquote>\n<p>Save my work.</p>\n</blockquote>\n<p>and more:</p>\n<blockquote>\n<p>Which version of this application represents reality right now?</p>\n</blockquote>\n<p>The parent-branch strategy helped with that. So did smaller pull requests, better issues, and simply talking to each other before two people changed the same area of the application.</p>\n<p>That is the funny thing about Git problems.</p>\n<p>A surprising number of them are actually communication problems that happen to eventually produce merge conflicts.</p>\n<h2 id=\"practicum-felt-familiar-because-of-chingu\">Practicum Felt Familiar Because of Chingu<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#practicum-felt-familiar-because-of-chingu\" aria-label=\"Link to this section\">#</a></h2>\n<p>A lot of practicum reminded me of <strong>Chingu Voyages</strong>, which makes sense because I had already done several Voyages as both a developer and a Product Owner.</p>\n<p>Chingu had already taught me that two developers can read the same requirement and walk away with completely different applications in their heads. It had already shown me how important communication becomes once multiple people share a repository, and it had already exposed me to team roles and product planning.</p>\n<p>The two programs just approached those experiences differently.</p>\n<h2 id=\"chingu-does-a-great-job-simulating-a-product-team\">Chingu Does a Great Job Simulating a Product Team<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#chingu-does-a-great-job-simulating-a-product-team\" aria-label=\"Link to this section\">#</a></h2>\n<p>One thing I genuinely like about Chingu is that a Voyage can include several different roles.</p>\n<p>Developers may work alongside a <strong>Product Owner, Scrum Master, and UI/UX Designer</strong>, which creates some separation between responsibilities. A designer is concentrating on how something should look and behave. A Product Owner is shaping priorities and requirements. A Scrum Master helps keep the process moving.</p>\n<p>Developers then have to take those ideas and make them real.</p>\n<p>And eventually everybody discovers that a button somebody drew in thirty seconds somehow requires half a day to implement correctly.</p>\n<p>That structure is useful.</p>\n<p>The challenge, particularly in some lower tiers, is that experience levels among developers can vary quite a bit. Everybody might technically satisfy the requirements for the Voyage while having very different levels of comfort with JavaScript, Git, architecture, APIs, or debugging.</p>\n<p>Sometimes you discover those differences while trying to ship something.</p>\n<p>Which, to be fair, is also pretty realistic.</p>\n<h2 id=\"code-the-dream-gave-us-a-different-baseline\">Code the Dream Gave Us a Different Baseline<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#code-the-dream-gave-us-a-different-baseline\" aria-label=\"Link to this section\">#</a></h2>\n<p>Code the Dream's practicum had another advantage: everyone had already completed defined coursework.</p>\n<p>That did not make everyone's skills identical—not even close—but it gave us shared reference points. We had all traveled through at least some of the same material before arriving at the project.</p>\n<p>The bigger difference was that we did not have every traditional product role separated out. We were mostly developers, so we had to wear more hats ourselves.</p>\n<p>Someone had to think about product. Someone had to think about design. Someone had to organize the work and maintain the backlog. Someone had to worry about whether CI was working.</p>\n<p>Usually everybody ended up doing more than whatever role they originally imagined themselves doing.</p>\n<p>And I think that was good for us.</p>\n<h2 id=\"the-people-part\">The People Part<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#the-people-part\" aria-label=\"Link to this section\">#</a></h2>\n<p><a href=\"https://www.linkedin.com/in/berenice-rojas\"><strong>Berenice</strong></a> stepped in beside me as a co-project lead, and that helped me more than I probably said at the time. She even played a role in designing the UI/UX.</p>\n<p>She gave me somebody to bounce ideas off of while we built epics, shaped user stories, created tasks, organized Jira, and tried to decide which pieces actually mattered enough to belong in our MVP.</p>\n<p>Berenice and I worked together to decide which parts were actually critical for an MVP. Those moved forward; the rest could wait.</p>\n<p>There are a lot of small judgment calls hidden inside project planning. Having another person there to challenge an idea—or confirm that I was not completely overthinking it—made that a lot easier.</p>\n<p><a href=\"http://www.linkedin.com/in/kristen-wishart\"><strong>Kristen</strong></a> and <a href=\"https://github.com/BytesOfStrength\"><strong>Maryzabeth</strong></a> took on another enormous part of the application.</p>\n<p>They worked together on our initial low-fidelity wireframes, which became high-fidelity wireframes and eventually the foundation for the UI and UX. Then they worked together on developing the curriculum that became the content driving much of Sprout.</p>\n<p>Kristen had a particularly good instinct for keeping us connected to the original vision. Software developers are exceptionally talented at taking a reasonable idea and turning it into fourteen features, three dashboards, a notification system, and possibly a social network.</p>\n<p>Sometimes somebody has to say:</p>\n<blockquote>\n<p>We probably do not need that.</p>\n</blockquote>\n<p>Kristen was good at that. A lot of her vision made it into the final product precisely because she was willing to pull us back toward the application we had actually agreed to build.</p>\n<p>Maryzabeth became one of those people every remote team quietly depends on. She contributed across both frontend and backend work, but she also became our unofficial secretary.</p>\n<p>She took notes, documented decisions, and if somebody missed a meeting or standup, she was usually the person making sure they knew what happened.</p>\n<p>That sounds like a small thing until you work on a distributed team.</p>\n<p>Then you discover that keeping everybody operating from the same version of reality is basically a feature of its own.</p>\n<p>Everybody ended up bringing something beyond code, and that is the part of team development I find most interesting.</p>\n<h2 id=\"we-also-had-actual-adults-in-the-room\">We Also Had Actual Adults in the Room<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#we-also-had-actual-adults-in-the-room\" aria-label=\"Link to this section\">#</a></h2>\n<p>Code the Dream pairs practicum teams with mentors, and our team had <a href=\"https://www.linkedin.com/in/hectorfgonzalez/\"><strong>Hector</strong></a> and <a href=\"https://www.linkedin.com/in/mntri4/\"><strong>Mario</strong></a>.</p>\n<p>Hector brought real-world software development experience into our conversations, while Mario brought a lot of optimism and was always willing to listen while we worked our way through something.</p>\n<p>What helped most was rarely somebody simply handing us an answer. Usually it was somebody with more experience asking a question we had not asked ourselves:</p>\n<blockquote>\n<p>Why did you build it this way?</p>\n</blockquote>\n<p>Sometimes we had a very good answer.</p>\n<p>Sometimes the answer turned out to be:</p>\n<blockquote>\n<p>Because that is how it currently is.</p>\n</blockquote>\n<p>Those conversations were useful too.</p>\n<h2 id=\"so-what-actually-changed\">So What Actually Changed?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#so-what-actually-changed\" aria-label=\"Link to this section\">#</a></h2>\n<p>This is probably where I would describe the year differently than I would have twelve months ago.</p>\n<p>I did not learn that teams exist. I had already worked on teams. I did not learn how to build a full-stack application. I had already built them. I did not discover MERN. I already knew it.</p>\n<p>What changed was the level at which I started thinking about all of those things.</p>\n<p>I understand React Context better than I did. I am much more comfortable with testing. I have more practical experience with Prisma. I learned more about CI and GitHub Actions than I expected to, and I got more interested in the infrastructure surrounding the code.</p>\n<p>I spent more time thinking about how work gets broken down before anyone writes it. I became more deliberate about branch strategies when several pieces of one feature were being developed at the same time. I watched an architectural decision solve one problem while creating three new ones, and I saw how much a shared component library matters when several developers are building the same UI.</p>\n<p>More than anything, I got another reminder that software development is full of tradeoffs disguised as decisions.</p>\n<p>Maybe that is the biggest thing I got out of this year.</p>\n<p>I did not necessarily add another stack to the list of technologies I know.</p>\n<p>I got better at seeing the system around them.</p>\n<h2 id=\"the-other-lesson-had-nothing-to-do-with-code\">The Other Lesson Had Nothing to Do With Code<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#the-other-lesson-had-nothing-to-do-with-code\" aria-label=\"Link to this section\">#</a></h2>\n<p>I also learned that progress does not require the schedule I thought it did.</p>\n<p>For a long time, I pictured serious development work happening in large, uninterrupted blocks: a free Saturday, several hours at a desk, headphones on, everything lined up.</p>\n<p>That sounds wonderful.</p>\n<p>It describes almost none of what the past year looked like.</p>\n<p>A lot of this year happened in pieces. Thirty minutes here. An hour after bedtime. A bug before dinner. A pull request during nap time. Reading yesterday's notes because I had absolutely no idea what the version of me from forty-eight hours ago had been thinking.</p>\n<p>And somehow it still added up.</p>\n<p>I used to think real progress required long stretches of uninterrupted time. It turns out a surprising amount of it comes from getting good at returning.</p>\n<p>Open the laptop. Find the note. Read the issue. Make the next change. Commit it. Come back later.</p>\n<p>That is not the montage version of learning software development.</p>\n<p>It is probably a lot closer to the real one.</p>\n<h2 id=\"the-part-that-made-any-of-this-possible\">The Part That Made Any of This Possible<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#the-part-that-made-any-of-this-possible\" aria-label=\"Link to this section\">#</a></h2>\n<p>Outside the project, my wife <a href=\"https://www.linkedin.com/in/jayanti-hari/\"><strong>Jayanti</strong></a> listened to far more discussion about failed tests, authentication bugs, GitHub Actions, pull requests, MongoDB, Render deployments, and merge conflicts than any reasonable marriage contract should require.</p>\n<p>She was also there through the much more important part of the year while we figured out how to become parents together.</p>\n<p>That included the unfinished Baby Book, the last-minute contraction feature, sleep schedules that were mostly theoretical, and plenty of development sessions squeezed into whatever part of the day happened to survive.</p>\n<p>It has been a year.</p>\n<p>A really good one, a very busy one, and often a very tired one.</p>\n<h2 id=\"so-what-s-next\">So... What's Next?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#so-what-s-next\" aria-label=\"Link to this section\">#</a></h2>\n<p>Practicum has wrapped.</p>\n<p>I may keep tinkering with Sprout. There are still architectural decisions I want to revisit, experiments I want to try, and a Jira backlog large enough to keep several imaginary development teams busy.</p>\n<p>But I am also ready to build other things.</p>\n<p>I have a Rust game I keep coming back to. I want to contribute to open source properly. Baby Book is still sitting somewhere in the back of my head, too.</p>\n<p>And I have a growing list of things from this past year that deserve articles of their own: CI pipelines, testing patterns, authentication races, architecture mistakes, Prisma, React, Rust, and the sort of problems where the first draft of the article usually begins with:</p>\n<blockquote>\n<p>I thought this would be easy.</p>\n</blockquote>\n<p>Those are usually the interesting ones.</p>\n<p>I have also applied to <strong>Code the Dream's apprenticeship program</strong>.</p>\n<p>I do not know yet whether I will get in, and strangely, I am okay leaving that part of the story there.</p>\n<p>Not knowing.</p>\n<p>Because that is actually where I am.</p>\n<p>There is a tendency when writing about learning software development to wait until you know how the story ends. Get the job. Finish the project. Earn the certificate. Then go back and write the neat version where everything looks like it was leading somewhere obvious.</p>\n<p>Real life rarely gives you that version while you are living it.</p>\n<p>Right now, I have finished one thing and I am waiting to find out what comes next. Maybe that is the apprenticeship. Maybe it is a job. Maybe it is open source.</p>\n<p>Maybe I disappear into Rust for six months and emerge with something deeply unnecessary.</p>\n<p>There are worse outcomes.</p>\n<p>What I do know is that I finally want to write again, and I have a lot more to write about than I did when this blog went quiet.</p>\n<p>So <em>Journey to Code</em> is back.</p>\n<p>Probably a little slower. Definitely with more interruptions.</p>\n<p>But I think I am okay with that now.</p>\n<p>The last year taught me that progress does not always look like moving quickly. Sometimes it just means you keep coming back.</p>\n<p>And, conveniently, I just did.</p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "What a Long, Strange Trip...\nCanonical: https://mnixblog.netlify.app/what-a-long-strange-trip/\nA wedding, a newborn, Code the Dream, Sprout, and a year of commits I never quite found time to write about.\nThe last time I posted was on April 28, 2025. I know the exact date because it was the same day I got married.\nLooking back, that might explain why this blog settled down soon after.\nNot because I stopped programming. I was still building, learning, and working through plenty of code. I just quit writing about it.\nAs time passed, the blog seemed to drift further down the backlog. When I found myself with some free time, I was more likely to spend it squashing bugs, working on a project, or learning something new than writing about any of it.\nSo before I start writing about what comes next, I should probably explain what has happened since then.\nBecause it was a year.\nFirst, a tiny detail I forgot to mention\nWhile participating in a Chingu (https://chingu.io) Voyage, I met my wife.\nYes, that Chingu.\nThe online platform that put together teams of software engineers and agile leaders to build projects suitable to their abilities, argue politely about Git branches, and every once in a while spend twenty minutes figuring out who forgot to pull before opening a PR.\nBefore this past year, I had completed several Chingu Voyages, working as a developer and a product owner, and even as a mentor. One of those Voyages just happened to work out particularly well.\nI met her.\nAs far as I am aware, almost nobody over there knows about it. It never really came up, and there never seemed to be a natural moment in a Discord channel to mention it. Plus it all escalated well after that Voyage ended.\nThis blog has always been about my experience writing code and sharing the lessons I've learned in order to help other developers grow and tap into their potential.\nSo this may seem like it comes out of left field, but Chingu gave me teammates, portfolio projects, experience working on software teams...\n...and a wife.\nMaybe not in that order.\nYou might say that it was a successful Voyage.\nBefore the Baby, There Was Baby Book\nBefore summer could even arrive, we found out our lives were about to change again!\nWe found out we were expecting a son, due November 7. We began to think about all of the little things that come with two people becoming parents.\nNaturally, we both looked at each other and said:\nWe could probably build an app for this.\nSo we began working on an idea we called Baby Book.\nIn the first phase, it was only a household task and grocery list tracking app. The long-term plan was to track milestones, achievements, appointments, and eventually the little things one parent might miss while the other was there—so we could stay on the same page.\nMore or less, we set out to build a shared record of our new favorite person's life as it began to unfold.\nWe never did polish the application to the original design plan, but for a little while it was actually useful.\nWhen she went into labor, I threw together a quick feature for helping her with breathing and contractions. When our son was born, Baby Book shifted toward the realities of parenthood. We started tracking feeding times and details, diaper changes, and little notes.\nIt did not take long before we did what most new parents probably do.\nWe fell into a rhythm and stopped stressing over those details as much.\nOur need for this app faded, and Baby Book quietly moved into the category titled:\nI should really come back to this someday.\nAnd I still may.\nSome of the original idea could still be useful today, especially around milestones and giving us an easy way to share the little things the other parent might miss.\nBut it is not exactly at the top of the backlog right now.\nA Wild Son Has Appeared\nOur son was supposed to arrive on November 7.\nApparently, he had his own release schedule. He arrived October 12, about four weeks early.\nAnybody who has had an infant can probably relate to what happened to my schedule next. If you haven't, imagine handing your calendar to a tiny version of yourself that only speaks babble, uses his mouth to read, and thinks 3:17 in the morning is the perfect time to discuss politics.\nThat is roughly the system, right?\nThe funny part is that another fairly important date had happened only five days earlier.\nOn October 7, I was accepted into Code the Dream's React course.\nCode the Dream Was Already in Motion\nI had actually applied to Code the Dream (https://codethedream.org/) months earlier, around April 2025.\nThe timing of the whole thing could have been a little better. But it is not like I brought the baby home, learned a thing or two about our new tiny boss, and decided:\nYou know what this situation needs?\nAnother commitment.\nThe decision had already been made.\nI had tested out of the Intro to Programming course, which is how I ended up going directly into React.\nBy this point, I knew that it was going to be a challenge, but it seemed reasonable enough.\nStrangely enough, his early arrival may actually have made the start of the course easier. I lost some preparation time, but I gained a few weeks to settle into being a parent before React started. And that made a big difference.\nBy the time the React cohort started, I was not deciding whether I wanted to begin another programming course while raising a newborn. I had already committed to the course.\nNow I just had to figure out what continuing looked like under completely different circumstances.\nThe answer was very different from the way I had worked before.\nLong, uninterrupted coding sessions became harder to find. Development started happening in smaller windows: thirty minutes here, an hour there, whatever time happened to exist between everything else.\nIf twenty minutes of a thirty-five-minute session were spent trying to remember what I had been doing the last time I opened the project, that was a problem.\nSo I started leaving myself better notes. I broke problems into smaller pieces. I became more deliberate about commits and less interested in keeping an entire feature in my head.\nWithout really planning to, becoming a parent forced me to get better at stopping halfway through something while leaving enough breadcrumbs that future me could figure out what past me had been doing.\nAnd then React started.\nReact, the Second Time Around\nI joined Code the Dream's React v3 course as part of the Lark Cohort, which ran from November through February.\nCode the Dream was not where I first learned React, or where I first built a full-stack application. I had already built MERN applications, worked with APIs, Express, and MongoDB, and completed several development projects with other people through Chingu.\nWhat CTD gave me was something different.\nIt gave me several months to go back through technologies I already knew and find the places where my understanding was thinner than I realized.\nOne of those places was Context.\nI had used Context before and understood what it did, but there is a difference between knowing that Context exists and getting comfortable designing state around it, combining it with reducers, deciding what actually belongs there, and thinking about what happens as your component tree gets larger.\nSometimes learning something for the second time is a lot more useful than learning it the first time.\nThe first time, you are mostly trying to make it work.\nThe second time, you start asking why it works that way.\nThat became a recurring theme during the course. It was less:\nHere is React.\nand more:\nYou know React. Now let's find the parts you have been getting away with not understanding deeply enough.\nMy guided project grew throughout the course. Reducers came in, Context became more important, routing got more complicated, external data got involved, state crossed more boundaries, and testing became a larger part of the way I worked.\nEventually something I had built myself six weeks earlier could surprise me, which is always reassuring.\nSomewhere during that process, the question I asked after completing something changed.\nIt used to be:\nDoes it work?\nI would click around a few times, see that everything looked fine, and move on.\nNow I am much more likely to ask:\nOkay, it works. What breaks it?\nWhat happens if the request fails? What happens if the user refreshes here? What if this state has not hydrated yet? What happens if the component disappears before an asynchronous operation finishes? What happens if somebody clicks the same button five times while the first request is still running?\nThe happy path is still important.\nI just trust it a lot less than I used to.\nThen I Went Back Through the Backend\nAfter React, I moved into Code the Dream's Node 26.2 Cohort, which ran from late March through June.\nAgain, Node and Express were not new territory. I had already built full-stack applications and worked with the MERN stack, but going through a dedicated backend course gave me an opportunity to spend more time on the details instead of treating the backend as whatever needed to exist so the frontend could work.\nWe spent time with routes, middleware, authentication, validation, error handling, HTTP semantics, database access, and application structure.\nOne technology I got a lot more practice with was Prisma.\nI had used Prisma before CTD, but not enough that I would have called myself particularly comfortable with it. CTD gave me more opportunities to use it deliberately instead of simply following the happy path through an ORM.\nThat distinction mattered.\nThere are a lot of technologies I have used. That does not necessarily mean I understand all of the decisions they are making for me.\nKnowing how to use a tool is one level.\nKnowing what that tool is abstracting away is another.\nThen Came Practicum\nAfter finishing the coursework, I joined Code the Dream's Summer 2026 Practicum.\nThis part felt familiar.\nI had already done several team projects through Chingu, so practicum was not my introduction to collaboration, Git, pull requests, requirements, product decisions, or the occasional joy of discovering two developers had spent an afternoon independently solving the same problem.\nWhat made practicum interesting was the amount of structure we had to build around the development itself.\nOur team built Sprout (https://github.com/Code-the-Dream-School/summer-26-js-practicum-team2), a financial literacy application.\nThe easiest way I have found to describe it is something like Duolingo pointed at personal finance: short lessons, quizzes, progress tracking, onboarding, and a guided learning path.\nThe idea was to make a subject people know they should understand feel less like sitting down with a textbook, because almost nobody wakes up Saturday morning thinking:\nYou know what would really make this weekend? Budgeting terminology.\nSprout was not the project I would have chosen on my own, and I think that made the experience more interesting.\nWhen something is your personal project, you can build whatever you want. On a team, the product does not belong entirely to you. You have to build the thing the group agreed to build.\nThat is a different skill.\nBefore We Built Much of Anything, We Built a Backlog\nOne of the first things I did was start planning.\nAnd then I planned some more.\nAnd then apparently I decided Jira had not suffered enough, because I built a pretty enormous backlog.\nThere are still plenty of tickets sitting in it that we never touched, and that was intentional. A backlog is not a promise that everything gets built. It is a record of things that could get built.\nWe split the core work across sprint weeks and broke larger ideas into epics, user stories, and smaller tasks. A Jira ticket would often become a GitHub issue. That issue gave us something concrete to build around and could then map to a branch.\nEventually there was a clear line connecting:\nidea → epic → story → issue → branch → pull request → application\nThat process became one of the more valuable parts of practicum for me because a large application does not get built by somebody opening an editor and thinking:\nToday I shall implement the application.\nYou build a small piece. Then another. Then another.\nSometimes several developers were contributing to the same larger feature, so we would use what amounted to a parent or integration branch. Instead of everybody immediately merging their work into development, we could combine related work there first, get the pieces cooperating, fix whatever exploded, and then merge the larger feature.\nIt added another layer to the Git workflow, but it also prevented development from becoming quite as adventurous.\nWe Never Really \"Finished\" an Epic\nSomething else became apparent pretty quickly: epics are big.\nReally big.\nWhen you only have a limited number of weeks, \"complete the epic\" is often the wrong goal. So we sliced useful pieces out of them instead.\nOne week we might improve onboarding. Later, we would come back and add something else. The learning path grew incrementally. Admin tools grew incrementally. Accounts grew incrementally. Testing grew incrementally.\nEven now, I do not think I could point at many of our larger epics and say:\nYep. Finished forever.\nThere are still stories sitting in the backlog, but the application works.\nThat taught me something useful about MVP development. An epic does not have to be complete for part of it to create value. Sometimes the right feature is simply the smallest useful slice of a much larger idea.\nIn a strange way, I suppose Baby Book had already taught me a smaller version of that lesson.\nIt never became the application I originally imagined, but for a brief period the pieces we actually needed were useful.\nSometimes software has already done its job before you finish everything you once imagined for it.\nThen Sprout Became Complicated\nSprout used React on the frontend, Node and Express on the backend, and MongoDB underneath. We used Tailwind CSS and eventually created a collection of shared components so the UI could stay relatively consistent while different developers worked on different screens.\nThat became important very quickly.\nWithout shared components, five developers can somehow invent seven buttons. Each one is slightly different; none of them are necessarily wrong, and together they make the application look like it was assembled by committee.\nBecause it was.\nShared components gave us some guardrails, but the more interesting architectural decisions happened around the curriculum.\nSeveral members of the team worked together to build our learning material into structured JSON, and a huge part of the application eventually revolved around that structure: lessons, sections, quizzes, questions, answers, and progress.\nThe JSON became something close to a content engine for the application.\nFor a while, that worked really well.\nThen we started asking uncomfortable questions. Questions like:\nShould the correct answers to a quiz really be sitting somewhere the browser can inspect?\nProbably not.\nOne Architecture Decision Created a Whole New Category of Bugs\nEventually we started integrating more of the curriculum data with MongoDB.\nPart of the motivation was protecting quiz answers. If the browser receives everything—including which answer is correct—then somebody who knows their way around developer tools can potentially skip the whole \"learning\" portion of the learning application.\nWhich feels slightly against the spirit of the thing.\nMoving more responsibility to the backend made sense, but architectural decisions rarely arrive alone. This one brought friends: more requests, more synchronization, more state, more opportunities for the frontend and backend to disagree, performance concerns, and some really annoying bugs.\nWe solved one problem and introduced an entirely different class of problems.\nThat does not necessarily mean the decision was wrong. It means the decision had a cost, and that might be one of the most useful things Sprout reinforced for me.\nArchitecture is usually not:\nWhich solution is correct?\nIt is more often:\nWhich set of problems would you rather have?\nI may revisit that part of Sprout in the future because I think there are better ways we could protect assessment data without making parts of the experience as expensive or fragile as they became.\nBut that is also why I am glad we tried it.\nYou learn a lot more from an architectural decision that creates unexpected problems than one that only exists in a diagram.\nTesting Was Only Half of What I Enjoyed\nTesting became a major part of my role on the project. I worked with Jest, Vitest, React Testing Library, and Playwright, and I liked the way testing forced us to think beyond the exact sequence of events we pictured when writing a feature.\nWhat happens if the component unmounts? What happens if the network request fails? What if authentication has not finished hydrating? What if somebody double-clicks? What if the frontend thinks a lesson is complete and the backend disagrees?\nWe found all of those.\nBut something else ended up being just as interesting to me:\nCI.\nI really enjoyed working with GitHub Actions and getting our tests running automatically as part of the development workflow.\nThere is something deeply satisfying about turning:\nSomebody should probably run the tests before we merge this.\ninto:\nThe repository literally will not let us forget.\nThat felt like another step beyond simply knowing how to write tests.\nA test suite sitting on somebody's laptop is useful. A test suite that runs automatically against changes is infrastructure.\nOnce I started working on that side of the project, I found myself liking it quite a bit: tests, coverage, automated checks, GitHub Actions, deployment workflows, and all the little pieces that make it harder for a team to accidentally ship something broken.\nNone of those things are particularly flashy.\nBut they make everything else easier to trust.\nSome Bugs Deserve Their Own ZIP Code\nWe still had plenty of bugs.\nSome were straightforward, friendly bugs. The sort that basically introduce themselves:\nHello. I am on line 47.\nThose are wonderful.\nOthers required the application to be in exactly the wrong state at exactly the right time.\nWe had authentication behavior where the frontend could decide where a user should be redirected before it had finished determining whether the user was authenticated. Everything worked unless the timing changed, which is always comforting.\nWe had asynchronous quiz operations where work could finish after the component that started it no longer existed. We had buttons users could click multiple times while a request was still running. We had lesson progression and quiz completion logic with occasionally incompatible definitions of reality.\nSome of those bugs looked completely fine when you clicked through the application manually.\nThen an automated test created the right sequence of events and everything fell apart.\nBeautiful.\nThose are the problems I enjoy the most now: the ones where the solution requires understanding the system rather than simply finding a typo.\nGit Got More Interesting Too\nWorking by yourself gives you a very misleading impression of Git.\nYou make a branch, write some code, commit it, merge it, and suddenly you feel pretty good about version control.\nWorking with several developers changes the situation, especially once branches start depending on other branches.\nSuddenly Git becomes less:\nSave my work.\nand more:\nWhich version of this application represents reality right now?\nThe parent-branch strategy helped with that. So did smaller pull requests, better issues, and simply talking to each other before two people changed the same area of the application.\nThat is the funny thing about Git problems.\nA surprising number of them are actually communication problems that happen to eventually produce merge conflicts.\nPracticum Felt Familiar Because of Chingu\nA lot of practicum reminded me of Chingu Voyages, which makes sense because I had already done several Voyages as both a developer and a Product Owner.\nChingu had already taught me that two developers can read the same requirement and walk away with completely different applications in their heads. It had already shown me how important communication becomes once multiple people share a repository, and it had already exposed me to team roles and product planning.\nThe two programs just approached those experiences differently.\nChingu Does a Great Job Simulating a Product Team\nOne thing I genuinely like about Chingu is that a Voyage can include several different roles.\nDevelopers may work alongside a Product Owner, Scrum Master, and UI/UX Designer, which creates some separation between responsibilities. A designer is concentrating on how something should look and behave. A Product Owner is shaping priorities and requirements. A Scrum Master helps keep the process moving.\nDevelopers then have to take those ideas and make them real.\nAnd eventually everybody discovers that a button somebody drew in thirty seconds somehow requires half a day to implement correctly.\nThat structure is useful.\nThe challenge, particularly in some lower tiers, is that experience levels among developers can vary quite a bit. Everybody might technically satisfy the requirements for the Voyage while having very different levels of comfort with JavaScript, Git, architecture, APIs, or debugging.\nSometimes you discover those differences while trying to ship something.\nWhich, to be fair, is also pretty realistic.\nCode the Dream Gave Us a Different Baseline\nCode the Dream's practicum had another advantage: everyone had already completed defined coursework.\nThat did not make everyone's skills identical—not even close—but it gave us shared reference points. We had all traveled through at least some of the same material before arriving at the project.\nThe bigger difference was that we did not have every traditional product role separated out. We were mostly developers, so we had to wear more hats ourselves.\nSomeone had to think about product. Someone had to think about design. Someone had to organize the work and maintain the backlog. Someone had to worry about whether CI was working.\nUsually everybody ended up doing more than whatever role they originally imagined themselves doing.\nAnd I think that was good for us.\nThe People Part\nBerenice (https://www.linkedin.com/in/berenice-rojas) stepped in beside me as a co-project lead, and that helped me more than I probably said at the time. She even played a role in designing the UI/UX.\nShe gave me somebody to bounce ideas off of while we built epics, shaped user stories, created tasks, organized Jira, and tried to decide which pieces actually mattered enough to belong in our MVP.\nBerenice and I worked together to decide which parts were actually critical for an MVP. Those moved forward; the rest could wait.\nThere are a lot of small judgment calls hidden inside project planning. Having another person there to challenge an idea—or confirm that I was not completely overthinking it—made that a lot easier.\nKristen (http://www.linkedin.com/in/kristen-wishart) and Maryzabeth (https://github.com/BytesOfStrength) took on another enormous part of the application.\nThey worked together on our initial low-fidelity wireframes, which became high-fidelity wireframes and eventually the foundation for the UI and UX. Then they worked together on developing the curriculum that became the content driving much of Sprout.\nKristen had a particularly good instinct for keeping us connected to the original vision. Software developers are exceptionally talented at taking a reasonable idea and turning it into fourteen features, three dashboards, a notification system, and possibly a social network.\nSometimes somebody has to say:\nWe probably do not need that.\nKristen was good at that. A lot of her vision made it into the final product precisely because she was willing to pull us back toward the application we had actually agreed to build.\nMaryzabeth became one of those people every remote team quietly depends on. She contributed across both frontend and backend work, but she also became our unofficial secretary.\nShe took notes, documented decisions, and if somebody missed a meeting or standup, she was usually the person making sure they knew what happened.\nThat sounds like a small thing until you work on a distributed team.\nThen you discover that keeping everybody operating from the same version of reality is basically a feature of its own.\nEverybody ended up bringing something beyond code, and that is the part of team development I find most interesting.\nWe Also Had Actual Adults in the Room\nCode the Dream pairs practicum teams with mentors, and our team had Hector (https://www.linkedin.com/in/hectorfgonzalez/) and Mario (https://www.linkedin.com/in/mntri4/).\nHector brought real-world software development experience into our conversations, while Mario brought a lot of optimism and was always willing to listen while we worked our way through something.\nWhat helped most was rarely somebody simply handing us an answer. Usually it was somebody with more experience asking a question we had not asked ourselves:\nWhy did you build it this way?\nSometimes we had a very good answer.\nSometimes the answer turned out to be:\nBecause that is how it currently is.\nThose conversations were useful too.\nSo What Actually Changed?\nThis is probably where I would describe the year differently than I would have twelve months ago.\nI did not learn that teams exist. I had already worked on teams. I did not learn how to build a full-stack application. I had already built them. I did not discover MERN. I already knew it.\nWhat changed was the level at which I started thinking about all of those things.\nI understand React Context better than I did. I am much more comfortable with testing. I have more practical experience with Prisma. I learned more about CI and GitHub Actions than I expected to, and I got more interested in the infrastructure surrounding the code.\nI spent more time thinking about how work gets broken down before anyone writes it. I became more deliberate about branch strategies when several pieces of one feature were being developed at the same time. I watched an architectural decision solve one problem while creating three new ones, and I saw how much a shared component library matters when several developers are building the same UI.\nMore than anything, I got another reminder that software development is full of tradeoffs disguised as decisions.\nMaybe that is the biggest thing I got out of this year.\nI did not necessarily add another stack to the list of technologies I know.\nI got better at seeing the system around them.\nThe Other Lesson Had Nothing to Do With Code\nI also learned that progress does not require the schedule I thought it did.\nFor a long time, I pictured serious development work happening in large, uninterrupted blocks: a free Saturday, several hours at a desk, headphones on, everything lined up.\nThat sounds wonderful.\nIt describes almost none of what the past year looked like.\nA lot of this year happened in pieces. Thirty minutes here. An hour after bedtime. A bug before dinner. A pull request during nap time. Reading yesterday's notes because I had absolutely no idea what the version of me from forty-eight hours ago had been thinking.\nAnd somehow it still added up.\nI used to think real progress required long stretches of uninterrupted time. It turns out a surprising amount of it comes from getting good at returning.\nOpen the laptop. Find the note. Read the issue. Make the next change. Commit it. Come back later.\nThat is not the montage version of learning software development.\nIt is probably a lot closer to the real one.\nThe Part That Made Any of This Possible\nOutside the project, my wife Jayanti (https://www.linkedin.com/in/jayanti-hari/) listened to far more discussion about failed tests, authentication bugs, GitHub Actions, pull requests, MongoDB, Render deployments, and merge conflicts than any reasonable marriage contract should require.\nShe was also there through the much more important part of the year while we figured out how to become parents together.\nThat included the unfinished Baby Book, the last-minute contraction feature, sleep schedules that were mostly theoretical, and plenty of development sessions squeezed into whatever part of the day happened to survive.\nIt has been a year.\nA really good one, a very busy one, and often a very tired one.\nSo... What's Next?\nPracticum has wrapped.\nI may keep tinkering with Sprout. There are still architectural decisions I want to revisit, experiments I want to try, and a Jira backlog large enough to keep several imaginary development teams busy.\nBut I am also ready to build other things.\nI have a Rust game I keep coming back to. I want to contribute to open source properly. Baby Book is still sitting somewhere in the back of my head, too.\nAnd I have a growing list of things from this past year that deserve articles of their own: CI pipelines, testing patterns, authentication races, architecture mistakes, Prisma, React, Rust, and the sort of problems where the first draft of the article usually begins with:\nI thought this would be easy.\nThose are usually the interesting ones.\nI have also applied to Code the Dream's apprenticeship program.\nI do not know yet whether I will get in, and strangely, I am okay leaving that part of the story there.\nNot knowing.\nBecause that is actually where I am.\nThere is a tendency when writing about learning software development to wait until you know how the story ends. Get the job. Finish the project. Earn the certificate. Then go back and write the neat version where everything looks like it was leading somewhere obvious.\nReal life rarely gives you that version while you are living it.\nRight now, I have finished one thing and I am waiting to find out what comes next. Maybe that is the apprenticeship. Maybe it is a job. Maybe it is open source.\nMaybe I disappear into Rust for six months and emerge with something deeply unnecessary.\nThere are worse outcomes.\nWhat I do know is that I finally want to write again, and I have a lot more to write about than I did when this blog went quiet.\nSo Journey to Code is back.\nProbably a little slower. Definitely with more interruptions.\nBut I think I am okay with that now.\nThe last year taught me that progress does not always look like moving quickly. Sometimes it just means you keep coming back.\nAnd, conveniently, I just did.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/what-a-long-strange-trip/#ob-discussion)",
      "date_modified": "2026-09-20T16:00:18Z",
      "date_published": "2026-09-19T14:57:16Z",
      "id": "https://mnixblog.netlify.app/what-a-long-strange-trip/",
      "image": "https://mnixblog.netlify.app/images/what-a-long-strange-trip/cover.png",
      "summary": "After a year away, I’m back with lessons from parenthood, Code the Dream, React, Node.js, testing, teamwork, and learning to keep coming back.",
      "tags": [
        "Frameworks",
        "Teamwork",
        "Career"
      ],
      "title": "What a Long, Strange Trip...",
      "url": "https://mnixblog.netlify.app/what-a-long-strange-trip/"
    },
    {
      "content_html": "<p>I grew up as the youngest offspring of one particularly tech-savvy individual who among many jobs, initially moved our family from many places like Texas, Florida, North Carolina, and eventually to West Virginia where I was raised as a child. The area in particular that I grew up is very rural, and essentially a decade or more behind modern times. However in our household this was almost uncertainly not the case. Since my father was such a connoisseur of technology, it's basically an inherited trait to me. For a little bit of a backstory, we wound up here as my father was writing assembly language code that would help facilitate the operation of coal power plants. So, naturally we always had the latest and greatest trends.</p>\n<p>Because of the available computing power at my fingertips I was often times able to experiment well before I should have with hardware. In our family, my dad would always upgrade his PC almost annually to the next latest and greatest system. One of my very first computers was a 286 processor with 16mhz. This introduced me to the world of the command line. I learned early how to navigate and traverse folders using Disk Operating System (or DOS). I learned the <code>del *.*</code> command at some point. At least one time or two, this got me into trouble. At home, it wasn't as big of a deal. After deleting DOS from my root directory once or twice, my dad decided to install his own standalone operating system for me that never let me reach the command line, instead I could only pick what game to play and it would type the commands for me.</p>\n<h2 id=\"the-tech-hand-me-down-dynasty\">The Tech Hand-Me-Down Dynasty<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#the-tech-hand-me-down-dynasty\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our household had its own tech ecosystem, with my dad at the top of the pyramid. He'd get the cutting-edge system, then his previous machine would go to my oldest brother, whose computer would then go to my middle brother, which meant I'd inherit whatever my middle brother was using. Being the youngest in a tech-obsessed family had its perks, even if I was always three generations behind the curve.</p>\n<p>When Dad upgraded to his first Pentium running at a mind-blowing 100MHz, it kicked off a beautiful cascade of hand-me-downs: oldest brother got Dad's 486 DX2 with 66MHz, middle brother received the 486DX with 33MHz, and I finally graduated to the 386 with 33MHz. Each time this happened, it felt like Christmas morning, even though I was essentially getting the technological equivalent of my brothers' worn-out sneakers.</p>\n<p>Living in a rural West Virginia town where the public schools didn't even connect to the internet until after 2000, our home stood out like a digital oasis in a technological desert. My friends would visit and stare in wonder at our setup. Their jaws would hit the floor when they realized we had dedicated phone lines just for internet—a luxury in the era of dial-up when most families had to choose between making calls or going online.</p>\n<p>Dad wasn't just tech-savvy; he ran a dial-up internet service provider. This meant we were always on the cutting edge, while my school was still using computers that might as well have been powered by hamster wheels. I never once experienced the frustration of having someone pick up the phone while I was online—a common trauma for most 90s kids.</p>\n<h2 id=\"doom-lan-parties-and-the-cruel-reality-of-crashed-map-editors\">DOOM, LAN Parties, and the Cruel Reality of Crashed Map Editors<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#doom-lan-parties-and-the-cruel-reality-of-crashed-map-editors\" aria-label=\"Link to this section\">#</a></h2>\n<p>The mid-90s were the golden age of DOOM, and our basement became command central for epic LAN parties around 1995. Back then, Windows couldn't hold a candle to DOS when it came to gaming performance. We would gather, connect our machines, and blast demons (or each other in deathmatch) for hours on end.</p>\n<p>But we weren't just players—we were creators too. The DOOM modding scene was where I cut my teeth on level design, spending countless hours crafting custom maps... only to have my underpowered 386 crash mid-creation, erasing everything I'd done. I can still feel that particular brand of heartbreak that comes from losing hours of creative work to the digital void. Nothing teaches you to save your work like losing an entire afternoon's worth of meticulously placed monsters and secret rooms.</p>\n<p>Storage in our house was a sight to behold. We literally had hundreds of 5.25\" floppy disks stashed everywhere—loaded with games, programming languages, and applications from computing's early days. These massive, actually-floppy disks are probably still boxed up somewhere in my parents' house, a physical museum of software history.</p>\n<p>I still remember the technological disappointment of our first 1x CD-ROM drive. On paper, it could hold way more data than any floppy. In practice? I discovered firsthand that it was often faster to install Windows using 64 separate 1.44\" floppy disks than waiting for that sluggish CD to do its thing. The only advantage was being able to start the installation and walk away instead of sitting there feeding in disk after disk like some sort of human vending machine.</p>\n<h2 id=\"the-windows-product-key-savant\">The Windows Product Key Savant<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#the-windows-product-key-savant\" aria-label=\"Link to this section\">#</a></h2>\n<p>Here's a weird party trick I still have: I can recite the Windows 95 and 98 product keys from memory. Seriously. After installing these operating systems dozens of times (usually after breaking something), those alphanumeric sequences are permanently etched into my brain alongside my social security number and childhood phone number.</p>\n<p>Was it strictly kosher to use the same key repeatedly? Probably not. But in those days, activation was simpler, and we were just kids excited to get our machines back up and running after whatever catastrophe had befallen them (usually self-inflicted).</p>\n<h2 id=\"accidental-webmaster-and-the-great-apache-incident\">Accidental Webmaster and the Great Apache Incident<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#accidental-webmaster-and-the-great-apache-incident\" aria-label=\"Link to this section\">#</a></h2>\n<p>While most kids were content playing Nintendo, my brothers and I were diving headfirst into the wild west of early web development. We honed our skills on Geocities and AngelFire, painstakingly writing HTML in Notepad and proudly publishing pages that were essentially 99% animated GIFs and \"Under Construction\" banners.</p>\n<p>When Dad discovered our interest, he kicked things up several notches—purchasing us a domain name, setting up a web server, and giving each of us our own subdomain. Suddenly we graduated from Notepad to the \"professional-grade\" Microsoft FrontPage and were learning about mysterious concepts like FTP and hosting. These were the early days of web development. JavaScript was not nearly as powerful as it has become.</p>\n<p>One of my more notorious tech moments came when I accidentally installed Apache on my PC and started hosting a web server without even realizing what I'd done. Dad went into full-on security panic mode, convinced we'd been hacked, only to discover it was just his clueless youngest son experimenting with software he didn't understand. When he demanded to know why I'd done it, I genuinely had no idea what he was talking about—I didn't even know what Apache was! I'd just clicked through some install prompts and suddenly our home network had an unexpected web server. The digital equivalent of accidentally building a shed in the backyard.</p>\n<h2 id=\"emulation-console-gaming-on-pc-terms\">Emulation: Console Gaming on PC Terms<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#emulation-console-gaming-on-pc-terms\" aria-label=\"Link to this section\">#</a></h2>\n<p>While my friends were saving up allowance money for GameBoy cartridges and Super Nintendo games, we were exploring the fascinating world of emulation. Why play Super Mario on a tiny GameBoy screen when you could play it on a computer monitor with the ability to save anywhere and apply cheats without those clunky GameShark devices?</p>\n<p>It wasn't that we didn't have consoles—our gaming collection included everything from ancient Ataris and Commodore 64s to Nintendo, Super Nintendo, Genesis, Sega CD, and N64. But the flexibility of emulation gave us superpowers that console gamers could only dream of.</p>\n<p>The crown jewel of our portable gaming collection wasn't even a Nintendo product—it was the Sega Nomad, a handheld that played full Genesis cartridges with a backlit screen when that was basically unheard of technology. This thing was truly revolutionary—a fully backlit portable console playing 16-bit games when the GameBoy was still rocking monochrome graphics and requiring external light sources.</p>\n<h2 id=\"breaking-free-my-first-custom-build\">Breaking Free: My First Custom Build<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#breaking-free-my-first-custom-build\" aria-label=\"Link to this section\">#</a></h2>\n<p>Fast forward to my teenage years. I was finally ready to break free from the hand-me-down cycle and build my very first PC from scratch. Spoiler alert: it didn't go smoothly.</p>\n<p>My first motherboard purchase ended with the vendor vanishing faster than my motivation to do homework. Still salty about that one, but it taught me an early lesson about researching vendors before handing over cash.</p>\n<p>Eventually, I assembled what felt like a supercomputer at the time: a Pentium 4 Dual Core running at 1.2GHz paired with some NVIDIA graphics card whose model number has long since faded from memory. But it ran World of Warcraft on maximum settings, and that's all that mattered to teenage me.</p>\n<h2 id=\"the-brother-borrowing-incident\">The Brother \"Borrowing\" Incident<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#the-brother-borrowing-incident\" aria-label=\"Link to this section\">#</a></h2>\n<p>When I moved out west, my computer stayed behind. At some point, my brother decided my PC looked lonely and \"borrowed\" it without asking (as brothers do). Then Christmas rolled around, and guess what appeared under the tree with my name on it? MY OWN PC. With a blown power supply.</p>\n<p>I never did replace that power supply. The PC sat gathering dust, a monument to sibling shenanigans and the importance of surge protectors.</p>\n<h2 id=\"the-corporate-castoff-renaissance\">The Corporate Castoff Renaissance<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#the-corporate-castoff-renaissance\" aria-label=\"Link to this section\">#</a></h2>\n<p>Years later, that same brother (yes, redemption arc incoming) was working at a bank that was throwing out perfectly good Dell Optiplex computers. Their only crime? Slow hard drives. These machines were destined for a landfill until he rescued one and brought one to me.</p>\n<p>The transformation was mind-blowing: swapping the old HDD for an SSD made boot time instantaneous. We're talking from \"go make a sandwich while Windows loads\" to \"blink and you'll miss it.\" These corporate castoffs were treasure in disguise!</p>\n<p>I happily used one of these rescued machines for years, only upgrading the video card with something cheap off eBay and naturally the power supply to support it. The best part? Unlike many modern proprietary systems, these old Dells accepted standard parts without too much fuss.</p>\n<h2 id=\"modern-times-from-prebuilt-to-power-user\">Modern Times: From Prebuilt to Power User<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#modern-times-from-prebuilt-to-power-user\" aria-label=\"Link to this section\">#</a></h2>\n<p>Fast forward to last year. I did something that would make younger me gasp in horror—I bought a prebuilt PC from Walmart. I know, I know! But with component prices being what they were, it was actually a good deal.</p>\n<p>Of course, I couldn't leave well enough alone. Soon I was upgrading to a Ryzen 5900X and stuffing it with 4x32GB RAM chips because... reasons? Definitely overkill since I don't even game much anymore. But there's something deeply satisfying about having more RAM than you'll ever need.</p>\n<p>When my fiancée moved in, I wanted us to have matching battle stations so we could work/game together. My brilliant plan: use the old Dell Optiplex case as the foundation for a second build.</p>\n<p>Cue the comedy of errors:</p>\n<ol>\n<li>\n<p>Bought a high-end Radeon 7600XT, only to discover neither of my power supplies could support it.</p>\n</li>\n<li>\n<p>Tried to install the motherboard in the Optiplex case, only to find out it does not support it.</p>\n</li>\n<li>\n<p>Attempted to bench test with my spare PSU, which didn't have the required 8-pin connector.</p>\n</li>\n<li>\n<p>Dismantled my prebuilt setup to test the new motherboard.</p>\n</li>\n<li>\n<p>Got the dreaded amber DRAM light.</p>\n</li>\n<li>\n<p>Finally realized I needed to actually connect a GPU to see anything (rookie mistake).</p>\n</li>\n<li>\n<p>Swapped in the 5900X and... red CPU light. Heart attack moment.</p>\n</li>\n<li>\n<p>Discovered several bent pins on my precious processor.</p>\n</li>\n</ol>\n<p>Ever tried to straighten CPU pins? It's like performing microsurgery with household items. I used a playing card to view each row and a sewing needle for gentle bending. After a significant amount of stress and perhaps some foul language, I managed to somehow, SOMEHOW, fix it without breaking off a single pin. Special thanks to Jayanti for holding the flashlight because I could not see a thing without her.</p>\n<h2 id=\"the-unfinished-symphony\">The Unfinished Symphony<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#the-unfinished-symphony\" aria-label=\"Link to this section\">#</a></h2>\n<p>The second PC isn't completed yet, but it's coming. It'll be a fully custom AM4 build featuring some of the best components it will support. Probably not the wisest financial decision, but since when has PC building been about making sensible choices?</p>\n<p>Looking back at my journey from that hand-me-down 286 to performing pin surgery on high-end AMD processors, I can't help but marvel at how far both technology and my relationship with it have come. From accidentally formatting drives with <code>del *.*</code> to carefully repairing processors that cost more than my first three computers combined, each disaster and triumph has deepened my understanding of these machines.</p>\n<p>My dad's tech obsession became my own, passing down through the generations like some kind of digital inheritance. That rural West Virginia kid with a 286 eventually became someone who could resurrect dead machines and build new ones from scratch. (On a good day)</p>\n<p>Would I recommend this hobby to others? Absolutely! Just maybe don't start by accidentally hosting web servers, formatting your boot drive, or lending your PC to siblings.</p>\n<p>Thank you for joining me on this nostalgic tech journey! Did you experience the hand-me-down computer cycle, DOOM LAN parties, or GeoCities web design? Share your first computer or gaming memories in the comments—I would love to hear how your path through personal computing compares to mine.</p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "From DOS to Desktop Disasters\nCanonical: https://mnixblog.netlify.app/from-dos-to-desktop-disasters/\nMy Life-Long PC Building Adventure\nI grew up as the youngest offspring of one particularly tech-savvy individual who among many jobs, initially moved our family from many places like Texas, Florida, North Carolina, and eventually to West Virginia where I was raised as a child. The area in particular that I grew up is very rural, and essentially a decade or more behind modern times. However in our household this was almost uncertainly not the case. Since my father was such a connoisseur of technology, it's basically an inherited trait to me. For a little bit of a backstory, we wound up here as my father was writing assembly language code that would help facilitate the operation of coal power plants. So, naturally we always had the latest and greatest trends.\nBecause of the available computing power at my fingertips I was often times able to experiment well before I should have with hardware. In our family, my dad would always upgrade his PC almost annually to the next latest and greatest system. One of my very first computers was a 286 processor with 16mhz. This introduced me to the world of the command line. I learned early how to navigate and traverse folders using Disk Operating System (or DOS). I learned the del *.* command at some point. At least one time or two, this got me into trouble. At home, it wasn't as big of a deal. After deleting DOS from my root directory once or twice, my dad decided to install his own standalone operating system for me that never let me reach the command line, instead I could only pick what game to play and it would type the commands for me.\nThe Tech Hand-Me-Down Dynasty\nOur household had its own tech ecosystem, with my dad at the top of the pyramid. He'd get the cutting-edge system, then his previous machine would go to my oldest brother, whose computer would then go to my middle brother, which meant I'd inherit whatever my middle brother was using. Being the youngest in a tech-obsessed family had its perks, even if I was always three generations behind the curve.\nWhen Dad upgraded to his first Pentium running at a mind-blowing 100MHz, it kicked off a beautiful cascade of hand-me-downs: oldest brother got Dad's 486 DX2 with 66MHz, middle brother received the 486DX with 33MHz, and I finally graduated to the 386 with 33MHz. Each time this happened, it felt like Christmas morning, even though I was essentially getting the technological equivalent of my brothers' worn-out sneakers.\nLiving in a rural West Virginia town where the public schools didn't even connect to the internet until after 2000, our home stood out like a digital oasis in a technological desert. My friends would visit and stare in wonder at our setup. Their jaws would hit the floor when they realized we had dedicated phone lines just for internet—a luxury in the era of dial-up when most families had to choose between making calls or going online.\nDad wasn't just tech-savvy; he ran a dial-up internet service provider. This meant we were always on the cutting edge, while my school was still using computers that might as well have been powered by hamster wheels. I never once experienced the frustration of having someone pick up the phone while I was online—a common trauma for most 90s kids.\nDOOM, LAN Parties, and the Cruel Reality of Crashed Map Editors\nThe mid-90s were the golden age of DOOM, and our basement became command central for epic LAN parties around 1995. Back then, Windows couldn't hold a candle to DOS when it came to gaming performance. We would gather, connect our machines, and blast demons (or each other in deathmatch) for hours on end.\nBut we weren't just players—we were creators too. The DOOM modding scene was where I cut my teeth on level design, spending countless hours crafting custom maps... only to have my underpowered 386 crash mid-creation, erasing everything I'd done. I can still feel that particular brand of heartbreak that comes from losing hours of creative work to the digital void. Nothing teaches you to save your work like losing an entire afternoon's worth of meticulously placed monsters and secret rooms.\nStorage in our house was a sight to behold. We literally had hundreds of 5.25\" floppy disks stashed everywhere—loaded with games, programming languages, and applications from computing's early days. These massive, actually-floppy disks are probably still boxed up somewhere in my parents' house, a physical museum of software history.\nI still remember the technological disappointment of our first 1x CD-ROM drive. On paper, it could hold way more data than any floppy. In practice? I discovered firsthand that it was often faster to install Windows using 64 separate 1.44\" floppy disks than waiting for that sluggish CD to do its thing. The only advantage was being able to start the installation and walk away instead of sitting there feeding in disk after disk like some sort of human vending machine.\nThe Windows Product Key Savant\nHere's a weird party trick I still have: I can recite the Windows 95 and 98 product keys from memory. Seriously. After installing these operating systems dozens of times (usually after breaking something), those alphanumeric sequences are permanently etched into my brain alongside my social security number and childhood phone number.\nWas it strictly kosher to use the same key repeatedly? Probably not. But in those days, activation was simpler, and we were just kids excited to get our machines back up and running after whatever catastrophe had befallen them (usually self-inflicted).\nAccidental Webmaster and the Great Apache Incident\nWhile most kids were content playing Nintendo, my brothers and I were diving headfirst into the wild west of early web development. We honed our skills on Geocities and AngelFire, painstakingly writing HTML in Notepad and proudly publishing pages that were essentially 99% animated GIFs and \"Under Construction\" banners.\nWhen Dad discovered our interest, he kicked things up several notches—purchasing us a domain name, setting up a web server, and giving each of us our own subdomain. Suddenly we graduated from Notepad to the \"professional-grade\" Microsoft FrontPage and were learning about mysterious concepts like FTP and hosting. These were the early days of web development. JavaScript was not nearly as powerful as it has become.\nOne of my more notorious tech moments came when I accidentally installed Apache on my PC and started hosting a web server without even realizing what I'd done. Dad went into full-on security panic mode, convinced we'd been hacked, only to discover it was just his clueless youngest son experimenting with software he didn't understand. When he demanded to know why I'd done it, I genuinely had no idea what he was talking about—I didn't even know what Apache was! I'd just clicked through some install prompts and suddenly our home network had an unexpected web server. The digital equivalent of accidentally building a shed in the backyard.\nEmulation: Console Gaming on PC Terms\nWhile my friends were saving up allowance money for GameBoy cartridges and Super Nintendo games, we were exploring the fascinating world of emulation. Why play Super Mario on a tiny GameBoy screen when you could play it on a computer monitor with the ability to save anywhere and apply cheats without those clunky GameShark devices?\nIt wasn't that we didn't have consoles—our gaming collection included everything from ancient Ataris and Commodore 64s to Nintendo, Super Nintendo, Genesis, Sega CD, and N64. But the flexibility of emulation gave us superpowers that console gamers could only dream of.\nThe crown jewel of our portable gaming collection wasn't even a Nintendo product—it was the Sega Nomad, a handheld that played full Genesis cartridges with a backlit screen when that was basically unheard of technology. This thing was truly revolutionary—a fully backlit portable console playing 16-bit games when the GameBoy was still rocking monochrome graphics and requiring external light sources.\nBreaking Free: My First Custom Build\nFast forward to my teenage years. I was finally ready to break free from the hand-me-down cycle and build my very first PC from scratch. Spoiler alert: it didn't go smoothly.\nMy first motherboard purchase ended with the vendor vanishing faster than my motivation to do homework. Still salty about that one, but it taught me an early lesson about researching vendors before handing over cash.\nEventually, I assembled what felt like a supercomputer at the time: a Pentium 4 Dual Core running at 1.2GHz paired with some NVIDIA graphics card whose model number has long since faded from memory. But it ran World of Warcraft on maximum settings, and that's all that mattered to teenage me.\nThe Brother \"Borrowing\" Incident\nWhen I moved out west, my computer stayed behind. At some point, my brother decided my PC looked lonely and \"borrowed\" it without asking (as brothers do). Then Christmas rolled around, and guess what appeared under the tree with my name on it? MY OWN PC. With a blown power supply.\nI never did replace that power supply. The PC sat gathering dust, a monument to sibling shenanigans and the importance of surge protectors.\nThe Corporate Castoff Renaissance\nYears later, that same brother (yes, redemption arc incoming) was working at a bank that was throwing out perfectly good Dell Optiplex computers. Their only crime? Slow hard drives. These machines were destined for a landfill until he rescued one and brought one to me.\nThe transformation was mind-blowing: swapping the old HDD for an SSD made boot time instantaneous. We're talking from \"go make a sandwich while Windows loads\" to \"blink and you'll miss it.\" These corporate castoffs were treasure in disguise!\nI happily used one of these rescued machines for years, only upgrading the video card with something cheap off eBay and naturally the power supply to support it. The best part? Unlike many modern proprietary systems, these old Dells accepted standard parts without too much fuss.\nModern Times: From Prebuilt to Power User\nFast forward to last year. I did something that would make younger me gasp in horror—I bought a prebuilt PC from Walmart. I know, I know! But with component prices being what they were, it was actually a good deal.\nOf course, I couldn't leave well enough alone. Soon I was upgrading to a Ryzen 5900X and stuffing it with 4x32GB RAM chips because... reasons? Definitely overkill since I don't even game much anymore. But there's something deeply satisfying about having more RAM than you'll ever need.\nWhen my fiancée moved in, I wanted us to have matching battle stations so we could work/game together. My brilliant plan: use the old Dell Optiplex case as the foundation for a second build.\nCue the comedy of errors:\nBought a high-end Radeon 7600XT, only to discover neither of my power supplies could support it.\nTried to install the motherboard in the Optiplex case, only to find out it does not support it.\nAttempted to bench test with my spare PSU, which didn't have the required 8-pin connector.\nDismantled my prebuilt setup to test the new motherboard.\nGot the dreaded amber DRAM light.\nFinally realized I needed to actually connect a GPU to see anything (rookie mistake).\nSwapped in the 5900X and... red CPU light. Heart attack moment.\nDiscovered several bent pins on my precious processor.\nEver tried to straighten CPU pins? It's like performing microsurgery with household items. I used a playing card to view each row and a sewing needle for gentle bending. After a significant amount of stress and perhaps some foul language, I managed to somehow, SOMEHOW, fix it without breaking off a single pin. Special thanks to Jayanti for holding the flashlight because I could not see a thing without her.\nThe Unfinished Symphony\nThe second PC isn't completed yet, but it's coming. It'll be a fully custom AM4 build featuring some of the best components it will support. Probably not the wisest financial decision, but since when has PC building been about making sensible choices?\nLooking back at my journey from that hand-me-down 286 to performing pin surgery on high-end AMD processors, I can't help but marvel at how far both technology and my relationship with it have come. From accidentally formatting drives with del *.* to carefully repairing processors that cost more than my first three computers combined, each disaster and triumph has deepened my understanding of these machines.\nMy dad's tech obsession became my own, passing down through the generations like some kind of digital inheritance. That rural West Virginia kid with a 286 eventually became someone who could resurrect dead machines and build new ones from scratch. (On a good day)\nWould I recommend this hobby to others? Absolutely! Just maybe don't start by accidentally hosting web servers, formatting your boot drive, or lending your PC to siblings.\nThank you for joining me on this nostalgic tech journey! Did you experience the hand-me-down computer cycle, DOOM LAN parties, or GeoCities web design? Share your first computer or gaming memories in the comments—I would love to hear how your path through personal computing compares to mine.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#ob-discussion)",
      "date_published": "2025-04-28T04:31:56Z",
      "id": "https://mnixblog.netlify.app/from-dos-to-desktop-disasters/",
      "image": "https://mnixblog.netlify.app/images/from-dos-to-desktop-disasters/cover.jpg",
      "summary": "Growing up on my dad's hand-me-down PCs: a 286 and DOS, DOOM LAN parties, the Great Apache Incident, and my first custom build.",
      "tags": [
        "Career"
      ],
      "title": "From DOS to Desktop Disasters",
      "url": "https://mnixblog.netlify.app/from-dos-to-desktop-disasters/"
    },
    {
      "content_html": "<p>Which Web Component features can you use today, and which are still ideas? This post sorts them, with a status table you can check against your own browser.</p>\n<p>Some features are ready now, some work in only some browsers, and a few are only proposals. Knowing which is which keeps us from building on something that does not exist.</p>\n<p><strong>Prerequisites in two lines:</strong> a custom element is a JavaScript class registered with <code>customElements.define()</code>, and a shadow root gives it private markup and styles. Everything below extends those two ideas.</p>\n<h2 id=\"what-ships-today-and-what-does-not\">What ships today and what does not<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#what-ships-today-and-what-does-not\" aria-label=\"Link to this section\">#</a></h2>\n<p>\"Works\" in the second column means a small script ran in headless Chrome 156 while editing this post. Firefox and Safari were not tested by us. The Status column was checked against <a href=\"https://webstatus.dev/\">webstatus.dev</a> and MDN on 26 October 2026, so treat it as a snapshot and look again before you rely on a row.</p>\n<table><thead><tr><th>Feature</th><th>In Chrome 156</th><th>Status, Oct 2026</th></tr></thead><tbody>\n<tr><td><code>::part()</code>: style what a component exposes</td><td>Works</td><td>Ships today</td></tr>\n<tr><td><code>:state()</code>: style a component by its own state</td><td>Works</td><td>Baseline newly available</td></tr>\n<tr><td>Declarative Shadow DOM: <code>shadowrootmode</code></td><td>Works</td><td>Baseline widely available</td></tr>\n<tr><td>Manual slot assignment: <code>slot.assign()</code></td><td>Works</td><td>Baseline widely available</td></tr>\n<tr><td><code>@scope</code>: limit where a CSS rule applies</td><td>Works</td><td>Baseline newly available</td></tr>\n<tr><td>Scoped registries</td><td>Works</td><td>Chrome, Edge, Safari only</td></tr>\n<tr><td><code>attachSlot()</code></td><td><code>undefined</code></td><td>Not an API</td></tr>\n<tr><td><code>accessibleRole</code> property</td><td>Not present</td><td>Not an API</td></tr>\n<tr><td>Declarative custom elements</td><td>Not testable</td><td>Proposal</td></tr>\n</tbody></table>\n<p>The dates behind the Status column, from webstatus.dev and MDN on 26 October 2026:</p>\n<ul>\n<li><code>:state()</code>: newly available since May 2024 (Chrome 125, Firefox 126, Safari 17.4).</li>\n<li>Declarative Shadow DOM: newly available in February 2024 (Chrome 111, Firefox 123, Safari 16.4) and widely available since August 2026. The older <code>shadowroot</code> attribute no longer works.</li>\n<li>Manual slot assignment: widely available in all three engines since March 2023.</li>\n<li><code>@scope</code>: newly available since March 2026.</li>\n<li>Scoped registries: Chrome and Edge 146, Safari 26. Firefox has not shipped it. It is in Nightly, and Mozilla has proposed shipping it in Firefox 159, so check before you rely on it.</li>\n<li><code>::part()</code> was not re-checked for this update.</li>\n</ul>\n<h2 id=\"the-rising-tide-emerging-trends\">The Rising Tide: Emerging Trends<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#the-rising-tide-emerging-trends\" aria-label=\"Link to this section\">#</a></h2>\n<p>The rest of this post covers libraries, state and a few patterns. Most of it is built on the standards above, not added to the browser, and none of it is needed to write a custom element.</p>\n<h3 id=\"web-component-libraries\">Web Component Libraries<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#web-component-libraries\" aria-label=\"Link to this section\">#</a></h3>\n<p>You do not need a library to write a custom element, and every example above works without one. A library saves boilerplate for the parts you end up writing again and again.</p>\n<p><strong>Lit</strong>, from Google, is a small base class that adds reactive properties and templates on top of custom elements. <strong>Stencil</strong> is a compiler: you write TypeScript and JSX and it produces standards-based custom elements. <strong>FAST</strong> and <strong>Shoelace</strong> are the other two names that come up most. Each project changes its name, its version and its advice over time, so check its own site for its current state before you pick one. We did not re-check them for this update.</p>\n<p>The cost is the usual one. A library is one more thing to learn and to keep up to date, and shadow DOM still makes global styling harder whichever one you choose.</p>\n<h3 id=\"is-state-management-going-native-beyond-properties-and-attributes\">Is State Management Going Native?: Beyond Properties and Attributes<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#is-state-management-going-native-beyond-properties-and-attributes\" aria-label=\"Link to this section\">#</a></h3>\n<p>The browser gives you less state management than a framework does, and that is fine. The part it does give you is <code>:state()</code>: a component can expose states such as \"loading\" or \"expanded\" to CSS without adding classes the page could overwrite. It has been Baseline newly available since May 2024 (Chrome 125, Firefox 126, Safari 17.4). Stores, signals and reducers are libraries and patterns, not platform features, and the examples below are patterns you can write yourself.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">State Management in Web Components</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;user-profile&gt;&lt;/user-profile&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body {\n        margin: 0;\n        padding: 20px;\n        font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;\n        display: flex;\n        justify-content: center;\n        align-items: center;\n        min-height: 100vh;\n        transition: background-color 0.3s ease, color 0.3s ease;\n    }\n\n    @media (prefers-color-scheme: dark) {\n        body {\n            background-color: #121212;\n            color: #f0f0f0;\n        }\n    }\n\n    @media (prefers-color-scheme: light) {\n        body {\n            background-color: #f8f9fa;\n            color: #333;\n        }\n    }\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Simple state management pattern for Web Components\nclass UserProfile extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n\n    // Initialize state\n    this.state = {\n      name: &#39;Guest User&#39;,\n      role: &#39;Visitor&#39;,\n      isLoggedIn: false,\n      darkMode: window.matchMedia(&#39;(prefers-color-scheme: dark)&#39;).matches };\n\n\n    // Listen for changes in color scheme preference\n    this.colorSchemeMedia = window.matchMedia(&#39;(prefers-color-scheme: dark)&#39;);\n    this.colorSchemeMedia.addEventListener(&#39;change&#39;, e =&gt; {\n      this.setState({ darkMode: e.matches });\n    });\n\n    this.render();\n  }\n\n  // State management method\n  setState(newState) {\n    this.state = { ...this.state, ...newState };\n    this.render();\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          font-family: system-ui, sans-serif;\n          max-width: 300px;\n          padding: 20px;\n          border: 1px solid ${this.state.darkMode ? &#39;#555&#39; : &#39;#ddd&#39;};\n          border-radius: 8px;\n          background-color: ${this.state.darkMode ? &#39;#222&#39; : &#39;#fff&#39;};\n          color: ${this.state.darkMode ? &#39;#eee&#39; : &#39;#333&#39;};\n        }\n        \n        .profile {\n          display: flex;\n          flex-direction: column;\n          align-items: center;\n        }\n        \n        .avatar {\n          width: 80px;\n          height: 80px;\n          border-radius: 50%;\n          background-color: ${this.state.isLoggedIn ? &#39;#4CAF50&#39; : this.state.darkMode ? &#39;#555&#39; : &#39;#ccc&#39;};\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          margin-bottom: 16px;\n          color: white;\n          font-size: 32px;\n        }\n        \n        .name {\n          font-size: 20px;\n          font-weight: bold;\n          margin-bottom: 4px;\n        }\n        \n        .role {\n          color: ${this.state.darkMode ? &#39;#aaa&#39; : &#39;#666&#39;};\n          margin-bottom: 16px;\n        }\n        \n        button {\n          background-color: ${this.state.isLoggedIn ? &#39;#f44336&#39; : &#39;#4CAF50&#39;};\n          color: white;\n          border: none;\n          padding: 8px 16px;\n          border-radius: 4px;\n          cursor: pointer;\n        }\n        \n        button:hover {\n          opacity: 0.9;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;profile&quot;&gt;\n        &lt;div class=&quot;avatar&quot;&gt;${this.state.name.charAt(0)}&lt;/div&gt;\n        &lt;div class=&quot;name&quot;&gt;${this.state.name}&lt;/div&gt;\n        &lt;div class=&quot;role&quot;&gt;${this.state.role}&lt;/div&gt;\n        &lt;button id=&quot;toggle-login&quot;&gt;\n          ${this.state.isLoggedIn ? &#39;Logout&#39; : &#39;Login&#39;}\n        &lt;/button&gt;\n      &lt;/div&gt;\n    `;\n\n    // Add event listeners\n    this.shadowRoot.querySelector(&#39;#toggle-login&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n      if (this.state.isLoggedIn) {\n        this.setState({\n          name: &#39;Guest User&#39;,\n          role: &#39;Visitor&#39;,\n          isLoggedIn: false });\n\n      } else {\n        this.setState({\n          name: &#39;Jane Doe&#39;,\n          role: &#39;Administrator&#39;,\n          isLoggedIn: true });\n\n      }\n    });\n  }\n\n  disconnectedCallback() {\n    // Clean up event listener when component is removed\n    if (this.colorSchemeMedia) {\n      this.colorSchemeMedia.removeEventListener(&#39;change&#39;, this.handleColorSchemeChange);\n    }\n  }}\n\n\ncustomElements.define(&#39;user-profile&#39;, UserProfile);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/the-future-of-web-components/\">the original article</a>.</p></figure>\n<h3 id=\"context-sharing-state-without-tight-coupling\">Context: Sharing State Without Tight Coupling<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#context-sharing-state-without-tight-coupling\" aria-label=\"Link to this section\">#</a></h3>\n<p>A context-style provider is a pattern, not a platform feature. A parent component holds a value, and the components inside it ask for that value by dispatching an event, so the provider and its consumers do not need to know about each other. A <code>ThemeProvider</code> is the usual example, and the one below is small enough to read in a minute. For plain style values, CSS custom properties are still simpler.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Advanced State Management with Context</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;theme-provider&gt;\n\t&lt;theme-consumer&gt;&lt;/theme-consumer&gt;\n\t&lt;theme-toggler&gt;&lt;/theme-toggler&gt;\n\t&lt;theme-controller&gt;&lt;/theme-controller&gt;\n&lt;/theme-provider&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Context-like pattern for Web Components\nclass ThemeContext {\n  constructor() {\n    // Check for user preference\n    const prefersDarkMode =\n    window.matchMedia &amp;&amp;\n    window.matchMedia(&quot;(prefers-color-scheme: dark)&quot;).matches;\n\n    this.theme = prefersDarkMode ? &quot;dark&quot; : &quot;light&quot;;\n    this.accentColor = &quot;#2196F3&quot;;\n    this.fontSize = &quot;medium&quot;;\n    this.borderRadius = &quot;4px&quot;;\n    this.listeners = new Set();\n\n    // Track the last changed setting to help with animations\n    this.lastChanged = null;\n\n    // Listen for system theme changes\n    this.mediaQuery = window.matchMedia(&quot;(prefers-color-scheme: dark)&quot;);\n    this.handleSystemThemeChange = e =&gt; {\n      this.setTheme(e.matches ? &quot;dark&quot; : &quot;light&quot;);\n    };\n\n    // Add listener for system theme changes if supported\n    if (this.mediaQuery.addEventListener) {\n      this.mediaQuery.addEventListener(&quot;change&quot;, this.handleSystemThemeChange);\n    }\n\n    // Apply initial theme to body/html/root\n    this.applyThemeToDocument(this.theme);\n  }\n\n  getTheme() {\n    return this.theme;\n  }\n\n  setTheme(newTheme) {\n    this.theme = newTheme;\n    this.lastChanged = &quot;theme&quot;;\n    this.applyThemeToDocument(newTheme);\n    this.notifyListeners();\n  }\n\n  applyThemeToDocument(theme) {\n    // Set theme on html and body elements\n    document.documentElement.style.backgroundColor =\n    theme === &quot;light&quot; ? &quot;#f5f5f5&quot; : &quot;#121212&quot;;\n    document.documentElement.style.transition = &quot;background-color 0.3s&quot;;\n\n    document.body.style.backgroundColor =\n    theme === &quot;light&quot; ? &quot;#f5f5f5&quot; : &quot;#121212&quot;;\n    document.body.style.color = theme === &quot;light&quot; ? &quot;#333333&quot; : &quot;#ffffff&quot;;\n    document.body.style.transition = &quot;background-color 0.3s, color 0.3s&quot;;\n\n    // Add/remove class on document root for additional styling hooks\n    if (theme === &quot;dark&quot;) {\n      document.documentElement.classList.add(&quot;dark-theme&quot;);\n    } else {\n      document.documentElement.classList.remove(&quot;dark-theme&quot;);\n    }\n  }\n\n  getAccentColor() {\n    return this.accentColor;\n  }\n\n  setAccentColor(newColor) {\n    this.accentColor = newColor;\n    this.lastChanged = &quot;accentColor&quot;;\n    this.notifyListeners();\n  }\n\n  getFontSize() {\n    return this.fontSize;\n  }\n\n  setFontSize(newSize) {\n    this.fontSize = newSize;\n    this.lastChanged = &quot;fontSize&quot;;\n    this.notifyListeners();\n  }\n\n  getBorderRadius() {\n    return this.borderRadius;\n  }\n\n  setBorderRadius(newRadius) {\n    this.borderRadius = newRadius;\n    this.lastChanged = &quot;borderRadius&quot;;\n    this.notifyListeners();\n  }\n\n  getLastChanged() {\n    return this.lastChanged;\n  }\n\n  // Get the current theme as a complete object\n  getCurrentThemeSettings() {\n    return {\n      theme: this.theme,\n      accentColor: this.accentColor,\n      fontSize: this.fontSize,\n      borderRadius: this.borderRadius };\n\n  }\n\n  subscribe(listener) {\n    this.listeners.add(listener);\n  }\n\n  unsubscribe(listener) {\n    this.listeners.delete(listener);\n  }\n\n  notifyListeners() {\n    this.listeners.forEach((listener) =&gt;\n    listener(this.getCurrentThemeSettings()));\n\n  }\n\n  disconnectMediaQuery() {\n    // Clean up media query listener if it exists\n    if (this.mediaQuery &amp;&amp; this.mediaQuery.removeEventListener) {\n      this.mediaQuery.removeEventListener(&quot;change&quot;, this.handleSystemThemeChange);\n    }\n  }}\n\n\n// Global context instance\nconst themeContext = new ThemeContext();\n\n// Add window unload handler to clean up listeners\nwindow.addEventListener(&quot;unload&quot;, () =&gt; {\n  themeContext.disconnectMediaQuery();\n});\n\n// Provider component\nclass ThemeProvider extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &quot;open&quot; });\n\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          font-family: system-ui, sans-serif;\n          padding: 20px;\n          background-color: ${\n    themeContext.getTheme() === &quot;light&quot; ? &quot;#ffffff&quot; : &quot;#333333&quot;\n    };\n          color: ${themeContext.getTheme() === &quot;light&quot; ? &quot;#333333&quot; : &quot;#ffffff&quot;};\n          border-radius: 8px;\n          transition: background-color 0.3s, color 0.3s;\n        }\n        \n        .container {\n          max-width: 600px;\n          margin: 0 auto;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;container&quot;&gt;\n        &lt;h2&gt;Theme Context Demo&lt;/h2&gt;\n        &lt;slot&gt;&lt;/slot&gt;\n      &lt;/div&gt;\n    `;\n\n    // Update when theme changes\n    this.updateTheme = settings =&gt; {\n      const theme = settings.theme;\n      const styleElement = this.shadowRoot.querySelector(&quot;style&quot;);\n\n      // Update the style element with new theme colors\n      styleElement.textContent = `\n        :host {\n          display: block;\n          font-family: system-ui, sans-serif;\n          padding: 20px;\n          background-color: ${theme === &quot;light&quot; ? &quot;#ffffff&quot; : &quot;#333333&quot;};\n          color: ${theme === &quot;light&quot; ? &quot;#333333&quot; : &quot;#ffffff&quot;};\n          border-radius: 8px;\n          transition: background-color 0.3s, color 0.3s;\n        }\n        \n        .container {\n          max-width: 600px;\n          margin: 0 auto;\n        }\n      `;\n    };\n\n    themeContext.subscribe(this.updateTheme);\n  }\n\n  disconnectedCallback() {\n    themeContext.unsubscribe(this.updateTheme);\n  }}\n\n\n// Consumer component\nclass ThemeConsumer extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &quot;open&quot; });\n    this.render();\n\n    // Update when theme changes\n    this.updateTheme = settings =&gt; {\n      // Store which value changed to highlight it for the user\n      this.lastChanged = themeContext.getLastChanged();\n      this.render();\n\n      // Reset highlight after animation completes\n      setTimeout(() =&gt; {\n        this.lastChanged = null;\n        this.render();\n      }, 1500);\n    };\n\n    themeContext.subscribe(this.updateTheme);\n  }\n\n  disconnectedCallback() {\n    themeContext.unsubscribe(this.updateTheme);\n  }\n\n  render() {\n    const settings = themeContext.getCurrentThemeSettings();\n    const theme = settings.theme;\n    const accentColor = settings.accentColor;\n    const fontSize = settings.fontSize;\n    const borderRadius = settings.borderRadius;\n    const lastChanged = this.lastChanged;\n\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          margin: 16px 0;\n          padding: 16px;\n          border-radius: ${borderRadius};\n          border: 1px solid ${theme === &quot;light&quot; ? &quot;#ddd&quot; : &quot;#555&quot;};\n          background-color: ${theme === &quot;light&quot; ? &quot;#f9f9f9&quot; : &quot;#444&quot;};\n          color: ${theme === &quot;light&quot; ? &quot;#333&quot; : &quot;#eee&quot;};\n          transition: all 0.3s;\n        }\n        \n        h3 {\n          margin-top: 0;\n          margin-bottom: 12px;\n          color: ${accentColor};\n          font-size: ${\n    fontSize === &quot;small&quot; ?\n    &quot;16px&quot; :\n    fontSize === &quot;medium&quot; ?\n    &quot;18px&quot; :\n    &quot;20px&quot;\n    };\n          transition: all 0.3s;\n        }\n        \n        .theme-properties {\n          display: grid;\n          grid-template-columns: 120px 1fr;\n          grid-gap: 8px;\n        }\n        \n        .property-name {\n          font-weight: bold;\n          opacity: 0.8;\n        }\n        \n        .property-value {\n          font-family: monospace;\n          padding: 2px 6px;\n          border-radius: ${borderRadius};\n          background-color: ${\n    theme === &quot;light&quot; ? &quot;rgba(0,0,0,0.05)&quot; : &quot;rgba(255,255,255,0.1)&quot;\n    };\n          transition: all 0.3s;\n        }\n        \n        .property-highlight {\n          position: relative;\n          animation: pulse 1.5s ease-in-out;\n        }\n        \n        .property-highlight::before {\n          content: &quot;&quot;;\n          position: absolute;\n          top: 0;\n          right: 0;\n          bottom: 0;\n          left: 0;\n          background-color: ${accentColor};\n          opacity: 0.2;\n          border-radius: ${borderRadius};\n          z-index: -1;\n        }\n        \n        .color-preview {\n          display: inline-block;\n          width: 14px;\n          height: 14px;\n          border-radius: 50%;\n          margin-right: 6px;\n          vertical-align: middle;\n        }\n        \n        .demo-text {\n          margin-top: 16px;\n          padding: 12px;\n          border-radius: ${borderRadius};\n          border-left: 4px solid ${accentColor};\n          font-size: ${\n    fontSize === &quot;small&quot; ?\n    &quot;14px&quot; :\n    fontSize === &quot;medium&quot; ?\n    &quot;16px&quot; :\n    &quot;18px&quot;\n    };\n          background-color: ${\n    theme === &quot;light&quot; ? &quot;rgba(0,0,0,0.02)&quot; : &quot;rgba(255,255,255,0.05)&quot;\n    };\n          transition: all 0.3s;\n        }\n        \n        @keyframes pulse {\n          0% { transform: scale(1); }\n          50% { transform: scale(1.03); }\n          100% { transform: scale(1); }\n        }\n      &lt;/style&gt;\n      \n      &lt;div&gt;\n        &lt;h3&gt;Theme State Observer&lt;/h3&gt;\n        \n        &lt;div class=&quot;theme-properties&quot;&gt;\n          &lt;span class=&quot;property-name&quot;&gt;Theme:&lt;/span&gt;\n          &lt;span class=&quot;property-value ${\n    lastChanged === &quot;theme&quot; ? &quot;property-highlight&quot; : &quot;&quot;\n    }&quot;&gt;\n            ${theme}\n          &lt;/span&gt;\n          \n          &lt;span class=&quot;property-name&quot;&gt;Accent Color:&lt;/span&gt;\n          &lt;span class=&quot;property-value ${\n    lastChanged === &quot;accentColor&quot; ? &quot;property-highlight&quot; : &quot;&quot;\n    }&quot;&gt;\n            &lt;span class=&quot;color-preview&quot; style=&quot;background-color: ${accentColor};&quot;&gt;&lt;/span&gt;\n            ${accentColor}\n          &lt;/span&gt;\n          \n          &lt;span class=&quot;property-name&quot;&gt;Font Size:&lt;/span&gt;\n          &lt;span class=&quot;property-value ${\n    lastChanged === &quot;fontSize&quot; ? &quot;property-highlight&quot; : &quot;&quot;\n    }&quot;&gt;\n            ${fontSize}\n          &lt;/span&gt;\n          \n          &lt;span class=&quot;property-name&quot;&gt;Border Radius:&lt;/span&gt;\n          &lt;span class=&quot;property-value ${\n    lastChanged === &quot;borderRadius&quot; ? &quot;property-highlight&quot; : &quot;&quot;\n    }&quot;&gt;\n            ${borderRadius}\n          &lt;/span&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;demo-text&quot;&gt;\n          This text demonstrates the current theme settings. The border radius, font size,\n          and colors are all controlled by the theme context.\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n  }}\n\n\n// Theme toggler component\nclass ThemeToggler extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &quot;open&quot; });\n\n    // Create bound event handler to avoid recreating it\n    this.handleToggle = this.handleToggle.bind(this);\n\n    // Subscribe to theme changes\n    this.updateTheme = () =&gt; {\n      this.render();\n    };\n\n    themeContext.subscribe(this.updateTheme);\n    this.render();\n  }\n\n  disconnectedCallback() {\n    // Clean up event listeners and subscriptions\n    themeContext.unsubscribe(this.updateTheme);\n    this.removeToggleListener();\n  }\n\n  removeToggleListener() {\n    const button = this.shadowRoot.querySelector(&quot;#theme-toggle&quot;);\n    if (button) {\n      button.removeEventListener(&quot;click&quot;, this.handleToggle);\n    }\n  }\n\n  handleToggle() {\n    const currentTheme = themeContext.getTheme();\n    themeContext.setTheme(currentTheme === &quot;light&quot; ? &quot;dark&quot; : &quot;light&quot;);\n  }\n\n  render() {\n    // Clean up previous event listeners before re-rendering\n    this.removeToggleListener();\n\n    const theme = themeContext.getTheme();\n\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          margin: 16px 0;\n          padding: 16px;\n          border-radius: 4px;\n          border: 1px solid ${theme === &quot;light&quot; ? &quot;#ddd&quot; : &quot;#555&quot;};\n          background-color: ${theme === &quot;light&quot; ? &quot;#f9f9f9&quot; : &quot;#444&quot;};\n          transition: background-color 0.3s, border-color 0.3s;\n        }\n        \n        button {\n          background-color: ${theme === &quot;light&quot; ? &quot;#4CAF50&quot; : &quot;#66BB6A&quot;};\n          color: white;\n          border: none;\n          padding: 8px 16px;\n          border-radius: 4px;\n          cursor: pointer;\n          font-size: 16px;\n          transition: background-color 0.3s;\n        }\n        \n        button:hover {\n          background-color: ${theme === &quot;light&quot; ? &quot;#3e8e41&quot; : &quot;#4CAF50&quot;};\n          box-shadow: 0 2px 4px rgba(0,0,0,0.2);\n        }\n      &lt;/style&gt;\n      \n      &lt;div&gt;\n        &lt;p&gt;Click the button to toggle the theme:&lt;/p&gt;\n        &lt;button id=&quot;theme-toggle&quot;&gt;\n          Switch to ${theme === &quot;light&quot; ? &quot;Dark&quot; : &quot;Light&quot;} Theme\n        &lt;/button&gt;\n      &lt;/div&gt;\n    `;\n\n    // Add event listener\n    this.shadowRoot.\n    querySelector(&quot;#theme-toggle&quot;).\n    addEventListener(&quot;click&quot;, this.handleToggle);\n  }}\n\n\n// Theme controller component\nclass ThemeController extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &quot;open&quot; });\n\n    // Get current theme settings\n    this.theme = themeContext.getTheme();\n    this.accentColor = themeContext.getAccentColor();\n    this.fontSize = themeContext.getFontSize();\n    this.borderRadius = themeContext.getBorderRadius();\n\n    // Define preset accent colors\n    this.accentColors = [\n    &quot;#2196F3&quot;, // Blue (default)\n    &quot;#F44336&quot;, // Red\n    &quot;#4CAF50&quot;, // Green\n    &quot;#FF9800&quot;, // Orange\n    &quot;#9C27B0&quot;, // Purple\n    &quot;#00BCD4&quot;, // Cyan\n    &quot;#607D8B&quot; // Blue Grey\n    ];\n\n    // Define preset font sizes\n    this.fontSizes = [&quot;small&quot;, &quot;medium&quot;, &quot;large&quot;];\n\n    // Define preset border radius values\n    this.borderRadiusOptions = [&quot;0px&quot;, &quot;4px&quot;, &quot;8px&quot;, &quot;12px&quot;, &quot;16px&quot;];\n\n    this.render();\n\n    // Update when theme changes\n    this.updateTheme = settings =&gt; {\n      this.theme = settings.theme;\n      this.accentColor = settings.accentColor;\n      this.fontSize = settings.fontSize;\n      this.borderRadius = settings.borderRadius;\n      this.render();\n    };\n\n    themeContext.subscribe(this.updateTheme);\n  }\n\n  disconnectedCallback() {\n    themeContext.unsubscribe(this.updateTheme);\n  }\n\n  addEventListeners() {\n    // Theme toggle\n    const themeToggle = this.shadowRoot.getElementById(&quot;theme-toggle&quot;);\n    themeToggle.addEventListener(&quot;click&quot;, () =&gt; {\n      themeContext.setTheme(this.theme === &quot;light&quot; ? &quot;dark&quot; : &quot;light&quot;);\n    });\n\n    // Accent color selection\n    const colorButtons = this.shadowRoot.querySelectorAll(&quot;.color-option&quot;);\n    colorButtons.forEach(button =&gt; {\n      button.addEventListener(&quot;click&quot;, () =&gt; {\n        const color = button.getAttribute(&quot;data-color&quot;);\n        themeContext.setAccentColor(color);\n      });\n    });\n\n    // Font size selection\n    const fontSizeSelect = this.shadowRoot.getElementById(&quot;font-size-select&quot;);\n    fontSizeSelect.value = this.fontSize;\n    fontSizeSelect.addEventListener(&quot;change&quot;, () =&gt; {\n      themeContext.setFontSize(fontSizeSelect.value);\n    });\n\n    // Border radius slider\n    const radiusSlider = this.shadowRoot.getElementById(&quot;radius-slider&quot;);\n    const radiusValue = this.shadowRoot.getElementById(&quot;radius-value&quot;);\n\n    // Set initial value\n    const currentRadiusIndex = this.borderRadiusOptions.indexOf(\n    this.borderRadius);\n\n    radiusSlider.value = currentRadiusIndex &gt;= 0 ? currentRadiusIndex : 1;\n    radiusValue.textContent = this.borderRadius;\n\n    radiusSlider.addEventListener(&quot;input&quot;, () =&gt; {\n      const newRadius = this.borderRadiusOptions[radiusSlider.value];\n      radiusValue.textContent = newRadius;\n      themeContext.setBorderRadius(newRadius);\n    });\n  }\n\n  render() {\n    const theme = this.theme;\n\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          margin: 16px 0;\n          padding: 16px;\n          border-radius: ${this.borderRadius};\n          border: 1px solid ${theme === &quot;light&quot; ? &quot;#ddd&quot; : &quot;#555&quot;};\n          background-color: ${theme === &quot;light&quot; ? &quot;#f9f9f9&quot; : &quot;#444&quot;};\n          color: ${theme === &quot;light&quot; ? &quot;#333&quot; : &quot;#eee&quot;};\n          transition: all 0.3s;\n          font-family: system-ui, sans-serif;\n        }\n        \n        h3 {\n          margin-top: 0;\n          color: ${this.accentColor};\n          transition: color 0.3s;\n        }\n        \n        .control-group {\n          margin-bottom: 16px;\n        }\n        \n        .control-group label {\n          display: block;\n          margin-bottom: 8px;\n          font-weight: bold;\n        }\n        \n        .theme-toggle {\n          cursor: pointer;\n          background-color: ${theme === &quot;light&quot; ? &quot;#333&quot; : &quot;#f5f5f5&quot;};\n          color: ${theme === &quot;light&quot; ? &quot;#fff&quot; : &quot;#333&quot;};\n          border: none;\n          border-radius: ${this.borderRadius};\n          padding: 8px 16px;\n          font-size: 14px;\n          display: flex;\n          align-items: center;\n          justify-content: space-between;\n          width: 140px;\n          transition: background-color 0.3s, color 0.3s;\n        }\n        \n        .theme-toggle .icon {\n          display: inline-block;\n          width: 16px;\n          height: 16px;\n          border-radius: 50%;\n          background-color: ${theme === &quot;light&quot; ? &quot;#f5f5f5&quot; : &quot;#333&quot;};\n          margin-left: 8px;\n          position: relative;\n        }\n        \n        .theme-toggle .icon::before {\n          content: &#39;&#39;;\n          position: absolute;\n          top: 3px;\n          left: ${theme === &quot;light&quot; ? &quot;3px&quot; : &quot;12px&quot;};\n          width: 10px;\n          height: 10px;\n          border-radius: 50%;\n          background-color: ${theme === &quot;light&quot; ? &quot;#333&quot; : &quot;#f5f5f5&quot;};\n          transition: left 0.3s;\n        }\n        \n        .color-options {\n          display: flex;\n          gap: 8px;\n          flex-wrap: wrap;\n        }\n        \n        .color-option {\n          width: 32px;\n          height: 32px;\n          border-radius: 50%;\n          cursor: pointer;\n          position: relative;\n          border: 2px solid ${theme === &quot;light&quot; ? &quot;#ddd&quot; : &quot;#555&quot;};\n          transition: transform 0.2s;\n        }\n        \n        .color-option:hover {\n          transform: scale(1.1);\n        }\n        \n        .color-option.active {\n          border: 2px solid ${theme === &quot;light&quot; ? &quot;#333&quot; : &quot;#fff&quot;};\n        }\n        \n        .color-option.active::after {\n          content: &quot;✓&quot;;\n          position: absolute;\n          top: 50%;\n          left: 50%;\n          transform: translate(-50%, -50%);\n          color: ${this.isDarkColor(this.accentColor) ? &quot;#fff&quot; : &quot;#333&quot;};\n          font-size: 18px;\n          font-weight: bold;\n        }\n        \n        select, input[type=&quot;range&quot;] {\n          width: 100%;\n          padding: 6px;\n          border-radius: ${this.borderRadius};\n          border: 1px solid ${theme === &quot;light&quot; ? &quot;#ddd&quot; : &quot;#555&quot;};\n          background-color: ${theme === &quot;light&quot; ? &quot;#fff&quot; : &quot;#333&quot;};\n          color: ${theme === &quot;light&quot; ? &quot;#333&quot; : &quot;#fff&quot;};\n        }\n        \n        .slider-container {\n          display: flex;\n          align-items: center;\n          gap: 12px;\n        }\n        \n        .slider-container input {\n          flex: 1;\n        }\n        \n        .value-display {\n          min-width: 40px;\n          text-align: center;\n          font-family: monospace;\n          padding: 4px 8px;\n          background-color: ${\n    theme === &quot;light&quot; ? &quot;rgba(0,0,0,0.05)&quot; : &quot;rgba(255,255,255,0.1)&quot;\n    };\n          border-radius: ${this.borderRadius};\n        }\n      &lt;/style&gt;\n      \n      &lt;div&gt;\n        &lt;h3&gt;Theme Controller&lt;/h3&gt;\n        \n        &lt;div class=&quot;control-group&quot;&gt;\n          &lt;label&gt;Theme Mode&lt;/label&gt;\n          &lt;button id=&quot;theme-toggle&quot; class=&quot;theme-toggle&quot;&gt;\n            ${theme === &quot;light&quot; ? &quot;Light Mode&quot; : &quot;Dark Mode&quot;}\n            &lt;span class=&quot;icon&quot;&gt;&lt;/span&gt;\n          &lt;/button&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;control-group&quot;&gt;\n          &lt;label&gt;Accent Color&lt;/label&gt;\n          &lt;div class=&quot;color-options&quot;&gt;\n            ${this.accentColors.\n    map(\n    color =&gt; `\n              &lt;div \n                class=&quot;color-option ${\n    color === this.accentColor ? &quot;active&quot; : &quot;&quot;\n    }&quot; \n                style=&quot;background-color: ${color};&quot;\n                data-color=&quot;${color}&quot;&gt;\n              &lt;/div&gt;\n            `).\n\n    join(&quot;&quot;)}\n          &lt;/div&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;control-group&quot;&gt;\n          &lt;label&gt;Font Size&lt;/label&gt;\n          &lt;select id=&quot;font-size-select&quot;&gt;\n            ${this.fontSizes.\n    map(\n    size =&gt; `\n              &lt;option value=&quot;${size}&quot; ${\n    this.fontSize === size ? &quot;selected&quot; : &quot;&quot;\n    }&gt;${size.charAt(0).toUpperCase() + size.slice(1)}&lt;/option&gt;\n            `).\n\n    join(&quot;&quot;)}\n          &lt;/select&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;control-group&quot;&gt;\n          &lt;label&gt;Border Radius&lt;/label&gt;\n          &lt;div class=&quot;slider-container&quot;&gt;\n            &lt;input \n              id=&quot;radius-slider&quot; \n              type=&quot;range&quot; \n              min=&quot;0&quot; \n              max=&quot;${this.borderRadiusOptions.length - 1}&quot; \n              value=&quot;${this.borderRadiusOptions.indexOf(this.borderRadius)}&quot; \n            /&gt;\n            &lt;div id=&quot;radius-value&quot; class=&quot;value-display&quot;&gt;${\n    this.borderRadius\n    }&lt;/div&gt;\n          &lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    this.addEventListeners();\n  }\n\n  // Helper to determine if a color is dark\n  isDarkColor(color) {\n    // Convert hex to RGB\n    const hex = color.replace(&quot;#&quot;, &quot;&quot;);\n    const r = parseInt(hex.substr(0, 2), 16);\n    const g = parseInt(hex.substr(2, 2), 16);\n    const b = parseInt(hex.substr(4, 2), 16);\n\n    // Calculate brightness\n    const brightness = (r * 299 + g * 587 + b * 114) / 1000;\n    return brightness &lt; 128;\n  }}\n\n\n// Register the theme controller component\ncustomElements.define(&quot;theme-controller&quot;, ThemeController);\n\n// Register the components\ncustomElements.define(&quot;theme-provider&quot;, ThemeProvider);\ncustomElements.define(&quot;theme-consumer&quot;, ThemeConsumer);\ncustomElements.define(&quot;theme-toggler&quot;, ThemeToggler);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/the-future-of-web-components/\">the original article</a>.</p></figure>\n<h3 id=\"declarative-shadow-dom-server-side-renaissance\">Declarative Shadow DOM: Server-Side Renaissance<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#declarative-shadow-dom-server-side-renaissance\" aria-label=\"Link to this section\">#</a></h3>\n<p>One of the most significant limitations of Web Components has long been server-side rendering capabilities. The Declarative Shadow DOM (DSD) is changing this landscape, enabling Web Components to be rendered on the server before reaching the browser.</p>\n<p>Enhance.dev is built around this approach. Here is the syntax. Note the attribute is <code>shadowrootmode</code>; the older <code>shadowroot</code> spelling from early drafts no longer creates a shadow root (checked in Chrome 156):</p>\n<pre><code class=\"language-html\">&lt;my-card&gt;\n  &lt;template shadowrootmode=\"open\"&gt;\n    &lt;style&gt;:host { display: block; }&lt;/style&gt;\n    &lt;slot name=\"title\"&gt;&lt;/slot&gt;\n  &lt;/template&gt;\n  &lt;h1 slot=\"title\"&gt;Hello from the server!&lt;/h1&gt;\n&lt;/my-card&gt;\n</code></pre>\n<p>The browser builds the shadow tree while parsing the HTML, so the component paints before any JavaScript runs.</p>\n<p>Check out this interactive demo to see this pattern in action:</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Declarative Shadow Dom</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;my-card counter=&quot;1&quot;&gt;\n      &lt;template shadowrootmode=&quot;open&quot;&gt;\n        &lt;style&gt;\n          :host {\n            display: block;\n            border: 1px solid var(--border-color, #ccc);\n            border-radius: 8px;\n            padding: 16px;\n            font-family: system-ui, sans-serif;\n            background-color: var(--background-color, #fff);\n            color: var(--text-color, #333);\n          }\n\n          .card-header {\n            border-bottom: 1px solid var(--divider-color, #eee);\n            padding-bottom: 10px;\n            margin-bottom: 10px;\n          }\n\n          ::slotted(h1) {\n            margin-top: 0;\n            color: var(--text-color, #333);\n          }\n          \n          .counter {\n            margin-top: 15px;\n            padding-top: 15px;\n            border-top: 1px solid var(--divider-color, #eee);\n          }\n          \n          button {\n            background-color: var(--background-color);\n            color: var(--text-color);\n            border: 1px solid var(--border-color);\n            padding: 5px 10px;\n            margin-right: 10px;\n            border-radius: 4px;\n          }\n        &lt;/style&gt;\n\n        &lt;div class=&quot;card-header&quot;&gt;\n          &lt;slot name=&quot;title&quot;&gt;Default Title&lt;/slot&gt;\n        &lt;/div&gt;\n        &lt;div class=&quot;card-body&quot;&gt;\n          &lt;slot&gt;Default content&lt;/slot&gt;\n        &lt;/div&gt;\n        &lt;div class=&quot;counter&quot;&gt;\n          &lt;p&gt;Count: &lt;span id=&quot;count&quot;&gt;0&lt;/span&gt;&lt;/p&gt;\n          &lt;button id=&quot;decrement&quot;&gt;-&lt;/button&gt;\n          &lt;button id=&quot;increment&quot;&gt;+&lt;/button&gt;\n        &lt;/div&gt;\n      &lt;/template&gt;\n\n      &lt;h1 slot=&quot;title&quot;&gt;Hello from Declarative Shadow DOM&lt;/h1&gt;\n      &lt;p&gt;\n        This card demonstrates Declarative Shadow DOM for server-side rendering.\n      &lt;/p&gt;\n    &lt;/my-card&gt;\n    \n    &lt;div class=&quot;explanation&quot;&gt;\n      &lt;h2&gt;Benefits of Declarative Shadow DOM&lt;/h2&gt;\n      &lt;p&gt;\n        Declarative Shadow DOM provides significant advantages over imperative shadow DOM creation:\n      &lt;/p&gt;\n      &lt;ul&gt;\n        &lt;li&gt;&lt;strong&gt;Server-Side Rendering:&lt;/strong&gt; Components with encapsulated styling can be rendered on the server, improving initial page load performance&lt;/li&gt;\n        &lt;li&gt;&lt;strong&gt;Zero Hydration Cost:&lt;/strong&gt; No JavaScript is required to initialize the shadow DOM, making components immediately interactive&lt;/li&gt;\n        &lt;li&gt;&lt;strong&gt;Progressive Enhancement:&lt;/strong&gt; Components work even before JavaScript loads or if JavaScript fails&lt;/li&gt;\n        &lt;li&gt;&lt;strong&gt;SEO Friendly:&lt;/strong&gt; Content is available in the initial HTML, making it more accessible to search engines&lt;/li&gt;\n        &lt;li&gt;&lt;strong&gt;Simplified Component Definition:&lt;/strong&gt; No need for separate template files or string literals - the component structure is defined directly in HTML&lt;/li&gt;\n      &lt;/ul&gt;\n      &lt;p&gt;\n        The component above demonstrates these benefits while also implementing dark mode support,\n        showing how declarative shadow DOM can be both highly performant and user-friendly.\n      &lt;/p&gt;\n    &lt;/div&gt;    \n    &lt;h2 class=&quot;code-header&quot;&gt;Declarative Shadow DOM Code:&lt;/h2&gt;\n    &lt;pre&gt;&lt;code&gt;&amp;lt;my-card counter=&quot;1&quot; &amp;gt;\n  &amp;lt;template shadowrootmode=&quot;open&quot;&amp;gt;\n    &amp;lt;style&amp;gt;\n      :host {\n        display: block;\n        border: 1px solid var(--border-color, #ccc);\n        border-radius: 8px;\n        padding: 16px;\n        font-family: system-ui, sans-serif;\n        background-color: var(--background-color, #fff);\n        color: var(--text-color, #333);\n      }\n\n      .card-header {\n        border-bottom: 1px solid var(--divider-color, #eee);\n        padding-bottom: 10px;\n        margin-bottom: 10px;\n      }\n\n      ::slotted(h1) {\n        margin-top: 0;\n        color: var(--text-color, #333);\n      }\n      \n      .counter {\n        margin-top: 15px;\n        padding-top: 15px;\n        border-top: 1px solid var(--divider-color, #eee);\n      }\n      \n      button {\n        background-color: var(--background-color);\n        color: var(--text-color);\n        border: 1px solid var(--border-color);\n        padding: 5px 10px;\n        margin-right: 10px;\n        border-radius: 4px;\n      }\n    &amp;lt;/style&amp;gt;\n\n    &amp;lt;div class=&quot;card-header&quot;&amp;gt;\n      &amp;lt;slot name=&quot;title&quot;&amp;gt;Default Title&amp;lt;/slot&amp;gt;\n    &amp;lt;/div&amp;gt;\n    &amp;lt;div class=&quot;card-body&quot;&amp;gt;\n      &amp;lt;slot&amp;gt;Default content&amp;lt;/slot&amp;gt;\n    &amp;lt;/div&amp;gt;\n    &amp;lt;div class=&quot;counter&quot;&amp;gt;\n      &amp;lt;p&amp;gt;Count: &amp;lt;span id=&quot;count&quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;\n      &amp;lt;button id=&quot;decrement&quot;&amp;gt;-&amp;lt;/button&amp;gt;\n      &amp;lt;button id=&quot;increment&quot;&amp;gt;+&amp;lt;/button&amp;gt;\n    &amp;lt;/div&amp;gt;\n  &amp;lt;/template&amp;gt;\n\n  &amp;lt;h1 slot=&quot;title&quot;&amp;gt;Hello from Declarative Shadow DOM&amp;lt;/h1&amp;gt;\n  &amp;lt;p&amp;gt;\n    This card demonstrates Declarative Shadow DOM for server-side rendering.\n  &amp;lt;/p&amp;gt;\n&amp;lt;/my-card&amp;gt;&lt;/code&gt;&lt;/pre&gt;\n\n    &lt;div class=&quot;explanation&quot;&gt;\n      &lt;h2&gt;Adding JavaScript Logic with Declarative Shadow DOM&lt;/h2&gt;\n      &lt;p&gt;\n        While declarative shadow DOM provides the HTML structure and styling, you can still add JavaScript logic through custom elements:\n      &lt;/p&gt;\n      &lt;ul&gt;\n        &lt;li&gt;&lt;strong&gt;Progressive Enhancement:&lt;/strong&gt; The component renders with its structure and styling before JavaScript loads&lt;/li&gt;\n        &lt;li&gt;&lt;strong&gt;Logic Attachment:&lt;/strong&gt; JavaScript enhances the pre-rendered component with behavior after loading&lt;/li&gt;\n        &lt;li&gt;&lt;strong&gt;Works either way:&lt;/strong&gt; The custom element class works with both declaratively and imperatively created shadow DOM&lt;/li&gt;\n        &lt;li&gt;&lt;strong&gt;Attribute Reactivity:&lt;/strong&gt; Using attributes and the built-in lifecycle callbacks enables state management&lt;/li&gt;\n      &lt;/ul&gt;\n      &lt;p&gt;\n        This approach gives you the best of both worlds: instant rendering with declarative shadow DOM,\n        plus fully interactive behavior once JavaScript executes.\n      &lt;/p&gt;\n    &lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">:root {\n        color-scheme: light dark;\n        --background-color: #ffffff;\n        --text-color: #333333;\n        --border-color: #cccccc;\n        --divider-color: #eeeeee;\n      }\n\n      @media (prefers-color-scheme: dark) {\n        :root {\n          --background-color: #222222;\n          --text-color: #f0f0f0;\n          --border-color: #555555;\n          --divider-color: #444444;\n        }\n      }\n\n      body {\n        background-color: var(--background-color);\n        color: var(--text-color);\n        margin: 20px;\n      }\n\n      pre {\n        background-color: var(--background-color);\n        border: 1px solid var(--border-color);\n        border-radius: 4px;\n        padding: 15px;\n        overflow: auto;\n        margin-top: 30px;\n      }\n      \n      code {\n        font-family: monospace;\n        color: var(--text-color);\n        white-space: pre;\n      }\n      \n      .code-header {\n        font-weight: bold;\n        margin-bottom: 10px;\n        margin-top: 30px;\n        color: var(--text-color);\n      }\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Check and polyfill if necessary\nif (!HTMLTemplateElement.prototype.hasOwnProperty(&quot;shadowRootMode&quot;)) {\n  document.\n  querySelectorAll(&quot;template[shadowrootmode]&quot;).\n  forEach(template =&gt; {\n    const mode = template.getAttribute(&quot;shadowrootmode&quot;);\n    const shadowRoot = template.parentNode.attachShadow({ mode });\n    shadowRoot.appendChild(template.content);\n    template.remove();\n  });\n}\n\n// Register the element with behavior\ncustomElements.define(&quot;my-card&quot;, class extends HTMLElement {\n  static get observedAttributes() {\n    return [&#39;counter&#39;];\n  }\n\n  constructor() {\n    super();\n\n    // Get access to shadow DOM elements (works regardless of how shadow DOM was created)\n    this._setupShadowDOM();\n  }\n\n  _setupShadowDOM() {\n    // If the shadow root already exists (from declarative shadow DOM)\n    // we can use it directly, otherwise it would have been created by the polyfill\n    const shadowRoot = this.shadowRoot;\n\n    // Get references to elements in the shadow DOM\n    this._countDisplay = shadowRoot.querySelector(&#39;#count&#39;);\n    this._incrementBtn = shadowRoot.querySelector(&#39;#increment&#39;);\n    this._decrementBtn = shadowRoot.querySelector(&#39;#decrement&#39;);\n\n    // Add event listeners\n    this._incrementBtn.addEventListener(&#39;click&#39;, () =&gt; this._incrementCount());\n    this._decrementBtn.addEventListener(&#39;click&#39;, () =&gt; this._decrementCount());\n\n    // Initialize with attribute value\n    this._updateCount(parseInt(this.getAttribute(&#39;counter&#39;) || &#39;0&#39;));\n  }\n\n  connectedCallback() {\n    // Make sure the count is updated with the latest attribute value\n    // This handles the case where attributes might have changed before connection\n    this._updateCount(parseInt(this.getAttribute(&#39;counter&#39;) || &#39;0&#39;));\n  }\n\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (name === &#39;counter&#39; &amp;&amp; this._countDisplay) {\n      this._updateCount(parseInt(newValue || &#39;0&#39;));\n    }\n  }\n\n  _incrementCount() {\n    const current = parseInt(this.getAttribute(&#39;counter&#39;) || &#39;0&#39;);\n    this.setAttribute(&#39;counter&#39;, current + 1);\n  }\n\n  _decrementCount() {\n    const current = parseInt(this.getAttribute(&#39;counter&#39;) || &#39;0&#39;);\n    this.setAttribute(&#39;counter&#39;, Math.max(0, current - 1));\n  }\n\n  _updateCount(value) {\n    if (this._countDisplay) {\n      this._countDisplay.textContent = value;\n    }\n  }});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/the-future-of-web-components/\">the original article</a>.</p></figure>\n<h3 id=\"state-management-evolves\">State Management Evolves<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#state-management-evolves\" aria-label=\"Link to this section\">#</a></h3>\n<p>State management in complex Web Components can quickly become unwieldy. The community has answered with a few small patterns, shown below.</p>\n<p>Signals libraries offer a React-inspired state model without the framework dependencies, while Lit State provides reactive state management specifically tailored to component systems. Most exciting is the Custom State Pseudo-Class, which now ships in browsers and lets you style components based on their internal state. A component adds a state with <code>this.attachInternals().states.add('loading')</code>, and CSS matches it with <code>:state()</code>:</p>\n<pre><code class=\"language-css\">my-element:state(loading) {\n  opacity: 0.5;\n}\n</code></pre>\n<p>These are patterns you can write today. None of them is a browser feature.</p>\n<p>Here is another interactive example that uses <code>ElementInternals.states</code> and the <code>:state()</code> pseudo-class, styling the component from the page and from inside its own shadow root.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Custom State Pseudo-Class Demo</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;state-component&gt;&lt;/state-component&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body {\n\tfont-family: system-ui, sans-serif;\n\tmax-width: 800px;\n\tmargin: 0 auto;\n\tpadding: 20px;\n\ttransition: background-color 0.3s, color 0.3s;\n}\n\n@media (prefers-color-scheme: dark) {\n\tbody {\n\t\tbackground-color: #121212;\n\t\tcolor: #e0e0e0;\n\t}\n}\n\n/* Custom states are styled from outside with :state(), with no attributes involved */\nstate-component:state(loading) {\n\topacity: 0.5;\n\tpointer-events: none;\n}\n\nstate-component:state(error) {\n\toutline: 3px solid #f44336;\n\toutline-offset: 4px;\n\tborder-radius: 8px;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class StateComponent extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &quot;open&quot; });\n\n    // ElementInternals.states is a CustomStateSet: whatever it holds,\n    // CSS can match with :state(name), from outside or via :host(:state(name)).\n    this._internals = this.attachInternals();\n\n    // Initial state\n    this.state = &quot;idle&quot;;\n    this._internals.states.add(this.state);\n\n    // One bound listener, so it can be removed again\n    this.handleColorSchemeChange = this.handleColorSchemeChange.bind(this);\n\n    // Auto-reset timer reference\n    this.resetTimer = null;\n\n    // Default auto-reset duration (ms)\n    this.resetDuration = 3000;\n\n    // Check dark mode preference\n    this.darkMode = window.matchMedia(&#39;(prefers-color-scheme: dark)&#39;).matches;\n\n    this.render();\n  }\n\n  static get observedAttributes() {\n    return [&#39;reset-duration&#39;, &#39;dark-mode&#39;];\n  }\n\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (name === &#39;reset-duration&#39; &amp;&amp; newValue) {\n      this.resetDuration = parseInt(newValue, 10);\n    } else if (name === &#39;dark-mode&#39;) {\n      this.darkMode = newValue === &#39;true&#39;;\n      this.render();\n    }\n  }\n\n  connectedCallback() {\n    // Listen for system color scheme changes\n    this.colorSchemeMedia = window.matchMedia(&#39;(prefers-color-scheme: dark)&#39;);\n    this.colorSchemeMedia.addEventListener(&#39;change&#39;, this.handleColorSchemeChange);\n  }\n\n  disconnectedCallback() {\n    // Clean up timers when element is removed\n    this.clearResetTimer();\n\n    // Remove event listener\n    if (this.colorSchemeMedia) {\n      this.colorSchemeMedia.removeEventListener(&#39;change&#39;, this.handleColorSchemeChange);\n    }\n  }\n\n  handleColorSchemeChange(e) {\n    this.darkMode = e.matches;\n    this.render();\n  }\n\n  clearResetTimer() {\n    if (this.resetTimer) {\n      clearTimeout(this.resetTimer);\n      this.resetTimer = null;\n\n      // Also clear the progress animation\n      const progressBar = this.shadowRoot.querySelector(&#39;.progress-bar&#39;);\n      if (progressBar) {\n        progressBar.style.animation = &#39;none&#39;;\n      }\n    }\n  }\n\n  setState(newState) {\n    this.state = newState;\n    this._internals.states.clear();\n    this._internals.states.add(newState);\n\n    // Clear any existing timers\n    this.clearResetTimer();\n\n    // Set automatic reset for non-idle states\n    if (newState !== &quot;idle&quot;) {\n      this.resetTimer = setTimeout(() =&gt; {\n        this.setState(&quot;idle&quot;);\n      }, this.resetDuration);\n\n      // Update progress bar animation duration after render\n      setTimeout(() =&gt; {\n        const progressBar = this.shadowRoot.querySelector(&#39;.progress-bar&#39;);\n        if (progressBar) {\n          progressBar.style.animation = `progress ${this.resetDuration / 1000}s linear forwards`;\n        }\n      }, 0);\n    }\n\n    this.render();\n  }\n\n  toggleDarkMode() {\n    this.darkMode = !this.darkMode;\n    this.setAttribute(&#39;dark-mode&#39;, this.darkMode);\n    this.render();\n  }\n\n  render() {\n    let icon, message, ariaLive;\n\n    switch (this.state) {\n      case &quot;loading&quot;:\n        icon = &quot;⏳&quot;;\n        message = &quot;Loading...&quot;;\n        ariaLive = &quot;polite&quot;;\n        break;\n      case &quot;success&quot;:\n        icon = &quot;✅&quot;;\n        message = &quot;Operation successful!&quot;;\n        ariaLive = &quot;assertive&quot;;\n        break;\n      case &quot;error&quot;:\n        icon = &quot;❌&quot;;\n        message = &quot;An error occurred.&quot;;\n        ariaLive = &quot;assertive&quot;;\n        break;\n      default: // idle\n        icon = &quot;⚪&quot;;\n        message = &quot;Component is idle. Click a button to change state.&quot;;\n        ariaLive = &quot;off&quot;;}\n\n\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          padding: 20px;\n          border: 1px solid #ddd;\n          border-radius: 8px;\n          transition: all 0.3s;\n          background-color: ${this.darkMode ? &#39;#222&#39; : &#39;#fff&#39;};\n          color: ${this.darkMode ? &#39;#e0e0e0&#39; : &#39;#333&#39;};\n        }\n        \n        h2 {\n          margin-top: 0;\n          color: ${this.darkMode ? &#39;#fff&#39; : &#39;#333&#39;};\n        }\n        \n        .container {\n          display: flex;\n          align-items: center;\n          margin-bottom: 20px;\n        }\n        \n        .icon {\n          font-size: 32px;\n          margin-right: 16px;\n        }\n\n        :host(:state(success)) .message {\n          color: #4caf50;\n        }\n\n        :host(:state(error)) .message {\n          color: #f44336;\n        }\n        \n        .message {\n          font-size: 18px;\n        }\n        \n        .buttons {\n          display: grid;\n          grid-template-columns: repeat(4, 1fr);\n          gap: 10px;\n          margin-bottom: 15px;\n        }\n        \n        button {\n          padding: 8px;\n          border: none;\n          border-radius: 4px;\n          cursor: pointer;\n          transition: all 0.2s;\n        }\n        \n        button:hover {\n          filter: brightness(0.9);\n        }\n        \n        button:active {\n          transform: translateY(1px);\n        }\n        \n        button:focus {\n          outline: 2px solid #2196F3;\n          outline-offset: 2px;\n        }\n        \n        button.idle {\n          background-color: ${this.darkMode ? &#39;#555&#39; : &#39;#e0e0e0&#39;};\n          color: ${this.darkMode ? &#39;#fff&#39; : &#39;#333&#39;};\n        }\n        \n        button.loading {\n          background-color: #FFC107;\n          color: ${this.darkMode ? &#39;#000&#39; : &#39;#333&#39;};\n        }\n        \n        button.success {\n          background-color: #4CAF50;\n          color: white;\n        }\n        \n        button.error {\n          background-color: #f44336;\n          color: white;\n        }\n        \n        button.theme-toggle {\n          background-color: ${this.darkMode ? &#39;#777&#39; : &#39;#ddd&#39;};\n          color: ${this.darkMode ? &#39;#fff&#39; : &#39;#333&#39;};\n          margin-top: 10px;\n        }\n        \n        .settings {\n          display: flex;\n          flex-wrap: wrap;\n          align-items: center;\n          gap: 12px;\n          margin-bottom: 15px;\n        }\n\n        .settings input {\n          width: 4em;\n        }\n\n        .reset-info {\n          font-size: 14px;\n          margin: 10px 0;\n          color: ${this.darkMode ? &#39;#aaa&#39; : &#39;#666&#39;};\n          display: ${this.state === &quot;idle&quot; ? &quot;none&quot; : &quot;block&quot;};\n        }\n        \n        .progress-container {\n          height: 4px;\n          width: 100%;\n          background-color: ${this.darkMode ? &#39;#444&#39; : &#39;#eee&#39;};\n          margin-bottom: 15px;\n          border-radius: 2px;\n          overflow: hidden;\n          display: ${this.state === &quot;idle&quot; ? &quot;none&quot; : &quot;block&quot;};\n        }\n        \n        .progress-bar {\n          height: 100%;\n          width: 100%;\n          background-color: #2196F3;\n          transform-origin: left;\n        }\n        \n        @keyframes progress {\n          0% { transform: scaleX(1); }\n          100% { transform: scaleX(0); }\n        }\n        \n        .settings {\n          margin-top: 20px;\n          padding-top: 15px;\n          border-top: 1px solid ${this.darkMode ? &#39;#444&#39; : &#39;#eee&#39;};\n        }\n        \n        .config {\n          display: flex;\n          align-items: center;\n          gap: 10px;\n        }\n        \n        .config label {\n          font-size: 14px;\n        }\n        \n        .config input {\n          width: 70px;\n          background-color: ${this.darkMode ? &#39;#333&#39; : &#39;#fff&#39;};\n          color: ${this.darkMode ? &#39;#fff&#39; : &#39;#333&#39;};\n          border: 1px solid ${this.darkMode ? &#39;#555&#39; : &#39;#ddd&#39;};\n          padding: 4px;\n          border-radius: 4px;\n        }\n        \n        small {\n          color: ${this.darkMode ? &#39;#aaa&#39; : &#39;#666&#39;};\n        }\n      &lt;/style&gt;\n      \n      &lt;div&gt;\n        &lt;h2&gt;Custom State Pseudo-Class Demo&lt;/h2&gt;\n        &lt;p&gt;This demonstrates how state changes can be exposed to CSS.&lt;/p&gt;\n        \n        &lt;div class=&quot;progress-container&quot; aria-hidden=&quot;true&quot;&gt;\n          &lt;div class=&quot;progress-bar&quot;&gt;&lt;/div&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;reset-info&quot; aria-live=&quot;polite&quot;&gt;\n          Will reset to idle in ${this.resetDuration / 1000} seconds\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;container&quot;&gt;\n          &lt;div class=&quot;icon&quot; aria-hidden=&quot;true&quot;&gt;${icon}&lt;/div&gt;\n          &lt;div class=&quot;message&quot; aria-live=&quot;${ariaLive}&quot;&gt;${message}&lt;/div&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;buttons&quot; role=&quot;group&quot; aria-label=&quot;State control buttons&quot;&gt;\n          &lt;button class=&quot;idle&quot; id=&quot;set-idle&quot;&gt;Idle&lt;/button&gt;\n          &lt;button class=&quot;loading&quot; id=&quot;set-loading&quot;&gt;Loading&lt;/button&gt;\n          &lt;button class=&quot;success&quot; id=&quot;set-success&quot;&gt;Success&lt;/button&gt;\n          &lt;button class=&quot;error&quot; id=&quot;set-error&quot;&gt;Error&lt;/button&gt;\n        &lt;/div&gt;\n\n        &lt;div class=&quot;settings&quot;&gt;\n          &lt;button id=&quot;toggle-theme&quot; type=&quot;button&quot;&gt;${this.darkMode ? &#39;Light&#39; : &#39;Dark&#39;} theme&lt;/button&gt;\n          &lt;label&gt;\n            Reset after\n            &lt;input id=&quot;reset-time&quot; type=&quot;number&quot; min=&quot;1&quot; max=&quot;30&quot; value=&quot;${this.resetDuration / 1000}&quot;&gt;\n            seconds\n          &lt;/label&gt;\n        &lt;/div&gt;\n\n        &lt;p&gt;\n          &lt;small&gt;\n            This uses ElementInternals.states: the page styles\n            state-component:state(loading), and the component styles\n            :host(:state(success)) from inside its shadow root.\n          &lt;/small&gt;\n        &lt;/p&gt;\n      &lt;/div&gt;\n    `;\n\n    // Add event listeners\n    this.shadowRoot.\n    querySelector(&quot;#set-idle&quot;).\n    addEventListener(&quot;click&quot;, () =&gt; {\n      this.setState(&quot;idle&quot;);\n    });\n\n    this.shadowRoot.\n    querySelector(&quot;#set-loading&quot;).\n    addEventListener(&quot;click&quot;, () =&gt; {\n      this.setState(&quot;loading&quot;);\n    });\n\n    this.shadowRoot.\n    querySelector(&quot;#set-success&quot;).\n    addEventListener(&quot;click&quot;, () =&gt; {\n      this.setState(&quot;success&quot;);\n    });\n\n    this.shadowRoot.\n    querySelector(&quot;#set-error&quot;).\n    addEventListener(&quot;click&quot;, () =&gt; {\n      this.setState(&quot;error&quot;);\n    });\n\n    // Add theme toggle listener\n    this.shadowRoot.\n    querySelector(&quot;#toggle-theme&quot;).\n    addEventListener(&quot;click&quot;, () =&gt; {\n      this.toggleDarkMode();\n    });\n\n    // Add reset duration input listener\n    this.shadowRoot.\n    querySelector(&quot;#reset-time&quot;).\n    addEventListener(&quot;change&quot;, e =&gt; {\n      const seconds = parseInt(e.target.value, 10);\n      if (seconds &gt; 0) {\n        this.resetDuration = seconds * 1000;\n        // If there&#39;s an active timer, restart it with new duration\n        if (this.state !== &quot;idle&quot;) {\n          this.setState(this.state);\n        }\n      }\n    });\n  }}\n\n\ncustomElements.define(&quot;state-component&quot;, StateComponent);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/the-future-of-web-components/\">the original article</a>.</p></figure>\n<h3 id=\"micro-frontends-the-enterprise-adoption\">Micro-Frontends: The Enterprise Adoption<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#micro-frontends-the-enterprise-adoption\" aria-label=\"Link to this section\">#</a></h3>\n<p>A micro-frontend splits one page into parts that separate teams build and ship. Web components give each part a boundary that does not depend on a framework. Adobe's Spectrum Web Components and Microsoft's FAST are published as web components, so any of these parts can use them whatever framework it is written in.</p>\n<h2 id=\"community-momentum-tools-libraries\">Community Momentum: Tools &amp; Libraries<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#community-momentum-tools-libraries\" aria-label=\"Link to this section\">#</a></h2>\n<h3 id=\"lit-and-tooling\">Lit and Tooling<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#lit-and-tooling\" aria-label=\"Link to this section\">#</a></h3>\n<p>Lit is the most widely used helper library in this post. It is a small base class that adds reactive properties and templates on top of custom elements. The Lit team also publishes experimental tools under the <code>@lit-labs</code> scope, such as <code>@lit-labs/analyzer</code>, which reads component source to describe it. Check Lit's own release notes for what is stable; this post makes no claims about future Lit versions.</p>\n<h3 id=\"scoped-custom-element-registries-solving-collisions\">Scoped Custom Element Registries: Solving Collisions<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#scoped-custom-element-registries-solving-collisions\" aria-label=\"Link to this section\">#</a></h3>\n<p>The registry of custom element names is global: one tag name can only point to one class for the whole page. Scoped custom element registries let a shadow root have its own registry. The snippet below ran correctly in Chrome 156. As of 26 October 2026 it also ships in Chrome and Edge 146 and Safari 26, but not in a stable Firefox, so feature-detect it (<code>typeof CustomElementRegistry</code>) and have a plan for the browsers without it. It does not work for customized built-in elements.</p>\n<pre><code class=\"language-javascript\">const registry = new CustomElementRegistry();\nregistry.define('my-element', MyElement);\nhost.attachShadow({ mode: 'open', customElementRegistry: registry });\ndocument.createElement('my-element', { customElementRegistry: registry });\n</code></pre>\n<p>Before browsers shipped it, the <code>@webcomponents/scoped-custom-element-registry</code> polyfill let teams try the pattern. Most apps never hit a tag-name clash, so this is a feature for the day two teams define <code>&lt;x-button&gt;</code> differently.</p>\n<p>Below is an interactive example with two components that share one tag name. The demo checks for support first, so in a browser without scoped registries (Firefox, as of today) you will see a notice instead of the components.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Scoped Custom Element Registries</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div class=&quot;demo-container&quot;&gt;\n\t&lt;header&gt;\n\t\t&lt;h2&gt;Scoped Custom Element Registries&lt;/h2&gt;\n\t\t&lt;button class=&quot;theme-toggle&quot; id=&quot;themeToggle&quot;&gt;Toggle Theme&lt;/button&gt;\n\t&lt;/header&gt;\n\t&lt;p&gt;\n\t\tThis demo uses scoped custom element registries, built into Chrome, Edge and Safari.\n\t\tThe same tag name can have completely different implementations in different component scopes.\n\t&lt;/p&gt;\n\n\t&lt;component-a&gt;&lt;/component-a&gt;\n\t&lt;component-b&gt;&lt;/component-b&gt;\n\t&lt;component-c&gt;&lt;/component-c&gt;\n\n\t&lt;div class=&quot;explanation&quot;&gt;\n\t\t&lt;h3&gt;About Scoped Custom Element Registries&lt;/h3&gt;\n\t\t&lt;p&gt;\n\t\t\tScoped Custom Element Registries are part of the future of web components,\n\t\t\tallowing developers to:\n\t\t&lt;/p&gt;\n\t\t&lt;ul&gt;\n\t\t\t&lt;li&gt;Avoid name collisions between components&lt;/li&gt;\n\t\t\t&lt;li&gt;Use the same tag name for different implementations in different contexts&lt;/li&gt;\n\t\t\t&lt;li&gt;Create truly encapsulated component systems&lt;/li&gt;\n\t\t\t&lt;li&gt;Support easier component versioning&lt;/li&gt;\n\t\t\t&lt;li&gt;Enable better component composition patterns&lt;/li&gt;\n\t\t&lt;/ul&gt;\n\t\t&lt;p&gt;\n\t\t\tEach component creates a &lt;code&gt;new CustomElementRegistry()&lt;/code&gt;, defines its own\n\t\t\t&lt;code&gt;custom-button&lt;/code&gt; in it, and passes it to\n\t\t\t&lt;code&gt;attachShadow({ customElementRegistry })&lt;/code&gt;.\n\t\t&lt;/p&gt;\n\t&lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">:root {\n\t--primary-color: #6200ee;\n\t--secondary-color: #03dac6;\n\t--background-color: #ffffff;\n\t--text-color: #333333;\n}\n\n.dark-mode {\n\t--primary-color: #bb86fc;\n\t--secondary-color: #03dac6;\n\t--background-color: #121212;\n\t--text-color: #e0e0e0;\n}\n\nbody {\n\tfont-family: system-ui, sans-serif;\n\tbackground-color: var(--background-color);\n\tcolor: var(--text-color);\n\ttransition: background-color 0.3s ease;\n}\n\n.demo-container {\n\tmax-width: 800px;\n\tmargin: 0 auto;\n\tpadding: 20px;\n}\n\ncomponent-a,\ncomponent-b,\ncomponent-c {\n\tdisplay: block;\n\tmargin: 20px 0;\n\tpadding: 20px;\n\tborder: 1px solid #ddd;\n\tborder-radius: 8px;\n}\n\nheader {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\tmargin-bottom: 20px;\n}\n\n.theme-toggle {\n\tbackground-color: var(--primary-color);\n\tcolor: white;\n\tborder: none;\n\tpadding: 8px 16px;\n\tborder-radius: 4px;\n\tcursor: pointer;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Scoped Custom Element Registries: each component owns a CustomElementRegistry,\n// passes it to attachShadow(), and creates elements from it. Nothing is added to\n// the global customElements registry, so the same tag can mean different things.\nconst scopedRegistriesSupported = (() =&gt; {\n  try {\n    new CustomElementRegistry();\n    return true;\n  } catch {\n    return false;\n  }\n})();\n\nclass ScopedElementsMixin {\n  constructor() {\n    this.registry = new CustomElementRegistry();\n  }\n\n  // Define a custom element in this component&#39;s scope\n  defineScopedElement(tagName, constructor) {\n    this.registry.define(tagName, constructor);\n    return constructor;\n  }\n\n  // Create an element from this scope&#39;s definitions\n  createScopedElement(tagName) {\n    return document.createElement(tagName, { customElementRegistry: this.registry });\n  }\n\n  // Check if a tag is defined in this scope\n  hasScopedElement(tagName) {\n    return this.registry.get(tagName) !== undefined;\n  }\n}\n\n\n// Component A with its own registry\nclass ComponentA extends HTMLElement {\n  constructor() {\n    super();\n    this.scopedElements = new ScopedElementsMixin();\n    this.attachShadow({ mode: &quot;open&quot;, customElementRegistry: this.scopedElements.registry });\n\n    // Define version A of the custom button\n    class CustomButtonA extends HTMLElement {\n      connectedCallback() {\n        this.innerHTML = `\n          &lt;style&gt;\n            button {\n              background-color: var(--primary-color);\n              color: white;\n              border: none;\n              padding: 10px 20px;\n              border-radius: 4px;\n              font-size: 16px;\n              cursor: pointer;\n              transition: all 0.3s ease;\n            }\n            button:hover {\n              opacity: 0.9;\n              transform: translateY(-2px);\n            }\n          &lt;/style&gt;\n          &lt;button&gt;Component A Button&lt;/button&gt;\n        `;\n\n        const button = this.querySelector(&quot;button&quot;);\n        button.addEventListener(&quot;click&quot;, () =&gt; {\n          const event = new CustomEvent(&quot;component-action&quot;, {\n            bubbles: true,\n            composed: true,\n            detail: { source: &quot;Component A&quot;, action: &quot;clicked&quot; } });\n\n          this.dispatchEvent(event);\n        });\n      }}\n\n\n    // Register in the scoped registry\n    this.scopedElements.defineScopedElement(&quot;custom-button&quot;, CustomButtonA);\n\n    this.render();\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        .card {\n          padding: 16px;\n          background-color: var(--background-color);\n          border-radius: 8px;\n          box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n        }\n        \n        h3 {\n          margin-top: 0;\n          color: var(--primary-color);\n        }\n        \n        .output {\n          margin-top: 10px;\n          font-style: italic;\n          color: var(--primary-color);\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;card&quot;&gt;\n        &lt;h3&gt;Component A&lt;/h3&gt;\n        &lt;p&gt;This component has its own scoped definition of custom-button:&lt;/p&gt;\n        &lt;div id=&quot;button-container&quot;&gt;&lt;/div&gt;\n        &lt;div class=&quot;output&quot; id=&quot;output-a&quot;&gt;&lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    // Create the scoped element\n    const button = this.scopedElements.createScopedElement(&quot;custom-button&quot;);\n    this.shadowRoot.querySelector(&quot;#button-container&quot;).appendChild(button);\n\n    // Listen for events from any component\n    document.addEventListener(&quot;component-action&quot;, e =&gt; {\n      const output = this.shadowRoot.querySelector(&quot;#output-a&quot;);\n      output.textContent = `Event received: ${e.detail.source} ${e.detail.action}`;\n    });\n  }}\n\n\n// Component B with its own registry\nclass ComponentB extends HTMLElement {\n  constructor() {\n    super();\n    this.scopedElements = new ScopedElementsMixin();\n    this.attachShadow({ mode: &quot;open&quot;, customElementRegistry: this.scopedElements.registry });\n\n    // Define version B of the custom button\n    class CustomButtonB extends HTMLElement {\n      connectedCallback() {\n        this.innerHTML = `\n          &lt;style&gt;\n            button {\n              background-color: var(--secondary-color);\n              color: black;\n              border: none;\n              padding: 12px 24px;\n              border-radius: 50px;\n              font-size: 16px;\n              font-weight: bold;\n              cursor: pointer;\n              transition: all 0.3s ease;\n            }\n            button:hover {\n              box-shadow: 0 4px 8px rgba(0,0,0,0.2);\n            }\n          &lt;/style&gt;\n          &lt;button&gt;Component B Button&lt;/button&gt;\n        `;\n\n        const button = this.querySelector(&quot;button&quot;);\n        button.addEventListener(&quot;click&quot;, () =&gt; {\n          const event = new CustomEvent(&quot;component-action&quot;, {\n            bubbles: true,\n            composed: true,\n            detail: { source: &quot;Component B&quot;, action: &quot;clicked&quot; } });\n\n          this.dispatchEvent(event);\n        });\n      }}\n\n\n    // Register in the scoped registry\n    this.scopedElements.defineScopedElement(&quot;custom-button&quot;, CustomButtonB);\n\n    this.render();\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        .card {\n          padding: 16px;\n          background-color: var(--background-color);\n          border-radius: 8px;\n          box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n        }\n        \n        h3 {\n          margin-top: 0;\n          color: var(--secondary-color);\n        }\n        \n        .output {\n          margin-top: 10px;\n          font-style: italic;\n          color: var(--secondary-color);\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;card&quot;&gt;\n        &lt;h3&gt;Component B&lt;/h3&gt;\n        &lt;p&gt;This component has its own scoped definition of custom-button:&lt;/p&gt;\n        &lt;div id=&quot;button-container&quot;&gt;&lt;/div&gt;\n        &lt;div class=&quot;output&quot; id=&quot;output-b&quot;&gt;&lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    // Create the scoped element\n    const button = this.scopedElements.createScopedElement(&quot;custom-button&quot;);\n    this.shadowRoot.querySelector(&quot;#button-container&quot;).appendChild(button);\n\n    // Listen for events from any component\n    document.addEventListener(&quot;component-action&quot;, e =&gt; {\n      const output = this.shadowRoot.querySelector(&quot;#output-b&quot;);\n      output.textContent = `Event received: ${e.detail.source} ${e.detail.action}`;\n    });\n  }}\n\n\n// Component C with nested scoped elements\nclass ComponentC extends HTMLElement {\n  constructor() {\n    super();\n    this.scopedElements = new ScopedElementsMixin();\n    this.attachShadow({ mode: &quot;open&quot;, customElementRegistry: this.scopedElements.registry });\n\n    // Define version C of the custom button\n    class CustomButtonC extends HTMLElement {\n      connectedCallback() {\n        this.innerHTML = `\n                &lt;style&gt;\n                  button {\n                    background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));\n                    color: white;\n                    border: none;\n                    padding: 10px 20px;\n                    border-radius: 4px;\n                    font-size: 16px;\n                    cursor: pointer;\n                    box-shadow: 0 2px 5px rgba(0,0,0,0.2);\n                    transition: all 0.3s ease;\n                  }\n                  button:hover {\n                    transform: scale(1.05);\n                  }\n                &lt;/style&gt;\n                &lt;button&gt;Component C Button&lt;/button&gt;\n              `;\n\n        const button = this.querySelector(&quot;button&quot;);\n        button.addEventListener(&quot;click&quot;, () =&gt; {\n          const event = new CustomEvent(&quot;component-action&quot;, {\n            bubbles: true,\n            composed: true,\n            detail: { source: &quot;Component C&quot;, action: &quot;clicked&quot; } });\n\n          this.dispatchEvent(event);\n        });\n      }}\n\n\n    // Define a nested component with its own registry\n    class NestedComponent extends HTMLElement {\n      constructor() {\n        super();\n        this.nestedRegistry = new ScopedElementsMixin();\n        this.attachShadow({ mode: &quot;open&quot;, customElementRegistry: this.nestedRegistry.registry });\n\n        // Define a unique version of custom-button for nested component\n        class NestedButton extends HTMLElement {\n          connectedCallback() {\n            this.innerHTML = `\n                    &lt;style&gt;\n                      button {\n                        background-color: transparent;\n                        color: var(--text-color);\n                        border: 2px solid currentColor;\n                        padding: 8px 16px;\n                        border-radius: 4px;\n                        font-size: 14px;\n                        cursor: pointer;\n                      }\n                    &lt;/style&gt;\n                    &lt;button&gt;Nested Button&lt;/button&gt;\n                  `;\n\n            const button = this.querySelector(&quot;button&quot;);\n            button.addEventListener(&quot;click&quot;, () =&gt; {\n              const event = new CustomEvent(&quot;nested-action&quot;, {\n                bubbles: true,\n                composed: true });\n\n              this.dispatchEvent(event);\n            });\n          }}\n\n\n        // Register nested button in its own registry\n        this.nestedRegistry.defineScopedElement(&quot;custom-button&quot;, NestedButton);\n\n        // Render nested component\n        this.shadowRoot.innerHTML = `\n                &lt;style&gt;\n                  .nested {\n                    margin: 10px;\n                    padding: 10px;\n                    border: 1px dashed var(--text-color);\n                    border-radius: 4px;\n                  }\n                  h4 {\n                    margin-top: 0;\n                    font-size: 14px;\n                  }\n                &lt;/style&gt;\n                &lt;div class=&quot;nested&quot;&gt;\n                  &lt;h4&gt;Nested Component Scope&lt;/h4&gt;\n                  &lt;p&gt;This nested component has its own scoped registry:&lt;/p&gt;\n                  &lt;div id=&quot;nested-button-container&quot;&gt;&lt;/div&gt;\n                &lt;/div&gt;\n              `;\n\n        // Create and append the nested button\n        const nestedButton = this.nestedRegistry.createScopedElement(\n        &quot;custom-button&quot;);\n\n        this.shadowRoot.\n        querySelector(&quot;#nested-button-container&quot;).\n        appendChild(nestedButton);\n\n        // Listen for clicks on the nested button\n        this.shadowRoot.addEventListener(&quot;nested-action&quot;, () =&gt; {\n          const event = new CustomEvent(&quot;component-action&quot;, {\n            bubbles: true,\n            composed: true,\n            detail: { source: &quot;Nested Component&quot;, action: &quot;clicked&quot; } });\n\n          this.dispatchEvent(event);\n        });\n      }}\n\n\n    // Register components in C&#39;s scope\n    this.scopedElements.defineScopedElement(&quot;custom-button&quot;, CustomButtonC);\n    this.scopedElements.defineScopedElement(&quot;nested-component&quot;, NestedComponent);\n\n    this.render();\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n            &lt;style&gt;\n              .card {\n                padding: 16px;\n                background-color: var(--background-color);\n                border-radius: 8px;\n                box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n              }\n              \n              h3 {\n                margin-top: 0;\n                background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));\n                -webkit-background-clip: text;\n                -webkit-text-fill-color: transparent;\n              }\n              \n              .component-registry {\n                margin-top: 15px;\n                font-size: 14px;\n              }\n              \n              .output {\n                margin-top: 10px;\n                font-style: italic;\n                background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));\n                -webkit-background-clip: text;\n                -webkit-text-fill-color: transparent;\n                font-weight: bold;\n              }\n            &lt;/style&gt;\n            \n            &lt;div class=&quot;card&quot;&gt;\n              &lt;h3&gt;Component C (Nested Components)&lt;/h3&gt;\n              &lt;p&gt;This component demonstrates nested scoped registries:&lt;/p&gt;\n              &lt;div id=&quot;button-container&quot;&gt;&lt;/div&gt;\n              &lt;div id=&quot;nested-container&quot;&gt;&lt;/div&gt;\n              &lt;div class=&quot;component-registry&quot;&gt;\n                &lt;p&gt;Registered elements in this scope: \n                  &lt;code&gt;custom-button&lt;/code&gt;, &lt;code&gt;nested-component&lt;/code&gt;\n                &lt;/p&gt;\n              &lt;/div&gt;\n              &lt;div class=&quot;output&quot; id=&quot;output-c&quot;&gt;&lt;/div&gt;\n            &lt;/div&gt;\n          `;\n\n    // Create the scoped elements\n    const button = this.scopedElements.createScopedElement(&quot;custom-button&quot;);\n    const nested = this.scopedElements.createScopedElement(&quot;nested-component&quot;);\n\n    this.shadowRoot.querySelector(&quot;#button-container&quot;).appendChild(button);\n    this.shadowRoot.querySelector(&quot;#nested-container&quot;).appendChild(nested);\n\n    // Listen for events from any component\n    document.addEventListener(&quot;component-action&quot;, e =&gt; {\n      const output = this.shadowRoot.querySelector(&quot;#output-c&quot;);\n      output.textContent = `Event received: ${e.detail.source} ${e.detail.action}`;\n    });\n  }}\n\n\n// Register the components\nif (scopedRegistriesSupported) {\n  customElements.define(&quot;component-a&quot;, ComponentA);\n  customElements.define(&quot;component-b&quot;, ComponentB);\n  customElements.define(&quot;component-c&quot;, ComponentC);\n} else {\n  const notice = document.createElement(&quot;p&quot;);\n  notice.className = &quot;explanation&quot;;\n  notice.textContent = &quot;This browser does not support scoped custom element registries yet, so the components cannot be shown.&quot;;\n  document.querySelector(&quot;component-a&quot;).before(notice);\n}\n\n// Theme toggle functionality\nconst themeToggleButton = document.getElementById(&quot;themeToggle&quot;);\n\n// Function to update button text based on current theme\nfunction updateThemeButtonText() {\n  themeToggleButton.textContent = document.body.classList.contains(&quot;dark-mode&quot;) ?\n  &quot;Light Theme&quot; :\n  &quot;Dark Theme&quot;;\n}\n\n// Check user&#39;s system preference for dark mode\nfunction initializeThemeBasedOnPreference() {\n  // Check if user prefers dark mode\n  const prefersDarkMode = window.matchMedia(&quot;(prefers-color-scheme: dark)&quot;).\n  matches;\n\n  // Set initial theme based on preference\n  if (prefersDarkMode) {\n    document.body.classList.add(&quot;dark-mode&quot;);\n  }\n\n  // Update button text to reflect initial state\n  updateThemeButtonText();\n}\n\n// Initialize theme when page loads\ninitializeThemeBasedOnPreference();\n\n// Theme toggle button click handler\nthemeToggleButton.addEventListener(&quot;click&quot;, () =&gt; {\n  document.body.classList.toggle(&quot;dark-mode&quot;);\n  updateThemeButtonText();\n});\n\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/the-future-of-web-components/\">the original article</a>.</p></figure>\n<h3 id=\"testing-gets-easier\">Testing Gets Easier<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#testing-gets-easier\" aria-label=\"Link to this section\">#</a></h3>\n<p>Web Test Runner can run component tests in real browsers, and Storybook has a web-components renderer that can read a component's description from a Custom Elements Manifest. Both reduce the need for framework-specific test setups.</p>\n<h2 id=\"on-the-horizon-features-to-watch\">On the Horizon: Features to Watch<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#on-the-horizon-features-to-watch\" aria-label=\"Link to this section\">#</a></h2>\n<h3 id=\"manual-slot-assignment\">Manual Slot Assignment<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#manual-slot-assignment\" aria-label=\"Link to this section\">#</a></h3>\n<p>Normally the browser fills slots by matching <code>slot=\"name\"</code> attributes. Manual assignment lets script decide instead. It ran in Chrome 156, and it is Baseline widely available (all three engines since March 2023).</p>\n<pre><code class=\"language-javascript\">const root = host.attachShadow({ mode: 'open', slotAssignment: 'manual' });\nconst slot = document.createElement('slot');\nroot.append(slot);\n\nslot.assign(host.querySelector('h1')); // this child now fills the slot\n</code></pre>\n<p>You may see an <code>element.attachSlot('header')</code> call in older articles. That method does not exist (it is <code>undefined</code> in Chrome 156). Use <code>slotAssignment: 'manual'</code> and <code>slot.assign()</code>.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Which of these is NOT a real browser API?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>slot.assign(node)</code> with <code>slotAssignment: 'manual'</code></li>\n<li><code>new CustomElementRegistry()</code></li>\n<li><code>element.attachSlot('header')</code></li>\n<li><code>internals.states.add('loading')</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: C. <code>element.attachSlot('header')</code></p>\n<p><code>attachSlot</code> does not exist. Manual slotting is <code>slotAssignment: 'manual'</code> plus <code>slot.assign()</code>; scoped registries and custom states are real and ran in Chrome 156.</p>\n</div>\n</div>\n</section>\n<h3 id=\"scope-scoped-rules-not-shadow-dom\"><code>@scope</code>: Scoped Rules, Not Shadow DOM<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#scope-scoped-rules-not-shadow-dom\" aria-label=\"Link to this section\">#</a></h3>\n<p><code>@scope</code> limits where a CSS rule applies in a normal stylesheet. It became Baseline newly available in March 2026:</p>\n<pre><code class=\"language-css\">@scope (.card) {\n  h1 { color: red; } /* Only matches h1 elements inside .card */\n}\n</code></pre>\n<p>It works in the light DOM and does not create or cross a shadow boundary (in the test, an <code>h1</code> inside <code>.card</code> turned red and an <code>h1</code> outside stayed black). Styling across a shadow boundary is done with CSS custom properties, <code>::part()</code> and <code>::slotted()</code>, all of which ship today.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>What does <code>@scope (.card) { h1 { color: red; } }</code> do?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Creates a shadow root on every <code>.card</code></li>\n<li>Applies the rule only to <code>h1</code> elements inside <code>.card</code></li>\n<li>Lets outside CSS style the inside of any shadow root</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Applies the rule only to <code>h1</code> elements inside <code>.card</code></p>\n<p><code>@scope</code> is a cascade feature for the normal DOM. Shadow roots are created with <code>attachShadow()</code>.</p>\n</div>\n</div>\n</section>\n<h3 id=\"webassembly-integration\">WebAssembly Integration<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#webassembly-integration\" aria-label=\"Link to this section\">#</a></h3>\n<p>WebAssembly is compiled code that runs in the browser. A custom element can call into it, for example to do heavy number crunching while keeping a plain HTML interface. Tools such as <code>wasm-bindgen</code> let Rust code talk to JavaScript. This is a combination of existing features, not a new web component standard.</p>\n<p>Check out a simulation of how we can bring WebAssembly into a web component below:</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">WebAssembly Component Demo</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;button id=&quot;dark-mode-toggle&quot; class=&quot;dark-mode-toggle&quot;&gt;🌙&lt;/button&gt;\n&lt;h1&gt;WebAssembly Component Integration&lt;/h1&gt;\n&lt;p&gt;\n\tThis component demonstrates how WebAssembly can be integrated with Web\n\tComponents for high-performance computation.\n&lt;/p&gt;\n\n&lt;wasm-component initial-value=&quot;5&quot; wasm-url=&quot;math_ops.wasm&quot;&gt;&lt;/wasm-component&gt;\n\n&lt;div style=&quot;margin-top: 30px&quot;&gt;\n\t&lt;h3&gt;Component API Usage&lt;/h3&gt;\n\t&lt;button id=&quot;external-set&quot;&gt;Set Value to 42&lt;/button&gt;\n\t&lt;button id=&quot;external-get&quot;&gt;Get Current Value&lt;/button&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body {\n\tfont-family: system-ui, sans-serif;\n\tmax-width: 800px;\n\tmargin: 0 auto;\n\tpadding: 20px;\n\ttransition: background-color 0.3s, color 0.3s;\n}\n\nbody.dark-mode {\n\tbackground-color: #121212;\n\tcolor: #e0e0e0;\n}\n\n.dark-mode-toggle {\n\tposition: absolute;\n\ttop: 20px;\n\tright: 20px;\n\tbackground: none;\n\tborder: none;\n\tcursor: pointer;\n\tfont-size: 24px;\n}\n\nh1,\nh3 {\n\ttransition: color 0.3s;\n}\n\nbody.dark-mode h1,\nbody.dark-mode h3 {\n\tcolor: #e0e0e0;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Dark mode functionality\nfunction initDarkMode() {\n\t// Check for user preference\n\tconst prefersDarkMode =\n\t\twindow.matchMedia &amp;&amp;\n\t\twindow.matchMedia(&quot;(prefers-color-scheme: dark)&quot;).matches;\n\n\t// Set initial mode based on preference\n\tif (prefersDarkMode) {\n\t\tdocument.body.classList.add(&quot;dark-mode&quot;);\n\t}\n\n\t// Add toggle listener\n\tdocument.getElementById(&quot;dark-mode-toggle&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\t\tdocument.body.classList.toggle(&quot;dark-mode&quot;);\n\t\tupdateDarkModeIcon();\n\t});\n\n\tupdateDarkModeIcon();\n}\n\nfunction updateDarkModeIcon() {\n\tconst isDarkMode = document.body.classList.contains(&quot;dark-mode&quot;);\n\tdocument.getElementById(&quot;dark-mode-toggle&quot;).textContent = isDarkMode\n\t\t? &quot;☀️&quot;\n\t\t: &quot;🌙&quot;;\n}\n\nclass WasmComponent extends HTMLElement {\n\tstatic observedAttributes = [&quot;initial-value&quot;, &quot;wasm-url&quot;];\n\n\tconstructor() {\n\t\tsuper();\n\t\tthis.attachShadow({ mode: &quot;open&quot; });\n\t\tthis.counter = 0;\n\t\tthis.isLoading = true;\n\t\tthis.hasError = false;\n\t\tthis.errorMessage = &quot;&quot;;\n\t\tthis.wasmModule = null;\n\t\tthis.MAX_VALUE = 1.9767639272283756e154; // Maximum allowed value\n\n\t\t// Check if dark mode is active when component is created\n\t\tthis.isDarkMode = document.body.classList.contains(&quot;dark-mode&quot;);\n\n\t\tthis.render();\n\t}\n\n\tconnectedCallback() {\n\t\t// Listen for dark mode changes on the body\n\t\tthis._darkModeObserver = new MutationObserver((mutations) =&gt; {\n\t\t\tmutations.forEach((mutation) =&gt; {\n\t\t\t\tif (mutation.attributeName === &quot;class&quot;) {\n\t\t\t\t\tthis.isDarkMode = document.body.classList.contains(&quot;dark-mode&quot;);\n\t\t\t\t\tthis.render();\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\n\t\tthis._darkModeObserver.observe(document.body, { attributes: true });\n\n\t\t// Get initial values from attributes\n\t\tif (this.hasAttribute(&quot;initial-value&quot;)) {\n\t\t\tthis.counter = parseInt(this.getAttribute(&quot;initial-value&quot;), 10) || 0;\n\t\t\tthis.updateDisplay();\n\t\t}\n\n\t\t// Load the WASM module\n\t\tconst wasmUrl = this.getAttribute(&quot;wasm-url&quot;) || &quot;math_ops.wasm&quot;;\n\t\tthis.loadWasmModule(wasmUrl);\n\t}\n\n\tdisconnectedCallback() {\n\t\t// Clean up observer when component is removed\n\t\tif (this._darkModeObserver) {\n\t\t\tthis._darkModeObserver.disconnect();\n\t\t}\n\t}\n\n\tattributeChangedCallback(name, oldValue, newValue) {\n\t\tif (name === &quot;initial-value&quot; &amp;&amp; this.shadowRoot) {\n\t\t\tthis.counter = parseInt(newValue, 10) || 0;\n\t\t\tthis.updateDisplay();\n\t\t}\n\n\t\tif (name === &quot;wasm-url&quot; &amp;&amp; oldValue !== newValue &amp;&amp; this.isConnected) {\n\t\t\tthis.isLoading = true;\n\t\t\tthis.hasError = false;\n\t\t\tthis.render();\n\t\t\tthis.loadWasmModule(newValue);\n\t\t}\n\t}\n\n\tasync loadWasmModule(url) {\n\t\ttry {\n\t\t\tthis.isLoading = true;\n\t\t\tthis.render();\n\n\t\t\tawait new Promise((resolve) =&gt; setTimeout(resolve, 1000));\n\n\t\t\tthis.wasmModule = {\n\t\t\t\texports: {\n\t\t\t\t\tincrement: (value) =&gt; {\n\t\t\t\t\t\tconst result = value + 1;\n\t\t\t\t\t\treturn result &gt; this.MAX_VALUE ? this.MAX_VALUE : result;\n\t\t\t\t\t},\n\t\t\t\t\tdecrement: (value) =&gt; value - 1,\n\t\t\t\t\tsquare: (value) =&gt; {\n\t\t\t\t\t\tconst result = value * value;\n\t\t\t\t\t\treturn result &gt; this.MAX_VALUE ? this.MAX_VALUE : result;\n\t\t\t\t\t},\n\t\t\t\t\tcube: (value) =&gt; {\n\t\t\t\t\t\tconst result = value * value * value;\n\t\t\t\t\t\treturn result &gt; this.MAX_VALUE ? this.MAX_VALUE : result;\n\t\t\t\t\t},\n\t\t\t\t\tdouble: (value) =&gt; {\n\t\t\t\t\t\tconst result = value * 2;\n\t\t\t\t\t\treturn result &gt; this.MAX_VALUE ? this.MAX_VALUE : result;\n\t\t\t\t\t},\n\t\t\t\t\thalf: (value) =&gt; value / 2,\n\t\t\t\t\tfactorial: (value) =&gt; {\n\t\t\t\t\t\tif (value &lt;= 1) return 1;\n\t\t\t\t\t\tlet result = 1;\n\t\t\t\t\t\tfor (let i = 2; i &lt;= value; i++) {\n\t\t\t\t\t\t\tresult *= i;\n\t\t\t\t\t\t\tif (result &gt; this.MAX_VALUE) return this.MAX_VALUE;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t},\n\t\t\t\t\tisPrime: (value) =&gt; {\n\t\t\t\t\t\tif (value &lt;= 1) return 0;\n\t\t\t\t\t\tif (value &lt;= 3) return 1;\n\t\t\t\t\t\tif (value % 2 === 0 || value % 3 === 0) return 0;\n\t\t\t\t\t\tlet i = 5;\n\t\t\t\t\t\twhile (i * i &lt;= value) {\n\t\t\t\t\t\t\tif (value % i === 0 || value % (i + 2) === 0) return 0;\n\t\t\t\t\t\t\ti += 6;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn 1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tthis.isLoading = false;\n\t\t\tthis.hasError = false;\n\n\t\t\tthis.dispatchEvent(\n\t\t\t\tnew CustomEvent(&quot;wasm-loaded&quot;, {\n\t\t\t\t\tbubbles: true,\n\t\t\t\t\tcomposed: true,\n\t\t\t\t\tdetail: { success: true }\n\t\t\t\t})\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tconsole.error(&quot;Failed to load WASM module:&quot;, error);\n\t\t\tthis.isLoading = false;\n\t\t\tthis.hasError = true;\n\t\t\tthis.errorMessage = error.message || &quot;Failed to load WebAssembly module&quot;;\n\n\t\t\tthis.dispatchEvent(\n\t\t\t\tnew CustomEvent(&quot;wasm-error&quot;, {\n\t\t\t\t\tbubbles: true,\n\t\t\t\t\tcomposed: true,\n\t\t\t\t\tdetail: { error }\n\t\t\t\t})\n\t\t\t);\n\t\t}\n\n\t\tthis.render();\n\t}\n\n\trender() {\n\t\tthis.shadowRoot.innerHTML = `\n              &lt;style&gt;\n                :host {\n                  display: block;\n                  padding: 20px;\n                  border: 1px solid ${this.isDarkMode ? &quot;#9575cd&quot; : &quot;#673AB7&quot;};\n                  border-radius: 8px;\n                  background-color: ${this.isDarkMode ? &quot;#1e1e2f&quot; : &quot;#f8f5ff&quot;};\n                  color: ${this.isDarkMode ? &quot;#e0e0e0&quot; : &quot;inherit&quot;};\n                  transition: background-color 0.3s, border-color 0.3s, color 0.3s;\n                }\n                \n                h2 {\n                  margin-top: 0;\n                  color: ${this.isDarkMode ? &quot;#9575cd&quot; : &quot;#673AB7&quot;};\n                  transition: color 0.3s;\n                }\n                \n                .display {\n                  font-size: 48px;\n                  text-align: center;\n                  margin: 20px 0;\n                  font-weight: bold;\n                  color: ${this.isDarkMode ? &quot;#9575cd&quot; : &quot;#673AB7&quot;};\n                  transition: color 0.3s;\n                }\n                \n                .buttons {\n                  display: flex;\n                  flex-wrap: wrap;\n                  justify-content: center;\n                  gap: 10px;\n                  margin-bottom: 15px;\n                }\n                \n                button {\n                  background-color: ${this.isDarkMode ? &quot;#9575cd&quot; : &quot;#673AB7&quot;};\n                  color: ${this.isDarkMode ? &quot;#121212&quot; : &quot;white&quot;};\n                  border: none;\n                  padding: 10px 20px;\n                  border-radius: 4px;\n                  cursor: pointer;\n                  font-size: 16px;\n                  transition: background-color 0.2s, color 0.2s;\n                }\n                \n                button:hover {\n                  background-color: ${this.isDarkMode ? &quot;#b39ddb&quot; : &quot;#5e35b1&quot;};\n                }\n                \n                button:disabled {\n                  background-color: ${this.isDarkMode ? &quot;#4a4a4a&quot; : &quot;#b39ddb&quot;};\n                  cursor: not-allowed;\n                }\n                \n                .loading {\n                  text-align: center;\n                  padding: 20px;\n                  color: ${this.isDarkMode ? &quot;#9575cd&quot; : &quot;#673AB7&quot;};\n                }\n                \n                .loading::after {\n                  content: &quot;...&quot;;\n                  animation: dots 1.5s infinite;\n                }\n                \n                .error {\n                  background-color: ${this.isDarkMode ? &quot;#3c1f1f&quot; : &quot;#ffebee&quot;};\n                  border: 1px solid ${this.isDarkMode ? &quot;#b71c1c&quot; : &quot;#f44336&quot;};\n                  color: ${this.isDarkMode ? &quot;#f48fb1&quot; : &quot;#d32f2f&quot;};\n                  padding: 10px;\n                  border-radius: 4px;\n                  margin: 10px 0;\n                }\n                \n                .note {\n                  margin-top: 20px;\n                  font-size: 14px;\n                  color: ${this.isDarkMode ? &quot;#aaa&quot; : &quot;#666&quot;};\n                  font-style: italic;\n                }\n                \n                .prime-indicator {\n                  font-size: 14px;\n                  margin-top: 5px;\n                  text-align: center;\n                  font-weight: bold;\n                }\n                \n                .prime-yes {\n                  color: ${this.isDarkMode ? &quot;#81c784&quot; : &quot;#4caf50&quot;};\n                }\n                \n                .prime-no {\n                  color: ${this.isDarkMode ? &quot;#e57373&quot; : &quot;#f44336&quot;};\n                }\n                \n                @keyframes dots {\n                  0%, 20% { content: &quot;.&quot;; }\n                  40% { content: &quot;..&quot;; }\n                  60%, 100% { content: &quot;...&quot;; }\n                }\n              &lt;/style&gt;\n              \n              &lt;div&gt;\n                &lt;h2&gt;Advanced WebAssembly Component Demo&lt;/h2&gt;\n                &lt;p&gt;This demonstrates how a Web Component integrates with WebAssembly.&lt;/p&gt;\n                \n                ${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.isLoading\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? `\n                  &lt;div class=&quot;loading&quot;&gt;Loading WebAssembly module&lt;/div&gt;\n                `\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: this.hasError\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? `\n                  &lt;div class=&quot;error&quot;&gt;\n                    &lt;strong&gt;Error:&lt;/strong&gt; ${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.errorMessage || &quot;Failed to load WebAssembly module&quot;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n                  &lt;/div&gt;\n                `\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: `\n                  &lt;div class=&quot;display&quot;&gt;${this.counter}&lt;/div&gt;\n                  \n                  &lt;div class=&quot;prime-indicator ${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.wasmModule?.exports.isPrime(this.counter)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? &quot;prime-yes&quot;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: &quot;prime-no&quot;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}&quot;&gt;\n                    ${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.wasmModule?.exports.isPrime(this.counter)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? &quot;This is a prime number!&quot;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: this.counter &gt; 1\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? &quot;This is not a prime number&quot;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: &quot;&quot;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n                  &lt;/div&gt;\n                  \n                  &lt;div class=&quot;buttons&quot;&gt;\n                    &lt;button id=&quot;decrement&quot; ?disabled=&quot;${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.isLoading\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}&quot;&gt;Decrement&lt;/button&gt;\n                    &lt;button id=&quot;increment&quot; ?disabled=&quot;${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.isLoading\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}&quot;&gt;Increment&lt;/button&gt;\n                    &lt;button id=&quot;double&quot; ?disabled=&quot;${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.isLoading\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}&quot;&gt;Double&lt;/button&gt;\n                    &lt;button id=&quot;half&quot; ?disabled=&quot;${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.isLoading\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}&quot;&gt;Half&lt;/button&gt;\n                    &lt;button id=&quot;square&quot; ?disabled=&quot;${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.isLoading\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}&quot;&gt;Square&lt;/button&gt;\n                    &lt;button id=&quot;cube&quot; ?disabled=&quot;${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.isLoading\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}&quot;&gt;Cube&lt;/button&gt;\n                    &lt;button id=&quot;factorial&quot; ?disabled=&quot;${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.isLoading || this.counter &gt; 12\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}&quot;&gt;Factorial&lt;/button&gt;\n                    &lt;button id=&quot;reset&quot; ?disabled=&quot;${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.isLoading\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}&quot;&gt;Reset&lt;/button&gt;\n                  &lt;/div&gt;\n                `\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n                \n                &lt;p class=&quot;note&quot;&gt;\n                  ${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.isLoading\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? &quot;Loading WebAssembly module...&quot;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: this.hasError\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? &quot;Failed to load WebAssembly module. Check console for details.&quot;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: &quot;Note: This simulates WASM integration. In a production environment, these operations would be performed by actual WebAssembly code.&quot;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n                &lt;/p&gt;\n              &lt;/div&gt;\n            `;\n\n\t\tif (!this.isLoading &amp;&amp; !this.hasError) {\n\t\t\tthis.shadowRoot.querySelector(&quot;#increment&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\t\t\t\tthis.counter = this.wasmModule.exports.increment(this.counter);\n\t\t\t\tthis.updateDisplay();\n\t\t\t\tthis.checkMaxValueReached();\n\t\t\t});\n\n\t\t\tthis.shadowRoot.querySelector(&quot;#decrement&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\t\t\t\tthis.counter = this.wasmModule.exports.decrement(this.counter);\n\t\t\t\tthis.updateDisplay();\n\t\t\t});\n\n\t\t\tthis.shadowRoot.querySelector(&quot;#square&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\t\t\t\tthis.counter = this.wasmModule.exports.square(this.counter);\n\t\t\t\tthis.updateDisplay();\n\t\t\t\tthis.checkMaxValueReached();\n\t\t\t});\n\n\t\t\tthis.shadowRoot.querySelector(&quot;#cube&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\t\t\t\tthis.counter = this.wasmModule.exports.cube(this.counter);\n\t\t\t\tthis.updateDisplay();\n\t\t\t\tthis.checkMaxValueReached();\n\t\t\t});\n\n\t\t\tthis.shadowRoot.querySelector(&quot;#double&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\t\t\t\tthis.counter = this.wasmModule.exports.double(this.counter);\n\t\t\t\tthis.updateDisplay();\n\t\t\t\tthis.checkMaxValueReached();\n\t\t\t});\n\n\t\t\tthis.shadowRoot.querySelector(&quot;#half&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\t\t\t\tthis.counter = this.wasmModule.exports.half(this.counter);\n\t\t\t\tthis.updateDisplay();\n\t\t\t});\n\n\t\t\tthis.shadowRoot.querySelector(&quot;#factorial&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\t\t\t\tif (this.counter &lt;= 12) {\n\t\t\t\t\tthis.counter = this.wasmModule.exports.factorial(this.counter);\n\t\t\t\t\tthis.updateDisplay();\n\t\t\t\t\tthis.checkMaxValueReached();\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tthis.shadowRoot.querySelector(&quot;#reset&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\t\t\t\tthis.counter = 0;\n\t\t\t\tthis.updateDisplay();\n\t\t\t});\n\t\t}\n\t}\n\n\tcheckMaxValueReached() {\n\t\tif (this.counter &gt;= this.MAX_VALUE) {\n\t\t\tconsole.warn(&quot;Maximum value reached&quot;);\n\t\t\tconst note = this.shadowRoot.querySelector(&quot;.note&quot;);\n\t\t\tif (note) {\n\t\t\t\tnote.innerHTML =\n\t\t\t\t\t&#39;&lt;strong style=&quot;color: #ff9800;&quot;&gt;Warning: Maximum value reached. Some operations may be limited.&lt;/strong&gt;&#39;;\n\n\t\t\t\tsetTimeout(() =&gt; {\n\t\t\t\t\tnote.innerHTML =\n\t\t\t\t\t\t&quot;Note: This simulates WASM integration. In a production environment, these operations would be performed by actual WebAssembly code.&quot;;\n\t\t\t\t}, 3000);\n\t\t\t}\n\t\t}\n\t}\n\n\tupdateDisplay() {\n\t\tif (this.shadowRoot.querySelector(&quot;.display&quot;)) {\n\t\t\tlet displayText = this.counter;\n\t\t\tif (this.counter &gt; 1e15) {\n\t\t\t\tdisplayText = this.counter.toExponential(2);\n\t\t\t}\n\t\t\tthis.shadowRoot.querySelector(&quot;.display&quot;).textContent = displayText;\n\n\t\t\tconst primeIndicator = this.shadowRoot.querySelector(&quot;.prime-indicator&quot;);\n\t\t\tif (primeIndicator) {\n\t\t\t\tconst isPrime = this.wasmModule.exports.isPrime(this.counter);\n\t\t\t\tprimeIndicator.className = `prime-indicator ${\n\t\t\t\t\tisPrime ? &quot;prime-yes&quot; : &quot;prime-no&quot;\n\t\t\t\t}`;\n\t\t\t\tprimeIndicator.textContent = isPrime\n\t\t\t\t\t? &quot;This is a prime number!&quot;\n\t\t\t\t\t: this.counter &gt; 1\n\t\t\t\t\t? &quot;This is not a prime number&quot;\n\t\t\t\t\t: &quot;&quot;;\n\t\t\t}\n\n\t\t\tconst factorialBtn = this.shadowRoot.querySelector(&quot;#factorial&quot;);\n\t\t\tif (factorialBtn) {\n\t\t\t\tfactorialBtn.disabled = this.counter &gt; 12;\n\t\t\t}\n\t\t}\n\n\t\tthis.dispatchEvent(\n\t\t\tnew CustomEvent(&quot;value-changed&quot;, {\n\t\t\t\tbubbles: true,\n\t\t\t\tcomposed: true,\n\t\t\t\tdetail: { value: this.counter }\n\t\t\t})\n\t\t);\n\t}\n\n\tgetValue() {\n\t\treturn this.counter;\n\t}\n\n\tsetValue(value) {\n\t\tconst parsedValue = parseInt(value, 10) || 0;\n\t\tthis.counter = parsedValue &gt; this.MAX_VALUE ? this.MAX_VALUE : parsedValue;\n\t\tthis.updateDisplay();\n\t\tthis.checkMaxValueReached();\n\t}\n}\n\ncustomElements.define(&quot;wasm-component&quot;, WasmComponent);\n\ndocument.addEventListener(&quot;DOMContentLoaded&quot;, initDarkMode);\n\ndocument.getElementById(&quot;external-set&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\tconst component = document.querySelector(&quot;wasm-component&quot;);\n\tcomponent.setValue(42);\n});\n\ndocument.getElementById(&quot;external-get&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\tconst component = document.querySelector(&quot;wasm-component&quot;);\n\talert(`Current value: ${component.getValue()}`);\n});\n\ndocument\n\t.querySelector(&quot;wasm-component&quot;)\n\t.addEventListener(&quot;value-changed&quot;, (e) =&gt; {\n\t\tconsole.log(&quot;Value changed:&quot;, e.detail.value);\n\t});\n\ndocument\n\t.querySelector(&quot;wasm-component&quot;)\n\t.addEventListener(&quot;wasm-loaded&quot;, (e) =&gt; {\n\t\tconsole.log(&quot;WASM module loaded successfully&quot;);\n\t});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/the-future-of-web-components/\">the original article</a>.</p></figure>\n<h2 id=\"challenges-that-remain\">Challenges That Remain<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#challenges-that-remain\" aria-label=\"Link to this section\">#</a></h2>\n<ul>\n<li><strong>Frameworks.</strong> React passes unknown props to custom elements as attributes in version 18 and earlier (checked with 18.3.1 in <a href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/\">the post on frameworks</a>). React 19 changed this and is not covered here.</li>\n<li><strong>Accessibility.</strong> Custom elements do not get built-in semantics. <code>ElementInternals.role</code> and the ARIA properties let a component declare its role; <code>internals.role = 'button'</code> worked in Chrome 156. A property named <code>accessibleRole</code> does not exist.</li>\n<li><strong>Learning.</strong> Documentation and examples spread across libraries remain the main barrier for newcomers.</li>\n</ul>\n<h2 id=\"conclusion\">Conclusion<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#conclusion\" aria-label=\"Link to this section\">#</a></h2>\n<p>You can now tell which Web Component features are safe to use and which are not. Use <code>::part()</code>, <code>:state()</code>, Declarative Shadow DOM (<code>shadowrootmode</code>), manual slotting, constructable stylesheets and custom events today. Check <code>@scope</code> against your oldest supported browser, and treat scoped registries as a feature for Chrome, Edge and Safari until Firefox ships it. When an article names a method you have not seen, search for it on MDN or run it in your console before building on it, as <code>attachSlot</code> shows.</p>\n<p>For a next step, build a small component that uses one of the \"ships today\" rows from the table above. To follow standards work, the <a href=\"https://www.w3.org/community/webcomponents/\">W3C Web Components Community Group</a> publishes its discussions.</p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/the-future-of-web-components/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "The Future of Web Components\nCanonical: https://mnixblog.netlify.app/the-future-of-web-components/\nWhere Innovation Meets Standardization\nWhich Web Component features can you use today, and which are still ideas? This post sorts them, with a status table you can check against your own browser.\nSome features are ready now, some work in only some browsers, and a few are only proposals. Knowing which is which keeps us from building on something that does not exist.\nPrerequisites in two lines: a custom element is a JavaScript class registered with customElements.define(), and a shadow root gives it private markup and styles. Everything below extends those two ideas.\nWhat ships today and what does not\n\"Works\" in the second column means a small script ran in headless Chrome 156 while editing this post. Firefox and Safari were not tested by us. The Status column was checked against webstatus.dev (https://webstatus.dev/) and MDN on 26 October 2026, so treat it as a snapshot and look again before you rely on a row.\nFeature\nIn Chrome 156\nStatus, Oct 2026\n::part(): style what a component exposes\nWorks\nShips today\n:state(): style a component by its own state\nWorks\nBaseline newly available\nDeclarative Shadow DOM: shadowrootmode\nWorks\nBaseline widely available\nManual slot assignment: slot.assign()\nWorks\nBaseline widely available\n@scope: limit where a CSS rule applies\nWorks\nBaseline newly available\nScoped registries\nWorks\nChrome, Edge, Safari only\nattachSlot()\nundefined\nNot an API\naccessibleRole property\nNot present\nNot an API\nDeclarative custom elements\nNot testable\nProposal\nThe dates behind the Status column, from webstatus.dev and MDN on 26 October 2026:\n:state(): newly available since May 2024 (Chrome 125, Firefox 126, Safari 17.4).\nDeclarative Shadow DOM: newly available in February 2024 (Chrome 111, Firefox 123, Safari 16.4) and widely available since August 2026. The older shadowroot attribute no longer works.\nManual slot assignment: widely available in all three engines since March 2023.\n@scope: newly available since March 2026.\nScoped registries: Chrome and Edge 146, Safari 26. Firefox has not shipped it. It is in Nightly, and Mozilla has proposed shipping it in Firefox 159, so check before you rely on it.\n::part() was not re-checked for this update.\nThe Rising Tide: Emerging Trends\nThe rest of this post covers libraries, state and a few patterns. Most of it is built on the standards above, not added to the browser, and none of it is needed to write a custom element.\nWeb Component Libraries\nYou do not need a library to write a custom element, and every example above works without one. A library saves boilerplate for the parts you end up writing again and again.\nLit, from Google, is a small base class that adds reactive properties and templates on top of custom elements. Stencil is a compiler: you write TypeScript and JSX and it produces standards-based custom elements. FAST and Shoelace are the other two names that come up most. Each project changes its name, its version and its advice over time, so check its own site for its current state before you pick one. We did not re-check them for this update.\nThe cost is the usual one. A library is one more thing to learn and to keep up to date, and shadow DOM still makes global styling harder whichever one you choose.\nIs State Management Going Native?: Beyond Properties and Attributes\nThe browser gives you less state management than a framework does, and that is fine. The part it does give you is :state(): a component can expose states such as \"loading\" or \"expanded\" to CSS without adding classes the page could overwrite. It has been Baseline newly available since May 2024 (Chrome 125, Firefox 126, Safari 17.4). Stores, signals and reducers are libraries and patterns, not platform features, and the examples below are patterns you can write yourself.\nHTML\n<user-profile></user-profile>\nCSS\nbody {\nmargin: 0;\npadding: 20px;\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;\ndisplay: flex;\njustify-content: center;\nalign-items: center;\nmin-height: 100vh;\ntransition: background-color 0.3s ease, color 0.3s ease;\n}\n@media (prefers-color-scheme: dark) {\nbody {\nbackground-color: #121212;\ncolor: #f0f0f0;\n}\n}\n@media (prefers-color-scheme: light) {\nbody {\nbackground-color: #f8f9fa;\ncolor: #333;\n}\n}\nJavaScript\n// Simple state management pattern for Web Components\nclass UserProfile extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\n// Initialize state\nthis.state = {\nname: 'Guest User',\nrole: 'Visitor',\nisLoggedIn: false,\ndarkMode: window.matchMedia('(prefers-color-scheme: dark)').matches };\n// Listen for changes in color scheme preference\nthis.colorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)');\nthis.colorSchemeMedia.addEventListener('change', e => {\nthis.setState({ darkMode: e.matches });\n});\nthis.render();\n}\n// State management method\nsetState(newState) {\nthis.state = { ...this.state, ...newState };\nthis.render();\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: system-ui, sans-serif;\nmax-width: 300px;\npadding: 20px;\nborder: 1px solid ${this.state.darkMode ? '#555' : '#ddd'};\nborder-radius: 8px;\nbackground-color: ${this.state.darkMode ? '#222' : '#fff'};\ncolor: ${this.state.darkMode ? '#eee' : '#333'};\n}\n.profile {\ndisplay: flex;\nflex-direction: column;\nalign-items: center;\n}\n.avatar {\nwidth: 80px;\nheight: 80px;\nborder-radius: 50%;\nbackground-color: ${this.state.isLoggedIn ? '#4CAF50' : this.state.darkMode ? '#555' : '#ccc'};\ndisplay: flex;\nalign-items: center;\njustify-content: center;\nmargin-bottom: 16px;\ncolor: white;\nfont-size: 32px;\n}\n.name {\nfont-size: 20px;\nfont-weight: bold;\nmargin-bottom: 4px;\n}\n.role {\ncolor: ${this.state.darkMode ? '#aaa' : '#666'};\nmargin-bottom: 16px;\n}\nbutton {\nbackground-color: ${this.state.isLoggedIn ? '#f44336' : '#4CAF50'};\ncolor: white;\nborder: none;\npadding: 8px 16px;\nborder-radius: 4px;\ncursor: pointer;\n}\nbutton:hover {\nopacity: 0.9;\n}\n</style>\n<div class=\"profile\">\n<div class=\"avatar\">${this.state.name.charAt(0)}</div>\n<div class=\"name\">${this.state.name}</div>\n<div class=\"role\">${this.state.role}</div>\n<button id=\"toggle-login\">\n${this.state.isLoggedIn ? 'Logout' : 'Login'}\n</button>\n</div>\n`;\n// Add event listeners\nthis.shadowRoot.querySelector('#toggle-login').addEventListener('click', () => {\nif (this.state.isLoggedIn) {\nthis.setState({\nname: 'Guest User',\nrole: 'Visitor',\nisLoggedIn: false });\n} else {\nthis.setState({\nname: 'Jane Doe',\nrole: 'Administrator',\nisLoggedIn: true });\n}\n});\n}\ndisconnectedCallback() {\n// Clean up event listener when component is removed\nif (this.colorSchemeMedia) {\nthis.colorSchemeMedia.removeEventListener('change', this.handleColorSchemeChange);\n}\n}}\ncustomElements.define('user-profile', UserProfile);\nState Management in Web Components on the canonical article (https://mnixblog.netlify.app/the-future-of-web-components/).\nContext: Sharing State Without Tight Coupling\nA context-style provider is a pattern, not a platform feature. A parent component holds a value, and the components inside it ask for that value by dispatching an event, so the provider and its consumers do not need to know about each other. A ThemeProvider is the usual example, and the one below is small enough to read in a minute. For plain style values, CSS custom properties are still simpler.\nHTML\n<theme-provider>\n<theme-consumer></theme-consumer>\n<theme-toggler></theme-toggler>\n<theme-controller></theme-controller>\n</theme-provider>\nJavaScript\n// Context-like pattern for Web Components\nclass ThemeContext {\nconstructor() {\n// Check for user preference\nconst prefersDarkMode =\nwindow.matchMedia &&\nwindow.matchMedia(\"(prefers-color-scheme: dark)\").matches;\nthis.theme = prefersDarkMode ? \"dark\" : \"light\";\nthis.accentColor = \"#2196F3\";\nthis.fontSize = \"medium\";\nthis.borderRadius = \"4px\";\nthis.listeners = new Set();\n// Track the last changed setting to help with animations\nthis.lastChanged = null;\n// Listen for system theme changes\nthis.mediaQuery = window.matchMedia(\"(prefers-color-scheme: dark)\");\nthis.handleSystemThemeChange = e => {\nthis.setTheme(e.matches ? \"dark\" : \"light\");\n};\n// Add listener for system theme changes if supported\nif (this.mediaQuery.addEventListener) {\nthis.mediaQuery.addEventListener(\"change\", this.handleSystemThemeChange);\n}\n// Apply initial theme to body/html/root\nthis.applyThemeToDocument(this.theme);\n}\ngetTheme() {\nreturn this.theme;\n}\nsetTheme(newTheme) {\nthis.theme = newTheme;\nthis.lastChanged = \"theme\";\nthis.applyThemeToDocument(newTheme);\nthis.notifyListeners();\n}\napplyThemeToDocument(theme) {\n// Set theme on html and body elements\ndocument.documentElement.style.backgroundColor =\ntheme === \"light\" ? \"#f5f5f5\" : \"#121212\";\ndocument.documentElement.style.transition = \"background-color 0.3s\";\ndocument.body.style.backgroundColor =\ntheme === \"light\" ? \"#f5f5f5\" : \"#121212\";\ndocument.body.style.color = theme === \"light\" ? \"#333333\" : \"#ffffff\";\ndocument.body.style.transition = \"background-color 0.3s, color 0.3s\";\n// Add/remove class on document root for additional styling hooks\nif (theme === \"dark\") {\ndocument.documentElement.classList.add(\"dark-theme\");\n} else {\ndocument.documentElement.classList.remove(\"dark-theme\");\n}\n}\ngetAccentColor() {\nreturn this.accentColor;\n}\nsetAccentColor(newColor) {\nthis.accentColor = newColor;\nthis.lastChanged = \"accentColor\";\nthis.notifyListeners();\n}\ngetFontSize() {\nreturn this.fontSize;\n}\nsetFontSize(newSize) {\nthis.fontSize = newSize;\nthis.lastChanged = \"fontSize\";\nthis.notifyListeners();\n}\ngetBorderRadius() {\nreturn this.borderRadius;\n}\nsetBorderRadius(newRadius) {\nthis.borderRadius = newRadius;\nthis.lastChanged = \"borderRadius\";\nthis.notifyListeners();\n}\ngetLastChanged() {\nreturn this.lastChanged;\n}\n// Get the current theme as a complete object\ngetCurrentThemeSettings() {\nreturn {\ntheme: this.theme,\naccentColor: this.accentColor,\nfontSize: this.fontSize,\nborderRadius: this.borderRadius };\n}\nsubscribe(listener) {\nthis.listeners.add(listener);\n}\nunsubscribe(listener) {\nthis.listeners.delete(listener);\n}\nnotifyListeners() {\nthis.listeners.forEach((listener) =>\nlistener(this.getCurrentThemeSettings()));\n}\ndisconnectMediaQuery() {\n// Clean up media query listener if it exists\nif (this.mediaQuery && this.mediaQuery.removeEventListener) {\nthis.mediaQuery.removeEventListener(\"change\", this.handleSystemThemeChange);\n}\n}}\n// Global context instance\nconst themeContext = new ThemeContext();\n// Add window unload handler to clean up listeners\nwindow.addEventListener(\"unload\", () => {\nthemeContext.disconnectMediaQuery();\n});\n// Provider component\nclass ThemeProvider extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: \"open\" });\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: system-ui, sans-serif;\npadding: 20px;\nbackground-color: ${\nthemeContext.getTheme() === \"light\" ? \"#ffffff\" : \"#333333\"\n};\ncolor: ${themeContext.getTheme() === \"light\" ? \"#333333\" : \"#ffffff\"};\nborder-radius: 8px;\ntransition: background-color 0.3s, color 0.3s;\n}\n.container {\nmax-width: 600px;\nmargin: 0 auto;\n}\n</style>\n<div class=\"container\">\n<h2>Theme Context Demo</h2>\n<slot></slot>\n</div>\n`;\n// Update when theme changes\nthis.updateTheme = settings => {\nconst theme = settings.theme;\nconst styleElement = this.shadowRoot.querySelector(\"style\");\n// Update the style element with new theme colors\nstyleElement.textContent = `\n:host {\ndisplay: block;\nfont-family: system-ui, sans-serif;\npadding: 20px;\nbackground-color: ${theme === \"light\" ? \"#ffffff\" : \"#333333\"};\ncolor: ${theme === \"light\" ? \"#333333\" : \"#ffffff\"};\nborder-radius: 8px;\ntransition: background-color 0.3s, color 0.3s;\n}\n.container {\nmax-width: 600px;\nmargin: 0 auto;\n}\n`;\n};\nthemeContext.subscribe(this.updateTheme);\n}\ndisconnectedCallback() {\nthemeContext.unsubscribe(this.updateTheme);\n}}\n// Consumer component\nclass ThemeConsumer extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: \"open\" });\nthis.render();\n// Update when theme changes\nthis.updateTheme = settings => {\n// Store which value changed to highlight it for the user\nthis.lastChanged = themeContext.getLastChanged();\nthis.render();\n// Reset highlight after animation completes\nsetTimeout(() => {\nthis.lastChanged = null;\nthis.render();\n}, 1500);\n};\nthemeContext.subscribe(this.updateTheme);\n}\ndisconnectedCallback() {\nthemeContext.unsubscribe(this.updateTheme);\n}\nrender() {\nconst settings = themeContext.getCurrentThemeSettings();\nconst theme = settings.theme;\nconst accentColor = settings.accentColor;\nconst fontSize = settings.fontSize;\nconst borderRadius = settings.borderRadius;\nconst lastChanged = this.lastChanged;\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nmargin: 16px 0;\npadding: 16px;\nborder-radius: ${borderRadius};\nborder: 1px solid ${theme === \"light\" ? \"#ddd\" : \"#555\"};\nbackground-color: ${theme === \"light\" ? \"#f9f9f9\" : \"#444\"};\ncolor: ${theme === \"light\" ? \"#333\" : \"#eee\"};\ntransition: all 0.3s;\n}\nh3 {\nmargin-top: 0;\nmargin-bottom: 12px;\ncolor: ${accentColor};\nfont-size: ${\nfontSize === \"small\" ?\n\"16px\" :\nfontSize === \"medium\" ?\n\"18px\" :\n\"20px\"\n};\ntransition: all 0.3s;\n}\n.theme-properties {\ndisplay: grid;\ngrid-template-columns: 120px 1fr;\ngrid-gap: 8px;\n}\n.property-name {\nfont-weight: bold;\nopacity: 0.8;\n}\n.property-value {\nfont-family: monospace;\npadding: 2px 6px;\nborder-radius: ${borderRadius};\nbackground-color: ${\ntheme === \"light\" ? \"rgba(0,0,0,0.05)\" : \"rgba(255,255,255,0.1)\"\n};\ntransition: all 0.3s;\n}\n.property-highlight {\nposition: relative;\nanimation: pulse 1.5s ease-in-out;\n}\n.property-highlight::before {\ncontent: \"\";\nposition: absolute;\ntop: 0;\nright: 0;\nbottom: 0;\nleft: 0;\nbackground-color: ${accentColor};\nopacity: 0.2;\nborder-radius: ${borderRadius};\nz-index: -1;\n}\n.color-preview {\ndisplay: inline-block;\nwidth: 14px;\nheight: 14px;\nborder-radius: 50%;\nmargin-right: 6px;\nvertical-align: middle;\n}\n.demo-text {\nmargin-top: 16px;\npadding: 12px;\nborder-radius: ${borderRadius};\nborder-left: 4px solid ${accentColor};\nfont-size: ${\nfontSize === \"small\" ?\n\"14px\" :\nfontSize === \"medium\" ?\n\"16px\" :\n\"18px\"\n};\nbackground-color: ${\ntheme === \"light\" ? \"rgba(0,0,0,0.02)\" : \"rgba(255,255,255,0.05)\"\n};\ntransition: all 0.3s;\n}\n@keyframes pulse {\n0% { transform: scale(1); }\n50% { transform: scale(1.03); }\n100% { transform: scale(1); }\n}\n</style>\n<div>\n<h3>Theme State Observer</h3>\n<div class=\"theme-properties\">\n<span class=\"property-name\">Theme:</span>\n<span class=\"property-value ${\nlastChanged === \"theme\" ? \"property-highlight\" : \"\"\n}\">\n${theme}\n</span>\n<span class=\"property-name\">Accent Color:</span>\n<span class=\"property-value ${\nlastChanged === \"accentColor\" ? \"property-highlight\" : \"\"\n}\">\n<span class=\"color-preview\" style=\"background-color: ${accentColor};\"></span>\n${accentColor}\n</span>\n<span class=\"property-name\">Font Size:</span>\n<span class=\"property-value ${\nlastChanged === \"fontSize\" ? \"property-highlight\" : \"\"\n}\">\n${fontSize}\n</span>\n<span class=\"property-name\">Border Radius:</span>\n<span class=\"property-value ${\nlastChanged === \"borderRadius\" ? \"property-highlight\" : \"\"\n}\">\n${borderRadius}\n</span>\n</div>\n<div class=\"demo-text\">\nThis text demonstrates the current theme settings. The border radius, font size,\nand colors are all controlled by the theme context.\n</div>\n</div>\n`;\n}}\n// Theme toggler component\nclass ThemeToggler extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: \"open\" });\n// Create bound event handler to avoid recreating it\nthis.handleToggle = this.handleToggle.bind(this);\n// Subscribe to theme changes\nthis.updateTheme = () => {\nthis.render();\n};\nthemeContext.subscribe(this.updateTheme);\nthis.render();\n}\ndisconnectedCallback() {\n// Clean up event listeners and subscriptions\nthemeContext.unsubscribe(this.updateTheme);\nthis.removeToggleListener();\n}\nremoveToggleListener() {\nconst button = this.shadowRoot.querySelector(\"#theme-toggle\");\nif (button) {\nbutton.removeEventListener(\"click\", this.handleToggle);\n}\n}\nhandleToggle() {\nconst currentTheme = themeContext.getTheme();\nthemeContext.setTheme(currentTheme === \"light\" ? \"dark\" : \"light\");\n}\nrender() {\n// Clean up previous event listeners before re-rendering\nthis.removeToggleListener();\nconst theme = themeContext.getTheme();\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nmargin: 16px 0;\npadding: 16px;\nborder-radius: 4px;\nborder: 1px solid ${theme === \"light\" ? \"#ddd\" : \"#555\"};\nbackground-color: ${theme === \"light\" ? \"#f9f9f9\" : \"#444\"};\ntransition: background-color 0.3s, border-color 0.3s;\n}\nbutton {\nbackground-color: ${theme === \"light\" ? \"#4CAF50\" : \"#66BB6A\"};\ncolor: white;\nborder: none;\npadding: 8px 16px;\nborder-radius: 4px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.3s;\n}\nbutton:hover {\nbackground-color: ${theme === \"light\" ? \"#3e8e41\" : \"#4CAF50\"};\nbox-shadow: 0 2px 4px rgba(0,0,0,0.2);\n}\n</style>\n<div>\n<p>Click the button to toggle the theme:</p>\n<button id=\"theme-toggle\">\nSwitch to ${theme === \"light\" ? \"Dark\" : \"Light\"} Theme\n</button>\n</div>\n`;\n// Add event listener\nthis.shadowRoot.\nquerySelector(\"#theme-toggle\").\naddEventListener(\"click\", this.handleToggle);\n}}\n// Theme controller component\nclass ThemeController extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: \"open\" });\n// Get current theme settings\nthis.theme = themeContext.getTheme();\nthis.accentColor = themeContext.getAccentColor();\nthis.fontSize = themeContext.getFontSize();\nthis.borderRadius = themeContext.getBorderRadius();\n// Define preset accent colors\nthis.accentColors = [\n\"#2196F3\", // Blue (default)\n\"#F44336\", // Red\n\"#4CAF50\", // Green\n\"#FF9800\", // Orange\n\"#9C27B0\", // Purple\n\"#00BCD4\", // Cyan\n\"#607D8B\" // Blue Grey\n];\n// Define preset font sizes\nthis.fontSizes = [\"small\", \"medium\", \"large\"];\n// Define preset border radius values\nthis.borderRadiusOptions = [\"0px\", \"4px\", \"8px\", \"12px\", \"16px\"];\nthis.render();\n// Update when theme changes\nthis.updateTheme = settings => {\nthis.theme = settings.theme;\nthis.accentColor = settings.accentColor;\nthis.fontSize = settings.fontSize;\nthis.borderRadius = settings.borderRadius;\nthis.render();\n};\nthemeContext.subscribe(this.updateTheme);\n}\ndisconnectedCallback() {\nthemeContext.unsubscribe(this.updateTheme);\n}\naddEventListeners() {\n// Theme toggle\nconst themeToggle = this.shadowRoot.getElementById(\"theme-toggle\");\nthemeToggle.addEventListener(\"click\", () => {\nthemeContext.setTheme(this.theme === \"light\" ? \"dark\" : \"light\");\n});\n// Accent color selection\nconst colorButtons = this.shadowRoot.querySelectorAll(\".color-option\");\ncolorButtons.forEach(button => {\nbutton.addEventListener(\"click\", () => {\nconst color = button.getAttribute(\"data-color\");\nthemeContext.setAccentColor(color);\n});\n});\n// Font size selection\nconst fontSizeSelect = this.shadowRoot.getElementById(\"font-size-select\");\nfontSizeSelect.value = this.fontSize;\nfontSizeSelect.addEventListener(\"change\", () => {\nthemeContext.setFontSize(fontSizeSelect.value);\n});\n// Border radius slider\nconst radiusSlider = this.shadowRoot.getElementById(\"radius-slider\");\nconst radiusValue = this.shadowRoot.getElementById(\"radius-value\");\n// Set initial value\nconst currentRadiusIndex = this.borderRadiusOptions.indexOf(\nthis.borderRadius);\nradiusSlider.value = currentRadiusIndex >= 0 ? currentRadiusIndex : 1;\nradiusValue.textContent = this.borderRadius;\nradiusSlider.addEventListener(\"input\", () => {\nconst newRadius = this.borderRadiusOptions[radiusSlider.value];\nradiusValue.textContent = newRadius;\nthemeContext.setBorderRadius(newRadius);\n});\n}\nrender() {\nconst theme = this.theme;\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nmargin: 16px 0;\npadding: 16px;\nborder-radius: ${this.borderRadius};\nborder: 1px solid ${theme === \"light\" ? \"#ddd\" : \"#555\"};\nbackground-color: ${theme === \"light\" ? \"#f9f9f9\" : \"#444\"};\ncolor: ${theme === \"light\" ? \"#333\" : \"#eee\"};\ntransition: all 0.3s;\nfont-family: system-ui, sans-serif;\n}\nh3 {\nmargin-top: 0;\ncolor: ${this.accentColor};\ntransition: color 0.3s;\n}\n.control-group {\nmargin-bottom: 16px;\n}\n.control-group label {\ndisplay: block;\nmargin-bottom: 8px;\nfont-weight: bold;\n}\n.theme-toggle {\ncursor: pointer;\nbackground-color: ${theme === \"light\" ? \"#333\" : \"#f5f5f5\"};\ncolor: ${theme === \"light\" ? \"#fff\" : \"#333\"};\nborder: none;\nborder-radius: ${this.borderRadius};\npadding: 8px 16px;\nfont-size: 14px;\ndisplay: flex;\nalign-items: center;\njustify-content: space-between;\nwidth: 140px;\ntransition: background-color 0.3s, color 0.3s;\n}\n.theme-toggle .icon {\ndisplay: inline-block;\nwidth: 16px;\nheight: 16px;\nborder-radius: 50%;\nbackground-color: ${theme === \"light\" ? \"#f5f5f5\" : \"#333\"};\nmargin-left: 8px;\nposition: relative;\n}\n.theme-toggle .icon::before {\ncontent: '';\nposition: absolute;\ntop: 3px;\nleft: ${theme === \"light\" ? \"3px\" : \"12px\"};\nwidth: 10px;\nheight: 10px;\nborder-radius: 50%;\nbackground-color: ${theme === \"light\" ? \"#333\" : \"#f5f5f5\"};\ntransition: left 0.3s;\n}\n.color-options {\ndisplay: flex;\ngap: 8px;\nflex-wrap: wrap;\n}\n.color-option {\nwidth: 32px;\nheight: 32px;\nborder-radius: 50%;\ncursor: pointer;\nposition: relative;\nborder: 2px solid ${theme === \"light\" ? \"#ddd\" : \"#555\"};\ntransition: transform 0.2s;\n}\n.color-option:hover {\ntransform: scale(1.1);\n}\n.color-option.active {\nborder: 2px solid ${theme === \"light\" ? \"#333\" : \"#fff\"};\n}\n.color-option.active::after {\ncontent: \"✓\";\nposition: absolute;\ntop: 50%;\nleft: 50%;\ntransform: translate(-50%, -50%);\ncolor: ${this.isDarkColor(this.accentColor) ? \"#fff\" : \"#333\"};\nfont-size: 18px;\nfont-weight: bold;\n}\nselect, input[type=\"range\"] {\nwidth: 100%;\npadding: 6px;\nborder-radius: ${this.borderRadius};\nborder: 1px solid ${theme === \"light\" ? \"#ddd\" : \"#555\"};\nbackground-color: ${theme === \"light\" ? \"#fff\" : \"#333\"};\ncolor: ${theme === \"light\" ? \"#333\" : \"#fff\"};\n}\n.slider-container {\ndisplay: flex;\nalign-items: center;\ngap: 12px;\n}\n.slider-container input {\nflex: 1;\n}\n.value-display {\nmin-width: 40px;\ntext-align: center;\nfont-family: monospace;\npadding: 4px 8px;\nbackground-color: ${\ntheme === \"light\" ? \"rgba(0,0,0,0.05)\" : \"rgba(255,255,255,0.1)\"\n};\nborder-radius: ${this.borderRadius};\n}\n</style>\n<div>\n<h3>Theme Controller</h3>\n<div class=\"control-group\">\n<label>Theme Mode</label>\n<button id=\"theme-toggle\" class=\"theme-toggle\">\n${theme === \"light\" ? \"Light Mode\" : \"Dark Mode\"}\n<span class=\"icon\"></span>\n</button>\n</div>\n<div class=\"control-group\">\n<label>Accent Color</label>\n<div class=\"color-options\">\n${this.accentColors.\nmap(\ncolor => `\n<div\nclass=\"color-option ${\ncolor === this.accentColor ? \"active\" : \"\"\n}\"\nstyle=\"background-color: ${color};\"\ndata-color=\"${color}\">\n</div>\n`).\njoin(\"\")}\n</div>\n</div>\n<div class=\"control-group\">\n<label>Font Size</label>\n<select id=\"font-size-select\">\n${this.fontSizes.\nmap(\nsize => `\n<option value=\"${size}\" ${\nthis.fontSize === size ? \"selected\" : \"\"\n}>${size.charAt(0).toUpperCase() + size.slice(1)}</option>\n`).\njoin(\"\")}\n</select>\n</div>\n<div class=\"control-group\">\n<label>Border Radius</label>\n<div class=\"slider-container\">\n<input\nid=\"radius-slider\"\ntype=\"range\"\nmin=\"0\"\nmax=\"${this.borderRadiusOptions.length - 1}\"\nvalue=\"${this.borderRadiusOptions.indexOf(this.borderRadius)}\"\n/>\n<div id=\"radius-value\" class=\"value-display\">${\nthis.borderRadius\n}</div>\n</div>\n</div>\n</div>\n`;\nthis.addEventListeners();\n}\n// Helper to determine if a color is dark\nisDarkColor(color) {\n// Convert hex to RGB\nconst hex = color.replace(\"#\", \"\");\nconst r = parseInt(hex.substr(0, 2), 16);\nconst g = parseInt(hex.substr(2, 2), 16);\nconst b = parseInt(hex.substr(4, 2), 16);\n// Calculate brightness\nconst brightness = (r * 299 + g * 587 + b * 114) / 1000;\nreturn brightness < 128;\n}}\n// Register the theme controller component\ncustomElements.define(\"theme-controller\", ThemeController);\n// Register the components\ncustomElements.define(\"theme-provider\", ThemeProvider);\ncustomElements.define(\"theme-consumer\", ThemeConsumer);\ncustomElements.define(\"theme-toggler\", ThemeToggler);\nAdvanced State Management with Context on the canonical article (https://mnixblog.netlify.app/the-future-of-web-components/).\nDeclarative Shadow DOM: Server-Side Renaissance\nOne of the most significant limitations of Web Components has long been server-side rendering capabilities. The Declarative Shadow DOM (DSD) is changing this landscape, enabling Web Components to be rendered on the server before reaching the browser.\nEnhance.dev is built around this approach. Here is the syntax. Note the attribute is shadowrootmode; the older shadowroot spelling from early drafts no longer creates a shadow root (checked in Chrome 156):\n<my-card>\n<template shadowrootmode=\"open\">\n<style>:host { display: block; }</style>\n<slot name=\"title\"></slot>\n</template>\n<h1 slot=\"title\">Hello from the server!</h1>\n</my-card>\nThe browser builds the shadow tree while parsing the HTML, so the component paints before any JavaScript runs.\nCheck out this interactive demo to see this pattern in action:\nHTML\n<my-card counter=\"1\">\n<template shadowrootmode=\"open\">\n<style>\n:host {\ndisplay: block;\nborder: 1px solid var(--border-color, #ccc);\nborder-radius: 8px;\npadding: 16px;\nfont-family: system-ui, sans-serif;\nbackground-color: var(--background-color, #fff);\ncolor: var(--text-color, #333);\n}\n.card-header {\nborder-bottom: 1px solid var(--divider-color, #eee);\npadding-bottom: 10px;\nmargin-bottom: 10px;\n}\n::slotted(h1) {\nmargin-top: 0;\ncolor: var(--text-color, #333);\n}\n.counter {\nmargin-top: 15px;\npadding-top: 15px;\nborder-top: 1px solid var(--divider-color, #eee);\n}\nbutton {\nbackground-color: var(--background-color);\ncolor: var(--text-color);\nborder: 1px solid var(--border-color);\npadding: 5px 10px;\nmargin-right: 10px;\nborder-radius: 4px;\n}\n</style>\n<div class=\"card-header\">\n<slot name=\"title\">Default Title</slot>\n</div>\n<div class=\"card-body\">\n<slot>Default content</slot>\n</div>\n<div class=\"counter\">\n<p>Count: <span id=\"count\">0</span></p>\n<button id=\"decrement\">-</button>\n<button id=\"increment\">+</button>\n</div>\n</template>\n<h1 slot=\"title\">Hello from Declarative Shadow DOM</h1>\n<p>\nThis card demonstrates Declarative Shadow DOM for server-side rendering.\n</p>\n</my-card>\n<div class=\"explanation\">\n<h2>Benefits of Declarative Shadow DOM</h2>\n<p>\nDeclarative Shadow DOM provides significant advantages over imperative shadow DOM creation:\n</p>\n<ul>\n<li><strong>Server-Side Rendering:</strong> Components with encapsulated styling can be rendered on the server, improving initial page load performance</li>\n<li><strong>Zero Hydration Cost:</strong> No JavaScript is required to initialize the shadow DOM, making components immediately interactive</li>\n<li><strong>Progressive Enhancement:</strong> Components work even before JavaScript loads or if JavaScript fails</li>\n<li><strong>SEO Friendly:</strong> Content is available in the initial HTML, making it more accessible to search engines</li>\n<li><strong>Simplified Component Definition:</strong> No need for separate template files or string literals - the component structure is defined directly in HTML</li>\n</ul>\n<p>\nThe component above demonstrates these benefits while also implementing dark mode support,\nshowing how declarative shadow DOM can be both highly performant and user-friendly.\n</p>\n</div>\n<h2 class=\"code-header\">Declarative Shadow DOM Code:</h2>\n<pre><code>&lt;my-card counter=\"1\" &gt;\n&lt;template shadowrootmode=\"open\"&gt;\n&lt;style&gt;\n:host {\ndisplay: block;\nborder: 1px solid var(--border-color, #ccc);\nborder-radius: 8px;\npadding: 16px;\nfont-family: system-ui, sans-serif;\nbackground-color: var(--background-color, #fff);\ncolor: var(--text-color, #333);\n}\n.card-header {\nborder-bottom: 1px solid var(--divider-color, #eee);\npadding-bottom: 10px;\nmargin-bottom: 10px;\n}\n::slotted(h1) {\nmargin-top: 0;\ncolor: var(--text-color, #333);\n}\n.counter {\nmargin-top: 15px;\npadding-top: 15px;\nborder-top: 1px solid var(--divider-color, #eee);\n}\nbutton {\nbackground-color: var(--background-color);\ncolor: var(--text-color);\nborder: 1px solid var(--border-color);\npadding: 5px 10px;\nmargin-right: 10px;\nborder-radius: 4px;\n}\n&lt;/style&gt;\n&lt;div class=\"card-header\"&gt;\n&lt;slot name=\"title\"&gt;Default Title&lt;/slot&gt;\n&lt;/div&gt;\n&lt;div class=\"card-body\"&gt;\n&lt;slot&gt;Default content&lt;/slot&gt;\n&lt;/div&gt;\n&lt;div class=\"counter\"&gt;\n&lt;p&gt;Count: &lt;span id=\"count\"&gt;0&lt;/span&gt;&lt;/p&gt;\n&lt;button id=\"decrement\"&gt;-&lt;/button&gt;\n&lt;button id=\"increment\"&gt;+&lt;/button&gt;\n&lt;/div&gt;\n&lt;/template&gt;\n&lt;h1 slot=\"title\"&gt;Hello from Declarative Shadow DOM&lt;/h1&gt;\n&lt;p&gt;\nThis card demonstrates Declarative Shadow DOM for server-side rendering.\n&lt;/p&gt;\n&lt;/my-card&gt;</code></pre>\n<div class=\"explanation\">\n<h2>Adding JavaScript Logic with Declarative Shadow DOM</h2>\n<p>\nWhile declarative shadow DOM provides the HTML structure and styling, you can still add JavaScript logic through custom elements:\n</p>\n<ul>\n<li><strong>Progressive Enhancement:</strong> The component renders with its structure and styling before JavaScript loads</li>\n<li><strong>Logic Attachment:</strong> JavaScript enhances the pre-rendered component with behavior after loading</li>\n<li><strong>Works either way:</strong> The custom element class works with both declaratively and imperatively created shadow DOM</li>\n<li><strong>Attribute Reactivity:</strong> Using attributes and the built-in lifecycle callbacks enables state management</li>\n</ul>\n<p>\nThis approach gives you the best of both worlds: instant rendering with declarative shadow DOM,\nplus fully interactive behavior once JavaScript executes.\n</p>\n</div>\nCSS\n:root {\ncolor-scheme: light dark;\n--background-color: #ffffff;\n--text-color: #333333;\n--border-color: #cccccc;\n--divider-color: #eeeeee;\n}\n@media (prefers-color-scheme: dark) {\n:root {\n--background-color: #222222;\n--text-color: #f0f0f0;\n--border-color: #555555;\n--divider-color: #444444;\n}\n}\nbody {\nbackground-color: var(--background-color);\ncolor: var(--text-color);\nmargin: 20px;\n}\npre {\nbackground-color: var(--background-color);\nborder: 1px solid var(--border-color);\nborder-radius: 4px;\npadding: 15px;\noverflow: auto;\nmargin-top: 30px;\n}\ncode {\nfont-family: monospace;\ncolor: var(--text-color);\nwhite-space: pre;\n}\n.code-header {\nfont-weight: bold;\nmargin-bottom: 10px;\nmargin-top: 30px;\ncolor: var(--text-color);\n}\nJavaScript\n// Check and polyfill if necessary\nif (!HTMLTemplateElement.prototype.hasOwnProperty(\"shadowRootMode\")) {\ndocument.\nquerySelectorAll(\"template[shadowrootmode]\").\nforEach(template => {\nconst mode = template.getAttribute(\"shadowrootmode\");\nconst shadowRoot = template.parentNode.attachShadow({ mode });\nshadowRoot.appendChild(template.content);\ntemplate.remove();\n});\n}\n// Register the element with behavior\ncustomElements.define(\"my-card\", class extends HTMLElement {\nstatic get observedAttributes() {\nreturn ['counter'];\n}\nconstructor() {\nsuper();\n// Get access to shadow DOM elements (works regardless of how shadow DOM was created)\nthis._setupShadowDOM();\n}\n_setupShadowDOM() {\n// If the shadow root already exists (from declarative shadow DOM)\n// we can use it directly, otherwise it would have been created by the polyfill\nconst shadowRoot = this.shadowRoot;\n// Get references to elements in the shadow DOM\nthis._countDisplay = shadowRoot.querySelector('#count');\nthis._incrementBtn = shadowRoot.querySelector('#increment');\nthis._decrementBtn = shadowRoot.querySelector('#decrement');\n// Add event listeners\nthis._incrementBtn.addEventListener('click', () => this._incrementCount());\nthis._decrementBtn.addEventListener('click', () => this._decrementCount());\n// Initialize with attribute value\nthis._updateCount(parseInt(this.getAttribute('counter') || '0'));\n}\nconnectedCallback() {\n// Make sure the count is updated with the latest attribute value\n// This handles the case where attributes might have changed before connection\nthis._updateCount(parseInt(this.getAttribute('counter') || '0'));\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === 'counter' && this._countDisplay) {\nthis._updateCount(parseInt(newValue || '0'));\n}\n}\n_incrementCount() {\nconst current = parseInt(this.getAttribute('counter') || '0');\nthis.setAttribute('counter', current + 1);\n}\n_decrementCount() {\nconst current = parseInt(this.getAttribute('counter') || '0');\nthis.setAttribute('counter', Math.max(0, current - 1));\n}\n_updateCount(value) {\nif (this._countDisplay) {\nthis._countDisplay.textContent = value;\n}\n}});\nDeclarative Shadow Dom on the canonical article (https://mnixblog.netlify.app/the-future-of-web-components/).\nState Management Evolves\nState management in complex Web Components can quickly become unwieldy. The community has answered with a few small patterns, shown below.\nSignals libraries offer a React-inspired state model without the framework dependencies, while Lit State provides reactive state management specifically tailored to component systems. Most exciting is the Custom State Pseudo-Class, which now ships in browsers and lets you style components based on their internal state. A component adds a state with this.attachInternals().states.add('loading'), and CSS matches it with :state():\nmy-element:state(loading) {\nopacity: 0.5;\n}\nThese are patterns you can write today. None of them is a browser feature.\nHere is another interactive example that uses ElementInternals.states and the :state() pseudo-class, styling the component from the page and from inside its own shadow root.\nHTML\n<state-component></state-component>\nCSS\nbody {\nfont-family: system-ui, sans-serif;\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\ntransition: background-color 0.3s, color 0.3s;\n}\n@media (prefers-color-scheme: dark) {\nbody {\nbackground-color: #121212;\ncolor: #e0e0e0;\n}\n}\n/* Custom states are styled from outside with :state(), with no attributes involved */\nstate-component:state(loading) {\nopacity: 0.5;\npointer-events: none;\n}\nstate-component:state(error) {\noutline: 3px solid #f44336;\noutline-offset: 4px;\nborder-radius: 8px;\n}\nJavaScript\nclass StateComponent extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: \"open\" });\n// ElementInternals.states is a CustomStateSet: whatever it holds,\n// CSS can match with :state(name), from outside or via :host(:state(name)).\nthis._internals = this.attachInternals();\n// Initial state\nthis.state = \"idle\";\nthis._internals.states.add(this.state);\n// One bound listener, so it can be removed again\nthis.handleColorSchemeChange = this.handleColorSchemeChange.bind(this);\n// Auto-reset timer reference\nthis.resetTimer = null;\n// Default auto-reset duration (ms)\nthis.resetDuration = 3000;\n// Check dark mode preference\nthis.darkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;\nthis.render();\n}\nstatic get observedAttributes() {\nreturn ['reset-duration', 'dark-mode'];\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === 'reset-duration' && newValue) {\nthis.resetDuration = parseInt(newValue, 10);\n} else if (name === 'dark-mode') {\nthis.darkMode = newValue === 'true';\nthis.render();\n}\n}\nconnectedCallback() {\n// Listen for system color scheme changes\nthis.colorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)');\nthis.colorSchemeMedia.addEventListener('change', this.handleColorSchemeChange);\n}\ndisconnectedCallback() {\n// Clean up timers when element is removed\nthis.clearResetTimer();\n// Remove event listener\nif (this.colorSchemeMedia) {\nthis.colorSchemeMedia.removeEventListener('change', this.handleColorSchemeChange);\n}\n}\nhandleColorSchemeChange(e) {\nthis.darkMode = e.matches;\nthis.render();\n}\nclearResetTimer() {\nif (this.resetTimer) {\nclearTimeout(this.resetTimer);\nthis.resetTimer = null;\n// Also clear the progress animation\nconst progressBar = this.shadowRoot.querySelector('.progress-bar');\nif (progressBar) {\nprogressBar.style.animation = 'none';\n}\n}\n}\nsetState(newState) {\nthis.state = newState;\nthis._internals.states.clear();\nthis._internals.states.add(newState);\n// Clear any existing timers\nthis.clearResetTimer();\n// Set automatic reset for non-idle states\nif (newState !== \"idle\") {\nthis.resetTimer = setTimeout(() => {\nthis.setState(\"idle\");\n}, this.resetDuration);\n// Update progress bar animation duration after render\nsetTimeout(() => {\nconst progressBar = this.shadowRoot.querySelector('.progress-bar');\nif (progressBar) {\nprogressBar.style.animation = `progress ${this.resetDuration / 1000}s linear forwards`;\n}\n}, 0);\n}\nthis.render();\n}\ntoggleDarkMode() {\nthis.darkMode = !this.darkMode;\nthis.setAttribute('dark-mode', this.darkMode);\nthis.render();\n}\nrender() {\nlet icon, message, ariaLive;\nswitch (this.state) {\ncase \"loading\":\nicon = \"⏳\";\nmessage = \"Loading...\";\nariaLive = \"polite\";\nbreak;\ncase \"success\":\nicon = \"✅\";\nmessage = \"Operation successful!\";\nariaLive = \"assertive\";\nbreak;\ncase \"error\":\nicon = \"❌\";\nmessage = \"An error occurred.\";\nariaLive = \"assertive\";\nbreak;\ndefault: // idle\nicon = \"⚪\";\nmessage = \"Component is idle. Click a button to change state.\";\nariaLive = \"off\";}\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\npadding: 20px;\nborder: 1px solid #ddd;\nborder-radius: 8px;\ntransition: all 0.3s;\nbackground-color: ${this.darkMode ? '#222' : '#fff'};\ncolor: ${this.darkMode ? '#e0e0e0' : '#333'};\n}\nh2 {\nmargin-top: 0;\ncolor: ${this.darkMode ? '#fff' : '#333'};\n}\n.container {\ndisplay: flex;\nalign-items: center;\nmargin-bottom: 20px;\n}\n.icon {\nfont-size: 32px;\nmargin-right: 16px;\n}\n:host(:state(success)) .message {\ncolor: #4caf50;\n}\n:host(:state(error)) .message {\ncolor: #f44336;\n}\n.message {\nfont-size: 18px;\n}\n.buttons {\ndisplay: grid;\ngrid-template-columns: repeat(4, 1fr);\ngap: 10px;\nmargin-bottom: 15px;\n}\nbutton {\npadding: 8px;\nborder: none;\nborder-radius: 4px;\ncursor: pointer;\ntransition: all 0.2s;\n}\nbutton:hover {\nfilter: brightness(0.9);\n}\nbutton:active {\ntransform: translateY(1px);\n}\nbutton:focus {\noutline: 2px solid #2196F3;\noutline-offset: 2px;\n}\nbutton.idle {\nbackground-color: ${this.darkMode ? '#555' : '#e0e0e0'};\ncolor: ${this.darkMode ? '#fff' : '#333'};\n}\nbutton.loading {\nbackground-color: #FFC107;\ncolor: ${this.darkMode ? '#000' : '#333'};\n}\nbutton.success {\nbackground-color: #4CAF50;\ncolor: white;\n}\nbutton.error {\nbackground-color: #f44336;\ncolor: white;\n}\nbutton.theme-toggle {\nbackground-color: ${this.darkMode ? '#777' : '#ddd'};\ncolor: ${this.darkMode ? '#fff' : '#333'};\nmargin-top: 10px;\n}\n.settings {\ndisplay: flex;\nflex-wrap: wrap;\nalign-items: center;\ngap: 12px;\nmargin-bottom: 15px;\n}\n.settings input {\nwidth: 4em;\n}\n.reset-info {\nfont-size: 14px;\nmargin: 10px 0;\ncolor: ${this.darkMode ? '#aaa' : '#666'};\ndisplay: ${this.state === \"idle\" ? \"none\" : \"block\"};\n}\n.progress-container {\nheight: 4px;\nwidth: 100%;\nbackground-color: ${this.darkMode ? '#444' : '#eee'};\nmargin-bottom: 15px;\nborder-radius: 2px;\noverflow: hidden;\ndisplay: ${this.state === \"idle\" ? \"none\" : \"block\"};\n}\n.progress-bar {\nheight: 100%;\nwidth: 100%;\nbackground-color: #2196F3;\ntransform-origin: left;\n}\n@keyframes progress {\n0% { transform: scaleX(1); }\n100% { transform: scaleX(0); }\n}\n.settings {\nmargin-top: 20px;\npadding-top: 15px;\nborder-top: 1px solid ${this.darkMode ? '#444' : '#eee'};\n}\n.config {\ndisplay: flex;\nalign-items: center;\ngap: 10px;\n}\n.config label {\nfont-size: 14px;\n}\n.config input {\nwidth: 70px;\nbackground-color: ${this.darkMode ? '#333' : '#fff'};\ncolor: ${this.darkMode ? '#fff' : '#333'};\nborder: 1px solid ${this.darkMode ? '#555' : '#ddd'};\npadding: 4px;\nborder-radius: 4px;\n}\nsmall {\ncolor: ${this.darkMode ? '#aaa' : '#666'};\n}\n</style>\n<div>\n<h2>Custom State Pseudo-Class Demo</h2>\n<p>This demonstrates how state changes can be exposed to CSS.</p>\n<div class=\"progress-container\" aria-hidden=\"true\">\n<div class=\"progress-bar\"></div>\n</div>\n<div class=\"reset-info\" aria-live=\"polite\">\nWill reset to idle in ${this.resetDuration / 1000} seconds\n</div>\n<div class=\"container\">\n<div class=\"icon\" aria-hidden=\"true\">${icon}</div>\n<div class=\"message\" aria-live=\"${ariaLive}\">${message}</div>\n</div>\n<div class=\"buttons\" role=\"group\" aria-label=\"State control buttons\">\n<button class=\"idle\" id=\"set-idle\">Idle</button>\n<button class=\"loading\" id=\"set-loading\">Loading</button>\n<button class=\"success\" id=\"set-success\">Success</button>\n<button class=\"error\" id=\"set-error\">Error</button>\n</div>\n<div class=\"settings\">\n<button id=\"toggle-theme\" type=\"button\">${this.darkMode ? 'Light' : 'Dark'} theme</button>\n<label>\nReset after\n<input id=\"reset-time\" type=\"number\" min=\"1\" max=\"30\" value=\"${this.resetDuration / 1000}\">\nseconds\n</label>\n</div>\n<p>\n<small>\nThis uses ElementInternals.states: the page styles\nstate-component:state(loading), and the component styles\n:host(:state(success)) from inside its shadow root.\n</small>\n</p>\n</div>\n`;\n// Add event listeners\nthis.shadowRoot.\nquerySelector(\"#set-idle\").\naddEventListener(\"click\", () => {\nthis.setState(\"idle\");\n});\nthis.shadowRoot.\nquerySelector(\"#set-loading\").\naddEventListener(\"click\", () => {\nthis.setState(\"loading\");\n});\nthis.shadowRoot.\nquerySelector(\"#set-success\").\naddEventListener(\"click\", () => {\nthis.setState(\"success\");\n});\nthis.shadowRoot.\nquerySelector(\"#set-error\").\naddEventListener(\"click\", () => {\nthis.setState(\"error\");\n});\n// Add theme toggle listener\nthis.shadowRoot.\nquerySelector(\"#toggle-theme\").\naddEventListener(\"click\", () => {\nthis.toggleDarkMode();\n});\n// Add reset duration input listener\nthis.shadowRoot.\nquerySelector(\"#reset-time\").\naddEventListener(\"change\", e => {\nconst seconds = parseInt(e.target.value, 10);\nif (seconds > 0) {\nthis.resetDuration = seconds * 1000;\n// If there's an active timer, restart it with new duration\nif (this.state !== \"idle\") {\nthis.setState(this.state);\n}\n}\n});\n}}\ncustomElements.define(\"state-component\", StateComponent);\nCustom State Pseudo-Class Demo on the canonical article (https://mnixblog.netlify.app/the-future-of-web-components/).\nMicro-Frontends: The Enterprise Adoption\nA micro-frontend splits one page into parts that separate teams build and ship. Web components give each part a boundary that does not depend on a framework. Adobe's Spectrum Web Components and Microsoft's FAST are published as web components, so any of these parts can use them whatever framework it is written in.\nCommunity Momentum: Tools & Libraries\nLit and Tooling\nLit is the most widely used helper library in this post. It is a small base class that adds reactive properties and templates on top of custom elements. The Lit team also publishes experimental tools under the @lit-labs scope, such as @lit-labs/analyzer, which reads component source to describe it. Check Lit's own release notes for what is stable; this post makes no claims about future Lit versions.\nScoped Custom Element Registries: Solving Collisions\nThe registry of custom element names is global: one tag name can only point to one class for the whole page. Scoped custom element registries let a shadow root have its own registry. The snippet below ran correctly in Chrome 156. As of 26 October 2026 it also ships in Chrome and Edge 146 and Safari 26, but not in a stable Firefox, so feature-detect it (typeof CustomElementRegistry) and have a plan for the browsers without it. It does not work for customized built-in elements.\nconst registry = new CustomElementRegistry();\nregistry.define('my-element', MyElement);\nhost.attachShadow({ mode: 'open', customElementRegistry: registry });\ndocument.createElement('my-element', { customElementRegistry: registry });\nBefore browsers shipped it, the @webcomponents/scoped-custom-element-registry polyfill let teams try the pattern. Most apps never hit a tag-name clash, so this is a feature for the day two teams define <x-button> differently.\nBelow is an interactive example with two components that share one tag name. The demo checks for support first, so in a browser without scoped registries (Firefox, as of today) you will see a notice instead of the components.\nHTML\n<div class=\"demo-container\">\n<header>\n<h2>Scoped Custom Element Registries</h2>\n<button class=\"theme-toggle\" id=\"themeToggle\">Toggle Theme</button>\n</header>\n<p>\nThis demo uses scoped custom element registries, built into Chrome, Edge and Safari.\nThe same tag name can have completely different implementations in different component scopes.\n</p>\n<component-a></component-a>\n<component-b></component-b>\n<component-c></component-c>\n<div class=\"explanation\">\n<h3>About Scoped Custom Element Registries</h3>\n<p>\nScoped Custom Element Registries are part of the future of web components,\nallowing developers to:\n</p>\n<ul>\n<li>Avoid name collisions between components</li>\n<li>Use the same tag name for different implementations in different contexts</li>\n<li>Create truly encapsulated component systems</li>\n<li>Support easier component versioning</li>\n<li>Enable better component composition patterns</li>\n</ul>\n<p>\nEach component creates a <code>new CustomElementRegistry()</code>, defines its own\n<code>custom-button</code> in it, and passes it to\n<code>attachShadow({ customElementRegistry })</code>.\n</p>\n</div>\n</div>\nCSS\n:root {\n--primary-color: #6200ee;\n--secondary-color: #03dac6;\n--background-color: #ffffff;\n--text-color: #333333;\n}\n.dark-mode {\n--primary-color: #bb86fc;\n--secondary-color: #03dac6;\n--background-color: #121212;\n--text-color: #e0e0e0;\n}\nbody {\nfont-family: system-ui, sans-serif;\nbackground-color: var(--background-color);\ncolor: var(--text-color);\ntransition: background-color 0.3s ease;\n}\n.demo-container {\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\n}\ncomponent-a,\ncomponent-b,\ncomponent-c {\ndisplay: block;\nmargin: 20px 0;\npadding: 20px;\nborder: 1px solid #ddd;\nborder-radius: 8px;\n}\nheader {\ndisplay: flex;\njustify-content: space-between;\nalign-items: center;\nmargin-bottom: 20px;\n}\n.theme-toggle {\nbackground-color: var(--primary-color);\ncolor: white;\nborder: none;\npadding: 8px 16px;\nborder-radius: 4px;\ncursor: pointer;\n}\nJavaScript\n// Scoped Custom Element Registries: each component owns a CustomElementRegistry,\n// passes it to attachShadow(), and creates elements from it. Nothing is added to\n// the global customElements registry, so the same tag can mean different things.\nconst scopedRegistriesSupported = (() => {\ntry {\nnew CustomElementRegistry();\nreturn true;\n} catch {\nreturn false;\n}\n})();\nclass ScopedElementsMixin {\nconstructor() {\nthis.registry = new CustomElementRegistry();\n}\n// Define a custom element in this component's scope\ndefineScopedElement(tagName, constructor) {\nthis.registry.define(tagName, constructor);\nreturn constructor;\n}\n// Create an element from this scope's definitions\ncreateScopedElement(tagName) {\nreturn document.createElement(tagName, { customElementRegistry: this.registry });\n}\n// Check if a tag is defined in this scope\nhasScopedElement(tagName) {\nreturn this.registry.get(tagName) !== undefined;\n}\n}\n// Component A with its own registry\nclass ComponentA extends HTMLElement {\nconstructor() {\nsuper();\nthis.scopedElements = new ScopedElementsMixin();\nthis.attachShadow({ mode: \"open\", customElementRegistry: this.scopedElements.registry });\n// Define version A of the custom button\nclass CustomButtonA extends HTMLElement {\nconnectedCallback() {\nthis.innerHTML = `\n<style>\nbutton {\nbackground-color: var(--primary-color);\ncolor: white;\nborder: none;\npadding: 10px 20px;\nborder-radius: 4px;\nfont-size: 16px;\ncursor: pointer;\ntransition: all 0.3s ease;\n}\nbutton:hover {\nopacity: 0.9;\ntransform: translateY(-2px);\n}\n</style>\n<button>Component A Button</button>\n`;\nconst button = this.querySelector(\"button\");\nbutton.addEventListener(\"click\", () => {\nconst event = new CustomEvent(\"component-action\", {\nbubbles: true,\ncomposed: true,\ndetail: { source: \"Component A\", action: \"clicked\" } });\nthis.dispatchEvent(event);\n});\n}}\n// Register in the scoped registry\nthis.scopedElements.defineScopedElement(\"custom-button\", CustomButtonA);\nthis.render();\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n.card {\npadding: 16px;\nbackground-color: var(--background-color);\nborder-radius: 8px;\nbox-shadow: 0 2px 4px rgba(0,0,0,0.1);\n}\nh3 {\nmargin-top: 0;\ncolor: var(--primary-color);\n}\n.output {\nmargin-top: 10px;\nfont-style: italic;\ncolor: var(--primary-color);\n}\n</style>\n<div class=\"card\">\n<h3>Component A</h3>\n<p>This component has its own scoped definition of custom-button:</p>\n<div id=\"button-container\"></div>\n<div class=\"output\" id=\"output-a\"></div>\n</div>\n`;\n// Create the scoped element\nconst button = this.scopedElements.createScopedElement(\"custom-button\");\nthis.shadowRoot.querySelector(\"#button-container\").appendChild(button);\n// Listen for events from any component\ndocument.addEventListener(\"component-action\", e => {\nconst output = this.shadowRoot.querySelector(\"#output-a\");\noutput.textContent = `Event received: ${e.detail.source} ${e.detail.action}`;\n});\n}}\n// Component B with its own registry\nclass ComponentB extends HTMLElement {\nconstructor() {\nsuper();\nthis.scopedElements = new ScopedElementsMixin();\nthis.attachShadow({ mode: \"open\", customElementRegistry: this.scopedElements.registry });\n// Define version B of the custom button\nclass CustomButtonB extends HTMLElement {\nconnectedCallback() {\nthis.innerHTML = `\n<style>\nbutton {\nbackground-color: var(--secondary-color);\ncolor: black;\nborder: none;\npadding: 12px 24px;\nborder-radius: 50px;\nfont-size: 16px;\nfont-weight: bold;\ncursor: pointer;\ntransition: all 0.3s ease;\n}\nbutton:hover {\nbox-shadow: 0 4px 8px rgba(0,0,0,0.2);\n}\n</style>\n<button>Component B Button</button>\n`;\nconst button = this.querySelector(\"button\");\nbutton.addEventListener(\"click\", () => {\nconst event = new CustomEvent(\"component-action\", {\nbubbles: true,\ncomposed: true,\ndetail: { source: \"Component B\", action: \"clicked\" } });\nthis.dispatchEvent(event);\n});\n}}\n// Register in the scoped registry\nthis.scopedElements.defineScopedElement(\"custom-button\", CustomButtonB);\nthis.render();\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n.card {\npadding: 16px;\nbackground-color: var(--background-color);\nborder-radius: 8px;\nbox-shadow: 0 2px 4px rgba(0,0,0,0.1);\n}\nh3 {\nmargin-top: 0;\ncolor: var(--secondary-color);\n}\n.output {\nmargin-top: 10px;\nfont-style: italic;\ncolor: var(--secondary-color);\n}\n</style>\n<div class=\"card\">\n<h3>Component B</h3>\n<p>This component has its own scoped definition of custom-button:</p>\n<div id=\"button-container\"></div>\n<div class=\"output\" id=\"output-b\"></div>\n</div>\n`;\n// Create the scoped element\nconst button = this.scopedElements.createScopedElement(\"custom-button\");\nthis.shadowRoot.querySelector(\"#button-container\").appendChild(button);\n// Listen for events from any component\ndocument.addEventListener(\"component-action\", e => {\nconst output = this.shadowRoot.querySelector(\"#output-b\");\noutput.textContent = `Event received: ${e.detail.source} ${e.detail.action}`;\n});\n}}\n// Component C with nested scoped elements\nclass ComponentC extends HTMLElement {\nconstructor() {\nsuper();\nthis.scopedElements = new ScopedElementsMixin();\nthis.attachShadow({ mode: \"open\", customElementRegistry: this.scopedElements.registry });\n// Define version C of the custom button\nclass CustomButtonC extends HTMLElement {\nconnectedCallback() {\nthis.innerHTML = `\n<style>\nbutton {\nbackground: linear-gradient(45deg, var(--primary-color), var(--secondary-color));\ncolor: white;\nborder: none;\npadding: 10px 20px;\nborder-radius: 4px;\nfont-size: 16px;\ncursor: pointer;\nbox-shadow: 0 2px 5px rgba(0,0,0,0.2);\ntransition: all 0.3s ease;\n}\nbutton:hover {\ntransform: scale(1.05);\n}\n</style>\n<button>Component C Button</button>\n`;\nconst button = this.querySelector(\"button\");\nbutton.addEventListener(\"click\", () => {\nconst event = new CustomEvent(\"component-action\", {\nbubbles: true,\ncomposed: true,\ndetail: { source: \"Component C\", action: \"clicked\" } });\nthis.dispatchEvent(event);\n});\n}}\n// Define a nested component with its own registry\nclass NestedComponent extends HTMLElement {\nconstructor() {\nsuper();\nthis.nestedRegistry = new ScopedElementsMixin();\nthis.attachShadow({ mode: \"open\", customElementRegistry: this.nestedRegistry.registry });\n// Define a unique version of custom-button for nested component\nclass NestedButton extends HTMLElement {\nconnectedCallback() {\nthis.innerHTML = `\n<style>\nbutton {\nbackground-color: transparent;\ncolor: var(--text-color);\nborder: 2px solid currentColor;\npadding: 8px 16px;\nborder-radius: 4px;\nfont-size: 14px;\ncursor: pointer;\n}\n</style>\n<button>Nested Button</button>\n`;\nconst button = this.querySelector(\"button\");\nbutton.addEventListener(\"click\", () => {\nconst event = new CustomEvent(\"nested-action\", {\nbubbles: true,\ncomposed: true });\nthis.dispatchEvent(event);\n});\n}}\n// Register nested button in its own registry\nthis.nestedRegistry.defineScopedElement(\"custom-button\", NestedButton);\n// Render nested component\nthis.shadowRoot.innerHTML = `\n<style>\n.nested {\nmargin: 10px;\npadding: 10px;\nborder: 1px dashed var(--text-color);\nborder-radius: 4px;\n}\nh4 {\nmargin-top: 0;\nfont-size: 14px;\n}\n</style>\n<div class=\"nested\">\n<h4>Nested Component Scope</h4>\n<p>This nested component has its own scoped registry:</p>\n<div id=\"nested-button-container\"></div>\n</div>\n`;\n// Create and append the nested button\nconst nestedButton = this.nestedRegistry.createScopedElement(\n\"custom-button\");\nthis.shadowRoot.\nquerySelector(\"#nested-button-container\").\nappendChild(nestedButton);\n// Listen for clicks on the nested button\nthis.shadowRoot.addEventListener(\"nested-action\", () => {\nconst event = new CustomEvent(\"component-action\", {\nbubbles: true,\ncomposed: true,\ndetail: { source: \"Nested Component\", action: \"clicked\" } });\nthis.dispatchEvent(event);\n});\n}}\n// Register components in C's scope\nthis.scopedElements.defineScopedElement(\"custom-button\", CustomButtonC);\nthis.scopedElements.defineScopedElement(\"nested-component\", NestedComponent);\nthis.render();\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n.card {\npadding: 16px;\nbackground-color: var(--background-color);\nborder-radius: 8px;\nbox-shadow: 0 2px 4px rgba(0,0,0,0.1);\n}\nh3 {\nmargin-top: 0;\nbackground: linear-gradient(45deg, var(--primary-color), var(--secondary-color));\n-webkit-background-clip: text;\n-webkit-text-fill-color: transparent;\n}\n.component-registry {\nmargin-top: 15px;\nfont-size: 14px;\n}\n.output {\nmargin-top: 10px;\nfont-style: italic;\nbackground: linear-gradient(45deg, var(--primary-color), var(--secondary-color));\n-webkit-background-clip: text;\n-webkit-text-fill-color: transparent;\nfont-weight: bold;\n}\n</style>\n<div class=\"card\">\n<h3>Component C (Nested Components)</h3>\n<p>This component demonstrates nested scoped registries:</p>\n<div id=\"button-container\"></div>\n<div id=\"nested-container\"></div>\n<div class=\"component-registry\">\n<p>Registered elements in this scope:\n<code>custom-button</code>, <code>nested-component</code>\n</p>\n</div>\n<div class=\"output\" id=\"output-c\"></div>\n</div>\n`;\n// Create the scoped elements\nconst button = this.scopedElements.createScopedElement(\"custom-button\");\nconst nested = this.scopedElements.createScopedElement(\"nested-component\");\nthis.shadowRoot.querySelector(\"#button-container\").appendChild(button);\nthis.shadowRoot.querySelector(\"#nested-container\").appendChild(nested);\n// Listen for events from any component\ndocument.addEventListener(\"component-action\", e => {\nconst output = this.shadowRoot.querySelector(\"#output-c\");\noutput.textContent = `Event received: ${e.detail.source} ${e.detail.action}`;\n});\n}}\n// Register the components\nif (scopedRegistriesSupported) {\ncustomElements.define(\"component-a\", ComponentA);\ncustomElements.define(\"component-b\", ComponentB);\ncustomElements.define(\"component-c\", ComponentC);\n} else {\nconst notice = document.createElement(\"p\");\nnotice.className = \"explanation\";\nnotice.textContent = \"This browser does not support scoped custom element registries yet, so the components cannot be shown.\";\ndocument.querySelector(\"component-a\").before(notice);\n}\n// Theme toggle functionality\nconst themeToggleButton = document.getElementById(\"themeToggle\");\n// Function to update button text based on current theme\nfunction updateThemeButtonText() {\nthemeToggleButton.textContent = document.body.classList.contains(\"dark-mode\") ?\n\"Light Theme\" :\n\"Dark Theme\";\n}\n// Check user's system preference for dark mode\nfunction initializeThemeBasedOnPreference() {\n// Check if user prefers dark mode\nconst prefersDarkMode = window.matchMedia(\"(prefers-color-scheme: dark)\").\nmatches;\n// Set initial theme based on preference\nif (prefersDarkMode) {\ndocument.body.classList.add(\"dark-mode\");\n}\n// Update button text to reflect initial state\nupdateThemeButtonText();\n}\n// Initialize theme when page loads\ninitializeThemeBasedOnPreference();\n// Theme toggle button click handler\nthemeToggleButton.addEventListener(\"click\", () => {\ndocument.body.classList.toggle(\"dark-mode\");\nupdateThemeButtonText();\n});\nScoped Custom Element Registries on the canonical article (https://mnixblog.netlify.app/the-future-of-web-components/).\nTesting Gets Easier\nWeb Test Runner can run component tests in real browsers, and Storybook has a web-components renderer that can read a component's description from a Custom Elements Manifest. Both reduce the need for framework-specific test setups.\nOn the Horizon: Features to Watch\nManual Slot Assignment\nNormally the browser fills slots by matching slot=\"name\" attributes. Manual assignment lets script decide instead. It ran in Chrome 156, and it is Baseline widely available (all three engines since March 2023).\nconst root = host.attachShadow({ mode: 'open', slotAssignment: 'manual' });\nconst slot = document.createElement('slot');\nroot.append(slot);\nslot.assign(host.querySelector('h1')); // this child now fills the slot\nYou may see an element.attachSlot('header') call in older articles. That method does not exist (it is undefined in Chrome 156). Use slotAssignment: 'manual' and slot.assign().\nQuestion: Which of these is NOT a real browser API?\nslot.assign(node) with slotAssignment: 'manual'\nnew CustomElementRegistry()\nelement.attachSlot('header')\ninternals.states.add('loading')\nAnswer: 3. element.attachSlot('header')\nExplanation: attachSlot does not exist. Manual slotting is slotAssignment: 'manual' plus slot.assign(); scoped registries and custom states are real and ran in Chrome 156.\n@scope: Scoped Rules, Not Shadow DOM\n@scope limits where a CSS rule applies in a normal stylesheet. It became Baseline newly available in March 2026:\n@scope (.card) {\nh1 { color: red; } /* Only matches h1 elements inside .card */\n}\nIt works in the light DOM and does not create or cross a shadow boundary (in the test, an h1 inside .card turned red and an h1 outside stayed black). Styling across a shadow boundary is done with CSS custom properties, ::part() and ::slotted(), all of which ship today.\nQuestion: What does @scope (.card) { h1 { color: red; } } do?\nCreates a shadow root on every .card\nApplies the rule only to h1 elements inside .card\nLets outside CSS style the inside of any shadow root\nAnswer: 2. Applies the rule only to h1 elements inside .card\nExplanation: @scope is a cascade feature for the normal DOM. Shadow roots are created with attachShadow().\nWebAssembly Integration\nWebAssembly is compiled code that runs in the browser. A custom element can call into it, for example to do heavy number crunching while keeping a plain HTML interface. Tools such as wasm-bindgen let Rust code talk to JavaScript. This is a combination of existing features, not a new web component standard.\nCheck out a simulation of how we can bring WebAssembly into a web component below:\nHTML\n<button id=\"dark-mode-toggle\" class=\"dark-mode-toggle\">🌙</button>\n<h1>WebAssembly Component Integration</h1>\n<p>\nThis component demonstrates how WebAssembly can be integrated with Web\nComponents for high-performance computation.\n</p>\n<wasm-component initial-value=\"5\" wasm-url=\"math_ops.wasm\"></wasm-component>\n<div style=\"margin-top: 30px\">\n<h3>Component API Usage</h3>\n<button id=\"external-set\">Set Value to 42</button>\n<button id=\"external-get\">Get Current Value</button>\n</div>\nCSS\nbody {\nfont-family: system-ui, sans-serif;\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\ntransition: background-color 0.3s, color 0.3s;\n}\nbody.dark-mode {\nbackground-color: #121212;\ncolor: #e0e0e0;\n}\n.dark-mode-toggle {\nposition: absolute;\ntop: 20px;\nright: 20px;\nbackground: none;\nborder: none;\ncursor: pointer;\nfont-size: 24px;\n}\nh1,\nh3 {\ntransition: color 0.3s;\n}\nbody.dark-mode h1,\nbody.dark-mode h3 {\ncolor: #e0e0e0;\n}\nJavaScript\n// Dark mode functionality\nfunction initDarkMode() {\n// Check for user preference\nconst prefersDarkMode =\nwindow.matchMedia &&\nwindow.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n// Set initial mode based on preference\nif (prefersDarkMode) {\ndocument.body.classList.add(\"dark-mode\");\n}\n// Add toggle listener\ndocument.getElementById(\"dark-mode-toggle\").addEventListener(\"click\", () => {\ndocument.body.classList.toggle(\"dark-mode\");\nupdateDarkModeIcon();\n});\nupdateDarkModeIcon();\n}\nfunction updateDarkModeIcon() {\nconst isDarkMode = document.body.classList.contains(\"dark-mode\");\ndocument.getElementById(\"dark-mode-toggle\").textContent = isDarkMode\n? \"☀️\"\n: \"🌙\";\n}\nclass WasmComponent extends HTMLElement {\nstatic observedAttributes = [\"initial-value\", \"wasm-url\"];\nconstructor() {\nsuper();\nthis.attachShadow({ mode: \"open\" });\nthis.counter = 0;\nthis.isLoading = true;\nthis.hasError = false;\nthis.errorMessage = \"\";\nthis.wasmModule = null;\nthis.MAX_VALUE = 1.9767639272283756e154; // Maximum allowed value\n// Check if dark mode is active when component is created\nthis.isDarkMode = document.body.classList.contains(\"dark-mode\");\nthis.render();\n}\nconnectedCallback() {\n// Listen for dark mode changes on the body\nthis._darkModeObserver = new MutationObserver((mutations) => {\nmutations.forEach((mutation) => {\nif (mutation.attributeName === \"class\") {\nthis.isDarkMode = document.body.classList.contains(\"dark-mode\");\nthis.render();\n}\n});\n});\nthis._darkModeObserver.observe(document.body, { attributes: true });\n// Get initial values from attributes\nif (this.hasAttribute(\"initial-value\")) {\nthis.counter = parseInt(this.getAttribute(\"initial-value\"), 10) || 0;\nthis.updateDisplay();\n}\n// Load the WASM module\nconst wasmUrl = this.getAttribute(\"wasm-url\") || \"math_ops.wasm\";\nthis.loadWasmModule(wasmUrl);\n}\ndisconnectedCallback() {\n// Clean up observer when component is removed\nif (this._darkModeObserver) {\nthis._darkModeObserver.disconnect();\n}\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === \"initial-value\" && this.shadowRoot) {\nthis.counter = parseInt(newValue, 10) || 0;\nthis.updateDisplay();\n}\nif (name === \"wasm-url\" && oldValue !== newValue && this.isConnected) {\nthis.isLoading = true;\nthis.hasError = false;\nthis.render();\nthis.loadWasmModule(newValue);\n}\n}\nasync loadWasmModule(url) {\ntry {\nthis.isLoading = true;\nthis.render();\nawait new Promise((resolve) => setTimeout(resolve, 1000));\nthis.wasmModule = {\nexports: {\nincrement: (value) => {\nconst result = value + 1;\nreturn result > this.MAX_VALUE ? this.MAX_VALUE : result;\n},\ndecrement: (value) => value - 1,\nsquare: (value) => {\nconst result = value * value;\nreturn result > this.MAX_VALUE ? this.MAX_VALUE : result;\n},\ncube: (value) => {\nconst result = value * value * value;\nreturn result > this.MAX_VALUE ? this.MAX_VALUE : result;\n},\ndouble: (value) => {\nconst result = value * 2;\nreturn result > this.MAX_VALUE ? this.MAX_VALUE : result;\n},\nhalf: (value) => value / 2,\nfactorial: (value) => {\nif (value <= 1) return 1;\nlet result = 1;\nfor (let i = 2; i <= value; i++) {\nresult *= i;\nif (result > this.MAX_VALUE) return this.MAX_VALUE;\n}\nreturn result;\n},\nisPrime: (value) => {\nif (value <= 1) return 0;\nif (value <= 3) return 1;\nif (value % 2 === 0 || value % 3 === 0) return 0;\nlet i = 5;\nwhile (i * i <= value) {\nif (value % i === 0 || value % (i + 2) === 0) return 0;\ni += 6;\n}\nreturn 1;\n}\n}\n};\nthis.isLoading = false;\nthis.hasError = false;\nthis.dispatchEvent(\nnew CustomEvent(\"wasm-loaded\", {\nbubbles: true,\ncomposed: true,\ndetail: { success: true }\n})\n);\n} catch (error) {\nconsole.error(\"Failed to load WASM module:\", error);\nthis.isLoading = false;\nthis.hasError = true;\nthis.errorMessage = error.message || \"Failed to load WebAssembly module\";\nthis.dispatchEvent(\nnew CustomEvent(\"wasm-error\", {\nbubbles: true,\ncomposed: true,\ndetail: { error }\n})\n);\n}\nthis.render();\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\npadding: 20px;\nborder: 1px solid ${this.isDarkMode ? \"#9575cd\" : \"#673AB7\"};\nborder-radius: 8px;\nbackground-color: ${this.isDarkMode ? \"#1e1e2f\" : \"#f8f5ff\"};\ncolor: ${this.isDarkMode ? \"#e0e0e0\" : \"inherit\"};\ntransition: background-color 0.3s, border-color 0.3s, color 0.3s;\n}\nh2 {\nmargin-top: 0;\ncolor: ${this.isDarkMode ? \"#9575cd\" : \"#673AB7\"};\ntransition: color 0.3s;\n}\n.display {\nfont-size: 48px;\ntext-align: center;\nmargin: 20px 0;\nfont-weight: bold;\ncolor: ${this.isDarkMode ? \"#9575cd\" : \"#673AB7\"};\ntransition: color 0.3s;\n}\n.buttons {\ndisplay: flex;\nflex-wrap: wrap;\njustify-content: center;\ngap: 10px;\nmargin-bottom: 15px;\n}\nbutton {\nbackground-color: ${this.isDarkMode ? \"#9575cd\" : \"#673AB7\"};\ncolor: ${this.isDarkMode ? \"#121212\" : \"white\"};\nborder: none;\npadding: 10px 20px;\nborder-radius: 4px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.2s, color 0.2s;\n}\nbutton:hover {\nbackground-color: ${this.isDarkMode ? \"#b39ddb\" : \"#5e35b1\"};\n}\nbutton:disabled {\nbackground-color: ${this.isDarkMode ? \"#4a4a4a\" : \"#b39ddb\"};\ncursor: not-allowed;\n}\n.loading {\ntext-align: center;\npadding: 20px;\ncolor: ${this.isDarkMode ? \"#9575cd\" : \"#673AB7\"};\n}\n.loading::after {\ncontent: \"...\";\nanimation: dots 1.5s infinite;\n}\n.error {\nbackground-color: ${this.isDarkMode ? \"#3c1f1f\" : \"#ffebee\"};\nborder: 1px solid ${this.isDarkMode ? \"#b71c1c\" : \"#f44336\"};\ncolor: ${this.isDarkMode ? \"#f48fb1\" : \"#d32f2f\"};\npadding: 10px;\nborder-radius: 4px;\nmargin: 10px 0;\n}\n.note {\nmargin-top: 20px;\nfont-size: 14px;\ncolor: ${this.isDarkMode ? \"#aaa\" : \"#666\"};\nfont-style: italic;\n}\n.prime-indicator {\nfont-size: 14px;\nmargin-top: 5px;\ntext-align: center;\nfont-weight: bold;\n}\n.prime-yes {\ncolor: ${this.isDarkMode ? \"#81c784\" : \"#4caf50\"};\n}\n.prime-no {\ncolor: ${this.isDarkMode ? \"#e57373\" : \"#f44336\"};\n}\n@keyframes dots {\n0%, 20% { content: \".\"; }\n40% { content: \"..\"; }\n60%, 100% { content: \"...\"; }\n}\n</style>\n<div>\n<h2>Advanced WebAssembly Component Demo</h2>\n<p>This demonstrates how a Web Component integrates with WebAssembly.</p>\n${\nthis.isLoading\n? `\n<div class=\"loading\">Loading WebAssembly module</div>\n`\n: this.hasError\n? `\n<div class=\"error\">\n<strong>Error:</strong> ${\nthis.errorMessage || \"Failed to load WebAssembly module\"\n}\n</div>\n`\n: `\n<div class=\"display\">${this.counter}</div>\n<div class=\"prime-indicator ${\nthis.wasmModule?.exports.isPrime(this.counter)\n? \"prime-yes\"\n: \"prime-no\"\n}\">\n${\nthis.wasmModule?.exports.isPrime(this.counter)\n? \"This is a prime number!\"\n: this.counter > 1\n? \"This is not a prime number\"\n: \"\"\n}\n</div>\n<div class=\"buttons\">\n<button id=\"decrement\" ?disabled=\"${\nthis.isLoading\n}\">Decrement</button>\n<button id=\"increment\" ?disabled=\"${\nthis.isLoading\n}\">Increment</button>\n<button id=\"double\" ?disabled=\"${\nthis.isLoading\n}\">Double</button>\n<button id=\"half\" ?disabled=\"${\nthis.isLoading\n}\">Half</button>\n<button id=\"square\" ?disabled=\"${\nthis.isLoading\n}\">Square</button>\n<button id=\"cube\" ?disabled=\"${\nthis.isLoading\n}\">Cube</button>\n<button id=\"factorial\" ?disabled=\"${\nthis.isLoading || this.counter > 12\n}\">Factorial</button>\n<button id=\"reset\" ?disabled=\"${\nthis.isLoading\n}\">Reset</button>\n</div>\n`\n}\n<p class=\"note\">\n${\nthis.isLoading\n? \"Loading WebAssembly module...\"\n: this.hasError\n? \"Failed to load WebAssembly module. Check console for details.\"\n: \"Note: This simulates WASM integration. In a production environment, these operations would be performed by actual WebAssembly code.\"\n}\n</p>\n</div>\n`;\nif (!this.isLoading && !this.hasError) {\nthis.shadowRoot.querySelector(\"#increment\").addEventListener(\"click\", () => {\nthis.counter = this.wasmModule.exports.increment(this.counter);\nthis.updateDisplay();\nthis.checkMaxValueReached();\n});\nthis.shadowRoot.querySelector(\"#decrement\").addEventListener(\"click\", () => {\nthis.counter = this.wasmModule.exports.decrement(this.counter);\nthis.updateDisplay();\n});\nthis.shadowRoot.querySelector(\"#square\").addEventListener(\"click\", () => {\nthis.counter = this.wasmModule.exports.square(this.counter);\nthis.updateDisplay();\nthis.checkMaxValueReached();\n});\nthis.shadowRoot.querySelector(\"#cube\").addEventListener(\"click\", () => {\nthis.counter = this.wasmModule.exports.cube(this.counter);\nthis.updateDisplay();\nthis.checkMaxValueReached();\n});\nthis.shadowRoot.querySelector(\"#double\").addEventListener(\"click\", () => {\nthis.counter = this.wasmModule.exports.double(this.counter);\nthis.updateDisplay();\nthis.checkMaxValueReached();\n});\nthis.shadowRoot.querySelector(\"#half\").addEventListener(\"click\", () => {\nthis.counter = this.wasmModule.exports.half(this.counter);\nthis.updateDisplay();\n});\nthis.shadowRoot.querySelector(\"#factorial\").addEventListener(\"click\", () => {\nif (this.counter <= 12) {\nthis.counter = this.wasmModule.exports.factorial(this.counter);\nthis.updateDisplay();\nthis.checkMaxValueReached();\n}\n});\nthis.shadowRoot.querySelector(\"#reset\").addEventListener(\"click\", () => {\nthis.counter = 0;\nthis.updateDisplay();\n});\n}\n}\ncheckMaxValueReached() {\nif (this.counter >= this.MAX_VALUE) {\nconsole.warn(\"Maximum value reached\");\nconst note = this.shadowRoot.querySelector(\".note\");\nif (note) {\nnote.innerHTML =\n'<strong style=\"color: #ff9800;\">Warning: Maximum value reached. Some operations may be limited.</strong>';\nsetTimeout(() => {\nnote.innerHTML =\n\"Note: This simulates WASM integration. In a production environment, these operations would be performed by actual WebAssembly code.\";\n}, 3000);\n}\n}\n}\nupdateDisplay() {\nif (this.shadowRoot.querySelector(\".display\")) {\nlet displayText = this.counter;\nif (this.counter > 1e15) {\ndisplayText = this.counter.toExponential(2);\n}\nthis.shadowRoot.querySelector(\".display\").textContent = displayText;\nconst primeIndicator = this.shadowRoot.querySelector(\".prime-indicator\");\nif (primeIndicator) {\nconst isPrime = this.wasmModule.exports.isPrime(this.counter);\nprimeIndicator.className = `prime-indicator ${\nisPrime ? \"prime-yes\" : \"prime-no\"\n}`;\nprimeIndicator.textContent = isPrime\n? \"This is a prime number!\"\n: this.counter > 1\n? \"This is not a prime number\"\n: \"\";\n}\nconst factorialBtn = this.shadowRoot.querySelector(\"#factorial\");\nif (factorialBtn) {\nfactorialBtn.disabled = this.counter > 12;\n}\n}\nthis.dispatchEvent(\nnew CustomEvent(\"value-changed\", {\nbubbles: true,\ncomposed: true,\ndetail: { value: this.counter }\n})\n);\n}\ngetValue() {\nreturn this.counter;\n}\nsetValue(value) {\nconst parsedValue = parseInt(value, 10) || 0;\nthis.counter = parsedValue > this.MAX_VALUE ? this.MAX_VALUE : parsedValue;\nthis.updateDisplay();\nthis.checkMaxValueReached();\n}\n}\ncustomElements.define(\"wasm-component\", WasmComponent);\ndocument.addEventListener(\"DOMContentLoaded\", initDarkMode);\ndocument.getElementById(\"external-set\").addEventListener(\"click\", () => {\nconst component = document.querySelector(\"wasm-component\");\ncomponent.setValue(42);\n});\ndocument.getElementById(\"external-get\").addEventListener(\"click\", () => {\nconst component = document.querySelector(\"wasm-component\");\nalert(`Current value: ${component.getValue()}`);\n});\ndocument\n.querySelector(\"wasm-component\")\n.addEventListener(\"value-changed\", (e) => {\nconsole.log(\"Value changed:\", e.detail.value);\n});\ndocument\n.querySelector(\"wasm-component\")\n.addEventListener(\"wasm-loaded\", (e) => {\nconsole.log(\"WASM module loaded successfully\");\n});\nWebAssembly Component Demo on the canonical article (https://mnixblog.netlify.app/the-future-of-web-components/).\nChallenges That Remain\nFrameworks. React passes unknown props to custom elements as attributes in version 18 and earlier (checked with 18.3.1 in the post on frameworks (https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/)). React 19 changed this and is not covered here.\nAccessibility. Custom elements do not get built-in semantics. ElementInternals.role and the ARIA properties let a component declare its role; internals.role = 'button' worked in Chrome 156. A property named accessibleRole does not exist.\nLearning. Documentation and examples spread across libraries remain the main barrier for newcomers.\nConclusion\nYou can now tell which Web Component features are safe to use and which are not. Use ::part(), :state(), Declarative Shadow DOM (shadowrootmode), manual slotting, constructable stylesheets and custom events today. Check @scope against your oldest supported browser, and treat scoped registries as a feature for Chrome, Edge and Safari until Firefox ships it. When an article names a method you have not seen, search for it on MDN or run it in your console before building on it, as attachSlot shows.\nFor a next step, build a small component that uses one of the \"ships today\" rows from the table above. To follow standards work, the W3C Web Components Community Group (https://www.w3.org/community/webcomponents/) publishes its discussions.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/the-future-of-web-components/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2025-04-20T04:00:25Z",
      "id": "https://mnixblog.netlify.app/the-future-of-web-components/",
      "image": "https://mnixblog.netlify.app/images/the-future-of-web-components/cover-v2.jpg",
      "summary": "Which Web Component features you can use today, which work in only some browsers, and which are still proposals, with a dated status table.",
      "tags": [
        "WebAssembly",
        "Web Components",
        "CSS",
        "Frameworks"
      ],
      "title": "The Future of Web Components",
      "url": "https://mnixblog.netlify.app/the-future-of-web-components/"
    },
    {
      "content_html": "<p>Pass a JavaScript object to a web component from React, and the component receives the text <code>[object Object]</code>. That is the whole problem this post solves: frameworks and web components pass data in different ways, and a few small habits bridge them.</p>\n<p>When a framework such as React meets a custom tag like <code>&lt;user-badge&gt;</code>, data and events can get lost on the way across. This post shows where, and how to stop it.</p>\n<p><strong>Prerequisites in two lines:</strong> a custom element can take small settings as HTML attributes (always strings) and richer data as JavaScript properties (<code>el.data = {...}</code>). It talks back with custom events that you listen for with <code>addEventListener</code>.</p>\n<p>React is covered in full and Vue is the second example. Angular and Svelte follow the same two ideas (property for data, <code>addEventListener</code> for events), so their sections are optional reading.</p>\n<h2 id=\"why-framework-integration-matters\">Why Framework Integration Matters<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#why-framework-integration-matters\" aria-label=\"Link to this section\">#</a></h2>\n<p>Web Components were designed to be universal, but the reality of modern web development means they must coexist with frameworks that have their own component models and lifecycles. Successful integration requires understanding both worlds and the boundaries between them.</p>\n<p>Here is the failure, so you can recognize it. In React 18 (checked with 18.3.1), this element receives a string, not your object:</p>\n<pre><code class=\"language-jsx\">&lt;user-badge user={{ name: 'Ada' }} /&gt;\n// the DOM now has user=\"[object Object]\", and the component's `user` property is untouched\n</code></pre>\n<p>React 18 writes unknown props on custom elements as attributes, and an attribute can only hold text. A function prop such as <code>onChartUpdate={fn}</code> is also not turned into an event listener for a custom event. React 19 changed how props reach custom elements and is not covered here, so check its release notes if you use it.</p>\n<p>What makes integration challenging?</p>\n<ul>\n<li>\n<p><strong>Different Data Flow Models</strong>: React's unidirectional data flow differs fundamentally from the property-based approach of Web Components.</p>\n</li>\n<li>\n<p><strong>Event Handling Discrepancies</strong>: Each framework has its own event system that must be reconciled with the DOM event model.</p>\n</li>\n<li>\n<p><strong>Lifecycle Management</strong>: Coordinating component lifecycle events between frameworks and Web Components requires careful orchestration.</p>\n</li>\n</ul>\n<p>Let's explore how to overcome these challenges with practical, reusable patterns.</p>\n<h2 id=\"using-web-components-with-modern-frameworks\">Using Web Components with Modern Frameworks<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#using-web-components-with-modern-frameworks\" aria-label=\"Link to this section\">#</a></h2>\n<h3 id=\"react-integration-strategies\">React Integration Strategies<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#react-integration-strategies\" aria-label=\"Link to this section\">#</a></h3>\n<p>React's declarative approach to UI challenges direct integration with the imperative DOM APIs of Web Components. Here are advanced patterns to bridge this gap:</p>\n<h4 id=\"1-the-ref-pattern-direct-dom-access\">1. The Ref Pattern: Direct DOM Access<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#1-the-ref-pattern-direct-dom-access\" aria-label=\"Link to this section\">#</a></h4>\n<pre><code class=\"language-javascript\">function MyReactComponent() {\n  const wcRef = useRef(null);\n\n  useEffect(() =&gt; {\n    if (wcRef.current) {\n      // Direct property assignment for complex data\n      wcRef.current.complexData = { key: \"value\" };\n\n      // Event handling with proper cleanup\n      const el = wcRef.current;\n      const handleEvent = (e) =&gt; console.log(e.detail);\n      el.addEventListener('custom-event', handleEvent);\n\n      return () =&gt; {\n        el.removeEventListener('custom-event', handleEvent);\n      };\n    }\n  }, []);\n\n  return &lt;my-web-component ref={wcRef} string-prop=\"This works\" /&gt;;\n}\n</code></pre>\n<p><strong>Key Insights:</strong></p>\n<ul>\n<li>\n<p>React passes string attributes directly, but complex data requires refs</p>\n</li>\n<li>\n<p>Event handling needs manual listeners with proper cleanup</p>\n</li>\n<li>\n<p>Ref access enables imperative methods on Web Components</p>\n</li>\n</ul>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>A page script wants a component to receive an object. Which call gives the component the object itself?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>el.setAttribute('data', obj)</code></li>\n<li><code>el.data = obj</code></li>\n<li><code>el.getAttribute('data')</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. <code>el.data = obj</code></p>\n<p><code>setAttribute</code> converts its value to a string (<code>\"[object Object]\"</code> for a plain object). Assigning a property keeps the object as it is.</p>\n</div>\n</div>\n</section>\n<p><strong>Coding challenge: Hand React's data to the element</strong></p>\n<p><code>&lt;user-badge&gt;</code> has a <code>user</code> property, but the React wrapper never sets it, so the preview says \"Hello, nobody yet\".</p>\n<p><strong>Expected result:</strong> after you finish the TODO the preview says \"Hello, Ada\".</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Set a property through a ref</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div id=&quot;root&quot;&gt;&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class UserBadge extends HTMLElement {\n  constructor() { super(); this.attachShadow({ mode: &#39;open&#39; }); }\n  set user(value) { this._user = value; this.render(); }\n  get user() { return this._user; }\n  connectedCallback() { this.render(); }\n  render() {\n    const name = this._user ? this._user.name : &#39;nobody yet&#39;;\n    this.shadowRoot.innerHTML = &#39;&lt;p&gt;Hello, &#39; + name + &#39;&lt;/p&gt;&#39;;\n  }\n}\ncustomElements.define(&#39;user-badge&#39;, UserBadge);\n\nconst { useRef, useEffect, createElement: h } = React;\n\nfunction Badge({ user }) {\n  const ref = useRef(null);\n  // TODO: in a useEffect, set ref.current.user = user (run again when user changes)\n  return h(&#39;user-badge&#39;, { ref });\n}\n\nReactDOM.createRoot(document.getElementById(&#39;root&#39;)).render(\n  h(Badge, { user: { name: &#39;Ada&#39; } })\n);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p><code>useEffect(() =&gt; { ref.current.user = user; }, [user]);</code> goes where the TODO is.</p>\n</aside>\n<h4 id=\"2-the-wrapper-pattern-creating-react-friendly-components\">2. The Wrapper Pattern: Creating React-Friendly Components<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#2-the-wrapper-pattern-creating-react-friendly-components\" aria-label=\"Link to this section\">#</a></h4>\n<p>React developers expect components with React-like patterns. Create wrapper components that abstract Web Component peculiarities:</p>\n<pre><code class=\"language-jsx\">// Wrapper that handles property mapping and events\nfunction EnhancedWebComponent({ data, onCustomEvent, children }) {\n  const ref = useRef(null);\n\n  useEffect(() =&gt; {\n    const element = ref.current;\n    if (element) {\n      // Set complex data\n      element.data = data;\n\n      // Handle events\n      const eventHandler = (e) =&gt; onCustomEvent(e.detail);\n      element.addEventListener('custom-event', eventHandler);\n\n      return () =&gt; element.removeEventListener('custom-event', eventHandler);\n    }\n  }, [data, onCustomEvent]);\n\n  return (\n    &lt;my-web-component ref={ref}&gt;\n      {children}\n    &lt;/my-web-component&gt;\n  );\n}\n\n// Usage feels like a normal React component\nfunction App() {\n  return (\n    &lt;EnhancedWebComponent \n      data={{complex: \"data\"}} \n      onCustomEvent={handleEvent}\n    &gt;\n      &lt;p&gt;Child content&lt;/p&gt;\n    &lt;/EnhancedWebComponent&gt;\n  );\n}\n</code></pre>\n<p><strong>Interactive Demo:</strong> Try the React integration patterns yourself</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">React + Web Components Integration</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div id=&quot;root&quot;&gt;&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">.demo-container {\n  font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, Oxygen, Ubuntu, Cantarell, &#39;Open Sans&#39;, &#39;Helvetica Neue&#39;, sans-serif;\n  max-width: 800px;\n  margin: 0 auto;\n  padding: 20px;\n}\n\n.examples {\n  display: flex;\n  flex-direction: column;\n  gap: 20px;\n}\n\n.example {\n  border: 1px solid #ddd;\n  border-radius: 8px;\n  padding: 16px;\n  background-color: #fff;\n}\n\nh2 {\n  color: #2c3e50;\n  margin-top: 0;\n}\n\nh3 {\n  color: #2c3e50;\n  margin-top: 0;\n}\n\n.button-group {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n  margin-top: 8px;\n}\n\n.react-button {\n  background-color: #61dafb;\n  color: #000;\n  border: none;\n  border-radius: 4px;\n  padding: 8px 16px;\n  cursor: pointer;\n  font-size: 16px;\n}\n\n.events {\n  margin-top: 16px;\n  padding: 8px;\n  background-color: #f8f9fa;\n  border-radius: 4px;\n  font-size: 14px;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">const { useState, useRef, useEffect } = React;\n\n// First, define the Web Component\n// First, define the Web Component\nclass SimpleCounter extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n    this._count = 0;\n    this.render();\n  }\n\n  static get observedAttributes() {\n    return [&#39;count&#39;];\n  }\n\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (name === &#39;count&#39;) {\n      this._count = parseInt(newValue || &#39;0&#39;, 10);\n      this.render();\n    }\n  }\n\n  get count() {\n    return this._count;\n  }\n\n  set count(value) {\n    this._count = value;\n    this.render();\n    this.dispatchEvent(new CustomEvent(&#39;counter-changed&#39;, {\n      bubbles: true,\n      composed: true,\n      detail: { count: this._count } }));\n\n  }\n\n  increment() {\n    this._count++;\n    this.render();\n    this.dispatchEvent(new CustomEvent(&#39;counter-changed&#39;, {\n      bubbles: true,\n      composed: true,\n      detail: { count: this._count } }));\n\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          font-family: sans-serif;\n          border: 2px solid #3498db;\n          border-radius: 8px;\n          padding: 16px;\n          margin: 8px 0;\n          background-color: #f8f9fa;\n        }\n        .counter {\n          font-size: 24px;\n          margin-bottom: 12px;\n        }\n        button {\n          background-color: #3498db;\n          color: white;\n          border: none;\n          border-radius: 4px;\n          padding: 8px 16px;\n          cursor: pointer;\n          font-size: 16px;\n          transition: background-color 0.2s;\n        }\n        button:hover {\n          background-color: #2980b9;\n        }\n      &lt;/style&gt;\n      &lt;div class=&quot;counter&quot;&gt;Count: ${this._count}&lt;/div&gt;\n      &lt;button id=&quot;increment&quot;&gt;Increment&lt;/button&gt;\n    `;\n\n    this.shadowRoot.querySelector(&#39;#increment&#39;).addEventListener(&#39;click&#39;, () =&gt; this.increment());\n  }\n\n  disconnectedCallback() {\n    const btn = this.shadowRoot.querySelector(&#39;#increment&#39;);\n    if (btn) {\n      btn.removeEventListener(&#39;click&#39;, () =&gt; this.increment());\n    }\n  }}\n\n\n// Register the web component\nif (!customElements.get(&#39;simple-counter&#39;)) {\n  customElements.define(&#39;simple-counter&#39;, SimpleCounter);\n}\n\n// React Components using React.createElement for CodePen compatibility\nfunction DirectInteractionExample() {\n  const [events, setEvents] = React.useState([]);\n  const counterRef = React.useRef(null);\n\n  React.useEffect(() =&gt; {\n    if (counterRef.current) {\n      // Set initial count\n      counterRef.current.count = 5;\n\n      // Set up event listener\n      const handleCounterChanged = e =&gt; {\n        setEvents(prev =&gt; [...prev, `Counter changed: ${e.detail.count}`]);\n      };\n\n      counterRef.current.addEventListener(&#39;counter-changed&#39;, handleCounterChanged);\n\n      // Clean up\n      return () =&gt; {\n        if (counterRef.current) {\n          counterRef.current.removeEventListener(&#39;counter-changed&#39;, handleCounterChanged);\n        }\n      };\n    }\n  }, []);\n\n  // Match Angular&#39;s button actions\n  const handleIncrement = () =&gt; {\n    if (counterRef.current) {\n      counterRef.current.increment();\n    }\n  };\n\n  const handleDecrement = () =&gt; {\n    if (counterRef.current &amp;&amp; counterRef.current.count &gt; 0) {\n      counterRef.current.count = counterRef.current.count - 1;\n      setEvents(prev =&gt; [...prev, `Counter decremented to: ${counterRef.current.count}`]);\n    }\n  };\n\n  const handleReset = () =&gt; {\n    if (counterRef.current) {\n      counterRef.current.count = 0;\n      setEvents(prev =&gt; [...prev, &#39;Counter reset to 0&#39;]);\n    }\n  };\n\n  const handleDoubleValue = () =&gt; {\n    if (counterRef.current) {\n      const currentValue = counterRef.current.count;\n      counterRef.current.count = currentValue * 2;\n      setEvents(prev =&gt; [...prev, `Doubled value to: ${counterRef.current.count}`]);\n    }\n  };\n\n  return React.createElement(&#39;div&#39;, { className: &#39;example&#39; },\n  React.createElement(&#39;h3&#39;, null, &#39;Using React with Web Components&#39;),\n  React.createElement(&#39;p&#39;, null, &#39;Direct component manipulation using refs&#39;),\n\n  React.createElement(&#39;simple-counter&#39;, { ref: counterRef }),\n\n  React.createElement(&#39;div&#39;, { className: &#39;button-group&#39; },\n  React.createElement(&#39;button&#39;, {\n    onClick: handleIncrement,\n    className: &#39;react-button&#39; },\n  &#39;Increment&#39;),\n\n  React.createElement(&#39;button&#39;, {\n    onClick: handleDecrement,\n    className: &#39;react-button&#39; },\n  &#39;Decrement&#39;),\n\n  React.createElement(&#39;button&#39;, {\n    onClick: handleReset,\n    className: &#39;react-button&#39; },\n  &#39;Reset&#39;)),\n\n\n  React.createElement(&#39;div&#39;, { className: &#39;button-group&#39; },\n  React.createElement(&#39;button&#39;, {\n    onClick: handleDoubleValue,\n    className: &#39;react-button&#39; },\n  &#39;Double Value&#39;)),\n\n\n  React.createElement(&#39;div&#39;, { className: &#39;events&#39; },\n  React.createElement(&#39;strong&#39;, null, &#39;Events:&#39;),\n  events.map((event, i) =&gt; React.createElement(&#39;div&#39;, { key: i }, event))));\n\n\n}\n\n// Main Demo Component\nfunction ReactWebComponentDemo() {\n  return React.createElement(&#39;div&#39;, { className: &#39;demo-container&#39; },\n  React.createElement(&#39;h2&#39;, null, &#39;React + Web Components Integration&#39;),\n  React.createElement(&#39;p&#39;, null,\n  &#39;This example demonstrates how React can interact with Web Components through refs and direct DOM manipulation.&#39;),\n\n\n  React.createElement(DirectInteractionExample));\n\n}\n\n// Render the demo\nReactDOM.render(React.createElement(ReactWebComponentDemo), document.getElementById(&#39;root&#39;));\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/\">the original article</a>.</p></figure>\n<h3 id=\"angular-integration-strategies\">Angular Integration Strategies<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#angular-integration-strategies\" aria-label=\"Link to this section\">#</a></h3>\n<p>Angular has built-in support for Web Components through its <code>@angular/elements</code> package, but deeper integration still requires specific techniques:</p>\n<h4 id=\"1-element-binding-with-custom-directives\">1. Element Binding with Custom Directives<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#1-element-binding-with-custom-directives\" aria-label=\"Link to this section\">#</a></h4>\n<pre><code class=\"language-typescript\">@Directive({\n  selector: 'my-web-component'\n})\nexport class WebComponentDirective implements OnInit, OnChanges, OnDestroy {\n  @Input() complexData: any;\n  @Output() customEvent = new EventEmitter&lt;any&gt;();\n\n  constructor(private el: ElementRef) {}\n\n  ngOnInit() {\n    this.el.nativeElement.addEventListener('custom-event', \n      this.handleCustomEvent.bind(this));\n  }\n\n  ngOnChanges(changes: SimpleChanges) {\n    if (changes.complexData) {\n      this.el.nativeElement.complexData = this.complexData;\n    }\n  }\n\n  ngOnDestroy() {\n    this.el.nativeElement.removeEventListener('custom-event', \n      this.handleCustomEvent.bind(this));\n  }\n\n  private handleCustomEvent(event: CustomEvent) {\n    this.customEvent.emit(event.detail);\n  }\n}\n</code></pre>\n<p><strong>Key Insights:</strong></p>\n<ul>\n<li>\n<p>Angular directives provide clean property and event binding</p>\n</li>\n<li>\n<p>OnChanges lifecycle hook updates properties efficiently</p>\n</li>\n<li>\n<p>ElementRef gives direct access to the native element</p>\n</li>\n</ul>\n<p><strong>Interactive Demo:</strong> Explore Angular integrations</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Angular + Web Components Integration</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div ng-app=&quot;webComponentDemo&quot; ng-controller=&quot;MainController&quot;&gt;\n\t&lt;div class=&quot;demo-container&quot;&gt;\n\t\t&lt;h2&gt;Angular + Web Components Integration&lt;/h2&gt;\n\t\t&lt;p&gt;\n\t\t\tThis example demonstrates how Angular can work with Web Components through directive bindings\n\t\t\tand direct DOM access.\n\t\t&lt;/p&gt;\n\n\t\t&lt;div class=&quot;examples&quot;&gt;\n\t\t\t&lt;div class=&quot;example&quot;&gt;\n\t\t\t\t&lt;h3&gt;Using Angular Directives&lt;/h3&gt;\n\t\t\t\t&lt;p&gt;Binding component state to Angular&#39;s data model&lt;/p&gt;\n\n\t\t\t\t&lt;simple-counter count=&quot;counterValue&quot; on-counter-changed=&quot;handleCounterEvent($event)&quot;&gt;\n\t\t\t\t&lt;/simple-counter&gt;\n\n\t\t\t\t&lt;button class=&quot;angular-button&quot; ng-click=&quot;incrementCounter()&quot;&gt;\n\t\t\t\t\tAngular: Increment Counter\n\t\t\t\t&lt;/button&gt;\n\n\t\t\t\t&lt;div class=&quot;events&quot;&gt;\n\t\t\t\t\t&lt;strong&gt;Current count in Angular: {{ counterValue }}&lt;/strong&gt;\n\t\t\t\t\t&lt;div ng-repeat=&quot;event in events track by $index&quot;&gt;\n\t\t\t\t\t\t{{ event }}\n\t\t\t\t\t&lt;/div&gt;\n\t\t\t\t&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\n\t\t\t&lt;div class=&quot;example&quot;&gt;\n\t\t\t\t&lt;h3&gt;Direct DOM Access&lt;/h3&gt;\n\t\t\t\t&lt;p&gt;Using element references to directly call component methods&lt;/p&gt;\n\n\t\t\t\t&lt;simple-counter id=&quot;direct-counter&quot;&gt;&lt;/simple-counter&gt;\n\n\t\t\t\t&lt;button class=&quot;angular-button&quot; ng-click=&quot;callDirectMethod()&quot;&gt;\n\t\t\t\t\tCall Component Method Directly\n\t\t\t\t&lt;/button&gt;\n\t\t\t&lt;/div&gt;\n\t\t&lt;/div&gt;\n\t&lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">.demo-container {\n\tfont-family: system-ui, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto,\n\t\tOxygen, Ubuntu, Cantarell, &quot;Open Sans&quot;, &quot;Helvetica Neue&quot;, sans-serif;\n\tmax-width: 800px;\n\tmargin: 0 auto;\n\tpadding: 20px;\n}\n\n.examples {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 20px;\n}\n\n.example {\n\tborder: 1px solid #ddd;\n\tborder-radius: 8px;\n\tpadding: 16px;\n\tbackground-color: #fff;\n}\n\nh2 {\n\tcolor: #2c3e50;\n\tmargin-top: 0;\n}\n\nh3 {\n\tcolor: #2c3e50;\n\tmargin-top: 0;\n}\n\n.angular-button {\n\tbackground-color: #dd0031;\n\tcolor: white;\n\tborder: none;\n\tborder-radius: 4px;\n\tpadding: 8px 16px;\n\tmargin-top: 8px;\n\tcursor: pointer;\n\tfont-size: 16px;\n}\n\n.events {\n\tmargin-top: 16px;\n\tpadding: 8px;\n\tbackground-color: #f8f9fa;\n\tborder-radius: 4px;\n\tfont-size: 14px;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Define our Web Component\nclass SimpleCounter extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &quot;open&quot; });\n    this._count = 0;\n    this.render();\n  }\n\n  static get observedAttributes() {\n    return [&quot;count&quot;];\n  }\n\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (name === &quot;count&quot;) {\n      this._count = parseInt(newValue || &quot;0&quot;, 10);\n      this.render();\n    }\n  }\n\n  get count() {\n    return this._count;\n  }\n\n  set count(value) {\n    this._count = value;\n    this.render();\n\n    // When set programmatically, update the attribute to keep in sync\n    this.setAttribute(&quot;count&quot;, value);\n  }\n\n  increment() {\n    this._count++;\n    this.render();\n    this.dispatchEvent(\n    new CustomEvent(&quot;counter-changed&quot;, {\n      bubbles: true,\n      composed: true,\n      detail: { count: this._count } }));\n\n\n\n    // Update the attribute to keep in sync with the property\n    this.setAttribute(&quot;count&quot;, this._count);\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n        &lt;style&gt;\n          :host {\n            display: block;\n            font-family: sans-serif;\n            border: 2px solid #dd0031;\n            border-radius: 8px;\n            padding: 16px;\n            margin: 8px 0;\n            background-color: #f8f9fa;\n          }\n          .counter {\n            font-size: 24px;\n            margin-bottom: 12px;\n          }\n          button {\n            background-color: #dd0031;\n            color: white;\n            border: none;\n            border-radius: 4px;\n            padding: 8px 16px;\n            cursor: pointer;\n            font-size: 16px;\n            transition: background-color 0.2s;\n          }\n          button:hover {\n            background-color: #a50023;\n          }\n        &lt;/style&gt;\n        &lt;div class=&quot;counter&quot;&gt;Count: ${this._count}&lt;/div&gt;\n        &lt;button id=&quot;increment&quot;&gt;Increment from Inside&lt;/button&gt;\n      `;\n\n    this.shadowRoot.\n    querySelector(&quot;#increment&quot;).\n    addEventListener(&quot;click&quot;, () =&gt; this.increment());\n  }\n\n  disconnectedCallback() {\n    const button = this.shadowRoot.querySelector(&quot;#increment&quot;);\n    if (button) {\n      button.removeEventListener(&quot;click&quot;, () =&gt; this.increment());\n    }\n  }}\n\n\n// Register our Web Component\nif (!customElements.get(&quot;simple-counter&quot;)) {\n  customElements.define(&quot;simple-counter&quot;, SimpleCounter);\n}\n\n// Angular app\nangular.\nmodule(&quot;webComponentDemo&quot;, []).\ncontroller(&quot;MainController&quot;, [\n&quot;$scope&quot;,\n&quot;$timeout&quot;,\nfunction ($scope, $timeout) {\n  // Initialize data\n  $scope.counterValue = 5;\n  $scope.events = [];\n\n  // Event handler for counter changes\n  $scope.handleCounterEvent = function (event) {\n    // Use $timeout to ensure Angular knows about the update\n    $timeout(function () {\n      $scope.counterValue = event.detail.count;\n      $scope.events.push(&quot;Counter changed: &quot; + event.detail.count);\n    });\n  };\n\n  // Method to increment the counter from Angular\n  $scope.incrementCounter = function () {\n    $scope.counterValue++;\n    // Angular will update the attribute automatically\n  };\n\n  // Method to call the component directly\n  $scope.callDirectMethod = function () {\n    var counter = document.getElementById(&quot;direct-counter&quot;);\n    if (counter) {\n      counter.increment();\n\n      // Add to the events list\n      $timeout(function () {\n        $scope.events.push(&quot;Called increment() method directly&quot;);\n      });\n    }\n  };\n\n  // Setup event handling for the direct counter\n  $timeout(function () {\n    var counter = document.getElementById(&quot;direct-counter&quot;);\n    if (counter) {\n      // Set initial value\n      counter.count = 10;\n\n      // Listen for events\n      counter.addEventListener(&quot;counter-changed&quot;, function (event) {\n        $timeout(function () {\n          $scope.events.push(&quot;Direct counter changed: &quot; + event.detail.count);\n        });\n      });\n    }\n  });\n\n  // We need to create a directive to bind our Web Component properly\n  // This is done to demonstrate the concept, but in CodePen it&#39;s simpler to\n  // do the binding manually as we&#39;ve done above\n}])\n\n\n// Add a simple directive to handle attribute binding\n.directive(&quot;simpleCounter&quot;, function () {\n  return {\n    restrict: &quot;E&quot;,\n    scope: {\n      count: &quot;=?&quot;, // Two-way binding (optional: the direct counter has no count attribute)\n      onCounterChanged: &quot;&amp;&quot; // Function binding\n    },\n    link: function (scope, element, attrs) {\n      // Watch for changes to the &#39;count&#39; attribute\n      scope.$watch(&quot;count&quot;, function (newValue) {\n        if (newValue !== undefined) {\n          element[0].count = newValue;\n        }\n      });\n\n      // Listen for counter-changed events\n      element[0].addEventListener(&quot;counter-changed&quot;, function (event) {\n        scope.$evalAsync(function () {\n          scope.count = event.detail.count;\n          scope.onCounterChanged({ $event: event });\n        });\n      });\n    } };\n\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/\">the original article</a>.</p></figure>\n<h3 id=\"vue-integration-strategies\">Vue Integration Strategies<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#vue-integration-strategies\" aria-label=\"Link to this section\">#</a></h3>\n<p>Vue offers excellent built-in support for Web Components with its attribute and event binding syntax:</p>\n<pre><code class=\"language-html\">&lt;template&gt;\n  &lt;my-web-component \n    :complex-prop=\"myData\" \n    @custom-event=\"handleEvent\"\n  /&gt;\n&lt;/template&gt;\n\n&lt;script&gt;\nexport default {\n  data() {\n    return {\n      myData: { key: 'value' }\n    }\n  },\n  methods: {\n    handleEvent(event) {\n      console.log(event.detail);\n    }\n  },\n  mounted() {\n    // For cases where direct property access is needed\n    this.$refs.myComponent.directMethod();\n  }\n}\n&lt;/script&gt;\n</code></pre>\n<p><strong>Key Insights:</strong></p>\n<ul>\n<li>\n<p>Vue's <code>:prop</code> syntax works with kebab-case attributes</p>\n</li>\n<li>\n<p>Native event handling with <code>@event-name</code></p>\n</li>\n<li>\n<p>Vue's reactivity system naturally updates Web Component properties</p>\n</li>\n</ul>\n<p><strong>Interactive Demo:</strong> Explore Vue integrations</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Vue + Web Components Integration</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div id=&quot;app&quot;&gt;\n\t&lt;h2&gt;Vue + Web Components Integration&lt;/h2&gt;\n\t&lt;p&gt;\n\t\tVue offers excellent support for Web Components with its built-in directive syntax.\n\t\tThis demo shows how properties and events work.\n\t&lt;/p&gt;\n\n\t&lt;div class=&quot;demo-container&quot;&gt;\n\t\t&lt;div class=&quot;example&quot;&gt;\n\t\t\t&lt;h3&gt;Basic Integration&lt;/h3&gt;\n\t\t\t&lt;p&gt;Vue&#39;s declarative syntax makes Web Component integration straightforward&lt;/p&gt;\n\n\t\t\t&lt;simple-counter :count=&quot;counterData&quot; @counter-changed=&quot;handleCounterEvent&quot;&gt;&lt;/simple-counter&gt;\n\n\t\t\t&lt;button class=&quot;vue-button&quot; @click=&quot;incrementCounter&quot;&gt;\n\t\t\t\tIncrement from Vue\n\t\t\t&lt;/button&gt;\n\n\t\t\t&lt;div class=&quot;events&quot;&gt;\n\t\t\t\t&lt;strong&gt;Current count in Vue data: {{ counterData }}&lt;/strong&gt;\n\t\t\t\t&lt;div v-for=&quot;(event, index) in events&quot; :key=&quot;index&quot;&gt;\n\t\t\t\t\t{{ event }}\n\t\t\t\t&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\t\t&lt;/div&gt;\n\n\t\t&lt;div class=&quot;example&quot;&gt;\n\t\t\t&lt;h3&gt;Using $refs for Direct Access&lt;/h3&gt;\n\t\t\t&lt;p&gt;When needed, Vue can access Web Components directly with $refs&lt;/p&gt;\n\n\t\t\t&lt;simple-counter ref=&quot;directCounter&quot;&gt;&lt;/simple-counter&gt;\n\n\t\t\t&lt;button class=&quot;vue-button&quot; @click=&quot;callComponentMethod&quot;&gt;\n\t\t\t\tCall component method directly\n\t\t\t&lt;/button&gt;\n\t\t&lt;/div&gt;\n\t&lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body {\n\tfont-family: system-ui, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto,\n\t\tOxygen, Ubuntu, Cantarell, &quot;Open Sans&quot;, &quot;Helvetica Neue&quot;, sans-serif;\n\tmax-width: 800px;\n\tmargin: 0 auto;\n\tpadding: 20px;\n}\n\n.demo-container {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 20px;\n}\n\n.example {\n\tborder: 1px solid #ddd;\n\tborder-radius: 8px;\n\tpadding: 16px;\n\tbackground-color: #fff;\n}\n\nh2 {\n\tcolor: #2c3e50;\n\tmargin-top: 0;\n}\n\nh3 {\n\tcolor: #2c3e50;\n\tmargin-top: 0;\n}\n\n.vue-button {\n\tbackground-color: #42b883;\n\tcolor: white;\n\tborder: none;\n\tborder-radius: 4px;\n\tpadding: 8px 16px;\n\tmargin-top: 8px;\n\tcursor: pointer;\n\tfont-size: 16px;\n}\n\n.events {\n\tmargin-top: 16px;\n\tpadding: 8px;\n\tbackground-color: #f8f9fa;\n\tborder-radius: 4px;\n\tfont-size: 14px;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Define Web Component\nclass SimpleCounter extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &quot;open&quot; });\n    this._count = 0;\n    this.render();\n  }\n\n  static get observedAttributes() {\n    return [&quot;count&quot;];\n  }\n\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (name === &quot;count&quot;) {\n      this._count = parseInt(newValue || &quot;0&quot;, 10);\n      this.render();\n    }\n  }\n\n  get count() {\n    return this._count;\n  }\n\n  set count(value) {\n    this._count = value;\n    this.render();\n  }\n\n  increment() {\n    this._count++;\n    this.render();\n    this.dispatchEvent(\n    new CustomEvent(&quot;counter-changed&quot;, {\n      bubbles: true,\n      composed: true,\n      detail: { count: this._count } }));\n\n\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n          &lt;style&gt;\n            :host {\n              display: block;\n              font-family: sans-serif;\n              border: 2px solid #42b883;\n              border-radius: 8px;\n              padding: 16px;\n              margin: 8px 0;\n              background-color: #f8f9fa;\n            }\n            .counter {\n              font-size: 24px;\n              margin-bottom: 12px;\n            }\n            button {\n              background-color: #42b883;\n              color: white;\n              border: none;\n              border-radius: 4px;\n              padding: 8px 16px;\n              cursor: pointer;\n              font-size: 16px;\n              transition: background-color 0.2s;\n            }\n            button:hover {\n              background-color: #33a06f;\n            }\n          &lt;/style&gt;\n          &lt;div class=&quot;counter&quot;&gt;Count: ${this._count}&lt;/div&gt;\n          &lt;button id=&quot;increment&quot;&gt;Increment&lt;/button&gt;\n        `;\n\n    this.shadowRoot.\n    querySelector(&quot;#increment&quot;).\n    addEventListener(&quot;click&quot;, () =&gt; this.increment());\n  }\n\n  disconnectedCallback() {\n    const button = this.shadowRoot.querySelector(&quot;#increment&quot;);\n    if (button) {\n      button.removeEventListener(&quot;click&quot;, () =&gt; this.increment());\n    }\n  }}\n\n\n// Register the Web Component\nif (!customElements.get(&quot;simple-counter&quot;)) {\n  customElements.define(&quot;simple-counter&quot;, SimpleCounter);\n}\n\n// Vue App\nconst app = Vue.createApp({\n  data() {\n    return {\n      counterData: 5,\n      events: [] };\n\n  },\n  methods: {\n    handleCounterEvent(event) {\n      this.counterData = event.detail.count;\n      this.events.push(`Counter changed: ${event.detail.count}`);\n    },\n    incrementCounter() {\n      this.counterData++;\n      this.$nextTick(() =&gt; {\n        // Vue&#39;s reactivity will update the attribute on the Web Component\n      });\n    },\n    callComponentMethod() {\n      // Direct method call via ref\n      this.$refs.directCounter.increment();\n      this.events.push(&quot;Called increment() method directly&quot;);\n    } },\n\n  mounted() {\n    // Initial setup if needed\n    this.$refs.directCounter.count = 10;\n  } });\n\n\napp.mount(&quot;#app&quot;);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/\">the original article</a>.</p></figure>\n<h3 id=\"svelte-integration-strategies\">Svelte Integration Strategies<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#svelte-integration-strategies\" aria-label=\"Link to this section\">#</a></h3>\n<p>Svelte treats Web Components as first-class citizens and makes integration particularly straightforward:</p>\n<pre><code class=\"language-html\">&lt;script&gt;\n  import { onMount } from 'svelte';\n\n  let el;\n  let myData = { key: 'value' };\n\n  // Reactive assignments automatically update the Web Component\n  $: if (el) {\n    el.complexData = myData;\n  }\n\n  function handleEvent(event) {\n    console.log(event.detail);\n  }\n\n  onMount(() =&gt; {\n    // If needed, do direct manipulation here\n    el.addEventListener('special-event', specialHandler);\n\n    return () =&gt; {\n      el.removeEventListener('special-event', specialHandler);\n    }\n  });\n&lt;/script&gt;\n\n&lt;my-web-component \n  bind:this={el} \n  on:custom-event={handleEvent}\n/&gt;\n</code></pre>\n<p><strong>Key Insights:</strong></p>\n<ul>\n<li>\n<p><code>bind:this</code> provides direct access to the element</p>\n</li>\n<li>\n<p>Svelte's reactivity automatically updates properties</p>\n</li>\n<li>\n<p>Svelte's event handling works natively with Web Component events</p>\n</li>\n</ul>\n<p><strong>Interactive Demo:</strong> Explore Svelte integration Techniques</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Svelte + Web Components Integration</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;!-- The Svelte component below (App) is compiled in your browser and mounted here. --&gt;\n&lt;div id=&quot;app&quot;&gt;&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body {\n  font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, Oxygen, Ubuntu, Cantarell, &#39;Open Sans&#39;, &#39;Helvetica Neue&#39;, sans-serif;\n  max-width: 800px;\n  margin: 0 auto;\n  padding: 20px;\n  background-color: #f8f9fa;\n}\n\n.demo-container {\n  margin: 20px 0;\n}\n\n.example {\n  border: 1px solid #ddd;\n  border-radius: 8px;\n  padding: 16px;\n  margin-bottom: 20px;\n  background-color: #fff;\n  box-shadow: 0 2px 4px rgba(0,0,0,0.05);\n}\n\nh2,\nh3 {\n  color: #2c3e50;\n  margin-top: 0;\n}\n\n.button-group {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n  margin-top: 8px;\n}\n\n.component-button,\n.svelte-button {\n  color: white;\n  border: none;\n  border-radius: 4px;\n  padding: 8px 16px;\n  cursor: pointer;\n  font-size: 16px;\n  transition: background-color 0.2s;\n}\n\n.component-button { background-color: #3498db; }\n.component-button:hover { background-color: #2980b9; }\n.svelte-button { background-color: #ff3e00; }\n.svelte-button:hover { background-color: #d03400; }\n\n.events {\n  margin-top: 16px;\n  padding: 12px;\n  background-color: #f8f9fa;\n  border-radius: 4px;\n  font-size: 14px;\n  border: 1px solid #e9ecef;\n  max-height: 200px;\n  overflow-y: auto;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// 1. A framework-agnostic Web Component: a counter with a `count` property,\n//    an `increment()` method, and a `counter-changed` event.\nclass SimpleCounter extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n    this._count = 0;\n    this.render();\n  }\n  static get observedAttributes() {\n    return [&#39;count&#39;];\n  }\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (name === &#39;count&#39;) this.count = parseInt(newValue || &#39;0&#39;, 10);\n  }\n  get count() {\n    return this._count;\n  }\n  set count(value) {\n    if (value === this._count) return;\n    this._count = value;\n    this.render();\n    this.dispatchEvent(new CustomEvent(&#39;counter-changed&#39;, {\n      bubbles: true,\n      composed: true,\n      detail: { count: this._count }\n    }));\n  }\n  increment() {\n    this.count = this._count + 1;\n  }\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          font-family: sans-serif;\n          border: 2px solid #ff3e00;\n          border-radius: 8px;\n          padding: 16px;\n          margin: 8px 0;\n          background-color: #f8f9fa;\n        }\n        .counter { font-size: 24px; margin-bottom: 12px; }\n        button {\n          background-color: #ff3e00;\n          color: white;\n          border: none;\n          border-radius: 4px;\n          padding: 8px 16px;\n          cursor: pointer;\n          font-size: 16px;\n        }\n        button:hover { background-color: #d03400; }\n      &lt;/style&gt;\n      &lt;div class=&quot;counter&quot;&gt;Count: ${this._count}&lt;/div&gt;\n      &lt;button id=&quot;increment&quot;&gt;Increment from Inside&lt;/button&gt;\n    `;\n    this.shadowRoot.querySelector(&#39;#increment&#39;).addEventListener(&#39;click&#39;, () =&gt; this.increment());\n  }\n}\nif (!customElements.get(&#39;simple-counter&#39;)) {\n  customElements.define(&#39;simple-counter&#39;, SimpleCounter);\n}\n\n// 2. A real Svelte component that uses it: bind:this for direct access,\n//    a reactive statement to push store values in, and on:counter-changed to listen.\nconst App = `\n&lt;script&gt;\n  import { onMount } from &#39;svelte&#39;;\n  import { writable } from &#39;svelte/store&#39;;\n\n  const count = writable(5);\n  const events = writable([]);\n  let counter;\n\n  // Reactive statement: whenever $count changes, update the Web Component\n  $: if (counter &amp;&amp; counter.count !== $count) {\n    counter.count = $count;\n  }\n\n  function log(message) {\n    events.update((list) =&gt; [message, ...list].slice(0, 8));\n  }\n\n  // The Web Component&#39;s own event, handled with Svelte&#39;s on: directive\n  function handleCounterChanged(event) {\n    count.set(event.detail.count);\n    log(&#39;counter-changed: &#39; + event.detail.count);\n  }\n\n  onMount(() =&gt; {\n    log(&#39;Mounted; bind:this gave us &lt;&#39; + counter.localName + &#39;&gt;&#39;);\n  });\n&lt;/script&gt;\n\n&lt;div class=&quot;demo-container&quot;&gt;\n  &lt;h2&gt;Svelte + Web Components Integration&lt;/h2&gt;\n  &lt;p&gt;A real Svelte component driving a Web Component with bind:this, a store, and its events&lt;/p&gt;\n\n  &lt;div class=&quot;example&quot;&gt;\n    &lt;h3&gt;Dual Approach Integration&lt;/h3&gt;\n    &lt;p&gt;Compare calling the Web Component&#39;s own method with updating a Svelte store&lt;/p&gt;\n\n    &lt;simple-counter bind:this={counter} on:counter-changed={handleCounterChanged}&gt;&lt;/simple-counter&gt;\n\n    &lt;div class=&quot;button-group&quot;&gt;\n      &lt;button class=&quot;component-button&quot; on:click={() =&gt; counter.increment()}&gt;Increment (Component)&lt;/button&gt;\n      &lt;button class=&quot;svelte-button&quot; on:click={() =&gt; count.update((n) =&gt; n + 1)}&gt;Increment (Svelte Store)&lt;/button&gt;\n    &lt;/div&gt;\n\n    &lt;div class=&quot;button-group&quot;&gt;\n      &lt;button class=&quot;component-button&quot; on:click={() =&gt; count.update((n) =&gt; Math.max(0, n - 1))}&gt;Decrement&lt;/button&gt;\n      &lt;button class=&quot;component-button&quot; on:click={() =&gt; count.set(0)}&gt;Reset&lt;/button&gt;\n      &lt;button class=&quot;component-button&quot; on:click={() =&gt; count.update((n) =&gt; n * 2)}&gt;Double Value&lt;/button&gt;\n    &lt;/div&gt;\n\n    &lt;div class=&quot;events&quot;&gt;\n      &lt;strong&gt;Current count in the Svelte store: {$count}&lt;/strong&gt;\n      {#each $events as event, i (i)}\n        &lt;div&gt;{event}&lt;/div&gt;\n      {/each}\n    &lt;/div&gt;\n  &lt;/div&gt;\n&lt;/div&gt;\n`;\n\n// 3. Compile the component in the browser (as the Svelte REPL does) and mount it.\n//    `svelte` is the compiler; `SvelteRuntime` holds the modules compiled code imports.\nfunction load(source) {\n  const { js } = svelte.compile(source, { name: &#39;App&#39;, generate: &#39;client&#39; });\n  const code = js.code\n    .replace(/^import\\s+[&#39;&quot;][^&#39;&quot;]+[&#39;&quot;];?$/gm, &#39;&#39;)\n    .replace(/^import\\s+\\*\\s+as\\s+([\\w$]+)\\s+from\\s+[&#39;&quot;]([^&#39;&quot;]+)[&#39;&quot;];?$/gm, &#39;const $1 = runtime(&quot;$2&quot;);&#39;)\n    .replace(/^import\\s+\\{([^}]*)\\}\\s+from\\s+[&#39;&quot;]([^&#39;&quot;]+)[&#39;&quot;];?$/gm,\n      (_, names, from) =&gt; `const {${names.replace(/\\s+as\\s+/g, &#39;: &#39;)}} = runtime(&quot;${from}&quot;);`)\n    .replace(/^export default /m, &#39;const Component = &#39;);\n  const runtime = (name) =&gt; {\n    if (!(name in SvelteRuntime.modules)) throw new Error(`This sandbox can only import Svelte&#39;s own modules, not &quot;${name}&quot;.`);\n    return SvelteRuntime.modules[name];\n  };\n  return new Function(&#39;runtime&#39;, `${code}\\nreturn Component;`)(runtime);\n}\n\ntry {\n  const { mount } = SvelteRuntime.modules.svelte;\n  mount(load(App), { target: document.getElementById(&#39;app&#39;) });\n  console.log(`Compiled and mounted with Svelte ${svelte.VERSION}`);\n} catch (error) {\n  console.error(error.message);\n}\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/\">the original article</a>.</p></figure>\n<h2 id=\"performance-optimization-techniques\">Performance Optimization Techniques<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#performance-optimization-techniques\" aria-label=\"Link to this section\">#</a></h2>\n<h3 id=\"lazy-loading-strategies\">Lazy-Loading Strategies<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#lazy-loading-strategies\" aria-label=\"Link to this section\">#</a></h3>\n<p>Loading Web Components only when needed can significantly improve initial page load performance:</p>\n<h4 id=\"dynamic-import-pattern\">Dynamic Import Pattern<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#dynamic-import-pattern\" aria-label=\"Link to this section\">#</a></h4>\n<pre><code class=\"language-javascript\">// Only load when needed\nconst loadComponent = async () =&gt; {\n  const { MyComponent } = await import('./my-component.js');\n  customElements.define('my-component', MyComponent);\n}\n\n// Call on demand or when component is about to be needed\nbutton.addEventListener('click', loadComponent);\n</code></pre>\n<h4 id=\"intersection-observer-pattern\">Intersection Observer Pattern<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#intersection-observer-pattern\" aria-label=\"Link to this section\">#</a></h4>\n<pre><code class=\"language-javascript\">// Setup observer to load component when scrolled into view\nconst observer = new IntersectionObserver((entries) =&gt; {\n  entries.forEach(entry =&gt; {\n    if (entry.isIntersecting) {\n      import('./components/lazy-component.js')\n        .then(() =&gt; {\n          // Component is now registered\n          observer.unobserve(entry.target);\n        });\n    }\n  });\n}, { rootMargin: '100px' });\n\n// Observe placeholder elements\ndocument.querySelectorAll('.component-placeholder')\n  .forEach(el =&gt; observer.observe(el));\n</code></pre>\n<p><strong>Interactive Demo:</strong> See lazy-loading in action</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Lazy-Loading Web Components</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div class=&quot;demo-container&quot;&gt;\n  &lt;h1&gt;Lazy-Loading Web Components&lt;/h1&gt;\n  &lt;p&gt;This demo shows how lazy-loading can improve performance by loading components only when needed.&lt;/p&gt;\n  \n  &lt;div class=&quot;performance-comparison&quot;&gt;\n    &lt;div class=&quot;tabs&quot;&gt;\n      &lt;button class=&quot;tab active&quot; data-tab=&quot;eager&quot;&gt;Eager Loading&lt;/button&gt;\n      &lt;button class=&quot;tab&quot; data-tab=&quot;dynamic&quot;&gt;Dynamic Import&lt;/button&gt;\n      &lt;button class=&quot;tab&quot; data-tab=&quot;intersection&quot;&gt;Intersection Observer&lt;/button&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;tab-content&quot; id=&quot;eager-tab&quot;&gt;\n      &lt;h3&gt;Eager Loading (All At Once)&lt;/h3&gt;\n      &lt;p&gt;All components are loaded immediately, which can slow down initial page load.&lt;/p&gt;\n      \n      &lt;div class=&quot;metrics&quot;&gt;\n        &lt;div class=&quot;metric&quot;&gt;\n          &lt;div class=&quot;metric-title&quot;&gt;Components Loaded:&lt;/div&gt;\n          &lt;div class=&quot;metric-value&quot; id=&quot;eager-count&quot;&gt;0&lt;/div&gt;\n        &lt;/div&gt;\n        &lt;div class=&quot;metric&quot;&gt;\n          &lt;div class=&quot;metric-title&quot;&gt;Initial Load Time:&lt;/div&gt;\n          &lt;div class=&quot;metric-value&quot; id=&quot;eager-time&quot;&gt;-&lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n      \n      &lt;button class=&quot;demo-button&quot; id=&quot;load-eager&quot;&gt;Load 10 Eager Components&lt;/button&gt;\n      &lt;div class=&quot;component-container&quot; id=&quot;eager-components&quot;&gt;&lt;/div&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;tab-content hidden&quot; id=&quot;dynamic-tab&quot;&gt;\n      &lt;h3&gt;Dynamic Import (On Demand)&lt;/h3&gt;\n      &lt;p&gt;Components are loaded on-click, improving initial page load but requiring user action.&lt;/p&gt;\n      \n      &lt;div class=&quot;metrics&quot;&gt;\n        &lt;div class=&quot;metric&quot;&gt;\n          &lt;div class=&quot;metric-title&quot;&gt;Components Loaded:&lt;/div&gt;\n          &lt;div class=&quot;metric-value&quot; id=&quot;dynamic-count&quot;&gt;0&lt;/div&gt;\n        &lt;/div&gt;\n        &lt;div class=&quot;metric&quot;&gt;\n          &lt;div class=&quot;metric-title&quot;&gt;Load Time:&lt;/div&gt;\n          &lt;div class=&quot;metric-value&quot; id=&quot;dynamic-time&quot;&gt;-&lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n      \n      &lt;button class=&quot;demo-button&quot; id=&quot;load-dynamic&quot;&gt;Load Components On-Demand&lt;/button&gt;\n      &lt;div class=&quot;component-container&quot; id=&quot;dynamic-components&quot;&gt;&lt;/div&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;tab-content hidden&quot; id=&quot;intersection-tab&quot;&gt;\n      &lt;h3&gt;Intersection Observer (Viewport-Based)&lt;/h3&gt;\n      &lt;p&gt;Components load automatically when scrolled into view - the best of both worlds.&lt;/p&gt;\n      \n      &lt;div class=&quot;metrics&quot;&gt;\n        &lt;div class=&quot;metric&quot;&gt;\n          &lt;div class=&quot;metric-title&quot;&gt;Components Visible:&lt;/div&gt;\n          &lt;div class=&quot;metric-value&quot; id=&quot;intersection-count&quot;&gt;0&lt;/div&gt;\n        &lt;/div&gt;\n        &lt;div class=&quot;metric&quot;&gt;\n          &lt;div class=&quot;metric-title&quot;&gt;Load Time per Component:&lt;/div&gt;\n          &lt;div class=&quot;metric-value&quot; id=&quot;intersection-time&quot;&gt;-&lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n      \n      &lt;p&gt;Scroll down to see components load as they enter the viewport:&lt;/p&gt;\n      &lt;div class=&quot;component-container&quot; id=&quot;intersection-components&quot;&gt;\n        &lt;div class=&quot;component-placeholder&quot; data-index=&quot;1&quot;&gt;Component will load when visible&lt;/div&gt;\n        &lt;div class=&quot;component-placeholder&quot; data-index=&quot;2&quot;&gt;Component will load when visible&lt;/div&gt;\n        &lt;div class=&quot;component-placeholder&quot; data-index=&quot;3&quot;&gt;Component will load when visible&lt;/div&gt;\n        &lt;div class=&quot;component-placeholder&quot; data-index=&quot;4&quot;&gt;Component will load when visible&lt;/div&gt;\n        &lt;div class=&quot;component-placeholder&quot; data-index=&quot;5&quot;&gt;Component will load when visible&lt;/div&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;code-examples&quot;&gt;\n    &lt;h2&gt;Implementation Examples&lt;/h2&gt;\n    \n    &lt;div class=&quot;code-block&quot;&gt;\n      &lt;h3&gt;Dynamic Import Pattern&lt;/h3&gt;\n      &lt;pre&gt;&lt;code&gt;// Only load when the user takes an action\nconst loadComponent = async () =&gt; {\n  const startTime = performance.now();\n  \n  // Dynamic import of the component module\n  const { MyComponent } = await import(&#39;./my-component.js&#39;);\n  \n  // Register the component only when needed\n  customElements.define(&#39;my-component&#39;, MyComponent);\n  \n  // Create and add the component to the page\n  const component = document.createElement(&#39;my-component&#39;);\n  document.querySelector(&#39;#container&#39;).appendChild(component);\n  \n  const loadTime = performance.now() - startTime;\n  console.log(`Component loaded in ${loadTime}ms`);\n}&lt;/code&gt;&lt;/pre&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;code-block&quot;&gt;\n      &lt;h3&gt;Intersection Observer Pattern&lt;/h3&gt;\n      &lt;pre&gt;&lt;code&gt;// Create observer for viewport detection\nconst observer = new IntersectionObserver((entries) =&gt; {\n  entries.forEach(entry =&gt; {\n    if (entry.isIntersecting) {\n      const placeholder = entry.target;\n      \n      // Load component when placeholder is visible\n      import(&#39;./lazy-component.js&#39;)\n        .then(module =&gt; {\n          // Replace placeholder with actual component\n          const component = document.createElement(&#39;lazy-component&#39;);\n          placeholder.parentNode.replaceChild(component, placeholder);\n          \n          // Stop observing this element\n          observer.unobserve(placeholder);\n        });\n    }\n  });\n}, { rootMargin: &#39;100px&#39; });\n\n// Start observing placeholders\ndocument.querySelectorAll(&#39;.component-placeholder&#39;)\n  .forEach(el =&gt; observer.observe(el));&lt;/code&gt;&lt;/pre&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body {\n  font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, Oxygen, Ubuntu, Cantarell, &#39;Open Sans&#39;, &#39;Helvetica Neue&#39;, sans-serif;\n  max-width: 900px;\n  margin: 0 auto;\n  padding: 20px;\n  line-height: 1.6;\n  color: #333;\n}\n\nh1, h2, h3 {\n  color: #2c3e50;\n}\n\n.demo-container {\n  margin: 20px 0;\n}\n\n.performance-comparison {\n  border: 1px solid #ddd;\n  border-radius: 8px;\n  overflow: hidden;\n  margin: 30px 0;\n}\n\n.tabs {\n  display: flex;\n  background-color: #f0f0f0;\n  border-bottom: 1px solid #ddd;\n}\n\n.tab {\n  padding: 12px 20px;\n  background: none;\n  border: none;\n  cursor: pointer;\n  font-size: 16px;\n  font-weight: 500;\n  color: #666;\n  transition: background-color 0.2s;\n}\n\n.tab:hover {\n  background-color: #e0e0e0;\n}\n\n.tab.active {\n  background-color: white;\n  color: #2c3e50;\n  border-bottom: 3px solid #3498db;\n}\n\n.tab-content {\n  padding: 20px;\n  background-color: white;\n}\n\n.hidden {\n  display: none;\n}\n\n.metrics {\n  display: flex;\n  gap: 20px;\n  margin: 20px 0;\n  padding: 15px;\n  background-color: #f8f9fa;\n  border-radius: 8px;\n}\n\n.metric {\n  flex: 1;\n}\n\n.metric-title {\n  font-weight: bold;\n  margin-bottom: 5px;\n  color: #666;\n}\n\n.metric-value {\n  font-size: 24px;\n  color: #3498db;\n}\n\n.demo-button {\n  background-color: #3498db;\n  color: white;\n  border: none;\n  border-radius: 4px;\n  padding: 10px 20px;\n  font-size: 16px;\n  cursor: pointer;\n  transition: background-color 0.2s;\n}\n\n.demo-button:hover {\n  background-color: #2980b9;\n}\n\n.component-container {\n  margin-top: 20px;\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\n  gap: 15px;\n}\n\n.component-placeholder {\n  height: 150px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  text-align: center;\n  border: 2px dashed #ccc;\n  border-radius: 8px;\n  background-color: #f5f5f5;\n  color: #777;\n  padding: 10px;\n}\n\n.code-examples {\n  margin-top: 40px;\n}\n\n.code-block {\n  margin: 20px 0;\n}\n\n.code-block pre {\n  background-color: #f5f5f5;\n  padding: 15px;\n  border-radius: 8px;\n  overflow-x: auto;\n  border: 1px solid #ddd;\n}\n\n.code-block code {\n  font-family: &#39;Consolas&#39;, &#39;Monaco&#39;, &#39;Courier New&#39;, monospace;\n  font-size: 14px;\n  line-height: 1.5;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// HTML\n\n\n\n// Define fancy component to be loaded\nclass FancyComponent extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n    this._rendered = false;\n    this._index = this.getAttribute(&#39;data-index&#39;) || &#39;?&#39;;\n    this._loadTime = this.getAttribute(&#39;data-load-time&#39;) || &#39;0&#39;;\n    this.render();\n  }\n\n  connectedCallback() {\n    if (!this._rendered) {\n      this.render();\n    }\n\n    // Simulate real-world component initialization work\n    this.simulateWork();\n  }\n\n  simulateWork() {\n    // CPU-intensive operation to simulate real component work\n    let result = 0;\n    for (let i = 0; i &lt; 100000; i++) {\n      result += Math.sqrt(i) * Math.random();\n    }\n\n    this.shadowRoot.querySelector(&#39;.value&#39;).textContent = result.toFixed(0);\n  }\n\n  render() {\n    this._rendered = true;\n\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          border-radius: 8px;\n          overflow: hidden;\n          box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n          background-color: white;\n          transition: transform 0.2s;\n        }\n        \n        :host(:hover) {\n          transform: translateY(-5px);\n        }\n        \n        .header {\n          background-color: #3498db;\n          color: white;\n          padding: 12px;\n          font-weight: bold;\n          text-align: center;\n        }\n        \n        .body {\n          padding: 15px;\n          text-align: center;\n        }\n        \n        .info {\n          display: flex;\n          justify-content: space-between;\n          margin-top: 15px;\n          font-size: 12px;\n          color: #777;\n        }\n        \n        .value {\n          font-size: 24px;\n          color: #2c3e50;\n          margin: 10px 0;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;header&quot;&gt;Component #${this._index}&lt;/div&gt;\n      &lt;div class=&quot;body&quot;&gt;\n        &lt;div&gt;Calculated Value:&lt;/div&gt;\n        &lt;div class=&quot;value&quot;&gt;Calculating...&lt;/div&gt;\n        &lt;div class=&quot;info&quot;&gt;\n          &lt;span&gt;Load time: ${this._loadTime}ms&lt;/span&gt;\n          &lt;span&gt;Memory: ~${(Math.random() * 5 + 1).toFixed(1)}MB&lt;/span&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n  }}\n\n\n// Initialize tabs\nconst tabs = document.querySelectorAll(&#39;.tab&#39;);\ntabs.forEach(tab =&gt; {\n  tab.addEventListener(&#39;click&#39;, () =&gt; {\n    // Deactivate all tabs\n    tabs.forEach(t =&gt; t.classList.remove(&#39;active&#39;));\n\n    // Hide all content\n    document.querySelectorAll(&#39;.tab-content&#39;).forEach(content =&gt; {\n      content.classList.add(&#39;hidden&#39;);\n    });\n\n    // Activate clicked tab\n    tab.classList.add(&#39;active&#39;);\n\n    // Show corresponding content\n    const tabId = tab.getAttribute(&#39;data-tab&#39;);\n    document.getElementById(`${tabId}-tab`).classList.remove(&#39;hidden&#39;);\n  });\n});\n\n// Register the component for eager loading\ncustomElements.define(&#39;fancy-component&#39;, FancyComponent);\n\n// Eager loading demo\ndocument.getElementById(&#39;load-eager&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n  const container = document.getElementById(&#39;eager-components&#39;);\n  container.innerHTML = &#39;&#39;;\n\n  const startTime = performance.now();\n\n  // Create 10 components at once\n  for (let i = 1; i &lt;= 10; i++) {\n    const component = document.createElement(&#39;fancy-component&#39;);\n    component.setAttribute(&#39;data-index&#39;, i);\n    container.appendChild(component);\n  }\n\n  const totalTime = performance.now() - startTime;\n\n  // Update metrics\n  document.getElementById(&#39;eager-count&#39;).textContent = &#39;10&#39;;\n  document.getElementById(&#39;eager-time&#39;).textContent = `${totalTime.toFixed(1)}ms`;\n});\n\n// Dynamic import demo\ndocument.getElementById(&#39;load-dynamic&#39;).addEventListener(&#39;click&#39;, async () =&gt; {\n  const container = document.getElementById(&#39;dynamic-components&#39;);\n  container.innerHTML = &#39;&#39;;\n\n  const startTime = performance.now();\n\n  // Load components one by one with a small delay to better see the process\n  for (let i = 1; i &lt;= 5; i++) {\n    const componentStartTime = performance.now();\n\n    // Simulate dynamic import (we already have the component)\n    await new Promise(resolve =&gt; setTimeout(resolve, 200));\n\n    const component = document.createElement(&#39;fancy-component&#39;);\n    component.setAttribute(&#39;data-index&#39;, i);\n    component.setAttribute(&#39;data-load-time&#39;, (performance.now() - componentStartTime).toFixed(1));\n    container.appendChild(component);\n\n    // Update count\n    document.getElementById(&#39;dynamic-count&#39;).textContent = i;\n  }\n\n  const totalTime = performance.now() - startTime;\n  document.getElementById(&#39;dynamic-time&#39;).textContent = `${totalTime.toFixed(1)}ms`;\n});\n\n// Intersection Observer demo\nconst observerOptions = {\n  rootMargin: &#39;0px&#39;,\n  threshold: 0.1 };\n\n\nconst componentObserver = new IntersectionObserver(entries =&gt; {\n  entries.forEach(entry =&gt; {\n    if (entry.isIntersecting) {\n      const placeholder = entry.target;\n      const index = placeholder.getAttribute(&#39;data-index&#39;);\n\n      // Measure loading time\n      const startTime = performance.now();\n\n      // Simulate dynamic import\n      setTimeout(() =&gt; {\n        const component = document.createElement(&#39;fancy-component&#39;);\n        component.setAttribute(&#39;data-index&#39;, index);\n\n        const loadTime = performance.now() - startTime;\n        component.setAttribute(&#39;data-load-time&#39;, loadTime.toFixed(1));\n\n        // Replace placeholder with actual component\n        placeholder.parentNode.replaceChild(component, placeholder);\n\n        // Update metrics\n        const countEl = document.getElementById(&#39;intersection-count&#39;);\n        countEl.textContent = parseInt(countEl.textContent || &#39;0&#39;) + 1;\n\n        document.getElementById(&#39;intersection-time&#39;).textContent = `${loadTime.toFixed(1)}ms`;\n      }, 300); // Simulated import delay\n\n      // Stop observing this element\n      componentObserver.unobserve(placeholder);\n    }\n  });\n}, observerOptions);\n\n// Start observing placeholders\ndocument.querySelectorAll(&#39;.component-placeholder&#39;).forEach(placeholder =&gt; {\n  componentObserver.observe(placeholder);\n});\n\n// Initialize counters\ndocument.getElementById(&#39;eager-count&#39;).textContent = &#39;0&#39;;\ndocument.getElementById(&#39;dynamic-count&#39;).textContent = &#39;0&#39;;\ndocument.getElementById(&#39;intersection-count&#39;).textContent = &#39;0&#39;;\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/\">the original article</a>.</p></figure>\n<h3 id=\"shadow-dom-performance\">Shadow DOM Performance<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#shadow-dom-performance\" aria-label=\"Link to this section\">#</a></h3>\n<p>The Shadow DOM provides encapsulation but comes with performance considerations:</p>\n<ol>\n<li>\n<p><strong>Containment for Performance</strong></p>\n<pre><code class=\"language-css\"> :host {\n   contain: content; /* Limit style recalculation scope */\n   container-type: inline-size; /* For container queries */\n }\n</code></pre>\n</li>\n<li>\n<p><strong>Minimizing Selectors</strong></p>\n<pre><code class=\"language-css\"> /* Avoid overly complex selectors that cross shadow boundaries */\n :host ::slotted(*) &gt; * { /* Expensive */ }\n\n /* Better to use direct, simple selectors */\n .my-item { /* More efficient */ }\n</code></pre>\n</li>\n<li>\n<p><strong>Slot Change Optimization</strong></p>\n<pre><code class=\"language-javascript\"> // Listen only when needed\n constructor() {\n   super();\n   this.attachShadow({ mode: 'open' }).innerHTML = '&lt;slot&gt;&lt;/slot&gt;';\n   this.shadowRoot.querySelector('slot')\n     .addEventListener('slotchange', this._handleSlotChange);\n }\n\n // Debounce frequent updates\n _handleSlotChange = debounce(() =&gt; {\n   this._processSlottedChildren();\n }, 100);\n</code></pre>\n</li>\n</ol>\n<h3 id=\"render-optimization\">Render Optimization<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#render-optimization\" aria-label=\"Link to this section\">#</a></h3>\n<p>Efficient rendering patterns can dramatically improve performance:</p>\n<ol>\n<li>\n<p><strong>Batched DOM Updates</strong></p>\n<pre><code class=\"language-javascript\"> // Poor: Multiple separate DOM operations\n this.shadowRoot.querySelector('.title').textContent = title;\n this.shadowRoot.querySelector('.desc').textContent = description;\n\n // Better: Batch DOM operations with DocumentFragment\n update(data) {\n   const fragment = document.createDocumentFragment();\n   // Build complete update in memory\n   const title = document.createElement('div');\n   title.className = 'title';\n   title.textContent = data.title;\n   fragment.appendChild(title);\n   // etc...\n\n   // Single DOM operation\n   const container = this.shadowRoot.querySelector('.container');\n   container.innerHTML = '';\n   container.appendChild(fragment);\n }\n</code></pre>\n</li>\n<li>\n<p><strong>RequestAnimationFrame for Visual Updates</strong></p>\n<pre><code class=\"language-javascript\"> updateVisuals() {\n   // Schedule visual updates in animation frame\n   requestAnimationFrame(() =&gt; {\n     this.elements.forEach(el =&gt; {\n       el.style.transform = `translate(${this.x}px, ${this.y}px)`;\n     });\n   });\n }\n</code></pre>\n</li>\n</ol>\n<p><strong>Interactive Demo:</strong> Compare optimized vs. unoptimized components</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Web Component Render Optimization</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div class=&quot;demo-container&quot;&gt;\n  &lt;h1&gt;Web Component Render Optimization&lt;/h1&gt;\n  &lt;p&gt;Compare optimized vs. unoptimized rendering approaches in Web Components&lt;/p&gt;\n  \n  &lt;div class=&quot;tabs&quot;&gt;\n    &lt;button class=&quot;tab active&quot; data-tab=&quot;basic&quot;&gt;Basic Comparison&lt;/button&gt;\n    &lt;button class=&quot;tab&quot; data-tab=&quot;techniques&quot;&gt;Optimization Techniques&lt;/button&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;tab-content&quot; id=&quot;basic-tab&quot;&gt;\n    &lt;h2&gt;Performance Comparison&lt;/h2&gt;\n    &lt;p&gt;Watch the difference between optimized and unoptimized component updates when handling frequent state changes.&lt;/p&gt;\n    \n    &lt;div class=&quot;control-panel&quot;&gt;\n      &lt;div class=&quot;control&quot;&gt;\n        &lt;label for=&quot;update-frequency&quot;&gt;Update Frequency:&lt;/label&gt;\n        &lt;select id=&quot;update-frequency&quot;&gt;\n          &lt;option value=&quot;500&quot;&gt;Low (500ms)&lt;/option&gt;\n          &lt;option value=&quot;100&quot;&gt;Medium (100ms)&lt;/option&gt;\n          &lt;option value=&quot;16&quot; selected&gt;High (16ms)&lt;/option&gt;\n        &lt;/select&gt;\n      &lt;/div&gt;\n      \n      &lt;div class=&quot;control&quot;&gt;\n        &lt;label for=&quot;item-count&quot;&gt;Item Count:&lt;/label&gt;\n        &lt;select id=&quot;item-count&quot;&gt;\n          &lt;option value=&quot;10&quot;&gt;Few (10)&lt;/option&gt;\n          &lt;option value=&quot;50&quot; selected&gt;Medium (50)&lt;/option&gt;\n          &lt;option value=&quot;100&quot;&gt;Many (100)&lt;/option&gt;\n        &lt;/select&gt;\n      &lt;/div&gt;\n      \n      &lt;button id=&quot;start-test&quot; class=&quot;demo-button&quot;&gt;Start Comparison&lt;/button&gt;\n      &lt;button id=&quot;stop-test&quot; class=&quot;demo-button&quot; disabled&gt;Stop&lt;/button&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;performance-results&quot;&gt;\n      &lt;div class=&quot;result-box&quot;&gt;\n        &lt;h3&gt;Unoptimized Component&lt;/h3&gt;\n        &lt;div class=&quot;metrics&quot;&gt;\n          &lt;div class=&quot;metric&quot;&gt;\n            &lt;div class=&quot;metric-title&quot;&gt;Frame Rate:&lt;/div&gt;\n            &lt;div class=&quot;metric-value&quot; id=&quot;unoptimized-fps&quot;&gt;0 FPS&lt;/div&gt;\n          &lt;/div&gt;\n          &lt;div class=&quot;metric&quot;&gt;\n            &lt;div class=&quot;metric-title&quot;&gt;Updates:&lt;/div&gt;\n            &lt;div class=&quot;metric-value&quot; id=&quot;unoptimized-updates&quot;&gt;0&lt;/div&gt;\n          &lt;/div&gt;\n        &lt;/div&gt;\n        &lt;div id=&quot;unoptimized-container&quot; class=&quot;component-container&quot;&gt;\n          &lt;div class=&quot;placeholder&quot;&gt;Click &quot;Start Comparison&quot; to initialize&lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n      \n      &lt;div class=&quot;result-box&quot;&gt;\n        &lt;h3&gt;Optimized Component&lt;/h3&gt;\n        &lt;div class=&quot;metrics&quot;&gt;\n          &lt;div class=&quot;metric&quot;&gt;\n            &lt;div class=&quot;metric-title&quot;&gt;Frame Rate:&lt;/div&gt;\n            &lt;div class=&quot;metric-value&quot; id=&quot;optimized-fps&quot;&gt;0 FPS&lt;/div&gt;\n          &lt;/div&gt;\n          &lt;div class=&quot;metric&quot;&gt;\n            &lt;div class=&quot;metric-title&quot;&gt;Updates:&lt;/div&gt;\n            &lt;div class=&quot;metric-value&quot; id=&quot;optimized-updates&quot;&gt;0&lt;/div&gt;\n          &lt;/div&gt;\n        &lt;/div&gt;\n        &lt;div id=&quot;optimized-container&quot; class=&quot;component-container&quot;&gt;\n          &lt;div class=&quot;placeholder&quot;&gt;Click &quot;Start Comparison&quot; to initialize&lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;tab-content hidden&quot; id=&quot;techniques-tab&quot;&gt;\n    &lt;h2&gt;Optimization Techniques&lt;/h2&gt;\n    \n    &lt;div class=&quot;technique&quot;&gt;\n      &lt;h3&gt;1. Batched DOM Updates&lt;/h3&gt;\n      &lt;p&gt;Instead of updating the DOM multiple times, batch changes into a single update using DocumentFragment.&lt;/p&gt;\n      \n      &lt;div class=&quot;code-comparison&quot;&gt;\n        &lt;div class=&quot;code-block&quot;&gt;\n          &lt;h4&gt;Unoptimized Approach&lt;/h4&gt;\n          &lt;pre&gt;&lt;code&gt;// Unoptimized: Multiple separate DOM operations\nupdateItems(items) {\n  // Clear existing items\n  this.listElement.innerHTML = &#39;&#39;;\n  \n  // Add each item individually (multiple DOM operations)\n  items.forEach(item =&gt; {\n    const li = document.createElement(&#39;li&#39;);\n    li.textContent = item.text;\n    this.listElement.appendChild(li);\n  });\n}&lt;/code&gt;&lt;/pre&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;code-block&quot;&gt;\n          &lt;h4&gt;Optimized Approach&lt;/h4&gt;\n          &lt;pre&gt;&lt;code&gt;// Optimized: Single DOM operation with DocumentFragment\nupdateItems(items) {\n  // Create fragment (in-memory container)\n  const fragment = document.createDocumentFragment();\n  \n  // Build complete structure in memory\n  items.forEach(item =&gt; {\n    const li = document.createElement(&#39;li&#39;);\n    li.textContent = item.text;\n    fragment.appendChild(li);\n  });\n  \n  // Single DOM operation\n  this.listElement.innerHTML = &#39;&#39;;\n  this.listElement.appendChild(fragment);\n}&lt;/code&gt;&lt;/pre&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;technique&quot;&gt;\n      &lt;h3&gt;2. RequestAnimationFrame for Visual Updates&lt;/h3&gt;\n      &lt;p&gt;Schedule visual updates to align with the browser&#39;s render cycle for smoother animations.&lt;/p&gt;\n      \n      &lt;div class=&quot;code-comparison&quot;&gt;\n        &lt;div class=&quot;code-block&quot;&gt;\n          &lt;h4&gt;Unoptimized Approach&lt;/h4&gt;\n          &lt;pre&gt;&lt;code&gt;// Unoptimized: Direct style changes\nupdatePosition(x, y) {\n  // This might cause layout thrashing if called frequently\n  this.element.style.transform = `translate(${x}px, ${y}px)`;\n}&lt;/code&gt;&lt;/pre&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;code-block&quot;&gt;\n          &lt;h4&gt;Optimized Approach&lt;/h4&gt;\n          &lt;pre&gt;&lt;code&gt;// Optimized: RAF for smooth visual updates\nupdatePosition(x, y) {\n  // Store the values to update\n  this._pendingX = x;\n  this._pendingY = y;\n  \n  // Only schedule update if none is pending\n  if (!this._updateScheduled) {\n    this._updateScheduled = true;\n    \n    requestAnimationFrame(() =&gt; {\n      // Apply all pending updates at once\n      this.element.style.transform = \n        `translate(${this._pendingX}px, ${this._pendingY}px)`;\n      \n      this._updateScheduled = false;\n    });\n  }\n}&lt;/code&gt;&lt;/pre&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;technique&quot;&gt;\n      &lt;h3&gt;3. Debouncing Property Changes&lt;/h3&gt;\n      &lt;p&gt;Limit the frequency of updates when properties change rapidly.&lt;/p&gt;\n      \n      &lt;div class=&quot;code-comparison&quot;&gt;\n        &lt;div class=&quot;code-block&quot;&gt;\n          &lt;h4&gt;Unoptimized Approach&lt;/h4&gt;\n          &lt;pre&gt;&lt;code&gt;// Unoptimized: Update on every property change\nset value(newValue) {\n  this._value = newValue;\n  this.render(); // May be called too frequently\n}&lt;/code&gt;&lt;/pre&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;code-block&quot;&gt;\n          &lt;h4&gt;Optimized Approach&lt;/h4&gt;\n          &lt;pre&gt;&lt;code&gt;// Optimized: Debounced updates\nconstructor() {\n  super();\n  this._value = 0;\n  this._debounceTimeout = null;\n}\n\nset value(newValue) {\n  this._value = newValue;\n  \n  // Clear previous timeout\n  clearTimeout(this._debounceTimeout);\n  \n  // Schedule a new update with a delay\n  this._debounceTimeout = setTimeout(() =&gt; {\n    this.render();\n  }, 16); // Roughly one frame at 60fps\n}&lt;/code&gt;&lt;/pre&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body {\n  font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, Oxygen, Ubuntu, Cantarell, &#39;Open Sans&#39;, &#39;Helvetica Neue&#39;, sans-serif;\n  max-width: 1200px;\n  margin: 0 auto;\n  padding: 20px;\n  line-height: 1.6;\n  color: #333;\n  background-color: #f8f9fa;\n}\n\nh1, h2, h3, h4 {\n  color: #2c3e50;\n}\n\n.demo-container {\n  margin: 20px 0;\n}\n\n.tabs {\n  display: flex;\n  gap: 2px;\n  margin-bottom: 20px;\n}\n\n.tab {\n  padding: 10px 20px;\n  background-color: #e9ecef;\n  border: none;\n  border-radius: 4px;\n  cursor: pointer;\n  font-size: 16px;\n  transition: background-color 0.2s;\n}\n\n.tab:hover {\n  background-color: #dee2e6;\n}\n\n.tab.active {\n  background-color: #3498db;\n  color: white;\n}\n\n.tab-content {\n  background-color: white;\n  padding: 20px;\n  border-radius: 8px;\n  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n}\n\n.hidden {\n  display: none;\n}\n\n.control-panel {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 15px;\n  align-items: center;\n  background-color: #f0f7ff;\n  padding: 15px;\n  border-radius: 8px;\n  margin-bottom: 20px;\n}\n\n.control {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n\nlabel {\n  font-weight: 500;\n}\n\nselect {\n  padding: 8px;\n  border-radius: 4px;\n  border: 1px solid #ced4da;\n  background-color: white;\n  font-size: 16px;\n}\n\n.demo-button {\n  background-color: #3498db;\n  color: white;\n  border: none;\n  border-radius: 4px;\n  padding: 8px 16px;\n  cursor: pointer;\n  font-size: 16px;\n  transition: background-color 0.2s;\n}\n\n.demo-button:hover {\n  background-color: #2980b9;\n}\n\n.demo-button:disabled {\n  background-color: #95a5a6;\n  cursor: not-allowed;\n}\n\n.performance-results {\n  display: grid;\n  grid-template-columns: 1fr 1fr;\n  gap: 20px;\n}\n\n.result-box {\n  background-color: white;\n  padding: 20px;\n  border-radius: 8px;\n  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n}\n\n.metrics {\n  display: flex;\n  gap: 20px;\n  margin: 15px 0;\n  padding: 15px;\n  background-color: #f8f9fa;\n  border-radius: 8px;\n}\n\n.metric {\n  flex: 1;\n}\n\n.metric-title {\n  font-weight: bold;\n  margin-bottom: 5px;\n  color: #666;\n}\n\n.metric-value {\n  font-size: 24px;\n  color: #3498db;\n}\n\n.component-container {\n  border: 1px solid #e9ecef;\n  border-radius: 8px;\n  padding: 15px;\n  height: 300px;\n  overflow: auto;\n  background-color: #f8f9fa;\n}\n\n.placeholder {\n  display: flex;\n  height: 100%;\n  align-items: center;\n  justify-content: center;\n  color: #7f8c8d;\n  font-style: italic;\n}\n\n.technique {\n  margin-bottom: 40px;\n  padding-bottom: 20px;\n  border-bottom: 1px solid #e9ecef;\n}\n\n.code-comparison {\n  display: grid;\n  grid-template-columns: 1fr 1fr;\n  gap: 20px;\n  margin-top: 15px;\n}\n\n.code-block {\n  background-color: #f8f9fa;\n  border-radius: 8px;\n  overflow: hidden;\n  border: 1px solid #e9ecef;\n}\n\n.code-block h4 {\n  margin: 0;\n  padding: 10px 15px;\n  background-color: #e9ecef;\n  color: #2c3e50;\n}\n\n.code-block pre {\n  margin: 0;\n  padding: 15px;\n  overflow-x: auto;\n}\n\n.code-block code {\n  font-family: &#39;Consolas&#39;, &#39;Monaco&#39;, &#39;Courier New&#39;, monospace;\n}\n\n/* Mobile responsiveness */\n@media (max-width: 768px) {\n  .performance-results, .code-comparison {\n    grid-template-columns: 1fr;\n  }\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class UnoptimizedGrid extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n    this.items = [];\n    this.updateCount = 0;\n    this.lastFrameTime = 0;\n    this.frameCount = 0;\n    this.fps = 0;\n\n    // Initialize the component\n    this.render();\n  }\n\n  // Called when the element is added to the document\n  connectedCallback() {\n    // Start FPS counter\n    this.startFpsCounter();\n  }\n\n  // Called when the element is removed from the document\n  disconnectedCallback() {\n    // Clean up\n    this.stopFpsCounter();\n  }\n\n  // Method to update the data items\n  updateData(items) {\n    this.items = items;\n    this.updateCount++;\n\n    // Update the metrics display\n    this.dispatchEvent(new CustomEvent(&#39;metrics-update&#39;, {\n      detail: {\n        updates: this.updateCount,\n        fps: this.fps } }));\n\n\n\n    // Render immediately - this is unoptimized\n    this.render();\n  }\n\n  // Unoptimized render method - rebuilds everything on each update\n  render() {\n    // Create the basic structure if it doesn&#39;t exist\n    if (!this.shadowRoot.querySelector(&#39;.grid&#39;)) {\n      this.shadowRoot.innerHTML = `\n        &lt;style&gt;\n          :host {\n            display: block;\n            width: 100%;\n          }\n          \n          .grid {\n            display: grid;\n            grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));\n            gap: 10px;\n            padding: 5px;\n          }\n          \n          .grid-item {\n            background-color: #3498db;\n            color: white;\n            padding: 10px;\n            border-radius: 4px;\n            text-align: center;\n            transition: background-color 0.2s;\n          }\n          \n          .grid-item:hover {\n            background-color: #2980b9;\n          }\n        &lt;/style&gt;\n        &lt;div class=&quot;grid&quot;&gt;&lt;/div&gt;\n      `;\n    }\n\n    const grid = this.shadowRoot.querySelector(&#39;.grid&#39;);\n\n    // Unoptimized: Clear and rebuild everything\n    grid.innerHTML = &#39;&#39;;\n\n    // Unoptimized: Add each item individually (causes multiple reflows)\n    this.items.forEach(item =&gt; {\n      const itemEl = document.createElement(&#39;div&#39;);\n      itemEl.className = &#39;grid-item&#39;;\n      itemEl.textContent = item.value;\n      grid.appendChild(itemEl);\n    });\n  }\n\n  // FPS tracking methods\n  startFpsCounter() {\n    this.frameCount = 0;\n    this.lastFrameTime = performance.now();\n    this.fpsInterval = setInterval(() =&gt; {\n      const now = performance.now();\n      const delta = now - this.lastFrameTime;\n\n      this.fps = Math.round(this.frameCount * 1000 / delta);\n\n      // Reset for next calculation\n      this.frameCount = 0;\n      this.lastFrameTime = now;\n\n      // Dispatch metrics update event\n      this.dispatchEvent(new CustomEvent(&#39;metrics-update&#39;, {\n        detail: {\n          updates: this.updateCount,\n          fps: this.fps } }));\n\n\n    }, 1000);\n  }\n\n  stopFpsCounter() {\n    clearInterval(this.fpsInterval);\n  }\n\n  // Called for each frame render\n  countFrame() {\n    this.frameCount++;\n  }}\n\n\n// Optimized Data Grid Component\nclass OptimizedGrid extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n    this.items = [];\n    this.updateCount = 0;\n    this.lastFrameTime = 0;\n    this.frameCount = 0;\n    this.fps = 0;\n\n    // Flag to track if a render has been scheduled\n    this._updatePending = false;\n\n    // Initialize the component\n    this.render();\n  }\n\n  // Called when the element is added to the document\n  connectedCallback() {\n    // Start FPS counter\n    this.startFpsCounter();\n  }\n\n  // Called when the element is removed from the document\n  disconnectedCallback() {\n    // Clean up\n    this.stopFpsCounter();\n  }\n\n  // Method to update the data items\n  updateData(items) {\n    this.items = items;\n    this.updateCount++;\n\n    // Update the metrics display\n    this.dispatchEvent(new CustomEvent(&#39;metrics-update&#39;, {\n      detail: {\n        updates: this.updateCount,\n        fps: this.fps } }));\n\n\n\n    // Optimized: Schedule a render using requestAnimationFrame\n    this.scheduleRender();\n  }\n\n  // Optimization 1: Schedule renders for better performance\n  scheduleRender() {\n    // Only schedule a new render if one isn&#39;t already pending\n    if (!this._updatePending) {\n      this._updatePending = true;\n\n      requestAnimationFrame(() =&gt; {\n        this.render();\n        this._updatePending = false;\n      });\n    }\n  }\n\n  // Optimized render method - uses DocumentFragment for batch updates\n  render() {\n    // Create the basic structure if it doesn&#39;t exist\n    if (!this.shadowRoot.querySelector(&#39;.grid&#39;)) {\n      this.shadowRoot.innerHTML = `\n        &lt;style&gt;\n          :host {\n            display: block;\n            width: 100%;\n          }\n          \n          .grid {\n            display: grid;\n            grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));\n            gap: 10px;\n            padding: 5px;\n          }\n          \n          .grid-item {\n            background-color: #3498db;\n            color: white;\n            padding: 10px;\n            border-radius: 4px;\n            text-align: center;\n            transition: background-color 0.2s;\n          }\n          \n          .grid-item:hover {\n            background-color: #2980b9;\n          }\n        &lt;/style&gt;\n        &lt;div class=&quot;grid&quot;&gt;&lt;/div&gt;\n      `;\n    }\n\n    const grid = this.shadowRoot.querySelector(&#39;.grid&#39;);\n\n    // Optimization 2: Use DocumentFragment for batched DOM updates\n    const fragment = document.createDocumentFragment();\n\n    // Build all elements in memory first\n    this.items.forEach(item =&gt; {\n      const itemEl = document.createElement(&#39;div&#39;);\n      itemEl.className = &#39;grid-item&#39;;\n      itemEl.textContent = item.value;\n      fragment.appendChild(itemEl);\n    });\n\n    // Optimization 3: Single DOM operation\n    grid.innerHTML = &#39;&#39;;\n    grid.appendChild(fragment);\n  }\n\n  // FPS tracking methods\n  startFpsCounter() {\n    this.frameCount = 0;\n    this.lastFrameTime = performance.now();\n    this.fpsInterval = setInterval(() =&gt; {\n      const now = performance.now();\n      const delta = now - this.lastFrameTime;\n\n      this.fps = Math.round(this.frameCount * 1000 / delta);\n\n      // Reset for next calculation\n      this.frameCount = 0;\n      this.lastFrameTime = now;\n\n      // Dispatch metrics update event\n      this.dispatchEvent(new CustomEvent(&#39;metrics-update&#39;, {\n        detail: {\n          updates: this.updateCount,\n          fps: this.fps } }));\n\n\n    }, 1000);\n  }\n\n  stopFpsCounter() {\n    clearInterval(this.fpsInterval);\n  }\n\n  // Called for each frame render\n  countFrame() {\n    this.frameCount++;\n  }}\n\n\n// Register our Web Components\ncustomElements.define(&#39;unoptimized-grid&#39;, UnoptimizedGrid);\ncustomElements.define(&#39;optimized-grid&#39;, OptimizedGrid);\n\n// Initialize demo\ndocument.addEventListener(&#39;DOMContentLoaded&#39;, () =&gt; {\n  // Setup tab navigation\n  const tabs = document.querySelectorAll(&#39;.tab&#39;);\n  tabs.forEach(tab =&gt; {\n    tab.addEventListener(&#39;click&#39;, () =&gt; {\n      // Deactivate all tabs\n      tabs.forEach(t =&gt; t.classList.remove(&#39;active&#39;));\n\n      // Hide all content\n      document.querySelectorAll(&#39;.tab-content&#39;).forEach(content =&gt; {\n        content.classList.add(&#39;hidden&#39;);\n      });\n\n      // Activate clicked tab\n      tab.classList.add(&#39;active&#39;);\n\n      // Show corresponding content\n      const tabId = tab.getAttribute(&#39;data-tab&#39;);\n      document.getElementById(`${tabId}-tab`).classList.remove(&#39;hidden&#39;);\n    });\n  });\n\n  let unoptimizedGrid;\n  let optimizedGrid;\n  let updateInterval;\n  let animationFrameId;\n\n  // Performance test controls\n  const startButton = document.getElementById(&#39;start-test&#39;);\n  const stopButton = document.getElementById(&#39;stop-test&#39;);\n  const frequencySelect = document.getElementById(&#39;update-frequency&#39;);\n  const itemCountSelect = document.getElementById(&#39;item-count&#39;);\n\n  // Function to generate random test data\n  function generateTestData(count) {\n    const data = [];\n    for (let i = 0; i &lt; count; i++) {\n      data.push({\n        id: i,\n        value: Math.floor(Math.random() * 100) });\n\n    }\n    return data;\n  }\n\n  // Update metrics displays\n  function setupMetricsListeners() {\n    unoptimizedGrid.addEventListener(&#39;metrics-update&#39;, e =&gt; {\n      document.getElementById(&#39;unoptimized-fps&#39;).textContent = `${e.detail.fps} FPS`;\n      document.getElementById(&#39;unoptimized-updates&#39;).textContent = e.detail.updates;\n    });\n\n    optimizedGrid.addEventListener(&#39;metrics-update&#39;, e =&gt; {\n      document.getElementById(&#39;optimized-fps&#39;).textContent = `${e.detail.fps} FPS`;\n      document.getElementById(&#39;optimized-updates&#39;).textContent = e.detail.updates;\n    });\n  }\n\n  // Animation frame counter\n  function countFrames() {\n    unoptimizedGrid?.countFrame();\n    optimizedGrid?.countFrame();\n    animationFrameId = requestAnimationFrame(countFrames);\n  }\n\n  // Start test button\n  startButton.addEventListener(&#39;click&#39;, () =&gt; {\n    // Create components if they don&#39;t exist\n    const unoptimizedContainer = document.getElementById(&#39;unoptimized-container&#39;);\n    const optimizedContainer = document.getElementById(&#39;optimized-container&#39;);\n\n    // Clear containers\n    unoptimizedContainer.innerHTML = &#39;&#39;;\n    optimizedContainer.innerHTML = &#39;&#39;;\n\n    // Create components\n    unoptimizedGrid = document.createElement(&#39;unoptimized-grid&#39;);\n    optimizedGrid = document.createElement(&#39;optimized-grid&#39;);\n\n    // Add to DOM\n    unoptimizedContainer.appendChild(unoptimizedGrid);\n    optimizedContainer.appendChild(optimizedGrid);\n\n    // Setup metrics listeners\n    setupMetricsListeners();\n\n    // Start frame counter\n    animationFrameId = requestAnimationFrame(countFrames);\n\n    // Get update frequency and item count\n    const updateFrequency = parseInt(frequencySelect.value);\n    const itemCount = parseInt(itemCountSelect.value);\n\n    // Start the update interval\n    updateInterval = setInterval(() =&gt; {\n      // Generate new random data\n      const data = generateTestData(itemCount);\n\n      // Update both components\n      unoptimizedGrid.updateData(data);\n      optimizedGrid.updateData(data);\n    }, updateFrequency);\n\n    // Update button states\n    startButton.disabled = true;\n    stopButton.disabled = false;\n    frequencySelect.disabled = true;\n    itemCountSelect.disabled = true;\n  });\n\n  // Stop test button\n  stopButton.addEventListener(&#39;click&#39;, () =&gt; {\n    // Clear the update interval\n    clearInterval(updateInterval);\n    cancelAnimationFrame(animationFrameId);\n\n    // Update button states\n    startButton.disabled = false;\n    stopButton.disabled = true;\n    frequencySelect.disabled = false;\n    itemCountSelect.disabled = false;\n  });\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/\">the original article</a>.</p></figure>\n<h2 id=\"advanced-patterns-and-best-practices\">Advanced Patterns and Best Practices<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#advanced-patterns-and-best-practices\" aria-label=\"Link to this section\">#</a></h2>\n<h3 id=\"state-management-across-boundaries\">State Management Across Boundaries<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#state-management-across-boundaries\" aria-label=\"Link to this section\">#</a></h3>\n<p>Managing state between Web Components and frameworks requires thoughtful patterns:</p>\n<h4 id=\"1-the-context-pattern\">1. The Context Pattern<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#1-the-context-pattern\" aria-label=\"Link to this section\">#</a></h4>\n<p>Create a shared context accessible to all components:</p>\n<pre><code class=\"language-javascript\">// context.js - Framework agnostic state management\nexport class ComponentContext {\n  constructor() {\n    this._data = {};\n    this._listeners = new Map();\n  }\n\n  get(key) {\n    return this._data[key];\n  }\n\n  set(key, value) {\n    this._data[key] = value;\n    if (this._listeners.has(key)) {\n      this._listeners.get(key).forEach(callback =&gt; callback(value));\n    }\n  }\n\n  subscribe(key, callback) {\n    if (!this._listeners.has(key)) {\n      this._listeners.set(key, new Set());\n    }\n    this._listeners.get(key).add(callback);\n\n    return () =&gt; {\n      this._listeners.get(key).delete(callback);\n    };\n  }\n}\n\n// Create shared instance\nexport const context = new ComponentContext();\n</code></pre>\n<p>Usage in components:</p>\n<pre><code class=\"language-javascript\">import { context } from './context.js';\n\nclass MyComponent extends HTMLElement {\n  connectedCallback() {\n    // Subscribe to state changes\n    this._unsubscribe = context.subscribe('userData', \n      (data) =&gt; this._updateFromState(data));\n\n    // Initial state\n    this._updateFromState(context.get('userData'));\n  }\n\n  disconnectedCallback() {\n    // Clean up subscription\n    this._unsubscribe();\n  }\n\n  _updateState(data) {\n    // Update shared state\n    context.set('userData', data);\n  }\n}\n</code></pre>\n<h4 id=\"2-the-message-bus-pattern\">2. The Message Bus Pattern<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#2-the-message-bus-pattern\" aria-label=\"Link to this section\">#</a></h4>\n<p>For loosely coupled components, a pub/sub event bus provides flexible communication:</p>\n<pre><code class=\"language-javascript\">// event-bus.js\nexport class EventBus {\n  constructor() {\n    this.listeners = {};\n  }\n\n  on(event, callback) {\n    if (!this.listeners[event]) {\n      this.listeners[event] = [];\n    }\n    this.listeners[event].push(callback);\n\n    return () =&gt; this.off(event, callback);\n  }\n\n  off(event, callback) {\n    if (this.listeners[event]) {\n      this.listeners[event] = this.listeners[event]\n        .filter(cb =&gt; cb !== callback);\n    }\n  }\n\n  emit(event, data) {\n    if (this.listeners[event]) {\n      this.listeners[event].forEach(callback =&gt; {\n        callback(data);\n      });\n    }\n  }\n}\n\n// Shared instance\nexport const eventBus = new EventBus();\n</code></pre>\n<h3 id=\"server-side-rendering-with-web-components\">Server-Side Rendering with Web Components<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#server-side-rendering-with-web-components\" aria-label=\"Link to this section\">#</a></h3>\n<p>Server-side rendering improves initial load performance. With Declarative Shadow DOM (<code>&lt;template shadowrootmode=\"open\"&gt;</code>), the server can send a component's shadow tree as plain HTML:</p>\n<pre><code class=\"language-html\">&lt;!-- Server-rendered output --&gt;\n&lt;my-component&gt;\n  &lt;template shadowrootmode=\"open\"&gt;\n    &lt;style&gt;\n      /* Shadow DOM styles */\n      .card { \n        padding: 16px;\n        border: 1px solid #ddd;\n      }\n    &lt;/style&gt;\n    &lt;div class=\"card\"&gt;\n      &lt;slot&gt;&lt;/slot&gt;\n    &lt;/div&gt;\n  &lt;/template&gt;\n  &lt;p&gt;This content is slotted from light DOM&lt;/p&gt;\n&lt;/my-component&gt;\n\n&lt;!-- Polyfill for browsers without Declarative Shadow DOM --&gt;\n&lt;script&gt;\n  if (!HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode')) {\n    document.querySelectorAll('template[shadowrootmode]').forEach(template =&gt; {\n      const mode = template.getAttribute('shadowrootmode');\n      const shadowRoot = template.parentNode.attachShadow({ mode });\n      shadowRoot.appendChild(template.content);\n      template.remove();\n    });\n  }\n&lt;/script&gt;\n</code></pre>\n<h3 id=\"testing-strategies\">Testing Strategies<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#testing-strategies\" aria-label=\"Link to this section\">#</a></h3>\n<p>Testing Web Components requires specialized approaches:</p>\n<h4 id=\"1-component-unit-testing\">1. Component Unit Testing<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#1-component-unit-testing\" aria-label=\"Link to this section\">#</a></h4>\n<pre><code class=\"language-javascript\">// Using web-test-runner and @open-wc/testing\nimport { html, fixture, expect } from '@open-wc/testing';\nimport '../src/my-component.js';\n\ndescribe('MyComponent', () =&gt; {\n  it('renders with default values', async () =&gt; {\n    const el = await fixture(html`&lt;my-component&gt;&lt;/my-component&gt;`);\n\n    expect(el.shadowRoot.querySelector('.title').textContent)\n      .to.equal('Default Title');\n  });\n\n  it('updates when properties change', async () =&gt; {\n    const el = await fixture(html`&lt;my-component&gt;&lt;/my-component&gt;`);\n\n    el.title = 'New Title';\n    await el.updateComplete; // For LitElement components\n\n    expect(el.shadowRoot.querySelector('.title').textContent)\n      .to.equal('New Title');\n  });\n\n  it('fires custom events', async () =&gt; {\n    const el = await fixture(html`&lt;my-component&gt;&lt;/my-component&gt;`);\n\n    // Setup event listener\n    let eventFired = false;\n    el.addEventListener('custom-event', () =&gt; eventFired = true);\n\n    // Trigger event\n    el.shadowRoot.querySelector('button').click();\n\n    expect(eventFired).to.be.true;\n  });\n});\n</code></pre>\n<h4 id=\"2-integration-testing-with-frameworks\">2. Integration Testing with Frameworks<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#2-integration-testing-with-frameworks\" aria-label=\"Link to this section\">#</a></h4>\n<pre><code class=\"language-jsx\">// React Testing Library example\nimport { render, fireEvent, screen } from '@testing-library/react';\nimport MyReactComponent from '../src/MyReactComponent';\n\n// Register Web Component if not done globally\nimport '../src/web-components/my-element.js';\n\ntest('React component interacts with Web Component', async () =&gt; {\n  render(&lt;MyReactComponent initialData=\"test\" /&gt;);\n\n  // Interact with the wrapped Web Component\n  fireEvent.click(screen.getByRole('button'));\n\n  // Check the result\n  expect(screen.getByText('Success')).toBeInTheDocument();\n});\n</code></pre>\n<h2 id=\"conclusion\">Conclusion<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#conclusion\" aria-label=\"Link to this section\">#</a></h2>\n<p>You can now bridge a framework and a web component:</p>\n<ul>\n<li>Pass objects and arrays as <strong>properties</strong> (through a ref in React), not as attributes.</li>\n<li>Listen for custom events with <strong><code>addEventListener</code></strong>, and remove the listener in the effect's cleanup.</li>\n<li>Wrap that once in a small component so the rest of your app never thinks about it.</li>\n</ul>\n<p>A framework can now hand a component an object and hear its events, which was the whole problem.</p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Advanced Patterns & Integration with Frameworks\nCanonical: https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/\nAdvanced Patterns for Web Components in Modern Frameworks\nPass a JavaScript object to a web component from React, and the component receives the text [object Object]. That is the whole problem this post solves: frameworks and web components pass data in different ways, and a few small habits bridge them.\nWhen a framework such as React meets a custom tag like <user-badge>, data and events can get lost on the way across. This post shows where, and how to stop it.\nPrerequisites in two lines: a custom element can take small settings as HTML attributes (always strings) and richer data as JavaScript properties (el.data = {...}). It talks back with custom events that you listen for with addEventListener.\nReact is covered in full and Vue is the second example. Angular and Svelte follow the same two ideas (property for data, addEventListener for events), so their sections are optional reading.\nWhy Framework Integration Matters\nWeb Components were designed to be universal, but the reality of modern web development means they must coexist with frameworks that have their own component models and lifecycles. Successful integration requires understanding both worlds and the boundaries between them.\nHere is the failure, so you can recognize it. In React 18 (checked with 18.3.1), this element receives a string, not your object:\n<user-badge user={{ name: 'Ada' }} />\n// the DOM now has user=\"[object Object]\", and the component's `user` property is untouched\nReact 18 writes unknown props on custom elements as attributes, and an attribute can only hold text. A function prop such as onChartUpdate={fn} is also not turned into an event listener for a custom event. React 19 changed how props reach custom elements and is not covered here, so check its release notes if you use it.\nWhat makes integration challenging?\nDifferent Data Flow Models: React's unidirectional data flow differs fundamentally from the property-based approach of Web Components.\nEvent Handling Discrepancies: Each framework has its own event system that must be reconciled with the DOM event model.\nLifecycle Management: Coordinating component lifecycle events between frameworks and Web Components requires careful orchestration.\nLet's explore how to overcome these challenges with practical, reusable patterns.\nUsing Web Components with Modern Frameworks\nReact Integration Strategies\nReact's declarative approach to UI challenges direct integration with the imperative DOM APIs of Web Components. Here are advanced patterns to bridge this gap:\n1. The Ref Pattern: Direct DOM Access\nfunction MyReactComponent() {\nconst wcRef = useRef(null);\nuseEffect(() => {\nif (wcRef.current) {\n// Direct property assignment for complex data\nwcRef.current.complexData = { key: \"value\" };\n// Event handling with proper cleanup\nconst el = wcRef.current;\nconst handleEvent = (e) => console.log(e.detail);\nel.addEventListener('custom-event', handleEvent);\nreturn () => {\nel.removeEventListener('custom-event', handleEvent);\n};\n}\n}, []);\nreturn <my-web-component ref={wcRef} string-prop=\"This works\" />;\n}\nKey Insights:\nReact passes string attributes directly, but complex data requires refs\nEvent handling needs manual listeners with proper cleanup\nRef access enables imperative methods on Web Components\nQuestion: A page script wants a component to receive an object. Which call gives the component the object itself?\nel.setAttribute('data', obj)\nel.data = obj\nel.getAttribute('data')\nAnswer: 2. el.data = obj\nExplanation: setAttribute converts its value to a string (\"[object Object]\" for a plain object). Assigning a property keeps the object as it is.\nCoding challenge: Hand React's data to the element\n<user-badge> has a user property, but the React wrapper never sets it, so the preview says \"Hello, nobody yet\".\nExpected result: after you finish the TODO the preview says \"Hello, Ada\".\nHTML\n<div id=\"root\"></div>\nJavaScript\nclass UserBadge extends HTMLElement {\nconstructor() { super(); this.attachShadow({ mode: 'open' }); }\nset user(value) { this._user = value; this.render(); }\nget user() { return this._user; }\nconnectedCallback() { this.render(); }\nrender() {\nconst name = this._user ? this._user.name : 'nobody yet';\nthis.shadowRoot.innerHTML = '<p>Hello, ' + name + '</p>';\n}\n}\ncustomElements.define('user-badge', UserBadge);\nconst { useRef, useEffect, createElement: h } = React;\nfunction Badge({ user }) {\nconst ref = useRef(null);\n// TODO: in a useEffect, set ref.current.user = user (run again when user changes)\nreturn h('user-badge', { ref });\n}\nReactDOM.createRoot(document.getElementById('root')).render(\nh(Badge, { user: { name: 'Ada' } })\n);\nSet a property through a ref on the canonical article (https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/).\nTip:\nuseEffect(() => { ref.current.user = user; }, [user]); goes where the TODO is.\n2. The Wrapper Pattern: Creating React-Friendly Components\nReact developers expect components with React-like patterns. Create wrapper components that abstract Web Component peculiarities:\n// Wrapper that handles property mapping and events\nfunction EnhancedWebComponent({ data, onCustomEvent, children }) {\nconst ref = useRef(null);\nuseEffect(() => {\nconst element = ref.current;\nif (element) {\n// Set complex data\nelement.data = data;\n// Handle events\nconst eventHandler = (e) => onCustomEvent(e.detail);\nelement.addEventListener('custom-event', eventHandler);\nreturn () => element.removeEventListener('custom-event', eventHandler);\n}\n}, [data, onCustomEvent]);\nreturn (\n<my-web-component ref={ref}>\n{children}\n</my-web-component>\n);\n}\n// Usage feels like a normal React component\nfunction App() {\nreturn (\n<EnhancedWebComponent\ndata={{complex: \"data\"}}\nonCustomEvent={handleEvent}\n>\n<p>Child content</p>\n</EnhancedWebComponent>\n);\n}\nInteractive Demo: Try the React integration patterns yourself\nHTML\n<div id=\"root\"></div>\nCSS\n.demo-container {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\n}\n.examples {\ndisplay: flex;\nflex-direction: column;\ngap: 20px;\n}\n.example {\nborder: 1px solid #ddd;\nborder-radius: 8px;\npadding: 16px;\nbackground-color: #fff;\n}\nh2 {\ncolor: #2c3e50;\nmargin-top: 0;\n}\nh3 {\ncolor: #2c3e50;\nmargin-top: 0;\n}\n.button-group {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 8px;\nmargin-top: 8px;\n}\n.react-button {\nbackground-color: #61dafb;\ncolor: #000;\nborder: none;\nborder-radius: 4px;\npadding: 8px 16px;\ncursor: pointer;\nfont-size: 16px;\n}\n.events {\nmargin-top: 16px;\npadding: 8px;\nbackground-color: #f8f9fa;\nborder-radius: 4px;\nfont-size: 14px;\n}\nJavaScript\nconst { useState, useRef, useEffect } = React;\n// First, define the Web Component\n// First, define the Web Component\nclass SimpleCounter extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis._count = 0;\nthis.render();\n}\nstatic get observedAttributes() {\nreturn ['count'];\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === 'count') {\nthis._count = parseInt(newValue || '0', 10);\nthis.render();\n}\n}\nget count() {\nreturn this._count;\n}\nset count(value) {\nthis._count = value;\nthis.render();\nthis.dispatchEvent(new CustomEvent('counter-changed', {\nbubbles: true,\ncomposed: true,\ndetail: { count: this._count } }));\n}\nincrement() {\nthis._count++;\nthis.render();\nthis.dispatchEvent(new CustomEvent('counter-changed', {\nbubbles: true,\ncomposed: true,\ndetail: { count: this._count } }));\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: sans-serif;\nborder: 2px solid #3498db;\nborder-radius: 8px;\npadding: 16px;\nmargin: 8px 0;\nbackground-color: #f8f9fa;\n}\n.counter {\nfont-size: 24px;\nmargin-bottom: 12px;\n}\nbutton {\nbackground-color: #3498db;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px 16px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.2s;\n}\nbutton:hover {\nbackground-color: #2980b9;\n}\n</style>\n<div class=\"counter\">Count: ${this._count}</div>\n<button id=\"increment\">Increment</button>\n`;\nthis.shadowRoot.querySelector('#increment').addEventListener('click', () => this.increment());\n}\ndisconnectedCallback() {\nconst btn = this.shadowRoot.querySelector('#increment');\nif (btn) {\nbtn.removeEventListener('click', () => this.increment());\n}\n}}\n// Register the web component\nif (!customElements.get('simple-counter')) {\ncustomElements.define('simple-counter', SimpleCounter);\n}\n// React Components using React.createElement for CodePen compatibility\nfunction DirectInteractionExample() {\nconst [events, setEvents] = React.useState([]);\nconst counterRef = React.useRef(null);\nReact.useEffect(() => {\nif (counterRef.current) {\n// Set initial count\ncounterRef.current.count = 5;\n// Set up event listener\nconst handleCounterChanged = e => {\nsetEvents(prev => [...prev, `Counter changed: ${e.detail.count}`]);\n};\ncounterRef.current.addEventListener('counter-changed', handleCounterChanged);\n// Clean up\nreturn () => {\nif (counterRef.current) {\ncounterRef.current.removeEventListener('counter-changed', handleCounterChanged);\n}\n};\n}\n}, []);\n// Match Angular's button actions\nconst handleIncrement = () => {\nif (counterRef.current) {\ncounterRef.current.increment();\n}\n};\nconst handleDecrement = () => {\nif (counterRef.current && counterRef.current.count > 0) {\ncounterRef.current.count = counterRef.current.count - 1;\nsetEvents(prev => [...prev, `Counter decremented to: ${counterRef.current.count}`]);\n}\n};\nconst handleReset = () => {\nif (counterRef.current) {\ncounterRef.current.count = 0;\nsetEvents(prev => [...prev, 'Counter reset to 0']);\n}\n};\nconst handleDoubleValue = () => {\nif (counterRef.current) {\nconst currentValue = counterRef.current.count;\ncounterRef.current.count = currentValue * 2;\nsetEvents(prev => [...prev, `Doubled value to: ${counterRef.current.count}`]);\n}\n};\nreturn React.createElement('div', { className: 'example' },\nReact.createElement('h3', null, 'Using React with Web Components'),\nReact.createElement('p', null, 'Direct component manipulation using refs'),\nReact.createElement('simple-counter', { ref: counterRef }),\nReact.createElement('div', { className: 'button-group' },\nReact.createElement('button', {\nonClick: handleIncrement,\nclassName: 'react-button' },\n'Increment'),\nReact.createElement('button', {\nonClick: handleDecrement,\nclassName: 'react-button' },\n'Decrement'),\nReact.createElement('button', {\nonClick: handleReset,\nclassName: 'react-button' },\n'Reset')),\nReact.createElement('div', { className: 'button-group' },\nReact.createElement('button', {\nonClick: handleDoubleValue,\nclassName: 'react-button' },\n'Double Value')),\nReact.createElement('div', { className: 'events' },\nReact.createElement('strong', null, 'Events:'),\nevents.map((event, i) => React.createElement('div', { key: i }, event))));\n}\n// Main Demo Component\nfunction ReactWebComponentDemo() {\nreturn React.createElement('div', { className: 'demo-container' },\nReact.createElement('h2', null, 'React + Web Components Integration'),\nReact.createElement('p', null,\n'This example demonstrates how React can interact with Web Components through refs and direct DOM manipulation.'),\nReact.createElement(DirectInteractionExample));\n}\n// Render the demo\nReactDOM.render(React.createElement(ReactWebComponentDemo), document.getElementById('root'));\nReact + Web Components Integration on the canonical article (https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/).\nAngular Integration Strategies\nAngular has built-in support for Web Components through its @angular/elements package, but deeper integration still requires specific techniques:\n1. Element Binding with Custom Directives\n@Directive({\nselector: 'my-web-component'\n})\nexport class WebComponentDirective implements OnInit, OnChanges, OnDestroy {\n@Input() complexData: any;\n@Output() customEvent = new EventEmitter<any>();\nconstructor(private el: ElementRef) {}\nngOnInit() {\nthis.el.nativeElement.addEventListener('custom-event',\nthis.handleCustomEvent.bind(this));\n}\nngOnChanges(changes: SimpleChanges) {\nif (changes.complexData) {\nthis.el.nativeElement.complexData = this.complexData;\n}\n}\nngOnDestroy() {\nthis.el.nativeElement.removeEventListener('custom-event',\nthis.handleCustomEvent.bind(this));\n}\nprivate handleCustomEvent(event: CustomEvent) {\nthis.customEvent.emit(event.detail);\n}\n}\nKey Insights:\nAngular directives provide clean property and event binding\nOnChanges lifecycle hook updates properties efficiently\nElementRef gives direct access to the native element\nInteractive Demo: Explore Angular integrations\nHTML\n<div ng-app=\"webComponentDemo\" ng-controller=\"MainController\">\n<div class=\"demo-container\">\n<h2>Angular + Web Components Integration</h2>\n<p>\nThis example demonstrates how Angular can work with Web Components through directive bindings\nand direct DOM access.\n</p>\n<div class=\"examples\">\n<div class=\"example\">\n<h3>Using Angular Directives</h3>\n<p>Binding component state to Angular's data model</p>\n<simple-counter count=\"counterValue\" on-counter-changed=\"handleCounterEvent($event)\">\n</simple-counter>\n<button class=\"angular-button\" ng-click=\"incrementCounter()\">\nAngular: Increment Counter\n</button>\n<div class=\"events\">\n<strong>Current count in Angular: {{ counterValue }}</strong>\n<div ng-repeat=\"event in events track by $index\">\n{{ event }}\n</div>\n</div>\n</div>\n<div class=\"example\">\n<h3>Direct DOM Access</h3>\n<p>Using element references to directly call component methods</p>\n<simple-counter id=\"direct-counter\"></simple-counter>\n<button class=\"angular-button\" ng-click=\"callDirectMethod()\">\nCall Component Method Directly\n</button>\n</div>\n</div>\n</div>\n</div>\nCSS\n.demo-container {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\nOxygen, Ubuntu, Cantarell, \"Open Sans\", \"Helvetica Neue\", sans-serif;\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\n}\n.examples {\ndisplay: flex;\nflex-direction: column;\ngap: 20px;\n}\n.example {\nborder: 1px solid #ddd;\nborder-radius: 8px;\npadding: 16px;\nbackground-color: #fff;\n}\nh2 {\ncolor: #2c3e50;\nmargin-top: 0;\n}\nh3 {\ncolor: #2c3e50;\nmargin-top: 0;\n}\n.angular-button {\nbackground-color: #dd0031;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px 16px;\nmargin-top: 8px;\ncursor: pointer;\nfont-size: 16px;\n}\n.events {\nmargin-top: 16px;\npadding: 8px;\nbackground-color: #f8f9fa;\nborder-radius: 4px;\nfont-size: 14px;\n}\nJavaScript\n// Define our Web Component\nclass SimpleCounter extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: \"open\" });\nthis._count = 0;\nthis.render();\n}\nstatic get observedAttributes() {\nreturn [\"count\"];\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === \"count\") {\nthis._count = parseInt(newValue || \"0\", 10);\nthis.render();\n}\n}\nget count() {\nreturn this._count;\n}\nset count(value) {\nthis._count = value;\nthis.render();\n// When set programmatically, update the attribute to keep in sync\nthis.setAttribute(\"count\", value);\n}\nincrement() {\nthis._count++;\nthis.render();\nthis.dispatchEvent(\nnew CustomEvent(\"counter-changed\", {\nbubbles: true,\ncomposed: true,\ndetail: { count: this._count } }));\n// Update the attribute to keep in sync with the property\nthis.setAttribute(\"count\", this._count);\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: sans-serif;\nborder: 2px solid #dd0031;\nborder-radius: 8px;\npadding: 16px;\nmargin: 8px 0;\nbackground-color: #f8f9fa;\n}\n.counter {\nfont-size: 24px;\nmargin-bottom: 12px;\n}\nbutton {\nbackground-color: #dd0031;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px 16px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.2s;\n}\nbutton:hover {\nbackground-color: #a50023;\n}\n</style>\n<div class=\"counter\">Count: ${this._count}</div>\n<button id=\"increment\">Increment from Inside</button>\n`;\nthis.shadowRoot.\nquerySelector(\"#increment\").\naddEventListener(\"click\", () => this.increment());\n}\ndisconnectedCallback() {\nconst button = this.shadowRoot.querySelector(\"#increment\");\nif (button) {\nbutton.removeEventListener(\"click\", () => this.increment());\n}\n}}\n// Register our Web Component\nif (!customElements.get(\"simple-counter\")) {\ncustomElements.define(\"simple-counter\", SimpleCounter);\n}\n// Angular app\nangular.\nmodule(\"webComponentDemo\", []).\ncontroller(\"MainController\", [\n\"$scope\",\n\"$timeout\",\nfunction ($scope, $timeout) {\n// Initialize data\n$scope.counterValue = 5;\n$scope.events = [];\n// Event handler for counter changes\n$scope.handleCounterEvent = function (event) {\n// Use $timeout to ensure Angular knows about the update\n$timeout(function () {\n$scope.counterValue = event.detail.count;\n$scope.events.push(\"Counter changed: \" + event.detail.count);\n});\n};\n// Method to increment the counter from Angular\n$scope.incrementCounter = function () {\n$scope.counterValue++;\n// Angular will update the attribute automatically\n};\n// Method to call the component directly\n$scope.callDirectMethod = function () {\nvar counter = document.getElementById(\"direct-counter\");\nif (counter) {\ncounter.increment();\n// Add to the events list\n$timeout(function () {\n$scope.events.push(\"Called increment() method directly\");\n});\n}\n};\n// Setup event handling for the direct counter\n$timeout(function () {\nvar counter = document.getElementById(\"direct-counter\");\nif (counter) {\n// Set initial value\ncounter.count = 10;\n// Listen for events\ncounter.addEventListener(\"counter-changed\", function (event) {\n$timeout(function () {\n$scope.events.push(\"Direct counter changed: \" + event.detail.count);\n});\n});\n}\n});\n// We need to create a directive to bind our Web Component properly\n// This is done to demonstrate the concept, but in CodePen it's simpler to\n// do the binding manually as we've done above\n}])\n// Add a simple directive to handle attribute binding\n.directive(\"simpleCounter\", function () {\nreturn {\nrestrict: \"E\",\nscope: {\ncount: \"=?\", // Two-way binding (optional: the direct counter has no count attribute)\nonCounterChanged: \"&\" // Function binding\n},\nlink: function (scope, element, attrs) {\n// Watch for changes to the 'count' attribute\nscope.$watch(\"count\", function (newValue) {\nif (newValue !== undefined) {\nelement[0].count = newValue;\n}\n});\n// Listen for counter-changed events\nelement[0].addEventListener(\"counter-changed\", function (event) {\nscope.$evalAsync(function () {\nscope.count = event.detail.count;\nscope.onCounterChanged({ $event: event });\n});\n});\n} };\n});\nAngular + Web Components Integration on the canonical article (https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/).\nVue Integration Strategies\nVue offers excellent built-in support for Web Components with its attribute and event binding syntax:\n<template>\n<my-web-component\n:complex-prop=\"myData\"\n@custom-event=\"handleEvent\"\n/>\n</template>\n<script>\nexport default {\ndata() {\nreturn {\nmyData: { key: 'value' }\n}\n},\nmethods: {\nhandleEvent(event) {\nconsole.log(event.detail);\n}\n},\nmounted() {\n// For cases where direct property access is needed\nthis.$refs.myComponent.directMethod();\n}\n}\n</script>\nKey Insights:\nVue's :prop syntax works with kebab-case attributes\nNative event handling with @event-name\nVue's reactivity system naturally updates Web Component properties\nInteractive Demo: Explore Vue integrations\nHTML\n<div id=\"app\">\n<h2>Vue + Web Components Integration</h2>\n<p>\nVue offers excellent support for Web Components with its built-in directive syntax.\nThis demo shows how properties and events work.\n</p>\n<div class=\"demo-container\">\n<div class=\"example\">\n<h3>Basic Integration</h3>\n<p>Vue's declarative syntax makes Web Component integration straightforward</p>\n<simple-counter :count=\"counterData\" @counter-changed=\"handleCounterEvent\"></simple-counter>\n<button class=\"vue-button\" @click=\"incrementCounter\">\nIncrement from Vue\n</button>\n<div class=\"events\">\n<strong>Current count in Vue data: {{ counterData }}</strong>\n<div v-for=\"(event, index) in events\" :key=\"index\">\n{{ event }}\n</div>\n</div>\n</div>\n<div class=\"example\">\n<h3>Using $refs for Direct Access</h3>\n<p>When needed, Vue can access Web Components directly with $refs</p>\n<simple-counter ref=\"directCounter\"></simple-counter>\n<button class=\"vue-button\" @click=\"callComponentMethod\">\nCall component method directly\n</button>\n</div>\n</div>\n</div>\nCSS\nbody {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\nOxygen, Ubuntu, Cantarell, \"Open Sans\", \"Helvetica Neue\", sans-serif;\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\n}\n.demo-container {\ndisplay: flex;\nflex-direction: column;\ngap: 20px;\n}\n.example {\nborder: 1px solid #ddd;\nborder-radius: 8px;\npadding: 16px;\nbackground-color: #fff;\n}\nh2 {\ncolor: #2c3e50;\nmargin-top: 0;\n}\nh3 {\ncolor: #2c3e50;\nmargin-top: 0;\n}\n.vue-button {\nbackground-color: #42b883;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px 16px;\nmargin-top: 8px;\ncursor: pointer;\nfont-size: 16px;\n}\n.events {\nmargin-top: 16px;\npadding: 8px;\nbackground-color: #f8f9fa;\nborder-radius: 4px;\nfont-size: 14px;\n}\nJavaScript\n// Define Web Component\nclass SimpleCounter extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: \"open\" });\nthis._count = 0;\nthis.render();\n}\nstatic get observedAttributes() {\nreturn [\"count\"];\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === \"count\") {\nthis._count = parseInt(newValue || \"0\", 10);\nthis.render();\n}\n}\nget count() {\nreturn this._count;\n}\nset count(value) {\nthis._count = value;\nthis.render();\n}\nincrement() {\nthis._count++;\nthis.render();\nthis.dispatchEvent(\nnew CustomEvent(\"counter-changed\", {\nbubbles: true,\ncomposed: true,\ndetail: { count: this._count } }));\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: sans-serif;\nborder: 2px solid #42b883;\nborder-radius: 8px;\npadding: 16px;\nmargin: 8px 0;\nbackground-color: #f8f9fa;\n}\n.counter {\nfont-size: 24px;\nmargin-bottom: 12px;\n}\nbutton {\nbackground-color: #42b883;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px 16px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.2s;\n}\nbutton:hover {\nbackground-color: #33a06f;\n}\n</style>\n<div class=\"counter\">Count: ${this._count}</div>\n<button id=\"increment\">Increment</button>\n`;\nthis.shadowRoot.\nquerySelector(\"#increment\").\naddEventListener(\"click\", () => this.increment());\n}\ndisconnectedCallback() {\nconst button = this.shadowRoot.querySelector(\"#increment\");\nif (button) {\nbutton.removeEventListener(\"click\", () => this.increment());\n}\n}}\n// Register the Web Component\nif (!customElements.get(\"simple-counter\")) {\ncustomElements.define(\"simple-counter\", SimpleCounter);\n}\n// Vue App\nconst app = Vue.createApp({\ndata() {\nreturn {\ncounterData: 5,\nevents: [] };\n},\nmethods: {\nhandleCounterEvent(event) {\nthis.counterData = event.detail.count;\nthis.events.push(`Counter changed: ${event.detail.count}`);\n},\nincrementCounter() {\nthis.counterData++;\nthis.$nextTick(() => {\n// Vue's reactivity will update the attribute on the Web Component\n});\n},\ncallComponentMethod() {\n// Direct method call via ref\nthis.$refs.directCounter.increment();\nthis.events.push(\"Called increment() method directly\");\n} },\nmounted() {\n// Initial setup if needed\nthis.$refs.directCounter.count = 10;\n} });\napp.mount(\"#app\");\nVue + Web Components Integration on the canonical article (https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/).\nSvelte Integration Strategies\nSvelte treats Web Components as first-class citizens and makes integration particularly straightforward:\n<script>\nimport { onMount } from 'svelte';\nlet el;\nlet myData = { key: 'value' };\n// Reactive assignments automatically update the Web Component\n$: if (el) {\nel.complexData = myData;\n}\nfunction handleEvent(event) {\nconsole.log(event.detail);\n}\nonMount(() => {\n// If needed, do direct manipulation here\nel.addEventListener('special-event', specialHandler);\nreturn () => {\nel.removeEventListener('special-event', specialHandler);\n}\n});\n</script>\n<my-web-component\nbind:this={el}\non:custom-event={handleEvent}\n/>\nKey Insights:\nbind:this provides direct access to the element\nSvelte's reactivity automatically updates properties\nSvelte's event handling works natively with Web Component events\nInteractive Demo: Explore Svelte integration Techniques\nHTML\n<!-- The Svelte component below (App) is compiled in your browser and mounted here. -->\n<div id=\"app\"></div>\nCSS\nbody {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\nbackground-color: #f8f9fa;\n}\n.demo-container {\nmargin: 20px 0;\n}\n.example {\nborder: 1px solid #ddd;\nborder-radius: 8px;\npadding: 16px;\nmargin-bottom: 20px;\nbackground-color: #fff;\nbox-shadow: 0 2px 4px rgba(0,0,0,0.05);\n}\nh2,\nh3 {\ncolor: #2c3e50;\nmargin-top: 0;\n}\n.button-group {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 8px;\nmargin-top: 8px;\n}\n.component-button,\n.svelte-button {\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px 16px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.2s;\n}\n.component-button { background-color: #3498db; }\n.component-button:hover { background-color: #2980b9; }\n.svelte-button { background-color: #ff3e00; }\n.svelte-button:hover { background-color: #d03400; }\n.events {\nmargin-top: 16px;\npadding: 12px;\nbackground-color: #f8f9fa;\nborder-radius: 4px;\nfont-size: 14px;\nborder: 1px solid #e9ecef;\nmax-height: 200px;\noverflow-y: auto;\n}\nJavaScript\n// 1. A framework-agnostic Web Component: a counter with a `count` property,\n//    an `increment()` method, and a `counter-changed` event.\nclass SimpleCounter extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis._count = 0;\nthis.render();\n}\nstatic get observedAttributes() {\nreturn ['count'];\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === 'count') this.count = parseInt(newValue || '0', 10);\n}\nget count() {\nreturn this._count;\n}\nset count(value) {\nif (value === this._count) return;\nthis._count = value;\nthis.render();\nthis.dispatchEvent(new CustomEvent('counter-changed', {\nbubbles: true,\ncomposed: true,\ndetail: { count: this._count }\n}));\n}\nincrement() {\nthis.count = this._count + 1;\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: sans-serif;\nborder: 2px solid #ff3e00;\nborder-radius: 8px;\npadding: 16px;\nmargin: 8px 0;\nbackground-color: #f8f9fa;\n}\n.counter { font-size: 24px; margin-bottom: 12px; }\nbutton {\nbackground-color: #ff3e00;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px 16px;\ncursor: pointer;\nfont-size: 16px;\n}\nbutton:hover { background-color: #d03400; }\n</style>\n<div class=\"counter\">Count: ${this._count}</div>\n<button id=\"increment\">Increment from Inside</button>\n`;\nthis.shadowRoot.querySelector('#increment').addEventListener('click', () => this.increment());\n}\n}\nif (!customElements.get('simple-counter')) {\ncustomElements.define('simple-counter', SimpleCounter);\n}\n// 2. A real Svelte component that uses it: bind:this for direct access,\n//    a reactive statement to push store values in, and on:counter-changed to listen.\nconst App = `\n<script>\nimport { onMount } from 'svelte';\nimport { writable } from 'svelte/store';\nconst count = writable(5);\nconst events = writable([]);\nlet counter;\n// Reactive statement: whenever $count changes, update the Web Component\n$: if (counter && counter.count !== $count) {\ncounter.count = $count;\n}\nfunction log(message) {\nevents.update((list) => [message, ...list].slice(0, 8));\n}\n// The Web Component's own event, handled with Svelte's on: directive\nfunction handleCounterChanged(event) {\ncount.set(event.detail.count);\nlog('counter-changed: ' + event.detail.count);\n}\nonMount(() => {\nlog('Mounted; bind:this gave us <' + counter.localName + '>');\n});\n</script>\n<div class=\"demo-container\">\n<h2>Svelte + Web Components Integration</h2>\n<p>A real Svelte component driving a Web Component with bind:this, a store, and its events</p>\n<div class=\"example\">\n<h3>Dual Approach Integration</h3>\n<p>Compare calling the Web Component's own method with updating a Svelte store</p>\n<simple-counter bind:this={counter} on:counter-changed={handleCounterChanged}></simple-counter>\n<div class=\"button-group\">\n<button class=\"component-button\" on:click={() => counter.increment()}>Increment (Component)</button>\n<button class=\"svelte-button\" on:click={() => count.update((n) => n + 1)}>Increment (Svelte Store)</button>\n</div>\n<div class=\"button-group\">\n<button class=\"component-button\" on:click={() => count.update((n) => Math.max(0, n - 1))}>Decrement</button>\n<button class=\"component-button\" on:click={() => count.set(0)}>Reset</button>\n<button class=\"component-button\" on:click={() => count.update((n) => n * 2)}>Double Value</button>\n</div>\n<div class=\"events\">\n<strong>Current count in the Svelte store: {$count}</strong>\n{#each $events as event, i (i)}\n<div>{event}</div>\n{/each}\n</div>\n</div>\n</div>\n`;\n// 3. Compile the component in the browser (as the Svelte REPL does) and mount it.\n//    `svelte` is the compiler; `SvelteRuntime` holds the modules compiled code imports.\nfunction load(source) {\nconst { js } = svelte.compile(source, { name: 'App', generate: 'client' });\nconst code = js.code\n.replace(/^import\\s+['\"][^'\"]+['\"];?$/gm, '')\n.replace(/^import\\s+\\*\\s+as\\s+([\\w$]+)\\s+from\\s+['\"]([^'\"]+)['\"];?$/gm, 'const $1 = runtime(\"$2\");')\n.replace(/^import\\s+\\{([^}]*)\\}\\s+from\\s+['\"]([^'\"]+)['\"];?$/gm,\n(_, names, from) => `const {${names.replace(/\\s+as\\s+/g, ': ')}} = runtime(\"${from}\");`)\n.replace(/^export default /m, 'const Component = ');\nconst runtime = (name) => {\nif (!(name in SvelteRuntime.modules)) throw new Error(`This sandbox can only import Svelte's own modules, not \"${name}\".`);\nreturn SvelteRuntime.modules[name];\n};\nreturn new Function('runtime', `${code}\\nreturn Component;`)(runtime);\n}\ntry {\nconst { mount } = SvelteRuntime.modules.svelte;\nmount(load(App), { target: document.getElementById('app') });\nconsole.log(`Compiled and mounted with Svelte ${svelte.VERSION}`);\n} catch (error) {\nconsole.error(error.message);\n}\nSvelte + Web Components Integration on the canonical article (https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/).\nPerformance Optimization Techniques\nLazy-Loading Strategies\nLoading Web Components only when needed can significantly improve initial page load performance:\nDynamic Import Pattern\n// Only load when needed\nconst loadComponent = async () => {\nconst { MyComponent } = await import('./my-component.js');\ncustomElements.define('my-component', MyComponent);\n}\n// Call on demand or when component is about to be needed\nbutton.addEventListener('click', loadComponent);\nIntersection Observer Pattern\n// Setup observer to load component when scrolled into view\nconst observer = new IntersectionObserver((entries) => {\nentries.forEach(entry => {\nif (entry.isIntersecting) {\nimport('./components/lazy-component.js')\n.then(() => {\n// Component is now registered\nobserver.unobserve(entry.target);\n});\n}\n});\n}, { rootMargin: '100px' });\n// Observe placeholder elements\ndocument.querySelectorAll('.component-placeholder')\n.forEach(el => observer.observe(el));\nInteractive Demo: See lazy-loading in action\nHTML\n<div class=\"demo-container\">\n<h1>Lazy-Loading Web Components</h1>\n<p>This demo shows how lazy-loading can improve performance by loading components only when needed.</p>\n<div class=\"performance-comparison\">\n<div class=\"tabs\">\n<button class=\"tab active\" data-tab=\"eager\">Eager Loading</button>\n<button class=\"tab\" data-tab=\"dynamic\">Dynamic Import</button>\n<button class=\"tab\" data-tab=\"intersection\">Intersection Observer</button>\n</div>\n<div class=\"tab-content\" id=\"eager-tab\">\n<h3>Eager Loading (All At Once)</h3>\n<p>All components are loaded immediately, which can slow down initial page load.</p>\n<div class=\"metrics\">\n<div class=\"metric\">\n<div class=\"metric-title\">Components Loaded:</div>\n<div class=\"metric-value\" id=\"eager-count\">0</div>\n</div>\n<div class=\"metric\">\n<div class=\"metric-title\">Initial Load Time:</div>\n<div class=\"metric-value\" id=\"eager-time\">-</div>\n</div>\n</div>\n<button class=\"demo-button\" id=\"load-eager\">Load 10 Eager Components</button>\n<div class=\"component-container\" id=\"eager-components\"></div>\n</div>\n<div class=\"tab-content hidden\" id=\"dynamic-tab\">\n<h3>Dynamic Import (On Demand)</h3>\n<p>Components are loaded on-click, improving initial page load but requiring user action.</p>\n<div class=\"metrics\">\n<div class=\"metric\">\n<div class=\"metric-title\">Components Loaded:</div>\n<div class=\"metric-value\" id=\"dynamic-count\">0</div>\n</div>\n<div class=\"metric\">\n<div class=\"metric-title\">Load Time:</div>\n<div class=\"metric-value\" id=\"dynamic-time\">-</div>\n</div>\n</div>\n<button class=\"demo-button\" id=\"load-dynamic\">Load Components On-Demand</button>\n<div class=\"component-container\" id=\"dynamic-components\"></div>\n</div>\n<div class=\"tab-content hidden\" id=\"intersection-tab\">\n<h3>Intersection Observer (Viewport-Based)</h3>\n<p>Components load automatically when scrolled into view - the best of both worlds.</p>\n<div class=\"metrics\">\n<div class=\"metric\">\n<div class=\"metric-title\">Components Visible:</div>\n<div class=\"metric-value\" id=\"intersection-count\">0</div>\n</div>\n<div class=\"metric\">\n<div class=\"metric-title\">Load Time per Component:</div>\n<div class=\"metric-value\" id=\"intersection-time\">-</div>\n</div>\n</div>\n<p>Scroll down to see components load as they enter the viewport:</p>\n<div class=\"component-container\" id=\"intersection-components\">\n<div class=\"component-placeholder\" data-index=\"1\">Component will load when visible</div>\n<div class=\"component-placeholder\" data-index=\"2\">Component will load when visible</div>\n<div class=\"component-placeholder\" data-index=\"3\">Component will load when visible</div>\n<div class=\"component-placeholder\" data-index=\"4\">Component will load when visible</div>\n<div class=\"component-placeholder\" data-index=\"5\">Component will load when visible</div>\n</div>\n</div>\n</div>\n<div class=\"code-examples\">\n<h2>Implementation Examples</h2>\n<div class=\"code-block\">\n<h3>Dynamic Import Pattern</h3>\n<pre><code>// Only load when the user takes an action\nconst loadComponent = async () => {\nconst startTime = performance.now();\n// Dynamic import of the component module\nconst { MyComponent } = await import('./my-component.js');\n// Register the component only when needed\ncustomElements.define('my-component', MyComponent);\n// Create and add the component to the page\nconst component = document.createElement('my-component');\ndocument.querySelector('#container').appendChild(component);\nconst loadTime = performance.now() - startTime;\nconsole.log(`Component loaded in ${loadTime}ms`);\n}</code></pre>\n</div>\n<div class=\"code-block\">\n<h3>Intersection Observer Pattern</h3>\n<pre><code>// Create observer for viewport detection\nconst observer = new IntersectionObserver((entries) => {\nentries.forEach(entry => {\nif (entry.isIntersecting) {\nconst placeholder = entry.target;\n// Load component when placeholder is visible\nimport('./lazy-component.js')\n.then(module => {\n// Replace placeholder with actual component\nconst component = document.createElement('lazy-component');\nplaceholder.parentNode.replaceChild(component, placeholder);\n// Stop observing this element\nobserver.unobserve(placeholder);\n});\n}\n});\n}, { rootMargin: '100px' });\n// Start observing placeholders\ndocument.querySelectorAll('.component-placeholder')\n.forEach(el => observer.observe(el));</code></pre>\n</div>\n</div>\n</div>\nCSS\nbody {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;\nmax-width: 900px;\nmargin: 0 auto;\npadding: 20px;\nline-height: 1.6;\ncolor: #333;\n}\nh1, h2, h3 {\ncolor: #2c3e50;\n}\n.demo-container {\nmargin: 20px 0;\n}\n.performance-comparison {\nborder: 1px solid #ddd;\nborder-radius: 8px;\noverflow: hidden;\nmargin: 30px 0;\n}\n.tabs {\ndisplay: flex;\nbackground-color: #f0f0f0;\nborder-bottom: 1px solid #ddd;\n}\n.tab {\npadding: 12px 20px;\nbackground: none;\nborder: none;\ncursor: pointer;\nfont-size: 16px;\nfont-weight: 500;\ncolor: #666;\ntransition: background-color 0.2s;\n}\n.tab:hover {\nbackground-color: #e0e0e0;\n}\n.tab.active {\nbackground-color: white;\ncolor: #2c3e50;\nborder-bottom: 3px solid #3498db;\n}\n.tab-content {\npadding: 20px;\nbackground-color: white;\n}\n.hidden {\ndisplay: none;\n}\n.metrics {\ndisplay: flex;\ngap: 20px;\nmargin: 20px 0;\npadding: 15px;\nbackground-color: #f8f9fa;\nborder-radius: 8px;\n}\n.metric {\nflex: 1;\n}\n.metric-title {\nfont-weight: bold;\nmargin-bottom: 5px;\ncolor: #666;\n}\n.metric-value {\nfont-size: 24px;\ncolor: #3498db;\n}\n.demo-button {\nbackground-color: #3498db;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 10px 20px;\nfont-size: 16px;\ncursor: pointer;\ntransition: background-color 0.2s;\n}\n.demo-button:hover {\nbackground-color: #2980b9;\n}\n.component-container {\nmargin-top: 20px;\ndisplay: grid;\ngrid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\ngap: 15px;\n}\n.component-placeholder {\nheight: 150px;\ndisplay: flex;\nalign-items: center;\njustify-content: center;\ntext-align: center;\nborder: 2px dashed #ccc;\nborder-radius: 8px;\nbackground-color: #f5f5f5;\ncolor: #777;\npadding: 10px;\n}\n.code-examples {\nmargin-top: 40px;\n}\n.code-block {\nmargin: 20px 0;\n}\n.code-block pre {\nbackground-color: #f5f5f5;\npadding: 15px;\nborder-radius: 8px;\noverflow-x: auto;\nborder: 1px solid #ddd;\n}\n.code-block code {\nfont-family: 'Consolas', 'Monaco', 'Courier New', monospace;\nfont-size: 14px;\nline-height: 1.5;\n}\nJavaScript\n// HTML\n// Define fancy component to be loaded\nclass FancyComponent extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis._rendered = false;\nthis._index = this.getAttribute('data-index') || '?';\nthis._loadTime = this.getAttribute('data-load-time') || '0';\nthis.render();\n}\nconnectedCallback() {\nif (!this._rendered) {\nthis.render();\n}\n// Simulate real-world component initialization work\nthis.simulateWork();\n}\nsimulateWork() {\n// CPU-intensive operation to simulate real component work\nlet result = 0;\nfor (let i = 0; i < 100000; i++) {\nresult += Math.sqrt(i) * Math.random();\n}\nthis.shadowRoot.querySelector('.value').textContent = result.toFixed(0);\n}\nrender() {\nthis._rendered = true;\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nborder-radius: 8px;\noverflow: hidden;\nbox-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\nbackground-color: white;\ntransition: transform 0.2s;\n}\n:host(:hover) {\ntransform: translateY(-5px);\n}\n.header {\nbackground-color: #3498db;\ncolor: white;\npadding: 12px;\nfont-weight: bold;\ntext-align: center;\n}\n.body {\npadding: 15px;\ntext-align: center;\n}\n.info {\ndisplay: flex;\njustify-content: space-between;\nmargin-top: 15px;\nfont-size: 12px;\ncolor: #777;\n}\n.value {\nfont-size: 24px;\ncolor: #2c3e50;\nmargin: 10px 0;\n}\n</style>\n<div class=\"header\">Component #${this._index}</div>\n<div class=\"body\">\n<div>Calculated Value:</div>\n<div class=\"value\">Calculating...</div>\n<div class=\"info\">\n<span>Load time: ${this._loadTime}ms</span>\n<span>Memory: ~${(Math.random() * 5 + 1).toFixed(1)}MB</span>\n</div>\n</div>\n`;\n}}\n// Initialize tabs\nconst tabs = document.querySelectorAll('.tab');\ntabs.forEach(tab => {\ntab.addEventListener('click', () => {\n// Deactivate all tabs\ntabs.forEach(t => t.classList.remove('active'));\n// Hide all content\ndocument.querySelectorAll('.tab-content').forEach(content => {\ncontent.classList.add('hidden');\n});\n// Activate clicked tab\ntab.classList.add('active');\n// Show corresponding content\nconst tabId = tab.getAttribute('data-tab');\ndocument.getElementById(`${tabId}-tab`).classList.remove('hidden');\n});\n});\n// Register the component for eager loading\ncustomElements.define('fancy-component', FancyComponent);\n// Eager loading demo\ndocument.getElementById('load-eager').addEventListener('click', () => {\nconst container = document.getElementById('eager-components');\ncontainer.innerHTML = '';\nconst startTime = performance.now();\n// Create 10 components at once\nfor (let i = 1; i <= 10; i++) {\nconst component = document.createElement('fancy-component');\ncomponent.setAttribute('data-index', i);\ncontainer.appendChild(component);\n}\nconst totalTime = performance.now() - startTime;\n// Update metrics\ndocument.getElementById('eager-count').textContent = '10';\ndocument.getElementById('eager-time').textContent = `${totalTime.toFixed(1)}ms`;\n});\n// Dynamic import demo\ndocument.getElementById('load-dynamic').addEventListener('click', async () => {\nconst container = document.getElementById('dynamic-components');\ncontainer.innerHTML = '';\nconst startTime = performance.now();\n// Load components one by one with a small delay to better see the process\nfor (let i = 1; i <= 5; i++) {\nconst componentStartTime = performance.now();\n// Simulate dynamic import (we already have the component)\nawait new Promise(resolve => setTimeout(resolve, 200));\nconst component = document.createElement('fancy-component');\ncomponent.setAttribute('data-index', i);\ncomponent.setAttribute('data-load-time', (performance.now() - componentStartTime).toFixed(1));\ncontainer.appendChild(component);\n// Update count\ndocument.getElementById('dynamic-count').textContent = i;\n}\nconst totalTime = performance.now() - startTime;\ndocument.getElementById('dynamic-time').textContent = `${totalTime.toFixed(1)}ms`;\n});\n// Intersection Observer demo\nconst observerOptions = {\nrootMargin: '0px',\nthreshold: 0.1 };\nconst componentObserver = new IntersectionObserver(entries => {\nentries.forEach(entry => {\nif (entry.isIntersecting) {\nconst placeholder = entry.target;\nconst index = placeholder.getAttribute('data-index');\n// Measure loading time\nconst startTime = performance.now();\n// Simulate dynamic import\nsetTimeout(() => {\nconst component = document.createElement('fancy-component');\ncomponent.setAttribute('data-index', index);\nconst loadTime = performance.now() - startTime;\ncomponent.setAttribute('data-load-time', loadTime.toFixed(1));\n// Replace placeholder with actual component\nplaceholder.parentNode.replaceChild(component, placeholder);\n// Update metrics\nconst countEl = document.getElementById('intersection-count');\ncountEl.textContent = parseInt(countEl.textContent || '0') + 1;\ndocument.getElementById('intersection-time').textContent = `${loadTime.toFixed(1)}ms`;\n}, 300); // Simulated import delay\n// Stop observing this element\ncomponentObserver.unobserve(placeholder);\n}\n});\n}, observerOptions);\n// Start observing placeholders\ndocument.querySelectorAll('.component-placeholder').forEach(placeholder => {\ncomponentObserver.observe(placeholder);\n});\n// Initialize counters\ndocument.getElementById('eager-count').textContent = '0';\ndocument.getElementById('dynamic-count').textContent = '0';\ndocument.getElementById('intersection-count').textContent = '0';\nLazy-Loading Web Components on the canonical article (https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/).\nShadow DOM Performance\nThe Shadow DOM provides encapsulation but comes with performance considerations:\nContainment for Performance\n:host {\ncontain: content; /* Limit style recalculation scope */\ncontainer-type: inline-size; /* For container queries */\n}\nMinimizing Selectors\n/* Avoid overly complex selectors that cross shadow boundaries */\n:host ::slotted(*) > * { /* Expensive */ }\n/* Better to use direct, simple selectors */\n.my-item { /* More efficient */ }\nSlot Change Optimization\n// Listen only when needed\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' }).innerHTML = '<slot></slot>';\nthis.shadowRoot.querySelector('slot')\n.addEventListener('slotchange', this._handleSlotChange);\n}\n// Debounce frequent updates\n_handleSlotChange = debounce(() => {\nthis._processSlottedChildren();\n}, 100);\nRender Optimization\nEfficient rendering patterns can dramatically improve performance:\nBatched DOM Updates\n// Poor: Multiple separate DOM operations\nthis.shadowRoot.querySelector('.title').textContent = title;\nthis.shadowRoot.querySelector('.desc').textContent = description;\n// Better: Batch DOM operations with DocumentFragment\nupdate(data) {\nconst fragment = document.createDocumentFragment();\n// Build complete update in memory\nconst title = document.createElement('div');\ntitle.className = 'title';\ntitle.textContent = data.title;\nfragment.appendChild(title);\n// etc...\n// Single DOM operation\nconst container = this.shadowRoot.querySelector('.container');\ncontainer.innerHTML = '';\ncontainer.appendChild(fragment);\n}\nRequestAnimationFrame for Visual Updates\nupdateVisuals() {\n// Schedule visual updates in animation frame\nrequestAnimationFrame(() => {\nthis.elements.forEach(el => {\nel.style.transform = `translate(${this.x}px, ${this.y}px)`;\n});\n});\n}\nInteractive Demo: Compare optimized vs. unoptimized components\nHTML\n<div class=\"demo-container\">\n<h1>Web Component Render Optimization</h1>\n<p>Compare optimized vs. unoptimized rendering approaches in Web Components</p>\n<div class=\"tabs\">\n<button class=\"tab active\" data-tab=\"basic\">Basic Comparison</button>\n<button class=\"tab\" data-tab=\"techniques\">Optimization Techniques</button>\n</div>\n<div class=\"tab-content\" id=\"basic-tab\">\n<h2>Performance Comparison</h2>\n<p>Watch the difference between optimized and unoptimized component updates when handling frequent state changes.</p>\n<div class=\"control-panel\">\n<div class=\"control\">\n<label for=\"update-frequency\">Update Frequency:</label>\n<select id=\"update-frequency\">\n<option value=\"500\">Low (500ms)</option>\n<option value=\"100\">Medium (100ms)</option>\n<option value=\"16\" selected>High (16ms)</option>\n</select>\n</div>\n<div class=\"control\">\n<label for=\"item-count\">Item Count:</label>\n<select id=\"item-count\">\n<option value=\"10\">Few (10)</option>\n<option value=\"50\" selected>Medium (50)</option>\n<option value=\"100\">Many (100)</option>\n</select>\n</div>\n<button id=\"start-test\" class=\"demo-button\">Start Comparison</button>\n<button id=\"stop-test\" class=\"demo-button\" disabled>Stop</button>\n</div>\n<div class=\"performance-results\">\n<div class=\"result-box\">\n<h3>Unoptimized Component</h3>\n<div class=\"metrics\">\n<div class=\"metric\">\n<div class=\"metric-title\">Frame Rate:</div>\n<div class=\"metric-value\" id=\"unoptimized-fps\">0 FPS</div>\n</div>\n<div class=\"metric\">\n<div class=\"metric-title\">Updates:</div>\n<div class=\"metric-value\" id=\"unoptimized-updates\">0</div>\n</div>\n</div>\n<div id=\"unoptimized-container\" class=\"component-container\">\n<div class=\"placeholder\">Click \"Start Comparison\" to initialize</div>\n</div>\n</div>\n<div class=\"result-box\">\n<h3>Optimized Component</h3>\n<div class=\"metrics\">\n<div class=\"metric\">\n<div class=\"metric-title\">Frame Rate:</div>\n<div class=\"metric-value\" id=\"optimized-fps\">0 FPS</div>\n</div>\n<div class=\"metric\">\n<div class=\"metric-title\">Updates:</div>\n<div class=\"metric-value\" id=\"optimized-updates\">0</div>\n</div>\n</div>\n<div id=\"optimized-container\" class=\"component-container\">\n<div class=\"placeholder\">Click \"Start Comparison\" to initialize</div>\n</div>\n</div>\n</div>\n</div>\n<div class=\"tab-content hidden\" id=\"techniques-tab\">\n<h2>Optimization Techniques</h2>\n<div class=\"technique\">\n<h3>1. Batched DOM Updates</h3>\n<p>Instead of updating the DOM multiple times, batch changes into a single update using DocumentFragment.</p>\n<div class=\"code-comparison\">\n<div class=\"code-block\">\n<h4>Unoptimized Approach</h4>\n<pre><code>// Unoptimized: Multiple separate DOM operations\nupdateItems(items) {\n// Clear existing items\nthis.listElement.innerHTML = '';\n// Add each item individually (multiple DOM operations)\nitems.forEach(item => {\nconst li = document.createElement('li');\nli.textContent = item.text;\nthis.listElement.appendChild(li);\n});\n}</code></pre>\n</div>\n<div class=\"code-block\">\n<h4>Optimized Approach</h4>\n<pre><code>// Optimized: Single DOM operation with DocumentFragment\nupdateItems(items) {\n// Create fragment (in-memory container)\nconst fragment = document.createDocumentFragment();\n// Build complete structure in memory\nitems.forEach(item => {\nconst li = document.createElement('li');\nli.textContent = item.text;\nfragment.appendChild(li);\n});\n// Single DOM operation\nthis.listElement.innerHTML = '';\nthis.listElement.appendChild(fragment);\n}</code></pre>\n</div>\n</div>\n</div>\n<div class=\"technique\">\n<h3>2. RequestAnimationFrame for Visual Updates</h3>\n<p>Schedule visual updates to align with the browser's render cycle for smoother animations.</p>\n<div class=\"code-comparison\">\n<div class=\"code-block\">\n<h4>Unoptimized Approach</h4>\n<pre><code>// Unoptimized: Direct style changes\nupdatePosition(x, y) {\n// This might cause layout thrashing if called frequently\nthis.element.style.transform = `translate(${x}px, ${y}px)`;\n}</code></pre>\n</div>\n<div class=\"code-block\">\n<h4>Optimized Approach</h4>\n<pre><code>// Optimized: RAF for smooth visual updates\nupdatePosition(x, y) {\n// Store the values to update\nthis._pendingX = x;\nthis._pendingY = y;\n// Only schedule update if none is pending\nif (!this._updateScheduled) {\nthis._updateScheduled = true;\nrequestAnimationFrame(() => {\n// Apply all pending updates at once\nthis.element.style.transform =\n`translate(${this._pendingX}px, ${this._pendingY}px)`;\nthis._updateScheduled = false;\n});\n}\n}</code></pre>\n</div>\n</div>\n</div>\n<div class=\"technique\">\n<h3>3. Debouncing Property Changes</h3>\n<p>Limit the frequency of updates when properties change rapidly.</p>\n<div class=\"code-comparison\">\n<div class=\"code-block\">\n<h4>Unoptimized Approach</h4>\n<pre><code>// Unoptimized: Update on every property change\nset value(newValue) {\nthis._value = newValue;\nthis.render(); // May be called too frequently\n}</code></pre>\n</div>\n<div class=\"code-block\">\n<h4>Optimized Approach</h4>\n<pre><code>// Optimized: Debounced updates\nconstructor() {\nsuper();\nthis._value = 0;\nthis._debounceTimeout = null;\n}\nset value(newValue) {\nthis._value = newValue;\n// Clear previous timeout\nclearTimeout(this._debounceTimeout);\n// Schedule a new update with a delay\nthis._debounceTimeout = setTimeout(() => {\nthis.render();\n}, 16); // Roughly one frame at 60fps\n}</code></pre>\n</div>\n</div>\n</div>\n</div>\n</div>\nCSS\nbody {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;\nmax-width: 1200px;\nmargin: 0 auto;\npadding: 20px;\nline-height: 1.6;\ncolor: #333;\nbackground-color: #f8f9fa;\n}\nh1, h2, h3, h4 {\ncolor: #2c3e50;\n}\n.demo-container {\nmargin: 20px 0;\n}\n.tabs {\ndisplay: flex;\ngap: 2px;\nmargin-bottom: 20px;\n}\n.tab {\npadding: 10px 20px;\nbackground-color: #e9ecef;\nborder: none;\nborder-radius: 4px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.2s;\n}\n.tab:hover {\nbackground-color: #dee2e6;\n}\n.tab.active {\nbackground-color: #3498db;\ncolor: white;\n}\n.tab-content {\nbackground-color: white;\npadding: 20px;\nborder-radius: 8px;\nbox-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n}\n.hidden {\ndisplay: none;\n}\n.control-panel {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 15px;\nalign-items: center;\nbackground-color: #f0f7ff;\npadding: 15px;\nborder-radius: 8px;\nmargin-bottom: 20px;\n}\n.control {\ndisplay: flex;\nalign-items: center;\ngap: 8px;\n}\nlabel {\nfont-weight: 500;\n}\nselect {\npadding: 8px;\nborder-radius: 4px;\nborder: 1px solid #ced4da;\nbackground-color: white;\nfont-size: 16px;\n}\n.demo-button {\nbackground-color: #3498db;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px 16px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.2s;\n}\n.demo-button:hover {\nbackground-color: #2980b9;\n}\n.demo-button:disabled {\nbackground-color: #95a5a6;\ncursor: not-allowed;\n}\n.performance-results {\ndisplay: grid;\ngrid-template-columns: 1fr 1fr;\ngap: 20px;\n}\n.result-box {\nbackground-color: white;\npadding: 20px;\nborder-radius: 8px;\nbox-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n}\n.metrics {\ndisplay: flex;\ngap: 20px;\nmargin: 15px 0;\npadding: 15px;\nbackground-color: #f8f9fa;\nborder-radius: 8px;\n}\n.metric {\nflex: 1;\n}\n.metric-title {\nfont-weight: bold;\nmargin-bottom: 5px;\ncolor: #666;\n}\n.metric-value {\nfont-size: 24px;\ncolor: #3498db;\n}\n.component-container {\nborder: 1px solid #e9ecef;\nborder-radius: 8px;\npadding: 15px;\nheight: 300px;\noverflow: auto;\nbackground-color: #f8f9fa;\n}\n.placeholder {\ndisplay: flex;\nheight: 100%;\nalign-items: center;\njustify-content: center;\ncolor: #7f8c8d;\nfont-style: italic;\n}\n.technique {\nmargin-bottom: 40px;\npadding-bottom: 20px;\nborder-bottom: 1px solid #e9ecef;\n}\n.code-comparison {\ndisplay: grid;\ngrid-template-columns: 1fr 1fr;\ngap: 20px;\nmargin-top: 15px;\n}\n.code-block {\nbackground-color: #f8f9fa;\nborder-radius: 8px;\noverflow: hidden;\nborder: 1px solid #e9ecef;\n}\n.code-block h4 {\nmargin: 0;\npadding: 10px 15px;\nbackground-color: #e9ecef;\ncolor: #2c3e50;\n}\n.code-block pre {\nmargin: 0;\npadding: 15px;\noverflow-x: auto;\n}\n.code-block code {\nfont-family: 'Consolas', 'Monaco', 'Courier New', monospace;\n}\n/* Mobile responsiveness */\n@media (max-width: 768px) {\n.performance-results, .code-comparison {\ngrid-template-columns: 1fr;\n}\n}\nJavaScript\nclass UnoptimizedGrid extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis.items = [];\nthis.updateCount = 0;\nthis.lastFrameTime = 0;\nthis.frameCount = 0;\nthis.fps = 0;\n// Initialize the component\nthis.render();\n}\n// Called when the element is added to the document\nconnectedCallback() {\n// Start FPS counter\nthis.startFpsCounter();\n}\n// Called when the element is removed from the document\ndisconnectedCallback() {\n// Clean up\nthis.stopFpsCounter();\n}\n// Method to update the data items\nupdateData(items) {\nthis.items = items;\nthis.updateCount++;\n// Update the metrics display\nthis.dispatchEvent(new CustomEvent('metrics-update', {\ndetail: {\nupdates: this.updateCount,\nfps: this.fps } }));\n// Render immediately - this is unoptimized\nthis.render();\n}\n// Unoptimized render method - rebuilds everything on each update\nrender() {\n// Create the basic structure if it doesn't exist\nif (!this.shadowRoot.querySelector('.grid')) {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nwidth: 100%;\n}\n.grid {\ndisplay: grid;\ngrid-template-columns: repeat(auto-fill, minmax(80px, 1fr));\ngap: 10px;\npadding: 5px;\n}\n.grid-item {\nbackground-color: #3498db;\ncolor: white;\npadding: 10px;\nborder-radius: 4px;\ntext-align: center;\ntransition: background-color 0.2s;\n}\n.grid-item:hover {\nbackground-color: #2980b9;\n}\n</style>\n<div class=\"grid\"></div>\n`;\n}\nconst grid = this.shadowRoot.querySelector('.grid');\n// Unoptimized: Clear and rebuild everything\ngrid.innerHTML = '';\n// Unoptimized: Add each item individually (causes multiple reflows)\nthis.items.forEach(item => {\nconst itemEl = document.createElement('div');\nitemEl.className = 'grid-item';\nitemEl.textContent = item.value;\ngrid.appendChild(itemEl);\n});\n}\n// FPS tracking methods\nstartFpsCounter() {\nthis.frameCount = 0;\nthis.lastFrameTime = performance.now();\nthis.fpsInterval = setInterval(() => {\nconst now = performance.now();\nconst delta = now - this.lastFrameTime;\nthis.fps = Math.round(this.frameCount * 1000 / delta);\n// Reset for next calculation\nthis.frameCount = 0;\nthis.lastFrameTime = now;\n// Dispatch metrics update event\nthis.dispatchEvent(new CustomEvent('metrics-update', {\ndetail: {\nupdates: this.updateCount,\nfps: this.fps } }));\n}, 1000);\n}\nstopFpsCounter() {\nclearInterval(this.fpsInterval);\n}\n// Called for each frame render\ncountFrame() {\nthis.frameCount++;\n}}\n// Optimized Data Grid Component\nclass OptimizedGrid extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis.items = [];\nthis.updateCount = 0;\nthis.lastFrameTime = 0;\nthis.frameCount = 0;\nthis.fps = 0;\n// Flag to track if a render has been scheduled\nthis._updatePending = false;\n// Initialize the component\nthis.render();\n}\n// Called when the element is added to the document\nconnectedCallback() {\n// Start FPS counter\nthis.startFpsCounter();\n}\n// Called when the element is removed from the document\ndisconnectedCallback() {\n// Clean up\nthis.stopFpsCounter();\n}\n// Method to update the data items\nupdateData(items) {\nthis.items = items;\nthis.updateCount++;\n// Update the metrics display\nthis.dispatchEvent(new CustomEvent('metrics-update', {\ndetail: {\nupdates: this.updateCount,\nfps: this.fps } }));\n// Optimized: Schedule a render using requestAnimationFrame\nthis.scheduleRender();\n}\n// Optimization 1: Schedule renders for better performance\nscheduleRender() {\n// Only schedule a new render if one isn't already pending\nif (!this._updatePending) {\nthis._updatePending = true;\nrequestAnimationFrame(() => {\nthis.render();\nthis._updatePending = false;\n});\n}\n}\n// Optimized render method - uses DocumentFragment for batch updates\nrender() {\n// Create the basic structure if it doesn't exist\nif (!this.shadowRoot.querySelector('.grid')) {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nwidth: 100%;\n}\n.grid {\ndisplay: grid;\ngrid-template-columns: repeat(auto-fill, minmax(80px, 1fr));\ngap: 10px;\npadding: 5px;\n}\n.grid-item {\nbackground-color: #3498db;\ncolor: white;\npadding: 10px;\nborder-radius: 4px;\ntext-align: center;\ntransition: background-color 0.2s;\n}\n.grid-item:hover {\nbackground-color: #2980b9;\n}\n</style>\n<div class=\"grid\"></div>\n`;\n}\nconst grid = this.shadowRoot.querySelector('.grid');\n// Optimization 2: Use DocumentFragment for batched DOM updates\nconst fragment = document.createDocumentFragment();\n// Build all elements in memory first\nthis.items.forEach(item => {\nconst itemEl = document.createElement('div');\nitemEl.className = 'grid-item';\nitemEl.textContent = item.value;\nfragment.appendChild(itemEl);\n});\n// Optimization 3: Single DOM operation\ngrid.innerHTML = '';\ngrid.appendChild(fragment);\n}\n// FPS tracking methods\nstartFpsCounter() {\nthis.frameCount = 0;\nthis.lastFrameTime = performance.now();\nthis.fpsInterval = setInterval(() => {\nconst now = performance.now();\nconst delta = now - this.lastFrameTime;\nthis.fps = Math.round(this.frameCount * 1000 / delta);\n// Reset for next calculation\nthis.frameCount = 0;\nthis.lastFrameTime = now;\n// Dispatch metrics update event\nthis.dispatchEvent(new CustomEvent('metrics-update', {\ndetail: {\nupdates: this.updateCount,\nfps: this.fps } }));\n}, 1000);\n}\nstopFpsCounter() {\nclearInterval(this.fpsInterval);\n}\n// Called for each frame render\ncountFrame() {\nthis.frameCount++;\n}}\n// Register our Web Components\ncustomElements.define('unoptimized-grid', UnoptimizedGrid);\ncustomElements.define('optimized-grid', OptimizedGrid);\n// Initialize demo\ndocument.addEventListener('DOMContentLoaded', () => {\n// Setup tab navigation\nconst tabs = document.querySelectorAll('.tab');\ntabs.forEach(tab => {\ntab.addEventListener('click', () => {\n// Deactivate all tabs\ntabs.forEach(t => t.classList.remove('active'));\n// Hide all content\ndocument.querySelectorAll('.tab-content').forEach(content => {\ncontent.classList.add('hidden');\n});\n// Activate clicked tab\ntab.classList.add('active');\n// Show corresponding content\nconst tabId = tab.getAttribute('data-tab');\ndocument.getElementById(`${tabId}-tab`).classList.remove('hidden');\n});\n});\nlet unoptimizedGrid;\nlet optimizedGrid;\nlet updateInterval;\nlet animationFrameId;\n// Performance test controls\nconst startButton = document.getElementById('start-test');\nconst stopButton = document.getElementById('stop-test');\nconst frequencySelect = document.getElementById('update-frequency');\nconst itemCountSelect = document.getElementById('item-count');\n// Function to generate random test data\nfunction generateTestData(count) {\nconst data = [];\nfor (let i = 0; i < count; i++) {\ndata.push({\nid: i,\nvalue: Math.floor(Math.random() * 100) });\n}\nreturn data;\n}\n// Update metrics displays\nfunction setupMetricsListeners() {\nunoptimizedGrid.addEventListener('metrics-update', e => {\ndocument.getElementById('unoptimized-fps').textContent = `${e.detail.fps} FPS`;\ndocument.getElementById('unoptimized-updates').textContent = e.detail.updates;\n});\noptimizedGrid.addEventListener('metrics-update', e => {\ndocument.getElementById('optimized-fps').textContent = `${e.detail.fps} FPS`;\ndocument.getElementById('optimized-updates').textContent = e.detail.updates;\n});\n}\n// Animation frame counter\nfunction countFrames() {\nunoptimizedGrid?.countFrame();\noptimizedGrid?.countFrame();\nanimationFrameId = requestAnimationFrame(countFrames);\n}\n// Start test button\nstartButton.addEventListener('click', () => {\n// Create components if they don't exist\nconst unoptimizedContainer = document.getElementById('unoptimized-container');\nconst optimizedContainer = document.getElementById('optimized-container');\n// Clear containers\nunoptimizedContainer.innerHTML = '';\noptimizedContainer.innerHTML = '';\n// Create components\nunoptimizedGrid = document.createElement('unoptimized-grid');\noptimizedGrid = document.createElement('optimized-grid');\n// Add to DOM\nunoptimizedContainer.appendChild(unoptimizedGrid);\noptimizedContainer.appendChild(optimizedGrid);\n// Setup metrics listeners\nsetupMetricsListeners();\n// Start frame counter\nanimationFrameId = requestAnimationFrame(countFrames);\n// Get update frequency and item count\nconst updateFrequency = parseInt(frequencySelect.value);\nconst itemCount = parseInt(itemCountSelect.value);\n// Start the update interval\nupdateInterval = setInterval(() => {\n// Generate new random data\nconst data = generateTestData(itemCount);\n// Update both components\nunoptimizedGrid.updateData(data);\noptimizedGrid.updateData(data);\n}, updateFrequency);\n// Update button states\nstartButton.disabled = true;\nstopButton.disabled = false;\nfrequencySelect.disabled = true;\nitemCountSelect.disabled = true;\n});\n// Stop test button\nstopButton.addEventListener('click', () => {\n// Clear the update interval\nclearInterval(updateInterval);\ncancelAnimationFrame(animationFrameId);\n// Update button states\nstartButton.disabled = false;\nstopButton.disabled = true;\nfrequencySelect.disabled = false;\nitemCountSelect.disabled = false;\n});\n});\nWeb Component Render Optimization on the canonical article (https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/).\nAdvanced Patterns and Best Practices\nState Management Across Boundaries\nManaging state between Web Components and frameworks requires thoughtful patterns:\n1. The Context Pattern\nCreate a shared context accessible to all components:\n// context.js - Framework agnostic state management\nexport class ComponentContext {\nconstructor() {\nthis._data = {};\nthis._listeners = new Map();\n}\nget(key) {\nreturn this._data[key];\n}\nset(key, value) {\nthis._data[key] = value;\nif (this._listeners.has(key)) {\nthis._listeners.get(key).forEach(callback => callback(value));\n}\n}\nsubscribe(key, callback) {\nif (!this._listeners.has(key)) {\nthis._listeners.set(key, new Set());\n}\nthis._listeners.get(key).add(callback);\nreturn () => {\nthis._listeners.get(key).delete(callback);\n};\n}\n}\n// Create shared instance\nexport const context = new ComponentContext();\nUsage in components:\nimport { context } from './context.js';\nclass MyComponent extends HTMLElement {\nconnectedCallback() {\n// Subscribe to state changes\nthis._unsubscribe = context.subscribe('userData',\n(data) => this._updateFromState(data));\n// Initial state\nthis._updateFromState(context.get('userData'));\n}\ndisconnectedCallback() {\n// Clean up subscription\nthis._unsubscribe();\n}\n_updateState(data) {\n// Update shared state\ncontext.set('userData', data);\n}\n}\n2. The Message Bus Pattern\nFor loosely coupled components, a pub/sub event bus provides flexible communication:\n// event-bus.js\nexport class EventBus {\nconstructor() {\nthis.listeners = {};\n}\non(event, callback) {\nif (!this.listeners[event]) {\nthis.listeners[event] = [];\n}\nthis.listeners[event].push(callback);\nreturn () => this.off(event, callback);\n}\noff(event, callback) {\nif (this.listeners[event]) {\nthis.listeners[event] = this.listeners[event]\n.filter(cb => cb !== callback);\n}\n}\nemit(event, data) {\nif (this.listeners[event]) {\nthis.listeners[event].forEach(callback => {\ncallback(data);\n});\n}\n}\n}\n// Shared instance\nexport const eventBus = new EventBus();\nServer-Side Rendering with Web Components\nServer-side rendering improves initial load performance. With Declarative Shadow DOM (<template shadowrootmode=\"open\">), the server can send a component's shadow tree as plain HTML:\n<!-- Server-rendered output -->\n<my-component>\n<template shadowrootmode=\"open\">\n<style>\n/* Shadow DOM styles */\n.card {\npadding: 16px;\nborder: 1px solid #ddd;\n}\n</style>\n<div class=\"card\">\n<slot></slot>\n</div>\n</template>\n<p>This content is slotted from light DOM</p>\n</my-component>\n<!-- Polyfill for browsers without Declarative Shadow DOM -->\n<script>\nif (!HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode')) {\ndocument.querySelectorAll('template[shadowrootmode]').forEach(template => {\nconst mode = template.getAttribute('shadowrootmode');\nconst shadowRoot = template.parentNode.attachShadow({ mode });\nshadowRoot.appendChild(template.content);\ntemplate.remove();\n});\n}\n</script>\nTesting Strategies\nTesting Web Components requires specialized approaches:\n1. Component Unit Testing\n// Using web-test-runner and @open-wc/testing\nimport { html, fixture, expect } from '@open-wc/testing';\nimport '../src/my-component.js';\ndescribe('MyComponent', () => {\nit('renders with default values', async () => {\nconst el = await fixture(html`<my-component></my-component>`);\nexpect(el.shadowRoot.querySelector('.title').textContent)\n.to.equal('Default Title');\n});\nit('updates when properties change', async () => {\nconst el = await fixture(html`<my-component></my-component>`);\nel.title = 'New Title';\nawait el.updateComplete; // For LitElement components\nexpect(el.shadowRoot.querySelector('.title').textContent)\n.to.equal('New Title');\n});\nit('fires custom events', async () => {\nconst el = await fixture(html`<my-component></my-component>`);\n// Setup event listener\nlet eventFired = false;\nel.addEventListener('custom-event', () => eventFired = true);\n// Trigger event\nel.shadowRoot.querySelector('button').click();\nexpect(eventFired).to.be.true;\n});\n});\n2. Integration Testing with Frameworks\n// React Testing Library example\nimport { render, fireEvent, screen } from '@testing-library/react';\nimport MyReactComponent from '../src/MyReactComponent';\n// Register Web Component if not done globally\nimport '../src/web-components/my-element.js';\ntest('React component interacts with Web Component', async () => {\nrender(<MyReactComponent initialData=\"test\" />);\n// Interact with the wrapped Web Component\nfireEvent.click(screen.getByRole('button'));\n// Check the result\nexpect(screen.getByText('Success')).toBeInTheDocument();\n});\nConclusion\nYou can now bridge a framework and a web component:\nPass objects and arrays as properties (through a ref in React), not as attributes.\nListen for custom events with addEventListener, and remove the listener in the effect's cleanup.\nWrap that once in a small component so the rest of your app never thinks about it.\nA framework can now hand a component an object and hear its events, which was the whole problem.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2025-04-13T15:21:21Z",
      "id": "https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/",
      "image": "https://mnixblog.netlify.app/images/advanced-patterns-and-integration-with-frameworks/cover-v2.jpg",
      "summary": "Pass objects from React to a custom element without getting [object Object], and handle events across the framework boundary.",
      "tags": [
        "Web Components",
        "Frameworks"
      ],
      "title": "Advanced Patterns & Integration with Frameworks",
      "url": "https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/"
    }
  ],
  "language": "en",
  "title": "Journey to Code",
  "version": "https://jsonfeed.org/version/1.1"
}