#include "tetris.h" #include 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(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::cells(const Piece& p) { std::array 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(n++)] = {static_cast(p.col + i % 4), static_cast(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(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(moved.col + dc); moved.row = static_cast(moved.row + dr); if (!fits(moved)) return false; piece_ = moved; return true; } bool Tetris::rotate(int by) { Piece turned = piece_; turned.turn = static_cast((turned.turn + 4 + by) & 3); for (auto& kick : kKicks) { Piece tried = turned; tried.col = static_cast(tried.col + kick[0]); tried.row = static_cast(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(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(events | Over); } well_[static_cast(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(r * kCols + col)] = cell(col, r - 1); for (int col = 0; col < kCols; col++) well_[static_cast(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(events | spawn(coming)); } uint8_t Tetris::step(uint32_t ms, const TetrisInput& in) { if (over_) return 0; uint8_t events = 0; ms = std::min(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(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