예제 소스
바둑 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;
}