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

예제 소스

테트리스 MoE 매크로 정책: 네 전문가로 착수 고르기 · 파일 31개

main_window.cpp

#include "main_window.h"

#include "tetris_widget.h"

#include <QEvent>
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QLabel>
#include <QShowEvent>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QTcpServer>
#include <QTcpSocket>
#include <QTimer>
#include <QVBoxLayout>
#include <QWidget>

static const QString kHelpText = QStringLiteral(
    "\n\n조작: ← → 이동, ↑ 회전(CW), Z CCW, ↓ 소프트, Space 하드\n"
    "P: 타이머 일시정지/재개 (순수 step 테스트용)\n"
    "R: 리셋\n"
    "+/-: 속도 조절 (tick ms)\n"
    "\nRL: --rl-port <port> 로 실행 후 Python 클라이언트가 action 전송\n"
    "TCP: human / human off(agent) — 사람 플레이 ↔ 정책 제어 전환");

static QString status_string(bool game_over, bool paused) {
    if (game_over) {
        return QStringLiteral("game_over");
    }
    return paused ? QStringLiteral("paused") : QStringLiteral("running");
}

MainWindow::MainWindow(QWidget* parent, int rl_port, int tick_ms)
    : QMainWindow(parent), rl_port_(rl_port), tick_ms_(tick_ms) {
    setWindowTitle(QStringLiteral("Tetris (Qt) — RL friendly"));
    resize(520, 640);

    auto* central = new QWidget(this);
    central->setStyleSheet(QStringLiteral("background:#181a20;"));
    auto* root = new QHBoxLayout(central);
    root->setContentsMargins(16, 16, 16, 16);
    root->setSpacing(16);

    board_ = new TetrisWidget(central);
    board_->set_engine(&engine_);
    root->addWidget(board_, 1);

    auto* side = new QVBoxLayout();
    side->setSpacing(12);
    hud_ = new QLabel(central);
    hud_->setWordWrap(true);
    hud_->setTextFormat(Qt::PlainText);
    hud_->setTextInteractionFlags(Qt::NoTextInteraction);
    hud_->setStyleSheet(QStringLiteral(
        "QLabel{"
        "color:#e6e6e6;"
        "background:#0f1116;"
        "border:1px solid #2a2f3a;"
        "border-radius:10px;"
        "padding:12px;"
        "font-size:14px;"
        "}"));
    hud_->setMinimumWidth(200);
    side->addWidget(hud_);
    side->addStretch(1);
    root->addLayout(side);

    setCentralWidget(central);

    // 키 입력은 MainWindow::keyPressEvent에서 처리. 보드/라벨이 포커스를 가져가면 키가 전달되지 않음.
    setFocusPolicy(Qt::StrongFocus);
    central->setFocusPolicy(Qt::NoFocus);
    board_->setFocusPolicy(Qt::NoFocus);
    hud_->setFocusPolicy(Qt::NoFocus);
    board_->installEventFilter(this);
    hud_->installEventFilter(this);
    central->installEventFilter(this);

    engine_.reset();
    // 초기 상태는 정지(paused): 사람이 P 또는 R을 눌러야 시작
    human_timer_enabled_ = false;
    update_hud();

    tick_ = new QTimer(this);
    connect(tick_, &QTimer::timeout, this, &MainWindow::on_tick);
    tick_->start(tick_ms_);

    if (rl_port_ > 0) {
        rl_start(rl_port_);
        // RL 모드: 클라이언트가 resume 또는 reset 커맨드로 시작
    }
}

static QString name_piece(TetrominoType t) {
    switch (t) {
        case TetrominoType::I:
            return QStringLiteral("I");
        case TetrominoType::O:
            return QStringLiteral("O");
        case TetrominoType::T:
            return QStringLiteral("T");
        case TetrominoType::S:
            return QStringLiteral("S");
        case TetrominoType::Z:
            return QStringLiteral("Z");
        case TetrominoType::J:
            return QStringLiteral("J");
        case TetrominoType::L:
            return QStringLiteral("L");
        default:
            return QStringLiteral("-");
    }
}

void MainWindow::update_hud() {
    QString s;
    s += QStringLiteral("lines: %1\n").arg(engine_.lines_cleared_total());
    s += QStringLiteral("last_reward: %1\n").arg(engine_.last_reward(), 0, 'f', 3);
    s += QStringLiteral("piece: %1\n").arg(engine_.piece_index());
    s += QStringLiteral("status: %1\n").arg(status_string(engine_.game_over(), !human_timer_enabled_));
    s += QStringLiteral("tick_ms: %1\n").arg(tick_ms_);
    if (rl_port_ > 0) {
        const QString st = rl_client_ ? QStringLiteral("connected") : QStringLiteral("waiting");
        s += QStringLiteral("rl: %1 (port %2)\n").arg(st).arg(rl_port_);
        if (rl_client_) {
            s += QStringLiteral("rl_mode: %1\n").arg(rl_human_mode_ ? QStringLiteral("human") : QStringLiteral("agent"));
        }
    }
    if (!rl_hud_extra_.isEmpty()) {
        s += QStringLiteral("\n");
        s += rl_hud_extra_;
        s += QStringLiteral("\n");
    }
    s += QStringLiteral("next: ");
    for (const auto t : engine_.preview_queue()) {
        s += name_piece(t);
        s += QLatin1Char(' ');
    }
    s += QLatin1Char('\n');
    hud_->setText(s + kHelpText);
    board_->set_paused(!human_timer_enabled_);
    board_->refresh();
}

void MainWindow::on_tick() {
    if (!human_timer_enabled_ || engine_.game_over()) {
        if (rl_client_) {
            rl_send_state();
        }
        return;
    }
    // RL client가 있으면 pending_action을 사용, 없으면 Noop
    const int a = pending_action_;
    pending_action_ = static_cast<int>(TetrisAction::Noop);
    engine_.step(static_cast<TetrisAction>(a));
    ++rl_step_seq_;
    last_step_action_ = a;
    update_hud();
    if (rl_client_) {
        rl_send_state();
    }
}

void MainWindow::step_human(TetrisAction a) {
    const int ac = static_cast<int>(a);
    engine_.step(a);
    ++rl_step_seq_;
    last_step_action_ = ac;
    update_hud();
    if (rl_client_) {
        rl_send_state();
    }
}

void MainWindow::showEvent(QShowEvent* event) {
    QMainWindow::showEvent(event);
    setFocus(Qt::OtherFocusReason);
}

bool MainWindow::eventFilter(QObject* watched, QEvent* event) {
    if (event->type() == QEvent::MouseButtonPress) {
        if (watched == board_ || watched == hud_ || watched == centralWidget()) {
            setFocus(Qt::MouseFocusReason);
        }
    }
    return false;
}

void MainWindow::keyPressEvent(QKeyEvent* event) {
    if (engine_.game_over() && event->key() != Qt::Key_R) {
        QMainWindow::keyPressEvent(event);
        return;
    }
    // RL agent 모드에서는 키 입력 무시(충돌 방지). human 모드·R은 허용.
    if (rl_client_ && !rl_human_mode_ && event->key() != Qt::Key_R) {
        event->ignore();
        return;
    }

    switch (event->key()) {
        case Qt::Key_Equal:  // '+' on many layouts (shift+'=')
        case Qt::Key_Plus:
            tick_ms_ = std::max(50, tick_ms_ - 50);
            if (tick_) {
                tick_->start(tick_ms_);
            }
            update_hud();
            break;
        case Qt::Key_Minus:
        case Qt::Key_Underscore:
            tick_ms_ = std::min(2000, tick_ms_ + 50);
            if (tick_) {
                tick_->start(tick_ms_);
            }
            update_hud();
            break;
        case Qt::Key_Left:
            step_human(TetrisAction::Left);
            break;
        case Qt::Key_Right:
            step_human(TetrisAction::Right);
            break;
        case Qt::Key_Up:
            step_human(TetrisAction::RotateCW);
            break;
        case Qt::Key_Down:
            step_human(TetrisAction::SoftDrop);
            break;
        case Qt::Key_Space:
            step_human(TetrisAction::HardDrop);
            break;
        case Qt::Key_Z:
            step_human(TetrisAction::RotateCCW);
            break;
        case Qt::Key_P:
            human_timer_enabled_ = !human_timer_enabled_;
            update_hud();
            break;
        case Qt::Key_R:
            engine_.reset();
            human_timer_enabled_ = false;  // 리셋 후 정지 상태에서 대기
            rl_step_seq_ = 0;
            last_step_action_ = -1;
            update_hud();
            break;
        default:
            QMainWindow::keyPressEvent(event);
            return;
    }
    event->accept();
}

void MainWindow::rl_start(int port) {
    rl_server_ = new QTcpServer(this);
    connect(rl_server_, &QTcpServer::newConnection, this, &MainWindow::on_rl_new_connection);
    rl_server_->listen(QHostAddress::LocalHost, static_cast<quint16>(port));
    update_hud();
}

void MainWindow::on_rl_new_connection() {
    if (!rl_server_) {
        return;
    }
    // single client
    if (rl_client_) {
        rl_client_->disconnectFromHost();
        rl_client_->deleteLater();
        rl_client_ = nullptr;
    }
    rl_client_ = rl_server_->nextPendingConnection();
    connect(rl_client_, &QTcpSocket::readyRead, this, &MainWindow::on_rl_ready_read);
    connect(rl_client_, &QTcpSocket::disconnected, this, &MainWindow::on_rl_disconnected);
    // 초기 상태 한 번 전송
    rl_send_state();
    update_hud();
}

void MainWindow::on_rl_disconnected() {
    if (rl_client_) {
        rl_client_->deleteLater();
        rl_client_ = nullptr;
    }
    pending_action_ = static_cast<int>(TetrisAction::Noop);
    rl_human_mode_ = false;
    update_hud();
}

void MainWindow::on_rl_ready_read() {
    if (!rl_client_) {
        return;
    }
    while (rl_client_->canReadLine()) {
        const QByteArray line = rl_client_->readLine().trimmed();
        if (line.isEmpty()) {
            continue;
        }
        if (line.startsWith("hud ")) {
            rl_hud_extra_ = QString::fromUtf8(line.mid(4));
            update_hud();
            continue;
        }
        if (line == "human" || line == "human on") {
            rl_human_mode_ = true;
            human_timer_enabled_ = false;
            pending_action_ = static_cast<int>(TetrisAction::Noop);
            update_hud();
            rl_send_state();
            continue;
        }
        if (line == "human off" || line == "agent") {
            rl_human_mode_ = false;
            if (!engine_.game_over()) {
                human_timer_enabled_ = true;
            }
            update_hud();
            rl_send_state();
            continue;
        }
        if (line == "pause") {
            human_timer_enabled_ = false;
            update_hud();
            rl_send_state();
            continue;
        }
        if (line == "resume") {
            // human 모드에서는 타이머를 켜지 않음(키보드 전용 플레이 유지)
            if (!rl_human_mode_ && !engine_.game_over()) {
                human_timer_enabled_ = true;
            }
            update_hud();
            rl_send_state();
            continue;
        }
        if (line == "restart" || line == "reset") {
            engine_.reset();
            rl_step_seq_ = 0;
            last_step_action_ = -1;
            if (rl_human_mode_) {
                human_timer_enabled_ = false;
            } else {
                human_timer_enabled_ = true;
            }
            pending_action_ = static_cast<int>(TetrisAction::Noop);
            update_hud();
            rl_send_state();
            continue;
        }
        bool ok = false;
        const int a = line.toInt(&ok);
        if (!ok) {
            continue;
        }
        if (a >= 0 && a < static_cast<int>(TetrisAction::Count)) {
            pending_action_ = a;
        }
    }
}

void MainWindow::rl_send_state() {
    if (!rl_client_) {
        return;
    }
    const auto& cfg = engine_.config();
    const int w = cfg.board_width;
    const int h = cfg.board_height;
    const int hidden = cfg.hidden_rows;
    const int visible_h = h - hidden;

    std::vector<uint8_t> cells;
    engine_.fill_render_grid(cells);

    QJsonArray grid;
    for (int y = hidden; y < h; ++y) {
        for (int x = 0; x < w; ++x) {
            grid.append(static_cast<int>(cells[y * w + x]));
        }
    }

    QJsonObject o;
    o.insert(QStringLiteral("w"), w);
    o.insert(QStringLiteral("visible_h"), visible_h);
    o.insert(QStringLiteral("grid"), grid);
    o.insert(QStringLiteral("reward"), engine_.last_reward());
    o.insert(QStringLiteral("lines"), engine_.lines_cleared_total());
    o.insert(QStringLiteral("piece"), engine_.piece_index());
    o.insert(QStringLiteral("piece_type"), static_cast<int>(engine_.current_type()));
    o.insert(QStringLiteral("piece_x"), engine_.current_x());
    o.insert(QStringLiteral("piece_y"), engine_.current_y());
    o.insert(QStringLiteral("piece_rot"), engine_.current_rotation());
    {
        QJsonArray preview_arr;
        for (const auto t : engine_.preview_queue()) {
            preview_arr.append(static_cast<int>(t));
        }
        o.insert(QStringLiteral("preview"), preview_arr);
    }
    o.insert(QStringLiteral("game_over"), engine_.game_over());
    o.insert(QStringLiteral("paused"), !human_timer_enabled_);
    o.insert(QStringLiteral("human_mode"), rl_human_mode_);
    o.insert(QStringLiteral("step_seq"), static_cast<qint64>(rl_step_seq_));
    o.insert(QStringLiteral("last_action"), last_step_action_);
    o.insert(QStringLiteral("status"), status_string(engine_.game_over(), !human_timer_enabled_));

    const QByteArray payload = QJsonDocument(o).toJson(QJsonDocument::Compact) + "\n";
    rl_client_->write(payload);
    rl_client_->flush();
}