예제 소스
테트리스 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();
}