From fcc5533c835e067c2827d8167559f91926f0d28f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Martin?= Date: Wed, 14 Oct 2026 18:46:49 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk --- lib/games/src/mines.cpp | 108 ++++++++ lib/games/src/mines.h | 56 ++++ lib/games/src/twenty48.cpp | 89 +++++++ lib/games/src/twenty48.h | 43 ++++ test/test_more_games/test_more_games.cpp | 312 +++++++++++++++++++++++ 5 files changed, 608 insertions(+) create mode 100644 lib/games/src/mines.cpp create mode 100644 lib/games/src/mines.h create mode 100644 lib/games/src/twenty48.cpp create mode 100644 lib/games/src/twenty48.h create mode 100644 test/test_more_games/test_more_games.cpp diff --git a/lib/games/src/mines.cpp b/lib/games/src/mines.cpp new file mode 100644 index 0000000..2b1a2d1 --- /dev/null +++ b/lib/games/src/mines.cpp @@ -0,0 +1,108 @@ +#include "mines.h" + +#include + +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((cells_[at(col, row)] & ~kCount) | n); + } +} + +void Mines::lay(const uint16_t* where, int n) { + for (auto& c : cells_) c &= static_cast(~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 todo{static_cast(at(col, row))}; + while (!todo.empty()) { + int i = todo.back(); + todo.pop_back(); + if (cells_[static_cast(i)] & (kOpen | kFlag)) continue; + cells_[static_cast(i)] |= kOpen; + opened_++; + if (cells_[static_cast(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(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 diff --git a/lib/games/src/mines.h b/lib/games/src/mines.h new file mode 100644 index 0000000..361c3a1 --- /dev/null +++ b/lib/games/src/mines.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include + +#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(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 cells_{}; + Phase phase_ = Phase::Fresh; + bool laid_ = false; + int flags_ = 0, opened_ = 0; + uint32_t elapsedMs_ = 0; +}; + +} // namespace roro::games diff --git a/lib/games/src/twenty48.cpp b/lib/games/src/twenty48.cpp new file mode 100644 index 0000000..4c5e63a --- /dev/null +++ b/lib/games/src/twenty48.cpp @@ -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(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(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 diff --git a/lib/games/src/twenty48.h b/lib/games/src/twenty48.h new file mode 100644 index 0000000..6238caa --- /dev/null +++ b/lib/games/src/twenty48.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include + +#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(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 cells_{}; + uint32_t score_ = 0; + bool over_ = false, reached_ = false; +}; + +} // namespace roro::games diff --git a/test/test_more_games/test_more_games.cpp b/test/test_more_games/test_more_games.cpp new file mode 100644 index 0000000..15f841d --- /dev/null +++ b/test/test_more_games/test_more_games.cpp @@ -0,0 +1,312 @@ +#include + +#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(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(seed * 7 % Mines::kCols), row = static_cast(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 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(); +}