Lines - A 3D Take on the Classic Color Lines (Lines 98)

Here’s my 3D rendering of the classic puzzle game Color Lines (Lines 98) in Three.js. It’s the marble five-in-a-row game many will remember from older Windows machines. You clear lines of 5+ same-coloured balls before the board fills up. I wanted to see if the glossy-marble presentation from my recent Abalone build could carry a faster, more casual game.

My goals were:

  • A tactile, physically convincing presentation
  • Keep the fast pattern-reading the game lives on, despite rendering in 3D
  • High performance (instanced marbles, single-source-of-truth board state) across devices
  • As always, single file. No build, no framework, no bundler.

CodePen | GitHub Pages | Source

A design decision worth calling out: the camera is locked orthographic at a shallow tilt. A perspective camera you could orbit would foreshorten the grid and make diagonal lines ambiguous, which kills the snap pattern-recognition the whole game depends on.

The board geometry is generated procedurally, with each cell a shallow spherical dish carved into a solid top (no through-holes), so the wood grain and lighting stay consistent whether a cell is full or empty. Movement uses a BFS over the empty cells. A marble can only slide where there’s a clear path, and reachable squares light up on selection.

And a novel thing for my demos: all the audio is synthesized live with the Web Audio API. This means: no sound files, it ships as one contained HTML.

Thanks for taking a look.

3 Likes

I like it, simple and fun.

1 Like

Quite addictive!

I do not understand some of the rules. The left arrow is a possible move, while the right arrow is forbidden move.

1 Like

@PavelBoytchev I’m glad you like it!

This one is a very sneaky bug, and I’m glad you told me. I looked over the BFS seems textbook… So I think the root-cause was that marbles could retain a tiny bit of residual velocity after being placed. Because the dish surface isn’t perfectly flat, that leftover motion was enough to drift a marble slightly off‑center. Once that happened, worldToCell() could round it into the wrong logical cell, which made the pathfinder think a space was occupied when it wasn’t.

I now snap each marble to the exact cell center and zero out its velocity inside claim(), so physics can’t nudge it afterward. Fix is deployed. Fingers crossed. Let me know if you spot this again, please.

1 Like