게시판바둑 on-policy 학습: 대국 로그를 KataGo 라벨로 바꾸기예제 소스

예제 소스

바둑 on-policy 학습: 대국 로그를 KataGo 라벨로 바꾸기 · 파일 169개

goboard.cpp

#include "goboard.h"

#include <QQueue>
#include <algorithm>

namespace {

inline bool inBounds(int n, int r, int c)
{
    return r >= 0 && r < n && c >= 0 && c < n;
}

constexpr int kDR[4] = {-1, 1, 0, 0};
constexpr int kDC[4] = {0, 0, -1, 1};

} // namespace

GoBoard::GoBoard()
{
    reset();
}

void GoBoard::reset()
{
    m_cells.assign(m_size * m_size, Stone::Empty);
    m_turn = Stone::Black;
    m_status = GameStatus::Playing;
    m_capturedByBlack = 0;
    m_capturedByWhite = 0;
    m_consecutivePasses = 0;
    m_moveCount = 0;
    m_lastMove = QPoint(-1, -1);
    m_lastWasPass = false;
    m_phase = GamePhase::Playing;
    m_lastPasser = Stone::Empty;
    m_hasFinalReport = false;
    m_finalReport = ScoreReport{};
    m_pastPositions.clear();
    m_pastPositions.insert(packed());
}

void GoBoard::setMode(GameMode m)
{
    m_mode = m;
}

void GoBoard::collectGroup(int row, int col, QVector<int> *outGroup,
                           QVector<int> *outLiberties) const
{
    if (outGroup)
        outGroup->clear();
    if (outLiberties)
        outLiberties->clear();

    const int N = m_size;
    const Stone color = cellAt(row, col);
    if (color == Stone::Empty)
        return;

    QVector<bool> visited(N * N, false);
    QVector<bool> libSeen(N * N, false);
    QQueue<int> q;
    const int startIdx = row * N + col;
    q.enqueue(startIdx);
    visited[startIdx] = true;

    while (!q.isEmpty()) {
        const int idx = q.dequeue();
        if (outGroup)
            outGroup->append(idx);
        const int r = idx / N;
        const int c = idx % N;
        for (int d = 0; d < 4; ++d) {
            const int nr = r + kDR[d];
            const int nc = c + kDC[d];
            if (!inBounds(N, nr, nc))
                continue;
            const int nidx = nr * N + nc;
            const Stone v = m_cells[nidx];
            if (v == Stone::Empty) {
                if (outLiberties && !libSeen[nidx]) {
                    libSeen[nidx] = true;
                    outLiberties->append(nidx);
                }
            } else if (v == color && !visited[nidx]) {
                visited[nidx] = true;
                q.enqueue(nidx);
            }
        }
    }
}

int GoBoard::liberties(int row, int col) const
{
    QVector<int> libs;
    collectGroup(row, col, nullptr, &libs);
    return libs.size();
}

QByteArray GoBoard::packed() const
{
    QByteArray p;
    p.resize(m_size * m_size);
    for (int i = 0; i < m_cells.size(); ++i)
        p[i] = static_cast<char>(static_cast<unsigned char>(m_cells[i]));
    return p;
}

bool GoBoard::wouldRecreatePastPosition(const QByteArray &p) const
{
    return m_pastPositions.contains(p);
}

bool GoBoard::isLegalMove(int row, int col, Stone who, MoveRejectReason *outReason) const
{
    auto setReason = [outReason](MoveRejectReason r) {
        if (outReason)
            *outReason = r;
    };
    setReason(MoveRejectReason::None);

    if (m_status != GameStatus::Playing || m_phase != GamePhase::Playing) {
        setReason(MoveRejectReason::GameNotPlaying);
        return false;
    }
    const int N = m_size;
    if (!inBounds(N, row, col)) {
        setReason(MoveRejectReason::OutOfBounds);
        return false;
    }
    if (who != Stone::Black && who != Stone::White) {
        setReason(MoveRejectReason::OutOfBounds);
        return false;
    }
    if (m_cells[row * N + col] != Stone::Empty) {
        setReason(MoveRejectReason::Occupied);
        return false;
    }

    GoBoard sim = *this; // 복사 후 시뮬레이션
    const Stone opp = opponent(who);
    sim.m_cells[row * N + col] = who;

    int captured = 0;
    QVector<int> visited(N * N, 0);
    for (int d = 0; d < 4; ++d) {
        const int nr = row + kDR[d];
        const int nc = col + kDC[d];
        if (!inBounds(N, nr, nc))
            continue;
        const int nidx = nr * N + nc;
        if (sim.m_cells[nidx] != opp || visited[nidx])
            continue;
        QVector<int> grp;
        QVector<int> libs;
        sim.collectGroup(nr, nc, &grp, &libs);
        for (int gi : grp)
            visited[gi] = 1;
        if (libs.isEmpty()) {
            for (int gi : grp) {
                sim.m_cells[gi] = Stone::Empty;
                ++captured;
            }
        }
    }

    if (sim.liberties(row, col) == 0) {
        setReason(MoveRejectReason::Suicide);
        return false;
    }

    if (sim.wouldRecreatePastPosition(sim.packed())) {
        setReason(MoveRejectReason::KoViolation);
        return false;
    }

    Q_UNUSED(captured);
    return true;
}

bool GoBoard::playAt(int row, int col, MoveRejectReason *outReason)
{
    const Stone who = m_turn;
    if (!isLegalMove(row, col, who, outReason))
        return false;

    const int N = m_size;
    const Stone opp = opponent(who);
    m_cells[row * N + col] = who;

    int captured = 0;
    QVector<int> visited(N * N, 0);
    for (int d = 0; d < 4; ++d) {
        const int nr = row + kDR[d];
        const int nc = col + kDC[d];
        if (!inBounds(N, nr, nc))
            continue;
        const int nidx = nr * N + nc;
        if (m_cells[nidx] != opp || visited[nidx])
            continue;
        QVector<int> grp;
        QVector<int> libs;
        collectGroup(nr, nc, &grp, &libs);
        for (int gi : grp)
            visited[gi] = 1;
        if (libs.isEmpty()) {
            for (int gi : grp) {
                m_cells[gi] = Stone::Empty;
                ++captured;
            }
        }
    }

    if (who == Stone::Black)
        m_capturedByBlack += captured;
    else
        m_capturedByWhite += captured;

    m_pastPositions.insert(packed());
    m_consecutivePasses = 0;
    m_lastMove = QPoint(row, col);
    m_lastWasPass = false;
    m_turn = opp;
    ++m_moveCount;
    return true;
}

void GoBoard::pass()
{
    // 새 정책: 패스 자체로는 게임이 끝나지 않는다. 게임 종료는 오직 resign() 으로.
    // (UI 에서 "패스" 버튼은 "패배 선언 의사" 확인 다이얼로그를 띄우고,
    //  사람/자동 actor 가 "패배" 를 선택하면 resign() 이 호출되며,
    //  "계속 두기" 면 이 함수는 호출되지 않는다.)
    // 따라서 이 함수는 호환성을 위한 단순 턴 토글만 수행한다.
    if (m_status != GameStatus::Playing || m_phase != GamePhase::Playing)
        return;
    ++m_consecutivePasses;
    m_lastMove = QPoint(-1, -1);
    m_lastWasPass = true;
    m_lastPasser = m_turn;
    m_turn = opponent(m_turn);
    ++m_moveCount;
}

void GoBoard::resign()
{
    if (m_status != GameStatus::Playing)
        return;
    m_status = (m_turn == Stone::Black) ? GameStatus::WhiteWins : GameStatus::BlackWins;
    m_phase = GamePhase::Finished;
}

void GoBoard::acceptScoring(const ScoreReport &finalReport)
{
    if (m_phase != GamePhase::AwaitingScoringDecision)
        return;
    m_finalReport = finalReport;
    m_hasFinalReport = true;
    if (finalReport.winner == Stone::Black)
        m_status = GameStatus::BlackWins;
    else if (finalReport.winner == Stone::White)
        m_status = GameStatus::WhiteWins;
    else
        m_status = GameStatus::Draw;
    m_phase = GamePhase::Finished;
}

void GoBoard::declineScoring()
{
    if (m_phase != GamePhase::AwaitingScoringDecision)
        return;
    // 패스한 쪽이 "둘 곳 없음" 을 인정한 셈. 상대가 계가 거부 → 패스 측 불계패.
    if (m_lastPasser == Stone::Black)
        m_status = GameStatus::WhiteWins;
    else if (m_lastPasser == Stone::White)
        m_status = GameStatus::BlackWins;
    else
        m_status = GameStatus::Draw;
    m_hasFinalReport = false; // 불계 → 점수표 없음
    m_phase = GamePhase::Finished;
}

Stone GoBoard::territoryOwner(const GoBoard &b, int row, int col)
{
    const int N = b.size();
    if (b.cellAt(row, col) != Stone::Empty)
        return b.cellAt(row, col);

    QVector<bool> visited(N * N, false);
    QQueue<int> q;
    q.enqueue(row * N + col);
    visited[row * N + col] = true;

    bool seenBlack = false;
    bool seenWhite = false;
    while (!q.isEmpty()) {
        const int idx = q.dequeue();
        const int r = idx / N;
        const int c = idx % N;
        for (int d = 0; d < 4; ++d) {
            const int nr = r + kDR[d];
            const int nc = c + kDC[d];
            if (!inBounds(N, nr, nc))
                continue;
            const int nidx = nr * N + nc;
            const Stone v = b.m_cells[nidx];
            if (v == Stone::Empty) {
                if (!visited[nidx]) {
                    visited[nidx] = true;
                    q.enqueue(nidx);
                }
            } else if (v == Stone::Black) {
                seenBlack = true;
            } else if (v == Stone::White) {
                seenWhite = true;
            }
            if (seenBlack && seenWhite)
                return Stone::Empty; // dame
        }
    }
    if (seenBlack && !seenWhite)
        return Stone::Black;
    if (seenWhite && !seenBlack)
        return Stone::White;
    return Stone::Empty;
}

ScoreReport GoBoard::scoreNow() const
{
    // 종국이 결정된 후에는 acceptScoring 으로 저장된 점수 (사석/dame 반분 적용)
    // 를 그대로 반환. 그렇지 않으면 보드 상태 기반 area 어림.
    if (m_phase == GamePhase::Finished && m_hasFinalReport)
        return m_finalReport;

    ScoreReport rep;
    rep.komi = m_komi;
    const int N = m_size;
    QVector<bool> visited(N * N, false);

    for (int r = 0; r < N; ++r) {
        for (int c = 0; c < N; ++c) {
            const Stone v = m_cells[r * N + c];
            if (v == Stone::Black)
                ++rep.blackStones;
            else if (v == Stone::White)
                ++rep.whiteStones;
        }
    }

    for (int r = 0; r < N; ++r) {
        for (int c = 0; c < N; ++c) {
            if (visited[r * N + c])
                continue;
            if (m_cells[r * N + c] != Stone::Empty) {
                visited[r * N + c] = true;
                continue;
            }
            // BFS over empty region
            QQueue<int> q;
            q.enqueue(r * N + c);
            visited[r * N + c] = true;
            QVector<int> region;
            bool seenBlack = false;
            bool seenWhite = false;
            while (!q.isEmpty()) {
                const int idx = q.dequeue();
                region.append(idx);
                const int rr = idx / N;
                const int cc = idx % N;
                for (int d = 0; d < 4; ++d) {
                    const int nr = rr + kDR[d];
                    const int nc = cc + kDC[d];
                    if (!inBounds(N, nr, nc))
                        continue;
                    const int nidx = nr * N + nc;
                    const Stone nv = m_cells[nidx];
                    if (nv == Stone::Empty) {
                        if (!visited[nidx]) {
                            visited[nidx] = true;
                            q.enqueue(nidx);
                        }
                    } else if (nv == Stone::Black) {
                        seenBlack = true;
                    } else {
                        seenWhite = true;
                    }
                }
            }
            const int sz = region.size();
            if (seenBlack && !seenWhite)
                rep.blackTerritory += sz;
            else if (seenWhite && !seenBlack)
                rep.whiteTerritory += sz;
            else
                rep.dame += sz;
        }
    }

    rep.finalBlack = rep.blackStones + rep.blackTerritory;
    rep.finalWhite = rep.whiteStones + rep.whiteTerritory + rep.komi;
    if (rep.finalBlack > rep.finalWhite)
        rep.winner = Stone::Black;
    else if (rep.finalWhite > rep.finalBlack)
        rep.winner = Stone::White;
    else
        rep.winner = Stone::Empty;
    return rep;
}

void GoBoard::finalize()
{
    const ScoreReport rep = scoreNow();
    if (rep.winner == Stone::Black)
        m_status = GameStatus::BlackWins;
    else if (rep.winner == Stone::White)
        m_status = GameStatus::WhiteWins;
    else
        m_status = GameStatus::Draw;
}