게시판장기로 이해하는 규칙 엔진과 평가 함수 탐색예제 소스

예제 소스

장기로 이해하는 규칙 엔진과 평가 함수 탐색 · 파일 59개

MainWindow.cpp

#include "MainWindow.h"

#include "BoardWidget.h"
#include "SetupDialog.h"

#include <QApplication>
#include <QFrame>
#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QMenuBar>
#include <QMessageBox>
#include <QPushButton>
#include <QStatusBar>
#include <QTimer>
#include <QVBoxLayout>
#include <cstdlib>
#include <ctime>

MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
    std::srand(static_cast<unsigned>(std::time(nullptr)));
    setWindowTitle(QStringLiteral("장기"));
    resize(1080, 820);

    auto* central = new QWidget;
    auto* root = new QHBoxLayout(central);
    root->setContentsMargins(16, 16, 16, 16);
    root->setSpacing(16);

    board_ = new BoardWidget;
    board_->setGame(&game_);
    root->addWidget(board_, 1);

    auto* side = new QFrame;
    side->setObjectName(QStringLiteral("sidePanel"));
    side->setFixedWidth(280);
    auto* sideLayout = new QVBoxLayout(side);
    sideLayout->setSpacing(10);

    auto* title = new QLabel(QStringLiteral("장기"));
    title->setObjectName(QStringLiteral("titleLabel"));
    sideLayout->addWidget(title);

    turnLabel_ = new QLabel;
    turnLabel_->setObjectName(QStringLiteral("turnLabel"));
    turnLabel_->setWordWrap(true);
    sideLayout->addWidget(turnLabel_);

    checkLabel_ = new QLabel;
    checkLabel_->setObjectName(QStringLiteral("checkLabel"));
    checkLabel_->setWordWrap(true);
    sideLayout->addWidget(checkLabel_);

    scoreLabel_ = new QLabel;
    scoreLabel_->setWordWrap(true);
    sideLayout->addWidget(scoreLabel_);

    auto* capTitle = new QLabel(QStringLiteral("잡은 말"));
    capTitle->setObjectName(QStringLiteral("sectionLabel"));
    sideLayout->addWidget(capTitle);
    capturedChoLabel_ = new QLabel;
    capturedHanLabel_ = new QLabel;
    capturedChoLabel_->setWordWrap(true);
    capturedHanLabel_->setWordWrap(true);
    sideLayout->addWidget(capturedChoLabel_);
    sideLayout->addWidget(capturedHanLabel_);

    auto* histTitle = new QLabel(QStringLiteral("기보"));
    histTitle->setObjectName(QStringLiteral("sectionLabel"));
    sideLayout->addWidget(histTitle);
    historyList_ = new QListWidget;
    historyList_->setAlternatingRowColors(true);
    sideLayout->addWidget(historyList_, 1);

    undoButton_ = new QPushButton(QStringLiteral("무르기"));
    passButton_ = new QPushButton(QStringLiteral("한수쉼"));
    bikjangButton_ = new QPushButton(QStringLiteral("빅장 선언"));
    resignButton_ = new QPushButton(QStringLiteral("기권"));
    auto* newButton = new QPushButton(QStringLiteral("새 대국"));
    newButton->setObjectName(QStringLiteral("primaryButton"));

    sideLayout->addWidget(undoButton_);
    sideLayout->addWidget(passButton_);
    sideLayout->addWidget(bikjangButton_);
    sideLayout->addWidget(resignButton_);
    sideLayout->addWidget(newButton);

    root->addWidget(side);
    setCentralWidget(central);

    auto* gameMenu = menuBar()->addMenu(QStringLiteral("대국"));
    gameMenu->addAction(QStringLiteral("새 대국"), this, &MainWindow::newGame);
    gameMenu->addAction(QStringLiteral("무르기"), this, &MainWindow::undoMove);
    gameMenu->addSeparator();
    gameMenu->addAction(QStringLiteral("종료"), qApp, &QApplication::quit);

    connect(board_, &BoardWidget::moveCommitted, this, &MainWindow::onHumanMoved);
    connect(board_, &BoardWidget::statusNeedsUpdate, this, &MainWindow::updateStatus);
    connect(undoButton_, &QPushButton::clicked, this, &MainWindow::undoMove);
    connect(passButton_, &QPushButton::clicked, this, &MainWindow::passMove);
    connect(bikjangButton_, &QPushButton::clicked, this, &MainWindow::claimBikjang);
    connect(resignButton_, &QPushButton::clicked, this, &MainWindow::resignGame);
    connect(newButton, &QPushButton::clicked, this, &MainWindow::newGame);

    setStyleSheet(QStringLiteral(R"(
        QMainWindow { background: #2b1d12; }
        QWidget#sidePanel {
            background: #3a2718;
            border: 1px solid #6a4a2c;
            border-radius: 12px;
        }
        QLabel { color: #f3e2c4; }
        QLabel#titleLabel { font-size: 28px; font-weight: 700; color: #f6d7a4; }
        QLabel#turnLabel { font-size: 16px; font-weight: 600; }
        QLabel#checkLabel { font-size: 15px; font-weight: 700; color: #ffb4a8; }
        QLabel#sectionLabel { font-size: 13px; color: #c9a36a; font-weight: 600; }
        QListWidget {
            background: #2b1d12;
            color: #f3e2c4;
            border: 1px solid #6a4a2c;
            border-radius: 8px;
        }
        QPushButton {
            background: #5a3b22;
            color: #f8ead0;
            border: 1px solid #8a6238;
            border-radius: 8px;
            padding: 8px 12px;
            font-weight: 600;
        }
        QPushButton:hover { background: #6b4729; }
        QPushButton:disabled { color: #9a8166; background: #3d2a1b; }
        QPushButton#primaryButton { background: #8a4b1f; }
        QMenuBar { background: #24180f; color: #f3e2c4; }
        QMenuBar::item:selected { background: #5a3b22; }
        QStatusBar { color: #f3e2c4; background: #24180f; }
    )"));

    startGame(settings_);
}

void MainWindow::startGame(const GameSettings& settings) {
    settings_ = settings;
    ai_.setDepth(settings_.aiDepth);
    game_.newGame(settings_.choFormation, settings_.hanFormation);
    game_.setBikjangEnabled(settings_.bikjangEnabled);
    board_->setFlipped(settings_.mode == GameSettings::Mode::HumanHan);
    board_->setInteractive(true);
    board_->clearSelection();
    aiBusy_ = false;
    updateStatus();
    QTimer::singleShot(120, this, &MainWindow::thinkIfAiTurn);
}

void MainWindow::newGame() {
    SetupDialog dialog(this);
    if (dialog.exec() != QDialog::Accepted) {
        return;
    }
    startGame(dialog.settings());
}

bool MainWindow::isHumanTurn() const {
    if (settings_.mode == GameSettings::Mode::HumanVsHuman) {
        return true;
    }
    if (settings_.mode == GameSettings::Mode::HumanCho) {
        return game_.sideToMove() == Side::Cho;
    }
    return game_.sideToMove() == Side::Han;
}

void MainWindow::onHumanMoved() {
    updateStatus();
    thinkIfAiTurn();
}

void MainWindow::thinkIfAiTurn() {
    if (game_.result() != GameResult::Playing || isHumanTurn() || aiBusy_) {
        board_->setInteractive(isHumanTurn() && game_.result() == GameResult::Playing);
        return;
    }
    board_->setInteractive(false);
    aiBusy_ = true;
    statusBar()->showMessage(QStringLiteral("컴퓨터가 수를 고르는 중..."));
    QTimer::singleShot(80, this, &MainWindow::playAiMove);
}

void MainWindow::playAiMove() {
    const Move move = ai_.chooseMove(game_);
    if (move.pass) {
        game_.passTurn();
    } else {
        game_.tryMove(move.from, move.to);
    }
    aiBusy_ = false;
    board_->clearSelection();
    updateStatus();
    thinkIfAiTurn();
}

QString MainWindow::resultText() const {
    switch (game_.result()) {
    case GameResult::ChoWin: return QStringLiteral("초 승리 (외통)");
    case GameResult::HanWin: return QStringLiteral("한 승리 (외통)");
    case GameResult::Draw: return QStringLiteral("무승부");
    case GameResult::Playing: return {};
    }
    return {};
}

void MainWindow::refreshCaptures() {
    auto text = [](Side side, const std::vector<Piece>& pieces) {
        QString line = (side == Side::Cho) ? QStringLiteral("초: ") : QStringLiteral("한: ");
        if (pieces.empty()) {
            line += QStringLiteral("없음");
            return line;
        }
        for (Piece p : pieces) {
            line += QString::fromUtf8(pieceGlyph(p));
            line += QLatin1Char(' ');
        }
        return line;
    };
    capturedChoLabel_->setText(text(Side::Cho, game_.capturedBy(Side::Cho)));
    capturedHanLabel_->setText(text(Side::Han, game_.capturedBy(Side::Han)));
}

void MainWindow::refreshHistory() {
    historyList_->clear();
    int ply = 1;
    for (const Move& move : game_.history()) {
        const QString row = QStringLiteral("%1. %2")
                                .arg(ply)
                                .arg(QString::fromStdString(game_.moveText(move)));
        historyList_->addItem(row);
        ++ply;
    }
    if (historyList_->count() > 0) {
        historyList_->scrollToBottom();
    }
}

void MainWindow::updateStatus() {
    const QString sideName = (game_.sideToMove() == Side::Cho) ? QStringLiteral("초 (楚)")
                                                               : QStringLiteral("한 (漢)");
    if (game_.result() != GameResult::Playing) {
        turnLabel_->setText(resultText());
        checkLabel_->setText({});
        statusBar()->showMessage(resultText());
        board_->setInteractive(false);
    } else {
        const QString who = isHumanTurn() ? QStringLiteral("당신") : QStringLiteral("컴퓨터");
        turnLabel_->setText(QStringLiteral("%1 차례 · %2").arg(sideName, who));
        if (game_.isCheck(game_.sideToMove())) {
            checkLabel_->setText(QStringLiteral("장군!"));
        } else if (game_.isBikjang()) {
            checkLabel_->setText(QStringLiteral("빅장 (궁이 마주 봄)"));
        } else {
            checkLabel_->setText({});
        }
        statusBar()->showMessage(QStringLiteral("수 %1").arg(game_.history().size()));
        board_->setInteractive(isHumanTurn() && !aiBusy_);
    }

    const double cho = game_.pieceScore(Side::Cho) / 10.0;
    const double han = game_.pieceScore(Side::Han) / 10.0;
    scoreLabel_->setText(QStringLiteral("점수  초 %1  ·  한 %2")
                             .arg(cho, 0, 'f', 1)
                             .arg(han, 0, 'f', 1));
    bikjangButton_->setEnabled(game_.result() == GameResult::Playing && game_.bikjangEnabled()
                               && game_.isBikjang() && !game_.isCheck(game_.sideToMove()) && !aiBusy_
                               && isHumanTurn());
    passButton_->setEnabled(game_.result() == GameResult::Playing && !game_.isCheck(game_.sideToMove())
                            && !aiBusy_ && isHumanTurn());
    undoButton_->setEnabled(!game_.history().empty() && !aiBusy_);
    resignButton_->setEnabled(game_.result() == GameResult::Playing && !aiBusy_ && isHumanTurn());
    refreshCaptures();
    refreshHistory();
    board_->update();
}

void MainWindow::undoMove() {
    if (aiBusy_) {
        return;
    }
    if (!game_.undo()) {
        return;
    }
    if (settings_.mode != GameSettings::Mode::HumanVsHuman) {
        game_.undo();
    }
    board_->clearSelection();
    updateStatus();
}

void MainWindow::passMove() {
    if (!isHumanTurn() || !game_.passTurn()) {
        return;
    }
    board_->clearSelection();
    updateStatus();
    thinkIfAiTurn();
}

void MainWindow::resignGame() {
    if (!isHumanTurn()) {
        return;
    }
    const auto answer = QMessageBox::question(this, QStringLiteral("기권"),
                                              QStringLiteral("이번 대국을 기권할까요?"));
    if (answer != QMessageBox::Yes) {
        return;
    }
    game_.resign(game_.sideToMove());
    updateStatus();
}

void MainWindow::claimBikjang() {
    if (!game_.claimBikjang()) {
        return;
    }
    updateStatus();
    QMessageBox::information(this, QStringLiteral("빅장"), QStringLiteral("빅장으로 무승부입니다."));
}