TETRIS// HOW TO PLAY
Solo Battle Leaderboard

How to Play

Tetris Terminal is a browser Tetris styled like a financial-data terminal. Pieces fall through a 10×20 playfield; clear rows by filling them. Play solo, in 1v1 battles, or with XNT stakes and TETRIS-token rewards.

What you're playing

What sets it apart:

tipYou can play every mode for free. Stakes are entirely optional — wallet connection just lets you appear on the leaderboard.

Modes

ModeURLDescription
Soloindex.htmlSingle-player. Score submits to the leaderboard if your wallet is connected.
Battle (lobby)battle.htmlOpen queue + invite codes + stake selector.
Matchmatch.html?code=XSplit-screen 1v1 with live garbage exchange.
Watchwatch.html?code=XRead-only spectator. No wallet required.
Replayreplay.html?code=XScrub through a finished match.
Leaderboardsleaderboard.htmlTop by solo score and battle wins.

Match codes are 5-char Crockford base32 — no ambiguous characters (0/1/I/L/O/U). 33M possible codes.

Controls

ActionKeyboardTouchOn-screen
Move left / right swipe ← / →◀ ▶
Soft drop (1 cell)swipe ↓
Rotatetap board
Hard drop (lock now)Space
Hold pieceH
Defensive toggle (battle)DDEF chip
Toggle music

On-screen buttons exist for touch devices but work in any browser. Keyboard input is debounced 70 ms so a held key doesn't fire faster than the engine ticks.

Solo play

Pieces and the 7-bag

Standard tetrominoes (I, O, T, J, L, S, Z) drawn from a 7-bag shuffler. Every 7 consecutive pieces contain exactly one of each, in a random order. You'll never see a long S/Z drought or an I-piece famine longer than 13 pieces apart.

NEXT and HELD

Scoring

Lines cleared at onceBase pointsNotes
Single100Self-defense only in battle
Double300Sends 1 garbage row in battle
Triple500Sends 2 garbage rows in battle
Tetris (4)800Sends 4 garbage rows in battle

Per-lock score:

final = base * current_level + combo * 50

Combo increments on every consecutive line-clearing lock; resets to 0 on a non-clearing lock. Level rises by 1 every 10 lines, capped at 15. Each level shortens the auto-fall interval (800 ms at L1 down to 20 ms at L15).

Audio

All sounds are synthesized in-browser via Web Audio — no audio files to download.

noteBrowser autoplay policy prevents music from starting on page load. Audio wakes on the first pointer or key event anywhere on the page.

Battle mode

How to start a match

  1. Open queue. On battle.html click FIND MATCH. The server pairs the two longest-waiting wallets; both clients navigate to the same match.html session — one as p1, the other as p2.
  2. Invite. On battle.html click CREATE. You get a 5-char code and a shareable link. Send it; whoever opens that URL becomes p2.

Match lifecycle

free:   create -> lobby -> playing -> finished / abandoned
staked: create -> awaiting_deposit -> lobby
              -> (p2 joins) awaiting_deposit -> playing
              -> finished / abandoned
StatusMeaning
awaiting_depositStake match awaiting either creator or joiner deposit.
lobbyCreated, waiting for opponent. Staked: p1 deposit confirmed.
playingBoth players in. Active gameplay.
finishedMatch ended via topout or forfeit. winner field set.
abandonedCancelled, swept, or otherwise terminated without a clean result.

Each player gets their own piece sequence

Both players use the 7-bag distribution, but their seeds differ — and each player's seed is only sent to that player, never to the opponent or any spectator. Shared-seed mode was tried first and discarded — it created compounding bad luck (both stuck in S/Z droughts together) and let players predict each other's queue.

Garbage rows — the core battle mechanic

When you clear lines, the surplus is sent to your opponent as gray rows with one random hole column. They push the opponent's stack up; if any of their blocks get pushed off the top of the playfield, they top out and lose.

Damage table

Lines you clearedGarbage you sendScore
1 (single)0100
2 (double)1300
3 (triple)2500
4 (tetris)4800

Singles are free score but zero damage. The whole game pressures you toward setting up tetrises.

The offset rule

Incoming garbage doesn't apply instantly — it sits in a queue. Your own clears cancel pending incoming garbage before sending any out:

You have 3 incoming garbage queued.
You clear a tetris (would send 4).
  -> 3 cancel against your own queue (queue now 0).
  -> 1 surplus is sent to opponent.

This is what makes Tetris-vs-Tetris feel back-and-forth: a fast counter completely neutralizes an incoming attack and still hits back with leftover damage.

When garbage actually lands

warnGarbage does not apply the moment it arrives. It lands on your next lock that does not clear lines. If you keep clearing every piece, the queue keeps offsetting and never lands. The moment you place a piece without clearing, the entire pending queue dumps on you at once.

Defensive mode

Tap DEF (or press D) to enter defensive mode. The button glows green; a DEF chip appears next to your handle (and opponents see it too).

While defensive:

A JUNK -1 banner flashes on each junk-row removal. If your stack is clean (no junk), defensive clears nothing extra.

tipWhen to go defensive: you're buried under garbage and need to dig.
When to stay offensive: opponent has a tall stack, your queue is short, and you can land a tetris within a few pieces.

Match end

XNT stakes

A staked battle puts XNT (X1 mainnet native token) into escrow from both players. Winner takes the full pot (2× stake).

Custody model

A single server-controlled keypair holds in-flight stakes. Its public key is logged on the server and shown in match snapshots so clients know where to deposit.

warnThere is no on-chain smart contract. Players trust the server operator to honor payouts. A trustless variant (per-match PDA via Anchor program) is a possible upgrade.

Lifecycle

StepFree matchStaked match
1. CreateStatus → lobbyStatus → awaiting_deposit
2. Creator depositsWallet signs transfer(stake) → escrow; server verifies on chain; status → lobby
3. JoinerClicks JOIN → playingClicks JOIN → awaiting_deposit → deposit → playing
4. Match endResult broadcastResult broadcast + escrow signs payout transfer to winner master

Unit: lamports (1 lamport = 10⁻⁹ XNT). UI accepts decimal XNT and converts.
Per-match cap: 100 XNT.

Why deposits use the master wallet (popup)

The session keypair is derivable from one master signature and lives in browser sessionStorage. That's fine for signing high-rate game events. It is not suitable for moving real money. So deposits use the master wallet — you explicitly approve each fund movement in your wallet's popup.

Orphan deposits

If a client signs a deposit but the /confirm-deposit POST never lands (tab closed, network blip), funds hit escrow with nothing crediting them. A reconciliation sweep runs every 5 minutes:

  1. Fetches the last ~200 inbound escrow signatures from X1.
  2. Skips any already credited to a match.
  3. Records new ones as pending in the escrow_orphans table.
  4. Refunds anything still pending after a 10-minute grace window, back to the original sender.

Safety caps: max 5 refunds per sweep, 20 per rolling hour.

TETRIS reward token

When the server has TETRIS_REWARD_MINT configured, match results distribute a Token-2022 SPL reward (symbol TETRIS, 6 decimals) to both players, proportional to their score (1 TETRIS per point at current K). The amount per slot is recorded on the match row and visible in your wallet panel + the match result overlay.

noteThe token reward is independent of XNT stakes. You can play free matches and still earn TETRIS — the reward is a play-to-earn signal that anchors leaderboard rank, not a competitive prize pool.

Spectating

Anyone can open watch.html?code=XXXXX for any match — live or finished. No wallet needed. Read-only view of both boards side-by-side, with score/lines/level for each, plus DOUBLE/TRIPLE/TETRIS banners on clears and garbage-incoming banners. Spectators also hear SFX for both sides.

Wallet & session keys

A wallet has two parts:

Derivation

master.signMessage("tetris-v0.1")  ->  64-byte signature
session_seed    = signature[0..32]
session_keypair = Ed25519.fromSeed(session_seed)

Same master + same message = same session keypair, every device, every browser. Reproducible.

Why two keys

Game actions (score posts, state pushes, line-clear claims) happen many times per second. Showing a wallet popup for each one is unworkable. So we sign once with the master to derive the session key, then use the session for all subsequent silent signing.

Strategy notes

basicsStack flat. Random gaps under future S/Z rotations destroy your odds. Keep the well at one consistent column and feed everything else to keep the surface low.
battleTrade tetrises, not singles. A single deals zero damage. A double trades 1 row. A tetris trades 4. Trading tetrises is the only way to consistently win against an equally skilled opponent.
offsetCounter, don't dodge. If you see incoming garbage, your best response is usually to immediately clear lines back. As long as your clears land before the next non-clearing lock, the offset rule cancels the incoming queue.
defensiveUse DEF tactically, not strategically. If you're already buried in garbage and the queue keeps growing, defensive mode is your dig-out tool. Get back to a flat stack, then toggle off and play offensively again.
holdHold the I-piece for the tetris. If you've been building a 4-row well, the I-piece is the only one that completes it. Park it in the hold slot until your well is ready, then swap.
Further reading — repo github.com/jacklevin74/x1-tetris: README.md (overview), GAMEPLAY.md (full protocol), ARCHITECTURE.md (code structure).