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:
- All randomness comes from one generator. It already does; every call goes through
core:math/rand. - 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 ofgame.odin: timing belongs in the loop (main.odin), which feeds the simulation a plaindt.
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_envimplementations:set_fps,play_sound/set_thrust(Chapter 11), andget_best_score/set_best_score(Chapter 10); - the
KEYMAPwhose slot order matches the OdinKEY_*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:sizeoptimises for a small binary;-o:speedtrades size for speed. For a game this small,-o:sizegives 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.jsanddist/index.htmlas 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
.wasmasapplication/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 + JScase. Appendix C walks through it, and the fullBatchinesslibrary 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
- 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.
- Build once with
-o:sizeand once with-o:speed; compare the wasm sizes and the FPS readout. - 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 asapplication/wasm.
Further reading
- Odin
core:math/rand: https://pkg.odin-lang.org/core/math/rand/ - Odin community: https://odin-lang.org/community/ · GitHub: https://github.com/odin-lang/Odin
- Karl Zylinski, _Understanding the Odin Programming Language_: https://odinbook.com/