글자 크기

바둑 좌표계의 미로: SGF, GTP, 그리고 화면 좌표

“Q16에 두세요.”

바둑을 배우면 자연스럽게 익히는 좌표 표기법이다. 가로는 A부터 T까지(I 제외), 세로는 1부터 19까지. 오른쪽 위 귀가 Q16이다.

근데 코드로 바둑판을 다루기 시작하면 — 지옥이 열린다.

SGF 파일에는 “pd”라고 적혀 있다. KataGo는 “Q16″을 말한다. 화면에 그리려면 픽셀 좌표가 필요하다. 그리고 이 셋은 전부 다른 규칙을 따른다.

세 가지 좌표계

1. SGF 좌표

SGF(Smart Game Format)는 바둑 기보의 표준 형식이다.

;B[pd];W[dd];B[qp]

흑이 pd에, 백이 dd에, 흑이 qp에 뒀다.

규칙:

  • 소문자 a부터 s까지 (19개)
  • 첫 글자가 열, 두 번째 글자가 행
  • 왼쪽 위가 aa, 오른쪽 아래가 ss
  • I를 건너뛰지 않는다 (a-s 전부 사용)

pd는? p열, d행. 오른쪽에서 4번째 열, 위에서 4번째 행. 오른쪽 위 귀의 화점이다.

2. GTP 좌표

GTP(Go Text Protocol)는 바둑 AI와 통신하는 프로토콜이다. KataGo, Leela Zero 모두 GTP를 쓴다.

play black Q16
play white D16

규칙:

  • 대문자 A부터 T까지 (I 제외, 19개)
  • 첫 글자가 열, 숫자가 행
  • 왼쪽 아래가 A1, 오른쪽 위가 T19
  • I를 건너뛴다 (A-H, J-T)

Q16은? Q열, 16행. 오른쪽에서 4번째 열, 아래에서 16번째 행. SGF의 pd와 같은 위치다.

3. 화면 좌표

프로그램 내부에서 바둑판을 2차원 배열로 표현한다.

board[row][col]  # row: 0-18, col: 0-18

여기서 row 0이 어디냐가 문제다.

  • 배열의 row 0을 위쪽으로 잡으면 SGF와 비슷하다
  • 배열의 row 0을 아래쪽으로 잡으면 GTP와 비슷하다

나는 아래쪽으로 잡았다. board[0][0]이 A1(왼쪽 아래)이다. GTP와 맞추면 KataGo 연동이 편하니까.

그런데 화면에 그릴 때는 또 달라진다. Plotly의 y축은 아래가 0이고 위로 갈수록 커진다. 근데 바둑판은 위에서 19, 아래에서 1이다.

내가 빠진 함정들

함정 1: I를 잊었다

처음엔 단순하게 생각했다.

# 잘못된 코드
gtp_col = chr(ord('A') + col)

A부터 S까지 19개면 되는 거 아냐?

아니다. GTP는 I를 건너뛴다. 이유는 1(숫자)과 I(알파벳)이 헷갈리기 때문이라고 한다. 그래서 A-H, J-T.

결과: J 이후의 모든 돌이 한 칸씩 밀렸다. R16이 Q16 자리에 그려졌다.

함정 2: 행 방향이 반대

# SGF 행 → 배열 인덱스
row = ord(sgf_coord[1]) - ord('a')  # 'd' -> 3

SGF에서 ‘d’는 위에서 4번째다 (a=1, b=2, c=3, d=4).
근데 내 배열에서 row 3은 아래에서 4번째다.

방향이 반대다. 변환할 때 19 - row를 빼먹으면 돌이 상하 뒤집혀서 그려진다.

함정 3: 0-indexed vs 1-indexed

# GTP 행 번호
row_num = int(gtp_coord[1:])  # "16" -> 16

# 배열 인덱스
row_idx = row_num - 1  # 15

GTP는 1부터 시작한다. 배열은 0부터 시작한다. 이걸 빼먹으면 한 칸씩 밀린다.

특히 귀찮은 건 열과 행이 다르다는 것:

  • 열: A=0, B=1, … (이미 0-indexed처럼 작동)
  • 행: 1=0, 2=1, … (변환 필요)

함정 4: 시점 회전

흑으로 두면 오른쪽 위 귀가 내 진영이다.
백으로 두면 왼쪽 아래 귀가 내 진영이다.

기보를 “내 시점”으로 보려면 180° 회전해야 한다.

if rotate_for_white:
    px = 18 - col
    py = row
else:
    px = col
    py = 18 - row

이거 잘못 구현하면 돌은 맞는 위치에 그려지는데, 클릭하면 엉뚱한 곳에 착수된다. 화면 좌표와 내부 좌표가 따로 놀기 때문이다.

디버깅 팁

좌표 버그는 눈으로 찾기 어렵다. “어? 돌이 한 칸 옆에 있네” 정도의 미묘한 차이니까.

내가 쓴 방법:

1. 귀 테스트

네 귀에 돌을 놓아본다. 오른쪽 위, 왼쪽 위, 오른쪽 아래, 왼쪽 아래. 한 곳이라도 이상하면 변환 로직에 버그가 있다.

2. 화점 테스트

화점(3-3, 3-9, 3-15 등)에 돌을 놓아본다. 화점은 위치가 명확하니까 틀리면 바로 보인다.

3. 좌표 출력

print(f"SGF: {sgf} → GTP: {gtp} → Display: ({px}, {py})")

변환 과정을 전부 출력해서 어디서 틀리는지 찾는다.

교훈

“표준이 여러 개면 표준이 없는 거다.”

SGF, GTP, 화면 좌표. 각각 나름의 이유로 만들어졌다. SGF는 파일 저장에 최적화됐고, GTP는 사람이 읽기 쉽게 설계됐고, 화면 좌표는 그래픽 라이브러리가 정한다.

누구 하나 양보하지 않으니, 개발자가 중간에서 번역해야 한다.

바둑만의 문제가 아니다. 시간대(UTC, KST, Unix timestamp), 인코딩(UTF-8, EUC-KR), 줄바꿈(LF, CRLF) — 어디서나 같은 패턴이 반복된다.

결국 필요한 건 명확한 변환 함수꼼꼼한 테스트다.


이 글은 늦깎이 바둑코치 시리즈의 열네 번째 글입니다.

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다