Public Access
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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user