게시판테트리스 MoE 매크로 정책: 네 전문가로 착수 고르기예제 소스

예제 소스

테트리스 MoE 매크로 정책: 네 전문가로 착수 고르기 · 파일 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_;
}