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
|
||||
@@ -0,0 +1,312 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include "mines.h"
|
||||
#include "twenty48.h"
|
||||
|
||||
using namespace roro::games;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
// ---- Minesweeper ----
|
||||
|
||||
namespace {
|
||||
uint16_t at(int col, int row) { return static_cast<uint16_t>(row * Mines::kCols + col); }
|
||||
int countOpen(const Mines& m) {
|
||||
int n = 0;
|
||||
for (int r = 0; r < Mines::kRows; r++)
|
||||
for (int c = 0; c < Mines::kCols; c++) n += m.open(c, r);
|
||||
return n;
|
||||
}
|
||||
int countMines(const Mines& m) {
|
||||
int n = 0;
|
||||
for (int r = 0; r < Mines::kRows; r++)
|
||||
for (int c = 0; c < Mines::kCols; c++) n += m.mine(c, r);
|
||||
return n;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void test_mines_the_first_dig_is_never_a_mine_and_opens_something() {
|
||||
for (uint32_t seed = 1; seed <= 30; seed++) {
|
||||
Mines m(seed);
|
||||
TEST_ASSERT_TRUE(m.phase() == Mines::Phase::Fresh);
|
||||
int col = static_cast<int>(seed * 7 % Mines::kCols), row = static_cast<int>(seed * 3 % Mines::kRows);
|
||||
uint8_t e = m.dig(col, row);
|
||||
TEST_ASSERT_TRUE(e & Mines::Opened);
|
||||
TEST_ASSERT_FALSE(e & Mines::Boom);
|
||||
TEST_ASSERT_EQUAL(Mines::kMines, countMines(m));
|
||||
TEST_ASSERT_EQUAL(0, m.around(col, row)); // nor is one next to it
|
||||
TEST_ASSERT_TRUE(countOpen(m) >= 4); // so its neighbours opened too
|
||||
TEST_ASSERT_TRUE(m.phase() == Mines::Phase::Running);
|
||||
}
|
||||
}
|
||||
|
||||
void test_mines_numbers_count_the_mines_around() {
|
||||
Mines m(1);
|
||||
const uint16_t where[] = {at(5, 5), at(6, 5), at(7, 6)};
|
||||
m.lay(where, 3);
|
||||
TEST_ASSERT_EQUAL(3, m.around(6, 6)); // all three touch it
|
||||
TEST_ASSERT_EQUAL(1, m.around(4, 4)); // only (5,5), by a corner
|
||||
TEST_ASSERT_EQUAL(2, m.around(6, 4)); // the two above which it sits
|
||||
TEST_ASSERT_EQUAL(0, m.around(20, 10));
|
||||
}
|
||||
|
||||
void test_mines_an_empty_cell_opens_up_to_the_numbers() {
|
||||
Mines m(1);
|
||||
const uint16_t where[] = {at(0, 0)};
|
||||
m.lay(where, 1);
|
||||
m.dig(15, 6);
|
||||
TEST_ASSERT_EQUAL(Mines::kCols * Mines::kRows - 1, countOpen(m)); // everything but the mine
|
||||
TEST_ASSERT_TRUE(m.open(1, 1));
|
||||
TEST_ASSERT_FALSE(m.open(0, 0));
|
||||
}
|
||||
|
||||
void test_mines_a_number_opens_alone() {
|
||||
Mines m(1);
|
||||
const uint16_t where[] = {at(5, 5)};
|
||||
m.lay(where, 1);
|
||||
TEST_ASSERT_EQUAL(Mines::Opened, m.dig(4, 4));
|
||||
TEST_ASSERT_EQUAL(1, countOpen(m));
|
||||
}
|
||||
|
||||
void test_mines_digging_a_mine_loses() {
|
||||
Mines m(1);
|
||||
const uint16_t where[] = {at(5, 5), at(20, 3)};
|
||||
m.lay(where, 2);
|
||||
m.dig(4, 4);
|
||||
uint8_t e = m.dig(5, 5);
|
||||
TEST_ASSERT_TRUE(e & Mines::Boom);
|
||||
TEST_ASSERT_TRUE(m.phase() == Mines::Phase::Lost);
|
||||
TEST_ASSERT_EQUAL(0, m.dig(0, 0)); // nothing more
|
||||
TEST_ASSERT_EQUAL(0, m.flag(0, 0));
|
||||
}
|
||||
|
||||
void test_mines_a_flag_guards_its_cell_and_counts() {
|
||||
Mines m(1);
|
||||
const uint16_t where[] = {at(5, 5)};
|
||||
m.lay(where, 1);
|
||||
TEST_ASSERT_EQUAL(Mines::kMines, m.minesLeft());
|
||||
TEST_ASSERT_EQUAL(Mines::Flagged, m.flag(5, 5));
|
||||
TEST_ASSERT_EQUAL(Mines::kMines - 1, m.minesLeft());
|
||||
TEST_ASSERT_EQUAL(0, m.dig(5, 5)); // not while it is flagged
|
||||
TEST_ASSERT_FALSE(m.open(5, 5));
|
||||
m.flag(5, 5); // taken off
|
||||
TEST_ASSERT_EQUAL(Mines::kMines, m.minesLeft());
|
||||
m.dig(4, 4);
|
||||
TEST_ASSERT_EQUAL(0, m.flag(4, 4)); // an open cell takes no flag
|
||||
}
|
||||
|
||||
void test_mines_a_number_with_its_flags_opens_the_rest() {
|
||||
Mines m(1);
|
||||
const uint16_t where[] = {at(5, 5), at(20, 3)};
|
||||
m.lay(where, 2);
|
||||
m.dig(4, 4); // a 1
|
||||
TEST_ASSERT_EQUAL(0, m.dig(4, 4)); // no flag yet: nothing
|
||||
m.flag(5, 5);
|
||||
uint8_t e = m.dig(4, 4);
|
||||
TEST_ASSERT_TRUE(e & Mines::Opened);
|
||||
TEST_ASSERT_TRUE(m.open(3, 3));
|
||||
TEST_ASSERT_TRUE(m.open(4, 5));
|
||||
TEST_ASSERT_FALSE(m.open(5, 5));
|
||||
}
|
||||
|
||||
void test_mines_a_wrong_flag_and_the_same_dig_loses() {
|
||||
Mines m(1);
|
||||
const uint16_t where[] = {at(5, 5), at(20, 3)};
|
||||
m.lay(where, 2);
|
||||
m.dig(4, 4);
|
||||
m.flag(3, 3); // the wrong cell
|
||||
TEST_ASSERT_TRUE(m.dig(4, 4) & Mines::Boom);
|
||||
}
|
||||
|
||||
void test_mines_opening_everything_else_wins() {
|
||||
Mines m(1);
|
||||
uint16_t many[Mines::kMines];
|
||||
for (int i = 0; i < Mines::kMines; i++) many[i] = at(i % Mines::kCols, i / Mines::kCols); // the first row and part of the second
|
||||
m.lay(many, Mines::kMines);
|
||||
uint8_t all = 0;
|
||||
for (int r = 0; r < Mines::kRows; r++)
|
||||
for (int c = 0; c < Mines::kCols; c++)
|
||||
if (!m.mine(c, r)) all |= m.dig(c, r);
|
||||
TEST_ASSERT_TRUE(all & Mines::Won);
|
||||
TEST_ASSERT_FALSE(all & Mines::Boom);
|
||||
TEST_ASSERT_TRUE(m.phase() == Mines::Phase::Won);
|
||||
}
|
||||
|
||||
void test_mines_the_clock_runs_from_the_first_dig_to_the_end() {
|
||||
Mines m(1);
|
||||
m.tick(5000);
|
||||
TEST_ASSERT_EQUAL(0, m.seconds()); // not before the first dig
|
||||
const uint16_t where[] = {at(5, 5), at(20, 3)};
|
||||
m.lay(where, 2);
|
||||
m.dig(4, 4);
|
||||
m.tick(2500);
|
||||
TEST_ASSERT_EQUAL(2, m.seconds());
|
||||
m.dig(5, 5);
|
||||
m.tick(9000);
|
||||
TEST_ASSERT_EQUAL(2, m.seconds()); // stopped
|
||||
m.start();
|
||||
TEST_ASSERT_EQUAL(0, m.seconds());
|
||||
TEST_ASSERT_EQUAL(0, countOpen(m));
|
||||
}
|
||||
|
||||
// ---- 2048 ----
|
||||
|
||||
namespace {
|
||||
void board(Twenty48& t, std::initializer_list<uint8_t> powers) {
|
||||
uint8_t p[16] = {};
|
||||
int i = 0;
|
||||
for (uint8_t v : powers) p[i++] = v;
|
||||
t.set(p);
|
||||
}
|
||||
int tiles(const Twenty48& t) {
|
||||
int n = 0;
|
||||
for (int r = 0; r < 4; r++)
|
||||
for (int c = 0; c < 4; c++) n += t.power(c, r) != 0;
|
||||
return n;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void test_2048_starts_with_two_tiles() {
|
||||
for (uint32_t seed = 1; seed <= 20; seed++) {
|
||||
Twenty48 t(seed);
|
||||
TEST_ASSERT_EQUAL(2, tiles(t));
|
||||
TEST_ASSERT_EQUAL(0, t.score());
|
||||
TEST_ASSERT_FALSE(t.over());
|
||||
}
|
||||
}
|
||||
|
||||
void test_2048_tiles_slide_to_the_side() {
|
||||
Twenty48 t(1);
|
||||
board(t, {0, 1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
|
||||
uint8_t e = t.slide(Twenty48::Dir::Left);
|
||||
TEST_ASSERT_TRUE(e & Twenty48::Moved);
|
||||
TEST_ASSERT_FALSE(e & Twenty48::Merged);
|
||||
TEST_ASSERT_EQUAL(2, t.value(0, 0));
|
||||
TEST_ASSERT_EQUAL(4, t.value(1, 0));
|
||||
TEST_ASSERT_EQUAL(3, tiles(t)); // and a new one came
|
||||
}
|
||||
|
||||
void test_2048_two_alike_become_one_and_score() {
|
||||
Twenty48 t(1);
|
||||
board(t, {1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
|
||||
uint8_t e = t.slide(Twenty48::Dir::Right);
|
||||
TEST_ASSERT_TRUE(e & Twenty48::Merged);
|
||||
TEST_ASSERT_EQUAL(4, t.value(3, 0));
|
||||
TEST_ASSERT_EQUAL(4, t.score());
|
||||
}
|
||||
|
||||
void test_2048_a_tile_merges_once_a_move() {
|
||||
Twenty48 t(1);
|
||||
board(t, {1, 1, 1, 1, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0});
|
||||
t.slide(Twenty48::Dir::Left);
|
||||
TEST_ASSERT_EQUAL(4, t.value(0, 0)); // 2 2 2 2 is 4 4, not 8
|
||||
TEST_ASSERT_EQUAL(4, t.value(1, 0));
|
||||
TEST_ASSERT_EQUAL(4, t.value(0, 1)); // 4 2 2 is 4 4, not 8
|
||||
TEST_ASSERT_EQUAL(4, t.value(1, 1));
|
||||
TEST_ASSERT_EQUAL(12, t.score());
|
||||
}
|
||||
|
||||
void test_2048_the_pair_nearest_the_wall_merges_first() {
|
||||
Twenty48 t(1);
|
||||
board(t, {1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
|
||||
t.slide(Twenty48::Dir::Right);
|
||||
TEST_ASSERT_EQUAL(4, t.value(3, 0));
|
||||
TEST_ASSERT_EQUAL(2, t.value(2, 0));
|
||||
}
|
||||
|
||||
void test_2048_up_and_down() {
|
||||
Twenty48 t(1);
|
||||
board(t, {1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0});
|
||||
t.slide(Twenty48::Dir::Down);
|
||||
TEST_ASSERT_EQUAL(4, t.value(0, 3)); // the 4 at the bottom...
|
||||
TEST_ASSERT_EQUAL(4, t.value(0, 2)); // ...and the two 2s made into one above it
|
||||
board(t, {0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0});
|
||||
t.slide(Twenty48::Dir::Up);
|
||||
TEST_ASSERT_EQUAL(16, t.value(3, 0));
|
||||
}
|
||||
|
||||
void test_2048_a_move_that_moves_nothing_is_no_move() {
|
||||
Twenty48 t(1);
|
||||
board(t, {1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
|
||||
TEST_ASSERT_EQUAL(0, t.slide(Twenty48::Dir::Left));
|
||||
TEST_ASSERT_EQUAL(0, t.slide(Twenty48::Dir::Up));
|
||||
TEST_ASSERT_EQUAL(2, tiles(t)); // no new tile either
|
||||
}
|
||||
|
||||
void test_2048_reaching_2048_is_said_once() {
|
||||
Twenty48 t(1);
|
||||
board(t, {10, 10, 0, 0, 10, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
|
||||
uint8_t e = t.slide(Twenty48::Dir::Left);
|
||||
TEST_ASSERT_TRUE(e & Twenty48::Reached);
|
||||
TEST_ASSERT_TRUE(t.reached());
|
||||
TEST_ASSERT_EQUAL(2048, t.value(0, 0));
|
||||
TEST_ASSERT_FALSE(t.over()); // and it goes on
|
||||
board(t, {10, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
|
||||
TEST_ASSERT_FALSE(t.slide(Twenty48::Dir::Left) & Twenty48::Reached);
|
||||
}
|
||||
|
||||
void test_2048_is_over_when_nothing_can_move() {
|
||||
Twenty48 t(1);
|
||||
board(t, {1, 2, 1, 2, 2, 1, 2, 1, 1, 2, 1, 2, 2, 1, 2, 1});
|
||||
TEST_ASSERT_TRUE(t.over());
|
||||
TEST_ASSERT_EQUAL(0, t.slide(Twenty48::Dir::Left));
|
||||
// One pair left: it is not over, and the pair can be made into one.
|
||||
board(t, {1, 2, 1, 2, 2, 1, 2, 1, 1, 2, 1, 2, 2, 1, 3, 3});
|
||||
TEST_ASSERT_FALSE(t.over());
|
||||
uint8_t e = t.slide(Twenty48::Dir::Left);
|
||||
TEST_ASSERT_TRUE(e & Twenty48::Merged);
|
||||
TEST_ASSERT_EQUAL(16, t.score());
|
||||
}
|
||||
|
||||
void test_2048_the_same_seed_is_the_same_game() {
|
||||
Twenty48 a(9), b(9);
|
||||
const Twenty48::Dir moves[] = {Twenty48::Dir::Left, Twenty48::Dir::Up, Twenty48::Dir::Right, Twenty48::Dir::Down};
|
||||
for (int i = 0; i < 60; i++) {
|
||||
a.slide(moves[i % 4]);
|
||||
b.slide(moves[i % 4]);
|
||||
}
|
||||
for (int r = 0; r < 4; r++)
|
||||
for (int c = 0; c < 4; c++) TEST_ASSERT_EQUAL(a.power(c, r), b.power(c, r));
|
||||
TEST_ASSERT_EQUAL(a.score(), b.score());
|
||||
}
|
||||
|
||||
void test_2048_new_tiles_are_twos_and_sometimes_fours() {
|
||||
int twos = 0, fours = 0;
|
||||
for (uint32_t seed = 1; seed <= 300; seed++) {
|
||||
Twenty48 t(seed);
|
||||
for (int r = 0; r < 4; r++)
|
||||
for (int c = 0; c < 4; c++) {
|
||||
twos += t.value(c, r) == 2;
|
||||
fours += t.value(c, r) == 4;
|
||||
}
|
||||
}
|
||||
TEST_ASSERT_EQUAL(600, twos + fours);
|
||||
TEST_ASSERT_TRUE(fours > 20 && fours < 120); // about one in ten
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_mines_the_first_dig_is_never_a_mine_and_opens_something);
|
||||
RUN_TEST(test_mines_numbers_count_the_mines_around);
|
||||
RUN_TEST(test_mines_an_empty_cell_opens_up_to_the_numbers);
|
||||
RUN_TEST(test_mines_a_number_opens_alone);
|
||||
RUN_TEST(test_mines_digging_a_mine_loses);
|
||||
RUN_TEST(test_mines_a_flag_guards_its_cell_and_counts);
|
||||
RUN_TEST(test_mines_a_number_with_its_flags_opens_the_rest);
|
||||
RUN_TEST(test_mines_a_wrong_flag_and_the_same_dig_loses);
|
||||
RUN_TEST(test_mines_opening_everything_else_wins);
|
||||
RUN_TEST(test_mines_the_clock_runs_from_the_first_dig_to_the_end);
|
||||
RUN_TEST(test_2048_starts_with_two_tiles);
|
||||
RUN_TEST(test_2048_tiles_slide_to_the_side);
|
||||
RUN_TEST(test_2048_two_alike_become_one_and_score);
|
||||
RUN_TEST(test_2048_a_tile_merges_once_a_move);
|
||||
RUN_TEST(test_2048_the_pair_nearest_the_wall_merges_first);
|
||||
RUN_TEST(test_2048_up_and_down);
|
||||
RUN_TEST(test_2048_a_move_that_moves_nothing_is_no_move);
|
||||
RUN_TEST(test_2048_reaching_2048_is_said_once);
|
||||
RUN_TEST(test_2048_is_over_when_nothing_can_move);
|
||||
RUN_TEST(test_2048_the_same_seed_is_the_same_game);
|
||||
RUN_TEST(test_2048_new_tiles_are_twos_and_sometimes_fours);
|
||||
return UNITY_END();
|
||||
}
|
||||
Reference in New Issue
Block a user