↓ Skip to main content
  1. Home projects/

River Raid: many attempts at an endless river

River Raid (Activision, 1982) is a vertical shooter. 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 hard, 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. The game had to fit in a 4 KB cartridge, so a long river map did not fit. The river is not stored at all. A pseudo-random number generator, a linear-feedback shift register, computes it on the fly. Each step decides how the next piece looks: its width, its turns, whether an island splits it.

The generator always starts from the same seed, so every game gets the same river and players can learn it like a hand-made level. The river is cut into sections that end with a bridge, and each bridge is a checkpoint. The console mirrors one half of the background onto the other, so the river is symmetric.

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. In a day I had the plane with a weapon system, a scrolling map of water and ground tiles, and collision along the banks, so touching the shore is a crash.

The plan was an infinite world of screen-sized chunks, created ahead of the camera and recycled behind it. It all depended on 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. It learns from a sample which tiles may stand next to which, then fills an empty map one cell at a time, always collapsing the most constrained cell first.

My version worked on rows, since the river scrolls row by row. It learned from a hand-made sample which tile may follow which, and how often each appears. Each new row was filled from left to right. Only the first cell was tied to the row above.

The rows were valid, but the river was not. The banks jumped from row to row, the water did not form one channel, and nothing guaranteed a lane wide enough to fly through. The commit messages tell it 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. It would still know only about neighbours, never the shape of the whole river.

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

So I turned it around: first decide where the river goes, then pick the tiles. This experiment is a Python tool with a small window. It 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 from one point to the next, so the river always continues and never dead-ends.
  2. The path is widened into a river of varying width, and sharp corners are rounded, so the banks look natural. Sometimes an island is left in the middle.
  3. Every land cell gets its tile from a rule table. The program looks at the 3×3 neighbourhood and picks the matching edge or corner tile.

Writing that rule table by hand was tedious. So in the tool I click a tile, press Space until it looks right, and press G to print a rule for that neighbourhood. I then ported the generator to Godot (GDScript) with the same rules and 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