예제 소스
장기로 이해하는 규칙 엔진과 평가 함수 탐색 · 파일 59개
BoardWidget.cpp
#include "BoardWidget.h"
#include "Game.h"
#include <QLineF>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QResizeEvent>
#include <algorithm>
#include <cmath>
namespace {
QColor hanColor() {
return QColor(176, 28, 36);
}
QColor choColor() {
return QColor(18, 102, 68);
}
QPainterPath octagon(QPointF center, qreal radius) {
QPainterPath path;
for (int i = 0; i < 8; ++i) {
const qreal angle = (22.5 + 45.0 * i) * 3.14159265358979323846 / 180.0;
const QPointF pt(center.x() + radius * std::cos(angle), center.y() + radius * std::sin(angle));
if (i == 0) {
path.moveTo(pt);
} else {
path.lineTo(pt);
}
}
path.closeSubpath();
return path;
}
} // namespace
BoardWidget::BoardWidget(QWidget* parent) : QWidget(parent) {
setMouseTracking(true);
setMinimumSize(520, 620);
setAutoFillBackground(false);
}
void BoardWidget::setGame(Game* game) {
game_ = game;
clearSelection();
update();
}
void BoardWidget::setFlipped(bool flipped) {
flipped_ = flipped;
update();
}
void BoardWidget::setInteractive(bool interactive) {
interactive_ = interactive;
}
void BoardWidget::clearSelection() {
selected_.reset();
targets_.clear();
update();
}
QSize BoardWidget::sizeHint() const {
return {640, 760};
}
QSize BoardWidget::minimumSizeHint() const {
return {480, 580};
}
void BoardWidget::resizeEvent(QResizeEvent*) {
const qreal left = 42;
const qreal right = 28;
const qreal top = 40;
const qreal bottom = 40;
cellW_ = (width() - left - right) / 8.0;
cellH_ = (height() - top - bottom) / 9.0;
originX_ = left;
originY_ = top;
}
QPointF BoardWidget::pointFor(Pos pos) const {
int file = pos.file;
int rank = pos.rank;
if (flipped_) {
file = 8 - file;
rank = 9 - rank;
}
return {originX_ + file * cellW_, originY_ + rank * cellH_};
}
std::optional<Pos> BoardWidget::posFromPoint(QPointF pt) const {
std::optional<Pos> best;
qreal bestDist = std::min(cellW_, cellH_) * 0.48;
for (int rank = 0; rank < 10; ++rank) {
for (int file = 0; file < 9; ++file) {
const Pos pos{file, rank};
const qreal d = QLineF(pt, pointFor(pos)).length();
if (d < bestDist) {
bestDist = d;
best = pos;
}
}
}
return best;
}
qreal BoardWidget::pieceRadius(PieceType type) const {
const qreal base = std::min(cellW_, cellH_);
switch (type) {
case PieceType::King: return base * 0.42;
case PieceType::Chariot:
case PieceType::Cannon: return base * 0.38;
case PieceType::Horse:
case PieceType::Elephant: return base * 0.36;
case PieceType::Advisor: return base * 0.33;
case PieceType::Pawn: return base * 0.29;
default: return base * 0.32;
}
}
QFont BoardWidget::pieceFont(qreal radius) const {
QFont font;
font.setFamilies({QStringLiteral("Songti SC"), QStringLiteral("STSong"),
QStringLiteral("Apple SD Gothic Neo"), QStringLiteral("Noto Serif CJK KR"),
QStringLiteral("Serif")});
font.setBold(true);
font.setPixelSize(static_cast<int>(radius * 1.15));
return font;
}
void BoardWidget::drawBoard(QPainter& p) const {
QLinearGradient bg(rect().topLeft(), rect().bottomRight());
bg.setColorAt(0, QColor(214, 168, 106));
bg.setColorAt(1, QColor(186, 132, 74));
p.fillRect(rect(), bg);
const QRectF board(
originX_ - cellW_ * 0.55,
originY_ - cellH_ * 0.55,
cellW_ * 8 + cellW_ * 1.1,
cellH_ * 9 + cellH_ * 1.1);
p.setPen(QPen(QColor(92, 54, 24, 180), 2));
p.setBrush(QColor(232, 193, 132));
p.drawRoundedRect(board, 10, 10);
p.setPen(QPen(QColor(62, 36, 18), 1.6));
for (int file = 0; file < 9; ++file) {
p.drawLine(pointFor({file, 0}), pointFor({file, 9}));
}
for (int rank = 0; rank < 10; ++rank) {
p.drawLine(pointFor({0, rank}), pointFor({8, rank}));
}
p.setPen(QPen(QColor(62, 36, 18), 2.1));
auto palaceX = [&](Pos a, Pos b) { p.drawLine(pointFor(a), pointFor(b)); };
palaceX({3, 0}, {5, 2});
palaceX({5, 0}, {3, 2});
palaceX({3, 7}, {5, 9});
palaceX({5, 7}, {3, 9});
p.setPen(QPen(QColor(62, 36, 18), 2.4));
p.setBrush(Qt::NoBrush);
p.drawRect(QRectF(pointFor({3, 0}), pointFor({5, 2})));
p.drawRect(QRectF(pointFor({3, 7}), pointFor({5, 9})));
QFont labelFont = font();
labelFont.setPointSize(11);
labelFont.setBold(true);
p.setFont(labelFont);
p.setPen(QColor(78, 46, 22));
const QString files = QStringLiteral("123456789");
for (int file = 0; file < 9; ++file) {
const int shown = flipped_ ? 8 - file : file;
const QPointF top = pointFor({file, 0}) - QPointF(0, cellH_ * 0.42);
const QPointF bot = pointFor({file, 9}) + QPointF(0, cellH_ * 0.42);
p.drawText(QRectF(top.x() - 12, top.y() - 14, 24, 18), Qt::AlignCenter, files.mid(shown, 1));
p.drawText(QRectF(bot.x() - 12, bot.y() - 4, 24, 18), Qt::AlignCenter, files.mid(shown, 1));
}
for (int rank = 0; rank < 10; ++rank) {
const int shown = flipped_ ? 9 - rank : rank;
const QPointF left = pointFor({0, rank}) - QPointF(cellW_ * 0.46, 0);
p.drawText(QRectF(left.x() - 18, left.y() - 10, 28, 20), Qt::AlignCenter,
QString::number(shown + 1));
}
p.setPen(choColor());
const QPointF choLabel = pointFor({8, flipped_ ? 0 : 9}) + QPointF(cellW_ * 0.42, 0);
p.drawText(QRectF(choLabel.x() - 8, choLabel.y() - 12, 36, 24), Qt::AlignLeft, QStringLiteral("초"));
p.setPen(hanColor());
const QPointF hanLabel = pointFor({8, flipped_ ? 9 : 0}) + QPointF(cellW_ * 0.42, 0);
p.drawText(QRectF(hanLabel.x() - 8, hanLabel.y() - 12, 36, 24), Qt::AlignLeft, QStringLiteral("한"));
}
void BoardWidget::drawHighlights(QPainter& p) const {
if (!game_) {
return;
}
if (const auto last = game_->lastMove()) {
if (!last->pass) {
p.setPen(Qt::NoPen);
p.setBrush(QColor(255, 170, 40, 90));
const qreal r = std::min(cellW_, cellH_) * 0.46;
p.drawEllipse(pointFor(last->from), r, r);
p.setBrush(QColor(255, 140, 20, 120));
p.drawEllipse(pointFor(last->to), r, r);
}
}
if (game_->isCheck(Side::Cho) || game_->isCheck(Side::Han)) {
const Side checked = game_->isCheck(game_->sideToMove()) ? game_->sideToMove()
: opposite(game_->sideToMove());
const Pos king = game_->kingPos(checked);
if (inBoard(king)) {
p.setPen(QPen(QColor(220, 40, 40, 200), 3));
p.setBrush(QColor(220, 40, 40, 50));
const qreal r = pieceRadius(PieceType::King) + 6;
p.drawEllipse(pointFor(king), r, r);
}
}
if (selected_) {
p.setPen(QPen(QColor(40, 90, 200), 3));
p.setBrush(QColor(80, 140, 255, 70));
const qreal r = std::min(cellW_, cellH_) * 0.46;
p.drawEllipse(pointFor(*selected_), r, r);
for (Pos t : targets_) {
const bool capture = static_cast<bool>(game_->at(t));
if (capture) {
p.setPen(QPen(QColor(200, 40, 40, 220), 2.5));
p.setBrush(Qt::NoBrush);
p.drawEllipse(pointFor(t), r * 0.92, r * 0.92);
} else {
p.setPen(Qt::NoPen);
p.setBrush(QColor(30, 90, 40, 160));
p.drawEllipse(pointFor(t), 7, 7);
}
}
}
}
void BoardWidget::drawPiece(QPainter& p, Pos pos, Piece piece) const {
const QPointF c = pointFor(pos);
const qreal r = pieceRadius(piece.type);
const QColor ink = piece.side == Side::Han ? hanColor() : choColor();
p.setPen(Qt::NoPen);
p.setBrush(QColor(40, 24, 10, 70));
p.drawEllipse(c + QPointF(2.2, 3.2), r, r);
QLinearGradient fill(c - QPointF(r, r), c + QPointF(r, r));
fill.setColorAt(0, QColor(255, 244, 214));
fill.setColorAt(1, QColor(232, 204, 150));
p.setBrush(fill);
p.setPen(QPen(QColor(92, 58, 24), 1.8));
p.drawPath(octagon(c, r));
p.setPen(QPen(ink.lighter(130), 1.0));
p.setBrush(Qt::NoBrush);
p.drawPath(octagon(c, r * 0.78));
p.setFont(pieceFont(r));
p.setPen(ink);
p.drawText(QRectF(c.x() - r, c.y() - r, r * 2, r * 2), Qt::AlignCenter,
QString::fromUtf8(pieceGlyph(piece)));
}
void BoardWidget::paintEvent(QPaintEvent*) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing, true);
p.setRenderHint(QPainter::TextAntialiasing, true);
drawBoard(p);
if (!game_) {
return;
}
drawHighlights(p);
for (int rank = 0; rank < 10; ++rank) {
for (int file = 0; file < 9; ++file) {
const Pos pos{file, rank};
const Piece piece = game_->at(pos);
if (piece) {
drawPiece(p, pos, piece);
}
}
}
}
void BoardWidget::mousePressEvent(QMouseEvent* event) {
if (!game_ || !interactive_ || game_->result() != GameResult::Playing) {
return;
}
const auto hit = posFromPoint(event->position());
if (!hit) {
clearSelection();
emit statusNeedsUpdate();
return;
}
const Piece piece = game_->at(*hit);
if (selected_) {
if (game_->tryMove(*selected_, *hit)) {
clearSelection();
emit moveCommitted();
return;
}
}
if (piece && piece.side == game_->sideToMove()) {
selected_ = *hit;
targets_ = game_->legalTargets(*hit);
update();
emit statusNeedsUpdate();
return;
}
clearSelection();
emit statusNeedsUpdate();
}