A micropost for a microgame

Estimated reading time 20 minutes

LCOLONQ is running a gamejam where folks make a microgame. If you've never played WarioWare or NitorInc then you might not know what that is. Basically, a very very short game (10s or less most of the time) that calls out and says "VERB" and then you use your controller or similar to try to understand the thing in front of you and get it done before the time runs out. The game keeps speeding up over time, and tosses a bunch of different things at you over time and it's plenty of fun.

Our job in this jam is to make a microgame (or many microgames) and pop them over to the funny greenzone letterman to be included in a community powered game for the amusement of everyone. LCOLONQ's instructions are fairly straightforward, make a state machine that boots your game when asked and reports a win or loss. Let's jump in, as I'm thinking this, much like the game, should be a short post. 2

The harness

To reiterate the instructions, this is the interface we need to mock out while we're developing:

  1. When the game is ready: send window.parent.postMessage({op: "ready"});. Not that this implies your game is running inside of an iframe.
  2. Wait until you get the message {op: "start", difficulty: some.floatnumber} from subscribing to messages like window.addEventListener("message", ev => console.log(ev.data));
  3. Ack the message via window.parent.postMessage({op: "started", verb: "catch!"});
  4. Play the game until you lose or win and then post back window.parent.postMessage({op: "done", win: true});

The main annoyance of this is that our game exists in an iframe, which makes for a somewhat cumbersome development process unless we make our own harness. So let's do that real quick. Javascript's sole saving grace is that it is easy to get down and dirty quickly. Though iframes are pain in the ass.

As documented here, having two windows communicate with each other is strife with problems and permissions that one must get right. There use to be entire libraries dedicated to the hacky ways one would get messages passed between iframes back in the day. Nowadays things are a bit simpler. You can see this live on the site here if you want, but it will also likely morph into the actual game over the course of this blog post, so I'd encourage to just run a python -m http.server or php -S kind of guy with two html files to follow along. Besides a basic html tag, you need an iframe and to confirm comms we'll just use a button:

<iframe src="/games/micro2026/game.html" allow="*" sandbox="allow-scripts allow-same-origin"></iframe>
<button id="send_start">Send Start</button>
<p id="child-messages"><p>

You'll notice that since this is test harness and not a production ready full blown clonqpowered harness that allow is set to *. I'm lazy, I don't want to know exactly which details to sort out for permissions and this works. So away we go. The other more important thing is sandbox which must be setup to allow the same origin or else this simply won't work at all. You'll see this if you exclude it:

Failed to execute ‘postMessage’ on ‘DOMWindow’: The target origin provided (‘null’)
does not match the recipient window’s origin (‘http://localhost:35729’).

This iframe will load up the game.html file, but before we touch on that we need a bit of javascript for our parent window to pretend to be LCOLONQ's system:

<script>
    const iframe = document.querySelector("iframe");
    iframe.onload = function() {
            const start = document.getElementById("send_start");
            start.onclick = () => {
                iframe.contentWindow.postMessage({op: "start", difficulty: 42.0}, '*');
            }
    };

    const childMessages = document.getElementById('child-messages');
    window.addEventListener("message", (ev) => {
        childMessages.textContent = childMessages.textContent + JSON.stringify(ev.data) + '\n';
    });
</script>

As you can probably tell, the button is unsurprisingly wired to trigger a message down to the iframe's document. Importantly, iframes aren't actually really pointing to the doc you told it to until it's done loading. The MDN page on iframes, contentWindows, and postMessage gets into the details of this, but basically you don't want to try to do anything with an iframe unless you know it's loaded to the right origin you asked it to be. So any scripting you do against a iframe should be done after its finished loading, and so you do everything in the callback.

The child messages here are just to help demonstrate to us that our game code is working and able to talk to the harness as expected. The simplest way to do this is by good ol' printf debugging style. So, appending to the textContent of a p tag it is. Now, onto the "game" code itself. The HTML is simple since we're just establishing a handshake right now, and the javascript is small too:

<body>
    <h1>Hi</h1>
    <p id="test">
        Make me something else.
    </p>
    <script type="application/javascript">
        function hello_world(event) {
            const p = document.getElementById("test");
            p.textContent = JSON.stringify(event.data);
            event.source.postMessage({op: "done", win: true});
        }
        window.addEventListener("message", hello_world);
    </script>
</body>

As you can see, we've followed the game jam instructions to add a window message listener, and within that handler we can see the data coming in to us. In this case, when we press the button on the harness it will send over the start op and a difficulty number. Rather than show [Object object], our hello_world function stringifys this and stuffs it into the p tag. The important thing here is the way we respond.

You can follow the gamejam's instruction to use window.parent but as noted by the MDN page you can also conveniently just reply to the event's source too as an easy handle. While I'm unsure if this would pose any problems from a potential harness that is loading multiple iframes up to show many different games to a player, I'm going to assume that LCOLONQ's harness will be smart enough to not broadcast messages to every game at once and so this will be safe.

Pressing the button shows that this handy dandy test harness is working:

And so with the messages flowing, we can improve the harness to be able to send the different messages noted by the instructions. Specifically, when our game gets its ducks in a row, it's supposed to tell the harness that it's ready to be called on. That's a simple enough affair, and it make life easy, let's just wrap that up in a function:

function game_ready() {
    window.parent.postMessage({op: "ready"});
}

While MDN does like the event.source method, if we just rely on the parent like this it makes the interface a little less verbose since I don't have to have a signature like ack_start_event(target) or similar where we'd basically always pass down event.source or window.parent anyway. When we've been waiting for a while and its our turn, we're supposed to respond to that start event with a simple acknowledgement:

function ack_start_event() {
    window.parent.postMessage({op: "started", verb: "catch!"});
}

I like the idea of having a simple helper to check if the event is the right shape, it doesn't really buy us much since really we'd just be doing this in the message handler, but eh, I'm channeling my inner Martin Fowler right now I guess:

function is_game_start_event(event) {
    return event?.op === 'start' && event?.difficulty > -1;
}

The last part of the protocol is the win or lose one. Which we just hardcoded to always say win. Our helper can be more flexible:

function send_game_done(player_won) {
    window.parent.postMessage({op: "done", win: !!player_won});
}

And then, putting it all together, we can bind the above method to a couple new buttons:

<h1>Hi</h1>
<p id="test">
    Make me something else.
</p>
<button onclick="send_game_done(true)">WIN</button>
<button onclick="send_game_done(false)">LOSE</button>

And trigger the ready on load:

function message_handler(event) {
    const p = document.getElementById("test");
    p.textContent = JSON.stringify(event.data);

    if (is_game_start_event(event.data)) {
        ack_start_event();
    }
}

window.addEventListener("message", message_handler);
window.onload = function() {
    game_ready();
};

And just like that, we've now got the whole protocol defined and ready to go for me to load the page, press start, then hit the two child buttons:

With that, we're ready to begin the game development proper.

What's our game?

Well, I lied about "catch" being part of the protocol for the acknowledgement. The verb we're sending up in our ack_start_event method is actually what's going to be displayed to the user warioware style as their instruction. So we need to tweak that, as I'm got two basic ideas for what our micro game will be.

The first is just a simple play on my usual gimmick whenever I go over into the "Green zone". 2 I normally announce my presence to the world by saying something along the lines of "the prophecy has been fulfilled". This, making sense to those on twitch because my name there is "peetseater", which is my inside joke about how often I eat pizza. Since LCOLONQ happens to stream on the days of the week where I typically pick up a pizza from dominos for dinner, I get to be true to my handle and eat pizza.

Now, "Fulfill" or "Herald" may not be good instructions, at the very least, having a pizza themed game (or games), is a great first start and a good follow through on my usual gimmick I think. So, for our first verb, we'll just have something like "FEED" or "DELIVER" and that sort of thing because I want to make a stupidly simple game to start. But we're getting slightly ahead of ourselves, following the instructions of jam, we need to get something better than an HTML button working:

Your game should be scaled to fill the entire browser window. We recommend that games render at 240x160, but this is not a hard requirement. Feel free to break/bend this rule if you like!

So let's make a canvas in our game page. Given that the expectation is to fill the game window, let's remove out buttons as well, they'll only clutter things up:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Microgame 2026</title>
    <style>
        html, body, canvas {
            margin: 0;
            padding: 0;
            text-align: center;
        }
        canvas {
            margin: 1px;
            background-color: darkcyan;
            min-width: 240px;
            min-height: 160px;
            width: 97%;
            height: 97%;
        }
    </style>
</head>
<body>
    <canvas id="game" width="240" height="160"></canvas>
    <script type="application/javascript">
        const canvas = document.getElementById("game");
        const ctx = canvas.getContext("2d");
        ctx.save();
        
        const offsetLeft = canvas.offsetLeft;
        const offsetTop = canvas.offsetTop;
    </script>
</body>

In case you're wondering why there's style here, it's because looking at the test harness, if I shove a 100% in for the height and width we get an annoying scroll bar in the iframe, but if I limit myself down a bit to a few bits shy of the window, it renders a little nicer. I'm not sure if LCOLONQ will be removing the overflow properties in his actual metagame player or not, but while I work I don't want to deal with the scrollbar, so slight styling ahoy.

Now, I've made a game with canvas before (dev blog here) so I've got some code we can pull from to get basic mouse controls going. Let's start by re-creating the buttons to trigger game win and lose with a simple click. First up, some simple state tracking in our scripts top level will make life easier:

const CLICK_STATE = {
    UP: 0,
    DOWN: 1
};
const cursor = {
    x: 0,
    y: 0,
    click: CLICK_STATE.UP,
    isUp: () => {
        return cursor.click === CLICK_STATE.UP;
    },
    isDown: () => {
        return cursor.click === CLICK_STATE.DOWN;
    }
};

Then we deal with all the potential ways that one can move and click a mouse.

addEventListener("mousemove", (e) => {
    cursor.x = e.clientX - offsetLeft;
    cursor.y = e.clientY - offsetTop;
});

addEventListener("touchmove", (e) => {
    e.preventDefault();
    let rect = canvas.getBoundingClientRect();
    cursor.x = e.touches[0].clientX - rect.left;
    cursor.y = e.touches[0].clientY - rect.top;
    },
    { passive: false },
);

addEventListener("touchstart", (e) => {
    cursor.click = CLICK_STATE.DOWN;  
    let rect = canvas.getBoundingClientRect();
    cursor.x = e.touches[0].clientX - rect.left;
    cursor.y = e.touches[0].clientY - rect.top;
});
addEventListener("mousedown", (e) => {
    cursor.click = CLICK_STATE.DOWN;
});

addEventListener("touchend", (e) => {
    cursor.click = CLICK_STATE.UP;  
});
addEventListener("mouseup", (e) => {
    cursor.click = CLICK_STATE.UP;
});

I'm not really expecting someone to play microgames on their phone where one needs to think about touch events and that sort of thing, but it doesn't hurt I think. And I already have the code from a previous example, so we can use it as is and be happy. That said, having click handles doesn't mean anything to us if we don't have an actual game loop going on. So, let's kick that off when the start message comes in

function game_loop(timestamp) {
    if (!ctx) {
        console.log('No canvas found?')
        send_game_done(true);
        return;
    }
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    if (cursor.isDown()) {
        if (cursor.x < 120) {
            send_game_done(true);
        } else {
            send_game_done(false);
        }
    }
    requestAnimationFrame(game_loop);
}

function message_handler(event) {
    if (is_game_start_event(event.data)) {
        game_loop();
        ack_start_event();
    }
}

You can see we have the worlds silliest conditions here for winning and losing. But importantly, it lets us find out that our code is properly doing nothing until the send button is clicked:

My flailing around is me spamming the click button by the way. Then once we trigger the send, the click works and tada! Messages. Great, so that's all working again and we can now decide what our game is. This is going to be super simple. "Deliver" a pizza to the outstretched hand that will appear. Let's make a simple sprite:

As you can see, we've got a simple house at night. The door opens, our favorite idol appears, the she wonders where her food is. The goal is simple. Give her the pizza before she slams the door and files a complaint with your employer. There's polish I can think of already that we could do, make a car pull up at the start, then kick off a timer or similar, if you fail make miku get mad and blow up the house or something. You know, the usual things a virtual idol does in her spare time.

Anyway, let's get this sprite loaded into the canvas. This isn't that hard to do thankfully. First, load up the image like it's regular HTML:

<img id="MikuWantsPizza" src="MikuWantsPizza1.png" style="display: none;"/>

Well, almost regular HTML. Unlike if you wanted to show an image on a web page, we're going to hide this. Reason being that the canvas is the only element we want shown on the page to the user. But, with an img element on the page, even not displayed, we can still get at its data:

const miku_sprite = document.getElementById("MikuWantsPizza");

/// then inside of game loop...
ctx.clearRect(0, 0, canvas.width, canvas.height);
let sx = 0;
ctx.drawImage(miku_sprite, sx, 0, 240, 160, 0, 0, 240, 160);

As you can see, we can easily tell the canvas to draw the sprite for us with drawImage.

And we can change which frame of the sprite by shifting sx over. At this point, I think the "game" is fully formed in my head. We're going to keep it very very simple. This is a timing game, you must give Miku her pizza when she's at the door and before she gets mad at you. In the simplest form, that means that there are certain frames that you can trigger the delivery that win and some that fail. So, we don't really need most of the complicated mouse tracking code. I just care about a single click, and we can use the click handler for that on the canvas itself.

canvas.addEventListener('click', () => {
    game_state.delivery_attempt = true;
});

But what is this game_state ? Simple. The frame we're on, the frame's that win, and if we've tried to deliver the pizza or not yet:

const game_state = {
    game_step: 0,
    max_steps: 4,
    time_between_steps_ms: 2000,
    delivery_attempt: false,
    ok_to_give: [2, 3],
};

Since the game may run multiple times, we need a way to reset it and that's easy enough:

function reset_game_state() {
    game_state.game_step = 0;
    game_state.delivery_attempt = false;
}

We could probably tweak the time between steps based on the difficulty input, but I'm not 100% sure what that number is going to be so let's leave that alone for now. Our game loop is really really simple to get the frames animating with our game state, and we can even handle the win and fail too:

let start;
function game_loop(timestamp) {
    if (!ctx) {
        console.log('No canvas found?');
        // you win, i'm not going to take a life from you
        send_game_done(true);
        return;
    }

    if (start === undefined) {
        start = timestamp;
    }
    const elapsed = timestamp - start;

    ctx.clearRect(0, 0, canvas.width, canvas.height);
    let idx = game_state.game_step;
    if (elapsed > game_state.time_between_steps_ms) {
        game_state.game_step += 1;
        start = timestamp;

        if (game_state.game_step >= game_state.max_steps) {
            game_state.game_step = game_state.max_steps - 3;
            send_game_done(false);
        }
    }
    ctx.drawImage(miku_sprite, idx * 240, 0, 240, 160, 0, 0, 240, 160);

    if (game_state.delivery_attempt) {
        if (game_state.ok_to_give.includes(game_state.game_step)) {
            send_game_done(true);
        } else {
            send_game_done(false);
        }
    }
    requestAnimationFrame(game_loop);
}

And you can see this working, though it's not very thrilling yet:

But it's pretty easy to control things like this. To have a win animation we just need to add some more frames and skip to those, and same for the fail animation. Alternatively I could load in a whole new image, but eh, why over-complicate things, we can just use lists of frames to play instead and have one asset to load and call it a day. In fact, let's uncomplicate things as much as possible:

const game_state = {
    game_step: 0,
    game_steps: [0, 1, 2, 3],
    time_between_steps_ms: 2000,
    delivery_attempt: false,
    pizza_delivered: false,
    play_ending: false,
    ok_to_give: [2, 3],
    win_frames: [4,5,6,7,8],
    time_between_ending_frames_ms: 300,
    fail_frames: [9, 10, 11, 12, 13, 14, 15],
    state: 'game',
};

function reset_game_state() {
    game_state.state = 'game';
    game_state.game_step = 0;
    game_state.delivery_attempt = false;
    game_state.pizza_delivered = false;
    game_state.play_ending = false;
    game_state.time_between_steps_ms = 2000;
}

canvas.addEventListener('click', () => {
    game_state.delivery_attempt = true;
});

With the state field, I can now easily separate out the game should be doing at each moment in its lifetime. Given that there might be a few moments between saying the game is done versus when the harness is transitioning between games, let's hold onto the last frame in a variable as that will make the code a little more readable:

const last_frame_win = game_state.win_frames[game_state.win_frames.length - 1];
const last_frame_fail = game_state.fail_frames[game_state.fail_frames.length - 1];
const last_frame_game = game_state.game_steps[game_state.game_steps.length - 1];

Then the top of the game loop is easy:

if (start === undefined) {
    start = timestamp;
}
const elapsed = timestamp - start;

let idx = game_state.game_step;
if (elapsed > game_state.time_between_steps_ms) {
    game_state.game_step += 1;
    start = timestamp;
}

switch (game_state.state) {

and each case can be viewed separately:

case 'game':
    if (last_frame_game < game_state.game_step) {
        // they failed due to time out
        game_state.state = 'failed';
        game_state.game_step = game_state.fail_frames[0];
        game_state.pizza_delivered = false;
        game_state.time_between_steps_ms = game_state.time_between_ending_frames_ms;
    }

    if (game_state.delivery_attempt) {
        game_state.time_between_steps_ms = game_state.time_between_ending_frames_ms;
        if (game_state.ok_to_give.includes(game_state.game_step)) {
            game_state.pizza_delivered = true;
            game_state.state = 'victory';
            game_state.game_step = game_state.win_frames[0];
        } else {
            game_state.state = 'failed';
            game_state.game_step = game_state.fail_frames[0];
        }
    }
    break;

the "main game loop" (all 4 frames of it) really just cases about finding out if the user has clicked while Meeks was waiting for her pizza or if the player was too early. If they wait too long or are too impatient, then we transition the game_step to failed and setup the next frame to be the first of the failure frames.

case 'failed':
    if (last_frame_fail - 1 < game_state.game_step) {
        game_state.time_between_steps_ms = 2000;
    }
    if (last_frame_fail < game_state.game_step) {
        game_state.game_step = last_frame_fail;
        send_game_done(false);
    }
    break;

The failure case is really simple. The only thing worth noting is that we hold the last frame for a little while to give it a full effect. Similar for victory:

case 'victory':
    if (last_frame_win - 1 < game_state.game_step) {
        game_state.time_between_steps_ms = 2000;
    }
    if (last_frame_win < game_state.game_step) {
        game_state.game_step = last_frame_win;
        send_game_done(true)
    }
    break; 

And by calling reset_game_state from within send_game_done as well as ack_start_event we ensure a clean slate for whenever the harness might ask us to game. Easy peasy.

Ok, and maybe one more thing for the character. We need a pizza to give to Miku. And best of all, we can just tweak one little place to have a really funny effect:

<img
    id="pizza"
    src="pizza.png"
    style="display: none; "
    />
...
const pizza_sprite = document.getElementById("pizza");
...

    case 'game':
        draw_pizza = true;

        if (last_frame_game < game_state.game_step) {
            ...
            draw_pizza = false;
        }

        if (game_state.delivery_attempt) {
            draw_pizza = false;
            ...
        }

        break;
    ...

ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(miku_sprite, idx * 240, 0, 240, 160, 0, 0, 240, 160);
if (draw_pizza) {
    ctx.drawImage(pizza_sprite, 0, 0, 700, 300, idx * 60, 130, 70, 30);
}

And this has the effect of the pizza sliding along past Miku if you screw up.

Ship it.

We could add music to this, add sound effects. Add other amusing things. And maybe we will. But honestly my goal with this quick blog post was to amuse myself for a few hours 3 and to try to get a basic micro game done before LCOLONQ streamed the rest of his testing harness work on the 29th of May so that if he wanted to try to integrate a foreign game into the work, he'd have a ready made example to go off of. So, away it goes.

Potentially if I want to I'll update it with sounds or something in the near future and release a version 2, but for now, this is all I wanted to throw together. Come and get it clonq! If you are LCOLONQ reading this then download the zip for the microgame collection here

Post 3am update

The post above was written between the hours of 11pm and 3am at night while giggling at the silly angry miku sprite. I wasn't sure about the difficulty value, but since then it looks like the technical spec for the jam has updated the the difficulty value is an integer and not a floating point. So, with that in mind, let's implement difficulty on the game.

My thought is pretty simple here. We'll divide the difficulty by 5, and then have each increment of this deduct 200ms from the total frame time. So you have to react faster. This will also let us limit it to a maximum, or well, maximum difficulty via minimum frame time of 400ms because otherwise it feels unfair. First up, let's add an integer to the game state:

const game_state = {
    ...
    difficulty: 0,
};

Then, we can tweak the reset function to handle the frame time. Right now it always sets it to 2000ms, so let's just change that:

function reset_game_state() {
    ...
    
    const difficulty_selection = Math.floor(game_state.difficulty / 5);
    switch (difficulty_selection) {
        case 0: game_state.time_between_steps_ms = 2000; break;
        case 1: game_state.time_between_steps_ms = 1800; break;
        case 2: game_state.time_between_steps_ms = 1600; break;
        case 3: game_state.time_between_steps_ms = 1400; break;
        case 4: game_state.time_between_steps_ms = 1200; break;
        case 5: game_state.time_between_steps_ms = 1000; break;
        case 6: game_state.time_between_steps_ms =  800; break;
        case 7: game_state.time_between_steps_ms =  600; break;
        default:
            game_state.time_between_steps_ms = 400;
            break;
    }
}

Now before you come swinging at my head saying, but Mr Pizza Eater, why don't you just 2000 - difficulty_selection * 200 and then Math.max(value, 400)? Because! I like how easy this is to read. Having to do math means thinking, looking at a lookup table is easy. This game jam comes at a great time for me, as I just finished up a relatively deep project with the miku miku tower game and so I'd like to shut my brain off a bit.

Anyway, coding style and taste aside, for this to work and do anything we need to set the difficulty based on the incoming message. That's easy enough, just do it before we ack the message:

function message_handler(event) {
    if (is_game_start_event(event.data)) {
        game_state.difficulty = event?.data?.difficulty || 0;
        ack_start_event();
        game_loop();
    }
}

To test this, we can update the harness with an input box like this:

<input  id="difficulty" type="number" value="0" />

Then send it along:

const iframe = document.querySelector("iframe");
const difficulty = document.getElementById('difficulty');
iframe.onload = function() {
    const start = document.getElementById("send_start");
    start.onclick = () => {
        iframe.contentWindow.postMessage({op: "start", difficulty: parseInt(difficulty.value,10)}, '*');
    }
};

As much as I dislike the loosey goosey nature of Javascript, it can't be denied that it does let you work at a high level of abstraction that lets you get stuff done relatively fast. And now, we have a sliding difficulty to enjoy:

Now, the other thing that's an open question that sort of isn't answered yet is if we can have music in this game or not. I've got that code from the previous browser game I made to pull on, so here's that simple sound loader:

class Sound {
    constructor(path) {
        this.audio = new Audio(path); 
        this.canPlay = false;
        const ref = this;
        this.audio.addEventListener("canplaythrough", (event) => {
            ref.canPlay = true;
        });
    }

    play() {
        if (this.canPlay) {
            this.audio.muted = false;
            this.audio.play();
        }
    }

    mute() {
        this.audio.muted = true;
    }
}

Then, because I want to play different sounds depending on the frame, I'll just bind them to the frame like so:

const pizza_sound = new Sound('pizza.mp3');
const pasta_sound = new Sound('pasta.mp3');
const deliverit_sound = new Sound('deliverit.mp3');
const failure_sound = new Sound('failure.mp3');
const frame_to_sound = {
    0: pizza_sound,
    1: pasta_sound,
    2: deliverit_sound,
    10: failure_sound,
    7: win_sound,
    8: win_sound,
};

In order to avoid spamming the audio every single game tick, a simple boolean can be tracked in the game_state (and reset as you'd expect in the reset function):

const game_state = {
    ...
    audio_played: false,
};

and then the game loop just plays it if there's a queue to do so:

function game_loop(timestamp) {
    ...
    if (elapsed > game_state.time_between_steps_ms) {
        game_state.game_step += 1;
        game_state.audio_played = false;
        start = timestamp;
    }
    
    if (!game_state.audio_played) {
        game_state.audio_played = true;
        const sound = frame_to_sound[game_state.game_step];
        if (sound) {
            frame_to_sound[game_state.game_step - 1]?.mute();
            sound.play();
        }
    }

    switch (game_state.state) {
        ...

This makes it really easy to play any sound effect on any given frame. Which is what we want. I suppose technically the call to mute might not always mute the playing audio, but this is fine for our purposes since the "deliver it" of Miku's voice can keep playing and overlap with the initial win or fail noise and I think that's just fine. Behold. The game with sound effects:

Given my use of the audio from Miku delivering the sauce, it's a bit hard to add background music as it will definitely conflict. We can use audio tools to make the voice a bit more prominent, but I'm not an audio engineer so I can't really trim out the background music myself very well at all. Ah well. I'll tweak and fiddle around with other sound effects (no need to note all of that here I think) and upload the result for LCOLONQ to enjoy adding to the game. Miku screams and all.

If you want a preview of the game, in the test harness I've made, then you can check it out here. If you need a test harness yourself to participate in the jam, feel free to re-use mine! Have fun everyone.

One last thing...

Whilst LCOLONQ was streaming today, questions were asked, and answers were given. It turns out that there's just one more thing we need to make sure happens here. Specifically, when a game is done, it needs to sit quietly in the background until the next start signal is send off. This shouldn't be too hard to add though, we've already got a state variable, we just need to add in an idle state of sorts:

case 'game':
    ...
case 'failed':
    ...
case 'victory':
    ...
case 'idle':
    start = undefined;
    return; 

"of sorts" because, well, really there's not much going on heres jumping out of the looping function and never calling requestAnimationFrame(game_loop) to trigger the next process. Which effectively freezes everything. Because the elapsed time we use to compute the game step is also frozen, the differential in this code gets HUUUUUUUDE:

if (start === undefined) {
    start = timestamp;
}
const elapsed = timestamp - start;

So, we just set start back to undefined so that we don't accidentally start game_step on 1 rather than 0. Simple enough, but easy to miss. Speaking of simple, to get into this idle state, we can just hook in after we send the done message to the harness:

function send_game_done(player_won) {
    window.parent.postMessage({op: "done", win: !!player_won});
    reset_game_state();
    game_state.state = 'idle';
}

And with that done, our game will now patiently wait in the background to be called on to be played again. And now? We ship it.