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,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
|
||||
Reference in New Issue
Block a user