예제 소스
오목 Next.js 웹과 VLM 착수 API 연결하기 · 파일 59개
boardmodel.cpp
#include "boardmodel.h"
#include <QString>
constexpr int OVERLINE_MIN = WIN_LENGTH + 1; /* 6목 이상 = 장목 (흑 금수) */
namespace {
inline bool isValidSize(int sz)
{
return sz == 13 || sz == 15 || sz == 19;
}
} // namespace
BoardModel::BoardModel(int sz)
: m_size(isValidSize(sz) ? sz : kDefaultBoardSize),
m_cells(m_size, QVector<Cell>(m_size, Cell::None))
{
}
void BoardModel::resize(int sz)
{
m_size = isValidSize(sz) ? sz : kDefaultBoardSize;
reset();
}
void BoardModel::reset()
{
m_cells = QVector<QVector<Cell>>(m_size, QVector<Cell>(m_size, Cell::None));
m_turn = Cell::Black;
m_status = GameStatus::Playing;
}
void BoardModel::setMode(GameMode m)
{
m_mode = m;
}
bool BoardModel::playAt(int row, int col, MoveRejectReason *outReason)
{
auto reject = [&](MoveRejectReason r) {
if (outReason)
*outReason = r;
return false;
};
if (outReason)
*outReason = MoveRejectReason::None;
if (m_status != GameStatus::Playing)
return reject(MoveRejectReason::GameNotPlaying);
if (row < 0 || row >= m_size || col < 0 || col >= m_size)
return reject(MoveRejectReason::OutOfBounds);
if (m_cells[row][col] != Cell::None)
return reject(MoveRejectReason::Occupied);
m_cells[row][col] = m_turn;
// 장목 (직선 6목 이상) — 흑·백 모두 금수 (사용자 룰).
if (hasOverline(m_cells, row, col, m_turn)) {
m_cells[row][col] = Cell::None;
return reject(MoveRejectReason::ChangmokBan);
}
if (isWin(row, col, m_turn)) {
m_status = (m_turn == Cell::Black) ? GameStatus::BlackWins : GameStatus::WhiteWins;
return true;
}
// 삼삼 금수: 흑·백 모두 적용.
//
// ※ 사용자 룰: 사사(4-4) 자체는 금수가 아님.
// * 빈 칸이 들어간 broken-4 패턴(예: `_W_WW_`) 은 이미 활삼으로 잡히므로
// broken-4 + 3 또는 broken-4 + broken-4 조합은 자연스럽게 위 33 검사에 걸림.
// * "꽉찬 4-4" (양 축 모두 contiguous XXXX 활사) 는 어떤 경우에도 허용.
{
const int nt = countOpenThreeAxes(m_cells, row, col, m_turn);
if (nt >= 2) {
m_cells[row][col] = Cell::None;
return reject(MoveRejectReason::SansanBan);
}
}
bool full = true;
for (int r = 0; r < m_size && full; ++r)
for (int c = 0; c < m_size && full; ++c)
if (m_cells[r][c] == Cell::None)
full = false;
if (full) {
m_status = GameStatus::Draw;
return true;
}
m_turn = (m_turn == Cell::Black) ? Cell::White : Cell::Black;
return true;
}
int BoardModel::extendGrid(const QVector<QVector<Cell>> &grid, int row, int col, int dr, int dc,
Cell stone)
{
const int N = int(grid.size());
int n = 0;
int r = row + dr;
int c = col + dc;
while (r >= 0 && r < N && c >= 0 && c < N && grid[r][c] == stone) {
++n;
r += dr;
c += dc;
}
return n;
}
bool BoardModel::winsOnGrid(const QVector<QVector<Cell>> &grid, int row, int col, Cell stone)
{
// 사용자 룰: 정확히 5목만 승리. 6목 이상은 장목으로 간주하여 승 아님 (호출자가 별도 처리).
static const int dr[] = {0, 1, 1, 1};
static const int dc[] = {1, 0, 1, -1};
for (int i = 0; i < 4; ++i) {
const int cnt = 1 + extendGrid(grid, row, col, dr[i], dc[i], stone)
+ extendGrid(grid, row, col, -dr[i], -dc[i], stone);
if (cnt == WIN_LENGTH)
return true;
}
return false;
}
bool BoardModel::hasOverline(const QVector<QVector<Cell>> &grid, int row, int col, Cell stone)
{
if (stone != Cell::Black && stone != Cell::White)
return false;
static const int dr[] = {0, 1, 1, 1};
static const int dc[] = {1, 0, 1, -1};
for (int i = 0; i < 4; ++i) {
const int cnt = 1 + extendGrid(grid, row, col, dr[i], dc[i], stone)
+ extendGrid(grid, row, col, -dr[i], -dc[i], stone);
if (cnt >= OVERLINE_MIN)
return true;
}
return false;
}
bool BoardModel::hasOpenThreeAlong(const QVector<QVector<Cell>> &grid, int row, int col, int dr,
int dc, Cell stone)
{
if (stone != Cell::Black && stone != Cell::White)
return false;
if (grid[row][col] != stone)
return false;
const int N = int(grid.size());
auto inBounds = [N](int y, int x) { return y >= 0 && y < N && x >= 0 && x < N; };
auto isEmptyAt = [&](const QVector<QVector<Cell>> &g, int y, int x) {
return inBounds(y, x) && g[y][x] == Cell::None;
};
// 정통 렌주(連珠) 규칙:
// 활삼(live three) 정의 — 같은 직선상(±4칸 윈도우) 빈 칸 한 곳에 흑을 더 두면 *활사*
// (open four = 양 끝 모두 빈 4목) 가 만들어지며, 그 활사가 (row,col) 도 포함해야 한다.
//
// 단, **이미 그 직선이 "사(四)"** 라면 활삼이 아니다.
// 즉, 같은 축의 어떤 빈 칸 한 곳에 흑을 두면 (row,col) 을 통과하는 정확히 5목이 만들어진다면
// 그 위치는 *사*이지 *삼* 이 아니다 (예: B_BBB, BBB_B, BB_BB, BBBB_, 등 — 빈사목/사충 등).
//
// 이 두 조건을 모두 만족시켜야 활삼으로 판정합니다. 그래야 "한 수에 5 가 되는 줄" 을
// 활삼으로 잘못 카운트해서 금삼삼/금사사 오판이 나는 것을 막을 수 있습니다.
// 1) 같은 축에서 (row,col) 을 통과하는 즉석 5목 이상 이면 그건 *사* → 활삼 아님.
// 사용자 룰에서는 흑·백 모두 6+ 가 장목 금수이므로, 5 이상이면 일단 *사* 로 본다.
auto makesFiveAlong = [&](int fr, int fc) -> bool {
QVector<QVector<Cell>> gg = grid;
gg[fr][fc] = stone;
const int cnt = 1
+ extendGrid(gg, row, col, dr, dc, stone)
+ extendGrid(gg, row, col, -dr, -dc, stone);
return cnt >= WIN_LENGTH;
};
for (int off = -5; off <= 5; ++off) {
if (off == 0)
continue;
const int fr = row + off * dr;
const int fc = col + off * dc;
if (!inBounds(fr, fc))
continue;
if (grid[fr][fc] != Cell::None)
continue;
if (makesFiveAlong(fr, fc))
return false;
}
// 2) 빈 칸 한 곳에 stone 을 두면 활사가 되는 자리가 있으며, 그 활사가 (row,col) 을 포함하는지.
for (int off = -4; off <= 4; ++off) {
if (off == 0)
continue;
const int er = row + off * dr;
const int ec = col + off * dc;
if (!inBounds(er, ec))
continue;
if (grid[er][ec] != Cell::None)
continue;
QVector<QVector<Cell>> g = grid;
g[er][ec] = stone;
int sR = er;
int sC = ec;
while (sR - dr >= 0 && sR - dr < N && sC - dc >= 0 && sC - dc < N
&& g[sR - dr][sC - dc] == stone) {
sR -= dr;
sC -= dc;
}
int newLen = 0;
int tR = sR;
int tC = sC;
while (tR >= 0 && tR < N && tC >= 0 && tC < N && g[tR][tC] == stone) {
++newLen;
tR += dr;
tC += dc;
}
if (newLen != 4)
continue;
const int e1r = sR - dr;
const int e1c = sC - dc;
const int e2r = sR + 4 * dr;
const int e2c = sC + 4 * dc;
if (!isEmptyAt(g, e1r, e1c) || !isEmptyAt(g, e2r, e2c))
continue;
bool rowColInRun = false;
for (int k = 0; k < 4 && !rowColInRun; ++k) {
if (sR + k * dr == row && sC + k * dc == col)
rowColInRun = true;
}
if (!rowColInRun)
continue;
return true;
}
return false;
}
bool BoardModel::hasOpenFourAlong(const QVector<QVector<Cell>> &grid, int row, int col, int dr,
int dc, Cell stone)
{
if (stone != Cell::Black && stone != Cell::White)
return false;
if (grid[row][col] != stone)
return false;
const int N = int(grid.size());
int r0 = row;
int c0 = col;
while (r0 - dr >= 0 && r0 - dr < N && c0 - dc >= 0 && c0 - dc < N
&& grid[r0 - dr][c0 - dc] == stone) {
r0 -= dr;
c0 -= dc;
}
int len = 0;
int rr = r0;
int cc = c0;
while (rr >= 0 && rr < N && cc >= 0 && cc < N && grid[rr][cc] == stone) {
++len;
rr += dr;
cc += dc;
}
if (len != 4)
return false;
const int ar = r0 - dr;
const int ac = c0 - dc;
const int br = r0 + len * dr;
const int bc = c0 + len * dc;
auto endsOpen = [&](int y, int x) {
if (y < 0 || y >= N || x < 0 || x >= N)
return false;
return grid[y][x] == Cell::None;
};
return endsOpen(ar, ac) && endsOpen(br, bc);
}
int BoardModel::countOpenThreeAxes(const QVector<QVector<Cell>> &grid, int row, int col,
Cell stone)
{
static const int dr[] = {0, 1, 1, 1};
static const int dc[] = {1, 0, 1, -1};
int n = 0;
for (int i = 0; i < 4; ++i) {
if (hasOpenThreeAlong(grid, row, col, dr[i], dc[i], stone))
++n;
}
return n;
}
int BoardModel::countOpenFourAxes(const QVector<QVector<Cell>> &grid, int row, int col,
Cell stone)
{
static const int dr[] = {0, 1, 1, 1};
static const int dc[] = {1, 0, 1, -1};
int n = 0;
for (int i = 0; i < 4; ++i) {
if (hasOpenFourAlong(grid, row, col, dr[i], dc[i], stone))
++n;
}
return n;
}
bool BoardModel::formsWallAttachedRun(int row, int col, Cell stone, int runLen) const
{
if (stone != Cell::Black && stone != Cell::White)
return false;
if (row < 0 || row >= m_size || col < 0 || col >= m_size)
return false;
if (m_cells[row][col] != Cell::None && m_cells[row][col] != stone)
return false;
if (runLen < 1)
return false;
auto cellAtH = [&](int r, int c) -> Cell {
if (r == row && c == col)
return stone;
return m_cells[r][c];
};
static const int dr[] = {0, 1, 1, 1};
static const int dc[] = {1, 0, 1, -1};
for (int i = 0; i < 4; ++i) {
int sr = row;
int sc = col;
while (true) {
const int nr = sr - dr[i];
const int nc = sc - dc[i];
if (nr < 0 || nr >= m_size || nc < 0 || nc >= m_size)
break;
if (cellAtH(nr, nc) != stone)
break;
sr = nr;
sc = nc;
}
int len = 0;
int er = sr;
int ec = sc;
while (er >= 0 && er < m_size && ec >= 0 && ec < m_size && cellAtH(er, ec) == stone) {
++len;
er += dr[i];
ec += dc[i];
}
if (len != runLen)
continue;
const int e1r = sr - dr[i];
const int e1c = sc - dc[i];
const int e2r = sr + len * dr[i];
const int e2c = sc + len * dc[i];
const bool wall1 = (e1r < 0 || e1r >= m_size || e1c < 0 || e1c >= m_size);
const bool wall2 = (e2r < 0 || e2r >= m_size || e2c < 0 || e2c >= m_size);
if (wall1 || wall2)
return true;
}
return false;
}
QString BoardModel::explainOpenLines(int row, int col, Cell stone) const
{
if (stone != Cell::Black && stone != Cell::White)
return {};
if (row < 0 || row >= m_size || col < 0 || col >= m_size)
return {};
QVector<QVector<Cell>> g = m_cells;
if (g[row][col] == Cell::None)
g[row][col] = stone;
static const int dr[] = {0, 1, 1, 1};
static const int dc[] = {1, 0, 1, -1};
static const char *axisName[] = {"가로 ─", "세로 │", "대각 \", "대각 /"};
auto cellChar = [stone](Cell c) -> QChar {
if (c == Cell::None)
return QLatin1Char('.');
if (c == stone)
return QLatin1Char('X');
return QLatin1Char('O');
};
QStringList rows;
for (int i = 0; i < 4; ++i) {
const bool open3 = hasOpenThreeAlong(g, row, col, dr[i], dc[i], stone);
const bool open4 = hasOpenFourAlong(g, row, col, dr[i], dc[i], stone);
QString line;
for (int off = -5; off <= 5; ++off) {
const int r = row + off * dr[i];
const int c = col + off * dc[i];
if (r < 0 || r >= m_size || c < 0 || c >= m_size) {
line.append(QLatin1Char('|'));
} else {
line.append(cellChar(g[r][c]));
}
}
QStringList tags;
if (open3)
tags << QStringLiteral("활삼");
if (open4)
tags << QStringLiteral("활사");
rows << QStringLiteral("%1 [%2] %3")
.arg(QString::fromUtf8(axisName[i]),
line,
tags.isEmpty() ? QStringLiteral("-") : tags.join(QLatin1Char('+')));
}
return rows.join(QLatin1Char('\n'));
}
bool BoardModel::hasKyungsuBan(int row, int col, Cell stone) const
{
if (stone != Cell::Black && stone != Cell::White)
return false;
if (row < 0 || row >= m_size || col < 0 || col >= m_size)
return false;
if (m_cells[row][col] != Cell::None)
return false;
// 즉석 5목 승리 수는 금수가 아님.
if (wouldWinIfPlaced(row, col, stone))
return false;
QVector<QVector<Cell>> g = m_cells;
g[row][col] = stone;
// 장목 (직선 6목 이상): 흑·백 모두 금수 (사용자 룰).
if (hasOverline(g, row, col, stone))
return true;
// 삼삼: 흑·백 모두 검사.
if (countOpenThreeAxes(g, row, col, stone) >= 2)
return true;
// 사사(4-4)는 금수가 아님 — 사용자 룰 (broken-4 조합은 33 검사로 처리).
return false;
}
bool BoardModel::wouldWinIfPlaced(int row, int col, Cell stone) const
{
if (row < 0 || row >= m_size || col < 0 || col >= m_size)
return false;
if (m_cells[row][col] != Cell::None)
return false;
QVector<QVector<Cell>> g = m_cells;
g[row][col] = stone;
// 장목은 흑·백 모두 승 아님 (사용자 룰).
if (hasOverline(g, row, col, stone))
return false;
return winsOnGrid(g, row, col, stone);
}
bool BoardModel::isWin(int row, int col, Cell stone) const
{
// 장목은 흑·백 모두 승 아님.
if (hasOverline(m_cells, row, col, stone))
return false;
return winsOnGrid(m_cells, row, col, stone);
}
QPoint BoardModel::findOpponentDoubleThreatBlock(Cell opp) const
{
if (opp == Cell::None)
return QPoint(-1, -1);
QVector<QVector<Cell>> g = m_cells;
auto wouldOppWinAt = [&](int wr, int wc) -> bool {
if (g[wr][wc] != Cell::None)
return false;
g[wr][wc] = opp;
// 장목은 흑·백 모두 승 아님 (사용자 룰).
const bool overline = hasOverline(g, wr, wc, opp);
const bool win = !overline && winsOnGrid(g, wr, wc, opp);
g[wr][wc] = Cell::None;
return win;
};
for (int br = 0; br < m_size; ++br) {
for (int bc = 0; bc < m_size; ++bc) {
if (g[br][bc] != Cell::None)
continue;
if (hasKyungsuBan(br, bc, opp))
continue;
g[br][bc] = opp;
int winCount = 0;
for (int wr = 0; wr < m_size && winCount < 2; ++wr) {
for (int wc = 0; wc < m_size && winCount < 2; ++wc) {
if (g[wr][wc] != Cell::None)
continue;
if (wouldOppWinAt(wr, wc))
++winCount;
}
}
g[br][bc] = Cell::None;
if (winCount >= 2)
return QPoint(bc, br);
}
}
return QPoint(-1, -1);
}