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:
2026-10-13 22:59:05 +02:00
co-authored by Claude Opus 5.5
parent 5c660d866d
commit 68d50b320f
9 changed files with 1417 additions and 0 deletions
+25
View File
@@ -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
+103
View File
@@ -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
+55
View File
@@ -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
+92
View File
@@ -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
+60
View File
@@ -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
+202
View File
@@ -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
+81
View File
@@ -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
+101
View File
@@ -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