sugarcraft / candy-flappy
Flappy-Bird-style terminal game on the SugarCraft stack, inspired by kbrgl/flapioca.
Requires
- php: ^8.3
- sugarcraft/sugar-core: dev-master
- sugarcraft/sugar-spring: dev-master
- sugarcraft/sugar-style: dev-master
Requires (Dev)
- phpunit/phpunit: ^10.0
- sugarcraft/sugar-testing: dev-master
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-05 01:00:36 UTC
README
CandyFlappy
candy-flappy — a Flappy-Bird-style game for the terminal, built on the SugarCraft stack, for PHP 8.3+. The bird's vertical motion is a SugarSpring projectile (gravity + an upward velocity kick on each tap), pipes scroll left at a fixed cell rate, collision is per-cell.
Formerly published as sugarcraft/honey-flap (renamed 2026-10; namespace SugarCraft\Flap\ → SugarCraft\Flappy\). The first run of bin/candy-flappy migrates a pre-rename ~/.config/.honey-flap high-score directory automatically.
Install & run
# From a clone of the monorepo: composer install ./bin/candy-flappy # or: php examples/play.php # As a library dependency (pre-1.0: the package publishes a dev branch only): # composer require sugarcraft/candy-flappy:@dev # Standalone executable (rebuilt on every push to master): wget https://github.com/detain/sugarcraft/releases/download/candy-flappy/dev/candy-flappy.phar php candy-flappy.phar
Keys
| Key | Action |
|---|---|
Space / Enter / ↑ / w |
Flap (the first flap also starts the run) |
r |
Restart — after a crash or mid-flight; lands on the ready card |
q / Esc / Ctrl+C |
Quit |
Other Ctrl-combinations do nothing; Ctrl+C is the only chord the game reads.
The board opens on a ready card: the bird hovers and the world is frozen until your first flap, so launching never drops you into the floor. Quitting mid-flight with a qualifying score settles it onto the leaderboard before the process exits — you do not have to die to keep your best.
Terminal requirements
The framed world is a fixed 62 x 21 cells (Game::MIN_COLS x Game::MIN_ROWS).
- Smaller viewport — a plain "needs a 62x21 terminal" notice replaces the
board and the world freezes (no ticks burn on an unplayable frame). Resize
back and the game resumes in place;
rnever snaps the layout away. TERM=dumb/ no TTY (ColorProfile::NoTty) — candy-flappy refuses to play with a plain message instead of spraying escape codes into a terminal that cannot render them.- ASCII-only terminal (
ColorProfile::Ascii, e.g.NO_COLORon a plain tty) — same layout in plain glyphs:+--+borders,#pipes, no emoji, no colour. - Colour tiers — 16/256-bit/TrueColor palettes degrade through sugar-style; the status hint collapses to a short form before any line could wrap past the window width.
Architecture
| File | Role |
|---|---|
Bird.php |
Wraps a SugarSpring Projectile — gravity pulls it down, flap() resets vertical velocity to a fixed kick, and fall speed is capped at terminal velocity. |
Pipe.php |
Single-column pipe pair with a centred gap. Slides left one cell per tick. |
TickMsg.php |
Frame-tick message fired by the game-tick subscription timer (Game::TICK_SECONDS, 1/30 s exactly). |
Game.php (Model) |
Pure-state world: bird + pipes + score + crashed flag. Injects PRNG closure for deterministic gap placements in tests. |
Renderer.php |
Pure view — single playfield walk, ANSI-styled glyphs, rounded border. |
PipeGenerator.php |
Generates pipes with variable gap height — gap shrinks as score increases, raising difficulty. Gap starts at 6 cells, shrinks by 1 every 5 points, floors at 3. |
The PRNG is injected as a Closure(int $maxInclusive): int so unit tests can pin the pipe layout to a specific sequence — the standard SugarCraft pattern.
Difficulty scaling
The pipe gap height adapts to the player's score:
| Score range | Gap height |
|---|---|
| 0–4 | 6 cells |
| 5–9 | 5 cells |
| 10–14 | 4 cells |
| 15+ | 3 cells |
Gap shrinks by 1 every 5 points, bottoming out at 3 cells to keep the game playable. This is implemented by PipeGenerator::gapHeightForScore() and applied automatically when Game spawns new pipes via PipeGenerator::makePipe().
Bird physics & tuning
The bird's vertical motion is a SugarSpring Projectile in the Y-down convention (positive velocity = falling). Three constants on Bird tune the feel for an 18-row playfield at 30 ticks/sec:
| Constant | Value | Role |
|---|---|---|
Bird::FLAP_KICK |
-10.0 |
Upward velocity (cells/sec) applied by a flap. |
Bird::GRAVITY |
18.0 |
Constant downward acceleration (cells/sec²). |
Bird::TICKS_PER_SEC |
30 |
Simulation rate baked into the projectile's deltaTime. |
Fall speed is clamped to Projectile::TERMINAL_GRAVITY (53 cells/sec) on every tick, so an uninterrupted drop can never accumulate enough velocity to teleport the bird several rows in a single frame and skip a pipe without a collision.
Crash rules
The game ends on any of three contacts, all handled identically:
- Floor — the bird's row reaches
Game::HEIGHT(row ≥ 18). - Top wall — the bird's row goes above the ceiling (row < 0). A hard flap into the top edge crashes just like the floor.
- Pipe — the bird's cell lands outside a pipe's open gap.
High-score persistence
On a game-over that beats the current best, the new score is merged into a leaderboard and written to scores.json under the config dir ($XDG_CONFIG_HOME or $HOME/.config, in .candy-flappy/). The list is bounded to the top Game::MAX_HIGH_SCORES (10) entries. The write runs off the synchronous update() path via a Cmd and swallows I/O errors, so a full or unwritable disk can never crash the render loop. The file itself is published through sugar-core's AtomicJsonFile (flock + temp-and-rename, 0600 mode) — never a bare truncate-write. Saved scores are loaded on the next Game::start(), which fails soft: a corrupt, unreadable, or oversized (over Game::MAX_SCORE_FILE_BYTES) save is quarantined aside as scores.json.corrupt-<timestamp> and play starts with an empty board, so a bad file can never brick launch. Entries are legality-clamped on read: non-integers, zero, negatives, and scores of Game::SCORE_CEILING or above are dropped.
Test
composer install vendor/bin/phpunit
Snapshot tests
Game frame output is pinned via sugar-testing's assertGoldenAnsi golden-file
snapshots (tests/fixtures/*.golden). Comparison is byte-exact and never
auto-rewrites: a differing golden fails. Any change to the ANSI playfield must be
intentional — to accept a new render, delete the fixture and re-run once with the
create-mode env var (sugar-testing only creates a golden when it is missing):
rm tests/fixtures/render-alive.golden UPDATE_GOLDENS=1 vendor/bin/phpunit --filter GoldenRenderTest # recreates vendor/bin/phpunit --filter GoldenRenderTest # must pass with no env
Scoring
The score increments by one for each pipe pair whose column passes the bird's column (per tick), so surviving is scored, not flapping. The top ten scores ever recorded on this machine are kept.
Tuning is compiled in, deliberately: physics constants, tick rate, gap bands and key bindings are NOT configurable. The leaderboard and the difficulty curve are one coherent game; knobs would let a session claim scores the defaults could not produce.
High scores, config, and environment
- Save file:
~/.config/.candy-flappy/scores.json(0600, atomic; a corrupt file is quarantined toscores.json.corrupt-<timestamp>instead of bricking launch). - Config base resolution:
XDG_CONFIG_HOMEif set, elseHOME/.config. With neither set the game refuses to launch rather than guess. - To reset the leaderboard:
rm ~/.config/.candy-flappy/scores.json. - Colour posture follows
sugar-core'sColorProfile::detect():NO_COLORforces the ASCII posture;CLICOLOR_FORCE/FORCE_COLORcan force colour on where detection would demure; a non-TTY stdout orTERM=dumbgets the plain refusal screen.
Dependencies
Runtime: PHP ^8.3, sugarcraft/sugar-core
(program loop, messages, Subscriptions, AtomicJsonFile),
sugarcraft/sugar-style
(the styled-cell renderer), and sugarcraft/sugar-spring
(spring physics: the bird's flap impulse and gravity ride Projectile).
Development only: PHPUnit ^10 and sugarcraft/sugar-testing
(assertGoldenAnsi).
Credits & inspiration
Design antecedent: kbrgl/flapioca; SugarCraft is developed as a native PHP project.
