Games: Minesweeper and 2048 as state machines

Minesweeper on 20 x 9 with 28 mines, laid at the first dig and never
under or next to it, with flags and the open-around-a-number dig; 2048
on 4 x 4, a tile merging once a move. Seeded, host-tested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
This commit is contained in:
2026-10-14 18:46:49 +02:00
co-authored by Claude Opus 5.5
parent b9d352555f
commit fcc5533c83
5 changed files with 608 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
#include "mines.h"
#include <vector>
namespace roro::games {
void Mines::start() {
cells_.fill(0);
phase_ = Phase::Fresh;
laid_ = false;
flags_ = opened_ = 0;
elapsedMs_ = 0;
}
void Mines::count() {
for (int row = 0; row < kRows; row++)
for (int col = 0; col < kCols; col++) {
int n = 0;
for (int dr = -1; dr <= 1; dr++)
for (int dc = -1; dc <= 1; dc++)
if ((dr || dc) && inside(col + dc, row + dr) && mine(col + dc, row + dr)) n++;
cells_[at(col, row)] = static_cast<uint8_t>((cells_[at(col, row)] & ~kCount) | n);
}
}
void Mines::lay(const uint16_t* where, int n) {
for (auto& c : cells_) c &= static_cast<uint8_t>(~kMine);
for (int i = 0; i < n; i++) cells_[where[i]] |= kMine;
laid_ = true;
count();
}
// kMines of them, anywhere but under the first dig and the eight cells around it.
void Mines::layAwayFrom(int col, int row) {
int placed = 0;
while (placed < kMines) {
int c = rng_.below(kCols), r = rng_.below(kRows);
if ((c >= col - 1 && c <= col + 1 && r >= row - 1 && r <= row + 1) || mine(c, r)) continue;
cells_[at(c, r)] |= kMine;
placed++;
}
laid_ = true;
count();
}
// Opens a cell, and from a cell with no mine around it, everything up to the numbers.
uint8_t Mines::reveal(int col, int row) {
if (open(col, row) || flagged(col, row)) return 0;
if (mine(col, row)) {
cells_[at(col, row)] |= kOpen;
phase_ = Phase::Lost;
return Boom;
}
std::vector<uint16_t> todo{static_cast<uint16_t>(at(col, row))};
while (!todo.empty()) {
int i = todo.back();
todo.pop_back();
if (cells_[static_cast<size_t>(i)] & (kOpen | kFlag)) continue;
cells_[static_cast<size_t>(i)] |= kOpen;
opened_++;
if (cells_[static_cast<size_t>(i)] & kCount) continue;
int c = i % kCols, r = i / kCols;
for (int dr = -1; dr <= 1; dr++)
for (int dc = -1; dc <= 1; dc++)
if ((dr || dc) && inside(c + dc, r + dr) && !open(c + dc, r + dr)) todo.push_back(static_cast<uint16_t>(at(c + dc, r + dr)));
}
return Opened;
}
uint8_t Mines::settle(uint8_t events) {
if (phase_ != Phase::Lost && opened_ == kCols * kRows - kMines) {
phase_ = Phase::Won;
events |= Won;
}
return events;
}
uint8_t Mines::dig(int col, int row) {
if (!inside(col, row) || phase_ == Phase::Lost || phase_ == Phase::Won) return 0;
if (!laid_) layAwayFrom(col, row);
if (phase_ == Phase::Fresh) phase_ = Phase::Running;
if (!open(col, row)) return settle(reveal(col, row));
// An open number with all its flags placed: what is left around it is opened, mines included
// if a flag is wrong.
int flags = 0;
for (int dr = -1; dr <= 1; dr++)
for (int dc = -1; dc <= 1; dc++)
if ((dr || dc) && inside(col + dc, row + dr) && flagged(col + dc, row + dr)) flags++;
if (!around(col, row) || flags != around(col, row)) return 0;
uint8_t events = 0;
for (int dr = -1; dr <= 1; dr++)
for (int dc = -1; dc <= 1; dc++)
if ((dr || dc) && inside(col + dc, row + dr)) events |= reveal(col + dc, row + dr);
return settle(events);
}
uint8_t Mines::flag(int col, int row) {
if (!inside(col, row) || phase_ == Phase::Lost || phase_ == Phase::Won || open(col, row)) return 0;
cells_[at(col, row)] ^= kFlag;
flags_ += flagged(col, row) ? 1 : -1;
return Flagged;
}
void Mines::tick(uint32_t ms) {
if (phase_ == Phase::Running) elapsedMs_ += ms;
}
} // namespace roro::games
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include "game_rng.h"
namespace roro::games {
// Minesweeper (issue #101's follow-up): a field of 20 x 9 with 28 mines, cells big enough to read
// on this screen. The mines are laid at
// the first dig, never under it or next to it, so the first dig always opens something. Digging
// an open number whose flags are all placed opens what is left around it. The clock runs from the
// first dig to the end.
class Mines {
public:
static constexpr int kCols = 20, kRows = 9, kMines = 28; // the classic "intermediate" share: one cell in six or seven
enum Event : uint8_t { Opened = 1, Flagged = 2, Boom = 4, Won = 8 };
enum class Phase : uint8_t { Fresh, Running, Lost, Won };
explicit Mines(uint32_t seed) : rng_(seed) { start(); }
void start();
uint8_t dig(int col, int row);
uint8_t flag(int col, int row); // put on a closed cell, or taken off
void tick(uint32_t ms); // the clock, while the game runs
Phase phase() const { return phase_; }
bool open(int col, int row) const { return cells_[at(col, row)] & kOpen; }
bool flagged(int col, int row) const { return cells_[at(col, row)] & kFlag; }
bool mine(int col, int row) const { return cells_[at(col, row)] & kMine; }
int around(int col, int row) const { return cells_[at(col, row)] & kCount; } // mines next to it
int minesLeft() const { return kMines - flags_; } // by the flags placed, right or wrong
uint32_t seconds() const { return elapsedMs_ / 1000; }
// For the tests: the mines put where one wants them, before any dig.
void lay(const uint16_t* where, int count);
private:
static constexpr uint8_t kCount = 0x0F, kMine = 0x10, kOpen = 0x20, kFlag = 0x40;
static size_t at(int col, int row) { return static_cast<size_t>(row * kCols + col); }
static bool inside(int col, int row) { return col >= 0 && col < kCols && row >= 0 && row < kRows; }
void layAwayFrom(int col, int row);
void count();
uint8_t reveal(int col, int row);
uint8_t settle(uint8_t events);
Rng rng_;
std::array<uint8_t, kCols * kRows> cells_{};
Phase phase_ = Phase::Fresh;
bool laid_ = false;
int flags_ = 0, opened_ = 0;
uint32_t elapsedMs_ = 0;
};
} // namespace roro::games
+89
View File
@@ -0,0 +1,89 @@
#include "twenty48.h"
namespace roro::games {
void Twenty48::start() {
cells_.fill(0);
score_ = 0;
over_ = reached_ = false;
spawn();
spawn();
}
void Twenty48::set(const uint8_t powers[kSize * kSize]) {
for (size_t i = 0; i < cells_.size(); i++) cells_[i] = powers[i];
over_ = !canMove();
}
void Twenty48::spawn() {
int empty = 0;
for (uint8_t c : cells_) empty += c == 0;
if (!empty) return;
int pick = rng_.below(empty);
uint8_t tile = rng_.below(10) == 0 ? 2 : 1;
for (auto& c : cells_) {
if (c) continue;
if (pick-- == 0) {
c = tile;
return;
}
}
}
bool Twenty48::canMove() const {
for (int row = 0; row < kSize; row++)
for (int col = 0; col < kSize; col++) {
uint8_t p = power(col, row);
if (!p) return true;
if (col + 1 < kSize && power(col + 1, row) == p) return true;
if (row + 1 < kSize && power(col, row + 1) == p) return true;
}
return false;
}
uint8_t Twenty48::slide(Dir dir) {
if (over_) return 0;
uint8_t events = 0;
// Each of the four lines, read from the side the tiles slide to.
for (int line = 0; line < kSize; line++) {
size_t index[kSize];
for (int i = 0; i < kSize; i++) {
int col = dir == Dir::Left ? i : dir == Dir::Right ? kSize - 1 - i : line;
int row = dir == Dir::Up ? i : dir == Dir::Down ? kSize - 1 - i : line;
index[i] = static_cast<size_t>(row * kSize + col);
}
uint8_t out[kSize] = {};
int n = 0;
bool merged = false; // the last tile put down came from a merge: it takes no other
for (int i = 0; i < kSize; i++) {
uint8_t p = cells_[index[i]];
if (!p) continue;
if (n > 0 && out[n - 1] == p && !merged) {
out[n - 1] = static_cast<uint8_t>(p + 1);
score_ += 1u << (p + 1);
merged = true;
events |= Merged;
if (p + 1 == 11 && !reached_) {
reached_ = true;
events |= Reached;
}
} else {
out[n++] = p;
merged = false;
}
}
for (int i = 0; i < kSize; i++) {
if (cells_[index[i]] != out[i]) events |= Moved;
cells_[index[i]] = out[i];
}
}
if (!events) return 0;
spawn();
if (!canMove()) {
over_ = true;
events |= Over;
}
return events;
}
} // namespace roro::games
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include "game_rng.h"
namespace roro::games {
// 2048: a board of 4 x 4. A move slides every tile as far as it goes; two alike that meet become
// one of twice the value, once a move. Then a new tile comes, a 2 nine times in ten, else a 4.
// The game is over when nothing can move. Reaching 2048 is said once, and the game goes on.
class Twenty48 {
public:
static constexpr int kSize = 4;
enum class Dir : uint8_t { Up, Right, Down, Left };
enum Event : uint8_t { Moved = 1, Merged = 2, Reached = 4, Over = 8 };
explicit Twenty48(uint32_t seed) : rng_(seed) { start(); }
void start();
uint8_t slide(Dir dir); // 0 when nothing can move that way
// A tile's value (2, 4, 8...), or 0 for an empty place.
uint32_t value(int col, int row) const { return power(col, row) ? 1u << power(col, row) : 0; }
uint8_t power(int col, int row) const { return cells_[static_cast<size_t>(row * kSize + col)]; } // 1 for a 2, 11 for 2048
uint32_t score() const { return score_; } // the sum of every tile made by a merge
bool over() const { return over_; }
bool reached() const { return reached_; }
// For the tests: the board set, a row after another, in powers of two.
void set(const uint8_t powers[kSize * kSize]);
private:
bool canMove() const;
void spawn();
Rng rng_;
std::array<uint8_t, kSize * kSize> cells_{};
uint32_t score_ = 0;
bool over_ = false, reached_ = false;
};
} // namespace roro::games