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