A physics bridge-building game for the browser, Android and the desktop. Build a bridge within a cash budget from road, heavy deck, wood, steel and cable, then send a vehicle across it. The bridge either holds, sags, or snaps into the river.
There are 84 levels in twelve chapters of seven that get harder as you go: from a 4 m brook in Groundwork, through cables in Tension, drawbridges in Heavy Traffic, block arches in Arches, trains and soft mud in Railway, concrete anchors in Anchorage, spans of 40 to 64 m in Main Cable, marching squads, gales and an earthquake in Master Works, and prestressed girders, a double-deck railway and drawbridges that split, lift and swing in Modern Works, and trains that meet on the span, balanced cantilevers and floods up to the deck in Both Ways, to the Grand Finale, where the hazards come together. Along the way the game introduces piers, slopes, lattice pylons, heavy deck, rock overhangs, cables, flood water, ship channels, drawbridges on hydraulic rams, convoys, toll bolts, rationed materials, hinged masts, concrete anchors, the curved main cable, concrete, concrete blocks, block arches, track, trains, dampers, marching squads, wind, earthquakes, mud, piles, floods, prestressing tendons, double decks, center locks, lift towers, swing bridges and oncoming traffic. Each level is tested in the scene of its topic: a river at golden hour, a desert canyon, a flood plain in the rain, the coast, snowy mountains, a city at night, a harbor, a fjord at dawn, a golden bay, a limestone gorge, the prairie, a storm, a river delta at dusk and an estuary at sunset. The chapters screen shows them as a map, one winding road through every level, or as a book of chapter cards. The game is in English, German, Swedish, French, Spanish, Brazilian Portuguese, Polish, Russian and Japanese (see Languages below). TODO.md lists planned features and improvements by priority.
Requires Node 22.15+ (it uses the built-in node:sqlite and module.registerHooks).
npm install
npm run dev # game + SQLite API at the printed localhost URL
npm test # physics, editor, scoring and storage tests
npm run lint # Oxlint
npm run smoke # end-to-end smoke test in a headless Chrome or Edge: build 1-1 with the keyboard, test, check the result and leaderboards
npm run build # static build in dist/
npm start # production: serves dist/ + the SQLite API on :3000
npm run tuner # tuner page: pick levels and effort, run the optimizer, edit the difficulty
npm run tune # level optimizer from the command line: tunes budgets, targets, bonus goals and marked geometry
npm run tune:unpack # readable JSON copies of src/levels.res in tools/tune/results
npm run verify # level verifier: checks every level's quality in a couple of minutes, without optimizing
npm run i18n # how complete each language's table is; npm run i18n -- de lists the missing keys
npm run screens # screenshots of every screen; -- --narrow for a 360 px phone, -- --lang de for a language
npm run desktop # build, then run the desktop app (see Desktop below)npm start reads PORT (default 3000) and DB_FILE (default data/bridgebuilder.db). Add ?debug to the URL for an FPS counter.
On first launch you enter your name, which creates a profile (or continues an existing one with that name). Each profile keeps its best score and stars per level, with what that best bridge cost, its peak stress and the bridge itself, plus its saved design for each level and the engineer's hints taken. Chapter challenge runs go into a shared top-10 table per chapter, under the profile name.
Saved designs carry a format version. A design saved before a level changed is fitted to the level as it is now instead of being thrown away: parts that no longer fit come off (a material no longer offered, a bolt that moved, a joint now out of bounds, parts over a new material limit), the game says how many, and a bridge that now costs more than the budget loads for trimming but can't be tested until it fits.
At startup the game probes api/health:
- Server present (
npm run dev,npm run preview,npm start): everything is stored in SQLite (server/api.mjs). The tables areprofiles,level_progressandscores. - No server (GitHub Pages or any static host): everything is stored in the browser's
localStorage, per device. - Android app: no server probe. Everything is stored on the phone in native preferences (
src/native.ts), which the system doesn't clear the way it can clear a WebView'slocalStorage. With Google Play Games signed in, the same save also goes to the Play Games cloud (see Android below). - Desktop app: no server probe. Everything is stored on the computer, in the app's data folder (
%LOCALAPPDATA%\se.cuplex.bridgebuilderon Windows).
The profile screen tells you which storage is in use. The last-used profile resumes automatically on the same device.
.github/workflows/pages.yml lints, tests and builds, then publishes dist/ on every push to main. Enable it once under Settings → Pages → Source: GitHub Actions. The build uses relative paths, so it works from the /<repo>/ sub-path. Shared bridge links point at https://bridgebuilder.cuplex.org, so the site belongs there: a DNS CNAME record from bridgebuilder to cuplexuser.github.io, and that domain under Settings → Pages → Custom domain, with Enforce HTTPS. The site then serves /.well-known/assetlinks.json for the Android App Link.
The web build, the Android app and the desktop app are the same game code. Everything that differs sits behind src/platform.ts:
- Which build (
platform:web,androidordesktop), also set on<html data-platform>. - UI profile (
uiProfile, on<html data-ui>):touchin the Android app and in browsers whose main pointer is a finger,desktopotherwise. Touch hides key hints and doesn't move focus to buttons. Desktop shows the keys and, on screens 900 px and wider, tightens buttons and the toolbar so more of the screen is bridge. - Level caps (
levelCaps): any build loads a custom level up to 96 m wide, but on a touch device the editor stops growing one at 64 m, where a phone's screen and physics still keep up. A wider level from a desktop keeps its width when edited on a phone. - Storage, files and sharing: device storage, level export and shared bridges (a download in the browser, a save dialog on desktop, the share sheet on Android and in browsers that have one), links that open a shared bridge, and the clipboard.
- Android only: the back button, haptics, Google Play Games and the full-game unlock.
Each shell's side is loaded on demand (src/native.ts for Android, desktop/src/bridge.js for desktop), so the web build carries none of it.
The result, collapse and game over cards offer SHARE THE BRIDGE (S): a 1200×675 picture of the bridge and a link that drives it. The picture is drawn at its own size from the run's stress log, at the moment it peaked for a crossing or half a second into a collapse, with the level, the score or what failed, and who built it (src/sharecard.ts, src/game/sharing.ts). The link is a short one, https://b.cuplex.org/Ab3dE9xQ: the share service (below) keeps the bridge under an 8-character code that is the bridge's own hash (src/share.ts), so the game knows the link before the upload is done. The game waits up to 1.5 s for the service to take the bridge, since a browser opens its share sheet only soon after the tap, then uploads the picture as a JPEG for the link's preview. When the service can't be reached, the game shares the long link instead, https://bridgebuilder.cuplex.org/?b=…, which carries the level, the design and the builder's name compressed into it, a few hundred characters for most bridges; long links keep working. In the app and in browsers with a share sheet the picture and the link go to it; elsewhere the link goes on the clipboard and the picture is downloaded, or saved through a dialog on desktop.
A short link opens a page with the bridge's picture, title and score for chat apps' previews, which sends a browser on to https://bridgebuilder.cuplex.org/?s=<code>; the game fetches the bridge from the service. Opening a link (on the site, or in the Android app through an App Link for either host) drives that bridge on its level the way a level's example does, under whatever profile is playing, or once one is picked. Anyone can watch, on any level. Afterwards the player builds there if that level is open to them, and goes back to the chapters otherwise. A bridge on a custom level, playtested from the level editor, shares the same way with the level itself in the link, a few hundred characters more. Opening one saves that level in the recipient's level editor, once, however often the link is opened, and they build on it after watching.
The site has to be served from bridgebuilder.cuplex.org for links to work, and Android only opens them in the app once public/.well-known/assetlinks.json carries the SHA-256 of the Play app signing key (the App signing page under App integrity in the Play Console). Until then they open in the browser. share-service/deploy.sh puts a copy of that file on b.cuplex.org too, so run it again after filling in the key.
share-service/ is a small Node service for Cloud Run (app.mjs, no framework). It stores shared bridges and their pictures in a private Cloud Storage bucket and answers the short links:
| Request | What it does |
|---|---|
PUT /api/bridges/<code> |
Stores a bridge. The code must be the SHA-256 of the body in base 62, the body must look like a bridge, and it's at most 64 KB. |
PUT /api/bridges/<code>/image |
Stores a stored bridge's preview picture: a 1200×675 JPEG, at most 1 MB. |
GET /api/bridges/<code> |
The bridge, for the game. Cached for a year, since a code always means the same bridge. |
GET /<code>, /<code>.jpg |
The preview page, which goes on to the game, and its picture. |
Nothing stored is ever overwritten or deleted: the service's account may only create and read objects. One address may store 40 bridges and pictures in 10 minutes. node share-service/index.mjs runs it locally on port 8787 with the files in data/shared/; start the game with VITE_SHARE_SERVICE=http://localhost:8787 to share through it.
It runs in project elemental-apex-511507-g2, region europe-west3. Cloud Run can't map a custom domain itself in that region, so Firebase Hosting answers b.cuplex.org and passes every request to the service (share-service/firebase.json); its CDN also caches the pictures and pages.
To set it up, or deploy a change:
- Billing: Cloud Run needs a billing account on the project (Billing in the Google Cloud console). At this size it stays within the free tiers; a budget alert under Billing → Budgets & alerts is a cheap safeguard.
- Sign in once:
gcloud auth loginandnpx firebase-tools login. - Deploy:
bash share-service/deploy.sh(Git Bash on Windows). It turns on the APIs, creates the bucketelemental-apex-511507-g2-shared-bridgesand the accountbridge-share@…, gives the project's default compute account the Cloud Run Builder role (source deploys build as it, and newer projects give it no roles), deploys the service from theDockerfile, adds Firebase to the project and deploys Hosting. Run again, it only deploys. If Firebase says the project has no Hosting site yet, open the Firebase console, pick the project, click Build → Hosting → Get started once, and run the script again. - Domain: in the Firebase console, Hosting → Add custom domain, enter
b.cuplex.org, and add the DNS records it lists (aTXTrecord to prove the domain is yours, and theArecords orCNAMEit shows forb) wherecuplex.org's DNS is kept. Firebase provisions the certificate itself once they resolve, which can take from minutes to a day. Ifcuplex.orghasCAArecords, they must allowletsencrypt.organdpki.goog. - Check:
https://b.cuplex.org/healthanswersok(not/healthz: Cloud Run keeps paths ending in z for itself). Before the domain is connected, the same works athttps://elemental-apex-511507-g2.web.app.
The service's logs are under Cloud Run → bridge-share → Logs. Shared bridges include the builder's profile name, shown in link previews; the privacy policy (public/privacy.html) says so.
On phones, members creak with a light buzz, every snap is a hard pulse, and a collapse is a long one (src/haptics.ts paces them). On devices that can vibrate, the pause menu has a switch for it.
The Android app is the same web build wrapped with Capacitor (app ID se.cuplex.bridgebuilder). The native project lives in android/. src/native.ts covers device storage, the back button (it does what Escape does, and leaves the app from the title screen), level export and shared bridges through the share sheet, links that open a shared bridge, the clipboard, haptics, saving when the app goes to the background, and the game's two Java plugins in android/app/src/main/java/se/cuplex/bridgebuilder/:
PlayGamesPlugin: Google Play Games Services v2. Players are signed in as the app starts. Achievements (src/achievements.ts) mostly follow from progress, so they come back on a new phone. A few come from moments in play: a first collapse, a finished or flawless challenge, crossing your own level. Leaderboards take the career score, total stars and each chapter's challenge. The profiles and progress on the phone go to a Play Games saved game, and a save from another phone is merged in, keeping the better result on every level. A fresh install signed in to Play Games plays under the player's Play Games name, with no profile screen. The Play Games bar on the leaderboards screen opens Google's leaderboards and achievements, or signs in.PlayBillingPlugin: Google Play Billing for the full game. The first three chapters are free (FREE_CHAPTERSinsrc/chapters.ts). The rest are one in-app product,full_game, bought once. Locked chapters show FULL GAME on the map and in the book, and opening one shows the offer with Play's price, a buy button and a restore link. Ownership is read from Play at every start, and the last answer is kept for when Play can't be reached. There are no ads. The web and desktop builds have no paywall.
To set them up in the Play Console:
- Play Games Services: create the project, then add the achievements and leaderboards listed in
android/app/src/main/res/values/games-ids.xml, each with its name and what it's for. Turn on Saved games. Then copy the ids from Get resources into that file by name,app_idincluded. The Console makes resource names from the names you give (First Crossing becomesachievement_first_crossing), so a different name only needs its line renamed. Withapp_idempty, Play Games stays off and the app runs without it.npm run games-idsrewrites the file after achievements change in the code, keeping the ids already filled in. - In-app product: a one-time product with the id
full_game, at the price you choose. Add your account as a license tester to buy it for free while testing.
Requires Android Studio (its bundled JDK is enough) and the Android SDK.
npm run android # build the web app and copy it into android/
npm run android:open # open the project in Android Studio
npm run android:run # build, copy and run on a connected device or emulatorRun npm run android after every web change before building in Android Studio. Release builds (a signed .aab for Google Play) are made in Android Studio under Build → Generate Signed App Bundle. Keep the keystore out of git.
The desktop app wraps the same web build in Tauri, a small Rust shell around the system's WebView (WebView2 on Windows) (desktop/). It serves dist/ from its own origin, so modules, the tuner worker and storage work as on the web. It keeps saves in the app's data folder, remembers the window's size and place, and saves exported levels and shared bridge pictures through a save dialog. F11 toggles full screen. There's no paywall, and the level editor goes up to 96 m.
npm run desktop # build, then run the app
npm run desktop:dev # run the app against the Vite dev server, which it starts: Ctrl+R reloads, F12 opens the developer tools
npm run desktop:dist # build a Windows installer and a portable .exe in release/Building it needs Rust as well as Node. The first build compiles Tauri and takes a few minutes; later ones are quick. desktop/tauri.conf.json configures the window's origin and the installers, and desktop/src/main.rs is the shell: the window, the save dialog and full screen. desktop/src/bridge.js runs in the page before the game and puts window.bridgeDesktop there. The installer fetches WebView2 if a computer lacks it, and the portable .exe expects it (Windows 10 and 11 have it). The config also has Linux (AppImage) and macOS (dmg) targets, which have to be built on those systems. The builds aren't code-signed, so Windows SmartScreen warns on first launch.
| Action | Mouse / touch | Keyboard |
|---|---|---|
| Build a member | Drag from a bolt or node | Arrows/WASD move cursor, Space/Enter to start and place (chains) |
| Lay road | One drag lays a whole run of road or heavy deck. The pieces follow one straight, even grade, even between banks at different heights. | Same |
| Add a joint mid-beam | Drag from (or onto) a point along an existing beam. The beam is split there at no extra cost. | Space with the cursor on the beam point |
| Remove a member | Tap it / right-click. A beam it had split is joined back up. | X or Delete at cursor |
| Paint concrete blocks | With Block in hand, drag across the grid: a block on every square you pass that touches rock or another block. A drag that starts on a block erases instead, and one that starts where no block can go pans the view; a tap there says why. Tap or right-click a block to take it off with any other material. | Space paints (or takes off) the block up and right of the cursor |
| Material | Toolbar | 1–9 and 0 pick the level's materials in toolbar order (each button shows its key), Q / E to cycle |
| Undo / redo | Toolbar | Z / Y (or Ctrl+Z / Ctrl+Y) |
| Test / back to edit | TEST button | T |
| Zoom / pan | Wheel, pinch, drag empty space | F refits |
| See under your finger | A magnifier above the finger shows the joint you're placing on touch screens | |
| Goals and rules | ⓘ button, or tap the ✦ goal under the level name | I |
| Stress graph after a test | Link on the result or collapse screen; tap the graph or a member | G, ←/→ to step, Esc back |
| Pause | II button | P / Esc |
| Menus | Buttons | Title: Enter continue, C chapters, H leaderboards, L level editor. Chapters: V switches between the map and the book, Enter plays the next level, 1–9 and 0 open the first ten chapters, ←/→ turn the book's pages. Chapter: 1–7 plays a level. Leaderboards: ←/→ tabs, 1–9 and 0 chapter. |
| Mute | Speaker button | M |
| Share a bridge | SHARE THE BRIDGE on the result, collapse or game over card | S |
Members can cross each other (X-bracing), but they can't lie along an existing member. For example, a wood beam can't run on top of the road.
Briefing. Every level opens with a briefing card: who crosses and how far, the level's tip, the three stars (cross, build under the target, keep peak stress below 75%), the bonus goal and what it means, and any special rules such as a drawbridge, a convoy, toll bolts, part limits, or a deck too weak for the vehicle. The ⓘ button or I brings it back while you build. Tutorial levels (the ones with a ghost) also offer Watch an example (W): the intended bridge drives across first, then you get your own blueprint back. On Bascule the ghost is that whole drawbridge, ready to trace. The bonus goal also sits under the level name and is checked live where it can be: ✓ or ✗ for cost, parts and banned materials as you build (with your current part count), while a stress goal is judged on the test drive.
Stress graph. After a test, See the stress graph (G) docks a graph under the bridge: the busiest member's load over the whole run, colored by stress, with the 75% safety line, the 100% breaking line, a mark for every break and the drawbridge phases shaded. It opens at the worst moment. Tap or drag across the graph to find the busiest member at any moment, ringed on the bridge (a broken one is shown dashed where it stood), or tap a member to follow its own load as a dashed curve.
L on the title screen opens the level editor. Make a new level or edit one of yours:
- Tools (1–8): Bolt adds or removes a bolt, Pier stands a pier under a bolt in the gap, Pylon raises a lattice pylon with a bolt on top, Mast raises a hinged mast whose top tips freely, Rock raises a 2 m rock shelf or island for concrete blocks and arches to stand on (and offers them), Mud lays a soft mud bank across the gap by dragging at the height of its top (and offers piles), Channel marks a ship channel by dragging across the gap (it stays clear up to the height you start at), and Erase removes whatever is under the tap. The cursor says what a tap will do before you tap. A tap acts where the finger lifts, so it can slide into place first under a magnifier, and two fingers pan and zoom without placing anything. Z and Y undo and redo.
- Settings (S): name and tip, the gap's width, far bank height and water level, the vehicle and up to four more behind it, a tall ship's mast height for a drawbridge, the materials on offer and any part limits, how far back concrete anchors may go on each bank, what a pile costs and how far a flood raises the river, budget, star target, toll per bolt, the bonus goal and the scene for the test drive.
- Auto-tune, under Money and goals in Settings, searches the level for 5 seconds and sets the budget, star target and bonus goal for the difficulty picked (Easy, Medium or Hard). It keeps the level's kind of bonus goal when it can be met, else picks a peak-stress cap, then a parts cap, then a cost cap. It starts from the last bridge you playtested, and the budget always covers that bridge. The bridges it finds are never shown, only what they tell: the note under the button gives the cheapest crossing found and the lowest peak stress, and the header warns if you then set the budget, star target or stress cap below them. It is a quick, rough search, so a sharper player can beat its numbers. Any edit, including the tune, can be undone. In dev builds (
npm run dev) two more checkboxes appear. Show the tune's bridges adds buttons that drive the cheapest bridge it found, or the one meeting its bonus goal, across the level. Search up to 20 s if nothing holds lets a slow level, such as a drawbridge with a convoy, keep searching past 5 seconds until something does. Production builds show neither. - Playtest (P) plays it like any level, with the briefing, scoring and stress graph, but nothing goes on your career or the leaderboards. The header says what still stops a level from being played, such as a drawbridge with no rams on offer.
- Export shows the level as JSON to copy or download, and Import reads one back from pasted text or a
.jsonfile. The format is the game's ownLevelDef(src/levels.ts), so a built-in level copied from the source imports too. Everything is checked and clamped on the way in.
Custom levels and the last bridge built on each are kept in this browser's local storage.
| Material | Price | Reach | Notes |
|---|---|---|---|
| Road | $180/m | 2.25 m | Drivable, laid in runs. Carries vehicles up to 30 t: everything but the semi. |
| Track | $300/m | 2.25 m | Railway deck, laid in runs like road. Trains run on it and nothing else. It never climbs more than 3%, the editor refuses a steeper piece. Stiff, heavy, and rated for 50 t a vehicle. |
| Heavy deck | $380/m | 2.25 m | Drivable, laid in runs. About twice as strong and stiff as road, but heavier, and a little stiff in bending, so it spreads a wheel load onto neighboring joints. Carries up to 60 t. For the semi, long spans and suspension decks. |
| Wood | $90/m | 3.2 m | Light and cheap. Buckles early in compression. |
| Steel | $240/m | 4.25 m | Strong in tension and compression. |
| Cable | $140/m | 10 m | Tension only: it goes slack instead of pushing. Hang decks from pylons and overhangs. |
| Main cable | $300/m | one drag | Thick and orange, three times a cable's strength. One drag from peak to peak, or down to a concrete anchor, lays it as a curve with a joint above each deck joint, and it counts as one part. Drag the ring on it up or down to set the sag; tapping any piece removes the whole cable. Hang the deck from it with plain cable. |
| Concrete | $160/m | 4.25 m | Five times steel's strength in compression and barely buckles, but heavy, and it cracks under a 20 kN pull. For tall towers on long spans. |
| Block | $120 a block | 1 m squares | Painted on the grid, each touching rock or another block along a side: nothing hangs in the air. Almost impossible to crush, heavy, and held together only weakly: pulled apart, a block cracks along its side. It bears on rock (bank tops, canyon walls, rock shelves, pier tops) without being bolted to it, so it can slide or tip. Bars attach at its corners but can't pass through it. |
| Block arch | $150/m² | one drag | A ring of wedge-shaped blocks 1 m thick, laid in one drag between two springings with a joint under each deck joint. Drag the handle at its crown to set the rise, or, with Block arch in hand, anywhere on the ring (a tap still takes it off). The rise steps a quarter meter, stops at the flattest the ring may be (a fifth of its span) and at the level's height limit, and let go where it can't be, such as up against the deck, it stays at the highest rise it could take. It pushes outward on its ends, so they need rock or blocks behind them, and its mortared joints crack and hinge where it's asked to bend. |
| Hydraulic ram | $420/m | 4.25 m | Magenta, drawn as a cylinder: a fat barrel on its lower end, a chrome rod above. Strong, heavy and expensive. On drawbridge levels it extends by 75% to lift the leaf, then pulls it back down. Elsewhere it's a stiff, pricey strut. |
| Tendon | $160/m | 16 m | A taut pale-steel strand with an anchor plate at each end, threaded straight through concrete blocks from corner to corner. Pulled tight to 250 kN as the test starts, it squeezes the blocks between its ends, so a beam of them can bend without cracking open. |
| Damper | $320/m | 4.5 m | Amber, drawn as an oil cylinder between two rods, with a coil spring around it. Its spring is soft, so it carries little standing load and gives under a slow one, but it pushes back hard on anything that moves it quickly. Braced into a bridge that swings, it turns the motion into heat. |
You pay by length, so splitting a beam to add a joint costs nothing. Each level lists the materials it offers.
Vehicles weigh from 9 t (the compact car) to 40 t (the semi). A vehicle heavier than a deck's rating crushes each piece it drives onto, however well the deck is braced. Heavy deck arrives at the end of chapter 3, with the semi. The semi rides on three axles, so its weight spreads along a hung deck the way a real tractor-trailer's does.
A road run gets a joint at every bolt it passes over, so a deck laid across a pylon's bolt is fastened to it.
Joints. A joint fails when too many loaded members pull on it at once: past its two busiest members, the rest of their stress ratios may add up to at most 180%. The busiest member then breaks. A joint glows while it's working hard. Clean load paths with few members per joint keep clear of it.
Chapters 4 and 5 add four mechanics, which levels can mix:
- Drawbridges. A tall ship has to pass through the channel before traffic may go. When you test, the bridge opens on its hydraulic rams, the ship sails through at the channel's middle, the bridge closes and the vehicle drives. Hinge the leaf at a bank or pier, stand a ram under it, and keep everything clear of the ship's mast.
- Convoys. Several vehicles cross nose to tail at the slowest one's speed, so the bridge carries them all at once. The level card lists them, and all of them have to reach the far side.
- Toll bolts. On some levels every bolt but the two road ends costs money to build from, shown next to the bolt and charged once, however many members use it.
- Material limits. Some levels cap how many parts of a material you may use, on top of the budget. The toolbar shows how many are left. Several cable levels in chapters 3 to 5 and 9 ration wood and steel this way, so the cables have to carry the deck.
Chapters 7 and 8 bring suspension bridges anchored in the ground:
- Hinged masts. A mast stands on a hinge at its foot, so its top bolt tips freely. It carries a load straight down its length but nothing sideways: a main cable pulling its top toward the middle tips it over unless a backstay pulls the other way.
- Concrete anchors. Drag a member onto the bank, from 1 m up to the level's reach behind the edge, and a 5 t concrete block is set there for $1,500. Removing the last member on it removes it, and refunds it. Joints may also go in the air above the anchor strip, 3.5 m or more over the road, to break a long backstay.
- Holding and tearing loose. A block holds by its weight against lift, and by friction plus the soil packed against it against a sideways pull. Lifting it also takes weight off the friction. So a steep backstay lifts it out and a flat one slides it; a block glows as it nears its limit, and past it the block tears out of its pit and drags. Two backstays to two blocks share the pull.
- Your own towers. On 7-6, Raise Your Own, there are no masts: brace a steel tower on each pair of piers, then backstay it.
Chapter 8 is about very long spans, 40 to 64 m:
- The main cable. It is laid in one drag and hangs on a parabola, its joints above the deck's joints so hangers drop plumb, and it is drawn as one smooth curve that keeps moving smoothly under load. The ring near its middle sets the sag: a deeper sag pulls less on the towers and anchors, but needs longer hangers near the towers (a hanger is plain cable, at most 10 m). A main cable may hang low over the anchor strip as it rises from its block, where nothing else may go.
- Concrete towers. Steel crushes under the weight of a long span long before any traffic arrives, so towers are concrete legs on a pier pair with steel struts and chevrons between them. The level sets a build ceiling for them.
- Heavier anchors. A level can set heavier concrete anchors (
blocks.tonnes): bigger in every direction, they hold more and cost in proportion. These levels use 20 t blocks for $6,000. The bolts hold no cables, so every main cable ends in concrete. - The levels. Saddle teaches the main cable on hinged masts with a full ghost. Concrete Pylons raises two towers, One Tower hangs both spans from a single tall one, Uneven Banks has a far bank 6 m higher, and Tall Ships lifts a 64 m deck over a ship channel for a semi, with two anchors a side.
Chapter 6 puts trains on the bridge:
- Trains. A locomotive (36 t) pulls up to four freight wagons (28 t each), coupled close together, so the whole train is on a short bridge at once: a long, heavy load rather than a point. Rail wheels run on track only, and drive and brake against it, so the deck takes the push.
- Track climbs at most 3%. Uneven banks get one straight run, and the bridge is built down to the piers from it.
- Brake stops. On a level with one (
brakein a level), the train brakes hard once its front reaches the mark, stands on the bridge, then goes on. While it stops its wheels shove the deck forward along its length.
Chapter 5 builds with concrete blocks, which only push:
- Blocks are painted a square meter at a time and simulated as stiff squares, each with its own corners, joined to the blocks beside them by mortared faces (see Mortared joints below). They take a lot of compression but only a small pull (25 kN), so a wall bent too far opens along its joints and a beam of blocks cracks at its root.
- Rock. Rock shelves at the foot of the canyon walls and islands in the river (
rocksin a level) are where blocks stand. A block corner bears on rock and grips it by friction, 0.7 times how hard it presses, but nothing bolts it down: a sideways push beyond that slides it, and a block can lift off. - Thrust. An arch carries the road as a push into its two ends, and the flatter it is, the harder it pushes outward. Spring it from a rock corner or a block wall against the rock and it holds; spring it from the open top of a shelf and it slides off. Its default rise is a quarter of its span, kept half a meter under a deck that's already built.
- Mortared joints. The wedge blocks of a ring each have their own corners, and meet their neighbors through a mortared face. While the mortar holds (25 kN across each edge), a joint acts as one piece and the ring carries a load as a whole. Pull one edge of a joint open harder than that and its mortar cracks for good, drawn as a dark seam: from then on the blocks only press on each other there, so where the line of thrust leaves the ring the joint gapes on one edge and hinges on the other, like real voussoirs. A ring that carries its load as thrust shrugs this off; a thin ring under a heavy point load, or a flat one asked to work as a beam, cracks into hinges and folds unless the load is spread by a stiff deck or its ends are buttressed.
- Towers and piers. Stack blocks on shelves that sit too deep for posts to reach, or on an island to make a pier where two arches meet and push against each other. Wood or steel posts stand on the ring's joints and the tower tops to carry the deck.
- Over the road. An arch may also spring from the bank bolts, or from piers built up level with the road, and rise above the deck, which then hangs from its underside on cables: a through arch, for where there's no rock below or a ship channel to keep clear. Its default rise stays under the level's height limit.
- The levels each ask for a different arch: Keystone (a ghost of the classic one on block towers), Hanging Deck (a through arch from bolt to bolt), High and Low (shelves at different heights), Short Supply (four blocks, stacked against the walls rather than out on the shelves, where the thrust cracks them) and Grand Viaduct (a semi over deck arches to island piers, and a through arch over the ship channel between them).
Chapter 9 shakes the bridge, in a storm scene:
- Marching. Squads of soldiers (
marchin a level) walk at 1.6 m/s, and every boot lands at once at the level's pace, pressing the deck harder and softer by a share of their weight. A light suspension bridge swings in time with them, more with every step, until a backstay tears out or the deck throws someone off. A squad is thrown once it is clear of the deck and falling faster than 2.5 m/s, or tipped more than about 29°, and its soldiers go into the river; boots that just bounce off a lively deck don't count. A crossing that loses one squad still counts, for its first star only, with no safety points and no bonus goal; a second thrown squad loses the run. - Wind. Gusts (
wind) come every few seconds, never quite still. They push on every member by how tall it stands across the wind and lift the deck by its length, like a wing. Hangers only pull, so a light deck rises off them. A gale can also flutter (flutter): it pushes a moving deck further the way it is already going, so a lively deck swings harder with every bounce, as at Tacoma Narrows, until it tears itself apart. A stiffening truss along the deck, or dampers, stop it. - Earthquakes. The ground (
quake) starts to shake when the lead vehicle reaches a mark, mostly sideways, building up quickly and dying away. A quake may sweep (sweep): it shakes faster and faster, so at some moment it matches whatever way the bridge sways and sets it swinging. Tall towers whip on their footings unless they're braced to the deck and the banks. - Dampers are the answer to motion. A stiff prop from the rock to a marching deck takes the bus's full weight as it passes and snaps; a damper gives under it but fights the swing.
- Brake stops on sliding bearings. A level's road ends may rest on sliding bearings (
sliding), drawn as rollers: they hold the bridge up and down but let it slide along. A braking train shoves it along its length, so it has to be braced down to the low bolts both ways, as railway bridges are (Signal Stop). - The levels. Slow March (a ghost of dampers in an X under a footbridge), Gale (a stiffening truss against the flutter), Aftershock (brace the towers against a sweeping quake), Parade (a bus and four squads, with three bolts on each drowned rock for dampers) and Storm Parade (the march in a fluttering gale).
Chapters 6 and 7 build on soft ground, in a river delta at dusk:
- Mud. Mud banks (
mudin a level) fill the riverbed. Nothing can be built inside them. A bolt on the mud holds a light load, but once its members press it down with more than 15 kN it sinks, faster the more it carries, until it reaches firmer ground 1.5 m down, and the deck sinks with it. - Piles. Where a level offers them (
piles), drag a member onto a mud bank to drive a pile there, on whole meters, for the level's price. A pile reaches rock, so it never sinks. Like a concrete anchor, it comes out again once nothing is built from it. - Floods. On some levels (
flood) the river rises once the lead vehicle reaches a mark. The current, at about the drag of a real flood, pushes downstream on every member by how much of it is under water, and any bolt on the mud that stands under the flood for a second and a half is scoured out. Piles stay. The flood also carries driftwood: logs float down one after another and ram whatever stands in the stream, jamming against it and pushing with the current. - The levels. Mudflats (a ghost: one pile under the middle), Soft Ground (free bolts on the mud that a truck sinks), Flash Flood (bolts that hold until the flood washes them out), Long Flats (36 m over two mud flats, with dear piles) and Floodtide (the school bus over 40 m, with a flood on the way).
Chapter 11 puts the hazards together, two lessons a level, each checked by the optimizer: Great Arch (one 36 m block arch for the semi, its springings built up in blocks), Parade, Washout Line (a freight train over silted mud in a flood: piles, not the free bolts), Storm Parade, Marsh March (the march over a marsh, where the free bolts in the mud hold a damper but sink under a stiff prop), Delta Haul (a semi on heavy deck over a delta in flood) and Last Crossing (48 m for the semi over a flooding delta, with a ship channel to keep clear).
Chapter 10, played before the finale (its id is 11, so its boards and saves don't clash with the finale's), brings in prestressed concrete, a second deck and three new drawbridges:
- Mortared blocks. Every concrete block has its own corners and meets the blocks beside it through mortared faces, like an arch ring's voussoirs: shut until pulled harder than 25 kN, then cracked for good. Blocks that only touch at a corner, like an arch springing from a tower, share it. A beam of blocks therefore cracks open along whichever side it is pulled apart, unless something squeezes it.
- Tendons. A tendon (
tendon) is a steel strand threaded straight through blocks, from corner to corner, and pulled tight to 250 kN as the test starts, squeezing every block between its ends. Low in a girder that sags, it keeps the underside shut. A tendon at the very bottom of a one-block beam pulls its top open instead; it has to stay within the middle of the beam, as in real prestressing. Over a middle pier the girder bends the other way, so a tendon along the middle of the beam serves both. - Two decks. A level may have a lower deck (
lower), whose own traffic, such as a freight train, comes out of a tunnel in each bank and crosses at the same time as the road. Braced to each other, the two decks are the chords of one deep truss. - Double bascules. Two leaves, one from each bank, meet at one joint on a center lock (
lock). The lock splits that joint into one per leaf, like a seat does, holds the two level while the bridge is shut, so each leaf helps carry the other, and lets go as they open. - Lift bridges. Guide towers (
lift) stand at the channel edges. Whatever rests on the seats there runs straight up and down their rails, so rams lift a span that is laid on its own from seat to seat, while the approaches stay put. Without the rails, two upright rams would let the span sway along like a parallelogram. - Swing bridges. On a swing level (
swing), the whole bridge turns aside on its pier for the ship, drawn narrowing toward the pivot. Its road ends lift off the banks while it turns, so it hangs from the two pivot bolts as a balanced cantilever, then jacks set the ends back down. - A ship that waits. With
ship.after, the traffic crosses first; then the bridge opens, the ship must get through untouched, and only then is the run won. - The levels. Tendon (a ghost: a shallow girder over a ship channel), Two Spans (a semi over a continuous girder), Double Bascule, Lift Bridge, Double Deck (a truck above a locomotive and wagon), Swing Bridge and Ship Waits (a freight train over a lift bridge, then the ship).
Chapter 12, played eleventh, before the finale, in an estuary at sunset:
- Oncoming traffic. A level may send traffic the other way too (
oncoming): a vehicle or a train in its own lane that waits on the far bank as far out as the main one waits on the near bank, sets off with it, and has to reach the near bank. The two meet out on the span, so it carries both at once: a 2 m Warren truss that holds one train breaks when two meet on it. The far lane is drawn behind the near one, mirrored and dimmed. - Balanced cantilevers. A block girder whose arms reach out from a pier with nothing under them bends the other way from a girder on two supports: its top opens over the pier. A tendon draped high over the pier and low at the tips keeps it shut.
- Floods that reach the deck. On Driftwood and Both Ways in Flood the river rises to a meter under the road, so every post and diagonal under the deck stands in the current and in the way of the logs. Trusses over the road stay out of reach.
- The levels. Passing Loop (a ghost: two trains meeting over one span), Meeting Point (two trucks, and a truss deep enough for both), Freight Both Ways (two long freight trains over piers), Cantilever (a ghost: arms out each way from one pier), Free Arms (arms from two piers meeting over a ship channel), Driftwood and Both Ways in Flood (a truck each way over four spans in a rising flood).
Every string the player reads goes through t() and tn() (src/i18n). English (src/i18n/en.ts) is the source, and each language has a table in src/i18n/locales/ that is loaded only when picked; a key a table lacks reads in English. Level names and tips, chapter text, materials, vehicles and scenes keep their English in the data files and are looked up by id (level.12.name), so translating never touches what the tuner fingerprints. Numbers, money (always US dollars), percentages and lists are formatted by Intl for the language, and plurals follow Intl.PluralRules, so Polish and Russian get their four forms.
The game follows the device's language, and Settings can pick another; Android 13 and later also offer it in the system's per-app language setting. Level text from the level editor stays as its maker wrote it.
All eight translations are machine-drafted, waiting for native speakers. tests/i18n.test.ts checks that every key exists in English, that placeholders and markup match, that each plural has the forms its language needs, and that tight labels fit their caps. npm run screens -- --narrow --lang <code> screenshots every screen on a 360 px phone in that language and fails on text that runs out of its box; text that ends in an ellipsis by design is only listed. npm run games-ids -- --names <dir> writes the Play Games achievement and leaderboard names in each language, for the Play Console's translations.
src/physics/world.tsis a small-step XPBD solver. Members are compliant distance constraints; a ram's rest length follows the drawbridge's opening. Stress is axial force over the member's capacity; compression capacity falls off with length, like buckling. A member breaks when its smoothed stress reaches 100%. A deck piece that breaks ends the run as a collapse even if the traffic could still get across; a part that breaks off, or a concrete anchor that tears loose, without bringing the deck down is no collapse, though it counts as 100% peak stress. Consecutive heavy-deck pieces also get a bending constraint that yields past a small force, like a hinge, so a heavy deck shares load between hangers but can't bridge a gap by bending alone. Each vehicle is a few particles in the same world, and its wheel contacts push load into the road members' nodes.buildWorld(src/physics/build.ts) turns a design into that world. The drawbridge timeline (open, ship, close, drive) and the ship's collision check live inTestRun(src/physics/testrun.ts), and floods, wind, quakes and marching feet insrc/physics/hazards.ts. A deck piece that a vehicle over its weight rating touches is driven to breaking stress within a few frames. A concrete anchor is a fixed joint that sums the pull of its members each step and compares it with what its weight, friction and the soil can hold (BLOCKinsrc/physics/materials.ts); past that it becomes a heavy free particle that drags over the bank. A mast is a free joint on top of one very stiff strut from a fixed hinge.src/levels.tsdefines each level as authored: geometry, vehicle, materials, tip. Its budget, target and bonus goal, plus any geometry the optimizer was allowed to adjust, come fromsrc/levels.res(see below).src/chapters.tsgroups levels into chapters and holds the unlock rules. Level ids are stable, and chapters list them in play order, so saved progress survives reordering.src/maker.tsis the level editor's logic: editing aLevelDefin place, checking it can be played, and reading and writing it as JSON.src/brief.tswrites the level briefing and judges the bonus goal live.src/physics/stresslog.tssamples every member's stress ten times a second during a test drive, for the stress graph (src/ui/graph.ts).src/rules.tsholds the build rules (reach, bounds, channels, overlaps, budget). The editor, the tests and the optimizer all check designs with it.src/solutions.tsholds a hand-made design per level: the intended answer. The optimizer starts from it, and budgets always leave room for it.tests/tuning.test.tsdrives the real vehicle over every level's tuned reference and bonus designs and over the hand-made ones, checks each level's shortcuts still fail, and fails when a level's tuning is stale.tests/physics.test.tscovers the solver itself: bending, weight ratings, stability, cables, and that a bare heavy deck crosses no level.tests/storage.test.tsruns the same behavior contract against both storage backends. The SQLite one runs against a real HTTP server on an in-memory database.- Rendering is Canvas 2D (
src/render/).src/render/themes.tsdefines each chapter's sky, land, water, trees, weather and time of day. The UI is DOM overlays (index.html,src/ui/ui.ts). All sound is synthesized with WebAudio (src/audio.ts).
npm run tune is an offline optimizer (tools/tune/) that decides each level's numbers, so they follow from what can actually be built rather than from guesswork:
- Search. For each level it looks for the cheapest design that crosses with peak stress at or below 92% and without breaking a single member. A genetic algorithm searches a structure grammar built from the level's geometry: deck spans, trusses over or under them, struts, posts and trestles from low anchors, hangers from high anchors, and sagging main cables between them. On drawbridge levels a support may be a ram, and where dampers are offered, a damper. Where a level has mud and piles, the even meters along the mud are supports too, each a pile if used. On levels with concrete anchors, mast tops count as high anchors, braced towers may stand on pairs of pier bolts, and each outermost mast or tower gets up to two backstays to blocks a chosen distance back, the first carrying its side span on hangers. Each result, and the hand-made design, is then polished by local search that removes members and swaps materials one at a time.
- Numbers. The target is the best cost times a slack that shrinks by chapter (by default ×1.3 down to ×1.18), and never less than what the hand-made design costs, so the intended answer earns the cost star. The budget is the best cost times ×2.0 down to ×1.55, at least $1,000 over the target, and never less than the hand-made design plus 35%. The search's best is hard for a person to match, so both leave real room. These slacks are the difficulty settings (see below).
- Bonus goal. It keeps the level's current kind of goal if it can: a stress cap the reference misses, a material the level can do without, or a parts cap. Each search also starts from the reference itself: upgraded member by member for a stress cap, stripped for a parts cap, or with the banned material swapped for its neighbor. Each goal comes with a design proving it can be met. A geometry whose bonus nothing proves is still usable; the optimizer only prefers another one that has both.
- Intent.
tools/tune/intents.tssays what each level is about.requiresnames materials the level is built around, or its piles: the best design without them must fail or blow the budget, which is how the cable levels keep their cables and the mud levels their piles.shortcutsare specific designs that must fail.minRoomasks that enough one-step variations of the best design still cross, so there's more than one way over.paramsandshapemark geometry the optimizer may change. Level 30's channel clearance, pylon positions and pylon heights are tried in order of preference until everything holds.
npm run tuner opens the tuner page on the dev server (/tuner; it is never part of a build). It lists every level with its status (tuned, stale, needs a re-tune), best and hand-made cost, star target, budget and bonus goal. Tick levels, pick an effort or a time budget, and start a run; the page shows progress, time left and the tuner's output live, can stop a run (finished levels are kept), and shows the matching command line.
From the command line, npm run tune takes the same options. --levels 7,30 tunes only those levels. --effort quick|normal|thorough|max trades time for search depth (the default is normal). --time <minutes> measures this machine and picks the most thorough effort expected to fit. --estimate just prints how long each effort would take. A live status line shows the stage, simulations per second, elapsed time and time left.
The level editor's Auto-tune (src/autotune.ts) runs the same search and polishing in the browser, on Web Workers (src/tunepool.ts), for a few seconds instead of minutes, with no intents, no hand-made design and no geometry changes. Its difficulties are fixed slacks over what the short search found.
Each test drive runs 30 seconds, or longer when a slow convoy needs it to get its tail across, such as marching squads (testSeconds in src/physics/testrun.ts). It runs designs in parallel on worker threads and restarts any worker that crashes or hangs. Every finished level is saved right away. An interrupted run continues where it stopped, and a level is only re-tuned when its inputs change (--fresh forces it). Bump PHYSICS_VERSION in src/physics/world.ts when a change alters how bridges behave; the tests then report every level as stale until it's tuned again.
npm run verify (tools/verify/verify.ts) checks the quality of every level in a couple of minutes, where a tuning run takes hours. It never optimizes. It test drives a fixed set of probe designs on each level, the tuner's structured starting designs (the stock answers a player tries first) plus a seeded handful of random ones from the same grammar, along with the tuned reference, the ghost and the level's shortcuts. It fails a level whose tuning is stale, whose reference no longer crosses on target, whose ghost doesn't cross when it should, or which a probe crosses within budget without what the level teaches: its required materials, or piles on its mud. It warns when a probe with a safe margin undercuts the reference, so the targets are too lenient; when the star target sits more than 60% over the reference because the hand-made design costs that much more; when the bonus goal comes free with the reference; and when a hazard (wind, quake, march, flood or brake stop) hardly matters, because it barely raises the reference's peak and breaks none of the probes and shortcuts that hold without it. It also checks tips, mud, piles and test drive lengths, and that each chapter's levels take as many parts as its card says. --levels 7,30 checks only those, --probes <n> sets the random probes per check (default 24), and probe designs that fail a check are saved in tools/verify/out to load in the game. It exits with 1 when a level fails, so it can gate a commit.
The slacks are editable without touching code. On the tuner page they read as room over the best design: a star-target room of +30% means the star target is 30% above the cheapest design the search found, and more room is easier. Each chapter has a star-target and a budget room, each level can have its own in place of its chapter's, and there's the budget's spare over the hand-made design. Every chapter shows a rating in words (the cost star from Easy to Very hard, the budget from Generous to Tight, thresholds in RATINGS in tools/tune/difficulty.ts) from the room its levels really get, an example level in dollars, and a difficulty curve plots every level's room in play order. The page previews every level's new numbers as you drag. Saving re-derives all of them from the stored search results at once, with no new search, and re-checks each level's shortcuts against its new budget. A level whose checks no longer hold is marked for a re-tune. The "requires" checks reuse designs found under the old budget, so after a large budget increase it's worth re-tuning the cable levels with --fresh.
Everything the tuner decides lives in one committed file, src/levels.res: the difficulty, each level's numbers and tuned geometry, the proof designs the tests drive, and the full search results that resuming and re-deriving need. It's gzip-packed JSON (about 10 kB), marked binary in .gitattributes, so a tuning run is a one-file change instead of thousands of JSON lines. A Vite plugin (vite.config.ts) and a Node loader hook (tools/tune/res-register.mjs) both import it as a module; the game only uses the numbers, and the build tree-shakes the rest away. Readable copies (tuned.json, designs.json, state.json, difficulty.json, report.md) are written to the git-ignored tools/tune/results/ whenever the resource changes; npm run tune:unpack regenerates them after a pull.
Chapters. Eighty-four levels in twelve chapters of seven, ordered by difficulty; each topic's levels stay together where they can, its ghost first. The first chapter is open from the start. A chapter opens once every level of the previous one has been crossed, and inside a chapter the levels open one after another. The chapters screen has two views, and remembers the one you used last. Map lays every level out as a node on one winding road, chapter by chapter under its heading, with the stars won on each, the road paved as far as you've crossed, the next level lit, and the locked ones ahead; it opens scrolled to the next level. Book shows the chapter cards six to a page, turned with the arrows, a swipe, the dots, or ←/→. Level codes such as 3-2 changed when the chapters were regrouped from five levels to seven, and the challenge boards started afresh; stars, records and saved designs are kept by level id, so they carried over. Continue on the title screen takes you to the first level you haven't crossed yet.
Level score. A crossing counts only if the deck holds: a deck piece that gives way on the way is a collapse, even if the vehicle makes it. A part that breaks off without bringing the deck down costs the safety star and is noted in the engineer's review, but the crossing counts. A failed run is marked COLLAPSED when something on the bridge broke, and FAILED otherwise, such as a vehicle stuck or a squad thrown. 500 for crossing, plus up to 1,000 for money left in the budget, plus up to 400 for safety (low peak stress), plus 250 for the bonus goal. You earn stars for crossing, for building at or under the target cost, and for a peak stress below 75%.
Bonus goals. Every level also has a bonus goal (✦), shown under the level name and on its card: build for less than a set amount, keep peak stress under a limit, use a limited number of parts (a split beam counts once), or build without one material. Meeting it earns the bonus star and 250 points. Free play has no lives: collapse, tweak and try again. Your saved design for each level comes back when you return.
Career. Your career score is the sum of your best score on every level. Improving any level raises it, so you never need to replay from the start.
Chapter challenges. A finished chapter unlocks its challenge: all seven levels in a row, from blank designs, with three lives. Each collapse costs a life, and a 3-star crossing earns one back. The run's total goes on that chapter's challenge board, even if you quit part-way; quitting asks first, and says how many points will be banked.
Engineer's review. After a crossing the result card says how the cost compares with the best known design (the optimizer's or the hand-made one, whichever is cheaper), counts parts that never carried more than 20% of their strength, and steel that wood could carry under 75% load, with what each costs. IMPROVE marks them on the blueprint until you change the bridge. The briefing offers hints: each one shows one more member of the best known design as a ghost to trace, and takes 100 points off every score on that level from then on (a crossing always keeps its 500). Challenges have no hints.
Leaderboards have four tabs:
- Career: every engineer, ranked by career score.
- Chapter: ranked by best scores on one chapter's seven levels.
- Levels: the record holder on each level of a chapter, with what the record bridge cost and its peak stress, and your own best when someone else holds it. Once you've crossed a level yourself, Watch it drives the record bridge across, keeping your own blueprint.
- Challenges: the top challenge runs on one chapter.
Bridge Builder is source-available under the PolyForm Noncommercial License 1.0.0. You may read, run, change and share the code for any noncommercial purpose: personal play, study, hobby projects, teaching and research. Selling it, or publishing a version with ads or in-app purchases, is not allowed. For any other use, open an issue on GitHub to ask. Anyone who passes the code on must include the license and its Required Notice: line.
The license covers the code. It doesn't let you use the name "Bridge Builder" or the app icon to present your own version as this game, in an app store or anywhere else.
The libraries the game uses keep their own licenses (MIT or Apache 2.0). Versions published before October 12, 2026 were released under the MIT License, and those copies stay under it.