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
+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