게시판오목으로 이해하는 자기대국과 모방 학습예제 소스

예제 소스

오목으로 이해하는 자기대국과 모방 학습 · 파일 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);
}