<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Cecilie Vennevik's blog</title>
    <link>https://www.cvennevik.no/blog/</link>
    <description>Recent posts on Cecilie Vennevik's blog</description>
    <language>en</language>
    <lastBuildDate>Sun, 25 Jan 2026 15:30:00 GMT</lastBuildDate>
    <atom:link href="https://www.cvennevik.no/blog/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>Crystal Spire #17: In my defense</title>
      <link>https://www.cvennevik.no/blog/crystal-spire-17-in-my-defense/</link>
      <pubDate>Sun, 25 Jan 2026 15:30:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/crystal-spire-17-in-my-defense/</guid>
      <content:encoded><![CDATA[<p>I'm pretty sure we'll actually implement <code>resolveAction()</code> now. But first: Remove this TODO comment that's now resolved.</p>
<pre><code class="language-js">// TODO: Test
function getActions(gameState) {
</code></pre>
<p>Commit: <em>&quot;Remove resolved TODO comment&quot;</em>.</p>
<p>Alright. Okay. Let's start off by copy-pasting the part we got to last time, before we stumbled into the action legality issue, and merging it with our new <code>isLegalAction()</code> code:</p>
<pre><code class="language-js">// index.js
function resolveAction(gameState, action) {
    let resultingGameState = {
        ...gameState,
        block: gameState.block + 5,
        energy: gameState.energy - 1
    };
    return [{ gameState: resultingGameState, probability: 1 }];
}

// ... snipped code ...

// Export in Node.js for testing
if (typeof module === 'object' &amp;&amp; module != null &amp;&amp; 'exports' in module) {
    module.exports = { getActions, isLegalAction, resolveAction };
}
</code></pre>
<pre><code class="language-js">// test.mjs
import { getActions, isLegalAction, resolveAction } from './index.js';

// ... snipped code ...

describe('resolveAction()', () =&gt; {
    it('resolves Defend', () =&gt; {
        fc.assert(
            fc.property(getArbGameState(), gameState =&gt; {
                let action = { name: 'Play Defend' };
                let result = resolveAction(gameState, action);
                let sumOfProbabilities = result.reduce((acc, x) =&gt; acc + x.probability, 0);
                assert.equal(sumOfProbabilities, 1);
                for (let outcome of result) {
                    assert.equal(outcome.gameState.block, gameState.block + 5);
                    assert.equal(outcome.gameState.energy, gameState.energy - 1);

                    let firstDefendIndex = gameState.hand.indexOf('Defend');
                    let expectedHand = gameState.hand.toSpliced(firstDefendIndex, 1);
                    assert.deepEqual(outcome.gameState.hand, expectedHand);
                }
            })
        );
    });
});
</code></pre>
<p>A few things to note:</p>
<ul>
<li><code>resolveAction()</code> only handles &quot;Play Defend&quot;, and even that is only half-implemented, we still need to move Defend from the hand to the discard pile.</li>
<li>The test is currently <em>trying</em> to assert that Defend is removed from the hand.</li>
<li>The test looks like a mess to me, oh moly.</li>
<li>We are still trying to resolve &quot;Play Defend&quot; for game states where it isn't legal.</li>
</ul>
<p>Let's comment out that last section of the assertion loop (<em>shudder</em>) so we get a passing test, and make <code>resolveAction()</code> handle illegal actions in a helpful way. For ease of debugging, <code>resolveAction()</code> should always check if the action is legal, and if not, throw an error (this is inefficient, but we'll worry about performance when we have a working system).</p>
<pre><code class="language-js">for (let outcome of result) {
    assert.equal(outcome.gameState.block, gameState.block + 5);
    assert.equal(outcome.gameState.energy, gameState.energy - 1);

    // TODO:
    // - Remove Defend from hand
    // - Add Defend to discard pile
    // let firstDefendIndex = gameState.hand.indexOf('Defend');
    // let expectedHand = gameState.hand.toSpliced(firstDefendIndex, 1);
    // assert.deepEqual(outcome.gameState.hand, expectedHand);
}
</code></pre>
<p>Cool, passing tests. Add a new failing test: <code>resolveAction()</code> should throw errors for illegal moves.</p>
<pre><code class="language-js">it('throws an error when the action is illegal', () =&gt; {
    fc.assert(
        fc.property(getArbGameState(), arbAction, (gameState, action) =&gt; {
            fc.pre(!isLegalAction(gameState, action));

            let error = null;
            try {
                resolveAction(gameState, action);
            } catch (e) {
                error = e;
            }

            assert.ok(error);
            assert.match(error.message, /illegal action/);
        })
    );
});
</code></pre>
<p><code>fc.pre()</code> comes in handy here to filter for illegal gameState-action combinations in one swing. Now, this test fails immediately on <code>assert.ok(error)</code> because error is falsy. Let's try to fix it in one swing:</p>
<pre><code class="language-js">function resolveAction(gameState, action) {
    if (!isLegalAction(gameState, action)) throw new Error('Cannot resolve an illegal action');

    let resultingGameState = {
        ...gameState,
        block: gameState.block + 5,
        energy: gameState.energy - 1
    };

    return [{ gameState: resultingGameState, probability: 1 }];
}
</code></pre>
<p>Hey, the test passes now, but &quot;resolves Defend&quot; fails now. Let's make &quot;Play Defend&quot; being legal a precondition:</p>
<pre><code class="language-js">it('resolves Defend', () =&gt; {
    fc.assert(
        fc.property(getArbGameState(), gameState =&gt; {
            let action = { name: 'Play Defend' };
            fc.pre(isLegalAction(gameState, action));
            // ... rest of test ...
        })
    );
});
</code></pre>
<p>There we go, test passes again. Though I'm curious - my editor told me the Error constructor has a second &quot;options&quot; parameter? Could we pass more useful context there, like what the illegal gameState plus action was?</p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Error">MDN says no</a>. That's disappointing. Maybe we can make an error subclass for that instead, then? Is that allowed?</p>
<pre><code class="language-js">class IllegalActionError extends Error {
    constructor(gameState, action) {
        super('IllegalActionError: Tried to resolve a game state with an illegal action.')
        this.gameState = gameState;
        this.action = action;
    }
}
</code></pre>
<pre><code class="language-js">if (!isLegalAction(gameState, action)) throw new IllegalActionError(gameState, action);
</code></pre>
<p>That works, but will the properties show in my test runner when a test fails? Let's remove the if-check and always throw an error on <code>resolveAction()</code> to find out.</p>
<pre><code>  Error: Property failed after 1 tests
  { seed: 1896921034, path: &quot;0:0:1:0:4:3:3:3&quot;, endOnFailure: true }
  Counterexample: [{&quot;hp&quot;:1,&quot;maxHp&quot;:80,&quot;block&quot;:0,&quot;energy&quot;:1,&quot;maxEnergy&quot;:3,&quot;hand&quot;:[&quot;Defend&quot;],&quot;drawPile&quot;:[],&quot;discardPile&quot;:[],&quot;relics&quot;:[],&quot;enemies&quot;:[{&quot;name&quot;:&quot;Jaw Worm&quot;,&quot;hp&quot;:42,&quot;maxHp&quot;:42,&quot;block&quot;:0,&quot;nextMove&quot;:[],&quot;moveHistory&quot;:[],&quot;buffs&quot;:[],&quot;debuffs&quot;:[]}]}]
  Shrunk 7 time(s)
  
  Hint: Enable verbose mode in order to have the list of all failing values encountered during the run
      -- snipped stack trace -- {
    [cause]: Error: IllegalActionError: Tried to resolve a game state with an illegal action.
        -- snipped stack trace -- {
      gameState: { hp: 1, maxHp: 80, block: 0, energy: 1, maxEnergy: 3, hand: [Array], drawPile: [], discardPile: [], relics: [], enemies: [Array] },
      action: { name: 'Play Defend' }
    }
  }
</code></pre>
<p>Hey! It does show up! That's lovely, let's keep it this way, I have a feeling it may come in handy later. Also, we can change our &quot;throws an error&quot; test to assert for a specific error class instead of a specific error message (right after returning the if-check and exporting and importing the class):</p>
<pre><code class="language-js">it('throws an IllegalActionError when the action is illegal', () =&gt; {
    fc.assert(
        fc.property(getArbGameState(), arbAction, (gameState, action) =&gt; {
            fc.pre(!isLegalAction(gameState, action));

            let error = null;
            try {
                resolveAction(gameState, action);
            } catch (e) {
                error = e;
            }

            assert.ok(error instanceof IllegalActionError);
        })
    );
});
</code></pre>
<p>Nice stuff, test passes. Okay. Back to resolving Defend. Reinstate the assertion to remove it from the hand:</p>
<pre><code class="language-js">let firstDefendIndex = gameState.hand.indexOf('Defend');
let expectedHand = gameState.hand.toSpliced(firstDefendIndex, 1);
assert.deepEqual(outcome.gameState.hand, expectedHand);
</code></pre>
<p>Fails, Defend is still in hand. Implement the fix:</p>
<pre><code class="language-js">function resolveAction(gameState, action) {
    if (!isLegalAction(gameState, action)) throw new IllegalActionError(gameState, action);

    let firstDefendIndex = gameState.hand.indexOf('Defend');
    let resultingGameState = {
        ...gameState,
        block: gameState.block + 5,
        energy: gameState.energy - 1,
        hand: gameState.hand.toSpliced(firstDefendIndex, 1)
    };

    return [{ gameState: resultingGameState, probability: 1 }];
}
</code></pre>
<p>Yeaurgh, the test and implementation are uncomfortably similar. Holding my nose and asserting the discard pile situation just the same:</p>
<pre><code class="language-js">let expectedDiscardPile = [...gameState.discardPile, 'Defend'];
assert.deepEqual(outcome.gameState.discardPile, expectedDiscardPile);
</code></pre>
<p>Test fails, implement fix:</p>
<pre><code class="language-js">function resolveAction(gameState, action) {
    if (!isLegalAction(gameState, action)) throw new IllegalActionError(gameState, action);

    let firstDefendIndex = gameState.hand.indexOf('Defend');
    let resultingGameState = {
        ...gameState,
        block: gameState.block + 5,
        energy: gameState.energy - 1,
        hand: gameState.hand.toSpliced(firstDefendIndex, 1),
        discardPile: [...gameState.discardPile, 'Defend']
    };

    return [{ gameState: resultingGameState, probability: 1 }];
}
</code></pre>
<p>Test passes. I think that's everything that happens when you play Defend. We can successfully resolve it now.</p>
<p>Okay. Let's review the test code. What's the damage?</p>
<pre><code class="language-js">it('resolves Defend', () =&gt; {
    fc.assert(
        fc.property(getArbGameState(), gameState =&gt; {
            let action = { name: 'Play Defend' };
            fc.pre(isLegalAction(gameState, action));

            let result = resolveAction(gameState, action);
            let sumOfProbabilities = result.reduce((acc, x) =&gt; acc + x.probability, 0);
            assert.equal(sumOfProbabilities, 1);
            for (let outcome of result) {
                assert.equal(outcome.gameState.block, gameState.block + 5);
                assert.equal(outcome.gameState.energy, gameState.energy - 1);

                let firstDefendIndex = gameState.hand.indexOf('Defend');
                let expectedHand = gameState.hand.toSpliced(firstDefendIndex, 1);
                assert.deepEqual(outcome.gameState.hand, expectedHand);

                let expectedDiscardPile = [...gameState.discardPile, 'Defend'];
                assert.deepEqual(outcome.gameState.discardPile, expectedDiscardPile);
            }
        })
    );
});
</code></pre>
<p>I mean, it could be worse. But it could be better. We can make the result assertion stronger and assert it always returns one outcome:</p>
<pre><code class="language-js">let result = resolveAction(gameState, action);
assert.equal(result.length, 1);
</code></pre>
<p>That lets us simplify and get rid of the for-loop:</p>
<pre><code class="language-js">it('resolves Defend', () =&gt; {
    fc.assert(
        fc.property(getArbGameState(), gameState =&gt; {
            let action = { name: 'Play Defend' };
            fc.pre(isLegalAction(gameState, action));

            let result = resolveAction(gameState, action);
            assert.equal(result.length, 1);

            let outcome = result[0];
            assert.equal(outcome.probability, 1);
            assert.equal(outcome.gameState.block, gameState.block + 5);
            assert.equal(outcome.gameState.energy, gameState.energy - 1);

            let firstDefendIndex = gameState.hand.indexOf('Defend');
            let expectedHand = gameState.hand.toSpliced(firstDefendIndex, 1);
            assert.deepEqual(outcome.gameState.hand, expectedHand);

            let expectedDiscardPile = [...gameState.discardPile, 'Defend'];
            assert.deepEqual(outcome.gameState.discardPile, expectedDiscardPile);
        })
    );
});
</code></pre>
<p>Okay. I'll take it. I don't see an immediate improvement to make here. I suspect we can add some helper functions to assert expected hands and discard piles when we have more tests that check the same thing, but adding them now seems premature.</p>
<p>We've done a lot of work now, it's high time to commit, but let's leave one little TODO note since we still have a weird <code>resolveAction()</code> implementation:</p>
<pre><code class="language-js">// TODO: Resolve other actions than Play Defend
function resolveAction(gameState, action) {
</code></pre>
<p>There. Commit: <em>&quot;Implement resolveAction() for Play Defend&quot;</em>.</p>
<hr>
<p>Progress is slow, I will admit. Live-blogging the thought process and every little code change slows everything right down. I'm also not sure what I value more - progress on the project, or progress on this blog series.</p>
<p>Factors to consider:</p>
<ul>
<li>I only have the opportunity to work on this 1-2 hours a week, at most.</li>
<li>I have taken breaks for months at a time and will do it again.</li>
<li>The blog posts make the progress of each work session more concrete and tangible.</li>
<li>The blog posts help me recover my train of thought between long breaks.</li>
<li>Writing forces me to justify my decisions and stay on track.</li>
<li>Writing a blog post will always take some time!</li>
<li>If I take a break from writing this style of post and make progress outside of them, it will break the continuous train of thought that you can follow along from the very start of the series.</li>
</ul>
<p>I see three options:</p>
<ol>
<li>Continue code-liveblogging like this, maybe taking slightly coarser steps.</li>
<li>Change the series from a liveblogging style to more of a devlog style, only sharing select snippets of code.
<ul>
<li>This would split the coding and writing activities into two separate chunks, giving me a backlog of progress to write about. I think this would make it harder to sit down and knock out another post and a bit of progress.</li>
</ul>
</li>
<li>Drop the series entirely and work in private.
<ul>
<li>This has the highest odds of killing the project. Having the project out in public on my website is a persistent encouragement to continue working on it.</li>
</ul>
</li>
</ol>
<p>In other words, there's no good option here but to keep trucking and tolerate that this be a slow-paced project. This does rhyme with how <a href="https://ronjeffries.com/">Ron Jeffries</a> (who inspired me to start this series) makes progress on his own projects, though he's written a <em>lot</em> more posts than me and hence accumulated a lot more progress.</p>
<p>Ah well. As long as the time spent feels meaningful.</p>
<hr>
<p>Next time: Strike! Bash! Probably not End Turn quite yet because that's a whole can of worms!</p>
<hr>
<p><em><a href="/crystal-spire/v17/">View this app version</a></em> | <em><a href="https://codeberg.org/cvennevik/crystal-spire/src/commit/a86fd3833ad0b906710a156b3d504f3c482e469c">Last commit: Implement resolveAction() for Play Defend</a></em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Crystal Spire #16: Is this legal?</title>
      <link>https://www.cvennevik.no/blog/crystal-spire-16-is-this-legal/</link>
      <pubDate>Sun, 18 Jan 2026 21:10:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/crystal-spire-16-is-this-legal/</guid>
      <content:encoded><![CDATA[<p>Good day, one and all. Today we start implementing <code>resolveAction()</code>.</p>
<p>First, a little design talk: What is the interface of <code>resolveAction()</code>? It needs to take two parameters:</p>
<ul>
<li>A game state</li>
<li>An action</li>
</ul>
<p>All actions resolve to a set of possible game states (at least one), with different probabilities. The combined probability of all the possible game states is 100%. To satisfy this, <code>resolveAction()</code> can return an array of objects, where each object has a <code>gameState</code> property and a <code>probability</code> property.</p>
<p>So far, the design just seems like a repeat of my C# attempt at this problem. Same minds think alike, huh? But it will do.</p>
<p>Ho-kay, let's test-drive this thing. First, a new test suite in <code>test.mjs</code>:</p>
<pre><code class="language-js">describe('resolveAction()', () =&gt; {
    // Tests go here
});
</code></pre>
<p>Then... hm, I'm not quite sure how best to do this. We have a few general properties we want to assert, like &quot;the probability of all possible game states sums to 100%&quot;. Then there are properties like &quot;playing a card removes it from our hand&quot; or &quot;playing a card puts it in the discard pile&quot;, but these properties come with some big asterisks down the line when certain cards and abilities come into play. Even &quot;playing Defend gives you 5 armor&quot; comes with asterisks (what if you have Dexterity? or Frail? or you get damaged when you play a card?). All this is making me feel uncertain of how good a fit property-based testing actually is for our project. But hey, the arbitraries for generating test data are nice, and at worst we're bringing an example-based mindset to a property-based tool, which still does the job.</p>
<p>Bah, we can handle the asterisks when we get to them. Let's test a simple action first: &quot;Play Defend&quot;.</p>
<pre><code class="language-js">it('resolves Defend', () =&gt; {
    fc.assert(
        fc.property(getArbGameState(), gameState =&gt; {
            let action = { name: 'Play Defend' };
            let result = resolveAction(gameState, action);
        })
    );
});
</code></pre>
<p>Now, the test fails because <code>resolveAction</code> is not defined yet, so let's implement and export that in <code>index.js</code>:</p>
<pre><code class="language-js">function resolveAction(gameState, action) {
    return []; // TODO: Implement this :)
}
</code></pre>
<pre><code class="language-js">// Export in Node.js for testing
if (typeof module === 'object' &amp;&amp; module != null &amp;&amp; 'exports' in module) {
    module.exports = { getActions, resolveAction };
}
</code></pre>
<p>Then import it in <code>test.mjs</code>:</p>
<pre><code class="language-js">import { getActions, resolveAction } from './index.js';
</code></pre>
<p>Test passes! Let's expand it to actually assert some things about the result. How about asserting the sum of probabilities?</p>
<pre><code class="language-js">it('resolves Defend', () =&gt; {
    fc.assert(
        fc.property(getArbGameState(), gameState =&gt; {
            let action = { name: 'Play Defend' };
            let result = resolveAction(gameState, action);
            let sumOfProbabilities = result.reduce((acc, x) =&gt; acc + x.probability, 0);
            assert.equal(sumOfProbabilities, 1);
        })
    );
});
</code></pre>
<p>Fails - the sum is 0. Let's make <code>resolveAction()</code> slightly more correct:</p>
<pre><code class="language-js">function resolveAction(gameState, action) {
    return [{ gameState, probability: 1 }];
}
</code></pre>
<p>Test passes! Yet it resolves all actions to the same game state. We need to assert some things about how it changes, like block increasing by 5.</p>
<pre><code class="language-js">it('resolves Defend', () =&gt; {
    fc.assert(
        fc.property(getArbGameState(), gameState =&gt; {
            let action = { name: 'Play Defend' };
            let result = resolveAction(gameState, action);
            let sumOfProbabilities = result.reduce((acc, x) =&gt; acc + x.probability, 0);
            assert.equal(sumOfProbabilities, 1);
            for (let outcome of result) {
                assert.equal(outcome.gameState.block, gameState.block + 5);
            }
        })
    );
});
</code></pre>
<p>Oof, my test complexity senses are tingling. But hey, failing test. So, make it pass...</p>
<pre><code class="language-js">function resolveAction(gameState, action) {
    let resultingGameState = {
        ...gameState,
        block: gameState.block + 5
    };
    return [{ gameState: resultingGameState, probability: 1 }];
}
</code></pre>
<p>Slowly getting somewhere. It also needs to reduce our energy by 1:</p>
<pre><code class="language-js">assert.equal(outcome.gameState.energy, gameState.energy - 1);
</code></pre>
<pre><code class="language-js">function resolveAction(gameState, action) {
    let resultingGameState = {
        ...gameState,
        block: gameState.block + 5,
        energy: gameState.energy - 1
    };
    return [{ gameState: resultingGameState, probability: 1 }];
}
</code></pre>
<p>A new fail and a new pass. Then, also, it needs to move Defend from our hand to the discard pile. Uh... How do we assert that?</p>
<pre><code class="language-js">let firstDefendIndex = gameState.hand.indexOf('Defend');
let expectedHand = gameState.hand.toSpliced(firstDefendIndex, 1);
assert.deepEqual(outcome.gameState.hand, expectedHand);
</code></pre>
<p>Okay, yeah, this really looks like the wrong way to write this test, but I want to see where this train wreck will lead us.</p>
<p>The test fails... for a hand containing just Strike. Mm. Right. Design question time: How should <code>resolveAction</code> handle illegal actions? Should it...</p>
<ul>
<li>...reject them with an exception?</li>
<li>...return an error message?</li>
<li>...return an empty array?</li>
<li>...assume it will not receive illegal actions and resolve them anyway?</li>
</ul>
<p>The last option would be best for performance (no extra logic to run at the top of each call), but until we have a rules engine that even works, I really believe the code ought to help us catch errors. The error should describe what was wrong, so we want some sort of error message. Making <code>resolveAction()</code> return a string when it fails might be weird to handle... I think throwing an exception is the right option.</p>
<p>You know, this legality thing sounds interesting enough that we should implement an <code>isLegalAction()</code> function first. Let's scrap all our changes and start over. New test:</p>
<pre><code class="language-js">describe('isLegalAction()', () =&gt; {
    it('handles Play Defend', () =&gt; {
        fc.assert(
            fc.property(getArbGameState(), gameState =&gt; {
                let result = isLegalAction(gameState, { name: 'Play Defend' });
            })
        );
    });
});
</code></pre>
<p>Fails because <code>isLegalAction</code> is not defined, so let's implement, export and import it:</p>
<pre><code class="language-js">function isLegalAction(gameState, action) {
    return true; // TODO: specify laws
}

// ...

// Export in Node.js for testing
if (typeof module === 'object' &amp;&amp; module != null &amp;&amp; 'exports' in module) {
    module.exports = { getActions, isLegalAction };
}
</code></pre>
<p>Alright, now to assert what the result ought to be:</p>
<pre><code class="language-js">describe('isLegalAction()', () =&gt; {
    it('handles Play Defend', () =&gt; {
        fc.assert(
            fc.property(getArbGameState(), gameState =&gt; {
                let result = isLegalAction(gameState, { name: 'Play Defend' });
                let expectedResult = gameState.hand.includes('Defend') &amp;&amp; gameState.energy &gt;= 1;
                assert.equal(result, expectedResult);
            })
        );
    });
});
</code></pre>
<p>Fails! Then let's implement it the simplest way to pass:</p>
<pre><code class="language-js">function isLegalAction(gameState, action) {
    return gameState.hand.includes('Defend') &amp;&amp; gameState.energy &gt;= 1;
}
</code></pre>
<p>Test passes. Cool. Now to do Play Strike:</p>
<pre><code class="language-js">it('handles Play Strike', () =&gt; {
    fc.assert(
        fc.property(getArbGameState(), fc.integer({ min: -1, max: 10 }), (gameState, enemyIndex) =&gt; {
            let result = isLegalAction(gameState, { name: 'Play Strike', enemyIndex });
            let expectedResult = gameState.hand.includes('Strike')
                &amp;&amp; gameState.energy &gt;= 1
                &amp;&amp; enemyIndex &gt;= 0
                &amp;&amp; enemyIndex &lt; gameState.enemies.length;
            assert.equal(result, expectedResult);
        })
    );
});
</code></pre>
<p>Test fails, make it pass (with proper distinction between the two actions now):</p>
<pre><code class="language-js">function isLegalAction(gameState, action) {
    if (action.name === 'Play Strike') {
        return gameState.hand.includes('Strike')
            &amp;&amp; gameState.energy &gt;= 1
            &amp;&amp; typeof action.enemyIndex === 'number'
            &amp;&amp; gameState.enemies[action.enemyIndex] != null;
    } else if (action.name === 'Play Defend') {
        return gameState.hand.includes('Defend')
            &amp;&amp; gameState.energy &gt;= 1;
    } else {
        return false;
    }
}
</code></pre>
<p>Test passes. Now we do Play Bash, same pattern:</p>
<pre><code class="language-js">it('handles Play Bash', () =&gt; {
    fc.assert(
        fc.property(getArbGameState(), fc.integer({ min: -1, max: 10 }), (gameState, enemyIndex) =&gt; {
            let result = isLegalAction(gameState, { name: 'Play Bash', enemyIndex });
            let expectedResult = gameState.hand.includes('Bash')
                &amp;&amp; gameState.energy &gt;= 2
                &amp;&amp; enemyIndex &gt;= 0
                &amp;&amp; enemyIndex &lt; gameState.enemies.length;
            assert.equal(result, expectedResult);
        })
    );
});
</code></pre>
<p>Red...</p>
<pre><code class="language-js">/* ... */
else if (action.name === 'Play Bash') {
    return gameState.hand.includes('Bash')
        &amp;&amp; gameState.energy &gt;= 2
        &amp;&amp; typeof action.enemyIndex === 'number'
        &amp;&amp; gameState.enemies[action.enemyIndex] != null;
}
/* ... */
</code></pre>
<p>...green. Now End Turn.</p>
<pre><code class="language-js">it('handles End Turn', () =&gt; {
    fc.assert(
        fc.property(getArbGameState(), gameState =&gt; {
            let result = isLegalAction(gameState, { name: 'End Turn' });
            let expectedResult = true;
            assert.equal(result, expectedResult);
        })
    );
});
</code></pre>
<pre><code class="language-js">/* ... */
else if (action.name === 'End Turn') {
    return true;
}
/* ... */
</code></pre>
<p>Red, green. That's all we need for now. We could add tests and checks for the game being over, but I believe it's enough that <code>getActions()</code> checks for it. I think we can do a slight refactor before we commit, because this function really looks like it ought to be a switch statement:</p>
<pre><code class="language-js">function isLegalAction(gameState, action) {
    switch (action.name) {
        case 'Play Strike':
            return gameState.hand.includes('Strike')
                &amp;&amp; gameState.energy &gt;= 1
                &amp;&amp; typeof action.enemyIndex === 'number'
                &amp;&amp; gameState.enemies[action.enemyIndex] != null;
        case 'Play Bash':
            return gameState.hand.includes('Bash')
                &amp;&amp; gameState.energy &gt;= 2
                &amp;&amp; typeof action.enemyIndex === 'number'
                &amp;&amp; gameState.enemies[action.enemyIndex] != null;
        case 'Play Defend':
            return gameState.hand.includes('Defend')
                &amp;&amp; gameState.energy &gt;= 1;
        case 'End Turn':
            return true;
        default:
            return false;
    }
}
</code></pre>
<p>Cool cool cool. Commit: <em>&quot;Add isLegalAction()&quot;</em></p>
<hr>
<p>Now - I see some potential here to go back and write the tests for <code>getActions()</code> a bit differently now, using this function. First we'll need an arbitrary to generate actions:</p>
<pre><code class="language-js">let arbAction = fc.oneof(
    fc.record({
        name: fc.constant('Play Strike'),
        enemyIndex: fc.integer({ min: 0, max: 9 })
    }),
    fc.record({
        name: fc.constant('Play Bash'),
        enemyIndex: fc.integer({ min: 0, max: 9 })
    }),
    fc.constant({ name: 'Play Defend' }),
    fc.constant({ name: 'End Turn' })
);
</code></pre>
<p>Hoping I'm using <code>fc.oneof()</code> correctly here. Then we write the test:</p>
<pre><code class="language-js">it('returns legal actions', () =&gt; {
    fc.assert(
        fc.property(getArbGameState(), arbAction, (gameState, action) =&gt; {
            let actions = getActions(gameState);
            let matches = actions.filter(x =&gt; deepEqual(x, action)).length;
            let expectedMatches = isLegalAction(gameState, action) ? 1 : 0;
            assert.equal(matches, expectedMatches);
        })
    );
});
</code></pre>
<p>Yes. Yes, here we have it. For every game state and action, if the action is legal, <code>getActions()</code> should return it, otherwise, it should not. This can replace our four action-specific tests <em>and</em> the duplicates test! Well, I hope it can, at least. The test passes, but let's prod at <code>getActions()</code> to see if this test actually catches bugs.</p>
<ul>
<li>If we don't return &quot;End Turn&quot;, it fails. Nice.</li>
<li>If we change the &quot;Play Bash&quot; to check for 3 energy instead of 2, it passes.
<ul>
<li>Wait. Oh no.</li>
</ul>
</li>
</ul>
<p>Right, okay, the test's not quite as strong as I'd like. Does it not run enough times to hit the case where we have Bash in hand, two energy, and a valid target? Do we need to up the run count?</p>
<pre><code class="language-js">it('returns legal actions', () =&gt; {
    fc.assert(
        fc.property(getArbGameState(), arbAction, (gameState, action) =&gt; {
            let actions = getActions(gameState);
            let matches = actions.filter(x =&gt; deepEqual(x, action)).length;
            let expectedMatches = isLegalAction(gameState, action) ? 1 : 0;
            assert.equal(matches, expectedMatches);
        }),
        { numRuns: 10000 }
    );
});
</code></pre>
<p>Apparently <code>numRuns</code> is 100 by default. A test that covers this much ground definitely ought to run more often than that. Give it a spin and... Yeah, there we go, property failed after 1674 tests. Now if we remove the error, it does take 160 milliseconds to pass instead of 4 milliseconds, so it has a cost, but it's still near-instant enough for my taste.</p>
<p>I'm curious whether we could and should split it into two tests, though... I saw the docs say something about preconditions, using <code>.filter</code> or <code>fc.pre</code>. Let's try the latter one:</p>
<pre><code class="language-js">it('returns legal actions', () =&gt; {
    fc.assert(
        fc.property(getArbGameState(), arbAction, (gameState, action) =&gt; {
            fc.pre(isLegalAction(gameState, action));
            let actions = getActions(gameState);
            let matches = actions.filter(x =&gt; deepEqual(x, action)).length;
            assert.equal(matches, 1);
        }),
        { numRuns: 5000 }
    );
});

it('does not return illegal actions', () =&gt; {
    fc.assert(
        fc.property(getArbGameState(), arbAction, (gameState, action) =&gt; {
            fc.pre(!isLegalAction(gameState, action));
            let actions = getActions(gameState);
            let matches = actions.filter(x =&gt; deepEqual(x, action)).length;
            assert.equal(matches, 0);
        }),
        { numRuns: 5000 }
    );
});
</code></pre>
<p>One test for legal actions, one test for illegal actions. Tidy! <code>fc.pre()</code> cancels the test if the precondition does not hold. Unfortunately, this looks like it doubles the run time - I guess it generates a ton of extra cases now that get canceled. It was a fun experiment, but let's roll it back to a single test.</p>
<p>I'm wondering now about test &quot;efficiency&quot; and distribution between different test cases. Maybe testing actions with enemyIndex above 5 is wasteful. And maybe we do not need to test &quot;End Turn&quot; so often. Apparently <code>fc.oneof</code> lets you pass weights?</p>
<pre><code class="language-js">let arbAction = fc.oneof(
    {
        weight: 5,
        arbitrary: fc.record({
            name: fc.constant('Play Strike'),
            enemyIndex: fc.integer({ min: 0, max: 5 })
        })
    },
    {
        weight: 5,
        arbitrary: fc.record({
            name: fc.constant('Play Bash'),
            enemyIndex: fc.integer({ min: 0, max: 5 })
        })
    },
    { weight: 2, arbitrary: fc.constant({ name: 'Play Defend' }) },
    { weight: 1, arbitrary: fc.constant({ name: 'End Turn' }) }
);
</code></pre>
<p>Testing this with the Bash energy change, and it doesn't really make a dramatic difference. If I turn the weight way up for &quot;Play Bash&quot; specifically, then it tends to find it in slightly fewer, but it's still within an order of magnitude. Let's roll this back too (but still lower the <code>enemyIndex</code> limit to 5).</p>
<p>Enough experimenting, time to commit: <em>&quot;Replace getActions() tests with 'returns valid actions'&quot;</em></p>
<hr>
<p>Small progress, but I'm happy. We learned a bit more about how we can use fast-check, and we found a design for our code that makes more sense with property-based testing. I didn't have a <code>isValidAction</code> function when we did this in C#! The tests were way different! Exciting differences!</p>
<p>Next time: implementing <code>resolveAction()</code>, for reals this time (probably)!</p>
<hr>
<p><em><a href="/crystal-spire/v16/">View this app version</a></em> | <em><a href="https://codeberg.org/cvennevik/crystal-spire/src/commit/3622385a73d24475bc96e95c0882af4ae0dcec29">Last commit: Replace getActions() tests with 'returns valid actions'</a></em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Crystal Spire #15: Back into actions</title>
      <link>https://www.cvennevik.no/blog/crystal-spire-15-back-into-actions/</link>
      <pubDate>Sun, 28 Dec 2025 11:00:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/crystal-spire-15-back-into-actions/</guid>
      <content:encoded><![CDATA[<p>Good day, one and all. It's time to make progress on our Slay the Spire solver. I'm not entirely clear on what needs doing, so let's make a list:</p>
<ol>
<li>Finish implementing <code>getActions()</code>.</li>
<li>Implement a function to resolve actions - something like <code>resolveAction(gameState, action)</code>?</li>
<li>Compute and render real actions on the page.</li>
<li>Something something implement a solver.</li>
</ol>
<ul>
<li>We'll think more about this one when we get to it.</li>
</ul>
<p>Having it written down helps! It also helps clarifying the design here - I think every action returned by <code>getActions()</code> should be valid input to <code>resolveAction()</code>. Hence, &quot;Defeat&quot; and &quot;Victory&quot; do not make any sense as actions, instead there should be no actions available when you win or lose.</p>
<p>Let's update the test we wrote last time to expect an empty list instead:</p>
<pre><code class="language-js">it('offers no actions when HP is zero', () =&gt; {
    fc.assert(
        fc.property(arbGameState, gameState =&gt; {
            if (gameState.hp === 0) {
                let actions = getActions(gameState);
                assert.deepEqual(actions, []);
            }
        })
    );
});
</code></pre>
<pre><code>$ node --test
▶ getActions()
  ✔ can play Defend and End Turn (1.181773ms)
  ✔ can play Strike and End Turn (0.146242ms)
  ✖ offers no actions HP is zero (7.617366ms)
    Error: Property failed after 23 tests
    { seed: -1695486517, path: &quot;22:0:0:0&quot;, endOnFailure: true }
    Counterexample: [{&quot;hp&quot;:0,&quot;maxHp&quot;:80,&quot;block&quot;:0,&quot;energy&quot;:0,&quot;maxEnergy&quot;:3,&quot;hand&quot;:[],&quot;drawPile&quot;:[],&quot;discardPile&quot;:[],&quot;relics&quot;:[],&quot;enemies&quot;:[]}]
    Shrunk 3 time(s)
</code></pre>
<p>Nice. Writing these tests feels really smooth. Then we update getActions to match:</p>
<pre><code class="language-js">if (gameState.hp === 0) {
    return [];
}
</code></pre>
<p>Tests pass! Commit: <em>&quot;getActions(): return no actions when dead&quot;</em>.</p>
<p>Our solver will have to implement a separate function to check for victory and defeat now, but that sounds sensible. Oh, speaking of victory, we win when there are no enemies left:</p>
<pre><code class="language-js">    it('offers no actions when no enemies are left', () =&gt; {
        fc.assert(
            fc.property(arbGameState, gameState =&gt; {
                if (gameState.enemies.length === 0) {
                    let actions = getActions(gameState);
                    assert.deepEqual(actions, []);
                }
            })
        );
    });
</code></pre>
<p>Test failed, so then we update the implementation:</p>
<pre><code class="language-js">if (gameState.hp === 0 || gameState.enemies.length === 0) {
    return [];
}
</code></pre>
<p>Tests pass, commit: <em>&quot;getActions(): return no actions when no enemies left&quot;</em>.</p>
<p>I'm noticing that our test logic looks suspiciously similar to our implementation logic. I'm also noticing that we have an <code>if</code>-check in our test. I have gotten the impression that tests should be as simple and explicit as possible, to make sure you understand what they are asserting, and to minimize the risk of logic errors. This is typically easy with example-based testing - given a specific input, expect a specific output, no logic to check for. But now we're asserting for properties that only hold for certain conditions, so we cannot write the test quite that simply. I'm smelling the risk of writing slightly tricky logic into the test, introducing an error, and writing that very same error into the implementation.</p>
<p>Maybe we can mitigate this risk by specifying the logic differently in our tests and our implementation. Maybe <a href="https://fast-check.dev/docs/core-blocks/arbitraries/combiners/any/#map">that <code>.map()</code> chaining method</a> can help us here.</p>
<pre><code class="language-js">it('offers no actions when HP is zero', () =&gt; {
    fc.assert(
        fc.property(arbGameState.map(x =&gt; ({ ...x, hp: 0 })), gameState =&gt; {
            let actions = getActions(gameState);
            assert.deepEqual(actions, []);
        })
    );
});

it('offers no actions when no enemies are left', () =&gt; {
    fc.assert(
        fc.property(arbGameState.map(x =&gt; ({ ...x, enemies: [] })), gameState =&gt; {
            let actions = getActions(gameState);
            assert.deepEqual(actions, []);
        })
    );
});
</code></pre>
<p>Yeah, that works. I mean, ideally, we'd change the game state arbitrary so that it didn't produce a variety of values for <code>hp</code> and <code>enemies</code> that we just discard, but this looks like an improvement. Speaking of improvement, I think &quot;it returns no actions&quot; sounds better than &quot;offers no actions&quot;, so let's change that as well. Commit: <em>&quot;Refactor property-based tests&quot;</em>.</p>
<p>While we're at it, let's rewrite the other tests too: &quot;can play Defend and End Turn&quot; and &quot;can play Strike and End Turn&quot;. These tests do not translate as cleanly, and I believe we need to test for a full handful of properties to replace them.</p>
<p>Given that both the player and enemies are alive:</p>
<ul>
<li>You can end the turn.</li>
<li>If you have Defend in hand and at least 1 Energy, you can play Defend.</li>
<li>If you have Strike in hand and at least 1 Energy, you can play Strike on any of the enemies.</li>
<li>If you have Bash in hand and at least 2 Energy, you can play Bash on any of the enemies.</li>
<li>You never get duplicate actions.</li>
</ul>
<p>There, work cut out for us. And we'll need a more powerful way to specify arbitrary game state variants to do it. How about a little arbitrary factory?</p>
<pre><code class="language-js">function getArbGameState(options = {}) {
    let defaults = {
        hp: fc.integer({ min: 1, max: 80 }),
        maxHp: fc.constant(80),
        block: fc.constant(0),
        energy: fc.nat({ max: 999 }),
        maxEnergy: fc.constant(3),
        hand: fc.array(arbCard, { minLength: 0, maxLength: 10 }),
        drawPile: fc.constant([]),
        discardPile: fc.constant([]),
        relics: fc.constant([]),
        enemies: fc.array(arbEnemy, { minLength: 1, maxLength: 5 })
    };
    return fc.record({ ...defaults, ...options });
}
</code></pre>
<p>Now we have a set of useful defaults (the player is alive, enemies are alive) with an optional parameter to specify new arbitraries for any property. Then we update our tests:</p>
<pre><code class="language-js">it('returns no actions when HP is zero', () =&gt; {
    fc.assert(
        fc.property(getArbGameState({ hp: fc.constant(0) }), gameState =&gt; {
            let actions = getActions(gameState);
            assert.deepEqual(actions, []);
        })
    );
});

it('returns no actions when no enemies are left', () =&gt; {
    fc.assert(
        fc.property(getArbGameState({ enemies: fc.constant([]) }), gameState =&gt; {
            let actions = getActions(gameState);
            assert.deepEqual(actions, []);
        })
    );
});
</code></pre>
<p>Tests pass! Commit: <em>&quot;tests: add getArbGameState() function&quot;</em>.</p>
<p>Now we can write all the tests we want. First, we can always end the turn:</p>
<pre><code class="language-js">it('returns End Turn', () =&gt; {
    fc.assert(
        fc.property(getArbGameState(), gameState =&gt; {
            let actions = getActions(gameState);
            // ... how do we assert this??
        })
    );
});
</code></pre>
<p>Okay we're not quite there yet. Uh. We want to assert that actions contains an object with one specific property with a specific value. It sounds like we need our <code>deepEqual()</code> function again. Time to dig through the Git log to recover it... <a href="https://codeberg.org/cvennevik/crystal-spire/commit/09da913d5bc97495c5417f97726a65803ef52a46#diff-df6a091d1844505b07e077d5ec678869b3067b5a">Ah, found it</a>.</p>
<p>Lil' copy-paste back into the project, and we can write:</p>
<pre><code class="language-js">it('returns End Turn', () =&gt; {
    fc.assert(
        fc.property(getArbGameState(), gameState =&gt; {
            let actions = getActions(gameState);
            return actions.some(x =&gt; deepEqual(x, { name: 'End Turn' }));
        })
    );
});
</code></pre>
<p>Nice! That passes. Though, I haven't seen it fail, and we're returning true/false now instead of using an <code>assert</code> function, so I'm paranoid that it doesn't actually test it properly. Let me just change the implementation to call it 'Finish Turn' instead to see that it fails... Okay yeah it fails, loudly, this works fine, undo the text change.</p>
<p>Next, let's ensure there are never any duplicate actions.</p>
<pre><code class="language-js">it('does not return duplicate actions', () =&gt; {
    fc.assert(
        fc.property(getArbGameState(), gameState =&gt; {
            let actions = getActions(gameState);
            return actions.every(x =&gt; {
                let matches = actions.filter(y =&gt; deepEqual(x, y));
                return matches.length === 1;
            });
        })
    );
});
</code></pre>
<p>Passes - the quadratic complexity makes me frown, but it's the simplest solution I see, and I'll take a simple test over a slightly more efficient test. Let's return the &quot;Play Bash&quot; action twice to make sure it fails... Yeah, it fails, undo the error.</p>
<p>Cool! Next up, Defend.</p>
<pre><code class="language-js">it('returns Play Defend', () =&gt; {
    fc.assert(
        fc.property(getArbGameState({
            hand: fc.array(arbCard, { minLength: 0, maxLength: 9 }).map(x =&gt; x.concat('Defend')),
            energy: fc.integer({ min: 1, max: 999 })
        }), gameState =&gt; {
            let actions = getActions(gameState);
            return actions.some(x =&gt; deepEqual(x, { name: 'Play Defend' }));
        })
    );
});
</code></pre>
<p>Passes! That way of specifying &quot;any hand of cards including Defend&quot; is clunky, though, and we'll have to repeat that pattern. Can we improve it? Do the docs offer a cleaner solution? ...three minutes of browsing say &quot;no&quot;. Let's extract a function, then.</p>
<pre><code class="language-js">function getArbHand(options = {}) {
    let includedCards = options?.with ?? [];
    let constraints = { minLength: 0, maxLength: 10 - includedCards.length };
    return fc.array(arbCard, constraints).map(x =&gt; x.concat(includedCards));
}
</code></pre>
<pre><code class="language-js">it('returns Play Defend', () =&gt; {
    fc.assert(
        fc.property(getArbGameState({
            hand: getArbHand({ with: ['Defend'] }),
            energy: fc.integer({ min: 1, max: 999 })
        }), gameState =&gt; {
            let actions = getActions(gameState);
            return actions.some(x =&gt; deepEqual(x, { name: 'Play Defend' }));
        })
    );
});
</code></pre>
<p>Passes! We move on.</p>
<p>Next, Strike. This one's different, because when we have multiple enemies, we can have multiple actions with different targets. Our current code doesn't even support multiple targets yet. We can start with a test constrained to a single enemy:</p>
<pre><code class="language-js">it('returns Play Strike', () =&gt; {
    fc.assert(
        fc.property(getArbGameState({
            hand: getArbHand({ with: ['Strike'] }),
            energy: fc.integer({ min: 1, max: 999 }),
            enemies: fc.tuple(arbEnemy)
        }), gameState =&gt; {
            let actions = getActions(gameState);
            return actions.some(x =&gt; deepEqual(x, { name: 'Play Strike', enemyIndex: 0 }));
        })
    );
});
</code></pre>
<p>Cool, that passes. But we also want to assert there are not additional &quot;Play Strike&quot; actions with invalid enemy indexes. Maybe like this?</p>
<pre><code class="language-js">it('returns Play Strike', () =&gt; {
    fc.assert(
        fc.property(getArbGameState({
            hand: getArbHand({ with: ['Strike'] }),
            energy: fc.integer({ min: 1, max: 999 }),
            enemies: fc.tuple(arbEnemy)
        }), gameState =&gt; {
            let actions = getActions(gameState);
            let strikeActions = actions.filter(x =&gt; x.name === 'Play Strike').toSorted((a, b) =&gt; a.enemyIndex - b.enemyIndex);
            assert.deepEqual(strikeActions, [{ name: 'Play Strike', enemyIndex: 0 }]);
        })
    );
});
</code></pre>
<p>Oof, it's getting a bit involved, but it works. We can complicate the code even more so it tests multiple enemies:</p>
<pre><code class="language-js">it('returns Play Strike', () =&gt; {
    fc.assert(
        fc.property(getArbGameState({
            hand: getArbHand({ with: ['Strike'] }),
            energy: fc.integer({ min: 1, max: 999 })
        }), gameState =&gt; {
            let actions = getActions(gameState);
            let strikeActions = actions
                .filter(x =&gt; x.name === 'Play Strike')
                .toSorted((a, b) =&gt; a.enemyIndex - b.enemyIndex);
            let expectedStrikeActions = [...gameState.enemies.entries()]
                .map(([idx, _value]) =&gt; ({ name: 'Play Strike', enemyIndex: idx }))
                .toSorted((a, b) =&gt; a.enemyIndex - b.enemyIndex);
            assert.deepEqual(strikeActions, expectedStrikeActions);
        })
    );
});
</code></pre>
<p>Oof, ouch, this is setting off my &quot;test is too complicated&quot; sensors. I would never do this with example-based testing. But, I mean, it works:</p>
<pre><code>AssertionError [ERR_ASSERTION]: Expected values to be loosely deep-equal:
    
    [
      {
        enemyIndex: 0,
        name: 'Play Strike'
      }
    ]
    
    should loosely deep-equal
    
    [
      {
        enemyIndex: 0,
        name: 'Play Strike'
      },
      {
        enemyIndex: 1,
        name: 'Play Strike'
      }
    ]
</code></pre>
<p>Sooo... Maybe it's fine? Maybe it's fine. We can fix the implementation now:</p>
<pre><code class="language-js">if (gameState.hand.includes('Strike') &amp;&amp; gameState.energy &gt;= 1) {
    for (let i = 0; i &lt; gameState.enemies.length; i++) {
        actions.push({ name: 'Play Strike', enemyIndex: i });
    }
}
</code></pre>
<p>Green! Onwards to Bash. It works the same way as Strike, except it costs two energy:</p>
<pre><code class="language-js">it('returns Play Bash', () =&gt; {
    fc.assert(
        fc.property(getArbGameState({
            hand: getArbHand({ with: ['Bash'] }),
            energy: fc.integer({ min: 2, max: 999 })
        }), gameState =&gt; {
            let actions = getActions(gameState);
            let bashActions = actions
                .filter(x =&gt; x.name === 'Play Bash')
                .toSorted((a, b) =&gt; a.enemyIndex - b.enemyIndex);
            let expectedBashActions = [...gameState.enemies.entries()]
                .map(([idx, _value]) =&gt; ({ name: 'Play Bash', enemyIndex: idx }))
                .toSorted((a, b) =&gt; a.enemyIndex - b.enemyIndex);
            assert.deepEqual(bashActions, expectedBashActions);
        })
    );
});
</code></pre>
<p>Yup, fails as expected. Implement fix:</p>
<pre><code class="language-js">if (gameState.hand.includes('Bash') &amp;&amp; gameState.energy &gt;= 2) {
    for (let i = 0; i &lt; gameState.enemies.length; i++) {
        actions.push({ name: 'Play Bash', enemyIndex: i });
    }
}
</code></pre>
<p>And the tests pass! I think this means <code>getActions()</code> is fully implemented for our Jaw Worm fight. Yay!</p>
<p>I notice now that these tests only assert the presence of actions when they should be there, and not the absence of actions when they should not be there. For instance, our tests pass if we do not require energy to play Bash. I mean. We could test this too. We could... maybe...</p>
<pre><code class="language-js">it('returns Play Bash', () =&gt; {
    fc.assert(
        fc.property(getArbGameState(), gameState =&gt; {
            let actions = getActions(gameState);
            let bashActions = actions
                .filter(x =&gt; x.name === 'Play Bash')
                .toSorted((a, b) =&gt; a.enemyIndex - b.enemyIndex);
            let canPlayBash = gameState.hand.includes('Bash') &amp;&amp; gameState.energy &gt;= 2;
            let expectedBashActions = canPlayBash
                ? [...gameState.enemies.entries()]
                    .map(([idx, _value]) =&gt; ({ name: 'Play Bash', enemyIndex: idx }))
                    .toSorted((a, b) =&gt; a.enemyIndex - b.enemyIndex)
                : [];
            assert.deepEqual(bashActions, expectedBashActions);
        })
    );
});
</code></pre>
<p>Lord almighty. I mean, um, both arrays are already sorted by enemyIndex, so we could trim the sorting...?</p>
<pre><code class="language-js">it('returns Play Bash', () =&gt; {
    fc.assert(
        fc.property(getArbGameState(), gameState =&gt; {
            let actions = getActions(gameState);
            let bashActions = actions.filter(x =&gt; x.name === 'Play Bash');
            let canPlayBash = gameState.hand.includes('Bash') &amp;&amp; gameState.energy &gt;= 2;
            let expectedBashActions = canPlayBash
                ? [...gameState.enemies.entries()]
                    .map(([idx, _value]) =&gt; ({ name: 'Play Bash', enemyIndex: idx }))
                : [];
            assert.deepEqual(bashActions, expectedBashActions);
        })
    );
});
</code></pre>
<p>You know what? Sure. Let's go with this. It's significantly less transparent than splitting it up into multiple tests, but it asserts exactly when we can play Bash, and it does it in a single test. We might get punished for this later, but we will take that punishment when it happens.</p>
<p>Let's rewrite the rest of the tests to the same style, extract a <code>getIndexes()</code> function, inline a few variables, and give the remaining variables similar names:</p>
<pre><code class="language-js">function getIndexes(array) {
    return [...array.entries()].map(([idx, _value]) =&gt; idx);
}

describe('getActions()', () =&gt; {
    it('returns no actions when HP is zero', () =&gt; {
        fc.assert(
            fc.property(getArbGameState({ hp: fc.constant(0) }), gameState =&gt; {
                let actions = getActions(gameState);
                assert.deepEqual(actions, []);
            })
        );
    });

    it('returns no actions when no enemies are left', () =&gt; {
        fc.assert(
            fc.property(getArbGameState({ enemies: fc.constant([]) }), gameState =&gt; {
                let actions = getActions(gameState);
                assert.deepEqual(actions, []);
            })
        );
    });

    it('does not return duplicate actions', () =&gt; {
        fc.assert(
            fc.property(getArbGameState(), gameState =&gt; {
                let actions = getActions(gameState);
                return actions.every(x =&gt; {
                    let matches = actions.filter(y =&gt; deepEqual(x, y));
                    return matches.length === 1;
                });
            })
        );
    });

    it('returns End Turn', () =&gt; {
        fc.assert(
            fc.property(getArbGameState(), gameState =&gt; {
                let actions = getActions(gameState).filter(x =&gt; x.name === 'End Turn');
                let expectedActions = [{ name: 'End Turn' }];
                assert.deepEqual(actions, expectedActions);
            })
        );
    });

    it('returns Play Defend', () =&gt; {
        fc.assert(
            fc.property(getArbGameState(), gameState =&gt; {
                let actions = getActions(gameState).filter(x =&gt; x.name === 'Play Defend');
                let expectedActions = gameState.hand.includes('Defend') &amp;&amp; gameState.energy &gt;= 1
                    ? [{ name: 'Play Defend' }]
                    : [];
                assert.deepEqual(actions, expectedActions);
            })
        );
    });

    it('returns Play Strike', () =&gt; {
        fc.assert(
            fc.property(getArbGameState(), gameState =&gt; {
                let actions = getActions(gameState).filter(x =&gt; x.name === 'Play Strike');
                let expectedActions = gameState.hand.includes('Strike') &amp;&amp; gameState.energy &gt;= 1
                    ? getIndexes(gameState.enemies).map(idx =&gt; ({ name: 'Play Strike', enemyIndex: idx }))
                    : [];
                assert.deepEqual(actions, expectedActions);
            })
        );
    });

    it('returns Play Bash', () =&gt; {
        fc.assert(
            fc.property(getArbGameState(), gameState =&gt; {
                let actions = getActions(gameState).filter(x =&gt; x.name === 'Play Bash');
                let expectedActions = gameState.hand.includes('Bash') &amp;&amp; gameState.energy &gt;= 2
                    ? getIndexes(gameState.enemies).map(idx =&gt; ({ name: 'Play Bash', enemyIndex: idx }))
                    : [];
                assert.deepEqual(actions, expectedActions);
            })
        );
    });
});
</code></pre>
<p>That looks like a fairly comprehensive test suite to me. Commit: <em>&quot;getActions(): support targetting different enemies&quot;</em>.</p>
<hr>
<p>We can now cross <code>getActions()</code> off our to-do list. The list is now:</p>
<ol>
<li>Implement <code>resolveAction(gameState, action)</code>.</li>
<li>Compute and render real actions on the page.</li>
<li>Implement a solver.</li>
</ol>
<p><code>resolveAction()</code> is going to be significantly more complicated. That's good. We can handle a challenge.</p>
<p>Until next time!</p>
<hr>
<p><em><a href="/crystal-spire/v15/">View this app version</a></em> | <em><a href="https://codeberg.org/cvennevik/crystal-spire/src/commit/95e799fc29b6452b2210ccc71cd6302adfdc225b">Last commit: getActions(): support targetting different enemies</a></em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Crystal Spire #14: End of the Yak</title>
      <link>https://www.cvennevik.no/blog/crystal-spire-14-end-of-the-yak/</link>
      <pubDate>Thu, 25 Dec 2025 16:45:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/crystal-spire-14-end-of-the-yak/</guid>
      <content:encoded><![CDATA[<p>Happy holidays! Today we're doing property-based testing.</p>
<hr>
<p>Let's kick things off by scrapping our own self-written test runner and switching to Node's. Having our own tests page is cute, but having less code to maintain is cuter.</p>
<p>I have not used Node's test runner before, so I'll have to reference <a href="https://nodejs.org/api/test.html">the documentation</a>. It looks like the <code>node:test</code> module exports a <code>test()</code> function (also aliased to <code>it()</code>) and a <code>suite()</code> function (also aliased to <code>describe()</code>). Tests fail when they throw an exception, and succeed when they don't. You can write assertions for tests by using the <code>node:assert</code> module. You run the tests by calling <code>node --test</code> on the command line, and it automatically discovers test files with names matching one of several patterns with &quot;test&quot; in it.</p>
<p>Let's convert our <code>tests.js</code> to a <code>test.js</code> file for Node. First, we rename it to <code>test.js</code>. Then we import <code>assert</code>, <code>describe</code> and <code>it</code> at the top of the file:</p>
<pre><code class="language-js">import assert from 'node:assert';
import { describe, it } from 'node:test';
</code></pre>
<p>Oh man it feels good being able to import things. Anyway. Next we declare a test suite with <code>describe()</code>:</p>
<pre><code class="language-js">describe('getActions()', () =&gt; {

});
</code></pre>
<p>Then - we'd better see the test runner in action as soon as we can - we write a failing test:</p>
<pre><code class="language-js">describe('getActions()', () =&gt; {
    it('fails', () =&gt; {
        assert.equal(true, false);
    });
});
</code></pre>
<p>And we run it with <code>node --test</code>:</p>
<pre><code>$ node --test
(node:49267) Warning: To load an ES module, set &quot;type&quot;: &quot;module&quot; in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
/home/cvennevik/dev/crystal-spire/test.js:1
import assert from 'node:assert';
^^^^^^

SyntaxError: Cannot use import statement outside a module
</code></pre>
<p>Ah. <code>test.js</code> is not an ES module, so it can't import. I might want to add <code>&quot;type&quot;: &quot;module&quot;</code> to the package, but that may have unexpected consequences - let's rename it to <code>test.mjs</code> for the moment. Then rerun <code>node --test</code>:</p>
<pre><code>$ node --test
▶ getActions()
  ✖ fails (1.473907ms)
    AssertionError [ERR_ASSERTION]: true == false
</code></pre>
<p>Yay! A failing test! Then we can change it to pass:</p>
<pre><code class="language-js">it('passes', () =&gt; {
    assert.equal(true, true);
});
</code></pre>
<pre><code>$ node --test
▶ getActions()
  ✔ passes (0.736913ms)
</code></pre>
<p>Beautiful. Now to convert the actual test. We take our array of test cases:</p>
<pre><code class="language-js">let testCases = [
    {
        name: 'Can Play Defend and End Turn',
        input: {
            // ... a game state ...
        },
        expectedOutput: [{ name: 'Play Defend' }, { name: 'End Turn' }]
    },
    {
        name: 'Can Play Strike and End Turn',
        input: {
            // ... another game state ...
        },
        expectedOutput: [{ name: 'Play Strike', enemyIndex: 0 }, { name: 'End Turn' }]
    }
];

for (let testCase of testCases) {
    testCase.actualOutput = getActions(testCase.input);
    testCase.passed = deepEqual(testCase.actualOutput, testCase.expectedOutput);
}
</code></pre>
<p>...and we add equivalent Node tests:</p>
<pre><code class="language-js">describe('getActions()', () =&gt; {
    it('can play Defend and End Turn', () =&gt; {
        let gameState = { /* ... a game state ... */ };
        let actions = getActions(gameState);
        assert.deepEqual(actions, [{ name: 'Play Defend' }, { name: 'End Turn' }]);
    });

    it('can play Strike and End Turn', () =&gt; {
        let gameState = { /* ... another game state ... */ };
        let actions = getActions(gameState);
        assert.deepEqual(actions, [{ name: 'Play Strike', enemyIndex: 0 }, { name: 'End Turn' }]);
    });
});
</code></pre>
<p>Then we try to run them:</p>
<pre><code>$ node --test
▶ getActions()
  ✖ can play Defend and End Turn (0.831261ms)
    ReferenceError [Error]: getActions is not defined
</code></pre>
<p>Ah. Right. We need to import <code>getActions</code>. Hm.</p>
<p>I am not entirely sure how to go about this. I think if we use <code>export</code> in <code>index.js</code>, that will give us an error in the browser, since it's not a module. Let's try it to verify:</p>
<pre><code class="language-js">export function getActions(gameState) {
    // ...
}
</code></pre>
<pre><code>Uncaught SyntaxError: export declarations may only appear at top level of a module
</code></pre>
<p>Yeah. So that doesn't work. I know there's another way to do exports with Node, though, which is to assign exports to <code>module.exports</code>, and we might be able to do that without the browser complaining. Pop this little bad boy at the end of <code>index.js</code>:</p>
<pre><code class="language-js">// Export in Node.js for testing
if (module !== undefined &amp;&amp; 'exports' in module) {
    module.exports = { getActions };
}
</code></pre>
<p>It works! Our page works as normal now. Okay, well, we do still get a nasty <code>Uncaught ReferenceError: module is not defined</code> in the console still, so actually, the if-check doesn't work at all. I've heard of <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis"><code>globalThis</code></a> being an environment-neutral way to access global variables, though, maybe we can access <code>module.exports</code> that way?</p>
<pre><code class="language-js">// Export in Node.js for testing
if ('module' in globalThis &amp;&amp; 'exports' in globalThis.module) {
    globalThis.module.exports = { getActions };
}
</code></pre>
<p>Browser doesn't complain, so that's one out of two steps passed. Now to try and import it in the test:</p>
<pre><code class="language-js">import { getActions } from './index.js';
</code></pre>
<pre><code>$ node --test
file:///home/cvennevik/dev/crystal-spire/test.mjs:3
import { getActions } from './index.js';
         ^^^^^^^^^^
SyntaxError: Named export 'getActions' not found. The requested module './index.js' is a CommonJS module, which may not support all module.exports as named exports.
</code></pre>
<p>Ah. The code fails to actually export <code>getActions</code>. Wow, modules aren't straightforward to half-use. I think we need to take a proper research timeout.</p>
<p>...</p>
<p>Okay. Several important facts learned with a few web searches:</p>
<ul>
<li><code>module.exports</code> is used by <a href="https://nodejs.org/api/modules.html">CommonJS modules</a>, as opposed to <code>import</code>/<code>export</code> which are used by <a href="https://nodejs.org/api/esm.html">ECMAScript modules</a>.
<ul>
<li>If we use <code>module.exports</code> in one file and <code>import</code> in another, then we're mixing CommonJS and ES modules, and we have to rely on Node's <a href="https://nodejs.org/api/esm.html#interoperability-with-commonjs">CommonJS/ES module interoperability</a>.</li>
</ul>
</li>
<li>CommonJS modules are <a href="https://nodejs.org/api/modules.html#the-module-wrapper">executed inside a function wrapper</a>, and variables like <code>module</code> and <code>require</code> are parameters of that function. That explains why we can't access those variables through <code>globalThis</code>.</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof">The <code>typeof</code> operator</a> works on undeclared variables!</li>
</ul>
<p>Putting this all together, we can solve the export problem like this:</p>
<pre><code class="language-js">// Export in Node.js for testing
if (typeof module === 'object' &amp;&amp; module != null &amp;&amp; 'exports' in module) {
    module.exports = { getActions };
}
</code></pre>
<p>The HTML page loads without error! What about the command line tests?</p>
<pre><code>$ node --test
▶ getActions()
  ✔ can play Defend and End Turn (1.18629ms)
  ✔ can play Strike and End Turn (0.149797ms)
▶ getActions() (2.403152ms)
ℹ tests 2
ℹ suites 1
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 58.991965
</code></pre>
<p>Oh bless. It actually works. That was significantly more work that I thought this would be.</p>
<p>Halfway there. The other half is saying goodbye to the old test code:</p>
<ul>
<li>We remove the link to <code>tests.html</code> from our rendered HTML.</li>
<li>We remove <code>tests.html</code>.</li>
<li>We remove <code>tests.css</code>.</li>
<li>We remove the leftover code from <code>test.mjs</code>.</li>
</ul>
<p>And a couple finishing touches to <code>package.json</code>:</p>
<ul>
<li>Add <code>&quot;type&quot;: &quot;commonjs&quot;</code> to explicitly declare that <code>.js</code> files are CommonJS modules, as I saw recommended in the Node.js documentation.
<ul>
<li>We can still use <code>.mjs</code> for ES modules, like we do with <code>test.mjs</code>.</li>
</ul>
</li>
<li>Add a &quot;test&quot; script for <code>node --test</code> to document how we run our tests, and to make the <code>npm test</code> command work.</li>
</ul>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;crystal-spire&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;type&quot;: &quot;commonjs&quot;,
  &quot;scripts&quot;: {
    &quot;test&quot;: &quot;node --test&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;fast-check&quot;: &quot;^4.5.2&quot;
  }
}
</code></pre>
<p>And commit: <em>&quot;Migrate tests to Node.js test runner&quot;</em>.</p>
<p>Whew.</p>
<hr>
<p>I said we're doing property-based testing today, and I'm sticking to my word. We're not stopping until we're there.</p>
<p>From my understanding, property-based testing is about writing properties that should hold for any possible input, then running the code against random possible inputs to test that the properties hold.</p>
<p>Let's begin by reading through <a href="https://fast-check.dev/docs/introduction/getting-started/"><code>fast-check</code>'s Getting Started guide</a>...</p>
<ul>
<li><code>fast-check</code> &quot;works in any test runner without needing any specific change&quot;.
<ul>
<li>...putting a pin in that thought, but moving on...</li>
</ul>
</li>
<li>It suggests importing the library like <code>import fc from 'fast-check'</code>.</li>
<li>Inside any test in your test runner of choice, you call <code>fc.assert</code>, which &quot;takes a <strong>property</strong> and runs it multiple times&quot;.
<ul>
<li>In case of failure, it will try to &quot;shrink&quot; the input value to the simplest possible value that causes the failure. This is neat.</li>
</ul>
</li>
<li>Inside <code>fc.assert</code>, we declare properties using <code>fc.property</code>, which takes a list of <strong>arbitraries</strong> to generate inputs with, and a <strong>predicate</strong> to test the inputs with.</li>
</ul>
<p>Okay! This seems like a manageable set of concepts. It looks like the final piece we need is defining our own arbitraries. Browsing the <a href="https://fast-check.dev/docs/core-blocks/arbitraries/">arbitraries documentation</a>, <code>fast-check</code> has:</p>
<ul>
<li>Primitives, like <code>fc.boolean()</code>, <code>fc.integer()</code>, <code>fc.string()</code>, and so on, with parameters to customize their range of values.</li>
<li>Composites, like <code>fc.tuple()</code>, <code>fc.array()</code>, <code>fc.func()</code>, <code>fc.object()</code>, and so on.</li>
<li>Combiners, like <code>fc.constant()</code>, <code>fc.subarray()</code>, <code>fc.option()</code>, and chaining methods like <code>.filter()</code> and <code>.map()</code>.</li>
</ul>
<p>Let's jump into the deep end, then, and define an arbitrary for game states. For generating objects with specific properties, we'll want to use <code>fc.record()</code>:</p>
<pre><code class="language-js">let arbGameState = fc.record({
    hp: 'todo',
    maxHp: 'todo',
    block: 'todo',
    energy: 'todo',
    maxEnergy: 'todo',
    hand: 'todo',
    drawPile: 'todo',
    discardPile: 'todo',
    relics: 'todo',
    enemies: 'todo'
});
</code></pre>
<p>For the numbers, we can use <code>fc.nat()</code> for all non-negative integer values:</p>
<pre><code class="language-js">let arbGameState = fc.record({
    hp: fc.nat(),
    maxHp: fc.nat(),
    block: fc.nat(),
    energy: fc.nat(),
    maxEnergy: fc.nat(),
    hand: 'todo',
    drawPile: 'todo',
    discardPile: 'todo',
    relics: 'todo',
    enemies: 'todo'
});
</code></pre>
<p>...although, no, this will generate invalid states where <code>hp</code> is greater than <code>maxHp</code>, and weird states where <code>energy</code> is in the hundreds of millions.</p>
<p>We could pass a max value to some of these, but for the sake of finding available moves, some of these never matter, so let's leave those a constant instead:</p>
<pre><code class="language-js">let arbGameState = fc.record({
    hp: fc.nat({ max: 80 }),
    maxHp: fc.constant(80),
    block: fc.constant(0),
    energy: fc.nat({ max: 999 }),
    maxEnergy: fc.constant(3),
    hand: 'todo',
    drawPile: 'todo',
    discardPile: 'todo',
    relics: 'todo',
    enemies: 'todo'
});
</code></pre>
<p>Then we have the hand and piles. We can define an arbitrary for cards:</p>
<pre><code class="language-js">let arbCard = fc.constantFrom('Strike', 'Defend', 'Bash');
</code></pre>
<p>Then define arrays using <code>fc.array()</code> - though, again, only cards in hand matter here, so <code>drawPile</code> and <code>discardPile</code> can be left as constant empty arrays, and relics also don't matter, so let's throw an empty array in there at the same time:</p>
<pre><code class="language-js">let arbGameState = fc.record({
    hp: fc.nat({ max: 80 }),
    maxHp: fc.constant(80),
    block: fc.constant(0),
    energy: fc.nat({ max: 999 }),
    maxEnergy: fc.constant(3),
    hand: fc.array(arbCard, { minLength: 0, maxLength: 10 }),
    drawPile: fc.constant([]),
    discardPile: fc.constant([]),
    relics: fc.constant([]),
    enemies: 'todo'
});
</code></pre>
<p>Finally, the <code>enemies</code> array. That will need an arbitrary for enemies:</p>
<pre><code class="language-js">let arbEnemy = fc.record({
    name: 'todo',
    hp: 'todo',
    maxHp: 'todo',
    block: 'todo',
    nextMove: 'todo',
    moveHistory: 'todo',
    buffs: 'todo',
    debuffs: 'todo'
});
</code></pre>
<p>...and I don't know of any relevant enemy variations, so let's make them all constants:</p>
<pre><code class="language-js">let arbEnemy = fc.record({
    name: fc.constant('Jaw Worm'),
    hp: fc.constant(42),
    maxHp: fc.constant(42),
    block: fc.constant(0),
    nextMove: fc.constant([]),
    moveHistory: fc.constant([]),
    buffs: fc.constant([]),
    debuffs: fc.constant([])
});
</code></pre>
<p>Then we can use it with <code>fc.array()</code> to specify <code>enemies</code>. I'm not sure whether the minimum number of enemies should be one or zero. I think killing the last enemy gets you to a game state with zero enemies, so let's make the lower bound zero:</p>
<pre><code class="language-js">let arbGameState = fc.record({
    hp: fc.nat({ max: 80 }),
    maxHp: fc.constant(80),
    block: fc.constant(0),
    energy: fc.nat({ max: 999 }),
    maxEnergy: fc.constant(3),
    hand: fc.array(arbCard, { minLength: 0, maxLength: 10 }),
    drawPile: fc.constant([]),
    discardPile: fc.constant([]),
    relics: fc.constant([]),
    enemies: fc.array(arbEnemy, { minLength: 0, maxLength: 5 })
});
</code></pre>
<p>There! We have an arbitrary for game states that hopefully works.</p>
<p>Now we write our first property-based test. Let's test for a property that I suspect will not pass: &quot;If the player has zero HP, the only available action is defeat.&quot;</p>
<pre><code class="language-js">it('only has Defeat available when HP is zero', () =&gt; {
    fc.assert(
        fc.property(arbGameState, gameState =&gt; {
            if (gameState.hp === 0) {
                let actions = getActions(gameState);
                assert.deepEqual(actions, [{ name: 'Defeat' }]);
            }
        })
    );
})
</code></pre>
<p>I don't think this is the ideal way to write the test - maybe, instead, the arbitrary should have <code>hp</code> as a constant of zero. But I think this will still work, and it's readable, and we can reconsider our approach when we have more tests.</p>
<p>Deep breath. Time to run it. Does it work?</p>
<pre><code>$ node --test
▶ getActions()
  ✔ can play Defend and End Turn (1.208453ms)
  ✔ can play Strike and End Turn (0.146097ms)
  ✖ only has Defeat available when HP is zero (7.154977ms)
    Error: Property failed after 10 tests
    { seed: -288422701, path: &quot;9:0:0:0:0&quot;, endOnFailure: true }
    Counterexample: [{&quot;hp&quot;:0,&quot;maxHp&quot;:80,&quot;block&quot;:0,&quot;energy&quot;:0,&quot;maxEnergy&quot;:3,&quot;hand&quot;:[],&quot;drawPile&quot;:[],&quot;discardPile&quot;:[],&quot;relics&quot;:[],&quot;enemies&quot;:[]}]
    Shrunk 4 time(s)
</code></pre>
<p>Haha!! It's alive!! We're doing property-based testing!!</p>
<p>Ahem. Now we can implement the fix:</p>
<pre><code class="language-js">function getActions(gameState) {
    if (gameState.hp === 0) {
        return [{ name: 'Defeat' }];
    }
    // ...
}
</code></pre>
<p>Then rerun the test:</p>
<pre><code>$ node --test
▶ getActions()
  ✔ can play Defend and End Turn (1.166435ms)
  ✔ can play Strike and End Turn (0.140492ms)
  ✔ only has Defeat available when HP is zero (7.334557ms)
</code></pre>
<p>We're green. We TDD'd with PBT. Our yak is shaved.</p>
<p>Commit: <em>&quot;Add 'Defeat' action&quot;</em>.</p>
<hr>
<p>I am really excited! We can finally return to implementing our solver, with the support of a testing tool I've wanted to try out for years. I'm stoked to figure out the design for this. Last time I did this in C# it got really involved, with a lot of files and a lot of tests. This time, I think we'll go with quite different strategies for both implementation and testing. I don't know what they will be yet! It'll be fun to find out!</p>
<p>See you next time :)</p>
<hr>
<p><em><a href="/crystal-spire/v14/">View this app version</a></em> | <em><a href="https://codeberg.org/cvennevik/crystal-spire/src/commit/1d04289fe180582797ccd23e595212791dafb02c">Last commit: Add 'Defeat' action</a></em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Crystal Spire #13: Haha, just kidding... unless?</title>
      <link>https://www.cvennevik.no/blog/crystal-spire-13-haha-just-kidding-unless/</link>
      <pubDate>Sun, 21 Dec 2025 19:49:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/crystal-spire-13-haha-just-kidding-unless/</guid>
      <content:encoded><![CDATA[<p>I want property-based tests for our project. Just a few hurdles to overcome:</p>
<ul>
<li>I have never written a property-based test before.</li>
<li>The most visible and actively maintained property-based testing library for JavaScript, <a href="https://fast-check.dev/">fast-check</a>, gives zero indication of being able to run standalone in a browser, and I don't want to add Node as a dependency if we can get away with it.</li>
<li>From what I can tell, implementing a decent property-based testing library is complicated.</li>
</ul>
<p>There's a ways we can go about this:</p>
<ol>
<li>Implement our own testing library. We'd be able to ensure it satisfies our requirements, and it would be a great learning experience. On the other hand, implementing a good enough library may take so much effort, it would take years before this series returns to actually implementing a Slay the Spire solver. It would be a massive <a href="https://projects.csail.mit.edu/gsb/old-archive/gsb-archive/gsb2000-02-11.html">yak shave</a>.</li>
<li>Find a way to run an existing library in the browser. It would let us get on with writing tests, and doing any property-based testing at all is still a great learning experience. On the other hand, the project wouldn't be as &quot;pure&quot; in terms of solving everything ourselves, and we'd be taking on a large amount of JavaScript to run our tests. Also it may not be possible.</li>
<li>Give up on running this project with just a browser and start relying on Node.</li>
<li>Give up on property-based tests and go back to example-based testing.</li>
</ol>
<p>Option 1 seems inappropriately ambitious. Option 2 seems potentially doable, although it may not give us the best tool for the job. Option 3 would mean getting access to a local JavaScript runtime and a package manager, which in turn gives us access to some of the best tools for both this job and many other jobs, but...</p>
<p>Okay, let's take a moment to consider. <em>Why</em> do I value keeping this project buildless and free of external dependencies?</p>
<ul>
<li>I want the code to be accessible to newcomers who do not already have all kinds of toolchains installed.</li>
<li>I want the code to be easy to copy into different contexts, without assuming a given framework or runtime.</li>
<li>I want the software to be <a href="https://dubroy.com/blog/cold-blooded-software/">cold-blooded</a>, so I can leave it for months and years and pick it up again just the way I left it.</li>
</ul>
<p>Not having to install anything extra to run the code is good! Building on a stable platform is good! Yet some things that are worth doing are not practical to do without extra dependencies. Some things require a web server. Some things require a database. Some things require a local JavaScript runtime like Node. Scratch that, <em>a lof of things</em> require a local JavaScript runtime like Node.</p>
<p>I also wonder if now, by the end of 2025, that Node qualifies as <a href="https://mcfunley.com/choose-boring-technology">boring technology</a>. It's 16 years old, ubiquitious, and seemingly fairly stable by now. I also wonder if property-based testing does <em>not</em> count as boring technology, and that we cannot realistically adopt it without paying the tax of an npm dependency.</p>
<p>And, unexpectedly, this self-imposed restriction is making me feel <em>lonely</em>. My setup today is a weird fringe thing that modern libraries and browser security mechanisms do not make space for. I have to make awkward workarounds and compromises and give up things that are table stakes for most developers. Doing web application development with no tooling, as it turns out, is not a path I want to invite others to follow. Certainly not for a project with such limited development time as this one.</p>
<p>Okay. Enough crisis of faith. It's time to bite the bullet.</p>
<p>Commit: <em>&quot;Add package.json&quot;</em></p>
<p>Commit: <em>&quot;Add node_modules to .gitignore&quot;</em></p>
<p>And we add our first dependency to <code>package.json</code>:</p>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;crystal-spire&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;devDependencies&quot;: {
    &quot;fast-check&quot;: &quot;^4.5.2&quot;
  }
}
</code></pre>
<pre><code>&gt; npm i

added 2 packages, and audited 3 packages in 658ms
</code></pre>
<p>Oh hey, <code>fast-check</code> actually only has a single transitive dependency, so our <code>node_modules</code> directory has a grand total of <em>two</em> packages in it. That's pretty neat!</p>
<p>Commit: <em>&quot;Add fast-check to devDependencies&quot;</em></p>
<p>That's enough excitement for one post. I still don't feel comfortable with this decision, but I accept the risk of regret.</p>
<p>Next time: property-based testing, for real this time.</p>
<hr>
<p><em><a href="/crystal-spire/v12/">View this app version</a></em> | <em><a href="https://codeberg.org/cvennevik/crystal-spire/src/commit/7183037e5a9185f72cd023a585041bf2a7cc10d6">Last commit: Add fast-check to devDependencies</a></em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Crystal Spire #12: A spot of cleaning</title>
      <link>https://www.cvennevik.no/blog/crystal-spire-12-a-spot-of-cleaning/</link>
      <pubDate>Sun, 14 Dec 2025 12:00:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/crystal-spire-12-a-spot-of-cleaning/</guid>
      <content:encoded><![CDATA[<p>Last time, the process of adding tests felt sloppy and clumsy. This time I want to put in some effort to make the process feel &quot;neat&quot; and &quot;elegant&quot; instead.</p>
<p>To begin, let's review the code and see if we can make it easier to work with. Points to note:</p>
<ol>
<li>The inline style block in <code>tests.html</code> could be extracted to a separate CSS file. It adds one more file, but leaves <code>tests.html</code> to act as glue between the different files, and gives us a natural spot to add more styles in the future.</li>
<li><code>tests.js</code> immediately runs code to render the page, making it impossible to reuse the file if we add another page with test code. We should extract a <code>render()</code> function like we did in <code>index.js</code>.</li>
<li>Taking that point more broadly, I think the global state and side effects in <code>index.js</code> and <code>tests.js</code> are design liabilities. I would rather convert them into libraries of pure functions, and add a little more glue code to <code>index.html</code> and <code>tests.html</code> to render the page.</li>
<li>We should consider converting the code from classic scripts to module scripts. Explicit imports and exports may come in handy.</li>
</ol>
<p>That looks like enough points to get started with. Point 1: Extract <code>tests.css</code>.</p>
<pre><code class="language-css">/* tests.css */
table, th, td {
    border: 1px solid grey;
    border-collapse: collapse;
}
</code></pre>
<pre><code class="language-html">&lt;!-- tests.html --&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;title&gt;Crystal Spire - Tests&lt;/title&gt;
    &lt;link rel=&quot;stylesheet&quot; href=&quot;./tests.css&quot;&gt;
&lt;/head&gt;
</code></pre>
<p>Commit: <em>&quot;Extract tests.css from tests.html&quot;</em>. Then onto point 2.</p>
<pre><code class="language-js">// tests.js
function renderTests() {
    document.body.innerHTML = `
    ...
    `;
}
</code></pre>
<pre><code class="language-html">&lt;!-- tests.html --&gt;
&lt;script&gt;renderTests()&lt;/script&gt;
</code></pre>
<p>Can't name it <code>render()</code> because that would collide with the same function from <code>index.js</code>. Another point towards using module scripts. Commit: <em>&quot;tests.js: Extract renderTests() function&quot;</em>.</p>
<p>Point 3 takes a bit of design judgement. We have a test suite, we want to run it, and we want to render the results. I do not know it should be decomposed (should we have a test suite object? a &quot;run tests&quot; function? a results object?), so let's make it all a single function.</p>
<pre><code class="language-js">// tests.js
function runAndRenderTests() {
    let testCases = [
        // ...
    ];

    for (let testCase of testCases) {
        testCase.actualOutput = getActions(testCase.input);
        testCase.passed = deepEqual(testCase.actualOutput, testCase.expectedOutput);
    }

    passedTestCases = testCases.filter(x =&gt; x.passed);
    failedTestCases = testCases.filter(x =&gt; !x.passed);

    return `
        ...
    `;
}
</code></pre>
<pre><code class="language-html">&lt;!-- tests.html --&gt;
&lt;script&gt;document.body.innerHTML = runAndRenderTests()&lt;/script&gt;
</code></pre>
<p>Commit: <em>&quot;tests.js: Convert to runAndRenderTests() function&quot;</em>.</p>
<p>Then onto <code>index.js</code>. Currently it deserializes <code>window.location.search</code> directly to get the game state. If we instead do that in <code>index.html</code> and pass the game state to the render function, that opens for rendering any game state we want.</p>
<pre><code class="language-html">&lt;!-- index.html --&gt;
&lt;script&gt;
let gameState = window.location.search ? deserialize(window.location.search) : defaultGameState;
document.body.innerHTML = render(gameState);
&lt;/script&gt;
</code></pre>
<pre><code class="language-js">// index.js
function render(gameState) {
    return `
        ...
    `;
}
</code></pre>
<p>Not a lot of code to change! Commit: <em>&quot;Make gameState a parameter to the render function&quot;</em>.</p>
<p>Finally, point 4 about <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules">JavaScript modules</a>. They have been broadly available since... what, 2018? I'm trying to find any downsides to switching to them, and the only things I can find is being forced into <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode">strict mode</a>, and a vague memory of some functionality having stricter security limitations when using modules? Both of these things sound like <em>upsides</em> in our case, so everything is pointing us towards modules.</p>
<p>Let's just add a little <code>type=&quot;module&quot;</code> to our inline script in <code>index.html</code>, and...</p>
<pre><code class="language-html">&lt;script type=&quot;module&quot;&gt;
import { render, deserialize } from './index.js';
let gameState = window.location.search ? deserialize(window.location.search) : defaultGameState;
document.body.innerHTML = render(gameState);
&lt;/script&gt;
</code></pre>
<pre><code>Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at file:///home/cvennevik/dev/crystal-spire/index.js. (Reason: CORS request not http).
</code></pre>
<p>Well fuck.</p>
<p>So if we use JavaScript modules, the browser enforces the Same Origin Policy on imports. And anything on the file system counts as a remote resource, so it blocks the request. That means our app becomes unusable as plain files on the file system, and requires a web server to function.</p>
<p>Given how we value being able to develop and run our code with minimal dependencies, adding a web server is <em>way</em> too steep a cost to pay for adopting modules. We have to scrap it and stick to classic scripts.</p>
<p>Maybe we should adopt &quot;strict mode&quot;, however. It looks like it will help catch mistakes loudly instead of silently. All it takes is adding <code>'use strict';</code> to the top of <code>index.js</code> and <code>tests.js</code>, and... Wait, what's this?</p>
<pre><code>Uncaught ReferenceError: assignment to undeclared variable passedTestCases
</code></pre>
<p>Ah! Two of the variables in tests.js are undeclared. Whoops! Thank you for pointing it out, strict mode, we'll fix it right away. Commit: <em>&quot;Use strict mode, fix undeclared variables&quot;</em>.</p>
<p>This post has already stretched over two days, so let's wrap it here. The code looks to be in good shape for further development now.</p>
<p>Next time: property-based testing, maybe?</p>
<hr>
<p><em><a href="/crystal-spire/v12/">View this app version</a></em> | <em><a href="https://codeberg.org/cvennevik/crystal-spire/src/commit/2c7260b737ce03f66eefc577f63307263e0e1350">Last commit: Use strict mode, fix undeclared variables</a></em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Crystal Spire #11: We need to test</title>
      <link>https://www.cvennevik.no/blog/crystal-spire-11-we-need-to-test/</link>
      <pubDate>Sat, 04 Oct 2025 16:05:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/crystal-spire-11-we-need-to-test/</guid>
      <content:encoded><![CDATA[<p>Happy Saturday. Let's implement a testing framework in roughly an hour.</p>
<hr>
<p>Part of the challenge I'm giving myself in this series is implementing everything in hand-written, buildless HTML + CSS + JS. I want to flex my web coding muscles and try some things I don't otherwise get to try.</p>
<p>I've used a few test frameworks over the years, but I've never tried making one myself. Nearly all of them involve using the command line as the test runner. We can't do that here, as I don't want to make this project dependent on something like Node. What we can do instead is use the browser as our test runner, a la <a href="https://jasmine.github.io/pages/getting_started.html">standalone Jasmine</a>.</p>
<p>To aid our thinking, let's code a little bit, and make a new HTML page to run our tests in. We make a new file titled <code>test.html</code>:</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;title&gt;Crystal Spire - Tests&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;noscript&gt;
        This application only works with JavaScript enabled.
    &lt;/noscript&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>Now let's sketch out what we want we want to see. We want to test that our <code>getActions</code> function works as expected. I think the simplest thing to implement would be an example-based approach, where we specify an input, pass it to the function, and compare the result to an expected output.</p>
<p>If the tests succeed, we can show something simple like...</p>
<pre><code class="language-html">&lt;h1&gt;Crystal Spire - Tests&lt;/h1&gt;
&lt;h2&gt;getActions&lt;/h2&gt;
&lt;p&gt;✅ 3 tests passed&lt;/p&gt;
</code></pre>
<p>And if any tests fail, we want to show how they failed:</p>
<pre><code class="language-html">&lt;h1&gt;Crystal Spire - Tests&lt;/h1&gt;
&lt;h2&gt;getActions&lt;/h2&gt;
&lt;p&gt;✅ 1 test passed&lt;/p&gt;
&lt;p&gt;❌ 2 tests failed&lt;/p&gt;
&lt;table&gt;
    &lt;tr&gt;
        &lt;th&gt;Input&lt;/th&gt;
        &lt;th&gt;Actual output&lt;/th&gt;
        &lt;th&gt;Expected output&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;code&gt;{ full gameState object }&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;[{ name: 'Play Defend' }]&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;[{ name: 'Play Defend' }, { name: 'End Turn' }]&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;code&gt;{ full gameState object }&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;[{ name: 'End Turn' }]&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;[{ name: 'End Turn' }, { name: 'Play Strike', enemyIndex: 0 }]&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
&lt;/table&gt;
</code></pre>
<p>I wasn't quite sure how to display the test results at first, but I think a table makes a lot of sense. The default display style doesn't give me any borders between cells in Firefox, though, so let's write our first bit of CSS up in the head, for readability:</p>
<pre><code class="language-html">&lt;style&gt;
    table, th, td {
        border: 1px solid black;
        border-collapse: collapse;
    }
&lt;/style&gt;
</code></pre>
<p>Yeah that looks alright. But, I have a sneaking suspicion the <code>{ full gameState object }</code> will look really long when it's a real object, so let's try pasting in the default gameState for something more realistic:</p>
<pre><code class="language-html">&lt;td&gt;&lt;code&gt;{ hp: 80, maxHp: 80, block: 0, energy: 3, maxEnergy: 3, hand: ['Bash', 'Defend', 'Defend', 'Defend', 'Strike'], drawPile: ['Defend', 'Strike', 'Strike', 'Strike', 'Strike'], discardPile: [], relics: ['Burning Blood'], enemies: [{ name: 'Jaw Worm', hp: 42, maxHp: 42, block: 0, nextMove: 'Chomp', moveHistory: [], buffs: [], debuffs: [] }]}&lt;/code&gt;&lt;/td&gt;
</code></pre>
<p>Wow that is hard to read on a single line. Maybe preformatted and pretty-printed would be better.</p>
<pre><code class="language-html">&lt;td&gt;&lt;pre&gt;&lt;code&gt;{
    hp: 80,
    maxHp: 80,
    block: 0,
    energy: 3,
    maxEnergy: 3,
    hand: ['Bash', 'Defend', 'Defend', 'Defend', 'Strike'],
    drawPile: ['Defend', 'Strike', 'Strike', 'Strike', 'Strike'],
    discardPile: [],
    relics: ['Burning Blood'],
    enemies: [
        {
            name: 'Jaw Worm',
            hp: 42,
            maxHp: 42,
            block: 0,
            nextMove: 'Chomp',
            moveHistory: [],
            buffs: [],
            debuffs: []
        }
    ]
}&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
</code></pre>
<p>Okay, that actually looks readable. I could give each test a name too:</p>
<pre><code class="language-html">&lt;th&gt;Name&lt;/th&gt;
</code></pre>
<pre><code class="language-html">&lt;td&gt;Can Play Defend and End Turn&lt;/td&gt;
</code></pre>
<pre><code class="language-html">&lt;td&gt;Can Play Strike and End Turn&lt;/td&gt;
</code></pre>
<p>It's kind of ugly, but it's servicable. Okay. Let's implement this.</p>
<p>First, we make a new JavaScript file for <code>test.html</code>, namely <code>test.js</code>. Then we create our test cases - actual real ones, this time:</p>
<pre><code class="language-js">let testCases = [
    {
        name: 'Can Play Defend and End Turn',
        input: {
            hp: 80,
            maxHp: 80,
            block: 0,
            energy: 3,
            maxEnergy: 3,
            hand: ['Defend'],
            drawPile: [],
            discardPile: [],
            relics: [],
            enemies: [
                {
                    name: 'Jaw Worm',
                    hp: 42,
                    maxHp: 42,
                    block: 0,
                    nextMove: 'Chomp',
                    moveHistory: [],
                    buffs: [],
                    debuffs: []
                }
            ]
        },
        expectedOutput: [{ name: 'Play Defend' }, { name: 'End Turn' }]
    },
    {
        name: 'Can Play Strike and End Turn',
        input: {
            hp: 80,
            maxHp: 80,
            block: 0,
            energy: 3,
            maxEnergy: 3,
            hand: ['Strike'],
            drawPile: [],
            discardPile: [],
            relics: [],
            enemies: [
                {
                    name: 'Jaw Worm',
                    hp: 42,
                    maxHp: 42,
                    block: 0,
                    nextMove: 'Chomp',
                    moveHistory: [],
                    buffs: [],
                    debuffs: []
                }
            ]
        },
        expectedOutput: [{ name: 'Play Strike', enemyIndex: 0 }, { name: 'End Turn' }]
    }
];
</code></pre>
<p>Then, we find the results of each test, by calling the function for each test case - and we might as well use the test case objects to store the results:</p>
<pre><code class="language-js">for (let testCase of testCases) {
    testCase.actualOutput = getActions(testCase.input);
}
</code></pre>
<p>And then we need to find out whether the test has passed or failed - whether actualOutput and expectedOutput match. So I guess we need to write a deep equality function that works for arrays, objects, and primitive values? There are definitely already implementations of this online, I've copied them before, but let me just write my best attempt off the top of my head...</p>
<pre><code class="language-js">function deepEqual(a, b) {
    // Passes equal cases of null, undefined, number, and string
    if (a === b) return true;

    // Passes equal cases of arrays
    if (Array.isArray(a)) {
        if (!Array.isArray(b)) return false;
        if (a.length !== b.length) return false;
        for (let i = 0; i &lt; a.length; i++) {
            if (!deepEqual(a[i], b[i])) {
                return false;
            }
        }
        
        return true;
    }

    // Passes equal cases of objects
    if (typeof a === 'object') {
        if (!typeof b === 'object') return false;

        let aKeys = Object.keys(a);
        let bKeys = Object.keys(b);
        if (aKeys.length !== bKeys.length) return false;

        for (let key of aKeys) {
            if (!bKeys.includes(key)) return false;
            if (!deepEqual(a[key], b[key])) return false;
        }

        return true;
    }

    // All equal cases we care about have passed - fail the equality
    return false;
}
</code></pre>
<p>Okay, whew, I hope that works. Let's take it for a spin:</p>
<pre><code class="language-js">for (let testCase of testCases) {
    testCase.actualOutput = getActions(testCase.input);
    testCase.passed = deepEqual(actualOutput, expectedOutput);
}
</code></pre>
<p>Then render the page based on this data, just like in <code>index.js</code>:</p>
<pre><code class="language-js">passedTestCases = testCases.filter(x =&gt; x.passed);
failedTestCases = testCases.filter(x =&gt; !x.passed);
document.body.innerHTML = `
    &lt;h1&gt;Crystal Spire - Tests&lt;/h1&gt;
    &lt;h2&gt;getActions&lt;/h2&gt;
    ${passedTestCases.length &gt; 0
        ? `&lt;p&gt;✅ ${passedTestCases.length} ${passedTestCases.length === 1 ? 'test' : 'tests'} passed&lt;/p&gt;`
        : ''
    }
    ${failedTestCases.length &gt; 0
        ? `
        &lt;p&gt;✅ ${failedTestCases.length} ${failedTestCases.length === 1 ? 'test' : 'tests'} passed&lt;/p&gt;
        &lt;table&gt;
            &lt;tr&gt;
                &lt;th&gt;Name&lt;/th&gt;
                &lt;th&gt;Input&lt;/th&gt;
                &lt;th&gt;Actual output&lt;/th&gt;
                &lt;th&gt;Expected output&lt;/th&gt;
            &lt;/tr&gt;
            ${failedTestCases.map(testCase =&gt; `
                &lt;tr&gt;
                    &lt;td&gt;${testCase.name}&lt;/td&gt;
                    &lt;td&gt;&lt;pre&gt;&lt;code&gt;${testCase.input}&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
                    &lt;td&gt;&lt;pre&gt;&lt;code&gt;${testCase.actualOutput}&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
                    &lt;td&gt;&lt;pre&gt;&lt;code&gt;${testCase.expectedOutput}&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
                &lt;/tr&gt;
            `)}
        `
        : ''
    }
`;
</code></pre>
<p>And plug the file into <code>test.html</code>, then try loading the page...</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;title&gt;Crystal Spire - Tests&lt;/title&gt;
    &lt;style&gt;
        table, th, td {
            border: 1px solid grey;
            border-collapse: collapse;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;noscript&gt;
        This application only works with JavaScript enabled.
    &lt;/noscript&gt;
&lt;/body&gt;
&lt;script src=&quot;test.js&quot;&gt;&lt;/script&gt;
&lt;/html&gt;
</code></pre>
<p>Ah, console error, getActions is not defined. I usually fix this with an import, but... what if we just...?</p>
<pre><code class="language-html">&lt;script src=&quot;index.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;test.js&quot;&gt;&lt;/script&gt;
</code></pre>
<p>Okay no that doesn't work because <code>index.js</code> also renders to our body. Right. Let's move the rendering logic of <code>index.js</code> into a render function:</p>
<pre><code class="language-js">function render() {
    document.body.innerHTML = `
        ...omitted...
    `;
}
</code></pre>
<p>I probably should factor it differently, but let's just get it working for now. So now we can call this function in <code>index.html</code> only:</p>
<pre><code class="language-html">&lt;script&gt;render()&lt;/script&gt;
</code></pre>
<p>Okay, now <code>index.html</code> still works. But <code>test.html</code> still fails because &quot;actualOutput is not defined&quot;. Need to go back to <code>test.js</code> and fix this block where I just wrote <code>deepEqual(actualOutput, expectedOutput)</code>, which gave a console error:</p>
<pre><code class="language-js">for (let testCase of testCases) {
    testCase.actualOutput = getActions(testCase.input);
    testCase.passed = deepEqual(testCase.actualOutput, testCase.expectedOutput);
}
</code></pre>
<p>And now, <em>now</em> - <code>test.html</code> renders! Wrongly. It says &quot;2 tests passed&quot;, but it displays a table with failed test results, and the input/output table cells say <code>[object Object]</code>. And there's a comma before the table.</p>
<p>One error at a time. It says &quot;2 tests passed&quot; because I copy-pasted it for the failed tests line, so let's fix that:</p>
<pre><code class="language-js">&lt;p&gt;❌ ${failedTestCases.length} ${failedTestCases.length === 1 ? 'test' : 'tests'} failed&lt;/p&gt;
</code></pre>
<p>Then the weird comma before the table... Oh, I also forgot to write <code>&lt;/table&gt;</code> at the end. Lovely of HTML for forgiving me forgetting that, let's enter it correctly. Oh, and I forgot to put a <code>.join('')</code> at the end of the row mapping, interpolating the plain array must somehow have resulted in a comma, fix that:</p>
<pre><code class="language-js">${failedTestCases.map(testCase =&gt; `
    &lt;tr&gt;
        &lt;td&gt;${testCase.name}&lt;/td&gt;
        &lt;td&gt;&lt;pre&gt;&lt;code&gt;${testCase.input}&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
        &lt;td&gt;&lt;pre&gt;&lt;code&gt;${testCase.actualOutput}&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
        &lt;td&gt;&lt;pre&gt;&lt;code&gt;${testCase.expectedOutput}&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
    &lt;/tr&gt;
`).join('')}
</code></pre>
<p>Then, let's use JSON.stringify to print the input/output prettier. The &quot;2&quot; argument at the end tells the function to use two spaces for indentation.</p>
<pre><code class="language-js">&lt;tr&gt;
    &lt;td&gt;${testCase.name}&lt;/td&gt;
    &lt;td&gt;&lt;pre&gt;&lt;code&gt;${JSON.stringify(testCase.input, null, 2)}&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
    &lt;td&gt;&lt;pre&gt;&lt;code&gt;${JSON.stringify(testCase.actualOutput, null, 2)}&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
    &lt;td&gt;&lt;pre&gt;&lt;code&gt;${JSON.stringify(testCase.expectedOutput, null, 2)}&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;
</code></pre>
<p>Now, finally, we have two successfully failing tests. We're expecting <code>{ &quot;name&quot;: &quot;End Turn&quot; }</code>, but we actually get <code>&quot;End Turn&quot;</code>, becuase I made a mistake last time:</p>
<pre><code class="language-js">function getActions(gameState) {
    /* ... */
    actions.push('End Turn');
    /* ... */
}
</code></pre>
<p>Now we can fix it:</p>
<pre><code class="language-js">function getActions(gameState) {
    /* ... */
    actions.push({ name: 'End Turn' });
    /* ... */
}
</code></pre>
<p>...and <code>test.html</code> tells us &quot;✅ 2 tests passed&quot;. That's it. We officially have tests for our project, and have used them to find and fix an error.</p>
<p>Before we forget, commit: <em>&quot;Add tests in test.html, fix 'End Turn' action in getActions()&quot;</em>.</p>
<hr>
<p>This felt sloppier than I want it to be. The code wasn't factored well for testing. The functions in <code>index.js</code> should be separated from the parts that read from <code>window.location.search</code> and write to <code>document.body.innerHTML</code>. And I made several plain mistakes that I would have caught if I had slowed down and re-read my code as I wrote it.</p>
<p>I'm also unsure about the example-based testing approach for the long run. The cases will get very verbose, and writing a thorough set of test cases will take a while. I think a property-based testing approach would suit this project better, so that after writing a generator for input game states, we can write properties for things like &quot;End Turn should always be available&quot; and &quot;if you have at least 1 Energy and a Strike in hand, you can Play Strike&quot;.</p>
<p>Still, what we have now is certainly better than nothing.</p>
<p>Oh, before we end, let's add some links to make the pages discoverable from each other:</p>
<pre><code class="language-js">// index.js, end of rendering code
&lt;a href=&quot;./test.html&quot;&gt;Tests page&lt;/a&gt;

// test.js, end of rendering code
&lt;a href=&quot;./index.html&quot;&gt;Main page&lt;/a&gt;
</code></pre>
<p>Commit: <em>&quot;Add links between tests page&quot;</em>.</p>
<p>...And heck it, the files should be named &quot;tests&quot; instead of &quot;test&quot;. Fix that, fix the links, fix the script source, last commit: <em>&quot;Rename 'test' files to 'tests'&quot;</em>.</p>
<p>Next time: Tidying and making this better, probably.</p>
<hr>
<p><em><a href="/crystal-spire/v11/">View this app version</a></em> | <em><a href="https://codeberg.org/cvennevik/crystal-spire/src/commit/e2c2d225d50efd4bc67d5e4b117ddf47f920bd9b">Last commit: Add getActions function</a></em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Crystal Spire #10: Finishing the easy bit</title>
      <link>https://www.cvennevik.no/blog/crystal-spire-10-finishing-the-easy-bit/</link>
      <pubDate>Sat, 16 Aug 2025 12:30:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/crystal-spire-10-finishing-the-easy-bit/</guid>
      <content:encoded><![CDATA[<p>Happy Saturday! Let's round up this game state business.</p>
<p>So, I'd forgotten that we already <em>do</em> have enemies' next move stored in state and rendered in the HTML. We just need to add their move history. Let's call it <code>moveHistory</code>.</p>
<pre><code class="language-js">{
    name: 'Jaw Worm',
    hp: 42,
    maxHp: 42,
    block: 0,
    nextMove: 'Chomp',
    moveHistory: [],
    buffs: [],
    debuffs: []
}
</code></pre>
<p>Then, for the &quot;end turn&quot; actions, add 'Chomp' to their move history:</p>
<pre><code class="language-js">&lt;a href=&quot;${encodeURI(serialize({
    ...defaultGameState,
    hp: 69,
    hand: ['Defend', 'Strike', 'Strike', 'Strike', 'Strike'],
    discardPile: ['Bash', 'Defend', 'Defend', 'Defend', 'Strike'],
    enemies: [{ ...defaultGameState.enemies[0], moveHistory: ['Chomp'] }]
}))}&quot;&gt;
    End turn 1 (60% chance)
&lt;/a&gt;
</code></pre>
<pre><code class="language-js">&lt;a href=&quot;${encodeURI(serialize({
    ...defaultGameState,
    hp: 69,
    hand: ['Defend', 'Strike', 'Strike', 'Strike', 'Strike'],
    discardPile: ['Bash', 'Defend', 'Defend', 'Defend', 'Strike'],
    enemies: [{ ...defaultGameState.enemies[0], moveHistory: ['Chomp'] }]
}))}&quot;&gt;
    End turn 2 (40% chance)
&lt;/a&gt;
</code></pre>
<p>Then finally, per enemy, display their move history:</p>
<pre><code class="language-js">${enemy.moveHistory.length &gt; 0 ? `&lt;p&gt;Past moves: ${enemy.moveHistory.join(', ')}&lt;/p&gt;` : ''}
</code></pre>
<p>Neat! Now the resulting states from the &quot;end turn&quot; action say &quot;Past moves: Chomp&quot;! Commit: <em>&quot;Add moveHistory to enemies&quot;</em>.</p>
<p>I noticed we don't update the Jaw Worm's next move in those URLs, so we need to fix that as well:</p>
<pre><code class="language-js">enemies: [{ ...defaultGameState.enemies[0], moveHistory: ['Chomp'], nextMove: 'Bellow' }]
</code></pre>
<pre><code class="language-js">enemies: [{ ...defaultGameState.enemies[0], moveHistory: ['Chomp'], nextMove: 'Thrash' }]
</code></pre>
<p>And then we'll need to update <code>moveDescriptions</code>:</p>
<pre><code class="language-js">let moveDescriptions = {
    'Chomp': 'Deal 11 damage',
    'Thrash': 'Deal 7 damage, gain 5 Block',
    'Bellow': 'Gain 3 Strength and 6 Block'
};
</code></pre>
<p>Done! Commit: <em>&quot;Update enemy nextMove in action URLs&quot;</em>.</p>
<p>We're now able to render any game state in the fight based on URL parameters. Nice.</p>
<hr>
<p>The next step will be a little more complicated. Those hardcoded actions and outcomes need to go, and we need to generate real ones based on the current game state. Once we have that, we'll be truly able to navigate through the entire fight.</p>
<p>I've been thinking about this a lot since last time, so let's establish some key concepts:</p>
<ul>
<li>An <strong>action</strong> is something the player can do anytime they're offered a choice. Examples:
<ul>
<li>Play a card</li>
<li>Use a potion</li>
<li>End the turn</li>
<li>Make a choice an effect asks of them (e.g. after drawing a card, <em>Warcry</em> asks you to place a card in hand back on top of the deck )</li>
</ul>
</li>
<li>An <strong>outcome</strong> is a resulting game state from an action.
<ul>
<li>An action may have multiple potential outcomes when randomness is involved (e.g. which cards you draw, what next move the enemy picks, etc.)</li>
</ul>
</li>
</ul>
<p>Based on this, I think we want two functions:</p>
<ul>
<li><code>getActions(gameState)</code> returns the actions you can take in a given game state.</li>
<li><code>getOutcomes(gameState, action)</code> returns the potential outcomes of an action, including the probability of each outcome.</li>
</ul>
<p>There's lots of ways we could structure the return values of these. In my first go implementing this in C#, &quot;action&quot; objects were tied to the game state they came from, and could <code>.Resolve()</code> to their potential outcomes. I'm going to try restricting actions and outcomes to be pure data this time around, on the hunch that shuffling pure data around will be easier to optimize for performance later than shuffling functions or methods around.</p>
<p>We can start out with this simple schema for actions:</p>
<pre><code class="language-js">{ name: 'Play Defend' }
</code></pre>
<p>Every action has a <code>name</code> property. Some actions may have additional properties:</p>
<pre><code class="language-js">{ name: 'Play Strike', enemyIndex: 0 }
</code></pre>
<p>We can write our <code>getOutcomes</code> function to read the additional properties only as needed, with different blocks of logic running depending on the action's <code>name</code>, which leaves a lot of flexibility for implementing future actions.</p>
<p>The outcome schema can be similarly simple:</p>
<pre><code class="language-js">{ gameState: gameState, probability: 0.6 }
</code></pre>
<p>Given this rough plan, we can break it down into three sequential steps:</p>
<ol>
<li>Implement <code>getActions</code></li>
<li>Implement <code>getOutcomes</code></li>
<li>Update our rendering logic to use these.</li>
</ol>
<p>Starting with step 1... &quot;Implement <code>getActions</code>&quot; seems a lot more complicated than anything we've done so far. Let's describe the requirements in plain English first, as far as they apply to the Jaw Worm fight:</p>
<ul>
<li>If we have <em>Strike</em> in hand and 1 or more Energy, we can 'Play Strike' on the Jaw Worm.</li>
<li>If we have <em>Bash</em> in hand and 2 or more Energy, we can 'Play Bash' on the Jaw Worm.</li>
<li>If we have <em>Defend</em> in hand and 1 or more Energy, we can 'Play Defend'.</li>
<li>We can always 'End Turn'.</li>
</ul>
<p>Oh. When we put it like this, it looks really straightforward to implement, actually.</p>
<pre><code class="language-js">function getActions(gameState) {
    let actions = [];
    if (gameState.hand.includes('Strike') &amp;&amp; gameState.energy &gt;= 1) {
        actions.push({ name: 'Play Strike', enemyIndex: 0 });
    }
    if (gameState.hand.includes('Bash') &amp;&amp; gameState.energy &gt;= 2) {
        actions.push({ name: 'Play Bash', enemyIndex: 0 });
    }
    if (gameState.hand.includes('Defend') &amp;&amp; gameState.energy &gt;= 1) {
        actions.push({ name: 'Play Defend' });
    }
    actions.push('End Turn');
    return actions;
}
</code></pre>
<p>Although, now I realize there are lots of cases our requirements don't handle. What if we're dead? What if we have multiple enemies?</p>
<p>And for that matter, how do we check that this code even works? So far, we've been refreshing the page, clicking around and seeing if everything looks correct, but now we have a lot more cases to check, and this isn't even wired up to the page yet.</p>
<p>I think this is the signal to start writing tests. For now, let's leave a comment above the function:</p>
<pre><code class="language-js">// TODO: Test
</code></pre>
<p>And commit: <em>&quot;Add getActions function</em>&quot;.</p>
<p>Next time: setting up our first tests!</p>
<hr>
<p><em><a href="/crystal-spire/v10/">View this app version</a></em> | <em><a href="https://codeberg.org/cvennevik/crystal-spire/src/commit/3ef1ac5b8d289ab1352c6abde946c20c29fa527e">Last commit: Add getActions function</a></em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Crystal Spire #9: Buffing the Worm&#39;s nails</title>
      <link>https://www.cvennevik.no/blog/crystal-spire-09-buffing-the-worms-nails/</link>
      <pubDate>Sat, 09 Aug 2025 13:00:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/crystal-spire-09-buffing-the-worms-nails/</guid>
      <content:encoded><![CDATA[<p>Small quality-of-life update: There is now a link at the bottom of each post in this series to a runnable version of the app, precisely as it was at the time of writing.</p>
<hr>
<p>The goal this time is to add enemy armor, buffs and debuffs to the dynamic state. The Jaw Worm gains armor and the Strength buff when using <em>Bellow</em>, and... hold up, I think I have some inconsistent terminology here.</p>
<p>Double-checking, and yes, it's not called &quot;armor&quot;, it's called &quot;block&quot;. Let's search, replace and commit: <em>&quot;Rename 'Armor' to 'Block'&quot;</em>.</p>
<p>Take two: The goal this time is to add enemy <em>block</em>, buffs and debuffs. The Jaw Worm gains block and the Strength buff when using <em>Bellow</em>, and gains the Vulnerable debuff when we play <em>Bash</em>.</p>
<p>Adding Vulnerable seems most straightforward, since we already have an action that's supposed to add it. We can add a <code>debuffs</code> property to the default Jaw Worm enemy object:</p>
<pre><code class="language-js">{
    name: 'Jaw Worm',
    hp: 42,
    maxHp: 42,
    nextMove: 'Chomp',
    debuffs: []
}
</code></pre>
<p>Then, we need to update the serialized &quot;enemies&quot; data in our URLs, to make sure they include the <code>debuffs</code> array. This is a bit of a pain, as I had to look up how to URI encode JSON again, and the strings I get now have less characters replaced than what's committed from before, which makes me nervous I'm doing it wrong. The data seems to deserialize fine, though, so let's just commit: <em>&quot;Add 'debuffs' array to enemy state&quot;</em>.</p>
<hr>
<p>Before we go any further, I want to reduce the pain of updating the URLs. Let's refactor and add some helpful functions. I want a <code>serialize</code> function that takes a game state object and serializes it to a URL query string, and I want a <code>deserialize</code> function that takes a URL query string and deserializes it to a game state object.</p>
<p>Step one: gather all our state variables into a single object.</p>
<pre><code class="language-js">let gameState = {
    hp: queryParams.get('hp') ?? 80,
    maxHp: queryParams.get('maxhp') ?? 80,
    block: queryParams.get('block'),
    energy: queryParams.get('energy') ?? 3,
    maxEnergy: queryParams.get('maxenergy') ?? 3,
    hand: queryParams.get('hand')?.split(',') ?? ['Bash', 'Defend', 'Defend', 'Defend', 'Strike'],
    drawPile: queryParams.get('draw')?.split(',') ?? ['Defend', 'Strike', 'Strike', 'Strike', 'Strike'],
    discardPile: queryParams.get('discard')?.split(',') ?? [],
    relics: queryParams.get('relics')?.split(',') ?? ['Burning Blood'],
    enemies: JSON.parse(queryParams.get('enemies')) ?? [
        {
            name: 'Jaw Worm',
            hp: 42,
            maxHp: 42,
            nextMove: 'Chomp',
            debuffs: []
        }
    ]
}
</code></pre>
<p>This also requires adding <code>gameState.</code> in front of everywhere we use these variables, e.g. <code>&lt;p&gt;HP: ${gameState.hp}/${gameState.maxHp}&lt;/p&gt;</code>. The page still loads fine, so this seems committable: <em>&quot;Gather game state variables into a gameState object&quot;</em>.</p>
<p>Step two: move our deserialization code into a <code>deserialize</code> function.</p>
<pre><code class="language-js">function deserialize(queryString) {
    let queryParams = new URLSearchParams(queryString);
    return  {
        hp: queryParams.get('hp') ?? 80,
        maxHp: queryParams.get('maxhp') ?? 80,
        block: queryParams.get('block'),
        energy: queryParams.get('energy') ?? 3,
        maxEnergy: queryParams.get('maxenergy') ?? 3,
        hand: queryParams.get('hand')?.split(',') ?? ['Bash', 'Defend', 'Defend', 'Defend', 'Strike'],
        drawPile: queryParams.get('draw')?.split(',') ?? ['Defend', 'Strike', 'Strike', 'Strike', 'Strike'],
        discardPile: queryParams.get('discard')?.split(',') ?? [],
        relics: queryParams.get('relics')?.split(',') ?? ['Burning Blood'],
        enemies: JSON.parse(queryParams.get('enemies')) ?? [
            {
                name: 'Jaw Worm',
                hp: 42,
                maxHp: 42,
                nextMove: 'Chomp',
                debuffs: []
            }
        ]
    }
}

let gameState = deserialize(window.location.search);
</code></pre>
<p>Runs fine, commit: <em>&quot;Extract deserialize(queryString) function&quot;</em>.</p>
<p>Step three: write a <code>serialize</code> function, and use it to generate the URLs we need.</p>
<pre><code class="language-js">function serialize(gameState) {
    return `?hp=${gameState.hp}`
        + `&amp;maxhp=${gameState.maxHp}`
        + `&amp;block=${gameState.block}`
        + `&amp;energy=${gameState.energy}`
        + `&amp;maxenergy=${gameState.maxEnergy}`
        + `&amp;hand=${String.join(gameState.hand, ',')}`
        + `&amp;draw=${String.join(gameState.drawPile, ',')}`
        + `&amp;discard=${String.join(gameState.discardPile, ',')}`
        + `&amp;relics=${String.join(gameState.relics, ',')}`
        + `&amp;enemies=${JSON.stringify(gameState.enemies)}`;
}
</code></pre>
<p>I <em>think</em> I did that right, so let's test it out:</p>
<pre><code>&lt;a href=&quot;${encodeURI(serialize({ ...gameState, energy: 1, hand: ['Defend', 'Defend', 'Defend', 'Strike'], discardPile: ['Bash'], enemies: [{ ...gameState.enemies[0], hp: 34 }]}))}&quot;&gt;
    Play Bash on Jaw Worm
&lt;/a&gt;
</code></pre>
<p>Still some effort to write out, but somewhat easier to read and modify. Testing it out, we get an error in the console: <em>&quot;Uncaught TypeError: String.join is not a function&quot;</em>. Whoops, I forgot how joining an array into a string works. This should work instead:</p>
<pre><code class="language-js">        + `&amp;hand=${gameState.hand.join(',')}`
        + `&amp;draw=${gameState.drawPile.join(',')}`
        + `&amp;discard=${gameState.discardPile.join(',')}`
        + `&amp;relics=${gameState.relics.join(',')}`
</code></pre>
<p>Success! The page renders, the URL looks alright, and clicking on it renders a correctly updated page! ...mostly.</p>
<p>Below player HP it now says:</p>
<pre><code>Block: null
</code></pre>
<p>That doesn't look right. When we don't have block, it gets serialized as the string &quot;null&quot;. Having no block probably shouldn't be stored as <code>null</code>. Storing it as 0 sounds correct, and since 0 is falsy, the templating logic we've written should still work.</p>
<p>Update <code>deserialize</code> to default <code>block</code> to 0:</p>
<pre><code class="language-js">        block: queryParams.get('block') ?? 0,
</code></pre>
<p>Bingo! Works like a charm now. Let's rewrite the remaining four URLs:</p>
<pre><code class="language-js">&lt;a href=&quot;${encodeURI(serialize({ ...gameState, energy: 2, hand: ['Bash', 'Defend', 'Defend', 'Defend'], discardPile: ['Strike'], enemies: [{ ...gameState.enemies[0], hp: 36 }] }))}&quot;&gt;
    Play Strike on Jaw Worm
&lt;/a&gt;
</code></pre>
<pre><code class="language-js">&lt;a href=&quot;${encodeURI(serialize({ ...gameState, block: 5, energy: 2, hand: ['Bash', 'Defend', 'Defend', 'Strike'], discardPile: ['Defend'] }))}&quot;&gt;
    Play Defend
&lt;/a&gt;
</code></pre>
<pre><code class="language-js">&lt;a href=&quot;${encodeURI(serialize({ ...gameState, hp: 69, hand: ['Defend', 'Strike', 'Strike', 'Strike', 'Strike'], discardPile: ['Bash', 'Defend', 'Defend', 'Defend', 'Strike'] }))}&quot;&gt;
    End turn 1 (60% chance)
&lt;/a&gt;
</code></pre>
<pre><code class="language-js">&lt;a href=&quot;${encodeURI(serialize({ ...gameState, hp: 69, hand: ['Defend', 'Strike', 'Strike', 'Strike', 'Strike'], discardPile: ['Bash', 'Defend', 'Defend', 'Defend', 'Strike'] }))}&quot;&gt;
    End turn 2 (40% chance)
&lt;/a&gt;
</code></pre>
<p>Yes, far more readable. And it all seems to work! Though, clicking around, it seems that by using <code>gameState</code> to produce our URLs, we can now end up in game states that partially combine the effects of each action. For instance, clicking &quot;Play Strike on Jaw Worm&quot; then &quot;Play Defend&quot; puts us in a state where we have 5 Block <em>and</em> the Jaw Worm has 36 HP. It's not a valid state, since we still have 2 Energy left and Strike in hand, but hey, kind of neat. Commit: <em>&quot;Create URLs with new serialize function&quot;</em>.</p>
<p>Neat as that bug is, let's clean it up by creating a <code>defaultGameState</code> and basing default state and the action URLs on that.</p>
<pre><code class="language-js">let defaultGameState = {
    hp: 80,
    maxHp: 80,
    block: 0,
    energy: 3,
    maxEnergy: 3,
    hand: ['Bash', 'Defend', 'Defend', 'Defend', 'Strike'],
    drawPile: ['Defend', 'Strike', 'Strike', 'Strike', 'Strike'],
    discardPile: [],
    relics: ['Burning Blood'],
    enemies: [
        {
            name: 'Jaw Worm',
            hp: 42,
            maxHp: 42,
            nextMove: 'Chomp',
            debuffs: []
        }
    ]
};
</code></pre>
<p>Now we can use that state if the URL has no query string, and remove the default values from <code>deserialize</code>, since every query string will now have fully serialized game state data:</p>
<pre><code class="language-js">let gameState = window.location.search ? deserialize(window.location.search) : defaultGameState;
</code></pre>
<pre><code class="language-js">function deserialize(queryString) {
    let queryParams = new URLSearchParams(queryString);
    return {
        hp: queryParams.get('hp'),
        maxHp: queryParams.get('maxhp'),
        block: queryParams.get('block'),
        energy: queryParams.get('energy'),
        maxEnergy: queryParams.get('maxenergy'),
        hand: queryParams.get('hand')?.split(','),
        drawPile: queryParams.get('draw')?.split(','),
        discardPile: queryParams.get('discard')?.split(','),
        relics: queryParams.get('relics')?.split(','),
        enemies: JSON.parse(queryParams.get('enemies'))
    }
}
</code></pre>
<p>Seems to work. Commit: <em>&quot;Replace defaults in deserialize() with defaultGameState object&quot;</em>. Then, replace our use of <code>gameState</code> with <code>defaultGameState</code> when generating URLs, like so:</p>
<pre><code class="language-js">&lt;a href=&quot;${encodeURI(serialize({ ...defaultGameState, energy: 1, hand: ['Defend', 'Defend', 'Defend', 'Strike'], discardPile: ['Bash'], enemies: [{ ...defaultGameState.enemies[0], hp: 34 }] }))}&quot;&gt;
    Play Bash on Jaw Worm
&lt;/a&gt;
</code></pre>
<p>Now the URLs stay consistent regardless of current game state. Commit: <em>&quot;Base URLs on defaultGameState instead of current gameState&quot;</em>.</p>
<p>Whoops. Seems to be a bug now where zero block gets deserialized as the string <code>&quot;0&quot;</code>, so the page says &quot;Block: 0&quot;. Let's update <code>deserialize</code> to convert strings to numbers where appropriate:</p>
<pre><code class="language-js">    hp: Number(queryParams.get('hp')),
    maxHp: Number(queryParams.get('maxhp')),
    block: Number(queryParams.get('block')),
    energy: Number(queryParams.get('energy')),
    maxEnergy: Number(queryParams.get('maxenergy')),
</code></pre>
<p>That fixes it. Commit: <em>&quot;Correctly deserialize number properties as numbers instead of strings&quot;</em></p>
<hr>
<p>Whew. I think that's enough refactoring done to the point where we can continue adding the Vulnerable debuff.</p>
<p>We need to store two aspects of debuffs in state: which debuff it is (by its name, probably), and how many &quot;stacks&quot; it has. Most buffs and debuffs are stackable, which either intensifies it (in Strength's case) or increases its duration (in Vulnerable's case).</p>
<p>Going off of this, we could represent the Vulnerable debuff as an object:</p>
<pre><code class="language-js">{ name: 'Vulnerable', stacks: 2 }
</code></pre>
<p>Add it to the enemy's <code>debuffs</code> when playing <em>Bash</em>:</p>
<pre><code class="language-js">&lt;a href=&quot;${encodeURI(serialize({ ...defaultGameState, energy: 1, hand: ['Defend', 'Defend', 'Defend', 'Strike'], discardPile: ['Bash'], enemies: [{ ...defaultGameState.enemies[0], hp: 34, debuffs: [{ name: 'Vulnerable', stacks: 2 }] }] }))}&quot;&gt;
    Play Bash on Jaw Worm
&lt;/a&gt;
</code></pre>
<p>Then for each enemy, render <code>enemy.debuffs</code> similarly to how we render cards and relics:</p>
<pre><code class="language-js">&lt;details open&gt;
    &lt;summary&gt;Debuffs (${enemy.debuffs.length})&lt;/summary&gt;
    &lt;ul&gt;
        ${enemy.debuffs.map(debuff =&gt;
            `&lt;li&gt;${debuff.name} (${debuff.stacks})&lt;/li&gt;`
        ).join('')}
    &lt;/ul&gt;
&lt;/details&gt;
</code></pre>
<p>Putting it all together, it works! That was easy enough. Though I'm not sure I like displaying a collapsible list of zero debuffs, so let's only render it when the enemy has at least one debuff:</p>
<pre><code class="language-js">${enemy.debuffs.length &gt; 0 ?
`&lt;details open&gt;
    &lt;summary&gt;Debuffs (${enemy.debuffs.length})&lt;/summary&gt;
    &lt;ul&gt;
        ${enemy.debuffs.map(debuff =&gt; `&lt;li&gt;${debuff.name} (${debuff.stacks})&lt;/li&gt;`).join('')}
    &lt;/ul&gt;
&lt;/details&gt;`
: ''}
</code></pre>
<p>Looks better. Now we can commit: <em>&quot;Add Vulnerable debuff when playing Bash&quot;</em>.</p>
<hr>
<p>For adding enemy block and Strength, none of the existing links in our template should lead to that, as the Jaw Worm is not about to use <em>Bellow</em>. To work around this, we can add a test link to the bottom of the page.</p>
<pre><code class="language-js">&lt;h2&gt;Test links&lt;/h2&gt;
&lt;a href=&quot;${encodeURI(serialize({ ...defaultGameState /* TODO: assign strength and block */ }))}&quot;&gt;
    Jaw Worm with 3 Strength and 6 Block
&lt;/a&gt;
</code></pre>
<p>Enemy block can be a <code>block</code> property, enemy buffs can be a <code>buffs</code> property, and each buff can have a <code>name</code> and <code>stacks</code>, just like debuffs.</p>
<p>We add the properties to our default game state:</p>
<pre><code class="language-js">{
    name: 'Jaw Worm',
    hp: 42,
    maxHp: 42,
    block: 0,
    nextMove: 'Chomp',
    buffs: [],
    debuffs: []
}
</code></pre>
<p>Update <code>block</code> and <code>buffs</code> in our test link:</p>
<pre><code class="language-js">&lt;a href=&quot;${encodeURI(serialize({ ...defaultGameState, enemies: [{ ...defaultGameState.enemies[0], block: 6, buffs: [{ name: 'Strength', stacks: 3 }] }] }))}&quot;&gt;
    Jaw Worm with 3 Strength and 6 Block
&lt;/a&gt;
</code></pre>
<p>Then render enemy block and buffs like we render player block and enemy debuffs:</p>
<pre><code class="language-js">${enemy.block ? `&lt;p&gt;Block: ${enemy.block}&lt;/p&gt;` : ''}
</code></pre>
<pre><code class="language-js">${enemy.buffs.length &gt; 0 ?
`&lt;details open&gt;
    &lt;summary&gt;Buffs (${enemy.buffs.length})&lt;/summary&gt;
    &lt;ul&gt;
        ${enemy.buffs.map(buff =&gt; `&lt;li&gt;${buff.name} (${buff.stacks})&lt;/li&gt;`).join('')}
    &lt;/ul&gt;
&lt;/details&gt;`
: ''}
</code></pre>
<p>Save, reload the page - curious, the page fails to render, and we have an error in the console: <em>&quot;Uncaught TypeError: can't access property &quot;length&quot;, enemy.buffs is undefined&quot;</em>. Turns out I have an old serialied state in the URL where <code>buffs</code> doesn't exist yet. Clearing the query string fixes it.</p>
<p>Now, clicking the test link, it says the Jaw Worm has 6 Block and a 3 Strength. Success! We can commit: <em>&quot;Add enemy block and buffs, add test link for this&quot;</em>.</p>
<hr>
<p>I'm fairly happy with progress this time. We have more maintainable code for serializing and deserializing game state, our action links are easier to keep up-to-date, and a little more of the game state is now representable.</p>
<p>If my counting is correct, the only piece of game state missing to be able to represent any moment in the Jaw Worm fight is the Jaw Worm's next move and its history of previous moves (to determine possible next moves). A good task for next time.</p>
<hr>
<p><em><a href="/crystal-spire/v9/">View this app version</a></em> | <em><a href="https://codeberg.org/cvennevik/crystal-spire/src/commit/e7139b1d6117008c221f849c19d693bec3404e32">Last commit: Set 'enemies' array based on query parameter</a></em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Crystal Spire #8: Facing our enemies</title>
      <link>https://www.cvennevik.no/blog/crystal-spire-08-facing-our-enemies/</link>
      <pubDate>Sat, 01 Feb 2025 15:38:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/crystal-spire-08-facing-our-enemies/</guid>
      <content:encoded><![CDATA[<p>Happy new year! Parenting is going great. We just moved my home computer out of the living room and into my office, so now, for the first time in a long while, I have comfortable spot to code in. The baby is napping longer and more consistently, too, so let's use a little of that time to chip away at this again. Dust off those programming muscles.</p>
<p>There's still a fair bit of game state to make dynamic, namely the enemy. This fight has one enemy, but there can be several enemies, and each of them has:</p>
<ul>
<li>A name</li>
<li>HP</li>
<li>Armor</li>
<li>Buffs and debuffs</li>
<li>Its next move</li>
<li>A history of previous moves
<ul>
<li>This affects which move it may choose next</li>
</ul>
</li>
</ul>
<p>All of these need to be made into a variable (an array of &quot;enemy&quot; objects, most likely), and then we need a way to encode it into the query string. Let's save the tricky bit for later, and hard-code the data first:</p>
<pre><code class="language-js">let enemies = [
    {
        name: 'Jaw Worm',
        hp: 42,
        maxHp: 42,
        nextMove: 'Chomp'
    }
];
</code></pre>
<p>Then use the data in our templating:</p>
<pre><code class="language-js">&lt;h2&gt;Enemies&lt;/h2&gt;
${enemies.map(enemy =&gt; `
    &lt;h3&gt;${enemy.name}&lt;/h3&gt;
    &lt;p&gt;HP: ${enemy.hp}/${enemy.maxHp}&lt;/p&gt;
    &lt;p&gt;Next move: ${enemy.nextMove}&lt;/p&gt;
`).join('')}
</code></pre>
<p>Looks okay, but we're missing the move description now. I don't think this is the long-term solution, but let's make a dictionary of descriptions, just like we did for relics, to keep it out of the dynamic state.</p>
<pre><code class="language-js">let moveDescriptions = { 'Chomp': 'Deal 11 damage' };
// ...
&lt;p&gt;Next move: ${enemy.nextMove} (${moveDescriptions[enemy.nextMove]})&lt;/p&gt;
</code></pre>
<p>Oh yeah, this is going to be a problem very soon. The Jaw Worm deals more than 11 damage when it gains strength, so a static dictionary of descriptions is not going to cut it. That's a future problem though, and an interesting one at that. Future us can spend a whole session figuring that out.</p>
<p>Commit: &quot;Render enemies based on 'enemies' array&quot;.</p>
<p>Now the difficult part. We need to encode this data, this array of objects, into our query string. We've only handled simple values and lists of simple values to this point. We need a different method for handling a list of objects.</p>
<p>Starting with the simplest thing that will possibly work... Can we just parse it as JSON?</p>
<pre><code class="language-js">let enemies = JSON.parse(queryParams.get('enemies')) ?? [
    {
        name: 'Jaw Worm',
        hp: 42,
        maxHp: 42,
        nextMove: 'Chomp'
    }
];
</code></pre>
<p>Well, the fallback value works correctly when &quot;enemies&quot; is missing from the query string.</p>
<p>Now what if we add <code>?enemies=[]</code> to it? Aha! No enemies!</p>
<p>Then what about <code>?enemies=[{&quot;name&quot;: &quot;Jaw Worm&quot;, &quot;hp&quot;: 30, &quot;maxHp&quot;: 42, &quot;nextMove&quot;: &quot;Chomp&quot;}]</code>? Goodness, me, it works. Didn't even need to deal with URI encoding.</p>
<p>Let's add this query parameter to our sketched action links, so we can finally see our attacks dealing damage:</p>
<pre><code class="language-js">&lt;a href=&quot;?maxhp=80&amp;maxenergy=3&amp;relics=Burning%20Blood&amp;hp=80&amp;energy=1&amp;hand=Defend,Defend,Defend,Strike&amp;draw=Defend,Strike,Strike,Strike,Strike&amp;discard=Bash&amp;enemies=%5B%7B%22name%22%3A%20%22Jaw%20Worm%22%2C%20%22hp%22%3A%2034%2C%20%22maxHp%22%3A%2042%2C%20%22nextMove%22%3A%20%22Chomp%22%7D%5D&quot;&gt;
    Play Bash on Jaw Worm
&lt;/a&gt;
// ...
&lt;a href=&quot;?maxhp=80&amp;maxenergy=3&amp;relics=Burning%20Blood&amp;hp=80&amp;energy=2&amp;hand=Bash,Defend,Defend,Defend&amp;draw=Defend,Strike,Strike,Strike,Strike&amp;discard=Strike&amp;enemies=%5B%7B%22name%22%3A%20%22Jaw%20Worm%22%2C%20%22hp%22%3A%2036%2C%20%22maxHp%22%3A%2042%2C%20%22nextMove%22%3A%20%22Chomp%22%7D%5D&quot;&gt;
    Play Strike on Jaw Worm
&lt;/a&gt;
</code></pre>
<p>Had to URI-encode the array to fit it into the HTML, but the JSON parsing still works. And these links work! We're dealing damage!</p>
<p>Commit: &quot;Set 'enemies' array based on query parameter&quot;.</p>
<p>All this URL parsing is very space-inefficient, but I don't think it the URL will grow long enough to cause issues while we're only running the Jaw Worm fight. It's good enough for now.</p>
<p>Hm, I keep excusing design flaws that I imagine we will fix later. I wonder if that's necessary.</p>
<p>Anyway. Next time: enemy armor, buffs, and debuffs.</p>
<hr>
<p><em><a href="/crystal-spire/v8/">View this app version</a></em> | <em><a href="https://codeberg.org/cvennevik/crystal-spire/src/commit/fe7c1d1bcd8ecaa20f4ef01108a4854c0cb896b9">Last commit: Set 'enemies' array based on query parameter</a></em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Crystal Spire #7: Armor</title>
      <link>https://www.cvennevik.no/blog/crystal-spire-07-armor/</link>
      <pubDate>Sat, 24 Aug 2024 09:30:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/crystal-spire-07-armor/</guid>
      <content:encoded><![CDATA[<p>We're back! With an updated series title! Nearly two weeks since the last post now. Hard to find time to write as a working parent.</p>
<p>Currently, our page is a bit of dynamically rendered HTML. Some of the content is based on the query string, others are hard-coded. The next logical step is making a little bit less of it hard-coded. Adding armor seems like a nice way to warm up.</p>
<p>Hmm. First I want to do a little refactoring. I want to extract our JavaScript to a separate file. It's not strictly necessary, and has a couple of cons (like adding another round-trip time on first page load, and not being able to throw our whole project around as a single HTML file), but it has several pros that I want right now:</p>
<ul>
<li>We can look at all the already-getting-quite-hairy parsing logic and templating without looking at all the HTML wrapping it.</li>
<li>We can comfortably remove one level of indentation.</li>
<li>Down the line, we want to be able to <em>test</em> the code, and having a separate file we can import the code from will help us out a lot.</li>
</ul>
<p>So, creating a file... What's the default name people give JavaScript files now, anyway? script.js? main.js? index.js? index.js lines up neatly with index.html in the file explorer, so let's go with that.</p>
<p>Wow, I've forgotten how to link to external JavaScript files from HTML, it's been so long since I set up a new project without a build system. Quick jump to the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script">MDN <code>&lt;script&gt;</code> element page</a>...</p>
<p>Whew, there's quite a few attributes to choose from nowadays! But after copying and pasting our JavaScript to the new file, it looks like we don't need any of the <code>type</code> or <code>async</code> or <code>defer</code> stuff, all we need to write is:</p>
<pre><code class="language-html">&lt;script src=&quot;index.js&quot;&gt;&lt;/script&gt;
</code></pre>
<p>I'm wondering if we should also add <code>type=&quot;module&quot;</code>. JavaScript modules are the modern way of structuring code, but a quick scan of <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules">the JavaScript modules page</a> reminds me that, yeah, it only really matters for exporting and importing code, and I'm not planning on splitting index.js into multiple files anytime soon. Let's keep it dead simple and omit it.</p>
<p>Commit: &quot;Extract JavaScript to index.js&quot;.</p>
<p>Back to armor. Let's start by sketching it up in hardcoded HTML:</p>
<pre><code class="language-html">&lt;p&gt;Armor: 5&lt;/p&gt;
</code></pre>
<p>Yeah, that's simple. And when there is no armor, we want to omit that paragraph entirely. Let's make it dynamic by adding a query parameter for &quot;armor&quot;:</p>
<pre><code class="language-js">let armor = queryParams.get('armor');
</code></pre>
<p>We're not assigning a fallback value here if &quot;armor&quot; is missing, because a <code>null</code> value is going to work fine the way we'll use it:</p>
<pre><code class="language-js">${armor ? `&lt;p&gt;Armor: ${armor}&lt;/p&gt;` : ''}
</code></pre>
<p>JavaScript type coercion is great, actually. If <code>armor</code> is any kind of <a href="https://developer.mozilla.org/en-US/docs/Glossary/Falsy">falsy value</a>, like <code>null</code>, <code>undefined</code>, or <code>0</code>, it evaluates to <code>false</code> and we render an empty string. If it has a <a href="https://developer.mozilla.org/en-US/docs/Glossary/Truthy">truthy value</a> - in other words, if we have any kind of armor - we render it in a paragraph. This is really handy and terse! But we need to see that it works, first. Let's add <code>&amp;armor=5</code> to our &quot;Play Defend&quot; query string:</p>
<pre><code class="language-html">&lt;a href=&quot;?maxhp=80&amp;maxenergy=3&amp;relics=Burning%20Blood&amp;hp=80&amp;armor=5&amp;energy=2&amp;hand=Bash,Defend,Defend,Strike&amp;draw=Defend,Strike,Strike,Strike,Strike&amp;discard=Defend&quot;&gt;
    Play Defend
&lt;/a&gt;
</code></pre>
<p>Wow, these URLs are getting terribly long. But it works! We get &quot;Armor: 5&quot; on that page, and nothing on the others. Commit: &quot;Render armor based on query parameter&quot;</p>
<p>Ah, my hour is already up. Small progress, but progress nonetheless.</p>
<hr>
<p><em><a href="/crystal-spire/v7/">View this app version</a></em> | <em><a href="https://codeberg.org/cvennevik/crystal-spire/src/commit/bc434acde371050735ad5e59736efdcb6ca71861/">Last commit: Render armor based on query parameter</a></em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Slay the Spire #6: Let&#39;s go to Codeberg!</title>
      <link>https://www.cvennevik.no/blog/slay-the-spire-06-lets-go-to-codeberg/</link>
      <pubDate>Sun, 11 Aug 2024 20:30:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/slay-the-spire-06-lets-go-to-codeberg/</guid>
      <content:encoded><![CDATA[<p>It's been a week since the last post of this series now, so I can definitely say that the pace of this series is slowing down after the end-of-holiday rush.</p>
<p>I've been thinking, for this project, that I want to make things fit into little steps, little sessions. If all I have is a little less than an hour, then I want to be able to fit writing a post and making some progress into that little hour. I have a few minutes to spare this evening, so let's see if we can put something out this week and give this thing a little heartbeat.</p>
<hr>
<p>I think it's time we set up version control for this. I want to have the file backed up somewhere else, I want to have a version history I can look back through, and I want to be able to link to specific snapshots of the code in these posts. No time better than now to get that set up.</p>
<p>Currently I host all my code on GitHub. I've thought about moving to a different host for anti-GitHub-Microsoft-monopoly reasons, but...</p>
<p>I was going to say I don't have time to set it up elsewhere right now, but do I know that's true? Let's set this project up on another Git hosting service I've been vaguely aware of: <a href="https://codeberg.org/">Codeberg</a>. It's a &quot;democratic, community-driven non-profit.&quot; Sounds neat!</p>
<h2>Step 1: Create account</h2>
<ul>
<li>Took me a couple of minutes, mostly waiting for the &quot;confirm your account&quot; email to show up.</li>
</ul>
<h2>Step 2: Go to settings and set up the SSH keys I expect I'll need</h2>
<ul>
<li>Oh hey I can set my pronouns for my profile here! Neat.</li>
<li>I already have an SSH public/private key pair set up locally, so probably I can just add my public key to Codeberg...?</li>
<li>Yup! Looks like it worked.</li>
</ul>
<h2>Step 3: Create a repository for our project</h2>
<ul>
<li>Gah, I already have a project named &quot;slay-the-spire-solver&quot; from my 2022 go at this. And naming it generically would be <em>boring</em>.</li>
<li>I think... I can make a swing for it an pick a name for the project now. What about...</li>
<li><strong>Crystal Spire?</strong></li>
<li>It's <em>just</em> unique enough (searching for it online gives me results for a World of Warcraft quest/item, I'm fine colliding with that), and crystal is a sort of material you can see through, but can also split light many, many ways? Sort of like we want to look through battles throughout the Spire?</li>
<li>That's it, I'm committing to it. <a href="https://codeberg.org/cvennevik/crystal-spire">cvennevik/crystal-spire</a> created. Description: &quot;A tool for analyzing combat encounters in Slay the Spire.&quot;</li>
</ul>
<h2>Step 4: Initialize the repository with our code</h2>
<ul>
<li>Okay so I want to do something here that will make this step harder. I want to create commits, retroactively, for the state of the code after each of the blog posts that I've written so far. I think it would be fun to follow the evolution of the project step by step like that.</li>
<li>I'll create a fresh &quot;crystal-spire&quot; directory on my computer and initialize the Git repository there.</li>
<li>Then, following my old blog posts, copying and pasting, let's see what we get...
<ul>
<li>First post. Create an initial index.html. <a href="https://codeberg.org/cvennevik/crystal-spire/commit/13ea21e15f66564b20ea0972187ce772c4437653">Commit: <em>&quot;#1: HTML first&quot;</em></a>.</li>
<li>Second post. <a href="https://codeberg.org/cvennevik/crystal-spire/commit/2fb0c59bcc6ab0799d350b69c9859b1059b7d95d">Commit: <em>&quot;#2: Actions and consequences&quot;</em></a>.</li>
<li>Third post. <a href="https://codeberg.org/cvennevik/crystal-spire/commit/12d6098e07ecd5de448b3d76e96805c9496145bb">Commit: <em>&quot;#3: Introducing links&quot;</em></a>.</li>
<li>Fourth post has no code changes.</li>
<li>Fifth post. Phew, here I can just copy and paste my latest version. <a href="https://codeberg.org/cvennevik/crystal-spire/commit/f6b46cd46502af1e872869ac2c8f6de71cfaa657">Commit: <em>&quot;#5: Going dynamic&quot;</em></a>.</li>
</ul>
</li>
</ul>
<p>And... that's it. The project is now version controlled. This is very comforting to me, as I like to use Git to keep track of current changes, and I commit frequently. When something's broken or half-done, it's stressful to me to have a lot of code in the air. I like being able to scrap everything I've done and return to a last known healthy point. Especially for a project like this that I want to make friendly to my short attention spans.</p>
<p>I'm going to go back and put links to these in all the previous posts. Ideally, I'd also want a link to the live version of the code at each version, so you can check it out and play around, but that looks non-trivial. Another time, maybe.</p>
<p>Now. One extra commit. To make the naming consistent.</p>
<pre><code class="language-html">&lt;title&gt;Crystal Spire&lt;/title&gt;
</code></pre>
<pre><code class="language-html">&lt;h1&gt;Crystal Spire&lt;/h1&gt;
</code></pre>
<p>Commit: &quot;Rename to Crystal Spire&quot;.</p>
<p>That's nice.</p>
<hr>
<p><em><a href="/crystal-spire/v6/">View this app version</a></em> | <em><a href="https://codeberg.org/cvennevik/crystal-spire/src/commit/2110563631b80369ec3d862d730d0b902e77a8b3/index.html">Last commit: &quot;Rename to Crystal Spire&quot;</a></em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>I can do hard things</title>
      <link>https://www.cvennevik.no/blog/i-can-do-hard-things/</link>
      <pubDate>Sun, 11 Aug 2024 09:00:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/i-can-do-hard-things/</guid>
      <content:encoded><![CDATA[<p>A couple of days ago a friend shared the post <a href="https://heydingus.net/blog/2024/8/you-and-i-can-do-hard-things">&quot;You (And I) Can Do Hard Things&quot; by Jarrod Blundy</a>. It placed the phrase &quot;you can do hard things&quot; squarely into my mind where it has been bouncing around since.</p>
<p>This morning I read <a href="https://angryweasel.substack.com/p/learning-and-hacking">&quot;Learning and Hacking&quot; by Alan Page</a>, which includes a copy of the essay &quot;Always Put Yourself On The Steepest Learning Curve.&quot; And now that's bouncing around in my mind too.</p>
<p>Add a shot of recent personal events, shake vigorously, and now I am <strong>losing my mind</strong>.</p>
<p>This week I crashed headfirst into what I'm being forced to call &quot;burnout,&quot; and I've realized that part of what brought it on is being persistently under-challenged. I haven't been doing &quot;hard things&quot; at my job for... hell, over a year now? And it's been grating on me to the point where, now, my soul cannot take it anymore?</p>
<p>And now Chance has it to put phrases like &quot;you can do hard things&quot; and &quot;put yourself on the steepest learning curve&quot; into my head this weekend and now I have too many things on my mind and I have to reconsider my entire work situation.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Slay the Spire #5: Going dynamic</title>
      <link>https://www.cvennevik.no/blog/slay-the-spire-05-going-dynamic/</link>
      <pubDate>Sun, 04 Aug 2024 14:15:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/slay-the-spire-05-going-dynamic/</guid>
      <content:encoded><![CDATA[<p>Hello! I am now, for the first time, writing this at a desktop computer. I'm kind of uncomfortable with how quickly I am able to write (I tend to stumble mentally when my typing catches up to my thinking), but this should make the development process a lot smoother.</p>
<p>Today we're going to have some fun. We're going to make our page content dynamic, based on the page URL.</p>
<p>To do that, we have to commit to an approach for rendering dynamic HTML.</p>
<hr>
<p>I'm a professional web developer. As is the way of things, of modern web development, I've only ever really worked with JavaScript frameworks. My experience with authoring dynamic web pages is mainly by using Vue. Building this project as a Vue app would be familiar and easy to me.</p>
<p>Here's the rub: I don't want to use a JavaScript framework for this project.</p>
<p>There's a mix of reasons for this, some more frivolous than others. If I'm being honest, the core reason is a certain sense of <em>craft snobbishness</em>. I like to author lean web pages that are fast to transfer, fast to parse, fast to run. Wherever possible, I like to build things myself using the features of the platform, instead of introducing dependencies like frameworks and libraries. I like to keep the total technology stack simple, shallow, shelf-stable.</p>
<p>I know, rationally, that it won't make a difference to user experience if I author this using a performant framework like Svelte or Solid. It would probably be less code to write, in total. I might get things done quicker. Be more productive.</p>
<p>But I don't wanna. I don't even wanna install any npm packages. I wanna try doing this all with vanilla JavaScript. Call it a learning experience, because I've never rendered dynamic UI in vanilla JavaScript before. But by God I am going to stumble my way through this and drag you with me.</p>
<hr>
<p>I did a little bit of research to see what approaches exist for changing the DOM with JavaScript. Of the ones I found, one stood out as being the least fuss to get started with: <code>innerHTML</code>.</p>
<p>Here's what we do: Add a little script to the bottom of our document...</p>
<pre><code class="language-html">&lt;script&gt;

&lt;/script&gt;
</code></pre>
<p>...and set <code>document.body.innerHTML</code>.</p>
<pre><code class="language-html">&lt;script&gt;
    document.body.innerHTML = &quot;&lt;h1&gt;Hello World&lt;/h1&gt;&quot;;
&lt;/script&gt;
</code></pre>
<p>Now our page says &quot;Hello World&quot;! We've successfully changed the page content with JavaScript, in one line of fairly uncomplicated code. Sadly, this also replaces all the HTML we've written up to this point, but we're about to fix that.</p>
<p>Bear with me. The next step may not be for the faint of heart.</p>
<pre><code class="language-html">&lt;body&gt;
    &lt;!-- Cut... --&gt;
&lt;/body&gt;
&lt;script&gt;
    // ...and paste.
    document.body.innerHTML = `
        &lt;h1&gt;Slay the Spire solver&lt;/h1&gt;
        &lt;h2&gt;Player: Ironclad&lt;/h2&gt;
        &lt;p&gt;HP: 80/80&lt;/p&gt;
        &lt;p&gt;Energy: 3/3&lt;/p&gt;
        &lt;details open&gt;
            &lt;summary&gt;Hand (5)&lt;/summary&gt;
            &lt;ul&gt;
                &lt;li&gt;Bash&lt;/li&gt;
                &lt;li&gt;Defend&lt;/li&gt;
                &lt;li&gt;Defend&lt;/li&gt;
                &lt;li&gt;Defend&lt;/li&gt;
                &lt;li&gt;Strike&lt;/li&gt;
            &lt;/ul&gt;
        &lt;/details&gt;
        &lt;details&gt;
            &lt;summary&gt;Draw pile (5)&lt;/summary&gt;
            &lt;ul&gt;
                &lt;li&gt;Defend&lt;/li&gt;
                &lt;li&gt;Strike&lt;/li&gt;
                &lt;li&gt;Strike&lt;/li&gt;
                &lt;li&gt;Strike&lt;/li&gt;
                &lt;li&gt;Strike&lt;/li&gt;
            &lt;/ul&gt;     
        &lt;/details&gt;
        &lt;details&gt;
            &lt;summary&gt;Discard pile (0)&lt;/summary&gt;
        &lt;/details&gt;
        &lt;details&gt;
            &lt;summary&gt;Relics (1)&lt;/summary&gt;
            &lt;ul&gt;
                &lt;li&gt;Burning Blood &lt;i&gt;(At the end of combat, heal 6 HP)&lt;/i&gt;&lt;/li&gt;
            &lt;/ul&gt;
        &lt;/details&gt;
        &lt;h2&gt;Enemies&lt;/h2&gt;
        &lt;h3&gt;Jaw Worm&lt;/h3&gt;
        &lt;p&gt;HP: 42/42&lt;/p&gt;
        &lt;p&gt;Next move: Chomp (Deal 11 damage)&lt;/p&gt;
        &lt;h2&gt;Actions&lt;/h2&gt;
        &lt;h3&gt;&lt;a href=&quot;#&quot;&gt;Play Bash on Jaw Worm&lt;/a&gt;&lt;/h3&gt;
        &lt;ul&gt;
            &lt;li&gt;Player: -2 Energy&lt;/li&gt;
            &lt;li&gt;Hand: -1 Bash&lt;/li&gt;
            &lt;li&gt;Discard pile: +1 Bash&lt;/li&gt;
            &lt;li&gt;Jaw Worm: -8 HP, +2 Vulnerable&lt;/li&gt;
        &lt;/ul&gt;
        &lt;h3&gt;&lt;a href=&quot;#&quot;&gt;Play Strike on Jaw Worm&lt;/a&gt;&lt;/h3&gt;
        &lt;ul&gt;
            &lt;li&gt;Player: -1 Energy&lt;/li&gt;
            &lt;li&gt;Hand: -1 Strike&lt;/li&gt;
            &lt;li&gt;Discard pile: +1 Strike&lt;/li&gt;
            &lt;li&gt;Jaw Worm: -6 HP&lt;/li&gt;
        &lt;/ul&gt;
        &lt;h3&gt;&lt;a href=&quot;#&quot;&gt;Play Defend&lt;/a&gt;&lt;/h3&gt;
        &lt;ul&gt;
            &lt;li&gt;Player: -1 Energy, +5 Armor&lt;/li&gt;
            &lt;li&gt;Hand: -1 Defend&lt;/li&gt;
            &lt;li&gt;Discard pile: +1 Defend&lt;/li&gt;
        &lt;/ul&gt;
        &lt;h3&gt;End turn&lt;/h3&gt;
        &lt;ul&gt;
            &lt;li&gt;Player: -11 HP&lt;/li&gt;
            &lt;li&gt;Hand: -1 Bash, -2 Defend, +3 Strike&lt;/li&gt;
            &lt;li&gt;Draw pile: -4 Strike, -1 Defend&lt;/li&gt;
            &lt;li&gt;Discard pile: +1 Bash, +3 Defend, +1 Strike&lt;/li&gt;
        &lt;/ul&gt;
        &lt;h4&gt;&lt;a href=&quot;#&quot;&gt;End turn 1 (60% chance)&lt;/a&gt;&lt;/h4&gt;
        &lt;ul&gt;
            &lt;li&gt;Jaw Worm: next move Bellow (gain 3 Strength and 6 Block)&lt;/li&gt;
        &lt;/ul&gt;
        &lt;h4&gt;&lt;a href=&quot;#&quot;&gt;End turn 2 (40% chance)&lt;/a&gt;&lt;/h4&gt;
        &lt;ul&gt;
            &lt;li&gt;Jaw Worm: next move Thrash (deal 7 damage, gain 5 Block)&lt;/li&gt;
        &lt;/ul&gt;
    `;
&lt;/script&gt;
</code></pre>
<p>Now - as long as you have JavaScript enabled - the page looks just like before. We've committed a slight crime against HTML by turning it into a JavaScript string, but hey, we're still authoring HTML, just uh, not in the actual HTML itself.</p>
<p>Oh, we should leave a note for visitors with JavaScript disabled...</p>
<pre><code class="language-html">&lt;body&gt;
    &lt;noscript&gt;
        This application only works with JavaScript enabled.
    &lt;/noscript&gt;
&lt;/body&gt;
</code></pre>
<p>Testing it by disabling JavaScript in the browser devtools. Yeah, it works.</p>
<p>Now what was the point of this crime? Well, now we can do this:</p>
<pre><code class="language-html">&lt;script&gt;
    let queryString = window.location.search;
    document.body.innerHTML = `
        &lt;h1&gt;Slay the Spire solver&lt;/h1&gt;
        &lt;p&gt;Query string: ${queryString}&lt;/p&gt;
        ...
    `;
&lt;/script&gt;
</code></pre>
<p>Opening <code>index.html</code>, and the new paragraph just says &quot;Query string&quot;. But if we open <code>index.html?hello=world</code>, we get &quot;Query string: ?hello=world&quot;! <strong>We can now use the query string to change page contents!</strong></p>
<p>Okay okay let's roll back that example and do something useful with this. Let's try setting the player HP using <code>?hp=80</code>.</p>
<pre><code class="language-js">let queryParams = new URLSearchParams(window.location.search);
let hp = queryParams.get('hp');
document.body.innerHTML = `
    ...
    &lt;p&gt;HP: ${hp}/80&lt;/p&gt;
    ...
`;
</code></pre>
<p>Visiting <code>index.html?hp=80</code>, it works! Oh, but visiting <code>index.html</code> it now says &quot;HP: null/80&quot;. We had better fall back to a default value when it's not set.</p>
<pre><code class="language-js">let hp = queryParams.get('hp') ?? 80;
</code></pre>
<p>There, back to normal. We can do the same for energy:</p>
<pre><code class="language-js">let energy = queryParams.get('energy') ?? 3;
// ...
&lt;p&gt;Energy: ${energy}/3&lt;/p&gt;
</code></pre>
<p>For cards in hand it's not as straightforward because it's a list. We could do <code>?hand=Bash,Defend,Defend,Defend,Strike</code>, and generate the list like this:</p>
<pre><code class="language-js">let hand = queryParams.get('hand')?.split(',') ?? ['Bash', 'Defend', 'Defend', 'Defend', 'Strike'];
// ...
&lt;details open&gt;
    &lt;summary&gt;Hand (${hand.length})&lt;/summary&gt;
    &lt;ul&gt;
        ${hand.map(card =&gt;
            `&lt;li&gt;${card}&lt;/li&gt;`
        ).join('')}
    &lt;/ul&gt;
&lt;/details&gt;
</code></pre>
<p>Same for discard pile and draw pile!</p>
<pre><code class="language-js">let drawPile = queryParams.get('draw')?.split(',') ?? ['Defend', 'Strike', 'Strike', 'Strike', 'Strike'];
let discardPile = queryParams.get('discard')?.split(',') ?? [];
// ...
&lt;details&gt;
    &lt;summary&gt;Draw pile (${drawPile.length})&lt;/summary&gt;
    &lt;ul&gt;
        ${drawPile.map(card =&gt;
            `&lt;li&gt;${card}&lt;/li&gt;`
        ).join('')}
    &lt;/ul&gt;
&lt;/details&gt;
&lt;details&gt;
    &lt;summary&gt;Discard pile (${discardPile.length})&lt;/summary&gt;
    &lt;ul&gt;
        ${discardPile.map(card =&gt;
            `&lt;li&gt;${card}&lt;/li&gt;`
        ).join('')}
    &lt;/ul&gt;
&lt;/details&gt;
</code></pre>
<p>And for completeness, though we won't change them our scenario, let's set maximum HP, maximum energy, and relics.</p>
<pre><code class="language-js">let maxHp = queryParams.get('maxhp') ?? 80;
let maxEnergy = queryParams.get('maxenergy') ?? 3;
let relics = queryParams.get('relics')?.split(',') ?? ['Burning Blood'];
// ...
&lt;p&gt;HP: ${hp}/${maxHp}&lt;/p&gt;
&lt;p&gt;Energy: ${energy}/${maxEnergy}&lt;/p&gt;
// ...
&lt;details&gt;
    &lt;summary&gt;Relics (${relics.length})&lt;/summary&gt;
    &lt;ul&gt;
        ${relics.map(relic =&gt;
            `&lt;li&gt;${relic}&lt;/li&gt;`
        ).join('')}
    &lt;/ul&gt;
&lt;/details&gt;
</code></pre>
<p>Sweet. All the player data can be controlled by query string now. Oh, but we lost one thing in the process: the description for Burning Blood. I think we could write relic descriptions in an object, and look it up from there:</p>
<pre><code class="language-js">let relicDescriptions = { 'Burning Blood': 'At the end of combat, heal 6 HP' };
// ...
`&lt;li&gt;${relic} &lt;i&gt;(${relicDescriptions[relic]})&lt;/i&gt;&lt;/li&gt;`
</code></pre>
<p>Yeah, works fine!</p>
<p>Next up would be enemy state, but I'm running out of time and energy for the day, and it looks tricky enough that I want to make it its own session. Before we wrap, let's make our links do something. We can't set all of the state yet, so we will just set the parts we are able to:</p>
<pre><code class="language-html">&lt;h3&gt;
    &lt;a href=&quot;?maxhp=80&amp;maxenergy=3&amp;relics=Burning%20Blood&amp;hp=80&amp;energy=1&amp;hand=Defend,Defend,Defend,Strike&amp;draw=Defend,Strike,Strike,Strike,Strike&amp;discard=Bash&quot;&gt;
        Play Bash on Jaw Worm
    &lt;/a&gt;
&lt;/h3&gt;
&lt;h3&gt;
    &lt;a href=&quot;?maxhp=80&amp;maxenergy=3&amp;relics=Burning%20Blood&amp;hp=80&amp;energy=2&amp;hand=Bash,Defend,Defend,Defend&amp;draw=Defend,Strike,Strike,Strike,Strike&amp;discard=Strike&quot;&gt;
        Play Strike on Jaw Worm
    &lt;/a&gt;
&lt;/h3&gt;
&lt;h3&gt;
    &lt;a href=&quot;?maxhp=80&amp;maxenergy=3&amp;relics=Burning%20Blood&amp;hp=80&amp;energy=2&amp;hand=Bash,Defend,Defend,Strike&amp;draw=Defend,Strike,Strike,Strike,Strike&amp;discard=Defend&quot;&gt;
        Play Defend
    &lt;/a&gt;
&lt;/h3&gt;
&lt;h4&gt;
    &lt;a href=&quot;?maxhp=80&amp;maxenergy=3&amp;relics=Burning%20Blood&amp;hp=69&amp;energy=3&amp;hand=Defend,Strike,Strike,Strike,Strike&amp;draw=&amp;discard=Bash,Defend,Defend,Defend,Strike&quot;&gt;
        End turn 1 (60% chance)
    &lt;/a&gt;
&lt;/h4&gt;
&lt;h4&gt;
    &lt;a href=&quot;?maxhp=80&amp;maxenergy=3&amp;relics=Burning%20Blood&amp;hp=69&amp;energy=3&amp;hand=Defend,Strike,Strike,Strike,Strike&amp;draw=&amp;discard=Bash,Defend,Defend,Defend,Strike&quot;&gt;
        End turn 2 (40% chance)
    &lt;/a&gt;
&lt;/h4&gt;
</code></pre>
<p>Now we have it! We have links to different game states! 🎉</p>
<p>There's just a few things left to fix before the scenario is fully-functioning, for some strained definition of &quot;just a few&quot;:</p>
<ol>
<li>Controlling enemy state via query string</li>
<li>Adding player armor</li>
<li>Adding enemy armor and debuffs</li>
<li>Generating valid actions based on the current state</li>
</ol>
<p>The first item will probably be one session of work, and the second and third another session. The fourth might take a little bit longer. Probably longer than all the work to that point put together.</p>
<p>Caveats before closing:</p>
<ul>
<li><code>innerHTML</code> is very simple and fun to work with. Setting it is also a <strong>security risk</strong> in case of malicious user input. Here, we're interpolating query parameters directly into the HTML, so it's trivial to craft URLs that add <em>anything</em> to the document. It's not ideal, but I'm choosing to accept this risk for now.</li>
<li>This simple scheme of using query parameters for each part of the state takes a lot of characters, and complex game states may need thousands of characters in the query string. Down the line, we'll want to encode this data in a more compact way.</li>
</ul>
<p>Mmh. Tired now. Write more later.</p>
<hr>
<p><em><a href="/crystal-spire/v5/">View this app version</a></em> | <em><a href="https://codeberg.org/cvennevik/crystal-spire/src/commit/f6b46cd46502af1e872869ac2c8f6de71cfaa657/index.html">Last commit: &quot;#5: Going dynamic&quot;</a></em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Slay the Spire #4: Links are real, and strong, and they are my friend</title>
      <link>https://www.cvennevik.no/blog/slay-the-spire-04-links-are-real/</link>
      <pubDate>Sat, 03 Aug 2024 19:26:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/slay-the-spire-04-links-are-real/</guid>
      <content:encoded><![CDATA[<p>The thing about doing a project as daily writing-and-coding sessions is you get a lot of time inbetween sessions to mull things over. Two things have been stuck in my mind since last time:</p>
<ol>
<li>My unease with the navigation issue, overriding and re-implementing link behavior.</li>
<li>How to dynamically render the HTML based on game state.</li>
</ol>
<p>I have a good idea for that second issue now, but I just returned from holiday, and lack the time and energy to finish it today. Ideally, I'd like to get a bit of coding done in each post I do, but if I leave this post in the pipes for a third day in a row, I'm going to feel constipated.</p>
<p>Today, we're only talking navigation.</p>
<hr>
<p>To recap the navigation issue: I concluded that we want to navigate between game states, as if they were pages, and that links are the most appropriate UI control for that. I also concluded that <em>we will have too much state to keep all the data in URLs</em>, meaning we have to override normal link behavior and re-implement it all to work with our application state.</p>
<p>I was aware, at the time, that it would be messy and difficult to do right. Having sat with it for a day, it now looks <em>even worse</em> in my mind.</p>
<p>When pages have state stored outside the URL, and links modify that state instead of navigating to a URL, here are all the affordances I can think of that we lose:</p>
<ul>
<li>Page history</li>
<li>Navigating to the next/previous page</li>
<li>Opening links in a new tab</li>
<li>Reloading pages without losing data</li>
<li>Browser bookmarks</li>
<li>Sharing page links with others</li>
</ul>
<p>Some of those items sound like a pain to re-implement to the standard of normal links. The rest of them sound <em>impossible</em> to re-implement to the same standard.</p>
<p>These affordances are <em>useful</em> and <em>expected</em>. Messing them up would make our app <em>kind of crap</em>. I don't want to make this project kind of crap, I want to make it <em>nice</em>. Messing with link behavior would make a mess that we cannot clean up.</p>
<p>However. I was struck with an idea. I think it <em>is</em> possible to fit all the necessary page state in less than a couple thousand characters. And if we accomplish that, the constraint that pushed us down this dark path in the first place is gone.</p>
<p>In other words:</p>
<p><strong>Plain links are back on menu!</strong></p>
<hr>
<p><em><a href="/crystal-spire/v3/">View this app version</a></em> | <em><a href="https://codeberg.org/cvennevik/crystal-spire/src/commit/12d6098e07ecd5de448b3d76e96805c9496145bb/index.html">Last commit: &quot;#3: Introducing links&quot;</a></em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Slay the Spire #3: Introducing links</title>
      <link>https://www.cvennevik.no/blog/slay-the-spire-03-introducing-links/</link>
      <pubDate>Thu, 01 Aug 2024 10:40:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/slay-the-spire-03-introducing-links/</guid>
      <content:encoded><![CDATA[<p>So I've been thinking.</p>
<p>Last time, I said we were aiming to explore the branching paths a game of Slay the Spire can take. A key feature - maybe <em>the</em> key feature - would be to view the resulting state of an action's outcome. And I think that means selecting an outcome, then having that outcome's state displayed on the screen, with all of its actions and all of their outcomes. And then you should be able to view a <em>next</em> outcome a level deeper, and again, and you should be able to move back to a previous one you have viewed.</p>
<p>This app needs to let you <strong>navigate between states.</strong></p>
<p>That means links. With custom behavior. And messing with the browser history so back/forward buttons work as expected.</p>
<p>I mean, <em>maybe</em> we can avoid that? Maybe the links can have query strings defining the complete next state? But no, no, the page should show you the path from the initial state, dozens and dozens of actions deep, with links to jump back to them. We can't fit all of that data into a query string, because I'm pretty sure browsers won't guarantee what happens when URLs exceed 2048 characters. And there's all kinds of other persistent state we may want.</p>
<p>I think there's no way around it.</p>
<p>Boy, what a can of worms we have in store for us.</p>
<hr>
<p>Thankfully, to sketch out the HTML, we can leave that can of worms on the table, unopened. That's all implementation details, and we're not implementing anything yet.</p>
<p>So. Every outcome wants a link. Every link wants text that describes the page it leads to. The outcome headings are precisely that. Let's reuse them.</p>
<pre><code class="language-html">&lt;h3&gt;&lt;a href=&quot;&quot;&gt;Play Bash on Jaw Worm&lt;/a&gt;&lt;/h3&gt;
</code></pre>
<p>Yeah, visually, that looks good to me. I'm unsure about the <code>href</code> value - I know that <code>href=&quot;#&quot;</code> is safe for links that shouldn't take you off the current page, but <code>href=&quot;&quot;</code> seems to have the same effect. Let me look up what the difference is.</p>
<p>Huh. Hard to test on my phone, but some people say that <code>href=&quot;&quot;</code> actually reloads the page, while <code>href=&quot;#&quot;</code> jumps to the top of the page? The latter seems like less trouble. If it's the wrong approach, it should be easy to fix later.</p>
<pre><code class="language-html">&lt;h3&gt;&lt;a href=&quot;#&quot;&gt;Play Bash on Jaw Worm&lt;/a&gt;&lt;/h3&gt;
</code></pre>
<p>Now for Strike:</p>
<pre><code class="language-html">&lt;h3&gt;&lt;a href=&quot;#&quot;&gt;Play Strike on Jaw Worm&lt;/a&gt;&lt;/h3&gt;
</code></pre>
<p>And Defend:</p>
<pre><code class="language-html">&lt;h3&gt;&lt;a href=&quot;#&quot;&gt;Play Defend&lt;/a&gt;&lt;/h3&gt;
</code></pre>
<p>And end turn... uh oh:</p>
<pre><code class="language-html">&lt;h3&gt;End turn&lt;/h3&gt;
&lt;ul&gt;
    &lt;li&gt;Player: -11 HP&lt;/li&gt;
    &lt;li&gt;Hand: -1 Bash, -2 Defend, +3 Strike&lt;/li&gt;
    &lt;li&gt;Draw pile: -4 Strike, -1 Defend&lt;/li&gt;
    &lt;li&gt;Discard pile: +1 Bash, +3 Defend, +1 Strike&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;a href=&quot;#&quot;&gt;60%&lt;/a&gt;&lt;/h4&gt;
&lt;ul&gt;
    &lt;li&gt;Jaw Worm: next move Bellow (gain 3 Strength and 6 Block)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;a href=&quot;#&quot;&gt;40%&lt;/a&gt;&lt;/h4&gt;
&lt;ul&gt;
    &lt;li&gt;Jaw Worm: next move Thrash (deal 7 damage, gain 5 Block)&lt;/li&gt;
&lt;/ul&gt;
</code></pre>
<p>I mean. It <em>works</em>. But &quot;60%&quot; and &quot;40%&quot; seem like terrible link texts, especially when we get more outcomes on the page with the same probabilities. I want link texts, taken out of context of where they are on the page, to give a decent idea of what they lead to, and to be unique.</p>
<p>Maybe if we expand the headings, swallow the cost of duplicating some text, it would be better.</p>
<pre><code class="language-html">&lt;h4&gt;&lt;a href=&quot;#&quot;&gt;End turn (60%)&lt;/a&gt;&lt;/h4&gt;
&lt;h4&gt;&lt;a href=&quot;#&quot;&gt;End turn (40%)&lt;/a&gt;&lt;/h4&gt;
</code></pre>
<p>Yeah. Better. And actually makes the page more readable to me. Still, the links won't be unique in the cases where they have the same probability.</p>
<p>Could we, uh, number them?</p>
<pre><code class="language-html">&lt;h4&gt;&lt;a href=&quot;#&quot;&gt;End turn 1 (60%)&lt;/a&gt;&lt;/h4&gt;
&lt;h4&gt;&lt;a href=&quot;#&quot;&gt;End turn 2 (40%)&lt;/a&gt;&lt;/h4&gt;
</code></pre>
<p>Looks... kind of weird, but I don't have a better idea for making them unique. Now we have two unexplained numbers right next to each other. It reads confusing to me, the meaning of the percentage looks <em>more</em> ambiguous now.</p>
<p>Would adding more text help us, again?</p>
<pre><code class="language-html">&lt;h4&gt;&lt;a href=&quot;#&quot;&gt;End turn 1 (60% chance)&lt;/a&gt;&lt;/h4&gt;
&lt;h4&gt;&lt;a href=&quot;#&quot;&gt;End turn 2 (40% chance)&lt;/a&gt;&lt;/h4&gt;
</code></pre>
<p>It's all subjective, so I don't know if other people finds this clearer, but this is my favorite so far. I'm happy to proceed with this.</p>
<p>Nice! The headings look better, and we now have links that promise us that we can explore all the ways the game can go. I like the look of this!</p>
<p>We actually have enough HTML sketched up now that we could stop it here and start working on implementing the logic, making this skeleton come alive. I think that's the right thing to do. Sketching up more features at this point would help surface design issues we will run into, but I trust our ability to solve them later better than our ability to prepare for them now.</p>
<hr>
<p>A lovely thing about these blog posts, these dev logs, is that it forces me to put each and every little thing I do into words. It forces me to justify my decisions. As a result, my development so far has been more deliberative, more steady and well-reasoned, than anything I can remember developing in private. I am very happy with every little step we've made along the way, even if - or maybe <em>because</em> - it's very small pieces of work, all in all.</p>
<p>These posts also crystallize my train of thought in a way where it's easy for me to pick up where I left off. If - sorry, <em>when</em> - I stop working on this project for a while, I have enough context made permanent on these pages that I should be able to pick it up exactly where I left off, even if it has been years.</p>
<p>I wonder if I'll have to put that theory to the test someday. Oh well.</p>
<p>Next time, let's write some JavaScript. Let's make things <em>change</em>.</p>
<hr>
<p><em><a href="/crystal-spire/v3/">View this app version</a></em> | <em><a href="https://codeberg.org/cvennevik/crystal-spire/src/commit/12d6098e07ecd5de448b3d76e96805c9496145bb/index.html">Last commit: &quot;#3: Introducing links&quot;</a></em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Slay the Spire #2: Actions and consequences</title>
      <link>https://www.cvennevik.no/blog/slay-the-spire-02-actions-and-consequences/</link>
      <pubDate>Wed, 31 Jul 2024 14:10:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/slay-the-spire-02-actions-and-consequences/</guid>
      <content:encoded><![CDATA[<p>This seems like a good time as any to start talking about what I want out of this project. In-between writing, I've had some time to mull it over.</p>
<p>I want to create a &quot;solver&quot; for Slay the Spire which functions as a <strong>highly interactive analytical tool</strong>. I am imagining it showing the possible actions you can take in any situation, their possible outcomes and their probabilities, and then letting you select an outcome to view and analyze further. I want a tree of past and future game states to move through. I want a search algorithm for optimal actions that is <strong>explainable, auditable, and tunable</strong>. I want to be able to <strong>query</strong> for specific outcomes like &quot;highest possible health remaining&quot; and &quot;highest guaranteed health remaining&quot; and &quot;least turns taken.&quot;</p>
<p>So, what I want is less a &quot;solver,&quot; something that spits out the solution for you to follow, makes you a passive user, and more a tool that invites you to be an active participant in the analytical process.</p>
<p>I think that's a neat vision to work towards.</p>
<hr>
<p>Today I want to sketch some more HTML and get some essential features in place. Particularly:</p>
<ul>
<li>Every possible action you can take in a game state</li>
<li>Every possible outcome from a given action</li>
</ul>
<p>Slay the Spire is a card game with a lot of inherent randomness in card draw order, as well as many random card effects, and some randomness in enemy moves. At any time in the game, you likely have many different cards you can play, and you can always end the turn. The game can branch in many different directions depending on the actions you choose, and branches yet again in the random outcomes actions have.</p>
<p>These branching paths are what we are here to explore. We need to display them.</p>
<p>For the opening scenario we've sketched, there are four possible actions we can take:</p>
<ul>
<li>Play Bash on Jaw Worm</li>
<li>Play Strike on Jaw Worm</li>
<li>Play Defend</li>
<li>End the turn</li>
</ul>
<p>One action at a time. Bash is an <em>attack</em> card that costs 2 energy to play, and reads: <em>&quot;Deal 8 damage. Apply 2 Vulnerable.&quot;</em> The result of playing it is:</p>
<ul>
<li>We go from 3 energy (our starting amount) to 1 energy.</li>
<li>Bash moves from the hand to the discard pile.</li>
<li>Jaw Worm goes from 42 to 34 HP.</li>
<li>Jaw Worm gets 2 turns of the <em>Vulnerable</em> debuff, which causes it to take 50% more damage from attacks.</li>
</ul>
<p>I realize now I forgot to add energy to the page last time. Better fix that first.</p>
<pre><code class="language-html">&lt;h2&gt;Player: Ironclad&lt;/h2&gt;
&lt;p&gt;HP: 80/80&lt;/p&gt;
&lt;p&gt;Energy: 3/3&lt;/p&gt;
</code></pre>
<p>Now. How do we present the action of playing Bash? I suspect this will be first hard and nuanced problem to solve. Let's be naive and continue to use headings for our first draft (and if we're lucky, the naive solution will be good enough):</p>
<pre><code class="language-html">&lt;h2&gt;Actions&lt;/h2&gt;
&lt;h3&gt;Play Bash on Jaw Worm&lt;/h3&gt;
</code></pre>
<p>Now the outcome, which is guaranteed:</p>
<pre><code class="language-html">&lt;h3&gt;Play Bash on Jaw Worm&lt;/h3&gt;
&lt;ul&gt;
    &lt;li&gt;Energy: -2&lt;/li&gt;
    &lt;li&gt;Hand: -1 Bash&lt;/li&gt;
    &lt;li&gt;Discard pile: +1 Bash&lt;/li&gt;
    &lt;li&gt;Jaw Worm: -8 HP, +2 Vulnerable&lt;/li&gt;
&lt;/ul&gt;
</code></pre>
<p>I am terribly, terribly unsure about how to present the outcomes of actions.</p>
<p>We could present them as the complete resulting game state, but that would take a lot of space and make it difficult to see what has changed. So presenting the change seems better.</p>
<p>We could present the change in a number of different ways. We could do &quot;Energy: -2&quot; or &quot;-2 Energy&quot; or &quot;3 → 1 Energy&quot; or &quot;Energy: 1/3.&quot; We could do &quot;Hand: -1 Bash&quot; or &quot;Hand: -Bash&quot; or &quot;Hand: Defend, Defend, Defend, Strike&quot; or &quot;Bash: Hand → Discard.&quot; Enemy damage has yet more possibilities: &quot;Jaw Worm: -8 HP&quot; or Jaw Worm: 34/42 HP&quot; or &quot;Jaw Worm: -8 (34) HP&quot; or Jaw Worm: 42 → 34 HP.&quot; And so on and so on.</p>
<p>How in the world are we to pick a single way to present this? Well, I have some preferences:</p>
<ul>
<li>It should be concise. Some actions will have many possible outcomes and many effects per outcome, so we may have to fit dozens of outcomes and hundreds of effects on the page. This data should be as digestible and comparable as possible.</li>
<li>I was going to write a speculative preference about sets of changes being easy to combine and &quot;do addition on,&quot; as I'm curious about modeling the data that way, but it sounded less and less important as I tried to write it. Discarding this idea for now.</li>
</ul>
<p>Let's aim for concise, digestible, and comparable, then. And for that, the first idea I sketched seems... fine. Good enough to sketch some more.</p>
<p>Two more card actions. Playing <em>Strike</em> costs 1 energy and deals 6 damage.</p>
<pre><code class="language-html">&lt;h3&gt;Play Strike on Jaw Worm&lt;/h3&gt;
&lt;ul&gt;
    &lt;li&gt;Energy: -1&lt;/li&gt;
    &lt;li&gt;Hand: -1 Strike&lt;/li&gt;
    &lt;li&gt;Discard pile: +1 Strike&lt;/li&gt;
    &lt;li&gt;Jaw Worm: -6 HP&lt;/li&gt;
&lt;/ul&gt;
</code></pre>
<p><em>Defend</em> is a <em>skill</em> card that costs 1 energy and reads &quot;Gain 5 Block.&quot; <em>Block</em> prevents the next X damage you would take this turn.</p>
<pre><code class="language-html">&lt;h3&gt;Play Defend&lt;/h3&gt;
&lt;ul&gt;
    &lt;li&gt;Energy: -1&lt;/li&gt;
    &lt;li&gt;Armor: +5&lt;/li&gt;
    &lt;li&gt;Hand: -1 Defend&lt;/li&gt;
    &lt;li&gt;Discard pile: +1 Defend&lt;/li&gt;
&lt;/ul&gt;
</code></pre>
<p>Hmm. What if we combined Energy and Armor to one line affecting the player?</p>
<pre><code class="language-html">&lt;h3&gt;Play Defend&lt;/h3&gt;
&lt;ul&gt;
    &lt;li&gt;Player: -1 Energy, +5 Armor&lt;/li&gt;
    &lt;li&gt;Hand: -1 Defend&lt;/li&gt;
    &lt;li&gt;Discard pile: +1 Defend&lt;/li&gt;
&lt;/ul&gt;
</code></pre>
<p>I kind of like that, &quot;-1 Energy&quot; reads nicer than &quot;Energy: -1.&quot; We could make them separate list items instead of bundling them, but enemies can also gain and lose armor, so I like the clarity of writing &quot;Player&quot; and &quot;Jaw Worm&quot; in front of changes to HP, armor, buffs and debuffs.</p>
<p>Let's change the other two actions to match.</p>
<pre><code class="language-html">&lt;h3&gt;Play Bash on Jaw Worm&lt;/h3&gt;
&lt;ul&gt;
    &lt;li&gt;Player: -2 Energy&lt;/li&gt;
    &lt;li&gt;Hand: -1 Bash&lt;/li&gt;
    &lt;li&gt;Discard pile: +1 Bash&lt;/li&gt;
    &lt;li&gt;Jaw Worm: -8 HP, +2 Vulnerable&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Play Strike on Jaw Worm&lt;/h3&gt;
&lt;ul&gt;
    &lt;li&gt;Player: -1 Energy&lt;/li&gt;
    &lt;li&gt;Hand: -1 Strike&lt;/li&gt;
    &lt;li&gt;Discard pile: +1 Strike&lt;/li&gt;
    &lt;li&gt;Jaw Worm: -6 HP&lt;/li&gt;
&lt;/ul&gt;
</code></pre>
<p>Looks good!</p>
<p>Okay, now for the big one: &quot;End turn.&quot; This causes a few things to happen:</p>
<ul>
<li>We discard our hand to the discard pile.</li>
<li>The enemy makes their move, hitting us for 11 damage.</li>
<li>We draw five cards from the draw pile.
<ul>
<li>We have precisely five cards in the draw pile, so we know which cards we will draw.</li>
</ul>
</li>
<li>We refresh our energy, setting it back to 3.</li>
<li>The enemy picks a new next move.
<ul>
<li><strong>This is random.</strong> After using Chomp, the Jaw Worm has:
<ul>
<li>45% chance of using Bellow (gain 3 Strength and 6 Block)</li>
<li>30% chance of using Thrash (deal 7 damage, gain 5 Block)</li>
<li><em>Normally</em> 25% chance of using Chomp again (deal 11 damage), but it cannot use Chomp twice in a row. The wiki isn't clear on this, but I assume this means that Bellow and Thrash become proportionally more likely, to 60% chance and 40% chance, respectively.</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>This means that ending the turn has two possible outcomes we need to present! How?</p>
<p>Well, headings seem to have been serving us well, so I see no reason to stop now.</p>
<pre><code class="language-html">&lt;h3&gt;End turn&lt;/h3&gt;
&lt;h4&gt;60%&lt;/h4&gt;
&lt;ul&gt;
    &lt;li&gt;Player: -11 HP&lt;/li&gt;
    &lt;li&gt;Hand: -1 Bash, -2 Defend, +3 Strike&lt;/li&gt;
    &lt;li&gt;Draw pile: -4 Strike, -1 Defend&lt;/li&gt;
    &lt;li&gt;Discard pile: +1 Bash, +3 Defend, +1 Strike&lt;/li&gt;
    &lt;li&gt;Jaw Worm: next move Bellow (gain 3 Strength and 6 Block)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;40%&lt;/h4&gt;
&lt;ul&gt;
    &lt;li&gt;Player: -11 HP&lt;/li&gt;
    &lt;li&gt;Hand: -1 Bash, -2 Defend, +3 Strike&lt;/li&gt;
    &lt;li&gt;Draw pile: -4 Strike, -1 Defend&lt;/li&gt;
    &lt;li&gt;Discard pile: +1 Bash, +3 Defend, +1 Strike&lt;/li&gt;
    &lt;li&gt;Jaw Worm: next move Thrash (deal 7 damage, gain 5 Block)&lt;/li&gt;
&lt;/ul&gt;
</code></pre>
<p>You know, this kind of works? It could be a lot more overwhelming. (It will become more overwhelming as we add more cards.)</p>
<p>Most details of the two outcomes are identical, guaranteed. What if we move the guaranteed bits to right after &quot;End turn,&quot; before the probabilities? Would that be better?</p>
<pre><code class="language-html">&lt;h3&gt;End turn&lt;/h3&gt;
&lt;ul&gt;
    &lt;li&gt;Player: -11 HP&lt;/li&gt;
    &lt;li&gt;Hand: -1 Bash, -2 Defend, +3 Strike&lt;/li&gt;
    &lt;li&gt;Draw pile: -4 Strike, -1 Defend&lt;/li&gt;
    &lt;li&gt;Discard pile: +1 Bash, +3 Defend, +1 Strike&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;60%&lt;/h4&gt;
&lt;ul&gt;
    &lt;li&gt;Jaw Worm: next move Bellow (gain 3 Strength and 6 Block)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;40%&lt;/h4&gt;
&lt;ul&gt;
    &lt;li&gt;Jaw Worm: next move Thrash (deal 7 damage, gain 5 Block)&lt;/li&gt;
&lt;/ul&gt;
</code></pre>
<p>Cons: We've gone from two blocks of changes to three, and need to mentally combine them. Pros: There is less to read, and we can clearly separate between guaranteed effects and random effects.</p>
<p>I think the pros outweigh the cons here. The pro/con calculus might shake out differently as game states get more complicated, but I'm happy to gently commit to this structure.</p>
<p>I'm less sure about how we present large changes to the hand, draw, and discard piles. I find it kind of messy to read. Not so much that I have other ideas I want to try out right now, but enough that I think we should keep an eye on it for later.</p>
<p>The &quot;60%&quot; and &quot;40%&quot; subheadings also look a bit too similar to the action headings with our raw, default HTML style. We will probably want to nest them more visibly under their parent heading very soon.</p>
<hr>
<p>Our little HTML sketch is getting quite fleshed out now. A little less straightforward, a little less certain, and a little more deliberative than last time, but we're still making forward progress.</p>
<p>I think we need, and I can get away with, one more round of HTML sketching before we can start thinking about implementing functionality. Right now, this is purely a document, with no hint of anywhere to interact. I don't know where the buttons will go! Or links? Are we using links? And what will happen when we click them?</p>
<p>Too many questions. Need more answers.</p>
<p>If you're reading this and these problems are getting your mind going as much as mine, feel free to message me <a href="mailto:cvennevik@gmail.com">by email</a> or <a href="https://www.hachyderm.io/@cvennevik">by fedi</a>. I'm not going to chat the project away outside of my blog, but I welcome ideas and suggestions.</p>
<p>Until next time!</p>
<hr>
<p><em><a href="/crystal-spire/v2/">View this app version</a></em> | <em><a href="https://codeberg.org/cvennevik/crystal-spire/src/commit/2fb0c59bcc6ab0799d350b69c9859b1059b7d95d/index.html">Last commit: &quot;#2: Actions and consequences&quot;</a></em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Slay the Spire #1: HTML first</title>
      <link>https://www.cvennevik.no/blog/slay-the-spire-01-html-first/</link>
      <pubDate>Tue, 30 Jul 2024 20:40:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/slay-the-spire-01-html-first/</guid>
      <content:encoded><![CDATA[<p>There are few things as daunting as a blank page.</p>
<p>Right now, all my solver project has to show for itself is a little bit of preamble and a vague idea. I do not have a scaffold to work on. I do not have a plan. I do not even have any clearly defined goals.</p>
<p>Before it's too late, let's get something down on the page, so we can look at it and discuss it. Let's write some HTML, in our first file, <code>index.html</code>.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;title&gt;Page title&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>My editor is kind enough to generate an empty HTML snippet for me. Let's replace the title:</p>
<pre><code class="language-html">&lt;title&gt;Slay the Spire solver&lt;/title&gt;
</code></pre>
<p>And let's add our first heading to the body:</p>
<pre><code class="language-html">&lt;h1&gt;Slay the Spire solver&lt;/h1&gt;
</code></pre>
<p>It's a thoroughly uninspiring name, and I want to find a more unique and identifiable name soon. Let's delay that until we have a bit of functionality, so we can pick a name that captures what our application actually does.</p>
<p>Opening the page, everything seems to display as expected! This is our Hello World. We now have something to start adding to.</p>
<p>At this point, I would consider making our first commit to version control. Alas: I do not have a development machine with me. I am coding this on my phone, using the first Android app I was able to find for web development. I do not have any version control set up, nor any command line available. Until I return home from my vacation, our process and tooling will stay exceedingly simple. So: no commiting anything right now.</p>
<p>Let's proceed to add something interesting. To start analyzing games and available actions and potential outcomes, I will at minimum need to display a game state. Let's start with that.</p>
<p>First, some player data:</p>
<pre><code class="language-html">&lt;h2&gt;Player: Ironclad&lt;/h2&gt;
&lt;p&gt;Health: 80/80&lt;/p&gt;
</code></pre>
<p>Already, I am forced to decide on how to present the data. I could structure this as a table, or as a list, or lists of lists, or sections, or something else entirely.</p>
<p>I've decided on using headings to separate the game state data into labeled sections for a few reasons:</p>
<ul>
<li>Headings are important for screen reader navigation.</li>
<li>Heading levels let me organize the data into a hierarchy of content.</li>
<li>Headings do not require any &quot;nesting&quot; of markup elements, compared to tables, lists, and lists of lists.</li>
</ul>
<p>For simple data like player health, I've opted to keep it to a dead simple paragraph tag. In my first sketches of this (<em>mea culpa</em>, I did try some of this out without you, before I had the space to write), I tried making it a table, or an unordered list. In the end, it all seemed like too much structure. A single-line paragraph for player health will suffice for now.</p>
<p>Now what other game state exists? There's the cards in hand, cards in draw pile, and cards in discard pile. Let's write up some example lists, for a realistic first turn:</p>
<pre><code class="language-html">&lt;h3&gt;Hand&lt;/h3&gt;
&lt;ul&gt;
    &lt;li&gt;Bash&lt;/li&gt;
    &lt;li&gt;Defend&lt;/li&gt;
    &lt;li&gt;Defend&lt;/li&gt;
    &lt;li&gt;Defend&lt;/li&gt;
    &lt;li&gt;Strike&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Draw pile&lt;/h3&gt;
&lt;ul&gt;
    &lt;li&gt;Defend&lt;/li&gt;
    &lt;li&gt;Strike&lt;/li&gt;
    &lt;li&gt;Strike&lt;/li&gt;
    &lt;li&gt;Strike&lt;/li&gt;
    &lt;li&gt;Strike&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Discard pile&lt;/h3&gt;
&lt;p&gt;Empty&lt;/p&gt;
</code></pre>
<p>Your hand can contain up to 10 cards, and your draw and discard piles can contain any number of cards, so these make sense as lists. Implicitly, I am structuring them as unordered sets, as order in hand does not matter and order in draw pile is typically unknown. Let's sort them alphabetically for consistency.</p>
<p>Though, that draw pile is taking up a lot of screen space for a relatively unimportant piece of information. What if we made it collapsible, in a <code>&lt;details&gt;</code> element?</p>
<pre><code class="language-html">&lt;details&gt;
    &lt;summary&gt;Draw pile&lt;/summary&gt;
    &lt;ul&gt;
        &lt;li&gt;Defend&lt;/li&gt;
        &lt;li&gt;Strike&lt;/li&gt;
        &lt;li&gt;Strike&lt;/li&gt;
        &lt;li&gt;Strike&lt;/li&gt;
        &lt;li&gt;Strike&lt;/li&gt;
    &lt;/ul&gt;
&lt;/details&gt;
</code></pre>
<p>Hmm, too little information when collapsed. Maybe if we add the card count to the summary...</p>
<pre><code class="language-html">&lt;summary&gt;Draw pile (5)&lt;/summary&gt;
</code></pre>
<p>This sits well with me. It also neatly mirrors the game UI, which only shows you the number of cards in draw pile by default.</p>
<p>Let's turn the hand and discard pile into <code>&lt;details&gt;</code> elements, too.</p>
<pre><code class="language-html">&lt;details&gt;
    &lt;summary&gt;Hand (5)&lt;/summary&gt;
    &lt;ul&gt;
        &lt;li&gt;Bash&lt;/li&gt;
        &lt;li&gt;Defend&lt;/li&gt;
        &lt;li&gt;Defend&lt;/li&gt;
        &lt;li&gt;Defend&lt;/li&gt;
        &lt;li&gt;Strike&lt;/li&gt;
    &lt;/ul&gt;
&lt;/details&gt;
&lt;!-- Draw pile --&gt;
&lt;details&gt;
    &lt;summary&gt;Discard pile (0)&lt;/summary&gt;
&lt;/details&gt;
</code></pre>
<p>Ah, but the hand should be open by default, because cards in hand are immediately relevant.</p>
<pre><code class="language-html">&lt;details open&gt;
</code></pre>
<p>That looks alright. In the future, I think I will want to add a little extra information to the cards, like energy cost, card type, effect, and such. For the &quot;sketching&quot; I'm doing now, I think it's better to skip it and add more essential things.</p>
<p>One more piece of player data: relics, which grant passive effects.The Ironclad's starting relic is <em>Burning Blood</em>, which heals 6 health at the end of every combat.</p>
<p>Ah, correction: It heals 6 <em>HP</em> at the start of every combat, according to the Slay the Spire wiki. I forgot the game uses the term &quot;HP,&quot; not &quot;health.&quot; Let's fix our display:</p>
<pre><code class="language-html">&lt;p&gt;HP: 80/80&lt;/p&gt;
</code></pre>
<p>And now, another <code>&lt;details&gt;</code> list for relics:</p>
<pre><code class="language-html">&lt;details&gt;
    &lt;summary&gt;Relics (1)&lt;/summary&gt;
    &lt;ul&gt;
        &lt;li&gt;Burning Blood (At the end of combat, heal 6 HP)&lt;/li&gt;
    &lt;/ul&gt;
&lt;/details&gt;
</code></pre>
<p>You know, I think the description would work well in italics. And though the <code>&lt;i&gt;</code> tag doesn't mean &quot;italic&quot; anymore, based on <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/i#usage_notes">MDN's usage notes</a>, I think I can swing it as semantically valid.</p>
<pre><code class="language-html">&lt;li&gt;Burning Blood &lt;i&gt;(At the end of combat, heal 6 HP)&lt;/i&gt;&lt;/li&gt;
</code></pre>
<p>I think that's every bit of player information. It's enough for our UI to start taking shape, at least.</p>
<p>Let's add the second half of the game state: the enemy. I want to start with the <em>Jaw Worm</em>, one of the three enemies you can encounter on the first floor.</p>
<pre><code class="language-html">&lt;h2&gt;Enemies&lt;/h2&gt;
&lt;h3&gt;Jaw Worm&lt;/h3&gt;
&lt;p&gt;HP: 42/42&lt;/p&gt;
</code></pre>
<p>We nest the &quot;Jaw Worm&quot; heading under an &quot;Enemies&quot; heading, since encounters can have more than one enemy.</p>
<p>Other than HP, we also know our enemies' <em>intention</em>, what move they will make when we end the turn. The Jaw Worm has three possible moves, but always starts with <em>Chomp</em>.</p>
<pre><code class="language-html">&lt;p&gt;Next move: Chomp &lt;i&gt;(Deal 11 damage)&lt;/i&gt;&lt;/p&gt;
</code></pre>
<p>Huh, I don't really like the italics here. Somehow I like it for relics, but not for enemy moves. Maybe because relics are more memorable and their descriptions are longer.</p>
<pre><code class="language-html">&lt;p&gt;Next move: Chomp (Deal 11 damage)&lt;/p&gt;
</code></pre>
<p>That's better.</p>
<hr>
<p>It's growing late, so we wrap the post here.</p>
<p>So far, I am happy with how smooth the process is. We've gone from &quot;nothing&quot; to &quot;something,&quot; which is major progress. I'm much more comfortable with the default, unstyled HTML look than I expected. At some point I will want to improve the presentation for clarity and usability, but the raw look will serve us well until the document structure stabilizes.</p>
<p>I'm also very happy I started drafting the HTML first instead of diving into JavaScript and writing logic. My C# version of the solver started with game logic, and the UI never graduated beyond printing game state to the console. It's very satisfying to me that this version is already more visually interesting than my last attempt.</p>
<p>More than that, I realize that starting with the HTML is extremely helpful for clarifying what I am making. It takes all my fuzzy, loose thoughts, and puts them in concrete terms on the page. It helps me see them better. It frees up my mind to think of next steps. It lets me start planning what features I want, see where they will fit in, start seeing friction and flaws where I need to adapt the design.</p>
<p>I feel very prepared to expand on this tomorrow. Probably writing more HTML, still: actions and outcomes. Until then!</p>
<hr>
<p><em><a href="/crystal-spire/v1/">View this app version</a></em> | <em><a href="https://codeberg.org/cvennevik/crystal-spire/src/commit/13ea21e15f66564b20ea0972187ce772c4437653/index.html">Last commit: &quot;#1: HTML first&quot;</a></em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Slay the Spire #0: Preamble</title>
      <link>https://www.cvennevik.no/blog/slay-the-spire-00-preamble/</link>
      <pubDate>Mon, 29 Jul 2024 10:05:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/slay-the-spire-00-preamble/</guid>
      <content:encoded><![CDATA[<p>I want to make a Slay the Spire solver again.</p>
<p>For the uninitiated: Slay the Spire is a video game where you fight your way through floor after floor of enemies until you face the Heart of the Spire. To get there, you have to defeat the enemies in turn-based battles by playing cards from your deck, of which there are dozens and dozens of different kinds - ones that hurt the enemy, ones that protect you, ones that power you up or weaken the enemy, and so on. At any point in a run, if you run out of health, you die and the run is over.</p>
<p>If you want to understand the game better and have a bit more context for what I'll be working with here, I suggest watching somebody playing a full run of the game - my favorite Slay the Spire streamers are <a href="http://www.youtube.com/@Baalorlord">Baalorlord</a> and <a href="http://www.youtube.com/@Jorbs">Jorbs</a>. <a href="https://youtu.be/vYkxc7eknWk">Here's a video of Baalorlord playing a run with the Ironclad that I quite like</a>.</p>
<p>If you would rather not spend time on that, but still want to see how I write a solver for the game, that’s fine too! I will have to implement each and every one of the mechanics of the game as I go, so it should be possible to follow along without too much prior knowledge.</p>
<hr>
<p>Back in 2022, I started a side project, just for fun, to write a program that would find the best possible moves to take in Slay the Spire. It was a C# project, I spent… a couple of months on it, I think? I got it to the point where it could simulate one of the first combat encounters of the game and pick reasonably good moves. It was a lot of fun to make.</p>
<p>Now, in 2024, I want to do it again, but different. I'm really interested in web development at this time, and I'm planning to write it as a single web page, using plain HTML, CSS, and lots of JavaScript.</p>
<p>Why? I want to place more emphasis on the UI and visualization, which is easier for me out-of-the-box with a web app. It is easier for people following along to check out the code themselves, play around with it, and maybe hack it a bit themselves - you only need your web browser. It's good practice for my web development job. And I get to publish it on my site.</p>
<p>There are several good reasons for me not to build it as a web app, the main one for me being that JavaScript gives poor performance for the kind of search algorithms I’ll be implementing, and performance is quickly going to become a bottleneck for the problems I'm looking to solve. I'm choosing to accept this - combinatorial explosion will cause me issues at some point no matter how much power and efficiency I throw at the problem, and I do not mind it coming to bite me slightly sooner. Learning more about JavaScript performance and optimization sounds fun, anyway!</p>
<p>Oh, and maybe the biggest reason for doing it this way: I figured out how to write HTML on my phone, so I can get this project started now, while I'm on holiday, away from my computer. 🙂</p>
<hr>
<p>I want to thank Ron Jeffries for inspiring me to write this. He’s written some lovely and entertaining series of blog posts of his own coding projects, and is currently writing a Sudoku solver (<a href="https://ronjeffries.com/articles/-x024/-z00/0/">first post here</a>). It looked fun enough that I was reminded of my old solver and wanted to do something similar myself.</p>
<p>I could get into what my goals with this are, what I am prioritizing, what I am deprioritizing, what to expect - but I'm growing tired of writing preamble. I'd rather my next bit of writing to actually get into it. We can figure all that big-picture stuff out as we go.</p>
<p>Onwards!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Having fun with SVG text</title>
      <link>https://www.cvennevik.no/blog/fun-with-svg-text/</link>
      <pubDate>Wed, 21 Feb 2024 20:15:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/fun-with-svg-text/</guid>
      <content:encoded><![CDATA[<p>Last weekend, I redesigned my blog. As part of this redesign, I wanted to make something fun for my front page. I started sketching a bit on paper, got an idea, and soon I was doing a search for &quot;css curved text&quot;.</p>
<p>The first result I found was the <a href="https://css-tricks.com/snippets/svg/curved-text-along-path/">Curved Text Along a Path tutorial</a> by Geoff Graham on CSS Tricks. It demonstrates how to add curved text to a web page using an inline SVG.</p>
<p>It was perfect.</p>
<p>About half an hour of experimenting and browsing the MDN Web Docs later, I was looking at the end result. It's the most beautiful thing I've made in years.</p>
<figure>
    <img
        src="/img/2024-02-18-front-page-screenshot.jpg"
        alt="My new front page, with links to my blog, webroll, Mastodon, and most prominently: a circular photo of a seal plush stuffed into a wine glass, with &quot;Welcome to my website&quot; written in balloon letters around the photo.">
    <figcaption>Pictured: My sweet little boy, trapped in glass.</figcaption>
</figure>
<p>Now, unless you have an above-average interest in HTML, CSS and vector graphics, you may want to tap out of this page now, because the rest of this will be me nerding out over how the sausage is made.</p>
<p>...</p>
<p>Still here?</p>
<p>Good! Let's jump in.</p>
<h2>Where we're going, we don't need Photoshop</h2>
<pre><code class="language-html">&lt;header&gt;
    &lt;svg viewBox=&quot;0 0 500 500&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;
        &lt;title&gt;Welcome to my website&lt;/title&gt;
        &lt;path id=&quot;top-curve&quot; d=&quot;M 100 100 C 100 90, 250 -40, 400 100&quot; stroke=&quot;transparent&quot; fill=&quot;transparent&quot;/&gt;
        &lt;path id=&quot;bottom-curve&quot; d=&quot;M 100 400 C 100 410, 250 540, 400 400&quot; stroke=&quot;transparent&quot; fill=&quot;transparent&quot;/&gt;
        &lt;text aria-hidden=&quot;true&quot; textLength=&quot;340&quot;&gt;
            &lt;textPath xlink:href=&quot;#top-curve&quot;&gt;Welcome to&lt;/textPath&gt;
        &lt;/text&gt;
        &lt;text aria-hidden=&quot;true&quot; textLength=&quot;340&quot;&gt;
            &lt;textPath xlink:href=&quot;#bottom-curve&quot; dominant-baseline=&quot;hanging&quot;&gt;my website&lt;/textPath&gt;
        &lt;/text&gt;
    &lt;/svg&gt;
    &lt;img src=&quot;/img/logo-500px.jpg&quot; alt=&quot;A seal in a wine glass&quot;&gt;
&lt;/header&gt;
</code></pre>
<p>This is the complete HTML for the front page header image and text. It is made up of two main elements:</p>
<ul>
<li>The <code>&lt;img&gt;</code> element, which draws the circular photo in the middle.</li>
<li>The <code>&lt;svg&gt;</code> element, which draws the text around it.</li>
</ul>
<h2>A side note on screen readers</h2>
<p>The word <em>&quot;draw&quot;</em> can be a bit misleading here, because it may lead you to believe the text is purely graphical.</p>
<figure>
    <img
        src="/img/2024-02-18-front-page-marked-text-v2.jpg"
        alt="Me selecting and highlighting part of the &quot;Welcome to&quot; text.">
    <figcaption>This brings me a silly amount of joy.</figcaption>
</figure>
<p>Yes - the SVG text functions pretty much like regular text! You can select it, copy it, and screen readers will even read it!</p>
<p><strong>That last point is a problem.</strong> When testing how the SVG reads with the built-in screen reader on Windows 11, it was... bad. It just does not read well. It may be that other screen readers handle SVG text better, but I do not want to rely on it.</p>
<p>To improve on this, I went with the most robust-looking solution I could find: I added a <code>&lt;title&gt;</code> element with equivalent text to the SVG, and hid the text elements from screen readers with <code>aria-hidden=&quot;true&quot;</code>. This made the reading experience significantly more pleasant.</p>
<p><em>(If you know of better, more accessible ways to do this, please contact me so I can update this post.)</em></p>
<h2>Styling</h2>
<p>To reliably wrap the text around the photo, I needed some reliable styling, and I needed that styling to be responsive across different screen sizes.</p>
<p>I solved this using a CSS grid where:</p>
<ul>
<li>The <code>&lt;img&gt;</code> and <code>&lt;svg&gt;</code> elements are always perfectly square.</li>
<li>They are always the same size.</li>
<li>They shrink on smaller screens.</li>
<li>They are placed directly on top of each other.</li>
</ul>
<pre><code class="language-css">.home-page header {
    display: grid;
    margin: 40px 0px;
    max-width: 500px; /* Will shrink, but keep height proportional if the screen is narrow */
}

.home-page header * {
    /* Place all child elements in the same, single cell of the grid */
    grid-column: 1;
    grid-row: 1;
    /* Ensure identical height and width */
    height: 100%;
    width: 100%;
}

.home-page header img {
    z-index: 1;
    border-radius: 100%; /* Makes the image circular */
    padding: 10%; /* Clears space for text around the image */
}

.home-page header svg {
    z-index: 2;
}
</code></pre>
<p>The image is made square by, well, being a square image. The SVG is made square by the <code>viewBox=&quot;0 0 500 500&quot;</code> property, which defines the internal dimensions of the SVG - the draw positions range from <code>(0,0)</code> to <code>(500,500)</code>.</p>
<p>And while we're in CSS world - the actual SVG text is also styled using CSS!</p>
<pre><code class="language-css">.home-page header svg text {
    fill: #89cff0;
    font-family: babycakes; /* Balloon font! https://www.fontspace.com/babycakes-font-f20531 */
    font-size: 36px;
    text-transform: uppercase;
}
</code></pre>
<h2>Text, paths, and text paths</h2>
<p>Let's look at the contents of the <code>&lt;svg&gt;</code> again, and remove <code>&lt;title&gt;</code> and <code>aria-hidden</code> for brevity.</p>
<pre><code class="language-html">&lt;path id=&quot;top-curve&quot; d=&quot;M 100 100 C 100 90, 250 -40, 400 100&quot; stroke=&quot;transparent&quot; fill=&quot;transparent&quot;/&gt;
&lt;path id=&quot;bottom-curve&quot; d=&quot;M 100 400 C 100 410, 250 540, 400 400&quot; stroke=&quot;transparent&quot; fill=&quot;transparent&quot;/&gt;
&lt;text textLength=&quot;340&quot;&gt;
    &lt;textPath xlink:href=&quot;#top-curve&quot;&gt;Welcome to&lt;/textPath&gt;
&lt;/text&gt;
&lt;text textLength=&quot;340&quot;&gt;
    &lt;textPath xlink:href=&quot;#bottom-curve&quot; dominant-baseline=&quot;hanging&quot;&gt;my website&lt;/textPath&gt;
&lt;/text&gt;
</code></pre>
<p>The <code>&lt;text&gt;</code> elements draw text. The <code>textLength</code> attributes say how many pixels long the text should be, and squishes or stretches the text to make it so. I gave both the top and bottom text elements <code>textLength=&quot;340&quot;</code> so they would stretch evenly left-to-right.</p>
<p>Inside them are the <code>&lt;textPath&gt;</code> elements, which draw text along a given path. The <code>xlink:href</code> attributes say which path they should follow. The <code>dominant-baseline=&quot;hanging&quot;</code> attribute places the bottom text <em>below</em> the path, instead of above it.</p>
<p>Finally, the transparent <code>&lt;path&gt;</code> elements supply the curves, as defined by their <code>d</code> attribute.</p>
<p>Can we talk about the curves? I <em>need</em> to talk to you about the curves.</p>
<h2>Bézier, you beautiful man</h2>
<p>Okay, so I've been in the rough vicinity of graphics and animation long enough to have heard &quot;Bézier curves&quot; referenced a few dozen times in my life. I never took the time to look up what they were. I assumed they were complicated and slightly magical, only taught in spellbooks sourced from the Graphics Wizards' tower.</p>
<p>To draw my text exactly how I wanted it, I needed Bézier curves. So I had to up look how they worked, and what all of the coordinates in the code samples meant.</p>
<p>It turns out I was bang on the money. They <em>are</em> magical.</p>
<figure>
    <img
        src="/img/bezier-quadratic.gif"
        alt="Animation of a quadratic (three control point) Bézier curve">
    <figcaption>Ah, Bézier, you've done it again!</figcaption>
</figure>
<p>I cannot hope to give a servicable tutorial for how they work - try the <a href="https://developer.mozilla.org/en-US/docs/Glossary/Bezier_curve">Bézier curve tutorial</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths">SVG Paths tutorial</a> on MDN for that.</p>
<p>In short, they consist of some number of <em>control points</em>, and move from the first control point to the final control point via the fun math-y thing the animation demonstrates. Two control points makes a linear curve (a line), three makes a quadratic curve, four makes a cubic curve, and so on.</p>
<p>This gave me just enough to go on to make a symmetric pair of cubic curves that gently curve around the image.</p>
<pre><code class="language-html">&lt;!-- Cubic curve from (100,100), via (100,90) and (250,-40), to (400,100) --&gt;
&lt;path id=&quot;top-curve&quot; d=&quot;M 100 100 C 100 90, 250 -40, 400 100&quot;/&gt;
</code></pre>
<p>Now, uh. In the middle of writing this, while reading the linked SVG Paths tutorial, I realized I could have made this using a simpler quadratic curve instead.</p>
<pre><code class="language-html">&lt;!-- Quadratic curve from (100,95), via (250,-25), to (400,94) --&gt;
&lt;path id=&quot;top-curve&quot; d=&quot;M 100 95 Q 250 -25, 400 95&quot;/&gt;
</code></pre>
<p>And. I also realized there's a non-Bézier &quot;arc&quot; curve. For drawing arcs. Like around a circle.</p>
<pre><code class="language-html">&lt;!-- Arc from (100,100), in a 210 px radius with sweep-flag enabled, to (400,100) --&gt;
&lt;path id=&quot;top-curve&quot; d=&quot;M 100 100 A 210 210 0 0 1 400 100&quot;/&gt;
</code></pre>
<p>It turns out arcs are a little bit complicated so for this use case I'm happy keeping it a Bézier curve.</p>
<p>Though... throwing a couple of arcs together <em>is</em> a simple way to draw a full circle path... so maybe...</p>
<p>I've already written up everything I learned so far, so let's close on this.</p>
<pre><code class="language-html">&lt;path id=&quot;curve&quot; d=&quot;M 100 100 A 210 210 0 0 1 400 400 A 210 210 0 0 1 100 100&quot; stroke=&quot;transparent&quot; fill=&quot;transparent&quot;/&gt;
&lt;text aria-hidden=&quot;true&quot; textLength=&quot;1320&quot;&gt;
    &lt;textPath xlink:href=&quot;#curve&quot;&gt;
        Imperial futures are only ever stolen presents.
    &lt;/textPath&gt;
&lt;/text&gt;
</code></pre>
<figure>
    <img
        src="/img/2024-02-21-imperial-futures-are-only-ever-stolen-presents.jpg"
        alt="My front page image, but now in a full circle around the photo it says &quot;Imperial futures are only ever stolen presents.&quot;">
    <figcaption>That's all, folks!</figcaption>
</figure>
]]></content:encoded>
    </item>
    
    <item>
      <title>Oops, I made code review painful</title>
      <link>https://www.cvennevik.no/blog/oops-i-made-code-review-painful/</link>
      <pubDate>Fri, 14 Apr 2023 20:15:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/oops-i-made-code-review-painful/</guid>
      <content:encoded><![CDATA[<p>A few months ago I wrote <a href="https://www.cvennevik.no/blog/code-reviews-are-overloaded/">a post</a> suggesting that you should limit your pull request reviews to their bare essentials, the &quot;bare essentials&quot; here meaning &quot;bugs and irreversible design decisions.&quot; I've had the chance to try doing this at work, and now I can share my findings with you:</p>
<p>Boy howdy, I do not enjoy this at all.</p>
<p>There is one critical issue I did not consider in my original post: whether or not the different parts of review were any enjoyable or engaging. As it turns out, by trimming out every &quot;non-essential&quot; concern from my reviews, I have trimmed away every bit of the activity that was somewhat enjoyable and engaging. My slimmed-down review process is fast, efficient, and insufferable.</p>
<p>Evaluating and discussing design choices, suggesting renames and refactorings, and taking the time to find and point out things I like - these were all things that engaged the parts of my brain that I enjoy using. It was inefficient and slow and occasionally tiring, but at the end of a review session, I was satisfied with my work.</p>
<p>Now, code reviews come in two variants: the small and easy ones that I can knock out quickly and get out of my sight, and the large and difficult ones that drain the soul out of me. 1000 line diffs used to be tough, multiple-hour review sessions where at the end I would be happy with my effort. This has been replaced with me stumbling away from the monitor trying to reawaken the parts of my brain that shut down halfway through after refusing to process any more of the code that I was jamming through my eye sockets.</p>
<p>At least there is a fun irony in all of happening because I made my reviews &quot;less demanding.&quot;</p>
<p>Now that I've subjected myself to my own suggested experiment, my feelings on pull requests reviews have cooled significantly. I do not want to do any more of these &quot;efficient&quot; reviews than I absolutely have to. Yet despite not enjoying it, I do not want to go back to the way I reviewed code before. Even if it is more bearable, it is still slow, inefficient and tiring.</p>
<p>Honestly, at this point, I just want to be subjected to as little code review possible, both as a reviewer and a author. This is something I am working on!</p>
<p>I'm a big fan of the <a href="https://martinfowler.com/articles/ship-show-ask.html">Ship / Show / Ask</a> model, which makes pull request reviews something authors explicitly opt into. To start pushing the needle away from &quot;ask for review on all changes all the time,&quot; I have asked for and received permission to merge low-risk changes without review. And if you haven't tried it before, let me tell you, being able to merge refactoring and test work straight into mainline feels so very freeing. I highly recommend it.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>A corner-cutter&#39;s confession</title>
      <link>https://www.cvennevik.no/blog/a-corner-cutters-confession/</link>
      <pubDate>Thu, 09 Feb 2023 20:45:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/a-corner-cutters-confession/</guid>
      <content:encoded><![CDATA[<p>Listen. I know I should have done this one better.</p>
<p>Yes, I didn't spend time improving the design before I went to work. I slotted my change right into the design that was already there. It fit okay! With some slight cruft, duplication, and clunky call chaining, but still! Don't look at me like that - even Kent Beck says &quot;making the change easy&quot; is the hard part, and my job is hard enough!</p>
<p>And yes, yes, I didn't write any tests first. I know it would have made my work easier, and refactoring less error-prone. And it would have helped me out in the design process. But I knew what I needed to do (mostly)! And I could just boot up the app and test it manually - it was familiar and convenient!</p>
<p>What's that? Did I write any tests after, then? Oh, no, I didn't do that either. I'd, uh, already tested all the use cases manually by the time I finished. It felt like a waste of time not to merge the work and move on. Yes, I have no cheap way of catching regressions now, I know, I know. Can we move on?</p>
<p>Because there's so much to move on to. I can't afford to feel like I'm slow. There's so many more things to do! Important things! Features! Stories! Bugs! <em>Pull requests!</em> The board is so long and the days are so short and <em>I need to do my part</em>.</p>
<p>...</p>
<p>I just... I know it's better to move slowly and deliberately. I know it's a matter of practice. And I know it relieves stress in the end.</p>
<p>But there's just so much code, so many loose threads, and so much work to do. I can only bear so much of it in my head.</p>
<p>Can you fault me for going along with the flow?</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>&#39;Technical debt&#39; is an incomplete model</title>
      <link>https://www.cvennevik.no/blog/technical-debt-is-an-incomplete-model/</link>
      <pubDate>Sun, 05 Feb 2023 10:51:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/technical-debt-is-an-incomplete-model/</guid>
      <content:encoded><![CDATA[<p>We throw around the &quot;technical debt&quot; metaphor a lot in software development. On project after project, we experience the system becoming harder to change as we work on it. Rushed design decisions come back to bite us in the butt. Progress slows to a crawl. We say we have accrued a lot of &quot;technical debt.&quot;</p>
<p>The &quot;technical debt&quot; metaphor is used to explain that we accrue <em>design flaws</em> as we work. These design flaws make it harder to change the system - the more flaws we have, the more we &quot;pay interest&quot; in increased time and effort to make new changes. To make the system easier to change again, we need to &quot;pay down the debt&quot; by fixing the design flaws. If we do not keep our &quot;technical debt&quot; in check, we risk accumulating so many design flaws that it is no longer economic to develop the system any further - we hit &quot;technical bankruptcy.&quot;</p>
<p>While this is a useful model, I've come to find it insufficient. It frames &quot;cost of change&quot; as something developers harm by introducing design flaws, and repair by removing design flaws. It is centered on the negative case.</p>
<p>The lessons I have learned about managing cost of change from the Extreme Programming community clashes with this framing, because the &quot;technical debt&quot; metaphor does not support the possibility that the cost of change can go <em>down</em> as you add changes to your system.</p>
<p>Eric Evans touches on this phenomenon through the lens of &quot;supple design&quot;:</p>
<blockquote>
<p>To have a project accelerate as development proceeds - rather than get weighed down by its own legacy - demands a design that is a pleasure to work with, inviting to change. A supple design.</p>
<p>— Eric Evans, <em>Domain-Driven Design</em>, Chapter Ten: &quot;Supple Design&quot;</p>
</blockquote>
<p>Eric compares the system design to a leather jacket that is initially stiff, but over months of use becomes comfortable and flexible in the joints. Similarly, when you keep making changes to the design as you work with the system, the parts you repeatedly need to change will become flexible and easy to change, while the rest of the design stays simple and firm.</p>
<p>Another way this phenomenon emerges is through the &quot;evolutionary design&quot; strategy. By building your design in small increments, keeping it as simple as you can, and reflecting and improving on the design with each and every change, you can manage to reduce the cost of change as you expand your system.</p>
<p>When James Shore describes this design strategy in <em>The Art of Agile Development</em>, he gives the example of a JavaScript project he did for one of his screencasts. As he added more and more features related to networking, each feature took less and less time to implement - from 12 hours, to 6 hours, to 3 hours, to under an hour - despite the later features being no less intricate than the earlier ones!</p>
<p>By improving the design with each and every change, the design does not merely stay out of our way. The design <em>actively supports and enables new changes</em>. By keeping the design clean while extending it with more functionality, we can make the system do more and more things with less effort. The codebase becomes a precious asset that accelerates our development, speeding us up instead of slowing us down.</p>
<p>These ways of reducing the cost of change are ill described as &quot;paying down technical debt.&quot; Instead, it is more accurate to say we are <strong><em>building technical wealth</em></strong>.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>I am a social animal</title>
      <link>https://www.cvennevik.no/blog/i-am-a-social-animal/</link>
      <pubDate>Sat, 04 Feb 2023 07:42:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/i-am-a-social-animal/</guid>
      <content:encoded><![CDATA[<p>I do not thrive alone at the computer. I do not thrive in peace and quiet. I do not thrive with a well-defined problem in front of me, with my headphones on, and a whole day ahead of me before I have to interact with another human.</p>
<p>I cope with it.</p>
<p>I muster the energy I have for the day. I devise elaborate techniques to focus myself. I work pomodoros. I meditate. I put on white noise. I journal. I break the work down into little digestible steps.</p>
<p>And so I manage being alone.</p>
<p>Yet sometimes, all the efforts in the world are not enough. Sometimes, I require that precious fruit: Another human's attention.</p>
<p>I carefully leave the confines of my desk, and I slink around the office to see what I can scavenge. Could there be a quick little question somewhere? A second opinion? A clarification? Oh, lucky day, a full discussion!</p>
<p>On this morsel, I return to my seat-burrow and savor my prize. My back straightens. My mind clears. I have been sated enough to function, for now, until I next need to go foraging.</p>
<p><em>How I long for the revitalizing presence of another human's company.</em></p>
<p>Some days these little morsels are not enough. Some days, I am starved.</p>
<p>My eyes glaze over the monitor. My arms are heavy. My body cannot sustain itself on table scraps of human interaction. It howls for <strong><em>collaboration</em></strong>.</p>
<p>When I get up from my seat, and look around, I see other humans. Alone at their computer. In peace and quiet. Each with their own well-defined problem in front of them, with their headphones on, with no need to talk to another human.</p>
<p>The air in the office is still. And I would do well not to disturb it.</p>
<p>I sit back down in my seat. I look at the time.</p>
<p>It is winter, yet.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Enforcing advanced type constraints with class constructors in TypeScript</title>
      <link>https://www.cvennevik.no/blog/type-constraints-with-class-constructors/</link>
      <pubDate>Fri, 20 Jan 2023 06:37:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/type-constraints-with-class-constructors/</guid>
      <content:encoded><![CDATA[<p>Frequently, when programming, I am working with data that I expect to follow a set of constraints.</p>
<ul>
<li>This parameter must be a string.</li>
<li>This return value cannot be null.</li>
<li>This object must have a username property.</li>
</ul>
<p>Constraints like these are typically common and easy to express in statically typed languages.</p>
<p>Sometimes (actually a lot of times) I am working with data that should follow stricter, more complicated constraints.</p>
<ul>
<li>This user-submitted application cannot have the <code>approvedTime</code> value set.</li>
<li>This username must be non-empty and cannot have special characters.</li>
<li>This <code>from</code> value must be before the <code>to</code> value.</li>
</ul>
<p>These constraints can be more tricky to express in a type definition, and I rarely see it attempted. Instead, I see functions either assume the data is valid, or run validation checks on the data that throw an error if it breaks a rule.</p>
<p>Following the advice of <em>&quot;parse, don't validate&quot;</em> (see <a href="https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/">the wonderful post by Alexis King</a>), I would rather that the type itself is able to enforce its own constraints. This guarantees and preserves the validity of the data as you pass it on to other functions, and reduces the risk of insufficient <em>and</em> redundant validation checks around your codebase.</p>
<p>With a few tricks, most constraints you can imagine can be enforced with a type definition. In this case, I'll be showing how to use <strong>classes</strong> to guarantee constraints on TypeScript data that simple <code>type</code> and <code>interface</code> declarations are unable to. (This technique also works in any statically typed language that supports classes.)</p>
<h2>The technique</h2>
<ol>
<li>Define a class containing the values you want to wrap.</li>
<li>In the constructor, check for any constraints you are interested in.</li>
<li>Throw an error if any of the constructor checks fail.</li>
</ol>
<p>Key to this technique being widely applicable is that <strong>you are allowed to write class wrappers for single values.</strong> You incur some overhead for having to instantiate each value as a class, and having to access the instance's value to use it, but in return you can enforce any constraint you can imagine in its constructor. Make this tradeoff at your own discretion.</p>
<p>To illustrate the technique, I've spun up a few examples showing what you can do with it.</p>
<h2>A palindrome type</h2>
<pre><code class="language-ts">class Palindrome {
    readonly value: string;

    constructor (value: string) {
        const reversedValue = value.split('').reverse().join('');
        if (value !== reversedValue) {
            throw new Error(`&quot;${value}&quot; is not a palindrome`);
        }

        this.value = value;
    }
}
</code></pre>
<p>To ensure the string is a palindrome, we reverse it and check if the reversed string is equal to the original string. If not, we throw an error. Then we save the string to the <code>value</code> field.</p>
<p>In practice, usage looks like this:</p>
<pre><code class="language-ts">const palindrome = new Palindrome('())(');
console.log(palindrome); // Output: Palindrome { value: '())(' }
console.log(palindrome.value); // Output: '())('

const invalidPalindrome = new Palindrome('(())');
// Error: &quot;(())&quot; is not a palindrome
</code></pre>
<p>The <code>Palindrome</code> class guarantees that every instance of <code>Palindrome</code> contains a string that has passed the constructor validation. If you have any functions that <em>must</em> have a palindrome, the <code>Palindrome</code> type is an effective way to enforce it.</p>
<p>If you would rather not throw an error, but check if the string is a palindrome and handle the invalid case another way, you can create a parse method that wraps the palindrome creation in a <code>try</code> block, and return <code>undefined</code> if it fails:</p>
<pre><code class="language-ts">class Palindrome {
    // ...

    static parse (value: string): Palindrome | undefined {
        try {
            return new Palindrome(value);
        } catch (error) {
            return error;
        }
    }
}

console.log(Palindrome.parse('())(')); // Output: Palindrome { value: '())(' }
console.log(Palindrome.parse('(())')); // Output: undefined
</code></pre>
<h2>A sorted array</h2>
<p>A constructor does not have to throw errors to ensure a constraint. It can also do the work to transform data to a desired form, then pin it in place.</p>
<p>For instance, you can create a <code>SortedArray</code> class that sorts your array for you:</p>
<pre><code class="language-ts">class SortedArray&lt;T&gt; {
    // Mark as ReadonlyArray to ensure contents stay sorted
    readonly contents: ReadonlyArray&lt;T&gt;;

    constructor (contents: T[]) {
        // Copy the array so we do not reorder the original array,
        // and prevent changes to the original array from affecting our sorted array
        const copy = [...contents];
        copy.sort();
        this.contents = copy;
    }
}

const sortedArray = new SortedArray([0, 5, 3, 4, 4, 2]);
console.log(sortedArray.contents); // Output: [ 0, 2, 3, 4, 4, 5 ]
</code></pre>
<p>This may be useful if you are working with algorithms that expect a sorted array, like search.</p>
<h2>A range with an estimate</h2>
<p>Classes can, of course, also enforce constraints for multiple values. While I was experimenting with a game-playing traditional AI, my search algorithm used an <code>EstimateRange</code> data type to describe the minimum, estimate, and maximum value of a given game state. To make sense, the minimum cannot be greater than the maximum, and the estimate must be between them.</p>
<p>Here is how this can be enforced in TypeScript:</p>
<pre><code class="language-ts">class EstimateRange {
    readonly minimum: number;
    readonly estimate: number;
    readonly maximum: number;

    constructor (minimum: number, estimate: number, maximum: number) {
        if (minimum &gt; maximum) {
            throw new Error(`Minimum (${minimum}) is greater than maximum (${maximum})`);
        } else if (estimate &gt; maximum) {
            throw new Error(`Estimate (${estimate}) is greater than maximum (${maximum})`);
        } else if (estimate &lt; minimum) {
            throw new Error(`Estimate (${estimate}) is less than minimum (${minimum})`);
        }

        this.minimum = minimum;
        this.estimate = estimate;
        this.maximum = maximum;
    }
}

console.log(new EstimateRange(0, 7, 10));
// Output: EstimateRange { minimum: 0, estimate: 7, maximum: 10 }

console.log(new EstimateRange(10, 7, 0));
// Error: Minimum (10) is greater than maximum (0)
</code></pre>
<p>The point I am trying to make with the variety of examples is that there is a lot you can do with classes. The constraints you can enforce are mostly limited by your imagination.</p>
<h2>A warning</h2>
<p>This trick comes with a caveat: <strong>If possible, you are better off enforcing constraints with simpler alternatives.</strong> I prefer using more concise type system features when I can. For TypeScript, you can browse the <a href="https://www.typescriptlang.org/docs/handbook/2/everyday-types.html">Everyday Types</a>, <a href="https://www.typescriptlang.org/docs/handbook/2/objects.html">Object Types</a> and <a href="https://www.typescriptlang.org/docs/handbook/2/types-from-types.html">Creating Types from Types</a> pages of the TypeScript handbook for inspiration.</p>
<p>When simpler alternatives for type checks are insufficient, class constructor validation is a powerful alternative to fall back on.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Using TypeScript to prevent common mistakes</title>
      <link>https://www.cvennevik.no/blog/using-typescript-to-prevent-common-mistakes/</link>
      <pubDate>Tue, 17 Jan 2023 00:43:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/using-typescript-to-prevent-common-mistakes/</guid>
      <content:encoded><![CDATA[<p>I have been struggling to write an article about type systems for about a week now. The ideas and angles I want to take kept changing between each writing session, preventing me from ever completing a single coherent article.</p>
<p>After venting about this on Mastodon, another user asked me what I thought about static types. They were mostly experienced with dynamically typed languages, and prefer the flexibility they offer.</p>
<p>It turns out this question was all I needed to focus and get my writing back on track - you may take this blog post as my longform answer.</p>
<p>In short: I think static types are a useful tool to prevent us from making very common mistakes. Like, really common. Like &quot;half of the bugs I investigate in JavaScript applications are caused by this&quot; common.</p>
<p>Here's a few examples to illustrate the types of mistakes I'm talking about, and how adding static types with TypeScript helps prevent them.</p>
<h2>Mistake #1: Accessing undefined property names</h2>
<p>Say we are working with an object containing user profile data, and we want to send an email to that user. To do this, we access the user's email address via <code>user.emailAddress</code> and pass it to <code>sendEmail</code>.</p>
<pre><code class="language-ts">function sendEmailToUser (user) {
    sendEmail(user.emailAddress);
}
</code></pre>
<p>But what if we are mistaken? What if the <code>user</code> object's property is actually named <code>emailaddress</code>, or <code>address</code>, or <code>username</code>, or - gasp - it does not actually have a property for email address at all? Well, then this code will instead attempt to send an email to <code>undefined</code>. That's no good.</p>
<p>To check for this potential issue, let us say we found the <code>UserProfile</code> type that describes the user data we expect, and specify that <code>user</code> is of type <code>UserProfile</code>.</p>
<pre><code class="language-ts">interface UserProfile {
    // ...
    contactInfo: {
        // ...
        emailAddress: string
    }
}

function sendEmailToUser (user: UserProfile) {
    sendEmail(user.emailAddress); // Causes a build error!
}
</code></pre>
<p>Oops! It turns out the <code>user.emailAddress</code> property does not exist. Now, because we are trying to access a property that does not exist on <code>UserProfile</code>, the TypeScript compiler produces an error.</p>
<p>The <code>UserProfile</code> type instead tells us that a <code>user.contactInfo.emailAddress</code> property exists. This is likely what we actually wanted to use, and replacing <code>user.emailAddress</code> with this will make the error go away.</p>
<h2>Mistake #2: Passing invalid data</h2>
<p>Say we are working with a map, and want to place an icon at the spot where the user's mouse pointer is:</p>
<pre><code class="language-ts">function getMousePosition () {
    // ...
}

function setIconPosition (position) {
    // ...
}

function placeIconAtMousePosition () {
    const mousePosition = getMousePosition();
    setIconPosition(mousePosition);
}
</code></pre>
<p>What can go wrong here? Well, we do not know the structure of the data <code>getMousePosition</code> returns, nor what <code>setIconPosition</code> accepts. Even if we already know we are working with longitude and latitude positions in the same coordinate system, the position could be represented as a <code>{ lon, lat }</code> object, or an <code>{ x, y }</code> object, or a <code>[lon, lat]</code> array (or even a <code>[lat, lon]</code> array!).</p>
<p>Without type annotations, this code looks perfectly valid, even if the data structures may be incompatible. Now, if we add the correct types to the functions using TypeScript, the incompatibility comes to light:</p>
<pre><code class="language-ts">function getMousePosition (): { lon: number, lat: number } {
    // ...
}

function setIconPosition (position: { x: number, y: number }) {
    // ...
}

function placeIconAtMousePosition () {
    const mousePosition = getMousePosition();
    setIconPosition(mousePosition); // Causes a build error!
}
</code></pre>
<p>With the function types specified, TypeScript reports that we made a mistake passing <code>mousePosition</code> directly into <code>setIconPosition</code>. Instead, we should convert the <code>{ lon, lat }</code> object to an <code>{ x, y }</code> object.</p>
<pre><code class="language-ts">function placeIconAtMousePosition () {
    const mousePosition = getMousePosition();
    setIconPosition({
        x: mousePosition.lon,
        y: mousePosition.lat
    });
}
</code></pre>
<h2>Mistake #3: Not handling undefined values</h2>
<p>The third common mistake TypeScript can prevent is the famed <em>&quot;billion dollar mistake&quot;</em>: <s>null</s> undefined values!</p>
<p>Let's reuse the map position code example and see what happens when we modify it. Say we change the implementation of <code>getMousePosition</code> so it returns <code>undefined</code> when the mouse is outside the map. TypeScript will not permit this since this does not match the return type of <code>getMousePosition</code>, so we change the return type so it can also be <code>undefined</code>:</p>
<pre><code class="language-ts">function getMousePosition (): { lon: number, lat: number } | undefined {
    // ...
}

function setIconPosition (position) {
    // ...
}

function placeIconAtMousePosition () {
    const mousePosition = getMousePosition();
    setIconPosition({
        x: mousePosition.lon, // Causes a build error!
        y: mousePosition.lat
    });
}
</code></pre>
<p>Oh no! This change actually breaks <code>placeIconAtMousePosition</code>, because it was written with the assumption that <code>getMousePosition</code> always returns a valid position. When it instead returns <code>undefined</code>, the code will throw a runtime error when trying to access <code>mousePosition.lon</code>.</p>
<p>If we were working with untyped JavaScript, this mistake may have managed to sneak in. Luckily, TypeScript caught this error for us. It refuses to compile until we have correctly handled the case where <code>mousePosition</code> is undefined. If we add a check for it, the error disappears:</p>
<pre><code class="language-ts">function placeIconAtMousePosition() {
    const mousePosition = getMousePosition();
    if (mousePosition === undefined) return;
    setIconPosition({
        x: mousePosition.lon,
        y: mousePosition.lat
    });
}
</code></pre>
<p>I like to set up development environments so it is easy to write correct code and hard to write incorrect code. Static types are only one of several tools I use for this, but they are one of my favorites. They tell us what we can and cannot do with our data, and they are effective for catching very common mistakes. Because of this, I find the overhead of opting into static typing with TypeScript well worth it.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Code reviews are overloaded</title>
      <link>https://www.cvennevik.no/blog/code-reviews-are-overloaded/</link>
      <pubDate>Sat, 07 Jan 2023 22:55:00 GMT</pubDate>
      <guid>https://www.cvennevik.no/blog/code-reviews-are-overloaded/</guid>
      <content:encoded><![CDATA[<h2>Code reviews are effective</h2>
<p><strong>Code reviews are effective for uncovering bugs.</strong> We have multiple large studies backing this claim, estimating that the bug-detection rate of code reviews is in the ballpark of 50%. This is better evidence than we have for most software development practices. From <a href="https://en.wikipedia.org/wiki/Code_review#Efficiency_and_effectiveness_of_reviews">the Wikipedia article on code review</a>:</p>
<blockquote>
<p>Capers Jones' ongoing analysis of over 12,000 software development projects showed that the latent defect discovery rate of formal inspection is in the 60-65% range. For informal inspection, the figure is less than 50%. The latent defect discovery rate for most forms of testing is about 30%. A code review case study published in the book Best Kept Secrets of Peer Code Review found that lightweight reviews can uncover as many bugs as formal reviews, but were faster and more cost-effective in contradiction to the study done by Capers Jones.</p>
</blockquote>
<p>In addition to their primary value in discovering bugs, they can also be used to assess and improve many other aspects of the code. Design, readability, maintainability, code quality, test quality and coverage, consistency with project style guidelines and documentation are all areas where reviewers can find issues and suggest improvements. The act of reviewing and giving feedback can even help transfer knowledge between developers and be a tool for mentoring and learning.</p>
<p>On account of these benefits, code reviews have become wildly popular, and most software projects mandate that all changes must be approved by one or more reviewers. This virtually always means a <em>pull request based workflow</em>, where developers branch out from mainline, make some changes, then open a pull request that requires approval from a reviewer to merge back into mainline. At time of writing, this is the predominant way of working in our industry.</p>
<h2>Branching causes problems</h2>
<p>In projects using pull requests, the most popular strategy for branching is <em>feature branching</em>, creating a branch for a single feature and opening a pull request when the feature is complete. This is a convenient and intuitive way of organizing changes. However, these feature branches tend to be long-lived (on the order of days or weeks), and long-lived branches cause some serious issues.</p>
<p>In part four of Thierry de Pauw's article series <a href="https://thinkinglabs.io/articles/2021/04/26/on-the-evilness-of-feature-branching.html">On the Evilness of Feature Branching</a>, he goes into <a href="https://thinkinglabs.io/articles/2022/05/30/on-the-evilness-of-feature-branching-the-problems.html">the problems</a> of feature branching. The article is worth reading in full, but to summarize some of its points:</p>
<ul>
<li><strong>It delays feedback</strong> on how well changes integrate with other team members' work and how it runs in production.</li>
<li><strong>It causes rework</strong> through merge conflicts.</li>
<li><strong>It discourages refactoring</strong> as they have a high risk of causing merge conflicts.</li>
<li><strong>It introduces batch work and inventory</strong>, trapping valuable work in the system and worsening the throughput, quality, stability and lead time of changes.</li>
<li><strong>It increases risks</strong> by batching changes into large sets which are more likely to break, and harder to find the cause of when they do.</li>
</ul>
<p>Key to these issues is that they are more frequent and more severe the longer the branches live and the larger the changes are. Conversely, shorter-lived branches and smaller changes cause less issues. We can nearly eliminate the branching issues by pushing this all the way to <a href="https://martinfowler.com/articles/branching-patterns.html#continuous-integration">continuous integration</a>, where everyone's work is merged into mainline every day, potentially even multiple times an hour.</p>
<p>Code reviews make this infeasible for most software teams.</p>
<h2>Mandatory code reviews encourage long-lived branches</h2>
<p>When merging your work requires another developer to review and approve it, merging <em>will</em> happen less frequently, and pull requests <em>will not</em> shrink beyond a certain size. Dragan Stepanović explains this best in his article <a href="https://www.infoq.com/articles/co-creation-patterns-software-development/">From Async Code Reviews to Co-Creation Patterns</a>.</p>
<p>In short, code reviews introduce long wait times to the integration process. First, after a pull request is opened, the author waits for a review. Then, if the reviewer discovers any issues they think the author should handle, the reviewer waits for the author to respond to the feedback. This cycle repeats some number of times until the reviewer is satisfied and approves the pull request.</p>
<p>These wait times encourage developers to start new work in the meanwhile (increasing work-in-progress), and to batch their changes into larger pull requests. This, again, makes each review take longer, making it harder for developers to find time to review them, and increasing the odds of multiple rounds of review - increasing wait times even more! This vicious cycle results in pull requests often taking multiple days before they are able to be merged.</p>
<p>If a team still tries to make a push for continuous integration in this environment, they are fighting against the stream. The more frequently team members try to integrate, the more often they have to interrupt each other to review and respond to reviews. Developer attention bounces between multiple tasks, flow efficiency (time spent <em>working</em> to time spent <em>waiting</em>) plummets, and productivity drops. Every team will hit a point where the pain of this is too high and will stop integrating their changes any more frequently - typically stopping well short of continuous integration.</p>
<h2>Code reviews are hard to replace</h2>
<p>Many developers who recognize these problems assert that this kind of code review is a net negative and should be done away with altogether. Dave Farley, co-author of <em>Continuous Delivery</em>, insists that <a href="https://www.davefarley.net/?p=247">you are better off not branching at all</a>:</p>
<blockquote>
<ul>
<li>Don't Branch!</li>
<li>Don't Branch!</li>
<li>Don't Branch!</li>
</ul>
</blockquote>
<p>Instead of doing after-the-fact code review, he and others recommend that you support the quality of your software through other practices. The top recommendations are pair programming and mob/ensemble programming, which function as a sort of continuous code review while boosting the flow of work and knowledge sharing within your team. Test-driven development and a &quot;<em><a href="https://www.jamesshore.com/v2/books/aoad2/no_bugs">No Bugs</a></em>&quot;, root-cause eliminating attitude help reduce bug rates even further. By employing these practices, you may achieve better results than relying on code reviews. And I <em>want</em> to believe this.</p>
<p>However, in teams that frequently catch serious errors in code review, <strong>this is hard to sell</strong>. Most developers do not use these alternative practices, and asking people to change the way they work and spend time practicing new skills is a big ask for most teams. Without these changes, slashing code review will in all likelihood lead to more defects being pushed to mainline and escaping to production. Software teams have very reasonable motives for not wanting to do this.</p>
<p>This leaves me conflicted. I cannot in good conscience say that most teams should drop mandatory code reviews and that this will not cause major issues. Yet, I am thoroughly convinced that continuous integration <em>is</em> a better way of working.</p>
<p>Trapped in the middle, I am here to suggest a compromise: <strong>Code reviews should be reduced to their bare essentials.</strong></p>
<h2>Code reviews hurt more the more they try to do</h2>
<p>Here is my line of reasoning:</p>
<ol>
<li>When you look for more things in a code review, it becomes more demanding and time-consuming.</li>
<li>When code reviews get harder, developers will put them off, and wait times will grow.</li>
<li>When wait times grow, branches will live longer and pull requests will get larger, feeding the cycle and causing integration pain.</li>
</ol>
<p><strong>Conclusion:</strong> <em>The more things you look for in a code review, the more you will experience integration pain.</em> Conversely, if you reduce the number of things you look for in a code review, you will be able to integrate your work more frequently. If review gets easy enough, you may even find continuous integration feasible!</p>
<p>With this in mind, it becomes clear that we have made the review process very hard for ourselves. The most common thing to do is to include <em>every possible thing</em> worth having an opinion on in the scope of review. For instance, take <a href="https://google.github.io/eng-practices/review/reviewer/looking-for.html#summary">Google's sumary of what a reviewer should look for</a>:</p>
<blockquote>
<p>In doing a code review, you should make sure that:</p>
<ul>
<li>The code is well-designed.</li>
<li>The functionality is good for the users of the code.</li>
<li>Any UI changes are sensible and look good.</li>
<li>Any parallel programming is done safely.</li>
<li>The code isn’t more complex than it needs to be.</li>
<li>The developer isn’t implementing things they might need in the future but don’t know they need now.</li>
<li>Code has appropriate unit tests.</li>
<li>Tests are well-designed.</li>
<li>The developer used clear names for everything.</li>
<li>Comments are clear and useful, and mostly explain why instead of what.</li>
<li>Code is appropriately documented (generally in g3doc).</li>
<li>The code conforms to our style guides.</li>
</ul>
</blockquote>
<p><strong>This is a lot!</strong> A lot of things to pay attention to while reviewing, a lot to write feedback on, a lot of comments for the author to respond to. Several concerns like code quality and design (and without an authoritative style guide, code style and formatting) are highly subjective, and have a higher chance of causing disagreements, discussions, and multiple rounds of review - skyrocketing wait times.</p>
<p>Not only does this bucket list of concerns make review harder, but discussions of fuzzier, less critical issues drown out discussion of bugs. Quoting <a href="https://en.wikipedia.org/wiki/Code_review#Efficiency_and_effectiveness_of_reviews">the Wikipedia article</a> again:</p>
<blockquote>
<p>Empirical studies provided evidence that up to 75% of code review defects affect software evolvability/maintainability rather than functionality [...] This also means that less than 15% of the issues discussed in code reviews are related to bugs.</p>
</blockquote>
<p>Despite our primary motivation for mandating code review being bug reduction, we spend the majority of our attention on other, less critical issues. Combining this diluted focus with its influence to make pull requests larger, it is likely that trying to improve more things with code review actually makes matters worse.</p>
<h2>Limit your code reviews to the most important concerns</h2>
<p>Since mandatory code reviews cause more issues the more concerns they look for, they should be stripped down to the bare minimum of concerns that must be improved before merge. This will reduce the costs of review while improving its effectiveness for the concerns you do look for. What this bare minimum set is may vary from project to project, and you are free to decide what this is for yourself.</p>
<p><strong>Personally, I am convinced that you should cut any concern from your list that is not externally visible and can be improved later.</strong> This includes:</p>
<ul>
<li>Formatting and code style</li>
<li>Code quality and internal design details</li>
<li>Comments and documentation</li>
<li>Test coverage and quality</li>
</ul>
<p>All of these things are (more or less) valuable, and we want to do them well. However, <strong>none of them are externally visible</strong>, meaning it does not matter to our users if we merge and deploy them to production. Additionally, <strong>all of them can be improved later</strong>, and frequent low-friction integration makes it <em>easier</em> to make such improvements. Including them in review would harm integration frequency, which makes it harder to improve the quality of our codebase when we discover these issues while working.</p>
<p>If a developer opens pull requests that aren't up to snuff on any of these points, you have a couple of alternatives instead of checking it during review:</p>
<ol>
<li><strong>Talk to the developer about it.</strong> Make sure you agree to a common set of standards, and that they have the environment and resources to learn how to fulfill them. This is a longer term solution that will save you frustration in the long run.</li>
<li><strong>Fix it yourself.</strong> If you see room for improvement beyond your team's common set of standards, do it yourself instead of asking the author to do it. It is more effective and efficient, and it contributes to your team's sense of collective code ownership - you all have the right and responsibility to make improvements when you see them. And, since this change is to an issue that should be ignored by reviewers, it should be quick and easy to approve and merge back into mainline.</li>
</ol>
<p>Conversely, changes that are externally visible, or cannot be improved later, are more worth reviewing:</p>
<ul>
<li><strong>Bugs and security issues.</strong> We should ideally never introduce any of these, and we want to minimize the chance of any of them escaping to production.</li>
<li><strong>Design decisions that are hard to undo.</strong> This includes both external API designs (which are very hard to change once they are public), user interface designs, and any changes to functionality. The cost of getting these wrong is high, so it is worth spending extra effort getting them right.</li>
</ul>
<p>By limiting your review scope to these core concerns, you minimize the cost of mandating code reviews, while maintaining quality control on the issues you care the most about.</p>
<p><em>(Limiting and focusing the objective of code review like this also makes it easier to see when code reviews become obsolete - when bug rates drop to acceptable levels pre-review, and when you discuss and refine your irreversible design decisions outside of review. It is very easy to imagine high-performing teams working like this.)</em></p>
<h2>This is a suggested experiment</h2>
<p>While this strategy of cutting the scope of review makes a lot of sense to me, I cannot predict how it will play out for you, in your team, in your circumstances. I do not know to what degree it will make reviews easier and reduce integration pain, and I do not know what unexpected side effects it will have.</p>
<p>What I do believe is that this experiment is low risk, it is not very disruptive, and the potential rewards are great enough that <strong>you should try it out</strong>. Talk it out with your team, find a scope of review to try, trial it for a week or two, then reflect on how it went. If you do not like the results, you can always go back to your old way of working afterwards.</p>
<p><strong>If you try this, or have already tried it, please message me to share your experience!</strong> Contact me on Mastodon at <a href="https://hachyderm.io/@cvennevik">@cvennevik@hachyderm.io</a> (so I can share it further, if you like!), or email me at <a href="mailto:cvennevik@gmail.com">cvennevik@gmail.com</a>.</p>
<p><em>Edit, April 14th 2023: <a href="https://www.cvennevik.no/blog/oops-i-made-code-review-painful/">I wrote a post describing my experience following this idea.</a></em></p>
]]></content:encoded>
    </item>
    
  </channel>
</rss>