Starfall

A small game for the browser: steer a ship along the bottom of the screen, catch the falling stars and dodge the rocks, which come faster and closer together the longer you last. The rules, the drawing and the shader are Refingere, compiled to WebAssembly, and drawn through WebGPU. This is the repository's examples/starfall, built with rfn --web --compile:

Loading the game…
It needs a browser with WebGPU - Chrome or Edge, Safari 26, or Firefox with WebGPU turned on.

It's a website like any other: an index.html whose script imports the game straight from its .rfn file. The page and the game talk both ways, and the game talks to the GPU.

The rules

game.rfn knows nothing about drawing. A World holds everything at one moment - the ship, what's falling, the sparks, the score and lives - and step moves it on by some seconds: the ship steered, bodies falling and turning, what touches the ship caught or hit. It returns what happened as Events, which the page plays sounds for. Its fields are changed in place with set, and @writes(world, &random) says so: the step changes the world it's given, and draws from &random for where things fall.

That makes the rules testable without a browser. Each @test injects a SeededRandom, so the same things fall every run, builds a world by hand - a rock right where the ship is - and checks what a step does to it. A long frame, from a tab that was hidden, is taken as a short one, so nothing jumps through the ship.

Drawing on the GPU

Everything on the screen is a sprite: the drifting specks behind, the rocks and stars, the sparks and the ship. sprites turns the world into eight floats for each - where it is, how big, its shape, its color and how it's turned - and a frame writes them all into one buffer laid out with step ≔ GpuVertexStep.Instance. Then one draw(6, count) draws a square per sprite, and the fragment shader cuts each to its shape pixel by pixel: a lumpy, lit rock, a five-pointed star, the ship with its flickering flame. The shader's numbers come from the game's own constants through #{ } - the field's width, the shapes - so the two can't disagree. See Graphics on the GPU for the Gpu calls.

The page and the game

The @js functions are what the page can call: start asks the browser to call frame on every frame it draws, press and release take the keys - and say whether the game used one, so the arrows don't scroll the page - and status comes back to JavaScript as a plain object. The game's state lives in @inject const game, made when the program loads; every call reads and changes it.

The other way round, the game calls the page. showStatus and playSound are declared in Refingere with @js("./page.ts") and written in TypeScript - the score beside the canvas, the best score in localStorage, and a short tone made with Web Audio when a star's caught or a rock hits. report calls showStatus only when something changed. A struct crosses as an object of its fields, in both directions - see JavaScript and TypeScript.

Building it

--web builds the page, compiles the game's folder to WebAssembly, and writes its TypeScript types beside it, as logic/starfall.d.rfn.ts - what main.ts is checked against. --web --compile writes the finished site, all static files, which is what runs above.

Concepts shown

  • A website whose logic is Refingere: importing a .rfn file from TypeScript.
  • Calling Refingere from JavaScript with @js functions, and JavaScript from Refingere with @js("./page.ts") declarations.
  • Structs crossing to JavaScript as plain objects, and the types written for them.
  • State shared across calls in an @inject const, changed in place with set.
  • Effects written down: @writes(world, &random), @pure, @effects.
  • Testing with an injected SeededRandom, so randomness repeats.
  • A #WGSL shader sharing constants through #{ }, instanced drawing, and a frame driven by gpu.animate.