<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Finn Frame]]></title><description><![CDATA[Finn Frame]]></description><link>https://finnframe.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Finn Frame</title><link>https://finnframe.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 17 Oct 2026 13:01:24 GMT</lastBuildDate><atom:link href="https://finnframe.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Write the test before the game edit]]></title><description><![CDATA[An exit that opens after three tokens sounds like a simple change. But does the third token finish the level, or does it merely unlock the way out? Can the player collect the same token twice? What ha]]></description><link>https://finnframe.hashnode.dev/write-the-test-before-the-game-edit</link><guid isPermaLink="true">https://finnframe.hashnode.dev/write-the-test-before-the-game-edit</guid><category><![CDATA[Game Development]]></category><dc:creator><![CDATA[Finn Frame]]></dc:creator><pubDate>Wed, 14 Oct 2026 10:08:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ac93c64cab50788a13878e2/b1b889c1-181f-4060-970f-db24b7ae069b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>An exit that opens after three tokens sounds like a simple change. But does the third token finish the level, or does it merely unlock the way out? Can the player collect the same token twice? What happens if they restart just before reaching the exit?</p>
<p>Those are different rules hiding inside one request. Leaving them for the editing agent to decide makes the result harder to judge: a working implementation can still be the wrong game.</p>
<p>Consider a hypothetical one-room game whose exit currently opens immediately. We want three distinct tokens to unlock it, followed by a separate action—reaching the exit—to finish. The example below is a proposed edit, not a game built or tested for this article.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ac93c64cab50788a13878e2/b1b889c1-181f-4060-970f-db24b7ae069b.png" alt="Concept illustration: an empty 0/3 counter above a closed gate becomes a full 3/3 counter above an open gate; a Reset arrow returns to the empty counter and restored tokens." style="display:block;margin:0 auto" />

<p><em>AI-generated illustration of the hypothetical rule: collect three tokens to open the gate; Reset restores the starting state. Not a screenshot or Rosebud result.</em></p>
<h2>A request with a clear finish line</h2>
<p><a href="https://rosebud.ai/tutorial">Rosebud's beginner workflow</a> describes trying the preview, requesting a focused change in chat and playing again. Before that editing step, decide what you will do in the preview to check the change. For this gate, the request could be:</p>
<blockquote>
<p>Start each run with three distinct tokens and “Tokens: 0/3”. Collecting a token removes it and increases the counter once.</p>
<p>Keep the exit locked at 0, 1 and 2. Touching it displays “Collect all 3 tokens to exit.” At 3, show “Exit ready”. Finish only when the player touches the exit, not when they collect the last token.</p>
<p>Keep the artwork, movement controls and speed, camera, audio and existing Restart control. Do not add enemies, timers, levels or online features.</p>
<p>Restart restores the original player position and all three tokens, returns the counter to 0/3, locks the exit and clears any exit or completion message.</p>
<p>Explain the planned changes before editing. Afterward, list the checks actually run and mark the rest “not run”.</p>
</blockquote>
<p>Notice that this asks for three states: locked, ready and finished. “Collect three tokens to win” would collapse the last two and remove the walk to the exit. If that walk matters to your design, it needs to survive the prompt.</p>
<p>The preservation paragraph serves a different purpose. Keeping the room and controls lets you compare the edit with the game you already know. If the agent also redesigns the level, an unexpected result becomes harder to trace.</p>
<h2>Play through the states separately</h2>
<p>Save the original build and note the browser and controls used. On the edited build, first touch the exit with no tokens, then with one and two. It should stay locked and explain what is missing.</p>
<p>Collect the third token, but <strong>stop short of the exit</strong>. This is the easy-to-miss check: the counter should read 3/3 and the gate should be ready, while the level keeps running. Only reaching the exit should finish it.</p>
<p>Next, start a fresh run and collect one token. Cross that token’s former position again; the count should remain at 1/3. That catches a different failure from unlocking too early: an object may disappear visually while its trigger remains active.</p>
<p>Try Restart at 2/3 and after completion. Check the objects as well as the counter. A display of 0/3 is misleading if one token is still missing or the exit remains open. Then compare the same short movement path with the original to check that controls, speed, camera, artwork and audio stayed as requested.</p>
<p>If a check fails, report the action and result before proposing a cause. A hypothetical report would be: “Build B, keyboard controls: crossed token A's former position after collecting it. Expected 1/3; observed 2/3. The token stayed invisible.” That is enough to target a repair without assuming which function caused it.</p>
<p>After the repair, repeat that check and the unlock and restart checks it might affect. Keep an unperformed check marked “not run”, even if the agent's explanation sounds convincing.</p>
<p>These observations settle whether the edit follows your rule. Whether someone understands the objective or enjoys the final walk to the exit still needs a playtest. The prompt gives that playtest a consistent game to start from.</p>
]]></content:encoded></item><item><title><![CDATA[Restart a browser game without carrying the old run into the new one]]></title><description><![CDATA[A restart button resets the score. Two seconds later, five points appear before the player has touched anything. One explanation: a bonus request started in the old round, finished in the new one, and]]></description><link>https://finnframe.hashnode.dev/restart-a-browser-game-without-carrying-the-old-run-into-the-new-one</link><guid isPermaLink="true">https://finnframe.hashnode.dev/restart-a-browser-game-without-carrying-the-old-run-into-the-new-one</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[Game Development]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Finn Frame]]></dc:creator><pubDate>Mon, 12 Oct 2026 11:25:34 GMT</pubDate><content:encoded><![CDATA[<p>A restart button resets the score. Two seconds later, five points appear before the player has touched anything. One explanation: a bonus request started in the old round, finished in the new one, and was still allowed to update the score.</p>
<p>Resetting a value does not revoke permission to write to it. That distinction is the heart of this bug.</p>
<p>Imagine a target game where one press earns one point and a separate bonus arrives after a delay. To expose the problem, request the bonus, restart before it arrives, then wait. The score should remain zero; one fresh target press should make it one. This is a constructed example, not an observation about a Rosebud game.</p>
<p>For an AI-assisted project, keep that sequence fixed while changing the implementation. Rosebud's <a href="https://rosebud.ai/tutorial">first-game tutorial</a> describes previewing, making one change, and playing again. Here the requested change is precise: a result from the previous round must not alter the current round.</p>
<h2>The request was valid when it started</h2>
<p>Label each round with an identity. A bonus request remembers the identity it started with. After waiting, it compares that identity with the current round. A mismatch means its result has expired, even if the result itself is correct.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ac93c64cab50788a13878e2/74e6d424-af2f-46ce-b28d-0fd9a9eaddfb.png" alt="A late five-point result from a retired run stops at an ownership-check barrier. A separate new run remains untouched at zero." style="display:block;margin:0 auto" />

<p><em>AI-generated editorial illustration: an old run's late result stops at an ownership check; the new run stays at zero. Conceptual diagram, not product UI or a runtime trace.</em></p>
<p>This complete, original experiment isolates that comparison. Run it as an <code>.mjs</code> file in Node, or paste it into a fresh JavaScript console that supports top-level <code>await</code>. <code>finishBonus(5)</code> resolves the promise manually, so no timer or network request is needed.</p>
<pre><code class="language-javascript">let currentRun = 0, score = 0;
const restart = () =&gt; {
  currentRun += 1;
  score = 0;
};
let finishBonus;
const bonus = new Promise(
  resolve =&gt; {
    finishBonus = resolve;
  }
);
async function requestBonus() {
  const owner = currentRun;
  const points = await bonus;
  if (owner !== currentRun)
    return;
  score += points;
}
const pending = requestBonus();
restart();
finishBonus(5);
await pending;
console.log(score); // 0
</code></pre>
<p>For a comparison, rerun the whole experiment in a fresh scope with the <code>restart();</code> line removed. It prints <code>5</code>: the same result is accepted when its round still exists. Removing the identity check instead makes the restarted version print <code>5</code>, reproducing the unwanted write.</p>
<p>The check must follow the wait. Before <code>await</code>, the old round is still current. After it, that may no longer be true. A real handler also needs this protection before displaying a late error or clearing a loading state in <code>finally</code>; an obsolete failure can overwrite the new round just as an obsolete success can.</p>
<h2>Cancellation and permission solve different problems</h2>
<p>The experiment discards a result. It does not stop work. JavaScript <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">promises have no general cancellation protocol</a>; cancellation, when available, belongs to the underlying operation.</p>
<p>A game therefore needs both resource cleanup and a rule for late arrivals. When retiring a run:</p>
<ul>
<li><p>Mark it inactive before releasing its resources. Also invalidate it when the round ends without a restart.</p>
</li>
<li><p>Cancel its recorded timeout handles with <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/clearTimeout"><code>clearTimeout()</code></a>.</p>
</li>
<li><p>Remove its gameplay listeners. Listeners registered with an <code>AbortSignal</code> can be removed together by aborting that signal, as <a href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener">MDN's event-listener reference</a> describes.</p>
</li>
<li><p>Give the next run fresh state and a new identity.</p>
</li>
</ul>
<p>Keep page-level controls, such as Restart itself, outside that per-round setup. Shared assets can survive too. The boundary follows ownership: repeatedly installing a new anonymous target handler can make one press score twice, while repeatedly destroying a shared texture cache wastes resources.</p>
<p>The small experiment covers only late-result ownership; it contains no input handling, timer cleanup or rendering. In an actual game, repeat the bonus/restart sequence and then try one ordinary interaction. Zero before the press and one afterward check two separate failure modes: the old result was rejected, and the new round received the input only once.</p>
]]></content:encoded></item><item><title><![CDATA[Why the last frame of your sprite animation disappears]]></title><description><![CDATA[Six frames at 100 milliseconds each should take 600 ms. A player that switches to idle as soon as it selects frame 5 cuts that short by 100 ms. The last drawing exists; its turn on screen has been rem]]></description><link>https://finnframe.hashnode.dev/why-the-last-frame-of-your-sprite-animation-disappears</link><guid isPermaLink="true">https://finnframe.hashnode.dev/why-the-last-frame-of-your-sprite-animation-disappears</guid><category><![CDATA[Game Development]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[animation]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Finn Frame]]></dc:creator><pubDate>Fri, 09 Oct 2026 20:37:52 GMT</pubDate><content:encoded><![CDATA[<p>Six frames at 100 milliseconds each should take 600 ms. A player that switches to idle as soon as it selects frame 5 cuts that short by 100 ms. The last drawing exists; its turn on screen has been removed.</p>
<p>This is one place to look when a chest snaps shut just as it opens. With zero-based indices, frame 5 starts at 500 ms and remains current until 600 ms. Selecting it and finishing it are different events.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ac93c64cab50788a13878e2/156706b4-4e45-4234-a059-4b89d1e4294d.png" alt="Six-frame spark illustration with a final-frame detail. Frame 5 starts at 500 ms and completes at 600 ms, receiving its full 100 ms interval." style="display:block;margin:0 auto" />

<p><em>AI-generated illustration of six hypothetical frames, each lasting 100 ms. The enlarged detail shows frame 5's interval from 500 to 600 ms. Not a Rosebud export or gameplay capture.</em></p>
<p>A small standalone JavaScript sampler can return both the current picture and the completion state:</p>
<pre><code class="language-javascript">function sampleClip(
  elapsedMs,
  frameCount = 6,
  frameMs = 100
) {
  const duration =
    frameCount * frameMs;
  return {
    frame: Math.min(
      frameCount - 1,
      Math.floor(
        elapsedMs / frameMs
      )
    ),
    done: elapsedMs &gt;= duration
  };
}
</code></pre>
<p>Its inputs must be finite: nonnegative elapsed milliseconds, a positive integer frame count and a positive frame duration with a finite total. After completion, the clamp keeps the index on the last frame. Replacing it with modulo would wrap the clip back to frame zero—a useful loop for a torch, but an unwanted reset for an opened chest.</p>
<h2>The clock and the imported sheet must agree</h2>
<p>The equal-duration assumption matters when you replace the artwork. Rosebud's <a href="https://rosebud.ai/sprite-animations">sprite-animation export guide</a> describes PNG artwork with JSON for frame rectangles, durations, pivot and playback behavior. Read those durations before connecting the export to a player. For unequal durations, choose the first cumulative frame-end time greater than elapsed time; completion occurs at their sum. Averaging away a long anticipation or recovery frame changes the animation.</p>
<p>The selected index chooses a source rectangle. A regular strip can use it with <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage"><code>drawImage</code></a>; a packed atlas needs its recorded rectangles and offsets. The sampler above is not an importer for either format.</p>
<p>Elapsed time also needs to survive late callbacks. Use the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame"><code>requestAnimationFrame</code> timestamp</a>, not a callback count. At 350 ms, the sample is frame 3, already 50 ms into its interval. Keep that remainder. Repeatedly throwing it away stretches the clip.</p>
<p>A jump from 400 to 650 ms can skip the last picture altogether. The sampler will report completion correctly, but it cannot make a display show pictures between callbacks. If a pose must be seen, explicitly hold it through a rendered frame rather than treating timeline arithmetic as proof that it appeared.</p>
<p>For pause-on-hide behavior, stop advancing the clip and reset the timing origin on resume; the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API">Page Visibility API</a> supplies that signal. Replay should reset elapsed time inside the existing animation loop, not create another callback chain.</p>
<h2>A completed clip can stay visible</h2>
<p>At 600 ms, a chest can remain open, a particle effect can stop drawing, or a character can switch to its idle clip. That choice belongs to the drawing or animation-state layer. It does not change the duration.</p>
<p>The <code>done</code> flag remains true on subsequent samples. A reward or transition should fire only when the previous sample was unfinished and the new one is finished. Reset that completion latch on a new playback, while keeping gameplay state separate: replaying a chest animation must not make collected loot available again. For damage at a particular time, detect crossing that time rather than waiting to draw a specific frame.</p>
<p>These boundary samples distinguish a short final hold from a bad drawing. Run them after the function above:</p>
<pre><code class="language-javascript">for (const time of [
  499, 500, 599, 600, 900
]) {
  const state =
    sampleClip(time);
  console.log(
    time,
    state.frame,
    state.done
  );
}
// 499 → 4, false;
// 500 → 5, false;
// 599 → 5, false
// 600 → 5, true;
// 900 → 5, true
</code></pre>
]]></content:encoded></item></channel></rss>