Files
joke-app/docs/games/omok.md
EJClaw f4d63e1544 docs: 온라인 보드게임 사이트 계획서 (공통 14편 + 게임 26종)
- 개요, 조사(BGA·한국 인기 보드게임·UI/UX), 아키텍처, 실시간 프로토콜,
  안정성·보안, 계정, 로비·방, UI/UX, 데이터 모델, 게임 엔진, 테스트,
  배포·운영, 로드맵(M0~M9), 사용자 결정 항목
- 게임별 규칙·엔진 설계·UI·테스트 체크리스트 26종

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 04:18:29 +09:00

24 KiB
Raw Blame History

오목 (omok)

마일스톤: M1 · 인원: 최소 2 ~ 최대 2명 · 예상 시간: 약 5~15분 · 난이도: 쉬움 (렌주룰은 보통)

1. 개요

  • 가로·세로·대각선 중 한 방향으로 자기 돌 5개를 먼저 연속으로 놓으면 이기는 2인 추상 전략 게임. 한국에서는 바둑판과 바둑돌로 누구나 한 번쯤 둬 본 국민 게임이며, "쌍삼 금지"가 일반인 사이의 사실상 표준 규칙이다.
  • 공식 경기 규칙은 렌주(Renju, 세계렌주연맹 RIF)이며, 한국오목협회도 렌주룰로 대회를 연다.
  • 인원 근거: 원작이 흑·백 2인 게임이다. 최소 2, 최대 2. 그 외 접속자는 모두 관전자.
  • 완전 정보 게임이라 숨길 정보가 없다. 다만 시드 RNG(시간 초과 자동 착수용)와 무르기/무승부 요청 상태 같은 서버 내부 값은 view에서 정리해서 내보낸다.

2. 모드와 옵션

옵션 키 설명 선택지 기본값
ruleSet 규칙 모드 (7장 참고) korean(한국식 오목) / renju(렌주룰) / free(자유룰) korean
freeWin 자유룰에서 승리로 인정할 줄 길이 fivePlus(5목 이상 모두 승) / exactFive(정확히 5목만 승, 6목 이상은 승리 아님) fivePlus
boardSize 판 크기 15 / 19 15
firstMoveCenter 흑 첫 수를 천원(정중앙)에 강제 auto(렌주만 켬) / on / off auto
colorAssignment 흑백 정하기 random / hostBlack / hostWhite / alternate(같은 방에서 재대국 시 교대) random
timeControl.kind 시간 제한 방식 none / perMove(한 수당 N초) / fischer(전체 시간 + 수당 추가) perMove
timeControl.perMoveSec 한 수당 제한 (perMove) 10 / 20 / 30 / 60 30
timeControl.baseSec / incSec 전체 시간 / 수당 추가 (fischer) 기본 3분+2초, 5분+3초, 10분+5초 프리셋 5분+3초
timeoutPolicy perMove에서 시간 초과 시 처리 autoMove(무작위 합법 수 자동 착수) / lose(시간패) autoMove
idleLimitSec timeControl=none일 때 무응답 한도 120 / 300 / 600 300
undo 무르기 요청 허용 횟수(1인당, 상대 동의 필요) off / 1 / 3 / unlimited 3
drawOffer 무승부 제안 허용 true / false true
showForbidden 금수 자리 표시 (금수 있는 규칙만) true / false true
hints 초보 도움말: 상대의 열린 3·4 경고 표시 off / warn warn

3. 구성물

  • 15×15(교차점 225개) 또는 19×19(361개) 판. 돌은 교차점에 놓는다.
  • 흑돌, 백돌: 개수 제한 없음(판이 가득 찰 때까지).
  • 좌표 표기: 열 AO(15줄, I 포함 — 렌주 표기법), 행은 아래에서 위로 115. 19줄은 AS, 119. 엔진 내부는 (x, y), x는 왼쪽→오른쪽 0부터, y는 위→아래 0부터. 표시 좌표는 열문자[x] + (N - y).
  • 천원 = 정중앙 (15줄: H8, 19줄: J10).

4. 준비(셋업)

  1. colorAssignment로 흑/백 결정 (random이면 시드 RNG 사용).
  2. 빈 판으로 시작. 흑이 먼저 둔다.
  3. firstMoveCenter=true면 흑의 첫 수는 천원만 합법.
  4. 시계 초기화: perMove는 첫 수부터 카운트, fischer는 양쪽 baseSec.

5. 진행 규칙

  • 흑부터 번갈아 한 수씩, 빈 교차점에 자기 돌 1개를 놓는다. 놓은 돌은 움직이거나 따내지 않는다.
  • 패스는 없다(판이 가득 차면 무승부로 끝남).
  • 착수 합법 조건 (모두 만족):
    1. 게임 진행 중이고 내 차례이다.
    2. 좌표가 판 안이고 비어 있다.
    3. firstMoveCenter가 켜져 있고 첫 수라면 천원이다.
    4. 해당 규칙에서 금수가 아니다 (아래 금수 판정). 단, 5목이 완성되는 수는 금수 판정보다 우선해 항상 합법이며 즉시 승리.
  • 착수 후 승리 판정: 놓은 돌을 지나는 4방향(가로, 세로, 대각 ↘, 대각 ↗)으로 연속된 같은 색 돌의 길이 len을 구한다.

5.1 용어 정의 (엔진 판정의 기준)

  • 줄(line): 한 방향으로 쭉 이어지는 교차점들.
  • 연속 길이 run(p, dir): p를 포함해 dir 방향과 반대 방향으로 끊김 없이 이어진 같은 색 돌 수.
  • 오목(five): run == 5인 줄. 장목(overline): run >= 6.
  • 승리 기준 isWin(len):
    • korean: 흑·백 모두 len == 5. 6목 이상은 승리가 아니다(금수도 아님, 그냥 둘 수 있으나 이기지 못함).
    • renju: 흑은 len == 5, 백은 len >= 5.
    • free: freeWin=fivePlus면 len >= 5, exactFive면 len == 5.
  • 사(four): 지금 놓은 돌 p를 포함하는 4개의 돌이 있어, 빈 자리 q 하나를 더 채우면 isWin을 만족하는 오목이 되는 상태. 한 방향 안의 사의 개수 = "q를 채웠을 때 생기는 오목에서 q를 뺀 4개의 돌 집합"의 서로 다른 개수.
    • 예: .●●●●.(양쪽이 모두 오목 자리) → 돌 집합이 같으므로 사 1개(열린 사, straight four).
    • 예: ●.●●●.● (가운데 ●●● 중 하나가 방금 둔 돌) → 왼쪽 빈칸을 채우면 왼쪽 5개, 오른쪽 빈칸을 채우면 오른쪽 5개. 돌 집합이 달라 한 줄에 사 2개 → 한 줄 4-4.
  • 열린 사(straight four): 사 중에서 같은 4개 돌 집합으로 오목을 만드는 빈 자리가 2곳 이상인 것 (막을 수 없는 사).
  • 삼(three): p를 포함한 줄에서 빈 자리 q 하나를 채우면 p와 q를 모두 포함하는 열린 사가 되는 상태. 단 q를 채우는 순간 오목이 되면 그 q는 제외하고, q가 그 색에게 금수 자리라면 그 q도 제외한다(이 마지막 조건 때문에 "가짜 삼"이 생긴다). 조건을 만족하는 q가 하나라도 있으면 그 방향은 삼 1개로 센다(한 방향당 최대 1개).

5.2 금수 판정 알고리즘

규칙별 금수:

규칙 흑 백
korean 3-3 금지 3-3 금지
renju 3-3, 4-4, 장목 금지 없음 (장목도 승리)
free 없음 없음

korean에서 4-4와 4-3은 허용, 6목은 금수가 아니지만 승리도 아니다. renju에서 4-3은 허용.

// board에 color 돌을 p에 놓는 것이 금수인지. 재귀로 가짜 3-3을 거른다.
function isForbidden(board, p, color, rule, depth = 0): boolean {
  if (!rule.has33 && !rule.has44 && !rule.hasOverline) return false;
  place(board, p, color);
  try {
    // 1) 오목이 되면 금수 아님 (오목 우선)
    for (const d of DIRS) if (isWin(run(board, p, d, color), color)) return false;
    // 2) 장목 (renju 흑만)
    if (rule.hasOverline) for (const d of DIRS) if (run(board, p, d, color) >= 6) return true;
    // 3) 4-4: 4방향 사 개수 합 >= 2 (한 줄 4-4 포함)
    if (rule.has44) {
      let fours = 0;
      for (const d of DIRS) fours += fourStoneSets(board, p, d, color).size;
      if (fours >= 2) return true;
    }
    // 4) 3-3: 진짜 삼이 있는 방향 수 >= 2
    if (rule.has33) {
      let threes = 0;
      for (const d of DIRS) if (isThree(board, p, d, color, rule, depth)) threes++;
      if (threes >= 2) return true;
    }
    return false;
  } finally { remove(board, p); }
}

// p(이미 놓인 상태)를 지나는 d 방향에서, 빈칸 q 하나로 오목이 되는 "4개 돌 집합" -> 완성 자리 수
function fourStoneSets(board, p, d, color): Map<string, number> {
  const sets = new Map();
  for (let k = -4; k <= 4; k++) {
    if (k === 0) continue;
    const q = p + k*d;
    if (!onBoard(q) || !isEmpty(board, q)) continue;
    place(board, q, color);
    const seg = runSegment(board, q, d, color);          // q를 지나는 연속 구간
    if (isWin(seg.length, color) && seg.includes(p)) {
      const key = seg.filter(s => s !== q).sort().join('|');
      sets.set(key, (sets.get(key) ?? 0) + 1);
    }
    remove(board, q);
  }
  return sets;
}

function isThree(board, p, d, color, rule, depth): boolean {
  for (let k = -4; k <= 4; k++) {
    if (k === 0) continue;
    const q = p + k*d;
    if (!onBoard(q) || !isEmpty(board, q)) continue;
    place(board, q, color);
    const makesFive = run(board, q, d, color) >= 5;
    const straight = !makesFive && [...fourStoneSets(board, p, d, color)]
      .some(([key, n]) => n >= 2 && key.includes(q));      // q를 포함하는 열린 사
    remove(board, q);
    if (straight && !isForbidden(board, q, color, rule, depth + 1)) return true;
  }
  return false;
}
  • 핵심 포인트
    • 오목 판정은 언제나 금수 판정보다 먼저다. 렌주 흑이 5목과 3-3(또는 4-4)을 동시에 만들면 승리.
    • 열린 사 판정에서 "오목"은 isWin 기준이다. 따라서 렌주 흑/한국식에서 한쪽 끝을 채우면 6목이 되는 자리는 오목 자리가 아니므로, 그런 사는 열린 사가 아니다 → 그런 줄은 삼이 아니다(장목 때문에 생기는 가짜 삼).
    • 삼의 확장 자리 q가 다시 금수인지 확인할 때 p는 이미 놓인 상태로 재귀한다. 재귀는 돌이 하나씩 늘어나므로 반드시 끝난다. 성능상 같은 (판, 자리, 색) 조합은 착수 1회 처리 동안 메모이제이션한다. 실제 판에서 깊이 3을 넘는 경우는 거의 없으며, 깊이 제한은 두지 않는다(정확성 우선).
    • korean 규칙에서도 같은 재귀를 쓴다(확장 자리가 3-3 금수면 가짜 삼).
    • 판정은 서버가 한다. 클라이언트에는 view의 forbiddenPoints로 결과만 보낸다(표시용).
  • 위 알고리즘은 문서 작성 시 스크립트로 아래 예시들을 모두 검증했다.

5.3 금수 판정 예시 (X = 지금 두려는 흑 자리, ○ = 백)

예 A — 진짜 3-3 (렌주·한국식 모두 금수)

10 + + + + + + + ● + + +
 9 + + + + + + + ● + + +
 8 + + + + + ● ● X + + +
 7 + + + + + + + + + + +
   A B C D E F G H I J K

H8에 두면 가로(F8·G8·H8)와 세로(H8·H9·H10) 두 방향 모두 양쪽이 열린 삼 → 금수.

예 B — 한 줄 4-4 (렌주 흑만 금수, 한국식은 허용)

 8 + + + ● + ● X ● + ● + +
   A B C D E F G H I J K L

G8에 두면 ●.●●●.● → E8을 채우면 D8H8 오목, I8을 채우면 F8J8 오목. 4돌 집합이 서로 달라 사 2개.

예 C — 한쪽이 막힌 삼은 삼이 아님 (금수 아님)

10 + + + + + + + ● + + +
 9 + + + + + + + ● + + +
 8 + + + + ○ ● ● X + + +
 7 + + + + + + + + + + +
   A B C D E F G H I J K

가로는 E8 백 때문에 열린 사를 만들 수 없다(I8을 채워도 J8 한쪽만 오목 자리). 삼은 세로 1개뿐 → 합법.

예 D — 장목 때문에 가짜 삼 (렌주 흑·한국식 모두 합법)

10 + + + + + + + ● + + + +
 9 + + + + + + + ● + + + +
 8 + + ● + + ● ● X + + ● +
 7 + + + + + + + + + + + +
   A B C D E F G H I J K L

가로 F8·G8·H8. I8을 채우면 J8 쪽 완성이 F8K8 6목이 되어 오목 자리가 아니고, E8을 채우면 D8 쪽 완성이 C8H8 6목이 된다. 어느 쪽도 열린 사가 안 되므로 가로는 삼이 아니다 → 삼 1개(세로)뿐 → 합법.

예 E — 확장 자리가 금수라서 가짜 삼 (렌주 흑 합법)

10 + + + + ● + + + + + +
 9 + + + + ● + + + + + +
 8 + + + + ? ● ● X + ○ +
 7 + + + + + ● + ● + + +
 6 + + + + + + ● ● + + +
   A B C D E F G H I J K

H8(X)에 두면 겉보기엔 가로(F8·G8·H8)와 세로(H8·H7·H6) 3-3이다. 그러나 가로를 열린 사로 만들 자리는 E8(?)뿐이다(I8을 채우면 J8이 백이라 한쪽만 열린 사). 그런데 E8은 흑에게 금수다: H8이 놓인 상태에서 E8에 두면 세로 E8·E9·E10 삼과 대각 E8·F7·G6 삼이 동시에 생기는 3-3이다. 따라서 가로는 가짜 삼이고, H8은 금수가 아니다.

예 F — 오목 우선: 흑이 한 수로 5목과 3-3을 동시에 만들면 승리. 렌주 흑이 한 방향 5목 + 다른 방향 장목이면? → 오목이 하나라도 있으면 승리(1단계에서 바로 false 반환).

6. 승패와 점수 계산

  • 착수 직후 isWin 줄이 있으면 착수자 승리 (reason: 'five').
  • 판이 가득 찼는데 승자가 없으면 무승부 (reason: 'boardFull'). 렌주 흑이 남은 빈칸이 모두 금수라 둘 곳이 없는 경우도 무승부로 처리한다(실전상 거의 없음, reason: 'noLegalMove').
  • 기권(resign) → 상대 승. 무승부 합의 → 무승부.
  • 시간 초과: fischer에서 시간이 0이 되면 시간패. perMove는 timeoutPolicy에 따름(lose면 시간패, autoMove면 자동 착수 후 계속).
  • 연결 끊김: 유예 시간 초과 시 onTimeout → 같은 정책 적용. 단 autoMove가 연속 3회 발생하면 그 플레이어 시간패(방치 방지).
  • 점수 개념은 없다. 결과 요약에 총 수(手) 수와 승리 줄 좌표를 남긴다.

7. 지역 룰 / 하우스 룰 (옵션으로 켜고 끄기)

  • 한국식 오목 (korean, 기본): 국내 일반인 규칙. 흑백 모두 3-3 금지, 4-4·4-3 허용, 6목 이상은 승리 아님(두는 것은 가능).
  • 렌주 (renju): RIF 공식 규칙의 착수 부분. 흑만 3-3·4-4·장목 금지, 백은 제약 없음·장목도 승리. 공식 대회의 개국 규칙(26주형, 스왑, 5수 제시 등 Soosõrv-8/Taraguchi-10)은 v1에서 구현하지 않고 firstMoveCenter만 제공한다. 추후 옵션 renjuOpening으로 확장 가능.
  • 자유룰 (free): 금수 없음. 기본은 5목 이상 승(국제 표준 freestyle gomoku 기준). exactFive를 켜면 정확히 5목만 승리(standard gomoku 계열).
  • 흑 선착 이점이 매우 크므로 재대국 시 colorAssignment=alternate를 권장한다(방 단위).
  • 무르기 허용 횟수, 금수 표시, 경고 힌트는 모두 옵션으로 끌 수 있다.

8. 엔진 설계

8.1 상태(State)

type Color = 'black' | 'white';
type Cell = 0 | 1 | 2;                       // 0 빈칸, 1 흑, 2 백
type Pt = { x: number; y: number };

interface OmokState {
  options: OmokOptions;
  size: 15 | 19;
  board: Cell[];                              // length size*size, index = y*size + x
  players: Record<Color, PlayerId>;
  turn: Color;
  moves: { color: Color; pt: Pt; at: number; auto: boolean }[]; // 기보
  phase: 'playing' | 'finished';
  winLine: Pt[] | null;                       // 승리한 5목 좌표
  clock: {
    remainingMs: Record<Color, number>;       // fischer 전용
    turnStartedAt: number;                    // 현재 차례 시작 시각(ms)
    autoMoveStreak: Record<Color, number>;
  };
  undo: { usedBy: Record<Color, number>; pending: { by: Color; plies: 1 | 2 } | null };
  draw: { pendingBy: Color | null; lastOfferPly: Record<Color, number> };
  forbidden: number[];                        // 둘 차례인 색의 금수 칸 캐시(판에서 계산 가능한 파생값)
  result: OmokResult | null;
}
// RNG 상태는 게임 상태에 넣지 않고 GameRunner가 따로 저장한다(09-game-engine.md 3절). view에는 절대 포함되지 않는다.

8.2 액션

type payload 누가 / 언제 검증 조건
place { x, y } 차례인 플레이어, playing 판 안, 빈칸, 첫 수 천원 조건, isForbidden == false (5목 완성 수는 항상 허용). 실패 사유 예: "이미 돌이 있는 자리예요", "흑은 3-3 자리에 둘 수 없어요(쌍삼 금지)", "흑은 4-4 자리에 둘 수 없어요", "흑은 6목 이상을 만들 수 없어요", "첫 수는 가운데에 둬야 해요"
requestUndo {} 플레이어 누구나, playing undo != off, 남은 횟수 > 0, pending 없음, 본인이 둔 수가 1개 이상. 직전 수가 본인 수면 1수, 상대가 이미 응수했으면 2수를 무른다
respondUndo { accept: boolean } 요청받은 상대, pending 존재 요청자 본인이 아님. 수락 시 moves에서 plies만큼 제거, 판 복원, 차례는 요청자, 요청자 사용 횟수 +1. 시계는 되돌리지 않음(사용한 시간 유지), turnStartedAt = now
offerDraw {} 플레이어, playing drawOffer=true, pendingBy == null, 직전 제안 후 본인이 3수 이상 둠(거절된 경우 도배 방지)
respondDraw { accept: boolean } 제안받은 상대 pendingBy가 상대. 수락 → 무승부 종료
cancelOffer {} 제안/요청한 본인 본인의 대기 중인 무르기·무승부 요청 취소
resign {} 플레이어, playing 언제든 가능
timeout {} 시스템(onTimeout이 생성) now >= deadline 또는 연결 끊김 유예 만료
  • 대기 중인 무르기·무승부 요청은 요청받은 쪽이 place를 하면 자동 거절로 간주해 삭제한다.
  • 모든 place 처리 시 시계 갱신: fischer면 remaining[turn] -= now - turnStartedAt, 0 이하면 시간패로 종료(착수 무효), 아니면 += incSec*1000. 이후 turnStartedAt = now.
  • 이벤트: stonePlaced, win, draw, undoRequested/Accepted/Declined, drawOffered/..., resigned, timeoutAutoMove, timeLoss.

8.3 공개/비공개 정보 (view)

  • 플레이어·관전자 공통 공개: 판, 차례, 기보, 승리 줄, 양쪽 남은 시간과 deadline, 대기 중인 무르기/무승부 요청 여부(누가 요청했는지), 남은 무르기 횟수, 결과.
  • 차례인 플레이어에게만: forbiddenPoints: Pt[] (금수 표시 옵션이 켜져 있을 때), hintThreats(힌트 옵션: 상대의 열린 삼/사 위치).
  • 관전자에게는 forbiddenPoints를 흑 기준으로 보여줄 수 있다(공개 정보이므로 허용, 해설용). 단 hints는 관전자에게 보내지 않는다(관전자가 플레이어에게 훈수 전달하는 것을 굳이 돕지 않기 위함).
  • 절대 포함 금지: rng(시드/상태 — 자동 착수 위치 예측 가능), 서버 내부 메모이제이션.

8.4 랜덤 요소 (시드 RNG 사용 지점)

  1. colorAssignment=random일 때 흑백 결정 (setup).
  2. timeoutPolicy=autoMove에서 자동 착수 위치 선택: 우선순위 (a) 내가 즉시 5목 완성 가능한 자리 (b) 상대의 5목 자리 막기 (c) 기존 돌에서 거리 2 이내의 합법 빈칸 중 균등 무작위 (d) 그 외 합법 빈칸 중 균등 무작위. 동순위 다수는 RNG로 선택. 판이 비어 있으면 천원. 그 외에는 RNG를 쓰지 않으므로, 같은 시드와 같은 액션열이면 항상 같은 결과가 나온다.

8.5 타이머·시간 초과·연결 끊김 시 자동 행동

  • deadline(state):
    • perMove: turnStartedAt + perMoveSec*1000
    • fischer: turnStartedAt + remainingMs[turn]
    • none: turnStartedAt + idleLimitSec*1000
    • finished: null
  • onTimeout(state, player)는 항상 { type: 'timeout' }을 반환하고, apply가 정책을 적용한다:
    • fischer 또는 none의 무응답 한도 초과, perMove + lose: 해당 플레이어 시간패.
    • perMove + autoMove: 8.4 규칙으로 자동 착수(moves[].auto = true), autoMoveStreak +1. 자동 착수 2번 뒤 세 번째로 연속 시간 초과하면 시간패. 정상 착수 시 streak 0으로 초기화.
  • 서버는 연결 끊긴 플레이어의 차례에 min(deadline, 끊긴 시각 + 방의 graceSec)에 onTimeout을 호출한다(유예 시간은 방 설정, 기본 60초). 끊긴 플레이어의 차례가 아닐 때는 아무것도 하지 않고, 차례가 오면 위 규칙을 따른다.
  • 시간 초과 직전에 도착한 place는 서버 도착 시각(ctx.now) 기준으로 판정한다.

8.6 종료 조건과 결과(GameResult)

interface OmokResult {
  ranking: { playerId: PlayerId; rank: 1 | 2 }[];   // 무승부면 둘 다 rank 1
  winner: Color | null;
  reason: 'five' | 'boardFull' | 'noLegalMove' | 'resign' | 'timeLoss' | 'drawAgreed';
  winLine: Pt[] | null;
  moveCount: number;
  summaryKo: string;   // 예: "흑 승 (H8~L8 오목, 37수)"
}

9. UI/UX

  • 모바일 세로: 상단에 상대 정보(닉네임, 돌 색, 남은 시간 바), 가운데 정사각 판(화면 폭 100%), 하단에 내 정보와 버튼 줄(무르기, 무승부, 기권, 규칙 보기). 15줄 판은 폰 폭 360px에서 교차점 간격이 약 23px라 48px 터치 기준을 못 맞추므로, 착수는 "탭 → 확대 미리보기 돌 표시 → 확인 버튼(48px 이상)" 2단계로 한다. 옵션으로 "한 번 탭으로 바로 두기"(PC 기본값).
  • 판 핀치 줌/드래그 지원(19줄 필수).
  • 마지막 수 표시(빨간 점), 승리 줄 강조 애니메이션.
  • 금수 자리는 흑 차례일 때 작은 ×표. 금수 자리 탭 시 이유를 말풍선으로 설명("여기는 쌍삼이라 둘 수 없어요").
  • 힌트 켜짐: 상대가 열린 삼/사를 만들면 해당 줄을 노란색으로 잠깐 표시.
  • 무르기/무승부 요청은 상대 화면에 큰 모달(수락/거절 버튼 각 48px 이상).
  • 규칙 보기: 선택된 규칙 모드의 요약 + 예시 그림(5.3의 예 A~D).
  • PC: 판 좌측, 기보·채팅 우측.
  • 관전자: 판 위에 "관전 중" 배지, 기보 되돌려 보기(관전자 로컬에서만).

10. 테스트 체크리스트

  • 가로/세로/↘/↗ 4방향 각각 5목 완성 시 승리, 판 가장자리에 닿는 5목도 승리.
  • korean: 흑·백 모두 6목 완성 시 승리 아님, 게임 계속. 6목을 만드는 수 자체는 합법.
  • renju: 백 6목은 승리, 흑이 6목을 만드는 수는 금수로 거부.
  • free + fivePlus: 6목 승리. free + exactFive: 6목 승리 아님.
  • 5.3 예 A: H8이 korean·renju 모두 금수, free는 합법.
  • 5.3 예 B: G8이 renju 흑 금수(한 줄 4-4), korean 합법.
  • 5.3 예 C·D·E: 모두 renju 흑 합법(가짜 3-3). 예 E에서 재귀 없이 판정하면 금수로 잘못 나오는지 대조 테스트로 확인.
  • 오목 우선: 흑이 5목과 4-4를 동시에 만드는 수 → 합법 + 흑 승.
  • korean에서 백의 3-3도 금수로 거부되는지.
  • firstMoveCenter: 첫 수가 천원이 아니면 거부, 두 번째 수부터는 자유.
  • 판 가득 참 → 무승부. 19줄에서도 동작.
  • 무르기: 내 수 직후 요청 → 1수 무름, 상대 응수 후 요청 → 2수 무름, 상대 거절 시 변화 없음, 요청 중 상대가 착수하면 요청 자동 소멸, 횟수 소진 시 거부.
  • 무승부 제안: 거절 후 3수 이내 재제안 거부, 수락 시 무승부 결과.
  • perMove + autoMove: 시간 초과 시 자동 착수가 합법 수이고 금수가 아니며, 같은 시드에서 항상 같은 자리. 3연속 자동 착수 시 시간패.
  • fischer: 남은 시간 0 이후 도착한 착수는 거부되고 시간패, 정상 착수 시 증가분 가산.
  • 연결 끊김: 끊긴 플레이어 차례에 유예 후 onTimeout 동작, 재접속 시 유예 타이머 해제.
  • 정보 누출: 플레이어/관전자 view JSON에 rng가 없는지, 관전자 view에 hintThreats가 없는지 스냅샷 테스트.
  • 리플레이: 같은 시드 + 같은 액션 로그로 재실행 시 최종 상태 해시 동일.

11. 참고 자료

12. 메모 (상표·법적 주의 등)

  • "오목", "Gomoku", "Renju"는 전통 게임 명칭으로 상표 문제 없음. "렌주"라는 명칭 사용도 무방하나 RIF 공식 경기가 아님을 오해하지 않도록 "렌주룰(연습용)"처럼 표기 권장.
  • 금전·포인트 베팅 요소 없음.
  • 개국 규칙(26주형 등)은 공식 대회 재현이 목적일 때만 필요하므로 v1 범위에서 제외했다.