예제 소스
바둑 on-policy 학습: 대국 로그를 KataGo 라벨로 바꾸기 · 파일 169개
boardwidget.cpp
#include "boardwidget.h"
#include <QMouseEvent>
#include <QPainter>
#include <QRadialGradient>
#include <QSizePolicy>
#include <QTimer>
#include <QtMath>
BoardWidget::BoardWidget(QWidget *parent)
: QWidget(parent)
{
setMouseTracking(false);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
m_blinkTimer = new QTimer(this);
m_blinkTimer->setInterval(550);
connect(m_blinkTimer, &QTimer::timeout, this, [this]() {
m_blinkOn = !m_blinkOn;
if (m_lastRow >= 0 && m_lastCol >= 0)
update();
});
}
void BoardWidget::refreshFromBoard(const QVector<Stone> &cells, int size)
{
m_cells = cells;
m_size = (size > 1) ? size : kBoardSize;
if (m_cells.isEmpty())
m_cells.assign(m_size * m_size, Stone::Empty);
update();
}
void BoardWidget::setScoringOverlay(const QSet<int> &deadStones,
const QSet<int> &territoryBlack,
const QSet<int> &territoryWhite,
const QSet<int> &dame)
{
m_deadSet = deadStones;
m_territoryBlk = territoryBlack;
m_territoryWht = territoryWhite;
m_dameSet = dame;
update();
}
void BoardWidget::clearScoringOverlay()
{
m_deadSet.clear();
m_territoryBlk.clear();
m_territoryWht.clear();
m_dameSet.clear();
update();
}
void BoardWidget::setLastMove(int row, int col, bool blink)
{
m_lastRow = row;
m_lastCol = col;
m_blinkOn = true;
if (row >= 0 && col >= 0 && blink) {
if (!m_blinkTimer->isActive())
m_blinkTimer->start();
} else {
m_blinkTimer->stop();
}
update();
}
void BoardWidget::gridLayout(qreal *outSpacing, QPointF *outOrigin) const
{
Q_ASSERT(outSpacing && outOrigin);
const qreal aw = qreal(width());
const qreal ah = qreal(height());
const qreal pad = qMax(qreal(14.0), qMin(aw, ah) * qreal(0.045));
const qreal availW = qMax(qreal(1.0), aw - 2.0 * pad);
const qreal availH = qMax(qreal(1.0), ah - 2.0 * pad);
const qreal side = qMax(qreal(1.0), qMin(availW, availH));
const int n = qMax(2, m_size);
*outSpacing = side / qreal(n - 1);
*outOrigin = QPointF(pad + (availW - side) * 0.5, pad + (availH - side) * 0.5);
}
QPointF BoardWidget::pointForIntersection(int row, int col) const
{
qreal sp = 1;
QPointF origin;
gridLayout(&sp, &origin);
return QPointF(origin.x() + qreal(col) * sp, origin.y() + qreal(row) * sp);
}
QSize BoardWidget::sizeHint() const { return QSize(620, 620); }
QSize BoardWidget::minimumSizeHint() const { return QSize(280, 280); }
void BoardWidget::drawBoard(QPainter &p, int wpx, int hpx, qreal sp, QPointF off) const
{
Q_UNUSED(wpx);
Q_UNUSED(hpx);
const QColor wood(QColor(220, 184, 116));
p.fillRect(QRectF(0, 0, wpx, hpx), wood);
const int N = m_size;
const qreal gridSpan = qreal(N - 1) * sp;
p.setPen(QPen(QColor(80, 50, 20), qMax(qreal(0.9), sp * 0.045)));
p.setBrush(Qt::NoBrush);
for (int i = 0; i < N; ++i) {
const qreal y = off.y() + qreal(i) * sp;
p.drawLine(QLineF(off.x(), y, off.x() + gridSpan, y));
}
for (int j = 0; j < N; ++j) {
const qreal x = off.x() + qreal(j) * sp;
p.drawLine(QLineF(x, off.y(), x, off.y() + gridSpan));
}
// 화점 (19×19: 9개) — 천원/네 모서리/네 변 중앙
{
const int mid = N / 2;
const int corner = (N >= 13) ? 3 : 2;
const int far = N - 1 - corner;
QVector<QPoint> hoshi;
hoshi.append(QPoint(mid, mid));
if (corner < far) {
hoshi.append(QPoint(corner, corner));
hoshi.append(QPoint(corner, far));
hoshi.append(QPoint(far, corner));
hoshi.append(QPoint(far, far));
if (N >= 19) {
hoshi.append(QPoint(mid, corner));
hoshi.append(QPoint(mid, far));
hoshi.append(QPoint(corner, mid));
hoshi.append(QPoint(far, mid));
}
}
const qreal hoshiR = qMax(qreal(2.0), sp * 0.11);
p.setPen(Qt::NoPen);
p.setBrush(QColor(40, 25, 10));
for (const QPoint &h : hoshi) {
const qreal x = off.x() + qreal(h.x()) * sp;
const qreal y = off.y() + qreal(h.y()) * sp;
p.drawEllipse(QPointF(x, y), hoshiR, hoshiR);
}
}
const qreal rr = qMax(qreal(2.0), sp * kStoneRadiusFactor);
for (int r = 0; r < N; ++r) {
for (int c = 0; c < N; ++c) {
const Stone v = (r * N + c < m_cells.size()) ? m_cells[r * N + c] : Stone::Empty;
if (v == Stone::Empty)
continue;
const QPointF cen(off.x() + qreal(c) * sp, off.y() + qreal(r) * sp);
QRadialGradient rg(cen - QPointF(rr * 0.3, rr * 0.3), rr * 1.6);
if (v == Stone::Black) {
rg.setColorAt(0.0, QColor(80, 80, 84));
rg.setColorAt(1.0, QColor(8, 8, 10));
p.setPen(QPen(Qt::black, 1));
} else {
rg.setColorAt(0.0, QColor(255, 255, 255));
rg.setColorAt(1.0, QColor(210, 210, 215));
p.setPen(QPen(QColor(80, 80, 84), 1));
}
p.setBrush(QBrush(rg));
p.drawEllipse(cen, rr, rr);
}
}
}
void BoardWidget::paintEvent(QPaintEvent *event)
{
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
qreal sp = 1;
QPointF off;
gridLayout(&sp, &off);
drawBoard(p, width(), height(), sp, off);
const int N = m_size;
const qreal rr = qMax(qreal(2.0), sp * kStoneRadiusFactor);
if (m_blinkOn && m_lastRow >= 0 && m_lastRow < N && m_lastCol >= 0 && m_lastCol < N) {
const QPointF cen = pointForIntersection(m_lastRow, m_lastCol);
const Stone v = (m_lastRow * N + m_lastCol < m_cells.size())
? m_cells[m_lastRow * N + m_lastCol]
: Stone::Empty;
if (v != Stone::Empty) {
const qreal markerR = qMax(qreal(2.0), rr * 0.55);
QPen markerPen((v == Stone::Black) ? QColor(255, 60, 60) : QColor(220, 30, 30));
markerPen.setWidthF(qMax(qreal(1.6), rr * 0.18));
p.setPen(markerPen);
p.setBrush(Qt::NoBrush);
p.drawEllipse(cen, markerR, markerR);
}
}
// ── 계가 오버레이 ──
if (!m_deadSet.isEmpty() || !m_territoryBlk.isEmpty()
|| !m_territoryWht.isEmpty() || !m_dameSet.isEmpty()) {
// 사석 X (큰 빨강 X)
const qreal xR = rr * 0.7;
QPen xPen(QColor(220, 30, 30));
xPen.setWidthF(qMax(qreal(2.0), rr * 0.22));
xPen.setCapStyle(Qt::RoundCap);
p.setPen(xPen);
for (int idx : m_deadSet) {
if (idx < 0 || idx >= N * N) continue;
const int r = idx / N;
const int c = idx % N;
const QPointF cen = pointForIntersection(r, c);
p.drawLine(QLineF(cen.x() - xR, cen.y() - xR,
cen.x() + xR, cen.y() + xR));
p.drawLine(QLineF(cen.x() - xR, cen.y() + xR,
cen.x() + xR, cen.y() - xR));
}
// 흑/백 영토 O (작은 동그라미)
const qreal tR = qMax(qreal(2.0), rr * 0.32);
for (int idx : m_territoryBlk) {
if (idx < 0 || idx >= N * N) continue;
const int r = idx / N;
const int c = idx % N;
const QPointF cen = pointForIntersection(r, c);
p.setPen(Qt::NoPen);
p.setBrush(QColor(20, 20, 20));
p.drawEllipse(cen, tR, tR);
}
for (int idx : m_territoryWht) {
if (idx < 0 || idx >= N * N) continue;
const int r = idx / N;
const int c = idx % N;
const QPointF cen = pointForIntersection(r, c);
p.setBrush(QColor(255, 255, 255));
p.setPen(QPen(QColor(80, 80, 84), qMax(qreal(1.0), rr * 0.08)));
p.drawEllipse(cen, tR, tR);
}
// 공배 (회색 작은 점)
const qreal dR = qMax(qreal(1.5), rr * 0.20);
for (int idx : m_dameSet) {
if (idx < 0 || idx >= N * N) continue;
const int r = idx / N;
const int c = idx % N;
const QPointF cen = pointForIntersection(r, c);
p.setPen(Qt::NoPen);
p.setBrush(QColor(120, 120, 120));
p.drawEllipse(cen, dR, dR);
}
}
QWidget::paintEvent(event);
}
QImage BoardWidget::renderToImage(int pxSize) const
{
const int s = qMax(240, pxSize);
QImage img(s, s, QImage::Format_RGB32);
img.fill(QColor(220, 184, 116));
QPainter p(&img);
p.setRenderHint(QPainter::Antialiasing);
const qreal pad = s * 0.045;
const qreal availW = s - 2.0 * pad;
const qreal availH = s - 2.0 * pad;
const qreal side = qMin(availW, availH);
const int N = qMax(2, m_size);
const qreal sp = side / qreal(N - 1);
QPointF off(pad + (availW - side) * 0.5, pad + (availH - side) * 0.5);
drawBoard(p, s, s, sp, off);
p.end();
return img;
}
void BoardWidget::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton) {
QWidget::mouseReleaseEvent(event);
return;
}
qreal sp = 1;
QPointF originScratch;
gridLayout(&sp, &originScratch);
Q_UNUSED(originScratch);
const QPointF pos = event->position();
qreal bestDist = sp * 0.55;
int bestR = -1;
int bestC = -1;
const int N = m_size;
for (int r = 0; r < N; ++r) {
for (int c = 0; c < N; ++c) {
const QPointF pt = pointForIntersection(r, c);
const qreal d = QLineF(pt, pos).length();
if (d <= bestDist) {
bestDist = d;
bestR = r;
bestC = c;
}
}
}
if (bestR >= 0 && bestC >= 0)
emit intersectionClicked(bestR, bestC);
QWidget::mouseReleaseEvent(event);
}