Search by

sugarcraft / candy-tetris

detain

Tetris clone built on the SugarCraft stack — SugarCraft runtime, SugarStyle styling. Standard SRS rules (7-bag RNG, ghost piece, hard drop, hold, level/speed curve, line-clear scoring). A real demo of the runtime, not a toy.

Package info

github.com/sugarcraft/candy-tetris

Documentation

Type:project

pkg:composer/sugarcraft/candy-tetris

Statistics

Installs: 0

Dependents: 1

Suggesters: 0

Stars: 1

Open Issues: 0

dev-master 2026-10-24 00:12 UTC

This package is auto-updated.

Last update: 2026-10-24 06:36:23 UTC


README

candy-tetris

CandyTetris

CI codecov Packagist Version License PHP

demo

Tetris built on the SugarCraft stack. SugarCraft runtime, SugarStyle for the rounded borders and per-piece colours, deterministic 7-bag RNG, ghost piece, hard drop, hold, level-driven gravity ramp, line-clear scoring.

Run it

composer install
./bin/candy-tetris         # Single player mode
./bin/candy-tetris -v      # VS Computer mode
./bin/candy-tetris --vs    # VS Computer mode (long form)
./bin/candy-tetris -h      # Usage screen (also --help)

Needs a colour-capable TTY of at least ~40 columns by 22 rows; empty cells render as · so the grid stays readable even without colour. Any flag other than the ones above is refused with a usage note on stderr and exit code 1.

VS Computer Mode

Compete against an AI opponent in split-screen mode. When you clear lines, garbage rows are sent to the computer. When the computer clears lines, garbage rows are sent to you. Last player standing wins!

vs-computer demo

VS Mode Controls

Key Action
← / → Move left / right
↑ / x Rotate clockwise
z Rotate counter-cw
↓ Soft drop
Space Hard drop
c Hold piece
p Pause / resume
q Quit

Hold works on your field, but the VS sidebar does not paint a hold card — the swap only becomes visible when the piece enters play.

The computer opponent uses weighted heuristics (board height, holes, gaps, lines) to make optimal moves. If both boards top out on the same tick, the computer wins: the player's field is checked first.

Controls

Key Action
← / → Move left / right
↑ / x Rotate clockwise
z Rotate counter-cw
↓ Soft drop
Space Hard drop
c Hold piece
p Pause / resume
r New game (after game over only)
q Quit

Game over

Top-out ends the game and paints a banner with the final score. Press r to start a completely new game — the score does not carry over (it embeds the level, so a restarted board falls at level-0 gravity again). q quits.

Hold

Press c to stash the falling piece into the hold slot; press it again on the next piece to swap it back in. You can only hold once per drop — the slot dims until the current piece locks.

hold demo

Lock delay

A grounded piece does not lock instantly — you get a real-time window (~500 ms by default, independent of the gravity speed, so it stays a constant half-second even at level 29) to slide or rotate it before it settles. The binary arms it via Game::startWithLockDelay(). Every successful move or rotation while grounded re-arms the full window, up to the SRS-style 15-reset cap — once the budget is spent the open window locks on expiry and further inputs no longer extend it. Moving the piece back off a surface closes the window and hands timing back to gravity; a hard drop bypasses it and locks immediately. Pausing extends the window instead of locking a frozen game.

Scoring

Line-clear scoring

Lines cleared Base points × (level + 1)
1 40
2 100
3 300
4 (Tetris) 1200

Level rises every 10 lines. Gravity speeds up at every level — by level 9 pieces fall every 6 frames, by level 29+ they fall every frame. The frame-rate-agnostic Score::framesPerRow() is what the gravity tick consults.

T-Spin scoring

When the last successful input before a T piece locks was a rotation (moves, drops and the fall itself reset that flag — settling under gravity keeps it) and two or more of the piece's four diagonal corner cells are already filled (walls count as filled), it scores as a T-Spin:

Type Base points
T-Spin 400 × (level + 1)
T-Spin Mini 100 × (level + 1)

T-Spin Mini is detected when exactly two corners are filled and they are the front pair — the top pair at rotation 0, the bottom pair at rotation 2. At the side-facing rotations (R/L) the front pair is undefined, so a two-corner spin there always scores as a full T-Spin.

Last-input rule: A T-Spin is active when the piece's last successful action before locking was a rotation (Game::$lastActionWasRotation, set by rotate, cleared by move/soft drop/hard drop — gravity steps leave it alone, so a rotate-then-settle spin is legal), AND at least two of the four diagonal corner cells around the T are occupied (wall/out-of-bounds = occupied). With exactly two filled corners and they are the front pair, it scores as a mini. See Scoring\TSpin::detect().

Back-to-Back (B2B) bonus

Consecutive Tetris clears or full T-Spins (non-mini) carry a 1.5× multiplier on the line-clear base points:

  • 4-line Tetris after a prior Tetris → 1200 × 1.5 × (level + 1)
  • Full T-Spin after a prior full T-Spin → 400 × 1.5 × (level + 1)

B2B resets when a 1–3 line clear or a T-Spin Mini breaks the streak.

Combo bonus

Consecutive line clears (regardless of type) build a combo counter — each combo step adds combo × 10 × (level + 1) bonus points. The counter resets to 0 on any clean (zero-line) piece placement.

Perfect clear bonus

When all lines are cleared at once and the board becomes completely empty, an additional +5000 × (level + 1) bonus is awarded.

Where the seams are

  • Bag / RNG: Bag takes an injectable pick closure; Game::start(?Bag) and Game::startWithLockDelay(?Bag) thread it through, so tests (and any deterministic replay tool) pin the sequence.
  • Kick table: src/Rotation/SrsKickTable.php transcribes the published SRS offsets verbatim (spec +y = up); the only sign conversion to board coordinates lives at Piece::rotationsWithKicks.
  • Scoring: src/Scoring/TSpin.php and src/Scoring/Score.php are pure value logic, independent of the renderer.
  • Modes: VS is a second Model (VsGame) wrapping two Game instances — copy that pattern for a third mode rather than branching inside Game.
  • Renderer: Renderer::render() is a pure static view function (state → ANSI string); it never mutates the game.

Shared foundations

The single-player playfield is painted into a canonical sugar-cellbuffer Buffer cell grid (10×20 visible); per-tetromino background style and ghost-piece foreground style are applied per-cell. The hold/next/score sidebar and the VS split-screen panels are sugar-style cards joined with its Layout (Layout::joinVertical / Layout::joinHorizontal), composited alongside the buffered playfield. Snapshot tests via sugar-testing pin canonical game states (starting board, T-spin completion, pause overlay).