Yo Odin · Chapter 12

Chapter 12: Determinism, the final build, and shipping it

This chapter: make randomness reproducible, assemble the final page and scripts, and ship the three static files anywhere.

Where we left off: a complete, sounding game.


12.1 Determinism: one source of randomness

Destroids uses randomness everywhere: spawn positions, asteroid shapes, particle spreads. Right now it flows through Odin's default generator (rand.float32, rand.float32_range), which is seeded from system entropy. That's fine for play, but if you ever want a reproducible run, for testing, for a replay, for debugging "that one impossible wave", you want two properties:

  1. All randomness comes from one generator. It already does; every call goes through core:math/rand.
  2. It's seeded at one well-defined point, the start of a new game, rather than implicitly.

Make the seed explicit and you can pin it:

import "core:math/rand"

seed_game :: proc(seed: u64) {
	rand.reset(seed)         // every subsequent rand.* call is now reproducible
}

Call seed_game(a_fixed_number) at the top of reset_game and every run plays identically: same waves, same shapes, same sparks. For normal play you'd seed from entropy once (seed_game(u64(rand.int63()))); for a test you'd pass a constant.

Why it matters. The moment randomness has a single, explicit entry point, "random" becomes a knob you control rather than a source of Heisenbugs. This is the same discipline as the game's single command buffer and single sound-id list: one source of truth, deliberately placed.

Gotcha. Determinism only holds if nothing seeds itself from the wall clock inside the simulation. Keep time.now() out of game.odin: timing belongs in the loop (main.odin), which feeds the simulation a plain dt.

12.2 The final page

index.html is the one file that ties Odin, the renderer, and the browser's capabilities together. It holds:

  • a <div id="app"> for the canvas;
  • the app_env implementations: set_fps, play_sound/set_thrust (Chapter 11), and get_best_score/set_best_score (Chapter 10);
  • the KEYMAP whose slot order matches the Odin KEY_* constants (Chapter 7);
  • a little inline CSS and a footer (controls, FPS, a mute button);
  • the Batch.instantiate("destroids.wasm", { app_env: { … } }) call and the keydown/keyup wiring.

It's plain and unminified: open it and every line is legible. The full listing is Appendix B.

<script src="./batch.js"></script>
<script>
  Batch.instantiate("destroids.wasm", {
    app_env: {
      set_fps: (fps) => { fpsEl.textContent = "FPS: " + Math.round(fps); },
      play_sound: (id) => playSound(id),
      set_thrust: (on) => setThrust(on !== 0),
      get_best_score: () => parseInt(localStorage.getItem("destroids_best_score") || "0", 10) || 0,
      set_best_score: (score) => localStorage.setItem("destroids_best_score", String(score)),
    },
  }).then((exports) => { /* keydown/keyup -> exports.destroids_key(slot, 1|0) */ });
</script>

12.3 The final build and run scripts

build.sh compiles the game and copies the two assets: three files out, all plain:

#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
mkdir -p dist

echo "==> Compiling Odin -> WebAssembly ..."
odin build src -out:dist/destroids.wasm -target:js_wasm32 -no-entry-point -o:size

echo "==> Copying assets ..."
cp web/batch.js dist/batch.js
cp index.html   dist/index.html

ls -lh dist/
WASM_BYTES=$(wc -c < dist/destroids.wasm)
if [ "$WASM_BYTES" -gt $((5 * 1024 * 1024)) ]; then
  echo "WARNING: destroids.wasm is $((WASM_BYTES / 1024))KB, over the 5MB soft budget"
else
  echo "==> destroids.wasm: $((WASM_BYTES / 1024))KB"
fi

A few build flags worth knowing:

  • -o:size optimises for a small binary; -o:speed trades size for speed. For a game this small, -o:size gives a ~50 KB wasm and runs at full frame rate anyway.
  • Optional: -extra-linker-flags:"--gc-sections --strip-all --lto-O3" squeezes the binary further. Entirely optional: the defaults are fine.

run.sh builds the game, builds the native server (Chapter 4), and serves:

#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
bash build.sh
echo "==> Building the dev server (native) ..."
odin build server -out:serve -o:size
echo "=== Starting server ==="
./serve

Why no minifier? The production build this game grew from runs its JS and HTML through a minifier to shave a few kilobytes. We skip it on purpose: the win is small, the cost is that your shipped files become unreadable. For a book, and honestly for most small games, readable output is worth more than the bytes. If you do want it later, run any minifier over dist/batch.js and dist/index.html as a final step; nothing else changes.

12.4 Deploying

The game is three static files: index.html, batch.js, destroids.wasm. Any static host serves it: GitHub Pages, Netlify, an S3 bucket, a plain nginx, with one requirement:

Gotcha. The host must serve .wasm as application/wasm (Chapter 4). Most modern static hosts do this by default; if the game loads locally but not when deployed, a wrong wasm MIME type is the first thing to check.

Your Chapter 4 server is a development tool: single-threaded, no caching. Don't deploy it; hand the three files to a real static host and you're done. There's no backend: the whole game runs in the visitor's browser.

12.5 Where to go next

You've built a renderer, a server, and a game: the machinery transfers directly. Some directions:

  • Extend batch. Add gradients, sprites/baking, filters, measureText: each is one opcode + encoder + JS case. Appendix C walks through it, and the full Batchiness library is the reference for how far this design scales.
  • Add entities the table-driven way. New enemy types, power-ups, weapons: define the data, add a spawner, a case in the updater, a case in the drawer. The fixed-array/active-flag pattern carries you a surprisingly long way.
  • Build a different game. The loop, the boundary, the batching, the no-alloc pools, the synthesised sound: that's a complete toolkit for any small canvas game in Odin.
  • Learn the whole language. This book taught the slice Destroids needed. For everything else, read Karl Zylinski's _Understanding the Odin Programming Language_ at odinbook.com, warmly recommended.
  • Join the community. The Odin Discord and GitHub are welcoming and active.

That's the whole build: from an empty directory to a game you understand top to bottom, with nothing but Odin and the browser. Go make something with it. Yo, Odin.

Checkpoint

Randomness has a single, seedable entry point; the final page, build.sh and run.sh are in place; and you can deploy the three static files to any host that serves application/wasm.

Exercises

  1. Pin the seed to a constant and play two runs back to back: confirm the opening waves are identical. Then reseed from entropy and confirm they differ.
  2. Build once with -o:size and once with -o:speed; compare the wasm sizes and the FPS readout.
  3. Deploy dist/ to a static host of your choice and load it on your phone (you'll watch, not fly: it's a keyboard game). Confirm the wasm is served as application/wasm.

Further reading