Skip to content
frgarciamesPublic

About

Tetris-style game built with Remix 3

Topics

Resources

Stars

10 stars

Watchers

0 watching

Forks

Latest commit

ย 

History

21 Commits

Folders and files

Repository files navigation

๐ŸŸฆ Retris

A competitive line-sprint Tetris-style with accounts, server-verified times, and full replays.

Race the clock to clear your target lines, then watch any run back frame-for-frame. Every leaderboard time is recomputed on the server from the recorded inputs โ€” so the ranking is honest by construction.

Built on Remix 3 (beta) ยท powered by Node's built-in SQLite in dev and Turso (libSQL) in production.


โœจ Highlights

  • ๐ŸŽฎ Modern guideline gameplay โ€” 7-bag randomizer, SRS rotation with wall kicks, ghost piece, hold, a 3-piece next queue, soft/hard drop, and DAS/ARR auto-shift.
  • ๐Ÿ Line-sprint modes โ€” clear 40 Lines or 20 Lines as fast as you can. Modes live in a registry and are trivial to extend.
  • ๐Ÿ” Accounts โ€” username + password auth, hashed with scrypt (node:crypto). No third-party services, no secrets to provision in dev.
  • ๐ŸŽž๏ธ Replays โ€” every finished run is stored and replayable with play / pause / scrub.
  • ๐Ÿฅ‡ Honest leaderboards โ€” the server re-simulates each submission to derive the canonical time and line count. Client-reported times are never trusted.
  • ๐Ÿงฉ One engine, three jobs โ€” the same deterministic core powers live play, server verification, and replay.
  • ๐ŸŽจ Themes โ€” switch between Midnight, Aesthetic, Futuristic, and Retro from the header; your choice is remembered.

๐Ÿง  How it works โ€” the deterministic engine

The whole game is reproducible from a single 32-bit seed plus the list of inputs, each tagged with the tick it happened on. The simulation is a pure, DOM-free, fixed-timestep (60 Hz) state machine: given the same seed and inputs, it always produces the same board, the same line count, and the same finishing time.

That one property is what makes everything else simple:

                      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
   seed + your keys โ†’ โ”‚   app/game/engine.ts (pure)   โ”‚ โ† deterministic, 60 Hz
                      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                         โ”‚            โ”‚            โ”‚
              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜            โ”‚            โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
              โ–ผ                       โ–ผ                       โ–ผ
        ๐Ÿ•น๏ธ  PLAY                โœ…  VERIFY                ๐ŸŽž๏ธ  REPLAY
   browser runs it live,   server re-runs the same   viewer re-runs it in
   recording {tick,action} inputs to compute the     playback, scrubbable
   for each input          authoritative time
  • Time is derived, not measured โ€” duration = finishingTick ร— (1000 / 60) ms. It can't drift between machines.
  • A replay is tiny โ€” just seed + actions[], not a stream of board states.
  • Cheat-resistance is free โ€” to fake a fast time you'd have to submit inputs that actually clear the lines that fast. The server replays them and rejects anything that doesn't reach the goal.

๐Ÿ› ๏ธ Tech stack

Layer Choice
Framework Remix 3 (beta) โ€” server-first, Web-API based, its own UI runtime (not React)
Runtime Node โ‰ฅ 24.3 with the experimental built-in node:sqlite
Database SQLite via remix/data-table + hand-written SQL migrations โ€” a local node:sqlite file in dev, Turso (libSQL embedded replica via libsql) in production
Auth Session cookies + node:crypto scrypt password hashing
Styling css() mixins (CSS-in-JS, server-rendered, adopted as a stylesheet)
Package mgr pnpm

The only runtime dependencies are remix itself and libsql (the Turso/libSQL client, used only when a Turso URL is configured) โ€” in plain local dev the database driver and password hashing both come from the Node standard library.


๐Ÿš€ Getting started

Prerequisites

  • Node โ‰ฅ 24.3 (for built-in node:sqlite)
  • pnpm (npm i -g pnpm)

Install & run

pnpm install     # install dependencies
pnpm dev         # start the dev server (auto-restart on change)

Then open http://localhost:44100.

Migrations run automatically on startup, creating db/retris.sqlite on first boot. Sign up, hit Play, and clear some lines.

Environment variables

Variable Default Notes
SESSION_SECRET insecure dev secret Used to sign session cookies. Required in production (the app refuses to boot without it).
ADMIN_USERNAME admin@example.test (dev/test) Username of the single admin account. Required in production (the app refuses to boot without it); dev/test fall back to admin@example.test.
PORT 44100 HTTP port.
NODE_ENV development production enables secure cookies + minification; test uses in-memory DB & sessions.
TURSO_DATABASE_URL (unset) When set, the app reads/writes through a Turso libSQL embedded replica synced from this primary instead of the local SQLite file. Leave unset for a plain local db/retris.sqlite.
TURSO_AUTH_TOKEN (unset) Auth token for the Turso primary referenced by TURSO_DATABASE_URL.

๐Ÿ“œ Scripts

Command What it does
pnpm dev Run the server with --watch (restarts on file changes).
pnpm start Run the server once (production-style).
pnpm migrate Apply pending database migrations explicitly.
pnpm test Run the test suite (node:test) โ€” engine, password hashing, HTTP flows.
pnpm typecheck Type-check the whole project with tsc --noEmit.

๐ŸŽฏ Controls

Action Keys
Move left/right โ† โ†’
Soft drop โ†“
Hard drop Space
Rotate CW โ†‘ / X
Rotate CCW Z / Ctrl
Hold C / Shift

๐ŸŽจ Themes

Pick a theme from the swatches in the header โ€” the choice is saved to a cookie and applied on every page (including the game board), with no page-specific work.

Theme Vibe
Midnight Calm slate-blue dark mode (default)
Aesthetic Soft pastel lavender, light
Futuristic Deep-space black with neon teal
Retro Green-phosphor / Game Boy CRT

A theme is just a set of CSS custom properties (--bg, --panel, --accent, โ€ฆ) in app/ui/themes.ts; the shell sets them on its root so they cascade everywhere. Adding one is a single entry there โ€” the switcher and every page pick it up automatically.

๐Ÿ—‚๏ธ Project structure

app/
โ”œโ”€ routes.ts            # Typed URL contract (source of truth for hrefs)
โ”œโ”€ router.ts            # Middleware stack + controller wiring; exports AppContext
โ”œโ”€ assets.ts            # Source-asset server (compiles browser modules)
โ”‚
โ”œโ”€ actions/             # Controllers โ€” return Response objects
โ”‚  โ”œโ”€ controller.tsx    #   home (leaderboards) + assets
โ”‚  โ”œโ”€ auth/             #   login ยท signup ยท logout
โ”‚  โ”œโ”€ play/             #   the play page (requires auth)
โ”‚  โ””โ”€ games/            #   submit (verify + store) ยท show (replay viewer)
โ”‚
โ”œโ”€ game/                # ๐Ÿง  Pure, deterministic, DOM-free engine (shared client+server)
โ”‚  โ”œโ”€ engine.ts         #   tick-based state machine + simulate()
โ”‚  โ”œโ”€ pieces.ts         #   tetromino shapes, SRS rotations & kick tables
โ”‚  โ”œโ”€ rng.ts            #   seeded PRNG + 7-bag
โ”‚  โ””โ”€ modes.ts          #   mode registry (40 Lines, 20 Lines, โ€ฆ)
โ”‚
โ”œโ”€ assets/              # Browser client entries & presentational UI
โ”‚  โ”œโ”€ entry.ts          #   boots the client runtime (run())
โ”‚  โ”œโ”€ game-board.tsx    #   live game: loop, input/DAS-ARR, submit-on-win
โ”‚  โ”œโ”€ replay-player.tsx #   playback with scrubber
โ”‚  โ””โ”€ board.tsx, game-view.tsx
โ”‚
โ”œโ”€ data/                # schema.ts + queries (users, games) + db.ts (sqlite + migrations)
โ”œโ”€ middleware/          # database ยท session ยท auth ยท render
โ”œโ”€ ui/                  # shared cross-route UI (layout, home, auth form, document)
โ””โ”€ utils/               # pure helpers (passwords, time formatting, redirects)

db/migrations/          # hand-written SQL migrations (immutable, checksum-tracked)
public/                 # static files served as-is
server.ts               # Node HTTP adapter

๐Ÿ—„๏ธ Database & migrations

  • In local dev the database lives at db/retris.sqlite (git-ignored) and is opened with Node's built-in node:sqlite.
  • In production, set TURSO_DATABASE_URL (+ TURSO_AUTH_TOKEN) to back the app with Turso. The app opens a libSQL embedded replica (db/replica.sqlite) via the libsql driver: reads hit the local replica instead of a ~200 ms network round-trip per query, writes are delegated to the Turso primary, and the replica re-syncs every 10 s. Everything still flows through the same synchronous sqlite adapter, so the rest of the code is unchanged.
  • Migrations are plain SQL files under db/migrations/<timestamp>_<name>/{up,down}.sql. They are applied automatically on startup and tracked by checksum, so they run exactly once.
  • Two tables: users (id, username, password hash) and games (the stored replays โ€” seed, mode, recorded actions JSON, plus the server-verified duration and line count).

To add a migration, create a new timestamped folder with an up.sql (and optional down.sql); it'll apply on the next boot or via pnpm migrate.


โž• Adding a game mode

Modes are data, not code paths. To add one, register it in app/game/modes.ts:

export const MODES: Record<string, GameMode> = {
  sprint40: { id: "sprint40", label: "40 Lines", goal: { type: "lines", count: 40 } },
  sprint20: { id: "sprint20", label: "20 Lines", goal: { type: "lines", count: 20 } },
  // sprint100: { id: 'sprint100', label: '100 Lines', goal: { type: 'lines', count: 100 } },
};

The engine checks the goal generically, the play page exposes it via /play?mode=<id>, and the home page automatically renders a play button and a leaderboard for every registered mode. The Goal union is ready for new goal kinds (e.g. a timed mode) when you want them.


๐Ÿงช Testing

pnpm test

The suite (Node's built-in test runner) covers the parts that matter most:

  • Engine โ€” determinism (same seed + inputs โ‡’ identical result), 7-bag fairness, line clears, and goal completion across modes.
  • Passwords โ€” scrypt hash/verify round-trips and rejection of bad input.
  • HTTP flows โ€” driving the router with real Requests: signup/login, and submitting a replay so the server re-simulation path (accept a real run, reject an incomplete/tampered one) is exercised end-to-end.

๐ŸŒ Production notes

SESSION_SECRET="$(openssl rand -hex 32)" NODE_ENV=production PORT=8080 pnpm start

In production the app:

  • requires SESSION_SECRET and fails fast without it,
  • marks session cookies Secure and minifies browser assets,
  • persists sessions to tmp/sessions/,
  • uses Turso (libSQL embedded replica) when TURSO_DATABASE_URL / TURSO_AUTH_TOKEN are set, falling back to the local db/retris.sqlite file otherwise.

Built with Remix 3 ยท clear forty, then clear it faster.

About

Tetris-style game built with Remix 3

Topics

Resources

Stars

10 stars

Watchers

0 watching

Forks

Contributors

Languages