예제 소스
VLM을 이용한 테트리스 학습 · 파일 31개
tetris_engine.cpp
#include "tetris_engine.h"
#include <algorithm>
namespace {
using Shape = std::array<int8_t, 16>;
Shape rotate_cw(const Shape& s) {
Shape out{};
for (int r = 0; r < 4; ++r) {
for (int c = 0; c < 4; ++c) {
out[r * 4 + c] = s[(3 - c) * 4 + r];
}
}
return out;
}
Shape base_shape(TetrominoType t) {
Shape s{};
switch (t) {
case TetrominoType::I:
s = {0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0};
break;
case TetrominoType::O:
s = {0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0};
break;
case TetrominoType::T:
s = {0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0};
break;
case TetrominoType::S:
s = {0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
break;
case TetrominoType::Z:
s = {1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0};
break;
case TetrominoType::J:
s = {1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0};
break;
case TetrominoType::L:
s = {0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0};
break;
default:
break;
}
return s;
}
struct ShapeTable {
std::array<std::array<Shape, 4>, 8> shapes{};
ShapeTable() {
for (int ti = 1; ti <= 7; ++ti) {
const auto t = static_cast<TetrominoType>(ti);
Shape cur = base_shape(t);
for (int r = 0; r < 4; ++r) {
shapes[ti][r] = cur;
if (t != TetrominoType::O) {
cur = rotate_cw(cur);
}
}
}
}
};
const ShapeTable kShapes;
int cell_value(TetrominoType t) {
return static_cast<int>(t);
}
} // namespace
TetrisEngine::TetrisEngine(TetrisEngineConfig cfg) : cfg_(std::move(cfg)) {
grid_.assign(cfg_.board_width * cfg_.board_height, 0);
preview_.resize(static_cast<size_t>(std::max(1, cfg_.preview_count)), TetrominoType::None);
bag_.resize(7);
}
void TetrisEngine::set_seed(uint64_t seed) { rng_.seed(seed); }
void TetrisEngine::reset() {
std::fill(grid_.begin(), grid_.end(), 0);
lines_cleared_total_ = 0;
game_over_ = false;
last_reward_ = 0.0;
piece_index_ = 0;
current_type_ = TetrominoType::None;
bag_pos_ = 0;
bag_.clear();
for (auto& p : preview_) {
p = TetrominoType::None;
}
refill_preview();
spawn_next();
}
TetrominoType TetrisEngine::draw_from_bag() {
if (bag_pos_ >= bag_.size()) {
bag_.assign({TetrominoType::I, TetrominoType::O, TetrominoType::T, TetrominoType::S,
TetrominoType::Z, TetrominoType::J, TetrominoType::L});
std::shuffle(bag_.begin(), bag_.end(), rng_);
bag_pos_ = 0;
}
return bag_[bag_pos_++];
}
void TetrisEngine::refill_preview() {
for (auto& p : preview_) {
if (p == TetrominoType::None) {
p = draw_from_bag();
}
}
}
void TetrisEngine::spawn_next() {
refill_preview();
current_type_ = preview_.front();
std::rotate(preview_.begin(), preview_.begin() + 1, preview_.end());
preview_.back() = TetrominoType::None;
refill_preview();
current_rotation_ = 0;
const int box = 4;
int spawn_x = (cfg_.board_width - box) / 2;
if (current_type_ == TetrominoType::O) {
spawn_x += 1;
}
int spawn_y = 0;
current_x_ = spawn_x;
current_y_ = spawn_y;
if (!fits(current_x_, current_y_, current_rotation_, current_type_)) {
game_over_ = true;
current_type_ = TetrominoType::None;
return;
}
++piece_index_;
}
TetrisEngine::Shape TetrisEngine::shape_for(TetrominoType t, int rot) const {
if (t == TetrominoType::None) {
return {};
}
const int idx = static_cast<int>(t);
const int r = rot & 3;
return kShapes.shapes[idx][r];
}
bool TetrisEngine::fits(int bx, int by, int rot, TetrominoType t) const {
const Shape s = shape_for(t, rot);
for (int r = 0; r < 4; ++r) {
for (int c = 0; c < 4; ++c) {
if (!s[r * 4 + c]) {
continue;
}
const int gy = by + r;
const int gx = bx + c;
if (gx < 0 || gx >= cfg_.board_width || gy >= cfg_.board_height) {
return false;
}
if (gy >= 0 && grid_[gy * cfg_.board_width + gx] != 0) {
return false;
}
}
}
return true;
}
void TetrisEngine::try_shift(int dx, int dy) {
if (current_type_ == TetrominoType::None || game_over_) {
return;
}
const int nx = current_x_ + dx;
const int ny = current_y_ + dy;
if (fits(nx, ny, current_rotation_, current_type_)) {
current_x_ = nx;
current_y_ = ny;
}
}
bool TetrisEngine::try_move_down() {
if (current_type_ == TetrominoType::None || game_over_) {
return false;
}
const int ny = current_y_ + 1;
if (fits(current_x_, ny, current_rotation_, current_type_)) {
current_y_ = ny;
return true;
}
return false;
}
bool TetrisEngine::try_rotate(int new_rot) {
static constexpr std::array<std::pair<int, int>, 7> kicks{{{0, 0}, {1, 0}, {-1, 0}, {0, -1}, {1, -1}, {-1, -1}, {2, 0}}};
for (const auto& k : kicks) {
const int nx = current_x_ + k.first;
const int ny = current_y_ + k.second;
if (fits(nx, ny, new_rot, current_type_)) {
current_x_ = nx;
current_y_ = ny;
current_rotation_ = new_rot;
return true;
}
}
return false;
}
void TetrisEngine::lock_piece() {
if (current_type_ == TetrominoType::None) {
return;
}
const Shape s = shape_for(current_type_, current_rotation_);
const int v = cell_value(current_type_);
for (int r = 0; r < 4; ++r) {
for (int c = 0; c < 4; ++c) {
if (!s[r * 4 + c]) {
continue;
}
const int gy = current_y_ + r;
const int gx = current_x_ + c;
if (gy >= 0 && gy < cfg_.board_height && gx >= 0 && gx < cfg_.board_width) {
grid_[gy * cfg_.board_width + gx] = static_cast<uint8_t>(v);
}
}
}
current_type_ = TetrominoType::None;
}
int TetrisEngine::clear_lines() {
int cleared = 0;
for (int y = cfg_.board_height - 1; y >= 0; --y) {
bool full = true;
for (int x = 0; x < cfg_.board_width; ++x) {
if (grid_[y * cfg_.board_width + x] == 0) {
full = false;
break;
}
}
if (!full) {
continue;
}
++cleared;
for (int yy = y; yy > 0; --yy) {
for (int x = 0; x < cfg_.board_width; ++x) {
grid_[yy * cfg_.board_width + x] = grid_[(yy - 1) * cfg_.board_width + x];
}
}
for (int x = 0; x < cfg_.board_width; ++x) {
grid_[x] = 0;
}
++y; // re-scan same row index after shift
}
return cleared;
}
int TetrisEngine::count_holes() const {
int holes = 0;
for (int x = 0; x < cfg_.board_width; ++x) {
bool seen_block = false;
for (int y = 0; y < cfg_.board_height; ++y) {
const uint8_t v = grid_[y * cfg_.board_width + x];
if (v != 0) {
seen_block = true;
} else if (seen_block) {
++holes;
}
}
}
return holes;
}
void TetrisEngine::fill_render_grid(std::vector<uint8_t>& out) const {
out = grid_;
if (current_type_ == TetrominoType::None) {
return;
}
const Shape s = shape_for(current_type_, current_rotation_);
const uint8_t v = static_cast<uint8_t>(cell_value(current_type_));
for (int r = 0; r < 4; ++r) {
for (int c = 0; c < 4; ++c) {
if (!s[r * 4 + c]) {
continue;
}
const int gy = current_y_ + r;
const int gx = current_x_ + c;
if (gy >= 0 && gy < cfg_.board_height && gx >= 0 && gx < cfg_.board_width) {
out[gy * cfg_.board_width + gx] = v;
}
}
}
}
int TetrisEngine::stack_height() const {
// Height measured from bottom: empty board => 0, any filled cell on bottom row => 1, ...
for (int y = 0; y < cfg_.board_height; ++y) {
for (int x = 0; x < cfg_.board_width; ++x) {
if (grid_[y * cfg_.board_width + x] != 0) {
return cfg_.board_height - y;
}
}
}
return 0;
}
std::vector<int> TetrisEngine::column_heights() const {
// Height measured from bottom for each column.
std::vector<int> hs(static_cast<size_t>(cfg_.board_width), 0);
for (int x = 0; x < cfg_.board_width; ++x) {
for (int y = 0; y < cfg_.board_height; ++y) {
if (grid_[y * cfg_.board_width + x] != 0) {
hs[static_cast<size_t>(x)] = cfg_.board_height - y;
break;
}
}
}
return hs;
}
int TetrisEngine::bumpiness() const {
const auto hs = column_heights();
int b = 0;
for (int x = 0; x + 1 < cfg_.board_width; ++x) {
b += std::abs(hs[static_cast<size_t>(x)] - hs[static_cast<size_t>(x + 1)]);
}
return b;
}
int TetrisEngine::stack_height_metric() const { return stack_height(); }
int TetrisEngine::bumpiness_metric() const { return bumpiness(); }
int TetrisEngine::holes_metric() const { return count_holes(); }
bool TetrisEngine::step(TetrisAction action) {
last_action_ = action;
last_reward_ = -cfg_.step_penalty;
if (game_over_) {
return false;
}
bool locked = false;
switch (action) {
case TetrisAction::Left:
try_shift(-1, 0);
break;
case TetrisAction::Right:
try_shift(1, 0);
break;
case TetrisAction::RotateCW:
try_rotate((current_rotation_ + 1) & 3);
break;
case TetrisAction::RotateCCW:
try_rotate((current_rotation_ + 3) & 3);
break;
case TetrisAction::SoftDrop:
if (!try_move_down()) {
locked = true;
}
break;
case TetrisAction::HardDrop:
while (try_move_down()) {
}
locked = true;
break;
case TetrisAction::Noop:
default:
break;
}
// Gravity for non-drop actions
if (action != TetrisAction::SoftDrop && action != TetrisAction::HardDrop) {
if (!try_move_down()) {
locked = true;
}
}
if (locked) {
const int holes_before = count_holes();
const int height_before = stack_height();
// Compute landing height before lock (current_type_ becomes None after lock_piece)
double landing_y = 0.0;
{
const Shape s = shape_for(current_type_, current_rotation_);
int sum_y = 0, count = 0;
for (int r = 0; r < 4; ++r) {
for (int c = 0; c < 4; ++c) {
if (s[r * 4 + c]) {
sum_y += current_y_ + r;
++count;
}
}
}
landing_y = count > 0 ? static_cast<double>(sum_y) / count : 0.0;
}
lock_piece();
const int cleared = clear_lines();
lines_cleared_total_ += cleared;
// Line clear reward (non-linear scaling)
double line_r = 0.0;
switch (cleared) {
case 1: line_r = cfg_.line_reward_1; break;
case 2: line_r = cfg_.line_reward_2; break;
case 3: line_r = cfg_.line_reward_3; break;
case 4: line_r = cfg_.line_reward_4; break;
default: break;
}
// Height reward: lower placement = higher reward
double height_r = cfg_.height_reward_scale * landing_y;
const int holes_after = count_holes();
const int new_holes = holes_after - holes_before;
double hole_penalty = 0.0;
if (new_holes > 0) {
hole_penalty = cfg_.hole_penalty_scale * static_cast<double>(new_holes);
}
// Stack height penalty: penalize increases of the board height (after line clears)
double height_penalty = 0.0;
if (cfg_.stack_height_penalty_scale > 0.0) {
const int height_after = stack_height();
const int dh = height_after - height_before;
if (dh > 0) {
height_penalty = cfg_.stack_height_penalty_scale * static_cast<double>(dh);
}
}
// Surface shaping penalties (post line-clear)
double bump_penalty = 0.0;
if (cfg_.bumpiness_penalty_scale > 0.0) {
bump_penalty = cfg_.bumpiness_penalty_scale * static_cast<double>(bumpiness());
}
double max_height_penalty = 0.0;
if (cfg_.max_height_penalty_scale > 0.0) {
max_height_penalty = cfg_.max_height_penalty_scale * static_cast<double>(stack_height());
}
last_reward_ += line_r + height_r - hole_penalty - height_penalty - bump_penalty - max_height_penalty;
spawn_next();
}
return !game_over_;
}