Minesweeper on the SugarCraft stack
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.
composer require sugarcraft/candy-mines
./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();
hjkl alongside arrows; cursor clamps at board edges.f flags / unflags the cursor cell; flagged cells can't be revealed by accident.c or middle-click on a satisfied number reveals all unflagged neighbours β mirrors classic left+right chord behaviour.microtime(true) for millisecond precision; frozen on win/lose and stored in stats.DifficultyStats persists games/wins/best time per difficulty to JSON atomically (tmp+rename) so crashes never corrupt the record.isWon = every non-mine revealed; exploded = at least one mine was clicked. O(1) via revealedCount β no grid scan on each move.r reseeds with a fresh layout, preserving the chosen board size.Board::serialize() emits versioned JSON; Board::unserialize() reconstructs the exact board state for mid-game suspend/resume.Ui\CustomDifficulty validates rows (2β50), cols (2β50), mines (1 to rowsΓcolsβ9) with i18n-aware error messages.| Class | Method | Description |
|---|---|---|
| Game | start(width, height, mines) | Start a new game |
| Game | withDifficulty(d) | Start with a preset difficulty |
| Game | elapsed() | Elapsed time in seconds (sub-second precision) |
| Game | recordResult(elapsed) | Record win/loss and update stats |
| Board | new(width, height, mines) | Create game board |
| Board | cell(x, y) | Get cell at position |
| Board | chord(x, y) | Chord-click β reveal safe neighbours of a satisfied number |
| Board | serialize() | Emit versioned JSON string ({v:1, w, h, m, p, e, r, c}) for mid-game save |
| Board | unserialize(data) | Reconstruct a Board from a serialize() payload |
| Ui\CustomDifficulty | fromInput(rows, cols, mines) | Validate custom board dimensions; throws with i18n key on violation |
| Ui\CustomDifficulty | defaults() | Factory β 9Γ9, 10 mines |
| DifficultyStats | load(path) | Load stats from atomic JSON file (null if absent) |
| DifficultyStats | save(path) | Persist stats atomically (tmp+rename) |
| DifficultyStats | withGame(d, won, time) | Return new DifficultyStats with the game recorded |
VHS-recorded GIFs of every example shipped with the app. Regenerated automatically on every push that touches the source.