Files
joke-app/docs/games/othello.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

156 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 리버시 (오셀로) (`othello`)
> 마일스톤: M2 · 인원: 최소 2 ~ 최대 2명 · 예상 시간: 약 10~15분 · 난이도: 쉬움
## 1. 개요
- 8×8 판에서 상대 돌을 내 돌 사이에 끼우면 뒤집어 내 색으로 만드는 2인 게임. 끝났을 때 자기 색 돌이 더 많으면 승리. 한국에서는 "오셀로"라는 이름으로 잘 알려져 있고 규칙 설명이 1분이면 끝나 어린이·어르신 모두에게 적합하다.
- 인원 근거: 원작이 흑·백 2인 게임. 최소 2, 최대 2.
- 규칙 기준: 세계오셀로연맹(WOF) 공식 규칙.
- 완전 정보 게임. view에서 제외할 것은 RNG 상태뿐.
- 표시 이름은 상표 문제로 "리버시"를 권장(12장).
## 2. 모드와 옵션
| 옵션 키 | 설명 | 선택지 | 기본값 |
|---|---|---|---|
| `colorAssignment` | 흑백 결정 | `random` / `hostBlack` / `hostWhite` / `alternate` | `random` |
| `emptyToWinner` | 종료 시 빈칸을 승자 점수에 더함(WOF 대회 방식, 승패에는 영향 없음 — 점수 표시만 바뀜) | `true` / `false` | `false` |
| `timeControl.kind` | 시간 방식 | `none` / `perMove` / `fischer` | `perMove` |
| `timeControl.perMoveSec` | 한 수당 제한 | 10 / 20 / 30 / 60 | 30 |
| `timeControl.baseSec` / `incSec` | 전체 시간 / 수당 추가 | 3+2, 5+3, 10+5 | 5+3 |
| `timeoutPolicy` | `perMove` 시간 초과 처리 | `autoMove`(무작위 합법 수) / `lose` | `autoMove` |
| `idleLimitSec` | `none` 무응답 한도 | 120 / 300 / 600 | 300 |
| `undo` | 무르기 허용 횟수(상대 동의) | `off` / `1` / `3` / `unlimited` | `3` |
| `drawOffer` | 무승부 제안 | `true` / `false` | `true` |
| `showLegalMoves` | 둘 수 있는 칸 표시 | `true` / `false` | `true` |
| `showFlipCount` | 둘 수 있는 칸에 뒤집히는 개수 숫자 표시(초보 도움말) | `true` / `false` | `false` |
| `disconnectGraceSec` | 연결 끊김 유예 | 30 / 60 / 120 | 60 |
## 3. 구성물
- 8×8 판(64칸). 돌은 칸 안에 놓는다.
- 한 면이 흑, 다른 면이 백인 원판 돌 64개(엔진에서는 개수 제한 없음 — 칸 수가 상한).
- 좌표: 열 a~h(왼→오), 행 1~8(위→아래, 오셀로 표준 표기). 내부 `(x, y)` 0~7, 표시 `'abcdefgh'[x] + (y+1)`.
## 4. 준비(셋업)
1. `colorAssignment`로 흑백 결정.
2. 가운데 4칸에 배치: d4 백, e4 흑, d5 흑, e5 백.
3. 흑이 먼저 둔다. 시작 시 흑의 합법 수는 d3, c4, f5, e6 네 곳.
## 5. 진행 규칙
- 합법 수: 빈칸 `p`에 내 돌을 놓았을 때, 8방향(가로·세로·대각) 중 적어도 한 방향에서 `p` 바로 옆부터 상대 돌이 1개 이상 연속되고 그 끝에 내 돌이 있는 경우.
- 착수 처리: 위 조건을 만족하는 모든 방향에 대해, 사이에 낀 상대 돌을 전부 내 색으로 뒤집는다. 뒤집힌 돌로 인해 새로 끼워지는 돌은 연쇄로 뒤집지 않는다(한 수에서 놓은 돌 기준의 직선만).
- 뒤집을 수 있는 칸이 있으면 반드시 그중 한 곳에 둬야 한다(자발적 패스 없음).
- 패스: 차례인 플레이어에게 합법 수가 하나도 없으면 자동으로 차례가 상대에게 넘어간다(서버가 처리, 플레이어 행동 불필요). 이벤트 `passed`를 남긴다.
- 종료: 양쪽 모두 합법 수가 없으면(판이 가득 찬 경우 포함) 즉시 종료.
- 예시: 시작 직후 흑이 d3에 두면 d4 백이 흑으로 뒤집힌다(d3–d4–d5 세로). 흑 4, 백 1.
## 6. 승패와 점수 계산
- 점수 = 종료 시 판 위 자기 색 돌 수.
- 많은 쪽 승리, 같으면 무승부.
- `emptyToWinner=true`(WOF 대회 방식): 빈칸이 남은 채 끝났으면 빈칸 수를 승자 점수에 더한다. 무승부면 빈칸을 반씩 나눈다(빈칸 수는 무승부 시 항상 짝수). 예: 흑 40, 백 20, 빈칸 4 → 표시 점수 44 : 20. 승패는 동일.
- 그 외 종료: 기권, 시간패, 무승부 합의.
- 기권·시간패로 끝나도 결과에 당시 돌 수를 함께 기록.
## 7. 지역 룰 / 하우스 룰 (옵션으로 켜고 끄기)
- 가운데 4칸 초기 배치를 직접 놓는 옛 방식(초기 4수를 번갈아 가운데 4칸에 두는 리버시 원형 규칙)은 지원하지 않는다. 표준 대각 배치 고정.
- 6×6, 10×10 판 변형은 v1 미지원(추후 `boardSize` 옵션 후보).
- 빈칸을 승자에게 주는 대회 점수 방식은 옵션.
## 8. 엔진 설계
### 8.1 상태(State)
```ts
type Color = 'black' | 'white';
type Cell = 0 | 1 | 2;
interface OthelloState {
options: OthelloOptions;
board: Cell[]; // 64, index = y*8 + x
players: Record<Color, PlayerId>;
turn: Color;
phase: 'playing' | 'finished';
moves: ({ kind: 'place'; color: Color; x: number; y: number; flipped: number[]; auto: boolean }
| { kind: 'pass'; color: Color })[]; // 자동 패스도 기록(무르기·리플레이용)
clock: { remainingMs: Record<Color, number>; turnStartedAt: number; autoMoveStreak: Record<Color, number> };
undo: { usedBy: Record<Color, number>; pending: { by: Color; plies: number } | null };
draw: { pendingBy: Color | null; lastOfferPly: Record<Color, number> };
result: OthelloResult | null;
rng: RngState;
}
```
### 8.2 액션
| type | payload | 누가 / 언제 | 검증 조건 |
|---|---|---|---|
| `place` | `{ x, y }` | 차례인 플레이어, `playing` | 판 안, 빈칸, 뒤집히는 돌이 1개 이상("여기에 두면 뒤집을 돌이 없어요") |
| `requestUndo` | `{}` | 플레이어 | 횟수 남음, 대기 없음, 본인 착수 존재. 무를 범위 = 기록의 끝에서부터 본인의 마지막 `place`까지(그 사이 자동 패스 포함) |
| `respondUndo` | `{ accept }` | 상대 | 수락 시 `moves`를 처음부터 재생해 판 복원, 차례 = 요청자. 시계 유지 |
| `offerDraw` / `respondDraw` / `cancelOffer` | | 플레이어 | 거절 후 본인 3수 이후 재제안 |
| `resign` | `{}` | 플레이어 | 언제든 |
| `timeout` | `{}` | 시스템 | 8.5 |
- `place` 처리 후: 상대에게 합법 수가 있으면 차례 넘김. 없고 나에게 있으면 상대 자동 패스 기록 후 다시 내 차례. 둘 다 없으면 종료.
- 시계: `fischer`는 착수 시 차감 후 증가분 가산. 자동 패스는 시간이 들지 않으며 다시 차례가 온 플레이어의 `turnStartedAt = now`.
- 이벤트: `placed`(뒤집힌 칸 목록 — 애니메이션용), `passed`, `gameEnded`.
### 8.3 공개/비공개 정보 (view)
- 공통 공개: 판, 차례, 돌 수, 기보, 마지막 수, 시계, `deadline`, 대기 요청, 결과.
- 차례인 플레이어에게만: `legalMoves: { x, y, flips: number }[]` (`showLegalMoves`/`showFlipCount` 옵션에 따라 클라이언트가 표시).
- 관전자: `legalMoves` 없음(해설 모드 필요 시 추후).
- 제외: `rng`.
### 8.4 랜덤 요소 (시드 RNG 사용 지점)
1. `colorAssignment=random`.
2. `timeoutPolicy=autoMove`의 자동 착수: 모서리(a1, h1, a8, h8)가 합법이면 그중 무작위, 아니면 전체 합법 수 중 균등 무작위. 모서리 우선은 시간 초과 당한 쪽이 지나치게 불리해지지 않게 하는 최소한의 배려.
### 8.5 타이머·시간 초과·연결 끊김 시 자동 행동
- `deadline(state)`: `perMove` → `turnStartedAt + perMoveSec*1000`, `fischer` → `turnStartedAt + remainingMs[turn]`, `none` → `turnStartedAt + idleLimitSec*1000`, 종료 → `null`.
- `onTimeout(state, player)` → `{ type: 'timeout' }`. `apply`:
- `perMove + autoMove`: 자동 착수(`auto: true`), streak +1, 3연속이면 시간패.
- 그 외: 시간패.
- 연결 끊김: 끊긴 플레이어 차례에 `min(deadline, 끊김 + disconnectGraceSec)`에 `onTimeout`.
### 8.6 종료 조건과 결과(GameResult)
```ts
interface OthelloResult {
ranking: { playerId: PlayerId; rank: 1 | 2 }[];
winner: Color | null;
reason: 'noMoves' | 'resign' | 'timeLoss' | 'drawAgreed';
discs: { black: number; white: number; empty: number };
displayScore: { black: number; white: number }; // emptyToWinner 반영
summaryKo: string; // "흑 승 40 : 24"
}
```
## 9. UI/UX
- 모바일 세로: 상단 상대(돌 수, 시계), 가운데 8×8 판(폰 360px에서 칸 약 44px — 칸 전체가 터치 영역, 48px에 근접하므로 한 번 탭 착수 허용), 하단 내 정보와 버튼.
- 둘 수 있는 칸은 작은 반투명 점. `showFlipCount`면 숫자.
- 착수 시 뒤집히는 돌을 놓은 돌에서 가까운 순서로 순차 회전 애니메이션(전체 400ms 이내).
- 자동 패스 시 "둘 곳이 없어서 차례가 넘어갔어요" 토스트(상대 화면에도).
- 돌 수 막대(흑/백 비율)를 상단에 상시 표시.
- 규칙 보기: 끼워서 뒤집기 3단계 그림, 패스 설명, 모서리 팁(선택).
- PC: 판 좌측, 기보·채팅 우측.
## 10. 테스트 체크리스트
- [ ] 초기 배치(d4 백, e4 흑, d5 흑, e5 백)와 흑의 첫 합법 수 4개(d3, c4, f5, e6).
- [ ] 8방향 각각 뒤집기, 한 수에 여러 방향 동시 뒤집기.
- [ ] 연쇄 뒤집기 없음(뒤집힌 돌이 만든 새 끼움은 뒤집지 않음).
- [ ] 뒤집을 돌이 없는 빈칸 착수 거부, 판 밖·이미 찬 칸 거부.
- [ ] 자동 패스: 상대 합법 수 없음 → 패스 기록 후 내 차례 유지.
- [ ] 양쪽 합법 수 없음 → 판이 다 차지 않아도 종료(예: 한 색이 전멸).
- [ ] 동점 무승부, `emptyToWinner` 점수 표시(승리·무승부 각각).
- [ ] 무르기: 자동 패스가 끼어 있는 경우에도 정확히 본인 마지막 착수 전으로 복원.
- [ ] `autoMove`: 모서리 우선, 같은 시드에서 같은 선택, 3연속 시간패.
- [ ] `fischer` 시간패, 증가분 가산.
- [ ] 연결 끊김 유예 후 처리, 재접속 시 해제.
- [ ] 정보 누출: 관전자·비차례 플레이어 view에 `legalMoves` 없음, 모든 view에 `rng` 없음.
- [ ] 리플레이 결정성.
## 11. 참고 자료
- World Othello Federation 공식 규칙: https://www.worldothello.org/about/about-othello/othello-rules/official-rules/english (초기 배치, 흑 선, 합법 수, 패스, 종료, 다수 승리)
- Wikipedia "Reversi": https://en.wikipedia.org/wiki/Reversi (오셀로/리버시 역사, 대회의 빈칸 승자 귀속 관행)
- 위키백과 「오셀로 (보드 게임)」: https://ko.wikipedia.org/wiki/%EC%98%A4%EC%85%80%EB%A1%9C_(%EB%B3%B4%EB%93%9C_%EA%B2%8C%EC%9E%84)
## 12. 메모 (상표·법적 주의 등)
- "Othello"/"오셀로"는 상표(현재 일본 MegaHouse 및 Othello Co. 계열이 보유)이므로 서비스 표시 이름은 일반 명칭 "리버시"를 권장한다. 규칙 설명에서 "오셀로와 같은 규칙" 정도의 비교 서술은 지명적 사용으로 보통 허용되나, 로고·트레이드 드레스(초록 판 + 특정 디자인) 모방은 피한다. 최종 표시 이름은 사용자 결정 사항.
- 게임 id `othello`는 내부 식별자일 뿐 UI에 노출하지 않는다(필요하면 `reversi`로 변경 가능).
- 금전·베팅 요소 없음.