↓ Skip to main content
  1. Home projects/

River Raid: many attempts at an endless river

River Raid (Activision, 1982) is a vertical shooter in which you fly up a river that never seems to end: shoot boats, helicopters and bridges, and fly over fuel depots before the tank runs dry. I wanted to rebuild it for the browser with a river generated as you fly. The shooting is the easy part. The river is not, and I tried several ways to make it.

How the original did it
#

Carol Shaw wrote River Raid for the Atari 2600, a console with 128 bytes of RAM, and the game had to fit in a 4 KB cartridge. A long river map simply does not fit, so the river is not stored at all: it is computed on the fly by a pseudo-random number generator, a linear-feedback shift register. Each step of the generator decides how the next piece of the river looks: its width, its turns, whether an island splits it.

The trick is the fixed seed. The generator always starts from the same value, so it produces exactly the same sequence every game. The river is random in its construction but identical every time you play, which lets players learn it like a hand-made level. The river is divided into sections that end with a bridge, and each bridge works as a checkpoint. And because the console’s background graphics are drawn as one half mirrored onto the other, the river is symmetric, left bank mirroring the right.

That is the bar: endless, always flyable, cheap to compute, and looking designed.

Attempt 1: the game skeleton in Excalibur.js
#

The first experiment was the game itself, in TypeScript on the Excalibur.js engine (game loop, rendering, an entity-component system and scenes). In a day I had:

  • the player’s plane in the lower third of the screen, with input and a weapon system with pooled bullets;
  • a smoothly scrolling map made of simple water and ground tiles, starting with open water;
  • collision bodies along the banks, rebuilt as the map scrolls, so touching the shore is a crash.

The plan was an infinite world of screen-sized chunks, each with its own river, bridges, enemies and fuel, created ahead of the camera and recycled behind it. Everything stood or fell with one question: where does the river come from?

Attempt 2: wave function collapse
#

Wave function collapse (WFC) is a popular way to generate tile maps. You give it a sample, it learns which tiles may stand next to which, and then it fills an empty map: every cell starts as “any tile”, the most constrained cell is collapsed to one tile, the choice is propagated to its neighbours, and so on until the map is full.

My version worked on rows, since the river scrolls row by row:

  1. From a hand-made sample map it learned which tile may follow which in a row, and how often each tile appears (its weight).
  2. Each new row was filled from left to right: pick a tile at random, weighted by frequency, from those still allowed, and narrow down the options for the next cell.
  3. Only the first cell of a row was tied to the row above it.

The rows themselves came out valid, but the river did not. With only one cell connected to the previous row, the banks jumped from row to row, the water did not form one continuous channel, and nothing guaranteed a lane wide enough to fly through. The commit messages tell the story in order: “WFC generating the river – now it is not OK”, then "…was not good, cleaned version". A full two-dimensional WFC would keep the banks continuous, but it still only knows about neighbours, never about the shape of the whole river.

Attempt 3: plan the path, then dress it with tiles
#

So I turned the problem around: first decide where the river goes, then worry about which tiles draw it. This experiment is a Python tool with a small window that generates an endless river row by row and lets me watch it scroll.

A river made by the generator
  1. The course is planned in patches. An A* search finds a path through the grid from one point to the next, so the river always continues and never dead-ends.
  2. The path is widened to a river of varying width, and sharp corners are filleted, so the banks look natural. Sometimes an island is left in the middle.
  3. Every land cell then gets its tile from a rule table: the program looks at the 3×3 neighbourhood (which neighbours are water, which are land) and picks the matching edge or corner tile.

Writing that rule table by hand was tedious, so the tool helps: click a tile, cycle through the tile types with Space until it looks right, and press G to print a new rule for exactly that neighbourhood. The generator was then ported to Godot (GDScript) with the same rules and the same editing keys.

This was the first version whose rivers looked like rivers.

Where it is now
#

The river generator works; the game around it does not exist yet. Enemies with health, bridges that take several hits, fuel and scoring are designed down to the class names, but not built. The 1982 original still wins: it does all this in 4 KB.

Related