3D Tetris
Classic Tetris, wrapped around a cylinder. Five game modes, ring-clear algorithm, accessible from keyboard alone.

TL;DR
- Cylindrical grid coordinate system.
- Instanced rendering for 500+ blocks at 60fps.
- Five game-loop variants on one engine.
- Keyboard-only playable; SR-described scene.
- Atomic React architecture (atoms / molecules / organisms).
Architecture
Pure game logic stays separate from rendering. Same engine, five game loops.
Problem
Classic Tetris is a solved game. Wrapping it around a cylinder breaks every assumption — pieces wrap around the edges, the camera never sees the whole board at once, and 'clear a row' becomes 'clear a ring'. The hard part isn't the rendering, it's keeping the game readable.
Approach
Built a cylindrical grid coordinate system with a ring-clear algorithm. Rendered with instanced Three.js geometry so 500+ blocks can animate at 60fps. Five game-loop variants (Survival, Daily, Mission, Combo, Risk/Reward) share one core engine. Accessible from keyboard alone with an SR-described 3D scene.
Deep dive
Cylindrical coordinates
Cells are addressed by (ring, slot, height). Movement left/right wraps modulo the ring slot count. The win condition for a 'line clear' is when every slot in a single (height, ring) pair is filled — a ring clear. This sounds tiny, but it changes pacing: there are always exits.
export const SLOTS_PER_RING = 12;
export const wrap = (slot: number) =>
((slot % SLOTS_PER_RING) + SLOTS_PER_RING) % SLOTS_PER_RING;
export function ringIsFull(grid: Grid, height: number) {
for (let slot = 0; slot < SLOTS_PER_RING; slot++) {
if (!grid.cell(slot, height)) return false;
}
return true;
}
export function clearFullRings(grid: Grid): number {
let cleared = 0;
for (let h = 0; h < grid.height; h++) {
if (ringIsFull(grid, h)) {
grid.collapseRing(h);
cleared++;
h--;
}
}
return cleared;
}Instanced rendering
Each cube isn't a mesh; it's an instance of one mesh with a per-instance transform matrix. Adding or removing a block updates one entry in a buffer. With 500+ blocks visible on a busy board, this is the difference between 60fps and 12fps on a Pixel 6.
Five modes, one engine
Each game-loop variant is a small strategy object: 'how do we score?', 'when do we end?', 'do power-ups exist?'. The engine doesn't know which mode is active. This kept the surface area small enough to keep iterating on game feel without a refactor.
Outcome
Prototype with extensive design docs. Demonstrates that 3D-native gameplay loops can stay accessible if you design the input model first.
Related


