目次

Tetris Take 11

作成日: 2023-07-27 (木)

初心者によるC++入門 #43 最終回 リセットできるようにする

main.cpp

#include "board.h"
#include "gfxaux.h"
#include "piece.h"
#include "shape.h"
#include "player.h"
#include "eventprocessor.h"

#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <iostream>
#include <algorithm>
#include <vector>

auto setup_board() -> BoardInfo;
auto setup_shapes(SDL_Texture *texture) -> ShapeGenerator;
auto setup_piece(BoardInfo const &board,
                 APoint const &spawn_point,
                 ShapeGenerator &shape_generator) -> Piece;
auto setup_player() -> Player;
auto setup_fence(BoardInfo const &board, SDL_Texture *texture) -> Fence;

void reset_game(Piece &piece,
                APoint spawn_point,
                BlockList &dead_blocks,
                Player::CommandQueue &command_queue) {
    piece.detach_blocks(dead_blocks);
    dead_blocks.clear();
    // clear queue
    while (!command_queue.empty()) {
        command_queue.pop();
    }
    piece.generate_next_shape();
    piece.reset_position(spawn_point);
}

void cleanup_perfect_lines(BlockList &dead_list, int width) {
    std::sort(dead_list.begin(), dead_list.end(),
        [] (auto &a, auto &b) {
            return a.global_position().y < b.global_position().y;
        });
        
    int const perfect_value = -10000;
    int count = 0;
    auto lap = dead_list.begin();

    auto move_down = [perfect_value] (auto &block) {
        auto [x, y] = block.local_position();
        if (y != perfect_value) {
            block.set_local_position(x, y + 1);
        }
    };
    
    auto mark_as_perfect = [perfect_value] (auto &block) {
        int x = block.local_position().x;
        block.set_local_position(x, perfect_value);
    };

    for (auto iter = dead_list.begin(); iter != dead_list.end(); ) {
        if (iter->global_position().y == lap->global_position().y) {
            ++count;
            ++iter;
        } else {
            if (count == width) {
                std::for_each(dead_list.begin(), lap, move_down);
                std::for_each(lap, iter, mark_as_perfect);
                //iter = dead_list.erase(lap, iter);
            }
            count = 0;
            lap = iter;
        }
    }
    
    if (count == width) {
        std::for_each(dead_list.begin(), lap, move_down);
        std::for_each(lap, dead_list.end(), mark_as_perfect);
        //dead_list.erase(lap, dead_list.end());
    }
    
    std::erase_if(dead_list, [perfect_value] (auto &block) {
        return block.local_position().y == perfect_value;
    });
}

void synch_frame_rate(Uint32 start, Uint32 msecs_per_update) {
    Uint32 next_start = start + msecs_per_update;
    Uint32 current = SDL_GetTicks();
    if (next_start >= current) {
        SDL_Delay(next_start - current);
    }
}

void process_commands(Player::CommandQueue &command_queue,
                      Piece &piece,
                      BlockList const &dead_blocks) {
    while (!command_queue.empty()) {
        Player::Action action = command_queue.front();
        switch (action) {
        case Player::Action::MoveLeft:
            piece.move_left(dead_blocks);
            break;
        case Player::Action::MoveRight:
            piece.move_right(dead_blocks);
            break;
        case Player::Action::RotateLeft:
            piece.rotate_left(dead_blocks);
            break;
        case Player::Action::RotateRight:
            piece.rotate_right(dead_blocks);
            break;
        default:
            break;
        }
        command_queue.pop();
    }
}

void process_piece_stuck(Piece &piece,
                         BlockList &dead_blocks,
                         APoint const &PieceSpawnPoint,
                         BoardInfo const &board) {
    if (piece.is_stuck()) {
        piece.detach_blocks(dead_blocks);
        cleanup_perfect_lines(dead_blocks, board.arena_width);
        
        piece.generate_next_shape();
        piece.reset_position(PieceSpawnPoint);
    }
}

int main(int argc, char **argv) {
    auto *WindowTitle = "TETRIS take 11";
    auto *CellTexturePath = "resources/cell.png";
    int const ScreenWidth = 600;  // px
    int const ScreenHeight = 550; // px
    
    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        std::cerr << "Error: init SDL\n";
        std::exit(1);
    }
    
    if (IMG_Init(IMG_INIT_PNG) != IMG_INIT_PNG) {
        std::cerr << "Error: " << IMG_GetError() << '\n';
        std::exit(1);
    }
    
    SDL_Window *window = SDL_CreateWindow(
            WindowTitle,
            SDL_WINDOWPOS_CENTERED,
            SDL_WINDOWPOS_CENTERED,
            ScreenWidth,
            ScreenHeight,
            0);
    if (window == nullptr) {
        std::cerr << "Error: create window\n";
        std::exit(1);
    }
    
    SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
    if (renderer == nullptr) {
        std::cerr << "Error: create renderer\n";
        std::exit(1);
    }
    
    SDL_Texture *cell_texture = IMG_LoadTexture(renderer, CellTexturePath);
    if (cell_texture == nullptr) {
        std::cerr << "Error: " << IMG_GetError() << '\n';
        std::exit(1);
    }
    
   
    // setup game
    int const ReadyCount = 40;
    APoint const PieceSpawnPoint{3, 0};
    int const MillisecsPerUpdate = 16;

    auto board = setup_board();
    auto shape_generator = setup_shapes(cell_texture);
    auto active_piece = setup_piece(board, PieceSpawnPoint, shape_generator);
    auto player = setup_player();
    auto fence = setup_fence(board, cell_texture);

    Player::CommandQueue command_queue;
    BlockList dead_blocks;

    // game loop
    EventProcessor event_processor;
    int ready_counter = ReadyCount;
    while (!event_processor.should_quit_game()) {
        Uint32 start = SDL_GetTicks();
        
        // process events
        event_processor.process();
        if (event_processor.should_reset_game()) {
            reset_game(active_piece, PieceSpawnPoint, dead_blocks, command_queue);
            event_processor.reset();
            ready_counter = ReadyCount;
        }
        
        // update
        if (ready_counter <= 0) {
            player.update(command_queue);
            process_commands(command_queue, active_piece, dead_blocks);
            active_piece.update(dead_blocks);
            process_piece_stuck(active_piece, dead_blocks, PieceSpawnPoint, board);
        } else {
            --ready_counter;
        }
        
        // render
        SDL_SetRenderDrawColor(renderer, 100, 100, 255, 255);
        SDL_RenderClear(renderer);
        draw_background(renderer, board);
        draw_fence(renderer, fence);
        draw_piece(renderer, active_piece);
        draw_blocks(renderer, dead_blocks);
        SDL_RenderPresent(renderer);

        synch_frame_rate(start, MillisecsPerUpdate);
    }

    // cleanup all texture references
    active_piece.detach_blocks(dead_blocks);
    dead_blocks.clear();
    fence.clear();
    shape_generator.unregister_all_shapes();
    
    // destroy SDL objects
    SDL_DestroyTexture(cell_texture);
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);

    IMG_Quit();
    SDL_Quit();

    return 0;
}

setupgame.cpp

#include "gfxaux.h"
#include "board.h"
#include "shape.h"
#include "piece.h"
#include "player.h"

BoardInfo setup_board() {
    return BoardInfo {
        30, 30,        // arena width and height
        16, 16,        // cell width and height
        PPoint{10, 10} // board position
    };
}

ShapeGenerator setup_shapes(SDL_Texture *texture) {
    Shape O;
    O.add_points({{0, 0}, {1, 0}, {0, 1}, {1, 1}});
    O.set_texture(texture, Yellow);
    O.set_center(0.5, 0.5);
    
    Shape T;
    T.add_points({{0, 0}, {1, 0}, {2, 0}, {1, 1}});
    T.set_texture(texture, Lime);
    T.set_center(1, 0);

    Shape I;
    I.add_points({{0, 0}, {1, 0}, {2, 0}, {3, 0}});
    I.set_texture(texture, Red);
    I.set_center(1.5, 0.5);

    Shape Z;
    Z.add_points({{0, 0}, {1, 0}, {1, 1}, {2, 1}});
    Z.set_texture(texture, Green);
    Z.set_center(1, 0);

    Shape S;
    S.add_points({{1, 0}, {2, 0}, {0, 1}, {1, 1}});
    S.set_texture(texture, Maroon);
    S.set_center(1, 0);

    Shape L;
    L.add_points({{0, 0}, {0, 1}, {0, 2}, {1, 2}});
    L.set_texture(texture, Aqua);
    L.set_center(0, 1);

    Shape J;
    J.add_points({{1, 0}, {1, 1}, {0, 2}, {1, 2}});
    J.set_texture(texture, Fuchsia);
    J.set_center(1, 1);
    
    ShapeGenerator generator;
    generator.register_shape(O);
    generator.register_shape(T);
    generator.register_shape(I);
    generator.register_shape(Z);
    generator.register_shape(S);
    generator.register_shape(L);
    generator.register_shape(J);
    return generator;
}

Piece setup_piece(BoardInfo const &board, APoint const &spawn_point, ShapeGenerator &shape_generator) {
    float falling_speed = 0.1;
    unsigned stuck_delay = 40;
    return Piece{&board, spawn_point , falling_speed, stuck_delay, shape_generator};
}

Player setup_player() {
    Player player;
    
    player.install_action(Player::Action::MoveLeft, SDL_SCANCODE_LEFT);
    player.install_action(Player::Action::MoveRight, SDL_SCANCODE_RIGHT);
    player.install_action(Player::Action::RotateLeft, SDL_SCANCODE_Z);
    player.install_action(Player::Action::RotateRight, SDL_SCANCODE_X);
    
    return player;
}

Fence setup_fence(BoardInfo const &board, SDL_Texture *texture) {
    SDL_Color color = Silver;

	Fence fence;
	
	// left wall
	for (int i{}; i < board.arena_height; ++i) {
		PPoint pt{0, i * board.cell_height};
		fence.emplace_back(&board, board.position, pt, texture, color);
	}

	// right wall
	for (int i{}; i < board.arena_height; ++i) {
		PPoint pt{(board.arena_width + 1) * board.cell_width, i * board.cell_height};
		fence.emplace_back(&board, board.position, pt, texture, color);
	}

	// floor
	for (int i{}; i < board.arena_width + 2; ++i) {
		PPoint pt{i * board.cell_width, board.arena_height * board.cell_height};
		fence.emplace_back(&board, board.position, pt, texture, color);
	}

	return fence;
}

rectaux.h

#ifndef RECTAUX_H
#define RECTAUX_H

#include <SDL2/SDL.h>

using APoint = SDL_Point;
using APointF = SDL_FPoint;
using PPoint = SDL_Point;

#endif

renderstates.h

#ifndef RENDERSTATES_H
#define RENDERSTATES_H

struct RenderStates {
    float tx = 0;
    float ty = 0;
};

#endif

gfxaux.h

#ifndef GFXAUX_H
#define GFXAUX_H

#include <SDL2/SDL.h>

SDL_Color const Aqua{0, 255, 255, 255};
SDL_Color const Black{0, 0, 0, 255};
SDL_Color const Blue{0, 0, 255, 255};
SDL_Color const Fuchsia{255, 0, 255, 255};
SDL_Color const Gray{128, 128, 128, 255};
SDL_Color const Green{0, 128, 0, 255};
SDL_Color const Lime{0, 255, 0, 255};
SDL_Color const Maroon{128, 0, 0, 255};
SDL_Color const Navy{0, 0, 128, 255};
SDL_Color const Olive{128, 128, 0, 255};
SDL_Color const Purple{128, 0, 128, 255};
SDL_Color const Red{255, 0, 0, 255};
SDL_Color const Silver{192, 192, 192, 255};
SDL_Color const Teal{0, 128, 128, 255};
SDL_Color const White{255, 255, 255, 255};
SDL_Color const Yellow{255, 255, 0, 255};

void draw_rectangle(SDL_Renderer *renderer, SDL_Rect const &rect, SDL_Color const &fc, SDL_Color const &oc);

#endif

gfxaux.cpp

#include "gfxaux.h"

void draw_rectangle(SDL_Renderer *renderer, SDL_Rect const &rect, SDL_Color const &fc, SDL_Color const &oc) {
    SDL_SetRenderDrawColor(renderer, fc.r, fc.g, fc.b, fc.a);
    SDL_RenderFillRect(renderer, &rect);
    SDL_SetRenderDrawColor(renderer, oc.r, oc.g, oc.b, oc.a);
    SDL_RenderDrawRect(renderer, &rect);
}

board.h

#ifndef BOARD_H
#define BOARD_H

#include "texturedcell.h"
#include "rectaux.h"
#include <SDL2/SDL.h>
#include <vector>

struct BoardInfo {
	int arena_width;
	int arena_height;
	int cell_width;
	int cell_height;
	PPoint position;
};

class CoordMediator {
public:
    CoordMediator(BoardInfo const *board);
    PPoint arena_to_pixel(APoint const &arena) const;
    PPoint arena_to_pixel(APointF const &arena) const;
    APoint pixel_to_arena(PPoint const &pixel) const;
    PPoint arena_origin_px() const;
    int arena_top() const;
    int arena_bottom() const;
    int arena_left() const;
    int arena_right() const;

    BoardInfo const *board_info() const;

private:
    BoardInfo const *board_;
};

using Fence = std::vector<TexturedCell>;

void draw_background(SDL_Renderer *renderer, BoardInfo const &board);
void draw_fence(SDL_Renderer *renderer, Fence const &fence);

#endif

board.cpp

#include "board.h"

#include "gfxaux.h"
#include "renderstates.h"

CoordMediator::CoordMediator(BoardInfo const *board)
    : board_{board}
{}

PPoint CoordMediator::arena_to_pixel(APoint const &arena) const {
    int x = arena.x * board_->cell_width;
    int y = arena.y * board_->cell_height;
    return {x, y};
}

PPoint CoordMediator::arena_to_pixel(APointF const &arena) const {
    int x = static_cast<int>(arena.x * board_->cell_width);
    int y = static_cast<int>(arena.y * board_->cell_height);
    return {x, y};
}

APoint CoordMediator::pixel_to_arena(PPoint const &pixel) const {
    int x = pixel.x / board_->cell_width;
    int y = pixel.y / board_->cell_height;
    return {x, y};
}

PPoint CoordMediator::arena_origin_px() const {
    int x = board_->position.x + board_->cell_width;
    int y = board_->position.y;
    return {x, y};
}

int CoordMediator::arena_top() const {
    return 0;
}

int CoordMediator::arena_bottom() const {
    return arena_top() + board_->arena_height - 1;
}

int CoordMediator::arena_left() const {
    return 0;
}
int CoordMediator::arena_right() const {
    return arena_left() + board_->arena_width - 1;
}

BoardInfo const *CoordMediator::board_info() const {
    return board_;
}

void draw_background(SDL_Renderer *renderer, BoardInfo const &board) {
    CoordMediator mediator{&board};
    auto [ox, oy] = mediator.arena_origin_px();
    SDL_Rect rect{ox, oy,
                  board.cell_width * board.arena_width,
                  board.cell_height * board.arena_height};
    draw_rectangle(renderer, rect, Black, Black);
}

void draw_fence(SDL_Renderer *renderer, Fence const &fence) {
    RenderStates states{};
	for (auto &cell : fence) {
		cell.draw(renderer, states);
	}
}

texturedcell.h

#ifndef TEXTUREDCELL_H
#define TEXTUREDCELL_H

#include "rectaux.h"
#include <SDL2/SDL.h>

struct BoardInfo;
struct RenderStates;

class TexturedCell {
public:
    TexturedCell(BoardInfo const *board,
         PPoint const &origin,
         PPoint const &position,
         SDL_Texture *texture,
         SDL_Color const &color);
    
    PPoint global_position() const;
    PPoint local_position() const;
    
    void draw(SDL_Renderer *renderer, RenderStates states) const;

private:
    BoardInfo const *board_;
    PPoint origin_;
    PPoint position_;
    SDL_Texture *texture_;
    SDL_Color color_;
};

#endif

texturedcell.cpp

#include "texturedcell.h"

#include "renderstates.h"
#include "board.h"

TexturedCell::TexturedCell(BoardInfo const *board,
           PPoint const &origin,
           PPoint const &position,
           SDL_Texture *texture,
           SDL_Color const &color)
        : board_{board}
        , origin_{origin}
        , position_{position}
        , texture_{texture}
        , color_{color}
{}

PPoint TexturedCell::global_position() const {
    return {origin_.x + position_.x, origin_.y + position_.y};
}

PPoint TexturedCell::local_position() const {
    return position_;
}

void TexturedCell::draw(SDL_Renderer *renderer, RenderStates states) const {
    auto [x, y] = global_position();
    SDL_Rect rect{x + static_cast<int>(states.tx),
                  y + static_cast<int>(states.ty),
                  board_->cell_width, board_->cell_height};
    
    SDL_SetTextureColorMod(texture_, color_.r, color_.g, color_.b);
    SDL_RenderCopy(renderer, texture_, nullptr, &rect);
}

block.h

#ifndef BLOCK_H
#define BLOCK_H

#include "texturedcell.h"
#include "rectaux.h"
#include "board.h"
#include "renderstates.h"
#include <SDL2/SDL.h>
#include <vector>

struct RenderStates;
class CoordMediator;
class Piece;

class Block {
public:
    Block(BoardInfo const *board,
          APoint const &position,
          SDL_Texture *texture,
          SDL_Color const &color,
          Piece const *parent = nullptr);

    void draw(SDL_Renderer *renderer, RenderStates states) const;
    APoint local_position() const;
    APoint global_position() const;
    void set_local_position(APoint const &position);
    void set_local_position(int x, int y);

    void detach_from_parent();
    
private:
    TexturedCell cell_;
    CoordMediator coord_mediator_;
    APoint position_;
    Piece const *parent_;
};

using BlockList = std::vector<Block>;

void draw_blocks(SDL_Renderer *renderer, BlockList const &blocks, RenderStates states = {});

#endif

block.cpp

#include "block.h"

#include "gfxaux.h"
#include "renderstates.h"
#include "board.h"
#include "piece.h"

namespace {
TexturedCell create_cell(BoardInfo const *board,
                         SDL_Texture *texture,
                         SDL_Color const &color) {
    CoordMediator mediator{board};
    PPoint origin = mediator.arena_origin_px();
    return TexturedCell{board, origin, APoint{0, 0}, texture, color};
}
} // ns anon 

Block::Block(BoardInfo const *board,
             APoint const &position,
             SDL_Texture *texture,
             SDL_Color const &color,
             Piece const *parent)
        : cell_{create_cell(board, texture, color)}
        , coord_mediator_{board}
        , position_{position}
        , parent_{parent}
{}
      
void Block::draw(SDL_Renderer *renderer, RenderStates states) const {
    SDL_FPoint tt;
    tt.x = states.tx + position_.x;
    tt.y = states.ty + position_.y;
    auto [tx, ty] = coord_mediator_.arena_to_pixel(tt);
    cell_.draw(renderer, RenderStates{float(tx), float(ty)});
}

APoint Block::local_position() const {
    return position_;
}

APoint Block::global_position() const {
    if (parent_ == nullptr) {
        return position_;
    }
    auto [px, py] = parent_->global_position();
    return {px + position_.x, py + position_.y};
}

void Block::set_local_position(APoint const &position) {
    position_ = position;
}

void Block::set_local_position(int x, int y) {
    position_.x = x;
    position_.y = y;
}

void Block::detach_from_parent() {
    position_ = global_position();
    parent_ = nullptr;
}

void draw_blocks(SDL_Renderer *renderer, BlockList const &blocks, RenderStates states) {
    for (auto &block : blocks) {
        block.draw(renderer, states);
    }
}

piece.h

#ifndef PIECE_H
#define PIECE_H

#include "rectaux.h"
#include "block.h"
#include "board.h"
#include "blockshape.h"
#include <SDL2/SDL.h>
#include <vector>

class ShapeGenerator;

class Piece {
public:
    Piece(BoardInfo const *board,
          APoint const &position,
          float falling_speed,
          unsigned stuck_delay,
          ShapeGenerator &shape_generator);
    
    void draw(SDL_Renderer *renderer) const;
    APoint local_position() const;
    APoint global_position() const;
    void update(BlockList const &dead_blocks);

    bool move_left(BlockList const &dead_blocks);
    bool move_right(BlockList const &dead_blocks);
    bool rotate_left(BlockList const &dead_blocks);
    bool rotate_right(BlockList const &dead_blocks);

    bool is_stuck() const;
    void detach_blocks(BlockList &destination);
    void reset_position(APoint const &position);
    void generate_next_shape();

private:
    enum class Rotation { Left, Right };

    int top() const;
    int bottom() const;
    int left() const;
    int right() const;
    bool is_landing(BlockList const &dead_blocks) const;
    bool is_overlapping(BlockList const &dead_blocks) const;
    bool inside_arena() const;
    void unchecked_rotate(Rotation direction);
    
private:
    BlockList blocks_;
    unsigned stuck_count_;
    CoordMediator coord_mediator_;
    BlockShapeBuilder block_shape_builder_;
    APoint position_;
    float falling_speed_;
    float fy_;
    unsigned stuck_delay_;
    ShapeGenerator &shape_generator_;
    APointF shape_center_;
};

void draw_piece(SDL_Renderer *renderer, Piece const &piece);

#endif

piece.cpp

#include "piece.h"

#include "gfxaux.h"
#include "renderstates.h"
#include "board.h"
#include "block.h"
#include "shape.h"
#include <cmath>
#include <cassert>
#include <algorithm>

namespace {
bool comp_block_x(Block const &a, Block const &b) {
    return a.local_position().x < b.local_position().x;
}

bool comp_block_y(Block const &a, Block const &b) {
    return a.local_position().y < b.local_position().y;
}
} // ns anon

Piece::Piece(BoardInfo const *board,
             APoint const &position,
             float falling_speed,
             unsigned stuck_delay,
             ShapeGenerator &shape_generator)
        : blocks_{}
        , stuck_count_{0}
        , coord_mediator_{board}
        , block_shape_builder_{board, this}
        , position_{}
        , falling_speed_{falling_speed}
        , fy_{}
        , stuck_delay_{stuck_delay}
        , shape_generator_{shape_generator}
        , shape_center_{} {
    reset_position(position);
    generate_next_shape();
}

void Piece::draw(SDL_Renderer *renderer) const {
    RenderStates states{float(position_.x), fy_};
    draw_blocks(renderer, blocks_, states);
}

APoint Piece::local_position() const {
    return position_;
}

APoint Piece::global_position() const {
    return position_;
}

void Piece::update(BlockList const &dead_blocks) {
    if (is_landing(dead_blocks)) {
        fy_ = std::trunc(fy_);
        ++stuck_count_;
    } else {
        fy_ += falling_speed_;
        position_.y = static_cast<int>(fy_);
        stuck_count_ = 0;
    }
}

bool Piece::move_left(BlockList const &dead_blocks) {
    position_.x -= 1;
    if (left() < coord_mediator_.arena_left()) {
        position_.x += 1;
        return false;
    }
    if (is_overlapping(dead_blocks)) {
        position_.x += 1;
        return false;
    }
    return true;
}

bool Piece::move_right(BlockList const &dead_blocks) {
    position_.x += 1;
    if (right() > coord_mediator_.arena_right()) {
        position_.x -= 1;
        return false;
    }
    if (is_overlapping(dead_blocks)) {
        position_.x -= 1;
        return false;
    }
    return true;
}

bool Piece::rotate_left(BlockList const &dead_blocks) {
    unchecked_rotate(Rotation::Left);
    if (!inside_arena()) {
        unchecked_rotate(Rotation::Right);
        return false;
    }
    if (is_overlapping(dead_blocks)) {
        unchecked_rotate(Rotation::Right);
        return false;
    }
    return true;
}

bool Piece::rotate_right(BlockList const &dead_blocks) {
    unchecked_rotate(Rotation::Right);
    if (!inside_arena()) {
        unchecked_rotate(Rotation::Left);
        return false;
    }
    if (is_overlapping(dead_blocks)) {
        unchecked_rotate(Rotation::Left);
        return false;
    }
    return true;
}

bool Piece::is_stuck() const {
    return stuck_count_ >= stuck_delay_;
}

void Piece::detach_blocks(BlockList &destination) {
    for (auto &block : blocks_) {
        block.detach_from_parent();
    }
    std::move(blocks_.begin(), blocks_.end(),
        std::back_inserter(destination));
    blocks_.clear();
}

void Piece::reset_position(APoint const &position) {
    position_ = position;
    fy_ = static_cast<float>(position_.y);
}

void Piece::generate_next_shape() {
    assert(blocks_.empty());
    Shape shape = shape_generator_.next_random_shape();
    block_shape_builder_.emplace_blocks(blocks_, shape);
    shape_center_ = shape.center();
}

int Piece::top() const {
    assert(!blocks_.empty());
    auto b = std::min_element(blocks_.begin(), blocks_.end(), comp_block_y);
    return b->global_position().y;
}

int Piece::bottom() const {
    assert(!blocks_.empty());
    auto b = std::max_element(blocks_.begin(), blocks_.end(), comp_block_y);
    return b->global_position().y;
}

int Piece::left() const {
    assert(!blocks_.empty());
    auto b = std::min_element(blocks_.begin(), blocks_.end(), comp_block_x);
    return b->global_position().x;
}

int Piece::right() const {
    assert(!blocks_.empty());
    auto b = std::max_element(blocks_.begin(), blocks_.end(), comp_block_x);
    return b->global_position().x;
}

bool Piece::is_landing(BlockList const &dead_blocks) const {
    if (bottom() >= coord_mediator_.arena_bottom()) {
        return true;
    }
    
    for (auto &myblock : blocks_) {
        for (auto &dead_block : dead_blocks) {
            auto [mx, my] = myblock.global_position();
            auto [dx, dy] = dead_block.global_position();
            if (mx == dx && my + 1 == dy) {
                return true;
            }
        }
    }
    
    return false;
}

bool Piece::is_overlapping(BlockList const &dead_blocks) const {
    for (auto &myblock : blocks_) {
        for (auto &dead_block : dead_blocks) {
            auto mx = myblock.global_position().x;
            auto [dx, dy] = dead_block.global_position();
            if (mx != dx) continue;
            
            float my = myblock.local_position().y + fy_;
            float diff = std::fabs(my - dy);
            if (diff < 0.6) {
                return true;
            }
        }
    }
    return false;
}

bool Piece::inside_arena() const {
    if (left() < coord_mediator_.arena_left()) {
        return false;
    }
    if (right() > coord_mediator_.arena_right()) {
        return false;
    }
    if (bottom() > coord_mediator_.arena_bottom()) {
        return false;
    }
    return true;
}

void Piece::unchecked_rotate(Rotation direction) {
    for (auto &block : blocks_) {
        auto [x, y] = block.local_position();
        float fx = x - shape_center_.x;
        float fy = y - shape_center_.y;

        float fxx{}, fyy{};
        if (direction == Rotation::Left) {
            fxx = fy;
            fyy = -fx;
        } else {
            assert(direction == Rotation::Right);
            fxx = -fy;
            fyy = fx;
        }
        
        fxx += shape_center_.x;
        fyy += shape_center_.y; 
        x = static_cast<int>(std::floor(fxx));
        y = static_cast<int>(std::floor(fyy));
        block.set_local_position(x, y);
    }
}

void draw_piece(SDL_Renderer *renderer, Piece const &piece) {
    piece.draw(renderer);
}

eventprocessor.h

#ifndef EVENTPROCESSOR_H
#define EVENTPROCESSOR_H

class EventProcessor {
public:
    EventProcessor();
    
    void process();
    void reset();
    bool should_quit_game() const;
    bool should_reset_game() const;
    
private:
    bool should_quit_;
    bool should_reset_;
};

#endif

eventprocessor.cpp

#include "eventprocessor.h"

#include <SDL2/SDL.h>

EventProcessor::EventProcessor()
    : should_quit_{false}
    , should_reset_{false}
{}

void EventProcessor::process() {
    SDL_Event event;
    while (SDL_PollEvent(&event)) {
        switch (event.type) {
        case SDL_QUIT:
            should_quit_ = true;
            break;
        case SDL_KEYDOWN:
            if (event.key.keysym.scancode == SDL_SCANCODE_ESCAPE) {
                should_quit_ = true;
            }
            if (event.key.keysym.scancode == SDL_SCANCODE_R) {
                should_reset_ = true;
            }
            break;
        default:
            break;
        }
    }
}

void EventProcessor::reset() {
    should_quit_ = false;
    should_reset_ = false;
}

bool EventProcessor::should_quit_game() const {
    return should_quit_;
}

bool EventProcessor::should_reset_game() const {
    return should_reset_;
}

keyboardstate.h

#ifndef KEYBOARDSTATE_H
#define KEYBOARDSTATE_H

#include <SDL2/SDL.h>
#include <vector>

class KeyboardState {
public:
    KeyboardState();
    
    void update();
    bool pressed(SDL_Scancode scancode) const;
    bool just_pressed(SDL_Scancode scancode) const;
    
private:
    void init();

private:
    std::vector<Uint8> cur_state_;
    std::vector<Uint8> old_state_;
};

#endif

keyboardstate.cpp

#include "keyboardstate.h"

#include <algorithm>

KeyboardState::KeyboardState() {
    init();
}

void KeyboardState::update() {
    int numkeys;
    auto *state = SDL_GetKeyboardState(&numkeys);
    
    if (old_state_.size() < cur_state_.size()) {
        old_state_.resize(cur_state_.size(), 0);
    }
    std::copy(cur_state_.begin(), cur_state_.end(), old_state_.begin());
    
    if (cur_state_.size() < static_cast<size_t>(numkeys)) {
        cur_state_.resize(numkeys, 0);
    }
    std::copy(state, state + numkeys, cur_state_.begin());
}

bool KeyboardState::pressed(SDL_Scancode scancode) const {
    return cur_state_[scancode] == 1;
}

bool KeyboardState::just_pressed(SDL_Scancode scancode) const {
    return old_state_[scancode] == 0 && cur_state_[scancode] == 1;
}

void KeyboardState::init() {
    cur_state_.clear();
    old_state_.clear();
    
    int numkeys;
    auto *state = SDL_GetKeyboardState(&numkeys);
    cur_state_.resize(numkeys, 0);
    old_state_.resize(numkeys, 0);
    
    std::copy(state, state + numkeys, cur_state_.begin());
}

player.h

#ifndef PLAYER_H
#define PLAYER_H

#include "keyboardstate.h"
#include <SDL2/SDL.h>
#include <map>
#include <queue>

class Piece;
class KeyboardState;

class Player {
public:
    enum class Action {
        MoveLeft,
        MoveRight,
        RotateLeft,
        RotateRight,
        SoftDrop,
        HardDrop,
    };
    
    using CommandQueue = std::queue<Action>;

    Player();
    
    void update(CommandQueue &command_queue);
    void install_action(Action action, SDL_Scancode scancode);
    
private:
    bool just_pressed(Action action) const;

private:
    KeyboardState keyboard_;
    std::map<Action, SDL_Scancode> action_map_;
};

#endif

player.cpp

#include "player.h"

#include "keyboardstate.h"

Player::Player()
    : keyboard_{}
{}
    
void Player::update(CommandQueue &command_queue) {
    keyboard_.update();
    for (auto [action, scancode] : action_map_) {
        if (keyboard_.just_pressed(scancode)) {
            command_queue.push(action);
        }
    }
}

void Player::install_action(Action action, SDL_Scancode scancode) {
    action_map_.insert({action, scancode});
}

bool Player::just_pressed(Action action) const {
    if (auto p = action_map_.find(action);
            p != action_map_.end()) {
        return keyboard_.just_pressed(p->second); 
    }
    return false;
}

shape.h

#ifndef SHAPE_H
#define SHAPE_H

#include "rectaux.h"
#include <SDL2/SDL.h>
#include <vector>
#include <random>

class Shape {
public:
    Shape();
    
    void add_points(std::initializer_list<APoint> points);
    void add_point(APoint const &point);
    size_t point_count() const;
    APoint point_at(size_t i) const;
    
    void set_texture(SDL_Texture *texture, SDL_Color const &color);
    std::pair<SDL_Texture *, SDL_Color> texture() const;

    void set_center(float x, float y);
    APointF center() const;

private:
    std::vector<APoint> pts_;
    SDL_Texture *texture_;
    SDL_Color color_;
    APointF center_;
};


class ShapeGenerator {
public:
    ShapeGenerator();
    ShapeGenerator(ShapeGenerator const &) = delete;
    ShapeGenerator &operator=(ShapeGenerator const &) = delete;
    ShapeGenerator(ShapeGenerator &&) = default;
    ShapeGenerator &operator=(ShapeGenerator &&) = default;
    
    void register_shape(Shape const &shape);
    void unregister_all_shapes();
    Shape next_random_shape();
    
private:
    std::mt19937 random_generator_;
    std::vector<Shape> shapes_;
};

#endif

shape.cpp

#include "shape.h"

#include <cassert>

Shape::Shape()
    : pts_{}
    , texture_{}
    , color_{}
    , center_{}
{}

void Shape::add_points(std::initializer_list<APoint> points) {
    pts_.insert(pts_.end(), points.begin(), points.end());
}

void Shape::add_point(APoint const &point) {
    pts_.push_back(point);
}

size_t Shape::point_count() const {
    return pts_.size();
}

APoint Shape::point_at(size_t i) const {
    return pts_.at(i);
}

void Shape::set_texture(SDL_Texture *texture, SDL_Color const &color) {
    texture_ = texture;
    color_ = color;
}

std::pair<SDL_Texture *, SDL_Color> Shape::texture() const {
    return {texture_, color_};
}

void Shape::set_center(float x, float y) {
    center_.x = x;
    center_.y = y;
}
APointF Shape::center() const {
    return center_;
}

ShapeGenerator::ShapeGenerator()
    : random_generator_{std::random_device{}()}
    , shapes_{}
{}

void ShapeGenerator::register_shape(Shape const &shape) {
    shapes_.push_back(shape);
}

void ShapeGenerator::unregister_all_shapes() {
    shapes_.clear();
}

Shape ShapeGenerator::next_random_shape() {
    assert(!shapes_.empty());
    std::uniform_int_distribution<size_t> distrib{0, shapes_.size() - 1};
    size_t shape_index = distrib(random_generator_);
    return shapes_.at(shape_index);
}

blockshape.h

#ifndef BLOCKSHAPE_H
#define BLOCKSHAPE_H

#include "block.h"

class Shape;
struct BoardInfo;
class Piece;

class BlockShapeBuilder {
public:
    BlockShapeBuilder(BoardInfo const *board, Piece const *parent);
    void emplace_blocks(BlockList &blocks, Shape const &shape) const;

private:
    BoardInfo const *board_;
    Piece const *parent_;
};

#endif

blockshape.cpp

#include "blockshape.h"

#include "shape.h"
#include <SDL2/SDL.h>

BlockShapeBuilder::BlockShapeBuilder(BoardInfo const *board, Piece const *parent)
    : board_{board}
    , parent_{parent}
{}

void BlockShapeBuilder::emplace_blocks(BlockList &blocks, Shape const &shape) const {
    auto const &[texture, color] = shape.texture();
    for (size_t i{}; i < shape.point_count(); ++i) {
        APoint point = shape.point_at(i);
        blocks.emplace_back(board_, point, texture, color, parent_);
    }
}

Makefile

SRCS = \
	main.cpp \
	setupgame.cpp \
	board.cpp \
	gfxaux.cpp \
	block.cpp \
	piece.cpp \
	player.cpp \
	keyboardstate.cpp \
	eventprocessor.cpp \
	shape.cpp \
	blockshape.cpp \
	texturedcell.cpp

OBJS = ${SRCS:%.cpp=%.o}
DEPS = ${SRCS:%.cpp=%.d}
EXECUTABLE = tetris.exe
CXXFLAGS = -std=c++20 -Wall
CPPFLAGS = -IC:\SDL2\include
LDFLAGS = -LC:\SDL2\lib
LDLIBS = -lmingw32 -lSDL2main -lSDL2 -lSDL2_image

.PHONY: all
all: ${EXECUTABLE}

${EXECUTABLE}: ${OBJS}
	${CXX} -o $@ $^ ${LDFLAGS} ${LDLIBS}

%.o: %.cpp
	${CXX} -c ${CXXFLAGS} -o $@ $< -MMD -MP ${CPPFLAGS}

.PHONY: clean
clean:
	${RM} ${EXECUTABLE} ${OBJS} ${DEPS}

-include ${DEPS}

resources

resources/cell.png

resources/cell.png