← All apps

CandyMines

πŸ’£ CandyMines

Minesweeper on the SugarCraft stack

port of go-sweep game minesweeper sugarcraft deterministic-rng

Customisable board (width / height / mine count), recursive zero-flood-fill, win / lose detection. Pure-state Board class so every reveal is unit-testable without touching the runtime.

Install

composer require sugarcraft/candy-mines

Quickstart

./bin/candy-mines              # 10Γ—10, 12 mines (defaults)
./bin/candy-mines 16 16 40     # custom board

// Programmatic:
use SugarCraft\Core\Program;
use SugarCraft\Core\ProgramOptions;
use SugarCraft\Mines\Game;

(new Program(Game::start(width: 16, height: 16, mines: 40),
             new ProgramOptions(useAltScreen: true)))->run();

What's in the box

First-click safetyMines aren't placed until the first reveal, with the clicked cell's 3Γ—3 neighbourhood excluded β€” every game gets a non-trivial flood-fill on click 1.
Recursive flood fillZero-adjacency cells reveal their neighbours transitively, exactly like the original.
Vim-style movementhjkl alongside arrows; cursor clamps at board edges.
Flag togglef flags / unflags the cursor cell; flagged cells can't be revealed by accident.
Chord clickc or middle-click on a satisfied number reveals all unflagged neighbours β€” mirrors classic left+right chord behaviour.
Sub-second timerGame timer uses microtime(true) for millisecond precision; frozen on win/lose and stored in stats.
Best-time persistenceDifficultyStats persists games/wins/best time per difficulty to JSON atomically (tmp+rename) so crashes never corrupt the record.
Win + lose detectionisWon = every non-mine revealed; exploded = at least one mine was clicked. O(1) via revealedCount β€” no grid scan on each move.
Restart keyr reseeds with a fresh layout, preserving the chosen board size.
Save / restore mid-gameBoard::serialize() emits versioned JSON; Board::unserialize() reconstructs the exact board state for mid-game suspend/resume.
Custom difficultyUi\CustomDifficulty validates rows (2–50), cols (2–50), mines (1 to rowsΓ—colsβˆ’9) with i18n-aware error messages.

Source & demos

Try the quickstart β†’

API

ClassMethodDescription
Gamestart(width, height, mines)Start a new game
GamewithDifficulty(d)Start with a preset difficulty
Gameelapsed()Elapsed time in seconds (sub-second precision)
GamerecordResult(elapsed)Record win/loss and update stats
Boardnew(width, height, mines)Create game board
Boardcell(x, y)Get cell at position
Boardchord(x, y)Chord-click β€” reveal safe neighbours of a satisfied number
Boardserialize()Emit versioned JSON string ({v:1, w, h, m, p, e, r, c}) for mid-game save
Boardunserialize(data)Reconstruct a Board from a serialize() payload
Ui\CustomDifficultyfromInput(rows, cols, mines)Validate custom board dimensions; throws with i18n key on violation
Ui\CustomDifficultydefaults()Factory β€” 9Γ—9, 10 mines
DifficultyStatsload(path)Load stats from atomic JSON file (null if absent)
DifficultyStatssave(path)Persist stats atomically (tmp+rename)
DifficultyStatswithGame(d, won, time)Return new DifficultyStats with the game recorded

Demos.

VHS-recorded GIFs of every example shipped with the app. Regenerated automatically on every push that touches the source.

Play

Play

Cursor walk, flood-fill reveal, flag plant.
Flagging

Flagging

f key toggles a flag on the cursor cell to mark suspected mines.