↓ Skip to main content
  1. Home projects/

River Raid: many attempts at an endless river

··Index: 8·

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 your tank runs dry. I wanted to rebuild the game for the browser with a river generated as you fly. The shooting turned out to be the easy part, but the river wasn’t, 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 all of the game had to fit in a 4 KB cartridge. A long river map simply doesn’t fit in that, so the river isn’t stored at all. A pseudo-random number generator (a linear-feedback shift register) computes the course on the fly, and every step of the generator decides how the next piece of the river looks: how wide, which way to turn, whether an island splits the water.

The trick is the fixed seed. The generator always starts from the same value and produces exactly the same sequence in every game, so the river is random by construction but identical each time you play, and players learn the course like a hand-made level. Sections of the river end with a bridge, and bridges work as checkpoints. Because the console draws background graphics as one half mirrored onto the other, left and right banks are symmetric too.

That’s the bar I wanted to reach: endless, always flyable, cheap to compute, and looking designed.

Attempt 1: the game skeleton in Excalibur.js
#

My first experiment was a game skeleton, written 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 of simple water and ground tiles, and collision bodies along the banks that get rebuilt as the map scrolls, so touching the shore is a crash.

The plan was to have an infinite world of screen-sized chunks, each with a river, bridges, enemies and fuel of their own, created ahead of the camera and recycled behind it. All of it 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. You give the algorithm a sample, it learns which tiles may stand next to which, and then fills an empty map where every cell starts as “any tile”, collapsing the most constrained cell to one tile and propagating that choice to the neighbours until the map is full.

My version worked on rows, since the river scrolls row by row. From a hand-made sample map the generator learned which tile may follow which in a row and how often each tile appears (the weight). New rows were filled from left to right, picking tiles at random, weighted by frequency, from those still allowed, and narrowing down the options for the next cell. Only the first cell of a row was tied to the row above it.

Rows themselves came out valid, but rivers didn’t. With just one cell connected to the previous row, banks jumped from row to row, water didn’t form one continuous channel, and nothing guaranteed a lane wide enough to fly through. My commit messages from that time say it plainly, first that the WFC river is “now it is not OK”, then "…was not good, cleaned version". A full two-dimensional WFC would keep the banks continuous, but would know only about neighbours and never about the shape of the whole river.

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

So I turned the problem around, to decide first where the river goes and only afterwards worry about which tiles to draw. 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 into a river of varying width, and sharp corners are filleted to make the banks look natural. Sometimes an island is left in the middle.
  3. Every land cell gets a 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.
River generator in three steps: plan a path with A*, widen it into a river, give every land cell a tile from a rule table1 plan the pathA* search through the grid,patch by patch2 widen itvarying width, roundedcorners, sometimes an island3 tile the land3x3 neighbourhood picksan edge or corner tilerule table, built with the toolclick a tile, Space cycles types, G prints a rulerepeats row by row,so the river never ends
The generator steps: path, river shape, tiles from a rule table

Writing that rule table by hand was tedious, so I made the tool help with the job. You click a tile, cycle through the tile types with Space until the spot looks right, and press G to print a new rule for exactly that neighbourhood. Later I ported the generator to Godot (GDScript) with the same rules and the same editing keys, and this was the first version whose rivers looked like rivers.

Where it is now
#

The river generator works, but there’s no game around it yet. Enemies with health, bridges that take several hits, fuel and scoring are designed down to the class names and haven’t been built, and the 1982 original does all of this in 4 KB.

Related