Search by

sugarcraft / candy-flappy

detain

Flappy-Bird-style terminal game on the SugarCraft stack, inspired by kbrgl/flapioca.

Package info

github.com/sugarcraft/candy-flappy

Homepage

Documentation

Type:project

pkg:composer/sugarcraft/candy-flappy

Statistics

Installs: 0

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

dev-master 2026-11-04 00:18 UTC

This package is auto-updated.

Last update: 2026-11-05 01:00:36 UTC


README

candy-flappy

CandyFlappy

CI codecov Packagist Version License PHP

demo

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; r never 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_COLOR on 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 to scores.json.corrupt-<timestamp> instead of bricking launch).
  • Config base resolution: XDG_CONFIG_HOME if set, else HOME/.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's ColorProfile::detect(): NO_COLOR forces the ASCII posture; CLICOLOR_FORCE / FORCE_COLOR can force colour on where detection would demure; a non-TTY stdout or TERM=dumb gets 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.