예제 소스
오목 좌표 LoRA: 15×15와 19×19 격자를 정확히 읽히기 · 파일 59개
boardwidget.cpp
#include "boardwidget.h"
#include <QMouseEvent>
#include <QPainter>
#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(500);
connect(m_blinkTimer, &QTimer::timeout, this, [this]() {
m_blinkOn = !m_blinkOn;
if (m_lastRow >= 0 && m_lastCol >= 0)
update();
});
}
void BoardWidget::refreshFromBoard(const QVector<QVector<Cell>> &cells)
{
m_cells = cells;
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(12.0), qMin(aw, ah) * qreal(0.04));
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, currentBoardSize());
*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(480, 480);
}
QSize BoardWidget::minimumSizeHint() const
{
return QSize(120, 120);
}
void BoardWidget::paintEvent(QPaintEvent *event)
{
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
const QColor wood(QColor(222, 191, 130));
p.fillRect(rect(), wood);
qreal sp = 1;
QPointF off;
gridLayout(&sp, &off);
const int N = currentBoardSize();
const qreal gridSpan = qreal(N - 1) * sp;
p.setPen(QPen(QColor(120, 80, 30), qMax(qreal(0.8), sp * 0.035)));
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));
}
// 화점 (중앙 + 네 모서리). 13/15/19 공통: 모서리 좌표는 (3, N-4).
{
const int mid = N / 2;
const int corner = 3;
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));
}
const qreal hoshiR = qMax(qreal(1.6), sp * 0.10);
p.setPen(Qt::NoPen);
p.setBrush(QColor(60, 40, 18));
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 Cell stone = (!m_cells.isEmpty()) ? m_cells[r][c] : Cell::None;
if (stone == Cell::None)
continue;
const QPointF cen = pointForIntersection(r, c);
QRadialGradient rg(cen - QPointF(rr * 0.25, rr * 0.25), rr * 1.4);
if (stone == Cell::Black) {
rg.setColorAt(0.0, QColor(70, 70, 74));
rg.setColorAt(1.0, QColor(10, 10, 10));
p.setPen(QPen(Qt::black, 1));
} else {
rg.setColorAt(0.0, QColor(255, 255, 255));
rg.setColorAt(1.0, QColor(215, 215, 219));
p.setPen(QPen(QColor(90, 90, 94), 1));
}
p.setBrush(QBrush(rg));
p.drawEllipse(cen, rr, rr);
}
}
// 마지막 수 깜빡임 마커 (붉은 링).
if (m_blinkOn && m_lastRow >= 0 && m_lastRow < N && m_lastCol >= 0 && m_lastCol < N
&& !m_cells.isEmpty() && m_cells[m_lastRow][m_lastCol] != Cell::None) {
const QPointF cen = pointForIntersection(m_lastRow, m_lastCol);
const qreal markerR = qMax(qreal(2.0), rr * 0.55);
QPen markerPen(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);
}
QWidget::paintEvent(event);
}
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.52;
int bestR = -1;
int bestC = -1;
const int N = currentBoardSize();
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);
}