Public Access
Games: Pong, Snake and Tetris as seeded state machines, and a tune
The rules of the three Games (issue #101), with no screen and no keyboard in them: time and keys go in, the new state and what happened come out, and a seed decides the dice. With a player for a tune a note at a time, and Korobeiniki's notes. 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,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace roro::games {
|
||||
|
||||
// The Games' own dice (issue #101): the same seed gives the same game, which is what lets their
|
||||
// rules be tested. xorshift32; never 0.
|
||||
class Rng {
|
||||
public:
|
||||
explicit Rng(uint32_t seed) : s_(seed ? seed : 0x9E3779B9u) {}
|
||||
uint32_t next() {
|
||||
s_ ^= s_ << 13;
|
||||
s_ ^= s_ >> 17;
|
||||
s_ ^= s_ << 5;
|
||||
return s_;
|
||||
}
|
||||
int below(int n) { return n <= 1 ? 0 : static_cast<int>(next() % static_cast<uint32_t>(n)); }
|
||||
float unit() { return (next() >> 8) / 16777216.0f; } // 0 up to 1
|
||||
|
||||
private:
|
||||
uint32_t s_;
|
||||
};
|
||||
|
||||
} // namespace roro::games
|
||||
@@ -0,0 +1,103 @@
|
||||
#include "pong.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace roro::games {
|
||||
|
||||
namespace {
|
||||
constexpr float kTick = 0.004f; // the ball moves a pixel at most between two looks
|
||||
constexpr float kSteepest = 0.87f; // 50 degrees, in radians: off the very end of a paddle
|
||||
float clampPaddle(float y) { return std::clamp(y, Pong::kPaddleH / 2, Pong::kHeight - Pong::kPaddleH / 2); }
|
||||
} // namespace
|
||||
|
||||
void Pong::start() {
|
||||
playerScore_ = deviceScore_ = rally_ = longest_ = 0;
|
||||
player_ = device_ = kHeight / 2;
|
||||
serve(rng_.below(2) == 0);
|
||||
}
|
||||
|
||||
void Pong::serve(bool toPlayer) {
|
||||
x_ = kWidth / 2;
|
||||
y_ = kHeight / 2;
|
||||
speed_ = kServeSpeed;
|
||||
float angle = (rng_.unit() - 0.5f) * 1.0f; // within 29 degrees of level
|
||||
vx_ = std::cos(angle) * speed_ * (toPlayer ? -1 : 1);
|
||||
vy_ = std::sin(angle) * speed_;
|
||||
rally_ = 0;
|
||||
wait_ = kServePause;
|
||||
phase_ = Phase::Serve;
|
||||
}
|
||||
|
||||
bool Pong::meets(float paddleY) const { return std::fabs(y_ - paddleY) <= kPaddleH / 2 + kBall / 2; }
|
||||
|
||||
// Where on the paddle it hit decides the angle it leaves at, and each return is a little faster.
|
||||
void Pong::bounceOff(float paddleY, bool toRight) {
|
||||
float where = std::clamp((y_ - paddleY) / (kPaddleH / 2), -1.0f, 1.0f);
|
||||
speed_ = std::min(kMaxSpeed, speed_ * kFaster);
|
||||
vx_ = std::cos(where * kSteepest) * speed_ * (toRight ? 1 : -1);
|
||||
vy_ = std::sin(where * kSteepest) * speed_;
|
||||
}
|
||||
|
||||
uint8_t Pong::step(float seconds, int move) {
|
||||
uint8_t events = 0;
|
||||
seconds = std::min(seconds, 0.25f); // a held-up loop is not a quarter of the field
|
||||
while (seconds > 0 && phase_ != Phase::Over) {
|
||||
float dt = std::min(seconds, kTick);
|
||||
seconds -= dt;
|
||||
events |= tick(dt, move);
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
uint8_t Pong::tick(float dt, int move) {
|
||||
player_ = clampPaddle(player_ + (move < 0 ? -1 : move > 0 ? 1 : 0) * kPlayerSpeed * dt);
|
||||
// The device watches the ball once it comes its way, and waits in the middle otherwise.
|
||||
float target = vx_ > 0 && phase_ == Phase::Play ? y_ : kHeight / 2;
|
||||
float reach = kDeviceSpeed * dt;
|
||||
device_ = clampPaddle(device_ + std::clamp(target - device_, -reach, reach));
|
||||
|
||||
if (phase_ == Phase::Serve) {
|
||||
wait_ -= dt;
|
||||
if (wait_ <= 0) phase_ = Phase::Play;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t events = 0;
|
||||
float before = x_;
|
||||
x_ += vx_ * dt;
|
||||
y_ += vy_ * dt;
|
||||
if (y_ < kBall / 2) {
|
||||
y_ = kBall - y_;
|
||||
vy_ = -vy_;
|
||||
events |= Wall;
|
||||
} else if (y_ > kHeight - kBall / 2) {
|
||||
y_ = 2 * (kHeight - kBall / 2) - y_;
|
||||
vy_ = -vy_;
|
||||
events |= Wall;
|
||||
}
|
||||
|
||||
const float playerFace = kMargin + kPaddleW + kBall / 2, deviceFace = kWidth - kMargin - kPaddleW - kBall / 2;
|
||||
if (vx_ < 0 && before >= playerFace && x_ < playerFace && meets(player_)) {
|
||||
x_ = playerFace;
|
||||
bounceOff(player_, true);
|
||||
rally_++;
|
||||
longest_ = std::max(longest_, rally_);
|
||||
events |= Hit;
|
||||
} else if (vx_ > 0 && before <= deviceFace && x_ > deviceFace && meets(device_)) {
|
||||
x_ = deviceFace;
|
||||
bounceOff(device_, false);
|
||||
events |= Hit;
|
||||
} else if (x_ < 0 || x_ > kWidth) {
|
||||
bool playerScored = x_ > kWidth;
|
||||
(playerScored ? playerScore_ : deviceScore_)++;
|
||||
events |= Point;
|
||||
if (playerScore_ >= kWinScore || deviceScore_ >= kWinScore) {
|
||||
phase_ = Phase::Over;
|
||||
events |= Over;
|
||||
} else serve(playerScored); // to whoever lost the point
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
} // namespace roro::games
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "game_rng.h"
|
||||
|
||||
namespace roro::games {
|
||||
|
||||
// Pong against the device (issue #101): the player's paddle on the left, the device's on the
|
||||
// right, first to 7. The ball goes faster with each return, and the device's paddle only moves so
|
||||
// fast: a steep, fast ball gets past it. Lengths are pixels of the 240 x 122 field, times seconds.
|
||||
class Pong {
|
||||
public:
|
||||
static constexpr float kWidth = 240, kHeight = 122;
|
||||
static constexpr float kPaddleH = 24, kPaddleW = 3, kMargin = 6, kBall = 4;
|
||||
static constexpr float kPlayerSpeed = 150, kDeviceSpeed = 88;
|
||||
static constexpr float kServeSpeed = 110, kMaxSpeed = 270, kFaster = 1.06f;
|
||||
static constexpr float kServePause = 0.8f;
|
||||
static constexpr int kWinScore = 7;
|
||||
|
||||
enum Event : uint8_t { Wall = 1, Hit = 2, Point = 4, Over = 8 };
|
||||
enum class Phase : uint8_t { Serve, Play, Over };
|
||||
|
||||
explicit Pong(uint32_t seed) : rng_(seed) { start(); }
|
||||
void start();
|
||||
// `seconds` later, with the player's paddle asked up (-1), down (1) or left alone (0).
|
||||
// What happened, as Events.
|
||||
uint8_t step(float seconds, int move);
|
||||
|
||||
Phase phase() const { return phase_; }
|
||||
float ballX() const { return x_; }
|
||||
float ballY() const { return y_; }
|
||||
float speed() const { return speed_; }
|
||||
float playerY() const { return player_; } // the paddle's middle
|
||||
float deviceY() const { return device_; }
|
||||
int playerScore() const { return playerScore_; }
|
||||
int deviceScore() const { return deviceScore_; }
|
||||
bool playerWon() const { return playerScore_ >= kWinScore; }
|
||||
int rally() const { return rally_; } // the player's returns since the serve
|
||||
int longestRally() const { return longest_; } // in this game
|
||||
|
||||
private:
|
||||
uint8_t tick(float dt, int move);
|
||||
void serve(bool toPlayer);
|
||||
bool meets(float paddleY) const;
|
||||
void bounceOff(float paddleY, bool toRight);
|
||||
|
||||
Rng rng_;
|
||||
Phase phase_ = Phase::Serve;
|
||||
float x_ = 0, y_ = 0, vx_ = 0, vy_ = 0, speed_ = 0;
|
||||
float player_ = 0, device_ = 0, wait_ = 0;
|
||||
int playerScore_ = 0, deviceScore_ = 0, rally_ = 0, longest_ = 0;
|
||||
};
|
||||
|
||||
} // namespace roro::games
|
||||
@@ -0,0 +1,92 @@
|
||||
#include "snake.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace roro::games {
|
||||
|
||||
namespace {
|
||||
bool opposite(Snake::Dir a, Snake::Dir b) { return (static_cast<int>(a) + 2) % 4 == static_cast<int>(b); }
|
||||
} // namespace
|
||||
|
||||
void Snake::start() {
|
||||
body_.clear();
|
||||
taken_.fill(false);
|
||||
for (int i = 0; i < 3; i++) { // three cells in the middle, heading right
|
||||
Cell c{static_cast<int8_t>(kCols / 2 - i), static_cast<int8_t>(kRows / 2)};
|
||||
body_.push_back(c);
|
||||
mark(c, true);
|
||||
}
|
||||
dir_ = Dir::Right;
|
||||
turnCount_ = 0;
|
||||
score_ = 0;
|
||||
sinceMs_ = 0;
|
||||
alive_ = true;
|
||||
placeFood();
|
||||
}
|
||||
|
||||
uint32_t Snake::intervalMs() const {
|
||||
uint32_t off = static_cast<uint32_t>(score_ / kFoodsAStep) * kFasterMs;
|
||||
return off >= kSlowestMs - kFastestMs ? kFastestMs : kSlowestMs - off;
|
||||
}
|
||||
|
||||
void Snake::turn(Dir dir) {
|
||||
Dir last = turnCount_ ? turns_[turnCount_ - 1] : dir_;
|
||||
if (dir == last || opposite(dir, last) || turnCount_ >= 2) return;
|
||||
turns_[turnCount_++] = dir;
|
||||
}
|
||||
|
||||
// On a free cell, each as likely as another.
|
||||
void Snake::placeFood() {
|
||||
int free = kCols * kRows - static_cast<int>(body_.size());
|
||||
if (free <= 0) return;
|
||||
int pick = rng_.below(free);
|
||||
for (int i = 0; i < kCols * kRows; i++) {
|
||||
if (taken_[static_cast<size_t>(i)]) continue;
|
||||
if (pick-- == 0) {
|
||||
food_ = {static_cast<int8_t>(i % kCols), static_cast<int8_t>(i / kCols)};
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t Snake::step(uint32_t ms) {
|
||||
uint8_t events = 0;
|
||||
if (!alive_) return 0;
|
||||
sinceMs_ += std::min<uint32_t>(ms, 500);
|
||||
while (alive_ && sinceMs_ >= intervalMs()) {
|
||||
sinceMs_ -= intervalMs();
|
||||
events |= move();
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
uint8_t Snake::move() {
|
||||
if (turnCount_) {
|
||||
dir_ = turns_[0];
|
||||
turns_[0] = turns_[1];
|
||||
turnCount_--;
|
||||
}
|
||||
static const int8_t dc[4] = {0, 1, 0, -1}, dr[4] = {-1, 0, 1, 0};
|
||||
Cell head{static_cast<int8_t>(body_.front().col + dc[static_cast<int>(dir_)]),
|
||||
static_cast<int8_t>(body_.front().row + dr[static_cast<int>(dir_)])};
|
||||
bool eats = head == food_;
|
||||
// The tail moves on as the head arrives: the head may go where the tail was. A snake that
|
||||
// dies stays as it was, whole.
|
||||
bool ontoTail = !eats && head == body_.back();
|
||||
if (head.col < 0 || head.col >= kCols || head.row < 0 || head.row >= kRows || (taken(head) && !ontoTail)) {
|
||||
alive_ = false;
|
||||
return Died;
|
||||
}
|
||||
if (!eats) {
|
||||
mark(body_.back(), false);
|
||||
body_.pop_back();
|
||||
}
|
||||
body_.push_front(head);
|
||||
mark(head, true);
|
||||
if (!eats) return Moved;
|
||||
score_++;
|
||||
placeFood();
|
||||
return Moved | Ate;
|
||||
}
|
||||
|
||||
} // namespace roro::games
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
|
||||
#include "game_rng.h"
|
||||
|
||||
namespace roro::games {
|
||||
|
||||
// Snake (issue #101): a field of 38 x 19 cells, the walls and its own body kill it, and it goes
|
||||
// faster as it eats. Time comes in from outside, in milliseconds.
|
||||
class Snake {
|
||||
public:
|
||||
static constexpr int kCols = 38, kRows = 19;
|
||||
static constexpr uint32_t kSlowestMs = 150, kFastestMs = 60, kFasterMs = 5;
|
||||
static constexpr int kFoodsAStep = 3; // it gets faster every so many
|
||||
|
||||
enum class Dir : uint8_t { Up, Right, Down, Left };
|
||||
enum Event : uint8_t { Moved = 1, Ate = 2, Died = 4 };
|
||||
struct Cell {
|
||||
int8_t col, row;
|
||||
bool operator==(const Cell& o) const { return col == o.col && row == o.row; }
|
||||
};
|
||||
|
||||
explicit Snake(uint32_t seed) : rng_(seed) { start(); }
|
||||
void start();
|
||||
// A turn asked for; it is taken at the next move. Two are remembered, so a quick corner (up,
|
||||
// then left) works; turning back on itself is not a turn.
|
||||
void turn(Dir dir);
|
||||
uint8_t step(uint32_t ms);
|
||||
|
||||
bool alive() const { return alive_; }
|
||||
int score() const { return score_; } // foods eaten
|
||||
const std::deque<Cell>& body() const { return body_; } // the head first
|
||||
Cell food() const { return food_; }
|
||||
Dir heading() const { return dir_; }
|
||||
uint32_t intervalMs() const; // between two moves, now
|
||||
// For the tests: the food put where one wants it.
|
||||
void putFood(Cell at) { food_ = at; }
|
||||
|
||||
private:
|
||||
bool taken(Cell c) const { return taken_[static_cast<size_t>(c.row * kCols + c.col)]; }
|
||||
void mark(Cell c, bool on) { taken_[static_cast<size_t>(c.row * kCols + c.col)] = on; }
|
||||
void placeFood();
|
||||
uint8_t move();
|
||||
|
||||
Rng rng_;
|
||||
std::deque<Cell> body_;
|
||||
std::array<bool, kCols * kRows> taken_{};
|
||||
Cell food_{0, 0};
|
||||
Dir dir_ = Dir::Right;
|
||||
Dir turns_[2] = {Dir::Right, Dir::Right};
|
||||
int turnCount_ = 0, score_ = 0;
|
||||
uint32_t sinceMs_ = 0;
|
||||
bool alive_ = true;
|
||||
};
|
||||
|
||||
} // namespace roro::games
|
||||
@@ -0,0 +1,202 @@
|
||||
#include "tetris.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace roro::games {
|
||||
|
||||
namespace {
|
||||
|
||||
// Each piece in its four turns, as the cells it fills in a 4 x 4 box: bit (row * 4 + col), the
|
||||
// top left first. They turn as in the Super Rotation System.
|
||||
constexpr uint16_t bits(const char* rows) {
|
||||
uint16_t v = 0;
|
||||
for (int i = 0; i < 16; i++)
|
||||
if (rows[i] == '#') v |= static_cast<uint16_t>(1u << i);
|
||||
return v;
|
||||
}
|
||||
constexpr uint16_t kShapes[7][4] = {
|
||||
{bits("....####........"), bits("..#...#...#...#."), bits("........####...."), bits(".#...#...#...#..")}, // I
|
||||
{bits(".##..##........."), bits(".##..##........."), bits(".##..##........."), bits(".##..##.........")}, // O
|
||||
{bits(".#..###........."), bits(".#...##..#......"), bits("....###..#......"), bits(".#..##...#......")}, // T
|
||||
{bits(".##.##.........."), bits(".#...##...#....."), bits(".....##.##......"), bits("#...##...#......")}, // S
|
||||
{bits("##...##........."), bits("..#..##..#......"), bits("....##...##....."), bits(".#..##..#.......")}, // Z
|
||||
{bits("#...###........."), bits(".##..#...#......"), bits("....###...#....."), bits(".#...#..##......")}, // J
|
||||
{bits("..#.###........."), bits(".#...#...##....."), bits("....###.#......."), bits("##...#...#......")}, // L
|
||||
};
|
||||
// A turn that does not fit where the piece is tries these shifts, in order: a step from a wall,
|
||||
// one up off the floor, two for the long piece.
|
||||
constexpr int8_t kKicks[6][2] = {{0, 0}, {-1, 0}, {1, 0}, {0, -1}, {-2, 0}, {2, 0}};
|
||||
constexpr int kLineScore[5] = {0, 100, 300, 500, 800};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::array<Tetris::Cell, 4> Tetris::cells(const Piece& p) {
|
||||
std::array<Cell, 4> out{};
|
||||
if (p.kind < 1 || p.kind > 7) return out;
|
||||
uint16_t shape = kShapes[p.kind - 1][p.turn & 3];
|
||||
int n = 0;
|
||||
for (int i = 0; i < 16 && n < 4; i++)
|
||||
if (shape & (1u << i)) out[static_cast<size_t>(n++)] = {static_cast<int8_t>(p.col + i % 4), static_cast<int8_t>(p.row + i / 4)};
|
||||
return out;
|
||||
}
|
||||
|
||||
void Tetris::start() {
|
||||
well_.fill(0);
|
||||
score_ = lines_ = lastCleared_ = 0;
|
||||
fallSince_ = heldMs_ = 0;
|
||||
heldDir_ = 0;
|
||||
repeating_ = over_ = softSpent_ = false;
|
||||
bagLeft_ = 0;
|
||||
next_ = deal();
|
||||
spawn(deal());
|
||||
}
|
||||
|
||||
// The bag: the seven pieces in a shuffled order, then seven again.
|
||||
uint8_t Tetris::deal() {
|
||||
if (bagLeft_ == 0) {
|
||||
for (int i = 0; i < 7; i++) bag_[i] = static_cast<uint8_t>(i + 1);
|
||||
for (int i = 6; i > 0; i--) std::swap(bag_[i], bag_[rng_.below(i + 1)]);
|
||||
bagLeft_ = 7;
|
||||
}
|
||||
return bag_[--bagLeft_];
|
||||
}
|
||||
|
||||
uint32_t Tetris::fallMs() const {
|
||||
uint32_t ms = kSlowestMs;
|
||||
for (int i = 0; i < level() && ms > kFastestMs; i++) ms = ms * 85 / 100;
|
||||
return std::max(ms, kFastestMs);
|
||||
}
|
||||
|
||||
// Inside the walls and above the floor, on no cell already filled. Above the well is free.
|
||||
bool Tetris::fits(const Piece& p) const {
|
||||
for (const Cell& c : cells(p)) {
|
||||
if (c.col < 0 || c.col >= kCols || c.row >= kRows) return false;
|
||||
if (c.row >= 0 && cell(c.col, c.row)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Tetris::shift(int dc, int dr) {
|
||||
Piece moved = piece_;
|
||||
moved.col = static_cast<int8_t>(moved.col + dc);
|
||||
moved.row = static_cast<int8_t>(moved.row + dr);
|
||||
if (!fits(moved)) return false;
|
||||
piece_ = moved;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Tetris::rotate(int by) {
|
||||
Piece turned = piece_;
|
||||
turned.turn = static_cast<uint8_t>((turned.turn + 4 + by) & 3);
|
||||
for (auto& kick : kKicks) {
|
||||
Piece tried = turned;
|
||||
tried.col = static_cast<int8_t>(tried.col + kick[0]);
|
||||
tried.row = static_cast<int8_t>(tried.row + kick[1]);
|
||||
if (fits(tried)) {
|
||||
piece_ = tried;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The piece comes in at the top, in the middle. If there is no room for it, the game is over.
|
||||
uint8_t Tetris::spawn(uint8_t kind) {
|
||||
piece_ = Piece{kind, 0, 3, static_cast<int8_t>(kind == 1 ? -1 : 0)};
|
||||
fallSince_ = 0;
|
||||
if (fits(piece_)) return 0;
|
||||
over_ = true;
|
||||
return Over;
|
||||
}
|
||||
|
||||
void Tetris::give(uint8_t kind) { spawn(kind); }
|
||||
|
||||
uint8_t Tetris::lock() {
|
||||
uint8_t events = Locked;
|
||||
for (const Cell& c : cells(piece_)) {
|
||||
if (c.row < 0) { // it came to rest above the well
|
||||
over_ = true;
|
||||
return static_cast<uint8_t>(events | Over);
|
||||
}
|
||||
well_[static_cast<size_t>(c.row * kCols + c.col)] = piece_.kind;
|
||||
}
|
||||
// Full rows go, and what is above comes down.
|
||||
int cleared = 0, before = level();
|
||||
for (int row = kRows - 1; row >= 0;) {
|
||||
bool full = true;
|
||||
for (int col = 0; col < kCols && full; col++) full = cell(col, row) != 0;
|
||||
if (!full) {
|
||||
row--;
|
||||
continue;
|
||||
}
|
||||
for (int r = row; r > 0; r--)
|
||||
for (int col = 0; col < kCols; col++) well_[static_cast<size_t>(r * kCols + col)] = cell(col, r - 1);
|
||||
for (int col = 0; col < kCols; col++) well_[static_cast<size_t>(col)] = 0;
|
||||
cleared++; // the same row again: it now holds the one that was above
|
||||
}
|
||||
lastCleared_ = cleared;
|
||||
if (cleared) {
|
||||
score_ += kLineScore[cleared] * (before + 1);
|
||||
lines_ += cleared;
|
||||
events |= Lines;
|
||||
if (level() > before) events |= LevelUp;
|
||||
}
|
||||
uint8_t coming = next_;
|
||||
next_ = deal();
|
||||
return static_cast<uint8_t>(events | spawn(coming));
|
||||
}
|
||||
|
||||
uint8_t Tetris::step(uint32_t ms, const TetrisInput& in) {
|
||||
if (over_) return 0;
|
||||
uint8_t events = 0;
|
||||
ms = std::min<uint32_t>(ms, 1000); // a held-up loop is not a piece through the floor
|
||||
|
||||
if (in.rotate && rotate(1)) events |= Turned;
|
||||
if (in.rotateBack && rotate(-1)) events |= Turned;
|
||||
|
||||
// Left and right: once at the press, then again and again while held.
|
||||
int dir = in.left == in.right ? 0 : in.left ? -1 : 1;
|
||||
if (dir != heldDir_) {
|
||||
heldDir_ = dir;
|
||||
heldMs_ = 0;
|
||||
repeating_ = false;
|
||||
if (dir && shift(dir, 0)) events |= Moved;
|
||||
} else if (dir) {
|
||||
heldMs_ += ms;
|
||||
uint32_t wait = repeating_ ? kRepeatEveryMs : kRepeatAfterMs;
|
||||
while (heldMs_ >= wait) {
|
||||
heldMs_ -= wait;
|
||||
repeating_ = true;
|
||||
wait = kRepeatEveryMs;
|
||||
if (shift(dir, 0)) events |= Moved;
|
||||
}
|
||||
}
|
||||
|
||||
if (in.hardDrop) {
|
||||
int rows = 0;
|
||||
while (shift(0, 1)) rows++;
|
||||
score_ += 2 * rows;
|
||||
return static_cast<uint8_t>(events | lock());
|
||||
}
|
||||
|
||||
// Falling faster is for the piece it was asked for: the key still held when that one lands
|
||||
// does not send the next one down too. It has to be let go and pressed again.
|
||||
if (!in.soft) softSpent_ = false;
|
||||
bool soft = in.soft && !softSpent_;
|
||||
uint32_t every = soft ? std::min(fallMs(), kSoftMs) : fallMs();
|
||||
fallSince_ += ms;
|
||||
while (!over_ && fallSince_ >= every) {
|
||||
fallSince_ -= every;
|
||||
if (shift(0, 1)) {
|
||||
if (soft) score_++;
|
||||
events |= Moved;
|
||||
} else {
|
||||
events |= lock();
|
||||
softSpent_ = in.soft;
|
||||
break; // the next piece starts its own fall
|
||||
}
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
} // namespace roro::games
|
||||
@@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "game_rng.h"
|
||||
|
||||
namespace roro::games {
|
||||
|
||||
// What the player asks of Tetris in one step: the first three for as long as the key is held,
|
||||
// the others once for each press.
|
||||
struct TetrisInput {
|
||||
bool left = false, right = false, soft = false;
|
||||
bool rotate = false, rotateBack = false, hardDrop = false;
|
||||
};
|
||||
|
||||
// Tetris (issue #101): a well of 10 x 20, the seven pieces dealt from a bag (each once in every
|
||||
// seven), the next one shown, a turn that shifts away from a wall, a soft and a hard drop, and a
|
||||
// fall that quickens every ten lines. No hold, no ghost. Time comes in milliseconds.
|
||||
class Tetris {
|
||||
public:
|
||||
static constexpr int kCols = 10, kRows = 20;
|
||||
static constexpr uint32_t kSlowestMs = 800, kFastestMs = 80;
|
||||
static constexpr uint32_t kSoftMs = 90; // a row, falling faster: quick, and still steered
|
||||
static constexpr uint32_t kRepeatAfterMs = 170, kRepeatEveryMs = 50; // a held left or right
|
||||
static constexpr int kLinesALevel = 10;
|
||||
|
||||
enum Event : uint8_t { Moved = 1, Turned = 2, Locked = 4, Lines = 8, LevelUp = 16, Over = 32 };
|
||||
// The pieces, 1 to 7, in the order I O T S Z J L; 0 is an empty cell.
|
||||
struct Piece {
|
||||
uint8_t kind = 0;
|
||||
uint8_t turn = 0; // 0 to 3, quarter turns clockwise
|
||||
int8_t col = 0, row = 0; // of the top left of its 4 x 4 box; the row may be above the well
|
||||
};
|
||||
struct Cell {
|
||||
int8_t col, row;
|
||||
};
|
||||
|
||||
explicit Tetris(uint32_t seed) : rng_(seed) { start(); }
|
||||
void start();
|
||||
uint8_t step(uint32_t ms, const TetrisInput& in);
|
||||
|
||||
bool over() const { return over_; }
|
||||
uint8_t cell(int col, int row) const { return well_[static_cast<size_t>(row * kCols + col)]; }
|
||||
const Piece& piece() const { return piece_; }
|
||||
uint8_t next() const { return next_; }
|
||||
int score() const { return score_; }
|
||||
int lines() const { return lines_; }
|
||||
int level() const { return lines_ / kLinesALevel; }
|
||||
uint32_t fallMs() const; // between two rows of fall, at this level
|
||||
int lastCleared() const { return lastCleared_; } // by the last piece locked
|
||||
|
||||
// The four cells a piece covers.
|
||||
static std::array<Cell, 4> cells(const Piece& p);
|
||||
// For the tests: a cell of the well set, and the piece in play replaced.
|
||||
void put(int col, int row, uint8_t kind) { well_[static_cast<size_t>(row * kCols + col)] = kind; }
|
||||
void give(uint8_t kind);
|
||||
|
||||
private:
|
||||
bool fits(const Piece& p) const;
|
||||
bool shift(int dc, int dr);
|
||||
bool rotate(int by);
|
||||
uint8_t lock();
|
||||
uint8_t spawn(uint8_t kind);
|
||||
uint8_t deal();
|
||||
|
||||
Rng rng_;
|
||||
std::array<uint8_t, kCols * kRows> well_{};
|
||||
Piece piece_;
|
||||
uint8_t next_ = 1;
|
||||
uint8_t bag_[7] = {};
|
||||
int bagLeft_ = 0;
|
||||
int score_ = 0, lines_ = 0, lastCleared_ = 0;
|
||||
uint32_t fallSince_ = 0, heldMs_ = 0;
|
||||
int heldDir_ = 0;
|
||||
bool repeating_ = false, over_ = false;
|
||||
bool softSpent_ = false; // the key that brought the last piece down is still held
|
||||
};
|
||||
|
||||
} // namespace roro::games
|
||||
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace roro::games {
|
||||
|
||||
// A tune as the speaker can play one: a note at a time, each so many eighths long. hz 0 is a rest.
|
||||
struct Note {
|
||||
uint16_t hz;
|
||||
uint8_t eighths;
|
||||
};
|
||||
|
||||
// Plays a tune round and round as time passes (issue #101). It makes no sound itself: step()
|
||||
// says when the next note begins, and the caller sounds it.
|
||||
class TunePlayer {
|
||||
public:
|
||||
// `at(i)` is the tune's i-th note, of `count`.
|
||||
TunePlayer(Note (*at)(size_t), size_t count, uint32_t eighthMs) : note_(at), count_(count), eighthMs_(eighthMs) {}
|
||||
|
||||
void restart() {
|
||||
at_ = 0;
|
||||
leftMs_ = 0;
|
||||
begun_ = false;
|
||||
}
|
||||
// `ms` later: true if a note begins now, with the note and how long it lasts. After a long
|
||||
// gap the notes that would have gone by are skipped, not played in a burst.
|
||||
bool step(uint32_t ms, Note& note, uint32_t& lengthMs) {
|
||||
if (!count_) return false;
|
||||
if (!begun_) {
|
||||
begun_ = true;
|
||||
ms = 0; // the time before the first note is nobody's
|
||||
} else if (ms < leftMs_) {
|
||||
leftMs_ -= ms;
|
||||
return false;
|
||||
} else {
|
||||
ms -= leftMs_;
|
||||
at_ = (at_ + 1) % count_;
|
||||
while (ms >= note_(at_).eighths * eighthMs_) { // whole notes missed
|
||||
ms -= note_(at_).eighths * eighthMs_;
|
||||
at_ = (at_ + 1) % count_;
|
||||
}
|
||||
}
|
||||
note = note_(at_);
|
||||
lengthMs = note.eighths * eighthMs_;
|
||||
leftMs_ = lengthMs - ms;
|
||||
return true;
|
||||
}
|
||||
size_t position() const { return at_; }
|
||||
// A new pace, from the next note on.
|
||||
void setEighthMs(uint32_t ms) { eighthMs_ = ms; }
|
||||
uint32_t eighthMs() const { return eighthMs_; }
|
||||
|
||||
private:
|
||||
Note (*note_)(size_t);
|
||||
size_t count_;
|
||||
uint32_t eighthMs_;
|
||||
size_t at_ = 0;
|
||||
uint32_t leftMs_ = 0;
|
||||
bool begun_ = false;
|
||||
};
|
||||
|
||||
// Korobeiniki, the Russian folk song (1861) that Tetris is played to: the tune twice, then its
|
||||
// slow second part.
|
||||
namespace korobeiniki {
|
||||
constexpr uint16_t E4 = 330, Gs4 = 415, A4 = 440, B4 = 494, C5 = 523, D5 = 587, E5 = 659, F5 = 698, G5 = 784, Gs5 = 831, A5 = 880;
|
||||
// clang-format off
|
||||
constexpr Note kFirst[] = {
|
||||
{E5, 2}, {B4, 1}, {C5, 1}, {D5, 2}, {C5, 1}, {B4, 1},
|
||||
{A4, 2}, {A4, 1}, {C5, 1}, {E5, 2}, {D5, 1}, {C5, 1},
|
||||
{B4, 3}, {C5, 1}, {D5, 2}, {E5, 2},
|
||||
{C5, 2}, {A4, 2}, {A4, 2}, {0, 2},
|
||||
{0, 1}, {D5, 2}, {F5, 1}, {A5, 2}, {G5, 1}, {F5, 1},
|
||||
{E5, 3}, {C5, 1}, {E5, 2}, {D5, 1}, {C5, 1},
|
||||
{B4, 2}, {B4, 1}, {C5, 1}, {D5, 2}, {E5, 2},
|
||||
{C5, 2}, {A4, 2}, {A4, 2}, {0, 2},
|
||||
};
|
||||
constexpr Note kSecond[] = {
|
||||
{E5, 4}, {C5, 4}, {D5, 4}, {B4, 4}, {C5, 4}, {A4, 4}, {Gs4, 4}, {B4, 2}, {0, 2},
|
||||
{E5, 4}, {C5, 4}, {D5, 4}, {B4, 4}, {C5, 2}, {E5, 2}, {A5, 4}, {Gs5, 4}, {0, 4},
|
||||
};
|
||||
// clang-format on
|
||||
constexpr size_t kFirstCount = sizeof(kFirst) / sizeof(kFirst[0]), kSecondCount = sizeof(kSecond) / sizeof(kSecond[0]);
|
||||
constexpr size_t kCount = 2 * kFirstCount + kSecondCount;
|
||||
constexpr uint32_t kEighthMs = 200; // 150 quarter notes a minute, at the first level
|
||||
constexpr uint32_t kFastestEighthMs = 110; // 272: as fast as it is still a tune
|
||||
constexpr uint32_t kFasterMs = 8; // off each eighth, a level
|
||||
// The tune hurries as the game does: its pace at a level of Tetris, the first being 0.
|
||||
constexpr uint32_t eighthMsAt(int level) {
|
||||
return level <= 0 ? kEighthMs
|
||||
: static_cast<uint32_t>(level) * kFasterMs >= kEighthMs - kFastestEighthMs ? kFastestEighthMs
|
||||
: kEighthMs - static_cast<uint32_t>(level) * kFasterMs;
|
||||
}
|
||||
|
||||
// The whole of it, in the order it is played.
|
||||
constexpr Note at(size_t i) {
|
||||
return i < kFirstCount ? kFirst[i] : i < 2 * kFirstCount ? kFirst[i - kFirstCount] : kSecond[i - 2 * kFirstCount];
|
||||
}
|
||||
} // namespace korobeiniki
|
||||
|
||||
} // namespace roro::games
|
||||
@@ -0,0 +1,698 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <set>
|
||||
|
||||
#include "pong.h"
|
||||
#include "snake.h"
|
||||
#include "tetris.h"
|
||||
#include "tune.h"
|
||||
|
||||
using namespace roro::games;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
// ---- Pong ----
|
||||
|
||||
namespace {
|
||||
// Plays until the phase or an event says so; the player follows the ball if `follow`.
|
||||
uint8_t playPong(Pong& p, bool follow, uint8_t until, int maxSteps = 200000) {
|
||||
for (int i = 0; i < maxSteps; i++) {
|
||||
int move = !follow ? 0 : p.ballY() < p.playerY() - 2 ? -1 : p.ballY() > p.playerY() + 2 ? 1 : 0;
|
||||
uint8_t e = p.step(0.01f, move);
|
||||
if (e & until) return e;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void test_pong_starts_with_a_serve_from_the_middle() {
|
||||
Pong p(1);
|
||||
TEST_ASSERT_TRUE(p.phase() == Pong::Phase::Serve);
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01, Pong::kWidth / 2, p.ballX());
|
||||
p.step(Pong::kServePause / 2, 0);
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01, Pong::kWidth / 2, p.ballX()); // it waits
|
||||
for (int i = 0; i < 100; i++) p.step(0.01f, 0);
|
||||
TEST_ASSERT_TRUE(p.phase() == Pong::Phase::Play);
|
||||
p.step(0.1f, 0);
|
||||
TEST_ASSERT_TRUE(std::fabs(p.ballX() - Pong::kWidth / 2) > 5);
|
||||
}
|
||||
|
||||
void test_pong_the_same_seed_is_the_same_game() {
|
||||
Pong a(42), b(42), c(43);
|
||||
for (int i = 0; i < 500; i++) {
|
||||
a.step(0.01f, i % 3 - 1);
|
||||
b.step(0.01f, i % 3 - 1);
|
||||
c.step(0.01f, i % 3 - 1);
|
||||
}
|
||||
TEST_ASSERT_EQUAL_FLOAT(a.ballX(), b.ballX());
|
||||
TEST_ASSERT_EQUAL_FLOAT(a.ballY(), b.ballY());
|
||||
TEST_ASSERT_TRUE(a.ballY() != c.ballY() || a.ballX() != c.ballX());
|
||||
}
|
||||
|
||||
void test_pong_the_ball_stays_between_the_walls() {
|
||||
Pong p(7);
|
||||
bool wall = false;
|
||||
for (int i = 0; i < 20000; i++) {
|
||||
uint8_t e = p.step(0.01f, p.ballY() < p.playerY() ? -1 : 1);
|
||||
wall |= (e & Pong::Wall) != 0;
|
||||
TEST_ASSERT_TRUE(p.ballY() >= 0 && p.ballY() <= Pong::kHeight);
|
||||
if (p.phase() == Pong::Phase::Over) p.start();
|
||||
}
|
||||
TEST_ASSERT_TRUE(wall);
|
||||
}
|
||||
|
||||
void test_pong_a_paddle_stays_on_the_field() {
|
||||
Pong p(3);
|
||||
for (int i = 0; i < 300; i++) p.step(0.01f, -1);
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01, Pong::kPaddleH / 2, p.playerY());
|
||||
for (int i = 0; i < 300; i++) p.step(0.01f, 1);
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01, Pong::kHeight - Pong::kPaddleH / 2, p.playerY());
|
||||
}
|
||||
|
||||
void test_pong_a_return_counts_and_speeds_the_ball_up() {
|
||||
Pong p(5);
|
||||
float served = Pong::kServeSpeed;
|
||||
// Follow the ball until the player returns it once.
|
||||
int before = p.rally();
|
||||
for (int i = 0; i < 100000 && p.rally() == before; i++) {
|
||||
int move = p.ballY() < p.playerY() - 2 ? -1 : p.ballY() > p.playerY() + 2 ? 1 : 0;
|
||||
p.step(0.01f, move);
|
||||
}
|
||||
TEST_ASSERT_EQUAL(1, p.rally());
|
||||
TEST_ASSERT_EQUAL(1, p.longestRally());
|
||||
TEST_ASSERT_TRUE(p.speed() > served);
|
||||
TEST_ASSERT_TRUE(p.speed() <= Pong::kMaxSpeed);
|
||||
}
|
||||
|
||||
void test_pong_a_miss_is_a_point_for_the_other_side() {
|
||||
Pong p(9);
|
||||
// The player never moves from the top corner: sooner or later the ball gets past.
|
||||
for (int i = 0; i < 300; i++) p.step(0.01f, -1);
|
||||
while (p.deviceScore() == 0 && p.phase() != Pong::Phase::Over) p.step(0.01f, -1);
|
||||
TEST_ASSERT_EQUAL(1, p.deviceScore());
|
||||
TEST_ASSERT_TRUE(p.phase() == Pong::Phase::Serve); // served again, from the middle
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01, Pong::kWidth / 2, p.ballX());
|
||||
TEST_ASSERT_EQUAL(0, p.rally());
|
||||
}
|
||||
|
||||
void test_pong_ends_at_seven() {
|
||||
Pong p(11);
|
||||
uint8_t e = playPong(p, false, Pong::Over); // a player who does nothing loses
|
||||
TEST_ASSERT_TRUE(e & Pong::Over);
|
||||
TEST_ASSERT_TRUE(p.phase() == Pong::Phase::Over);
|
||||
TEST_ASSERT_EQUAL(Pong::kWinScore, std::max(p.playerScore(), p.deviceScore()));
|
||||
TEST_ASSERT_EQUAL(0, p.step(1.0f, 1)); // nothing moves once it is over
|
||||
p.start();
|
||||
TEST_ASSERT_EQUAL(0, p.deviceScore());
|
||||
TEST_ASSERT_TRUE(p.phase() == Pong::Phase::Serve);
|
||||
}
|
||||
|
||||
void test_pong_the_device_can_be_beaten() {
|
||||
// A player who always meets the ball: the device, slower than a steep fast ball, must lose
|
||||
// points over a few games.
|
||||
int playerPoints = 0;
|
||||
for (uint32_t seed = 1; seed <= 5; seed++) {
|
||||
Pong p(seed);
|
||||
playPong(p, true, Pong::Over, 2000000);
|
||||
playerPoints += p.playerScore();
|
||||
}
|
||||
TEST_ASSERT_TRUE(playerPoints > 0);
|
||||
}
|
||||
|
||||
void test_pong_the_device_paddle_has_a_top_speed() {
|
||||
Pong p(13);
|
||||
float before = p.deviceY();
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
p.step(0.01f, 0);
|
||||
TEST_ASSERT_TRUE(std::fabs(p.deviceY() - before) <= Pong::kDeviceSpeed * 0.01f + 0.01f);
|
||||
before = p.deviceY();
|
||||
if (p.phase() == Pong::Phase::Over) p.start();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Snake ----
|
||||
|
||||
namespace {
|
||||
void moves(Snake& s, int n) {
|
||||
for (int i = 0; i < n && s.alive(); i++) s.step(s.intervalMs());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void test_snake_starts_in_the_middle_three_long() {
|
||||
Snake s(1);
|
||||
TEST_ASSERT_TRUE(s.alive());
|
||||
TEST_ASSERT_EQUAL(3, s.body().size());
|
||||
TEST_ASSERT_EQUAL(Snake::kCols / 2, s.body().front().col);
|
||||
TEST_ASSERT_EQUAL(Snake::kRows / 2, s.body().front().row);
|
||||
TEST_ASSERT_TRUE(s.heading() == Snake::Dir::Right);
|
||||
for (auto& c : s.body()) TEST_ASSERT_FALSE(c == s.food());
|
||||
}
|
||||
|
||||
void test_snake_moves_at_its_pace() {
|
||||
Snake s(1);
|
||||
s.putFood({0, 0});
|
||||
TEST_ASSERT_EQUAL(0, s.step(Snake::kSlowestMs - 1));
|
||||
TEST_ASSERT_EQUAL(Snake::Moved, s.step(1));
|
||||
TEST_ASSERT_EQUAL(Snake::kCols / 2 + 1, s.body().front().col);
|
||||
TEST_ASSERT_EQUAL(3, s.body().size());
|
||||
}
|
||||
|
||||
void test_snake_turns_but_not_back_on_itself() {
|
||||
Snake s(1);
|
||||
s.putFood({0, 0});
|
||||
s.turn(Snake::Dir::Left); // straight back: not a turn
|
||||
moves(s, 1);
|
||||
TEST_ASSERT_TRUE(s.heading() == Snake::Dir::Right);
|
||||
s.turn(Snake::Dir::Up);
|
||||
moves(s, 1);
|
||||
TEST_ASSERT_TRUE(s.heading() == Snake::Dir::Up);
|
||||
TEST_ASSERT_EQUAL(Snake::kRows / 2 - 1, s.body().front().row);
|
||||
}
|
||||
|
||||
void test_snake_remembers_a_quick_corner() {
|
||||
Snake s(1);
|
||||
s.putFood({0, 0});
|
||||
s.turn(Snake::Dir::Up); // both before the next move
|
||||
s.turn(Snake::Dir::Left); // would be "back" for a snake heading right, but not after the first
|
||||
s.turn(Snake::Dir::Down); // a third is not remembered
|
||||
moves(s, 1);
|
||||
TEST_ASSERT_TRUE(s.heading() == Snake::Dir::Up);
|
||||
moves(s, 1);
|
||||
TEST_ASSERT_TRUE(s.heading() == Snake::Dir::Left);
|
||||
moves(s, 1);
|
||||
TEST_ASSERT_TRUE(s.heading() == Snake::Dir::Left);
|
||||
TEST_ASSERT_TRUE(s.alive());
|
||||
}
|
||||
|
||||
void test_snake_eats_grows_and_gets_new_food() {
|
||||
Snake s(1);
|
||||
Snake::Cell head = s.body().front();
|
||||
s.putFood({static_cast<int8_t>(head.col + 1), head.row});
|
||||
uint8_t e = s.step(s.intervalMs());
|
||||
TEST_ASSERT_TRUE(e & Snake::Ate);
|
||||
TEST_ASSERT_EQUAL(1, s.score());
|
||||
TEST_ASSERT_EQUAL(4, s.body().size());
|
||||
for (auto& c : s.body()) TEST_ASSERT_FALSE(c == s.food());
|
||||
TEST_ASSERT_TRUE(s.food().col >= 0 && s.food().col < Snake::kCols && s.food().row >= 0 && s.food().row < Snake::kRows);
|
||||
}
|
||||
|
||||
void test_snake_dies_at_a_wall() {
|
||||
Snake s(1);
|
||||
s.putFood({0, 0});
|
||||
uint8_t last = 0;
|
||||
for (int i = 0; i < Snake::kCols && s.alive(); i++) last = s.step(s.intervalMs());
|
||||
TEST_ASSERT_FALSE(s.alive());
|
||||
TEST_ASSERT_TRUE(last & Snake::Died);
|
||||
TEST_ASSERT_EQUAL(Snake::kCols - 1, s.body().front().col); // it stopped at the edge
|
||||
TEST_ASSERT_EQUAL(3, s.body().size()); // whole
|
||||
TEST_ASSERT_EQUAL(0, s.step(1000));
|
||||
}
|
||||
|
||||
void test_snake_dies_on_itself() {
|
||||
Snake s(1);
|
||||
// Fed four times in a row, it is long enough to bite itself in a tight turn.
|
||||
for (int i = 0; i < 4; i++) {
|
||||
Snake::Cell head = s.body().front();
|
||||
s.putFood({static_cast<int8_t>(head.col + 1), head.row});
|
||||
s.step(s.intervalMs());
|
||||
}
|
||||
s.putFood({0, 0});
|
||||
TEST_ASSERT_EQUAL(7, s.body().size());
|
||||
s.turn(Snake::Dir::Down);
|
||||
moves(s, 1);
|
||||
s.turn(Snake::Dir::Left);
|
||||
moves(s, 1);
|
||||
s.turn(Snake::Dir::Up);
|
||||
moves(s, 1);
|
||||
TEST_ASSERT_FALSE(s.alive());
|
||||
}
|
||||
|
||||
void test_snake_may_follow_its_own_tail() {
|
||||
Snake s(1);
|
||||
Snake::Cell head = s.body().front();
|
||||
s.putFood({static_cast<int8_t>(head.col + 1), head.row});
|
||||
s.step(s.intervalMs()); // four long: a square of 2 x 2 is its own tail chased
|
||||
s.putFood({0, 0});
|
||||
for (auto d : {Snake::Dir::Down, Snake::Dir::Left, Snake::Dir::Up, Snake::Dir::Right, Snake::Dir::Down}) {
|
||||
s.turn(d);
|
||||
moves(s, 1);
|
||||
}
|
||||
TEST_ASSERT_TRUE(s.alive());
|
||||
}
|
||||
|
||||
void test_snake_gets_faster_as_it_eats() {
|
||||
Snake s(1);
|
||||
uint32_t start = s.intervalMs();
|
||||
TEST_ASSERT_EQUAL(Snake::kSlowestMs, start);
|
||||
for (int i = 0; i < Snake::kFoodsAStep; i++) {
|
||||
Snake::Cell head = s.body().front();
|
||||
s.putFood({static_cast<int8_t>(head.col + 1), head.row});
|
||||
s.step(s.intervalMs());
|
||||
}
|
||||
TEST_ASSERT_EQUAL(start - Snake::kFasterMs, s.intervalMs());
|
||||
}
|
||||
|
||||
void test_snake_the_same_seed_puts_the_food_in_the_same_places() {
|
||||
Snake a(77), b(77), c(78);
|
||||
TEST_ASSERT_TRUE(a.food() == b.food());
|
||||
bool differs = !(a.food() == c.food());
|
||||
for (int i = 0; i < 3; i++) {
|
||||
for (Snake* s : {&a, &b, &c}) {
|
||||
Snake::Cell head = s->body().front();
|
||||
s->putFood({static_cast<int8_t>(head.col + 1), head.row});
|
||||
s->step(s->intervalMs());
|
||||
}
|
||||
TEST_ASSERT_TRUE(a.food() == b.food());
|
||||
differs |= !(a.food() == c.food());
|
||||
}
|
||||
TEST_ASSERT_TRUE(differs);
|
||||
}
|
||||
|
||||
// ---- Tetris ----
|
||||
|
||||
namespace {
|
||||
const TetrisInput kNothing;
|
||||
TetrisInput press(bool TetrisInput::*key) {
|
||||
TetrisInput in;
|
||||
in.*key = true;
|
||||
return in;
|
||||
}
|
||||
int filled(const Tetris& t) {
|
||||
int n = 0;
|
||||
for (int r = 0; r < Tetris::kRows; r++)
|
||||
for (int c = 0; c < Tetris::kCols; c++) n += t.cell(c, r) != 0;
|
||||
return n;
|
||||
}
|
||||
// A row filled but for one column.
|
||||
void fillRow(Tetris& t, int row, int gap) {
|
||||
for (int c = 0; c < Tetris::kCols; c++)
|
||||
if (c != gap) t.put(c, row, 7);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void test_tetris_deals_each_piece_once_in_seven() {
|
||||
Tetris t(5);
|
||||
for (int bag = 0; bag < 4; bag++) {
|
||||
std::set<int> seen;
|
||||
for (int i = 0; i < 7; i++) {
|
||||
seen.insert(t.piece().kind);
|
||||
t.step(0, press(&TetrisInput::hardDrop));
|
||||
if (t.over()) t.start(), i = -1, seen.clear(); // start over on a full well
|
||||
}
|
||||
TEST_ASSERT_EQUAL(7, seen.size());
|
||||
t.start();
|
||||
}
|
||||
}
|
||||
|
||||
void test_tetris_shows_the_piece_that_comes_next() {
|
||||
Tetris t(9);
|
||||
uint8_t next = t.next();
|
||||
t.step(0, press(&TetrisInput::hardDrop));
|
||||
TEST_ASSERT_EQUAL(next, t.piece().kind);
|
||||
}
|
||||
|
||||
void test_tetris_a_piece_falls_at_the_levels_pace() {
|
||||
Tetris t(1);
|
||||
t.give(3); // a T
|
||||
int row = t.piece().row;
|
||||
TEST_ASSERT_EQUAL(0, t.step(Tetris::kSlowestMs - 1, kNothing));
|
||||
TEST_ASSERT_EQUAL(row, t.piece().row);
|
||||
TEST_ASSERT_EQUAL(Tetris::Moved, t.step(1, kNothing));
|
||||
TEST_ASSERT_EQUAL(row + 1, t.piece().row);
|
||||
}
|
||||
|
||||
void test_tetris_a_soft_drop_is_faster_and_scores() {
|
||||
Tetris t(1);
|
||||
t.give(3);
|
||||
int row = t.piece().row;
|
||||
t.step(Tetris::kSoftMs * 3, press(&TetrisInput::soft));
|
||||
TEST_ASSERT_EQUAL(row + 3, t.piece().row);
|
||||
TEST_ASSERT_EQUAL(3, t.score());
|
||||
}
|
||||
|
||||
void test_tetris_a_held_soft_drop_stops_with_its_piece() {
|
||||
Tetris t(1);
|
||||
t.give(2);
|
||||
TetrisInput soft = press(&TetrisInput::soft);
|
||||
uint8_t all = 0;
|
||||
for (int i = 0; i < 40 && !(all & Tetris::Locked); i++) all |= t.step(Tetris::kSoftMs, soft);
|
||||
TEST_ASSERT_TRUE(all & Tetris::Locked);
|
||||
// The key is still down: the next piece falls at the level's pace, not the fast one.
|
||||
int row = t.piece().row, score = t.score();
|
||||
t.step(Tetris::kSoftMs * 4, soft);
|
||||
TEST_ASSERT_EQUAL(row, t.piece().row);
|
||||
TEST_ASSERT_EQUAL(score, t.score());
|
||||
t.step(0, kNothing); // let go, and pressed again: it works
|
||||
t.step(Tetris::kSoftMs * 2, soft);
|
||||
TEST_ASSERT_TRUE(t.piece().row > row);
|
||||
}
|
||||
|
||||
void test_tetris_moves_stop_at_the_walls() {
|
||||
Tetris t(1);
|
||||
t.give(2); // an O: two wide
|
||||
for (int i = 0; i < 20; i++) {
|
||||
t.step(0, press(&TetrisInput::left));
|
||||
t.step(0, kNothing); // released: the next press is a press
|
||||
}
|
||||
int leftmost = Tetris::kCols;
|
||||
for (auto& c : Tetris::cells(t.piece())) leftmost = std::min<int>(leftmost, c.col);
|
||||
TEST_ASSERT_EQUAL(0, leftmost);
|
||||
for (int i = 0; i < 20; i++) {
|
||||
t.step(0, press(&TetrisInput::right));
|
||||
t.step(0, kNothing);
|
||||
}
|
||||
int rightmost = 0;
|
||||
for (auto& c : Tetris::cells(t.piece())) rightmost = std::max<int>(rightmost, c.col);
|
||||
TEST_ASSERT_EQUAL(Tetris::kCols - 1, rightmost);
|
||||
}
|
||||
|
||||
void test_tetris_a_held_key_repeats() {
|
||||
Tetris t(1);
|
||||
t.give(2);
|
||||
int col = t.piece().col;
|
||||
TetrisInput left = press(&TetrisInput::left);
|
||||
t.step(0, left); // the press: one step at once
|
||||
TEST_ASSERT_EQUAL(col - 1, t.piece().col);
|
||||
t.step(Tetris::kRepeatAfterMs - 1, left);
|
||||
TEST_ASSERT_EQUAL(col - 1, t.piece().col); // not yet
|
||||
t.step(1, left);
|
||||
TEST_ASSERT_EQUAL(col - 2, t.piece().col);
|
||||
t.step(Tetris::kRepeatEveryMs, left);
|
||||
TEST_ASSERT_EQUAL(col - 3, t.piece().col);
|
||||
TetrisInput both = left;
|
||||
both.right = true; // both held: neither
|
||||
t.step(Tetris::kRepeatEveryMs * 3, both);
|
||||
TEST_ASSERT_EQUAL(col - 3, t.piece().col);
|
||||
}
|
||||
|
||||
void test_tetris_turns_and_turns_back() {
|
||||
Tetris t(1);
|
||||
t.give(3);
|
||||
auto before = Tetris::cells(t.piece());
|
||||
TEST_ASSERT_EQUAL(Tetris::Turned, t.step(0, press(&TetrisInput::rotate)));
|
||||
TEST_ASSERT_EQUAL(1, t.piece().turn);
|
||||
t.step(0, press(&TetrisInput::rotateBack));
|
||||
TEST_ASSERT_EQUAL(0, t.piece().turn);
|
||||
auto after = Tetris::cells(t.piece());
|
||||
for (int i = 0; i < 4; i++) {
|
||||
TEST_ASSERT_EQUAL(before[i].col, after[i].col);
|
||||
TEST_ASSERT_EQUAL(before[i].row, after[i].row);
|
||||
}
|
||||
for (int i = 0; i < 4; i++) t.step(0, press(&TetrisInput::rotate));
|
||||
TEST_ASSERT_EQUAL(0, t.piece().turn); // four quarter turns: as it was
|
||||
}
|
||||
|
||||
void test_tetris_a_turn_against_a_wall_shifts_the_piece() {
|
||||
Tetris t(1);
|
||||
t.give(1); // the long one
|
||||
t.step(0, press(&TetrisInput::rotate)); // upright
|
||||
for (int i = 0; i < 10; i++) {
|
||||
t.step(0, press(&TetrisInput::right));
|
||||
t.step(0, kNothing);
|
||||
}
|
||||
for (auto& c : Tetris::cells(t.piece())) TEST_ASSERT_EQUAL(Tetris::kCols - 1, c.col); // against the right wall
|
||||
TEST_ASSERT_EQUAL(Tetris::Turned, t.step(0, press(&TetrisInput::rotate))); // flat again: it needs four columns
|
||||
for (auto& c : Tetris::cells(t.piece())) TEST_ASSERT_TRUE(c.col >= Tetris::kCols - 4 && c.col < Tetris::kCols);
|
||||
}
|
||||
|
||||
void test_tetris_a_turn_with_no_room_does_not_happen() {
|
||||
Tetris t(1);
|
||||
t.give(1);
|
||||
t.step(0, press(&TetrisInput::rotate));
|
||||
// Walled in on both sides, a column wide.
|
||||
int col = Tetris::cells(t.piece())[0].col;
|
||||
for (int r = 0; r < Tetris::kRows; r++)
|
||||
for (int c = 0; c < Tetris::kCols; c++)
|
||||
if (c != col) t.put(c, r, 7);
|
||||
TEST_ASSERT_EQUAL(0, t.step(0, press(&TetrisInput::rotate)));
|
||||
TEST_ASSERT_EQUAL(1, t.piece().turn);
|
||||
}
|
||||
|
||||
void test_tetris_a_hard_drop_lands_locks_and_scores() {
|
||||
Tetris t(1);
|
||||
t.give(2); // an O
|
||||
int from = t.piece().row;
|
||||
uint8_t e = t.step(0, press(&TetrisInput::hardDrop));
|
||||
TEST_ASSERT_TRUE(e & Tetris::Locked);
|
||||
TEST_ASSERT_EQUAL(4, filled(t));
|
||||
TEST_ASSERT_NOT_EQUAL(0, t.cell(4, Tetris::kRows - 1));
|
||||
TEST_ASSERT_NOT_EQUAL(0, t.cell(5, Tetris::kRows - 2));
|
||||
TEST_ASSERT_EQUAL(2 * (Tetris::kRows - 2 - from), t.score()); // two a row fallen
|
||||
TEST_ASSERT_EQUAL(0, t.lastCleared());
|
||||
}
|
||||
|
||||
void test_tetris_a_piece_locks_when_it_can_fall_no_more() {
|
||||
Tetris t(1);
|
||||
t.give(2);
|
||||
uint8_t all = 0;
|
||||
for (int i = 0; i < Tetris::kRows + 2 && !(all & Tetris::Locked); i++) all |= t.step(Tetris::kSlowestMs, kNothing);
|
||||
TEST_ASSERT_TRUE(all & Tetris::Locked);
|
||||
TEST_ASSERT_EQUAL(4, filled(t));
|
||||
}
|
||||
|
||||
void test_tetris_a_full_row_goes_and_the_rest_comes_down() {
|
||||
Tetris t(1);
|
||||
fillRow(t, Tetris::kRows - 1, 4); // the gap where the O will not fit: use the long one upright
|
||||
t.put(0, Tetris::kRows - 2, 5); // something above, to see it come down
|
||||
t.give(1);
|
||||
t.step(0, press(&TetrisInput::rotate)); // upright, in column 5
|
||||
t.step(0, press(&TetrisInput::left)); // column 4
|
||||
uint8_t e = t.step(0, press(&TetrisInput::hardDrop));
|
||||
TEST_ASSERT_TRUE(e & Tetris::Lines);
|
||||
TEST_ASSERT_EQUAL(1, t.lastCleared());
|
||||
TEST_ASSERT_EQUAL(1, t.lines());
|
||||
TEST_ASSERT_EQUAL(5, t.cell(0, Tetris::kRows - 1)); // what was above
|
||||
TEST_ASSERT_EQUAL(1, t.cell(4, Tetris::kRows - 1)); // and the rest of the long piece
|
||||
TEST_ASSERT_EQUAL(0, t.cell(1, Tetris::kRows - 1));
|
||||
TEST_ASSERT_EQUAL(1 + 3, filled(t));
|
||||
}
|
||||
|
||||
void test_tetris_four_rows_at_once_score_most() {
|
||||
Tetris t(1);
|
||||
for (int r = Tetris::kRows - 4; r < Tetris::kRows; r++) fillRow(t, r, 9);
|
||||
t.give(1);
|
||||
t.step(0, press(&TetrisInput::rotate));
|
||||
for (int i = 0; i < 10; i++) {
|
||||
t.step(0, press(&TetrisInput::right));
|
||||
t.step(0, kNothing);
|
||||
}
|
||||
int before = t.score();
|
||||
uint8_t e = t.step(0, press(&TetrisInput::hardDrop));
|
||||
TEST_ASSERT_TRUE(e & Tetris::Lines);
|
||||
TEST_ASSERT_EQUAL(4, t.lastCleared());
|
||||
TEST_ASSERT_EQUAL(0, filled(t));
|
||||
int dropped = (t.score() - before) - 800;
|
||||
TEST_ASSERT_TRUE(dropped > 0 && dropped <= 2 * Tetris::kRows); // 800 for the four, and the drop
|
||||
}
|
||||
|
||||
void test_tetris_ten_lines_are_a_level_and_a_faster_fall() {
|
||||
Tetris t(1);
|
||||
TEST_ASSERT_EQUAL(0, t.level());
|
||||
TEST_ASSERT_EQUAL(Tetris::kSlowestMs, t.fallMs());
|
||||
uint8_t all = 0;
|
||||
for (int i = 0; i < 3; i++) { // 4 + 4 + 4
|
||||
for (int r = Tetris::kRows - 4; r < Tetris::kRows; r++) fillRow(t, r, 9);
|
||||
t.give(1);
|
||||
t.step(0, press(&TetrisInput::rotate));
|
||||
for (int k = 0; k < 10; k++) {
|
||||
t.step(0, press(&TetrisInput::right));
|
||||
t.step(0, kNothing);
|
||||
}
|
||||
all |= t.step(0, press(&TetrisInput::hardDrop));
|
||||
}
|
||||
TEST_ASSERT_EQUAL(12, t.lines());
|
||||
TEST_ASSERT_EQUAL(1, t.level());
|
||||
TEST_ASSERT_TRUE(all & Tetris::LevelUp);
|
||||
TEST_ASSERT_EQUAL(Tetris::kSlowestMs * 85 / 100, t.fallMs());
|
||||
}
|
||||
|
||||
void test_tetris_the_fall_never_gets_faster_than_its_limit() {
|
||||
Tetris t(1);
|
||||
for (int i = 0; i < 80; i++) {
|
||||
for (int r = Tetris::kRows - 4; r < Tetris::kRows; r++) fillRow(t, r, 9);
|
||||
t.give(1);
|
||||
t.step(0, press(&TetrisInput::rotate));
|
||||
for (int k = 0; k < 10; k++) {
|
||||
t.step(0, press(&TetrisInput::right));
|
||||
t.step(0, kNothing);
|
||||
}
|
||||
t.step(0, press(&TetrisInput::hardDrop));
|
||||
}
|
||||
TEST_ASSERT_TRUE(t.level() >= 30);
|
||||
TEST_ASSERT_EQUAL(Tetris::kFastestMs, t.fallMs());
|
||||
}
|
||||
|
||||
void test_tetris_is_over_when_a_piece_has_no_room() {
|
||||
Tetris t(3);
|
||||
uint8_t last = 0;
|
||||
for (int i = 0; i < 40 && !t.over(); i++) last = t.step(0, press(&TetrisInput::hardDrop)); // all in the middle
|
||||
TEST_ASSERT_TRUE(t.over());
|
||||
TEST_ASSERT_TRUE(last & Tetris::Over);
|
||||
TEST_ASSERT_EQUAL(0, t.step(1000, press(&TetrisInput::hardDrop)));
|
||||
t.start();
|
||||
TEST_ASSERT_FALSE(t.over());
|
||||
TEST_ASSERT_EQUAL(0, filled(t));
|
||||
TEST_ASSERT_EQUAL(0, t.score());
|
||||
}
|
||||
|
||||
void test_tetris_the_same_seed_deals_the_same_pieces() {
|
||||
Tetris a(21), b(21), c(22);
|
||||
bool differs = false;
|
||||
for (int i = 0; i < 14; i++) {
|
||||
TEST_ASSERT_EQUAL(a.piece().kind, b.piece().kind);
|
||||
differs |= a.piece().kind != c.piece().kind;
|
||||
for (Tetris* t : {&a, &b, &c}) {
|
||||
t->step(0, press(&TetrisInput::hardDrop));
|
||||
if (t->over()) t->start();
|
||||
}
|
||||
}
|
||||
TEST_ASSERT_TRUE(differs);
|
||||
}
|
||||
|
||||
void test_tetris_every_piece_in_every_turn_has_four_cells_in_its_box() {
|
||||
for (uint8_t kind = 1; kind <= 7; kind++)
|
||||
for (uint8_t turn = 0; turn < 4; turn++) {
|
||||
Tetris::Piece p{kind, turn, 0, 0};
|
||||
std::set<int> where;
|
||||
for (auto& c : Tetris::cells(p)) {
|
||||
TEST_ASSERT_TRUE(c.col >= 0 && c.col < 4 && c.row >= 0 && c.row < 4);
|
||||
where.insert(c.row * 4 + c.col);
|
||||
}
|
||||
TEST_ASSERT_EQUAL(4, where.size());
|
||||
}
|
||||
}
|
||||
|
||||
// ---- The tune ----
|
||||
|
||||
void test_a_tune_gives_its_notes_in_time() {
|
||||
static const Note kThree[] = {{440, 2}, {0, 1}, {880, 1}};
|
||||
TunePlayer p([](size_t i) { return kThree[i]; }, 3, 100);
|
||||
Note n{};
|
||||
uint32_t ms = 0;
|
||||
TEST_ASSERT_TRUE(p.step(0, n, ms)); // the first note at once
|
||||
TEST_ASSERT_EQUAL(440, n.hz);
|
||||
TEST_ASSERT_EQUAL(200, ms);
|
||||
TEST_ASSERT_FALSE(p.step(199, n, ms));
|
||||
TEST_ASSERT_TRUE(p.step(1, n, ms)); // a rest is a note too: the caller plays nothing
|
||||
TEST_ASSERT_EQUAL(0, n.hz);
|
||||
TEST_ASSERT_EQUAL(100, ms);
|
||||
TEST_ASSERT_TRUE(p.step(100, n, ms));
|
||||
TEST_ASSERT_EQUAL(880, n.hz);
|
||||
TEST_ASSERT_TRUE(p.step(100, n, ms)); // and round again
|
||||
TEST_ASSERT_EQUAL(440, n.hz);
|
||||
TEST_ASSERT_EQUAL(0, p.position());
|
||||
}
|
||||
|
||||
void test_a_tune_keeps_time_over_uneven_steps() {
|
||||
static const Note kTwo[] = {{440, 1}, {880, 1}};
|
||||
TunePlayer p([](size_t i) { return kTwo[i]; }, 2, 100);
|
||||
Note n{};
|
||||
uint32_t ms = 0;
|
||||
p.step(0, n, ms);
|
||||
int notes = 1;
|
||||
for (int i = 0; i < 300; i++) notes += p.step(33, n, ms); // 9.9 seconds in steps of 33 ms
|
||||
TEST_ASSERT_EQUAL(100, notes); // one every 100 ms: the 33s do not add up to a drift
|
||||
}
|
||||
|
||||
void test_a_tune_skips_what_a_long_gap_missed() {
|
||||
static const Note kFour[] = {{100, 1}, {200, 1}, {300, 1}, {400, 1}};
|
||||
TunePlayer p([](size_t i) { return kFour[i]; }, 4, 100);
|
||||
Note n{};
|
||||
uint32_t ms = 0;
|
||||
p.step(0, n, ms);
|
||||
TEST_ASSERT_TRUE(p.step(250, n, ms)); // 200 and half of 300 went by
|
||||
TEST_ASSERT_EQUAL(300, n.hz);
|
||||
TEST_ASSERT_FALSE(p.step(49, n, ms));
|
||||
TEST_ASSERT_TRUE(p.step(1, n, ms));
|
||||
TEST_ASSERT_EQUAL(400, n.hz);
|
||||
p.restart();
|
||||
TEST_ASSERT_TRUE(p.step(0, n, ms));
|
||||
TEST_ASSERT_EQUAL(100, n.hz);
|
||||
}
|
||||
|
||||
void test_korobeiniki_is_whole_bars() {
|
||||
// Each part is eight bars of eight eighths, and it begins on its famous E.
|
||||
int first = 0, second = 0;
|
||||
for (size_t i = 0; i < korobeiniki::kFirstCount; i++) first += korobeiniki::kFirst[i].eighths;
|
||||
for (size_t i = 0; i < korobeiniki::kSecondCount; i++) second += korobeiniki::kSecond[i].eighths;
|
||||
TEST_ASSERT_EQUAL(64, first);
|
||||
TEST_ASSERT_EQUAL(64, second);
|
||||
TEST_ASSERT_EQUAL(659, korobeiniki::at(0).hz);
|
||||
TEST_ASSERT_EQUAL(659, korobeiniki::at(korobeiniki::kFirstCount).hz); // the tune again
|
||||
TEST_ASSERT_EQUAL(659, korobeiniki::at(2 * korobeiniki::kFirstCount).hz); // then the slow part
|
||||
TEST_ASSERT_EQUAL(4, korobeiniki::at(2 * korobeiniki::kFirstCount).eighths);
|
||||
TEST_ASSERT_EQUAL(0, korobeiniki::at(korobeiniki::kCount - 1).hz); // and a breath before it starts over
|
||||
}
|
||||
|
||||
void test_the_tune_hurries_with_the_level() {
|
||||
TEST_ASSERT_EQUAL(200, korobeiniki::eighthMsAt(0));
|
||||
TEST_ASSERT_EQUAL(192, korobeiniki::eighthMsAt(1));
|
||||
TEST_ASSERT_EQUAL(160, korobeiniki::eighthMsAt(5));
|
||||
TEST_ASSERT_EQUAL(korobeiniki::kFastestEighthMs, korobeiniki::eighthMsAt(12));
|
||||
TEST_ASSERT_EQUAL(korobeiniki::kFastestEighthMs, korobeiniki::eighthMsAt(99));
|
||||
for (int level = 0; level < 20; level++) TEST_ASSERT_TRUE(korobeiniki::eighthMsAt(level + 1) <= korobeiniki::eighthMsAt(level));
|
||||
}
|
||||
|
||||
void test_a_new_pace_starts_at_the_next_note() {
|
||||
static const Note kTwo[] = {{440, 1}, {880, 1}};
|
||||
TunePlayer p([](size_t i) { return kTwo[i]; }, 2, 100);
|
||||
Note n{};
|
||||
uint32_t ms = 0;
|
||||
p.step(0, n, ms);
|
||||
p.setEighthMs(50);
|
||||
TEST_ASSERT_FALSE(p.step(99, n, ms)); // the note that had begun keeps its length
|
||||
TEST_ASSERT_TRUE(p.step(1, n, ms));
|
||||
TEST_ASSERT_EQUAL(50, ms);
|
||||
TEST_ASSERT_TRUE(p.step(50, n, ms));
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_pong_starts_with_a_serve_from_the_middle);
|
||||
RUN_TEST(test_pong_the_same_seed_is_the_same_game);
|
||||
RUN_TEST(test_pong_the_ball_stays_between_the_walls);
|
||||
RUN_TEST(test_pong_a_paddle_stays_on_the_field);
|
||||
RUN_TEST(test_pong_a_return_counts_and_speeds_the_ball_up);
|
||||
RUN_TEST(test_pong_a_miss_is_a_point_for_the_other_side);
|
||||
RUN_TEST(test_pong_ends_at_seven);
|
||||
RUN_TEST(test_pong_the_device_can_be_beaten);
|
||||
RUN_TEST(test_pong_the_device_paddle_has_a_top_speed);
|
||||
RUN_TEST(test_snake_starts_in_the_middle_three_long);
|
||||
RUN_TEST(test_snake_moves_at_its_pace);
|
||||
RUN_TEST(test_snake_turns_but_not_back_on_itself);
|
||||
RUN_TEST(test_snake_remembers_a_quick_corner);
|
||||
RUN_TEST(test_snake_eats_grows_and_gets_new_food);
|
||||
RUN_TEST(test_snake_dies_at_a_wall);
|
||||
RUN_TEST(test_snake_dies_on_itself);
|
||||
RUN_TEST(test_snake_may_follow_its_own_tail);
|
||||
RUN_TEST(test_snake_gets_faster_as_it_eats);
|
||||
RUN_TEST(test_snake_the_same_seed_puts_the_food_in_the_same_places);
|
||||
RUN_TEST(test_tetris_deals_each_piece_once_in_seven);
|
||||
RUN_TEST(test_tetris_shows_the_piece_that_comes_next);
|
||||
RUN_TEST(test_tetris_a_piece_falls_at_the_levels_pace);
|
||||
RUN_TEST(test_tetris_a_soft_drop_is_faster_and_scores);
|
||||
RUN_TEST(test_tetris_a_held_soft_drop_stops_with_its_piece);
|
||||
RUN_TEST(test_tetris_moves_stop_at_the_walls);
|
||||
RUN_TEST(test_tetris_a_held_key_repeats);
|
||||
RUN_TEST(test_tetris_turns_and_turns_back);
|
||||
RUN_TEST(test_tetris_a_turn_against_a_wall_shifts_the_piece);
|
||||
RUN_TEST(test_tetris_a_turn_with_no_room_does_not_happen);
|
||||
RUN_TEST(test_tetris_a_hard_drop_lands_locks_and_scores);
|
||||
RUN_TEST(test_tetris_a_piece_locks_when_it_can_fall_no_more);
|
||||
RUN_TEST(test_tetris_a_full_row_goes_and_the_rest_comes_down);
|
||||
RUN_TEST(test_tetris_four_rows_at_once_score_most);
|
||||
RUN_TEST(test_tetris_ten_lines_are_a_level_and_a_faster_fall);
|
||||
RUN_TEST(test_tetris_the_fall_never_gets_faster_than_its_limit);
|
||||
RUN_TEST(test_tetris_is_over_when_a_piece_has_no_room);
|
||||
RUN_TEST(test_tetris_the_same_seed_deals_the_same_pieces);
|
||||
RUN_TEST(test_tetris_every_piece_in_every_turn_has_four_cells_in_its_box);
|
||||
RUN_TEST(test_a_tune_gives_its_notes_in_time);
|
||||
RUN_TEST(test_a_tune_keeps_time_over_uneven_steps);
|
||||
RUN_TEST(test_a_tune_skips_what_a_long_gap_missed);
|
||||
RUN_TEST(test_korobeiniki_is_whole_bars);
|
||||
RUN_TEST(test_the_tune_hurries_with_the_level);
|
||||
RUN_TEST(test_a_new_pace_starts_at_the_next_note);
|
||||
return UNITY_END();
|
||||
}
|
||||
Reference in New Issue
Block a user