예제 소스
오목 Next.js 웹과 VLM 착수 API 연결하기 · 파일 59개
boardmodel.h
#pragma once
#include <QPoint>
#include <QVector>
constexpr int WIN_LENGTH = 5;
/// 보드 크기 옵션. 13/15/19 만 지원.
constexpr int kDefaultBoardSize = 13;
/// 정통 BOARD_SIZE 매크로는 더 이상 글로벌 상수가 아닙니다.
/// 호환성을 위해 "기본 크기" 로만 노출합니다 (런타임에는 BoardModel::size() 를 쓰세요).
constexpr int BOARD_SIZE = kDefaultBoardSize;
enum class Cell : uint8_t { None = 0, Black = 1, White = 2 };
enum class GameMode {
HumanVsHuman,
HumanVsAi,
HumanVsVlmTcp,
AiVsAi,
AiVsVlm, // 로컬 AI 와 VLM 이 대국 (학습/평가용 자동 진행).
VlmVsVlm, // 양쪽 모두 VLM (동일 TCP 서버·단일 모델, 차례마다 ai_side 만 바꿔 호출).
};
enum class GameStatus { Playing, BlackWins, WhiteWins, Draw };
enum class MoveRejectReason {
None,
GameNotPlaying,
OutOfBounds,
Occupied,
SansanBan, // 삼삼: 흑·백 모두 적용
DoubleFourBanBlack, // (사용자 룰: 사용 안 함, 호환을 위해 남김)
ChangmokBan, // 장목: 흑·백 모두 적용 (직선 6목 이상)
ChangmokBanBlack = ChangmokBan, // 호환 별칭 (기존 코드).
};
class BoardModel {
public:
explicit BoardModel(int sz = kDefaultBoardSize);
/** 크기를 바꾸며 새로 시작합니다. sz 는 13/15/19 중 하나여야 합니다(아니면 기본값). */
void resize(int sz);
int size() const { return m_size; }
void reset();
/** 금수 등으로 두지 못할 때 `outReason` 에 이유가 설정됩니다. */
bool playAt(int row, int col, MoveRejectReason *outReason = nullptr);
Cell cellAt(int row, int col) const { return m_cells[row][col]; }
GameStatus status() const { return m_status; }
Cell currentTurn() const { return m_turn; }
GameMode mode() const { return m_mode; }
void setMode(GameMode m);
/** 사람이 잡는 색 (HumanVsAi/HumanVsVlmTcp 에서만 의미 있음). 기본 Black. */
Cell humanSide() const { return m_humanSide; }
void setHumanSide(Cell c) { m_humanSide = (c == Cell::White) ? Cell::White : Cell::Black; }
/** VLM 이 잡는 색 (HumanVsVlmTcp/AiVsVlm; VlmVsVlm 에서는 미사용). 기본 White. */
Cell vlmSide() const { return m_vlmSide; }
void setVlmSide(Cell c) { m_vlmSide = (c == Cell::Black) ? Cell::Black : Cell::White; }
/** 해당 칸이 비어 있고 돌을 두었을 때 (장목 제외 포함) 정확히 5목으로 승리하는지. */
bool wouldWinIfPlaced(int row, int col, Cell stone) const;
/**
* 사용자 룰 오목 금수 검사 (흑·백 동일):
* - 장목 (직선 6목 이상): 금수
* - 삼삼 (활삼 2개 이상): 금수
* - 사사 (4-4): 허용 (broken-4 조합은 이미 33 검사에서 잡힘)
* 해당 칸이 비어 있고 즉석 5목 승리수가 아닐 때만 의미.
*/
bool hasKyungsuBan(int row, int col, Cell stone) const;
/** 호환: 흑 전용 wrapper. */
bool hasKyungsuBanForBlack(int row, int col) const { return hasKyungsuBan(row, col, Cell::Black); }
/**
* 상대(opp) 가 한 수 더 두면 5목을 만들 자리(double threat 포함, "오픈4" 등)가
* 생기는 자리를 미리 차단해야 한다. 그런 자리를 찾으면 (col=x, row=y)로 반환.
* 못 찾으면 (-1,-1) 반환.
*/
QPoint findOpponentDoubleThreatBlock(Cell opponent) const;
/**
* (row, col) 에 stone 을 놓는다고 가정했을 때, 어느 한 축에서 같은 색 *maximal run*
* 의 길이가 정확히 runLen (=3 또는 4) 이고 그 run 의 양 끝 중 적어도 한쪽이
* 보드 밖(벽)에 닿아 있는지 (= "벽에 붙은 3/4") 검사합니다.
*
* 정통 규칙은 아니며, AI/VLM 이 약한 모양을 피하도록 하는 휴리스틱 가드용입니다.
* 즉시승수/즉시 차단보다는 항상 우선순위가 낮아야 합니다.
*/
bool formsWallAttachedRun(int row, int col, Cell stone, int runLen) const;
/**
* 진단용. (row, col) 에 stone 을 두면 어느 축이 활삼/활사로 잡히는지,
* 그리고 해당 축의 한 줄 단면("BWWW_ 등") 을 사람이 읽을 수 있게 돌려줍니다.
* 거부 메시지에 붙여서 false-positive 를 빠르게 디버깅하기 위함.
*/
QString explainOpenLines(int row, int col, Cell stone) const;
private:
bool isWin(int row, int col, Cell stone) const;
int boardSize() const { return m_size; }
static int extendGrid(const QVector<QVector<Cell>> &grid, int row, int col, int dr, int dc,
Cell stone);
/// 정확히 5목 (cnt == 5) 일 때만 승. 6목 이상은 장목으로 간주하여 승 아님.
static bool winsOnGrid(const QVector<QVector<Cell>> &grid, int row, int col, Cell stone);
/// 흑·백 어느 색이든 직선 6목 이상이면 true (장목 = 금수).
static bool hasOverline(const QVector<QVector<Cell>> &grid, int row, int col, Cell stone);
static bool hasOpenThreeAlong(const QVector<QVector<Cell>> &grid, int row, int col, int dr,
int dc, Cell stone);
static bool hasOpenFourAlong(const QVector<QVector<Cell>> &grid, int row, int col, int dr,
int dc, Cell stone);
static int countOpenThreeAxes(const QVector<QVector<Cell>> &grid, int row, int col, Cell stone);
static int countOpenFourAxes(const QVector<QVector<Cell>> &grid, int row, int col, Cell stone);
int m_size;
QVector<QVector<Cell>> m_cells;
Cell m_turn = Cell::Black;
Cell m_humanSide = Cell::Black;
Cell m_vlmSide = Cell::White;
GameMode m_mode = GameMode::HumanVsHuman;
GameStatus m_status = GameStatus::Playing;
};