Files
joke-app/docs/games/othello.md
EJClaw 19c163f4d2 시간 제한 없음은 정말 무제한, 대기실 설정은 바꾸면 바로 적용, 모르는 사람 기다리기(빠른 시작·공개 방) 제거
- 오목에서 [시간 제한 없음]을 골라도 30초가 흐르던 문제: 대기실 설정이 [설정 저장]을 눌러야
  서버에 반영되어, 누르지 않고 [게임 시작]하면 기본값(한 수 30초)으로 시작했다. 이제 설정은
  바꾸는 즉시 서버에 보낸다(저장 버튼 없음, 휴대폰 시트는 [닫기]만). 빠르게 여러 번 눌러도
  먼저 온 방 정보가 새 값을 되돌리지 않게 lib/option-sync.ts로 맞춘다.
- 시간 제한 없음(오목·체스·장기·오델로·바둑·쿼리도): 무응답 한도(idleLimitSec, 5~10분 뒤 시간패)와
  그 카운트다운을 없앴다. 체스는 첫 수 제한도 피셔일 때만. 연결이 끊긴 사람의 접속 유예는 그대로.
- 홈의 [지금 열린 방] 탭, 게임 소개의 [아무나와 빠른 시작]·기다리는 방 목록, 대기실의 방 공개
  설정을 뺐다(방은 모두 친구만). 서버 API는 남겨 두고 화면에서만 부르지 않는다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 20:42:46 +09:00

155 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` |
| `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` → `null`(마감 없음, 무응답 시간패 없음), 종료 → `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`로 변경 가능).
- 금전·베팅 요소 없음.