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

212 lines
16 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.
# 벽 쌓기 길찾기 (쿼리도 방식) (`quoridor`)
> 마일스톤: M7 · 인원: 최소 2 ~ 최대 4명 (2인 또는 4인만, 3인 불가) · 예상 시간: 약 10~20분 · 난이도: 쉬움~보통
## 1. 개요
- 9×9 판에서 자기 말을 반대편 끝줄까지 먼저 보내면 이기는 게임. 매 차례 말을 움직이거나 벽을 세워 상대의 길을 돌아가게 만든다. 단, 누구의 길도 완전히 막아서는 안 된다. 규칙이 직관적이라 초등학생 보드게임 수업과 가족 게임으로 국내에서도 인기가 있다.
- 인원 근거: 원작(Gigamic, 1997, Mirko Marchesi 디자인)은 2인 또는 4인용. `minPlayers = 2`, `maxPlayers = 4`이며, setup에서 플레이어 수가 3이면 거부한다("2명 또는 4명이 필요해요").
- 완전 정보 게임. view에서 제외할 것은 RNG 상태뿐.
- 원작명은 상표이므로 표시 이름은 일반 명칭을 권장(12장).
## 2. 모드와 옵션
| 옵션 키 | 설명 | 선택지 | 기본값 |
|---|---|---|---|
| `wallsPerPlayer` | 1인당 벽 수 | 2인: 10, 4인: 5 고정 (원작). 하우스 룰로 2인 6~10 선택 가능 | 2인 10 / 4인 5 |
| `diagonalJumpTrigger` | 대각선 점프가 허용되는 조건 | `wallEdgePawn`(뒤가 벽·판 끝·다른 말이면 대각 허용) / `wallOnly`(벽일 때만, Gigamic 2인 규칙서 문구 그대로) | `wallEdgePawn` |
| `seatOrder` | 시작 위치·순서 | `random` / `joinOrder` | `random` |
| `continueForRanking` | 4인에서 1등이 나온 뒤에도 계속해서 2·3등을 가릴지 | `true` / `false` | `false` |
| `timeControl.kind` | 시간 방식 | `none` / `perMove` / `fischer` | `perMove` |
| `timeControl.perMoveSec` | 한 수당 제한 | 15 / 30 / 60 | 30 |
| `timeControl.baseSec` / `incSec` | 전체 / 수당 추가 | 3+2, 5+3 | 5+3 |
| `timeoutPolicy` | `perMove` 시간 초과 처리 | `autoMove`(최단 경로로 말 1칸 이동) / `lose` | `autoMove` |
| `idleLimitSec` | `none` 무응답 한도 | 120 / 300 | 300 |
| `undo` | 무르기 허용 횟수(다른 모든 플레이어 동의) | `off` / `1` / `3` / `unlimited` | `3` |
| `drawOffer` | 무승부 제안(남은 모든 플레이어 동의) | `true` / `false` | `true` |
| `showPath` | 내 최단 경로 표시(힌트) | `true` / `false` | `true` |
| `disconnectGraceSec` | 연결 끊김 유예 | 30 / 60 / 120 | 60 |
## 3. 구성물
- 9×9 칸(81칸) 판. 칸과 칸 사이에 홈(groove)이 있어 벽을 끼운다.
- 말 2개 또는 4개(색 구분).
- 벽 20개. 각 벽은 칸 2개 길이.
- 좌표: 열 a~i(왼→오), 행 1~9(아래→위). 내부 칸 `(c, r)` 0~8, `r=0`이 1행.
- 벽 앵커: 벽의 가운데는 항상 홈의 교차점(8×8 = 64곳)에 온다. 앵커 `(c, r)` (0~7)은 칸 `(c,r) (c+1,r) (c,r+1) (c+1,r+1)` 네 칸의 가운데 교차점.
- 가로벽 `H(c,r)`: `(c,r)↔(c,r+1)`, `(c+1,r)↔(c+1,r+1)` 이동을 막는다.
- 세로벽 `V(c,r)`: `(c,r)↔(c+1,r)`, `(c,r+1)↔(c+1,r+1)` 이동을 막는다.
## 4. 준비(셋업)
1. 플레이어 수 확인(2 또는 4). `seatOrder=random`이면 RNG로 자리 배정.
2. 시작 위치와 목표:
| 자리 | 시작 칸 | 목표 | 차례 순서 |
|---|---|---|---|
| 남(S) | e1 `(4,0)` | 9행 전체 (`r=8`) | 1 |
| 서(W, 4인) | a5 `(0,4)` | i열 전체 (`c=8`) | 2 |
| 북(N) | e9 `(4,8)` | 1행 전체 (`r=0`) | 2인: 2 / 4인: 3 |
| 동(E, 4인) | i5 `(8,4)` | a열 전체 (`c=0`) | 4 |
(4인 순서는 판을 위에서 볼 때 시계 방향: 남 → 서 → 북 → 동.)
3. 벽 배분: 2인 10개씩, 4인 5개씩.
4. 남이 먼저 둔다.
## 5. 진행 규칙
### 5.1 턴
- 차례인 플레이어는 다음 중 하나만 한다: (A) 말 이동, (B) 남은 벽이 있으면 벽 1개 설치. 패스 없음(단 5.4 예외).
### 5.2 말 이동
- 기본: 상하좌우 인접 칸으로 1칸. 사이에 벽이 없고, 판 안이며, 다른 말이 없어야 한다. 대각선 1칸 이동은 기본 이동이 아니다.
- 직선 점프: 인접 칸(방향 d)에 다른 말 Q가 있고 나와 Q 사이에 벽이 없을 때, Q 너머 칸(같은 방향 d)이 판 안이고 Q와 그 칸 사이에 벽이 없고 비어 있으면 그 칸으로 뛰어넘는다.
- 대각 점프: 위 직선 점프가 불가능한 경우(`wallEdgePawn`: Q 뒤가 벽·판 끝·다른 말 / `wallOnly`: Q 뒤가 벽일 때만), Q의 좌우(방향 d에 수직인 두 방향) 인접 칸 중 Q와 그 칸 사이에 벽이 없고 판 안이며 비어 있는 칸으로 이동할 수 있다.
- 한 번에 말 1개만 뛰어넘는다(4인에서 말 2개가 일렬이어도 2개를 넘는 점프 불가).
- 직선 점프가 가능하면 대각 점프는 불가.
### 5.3 벽 설치
- 합법 조건(모두 만족):
1. 남은 벽이 1개 이상.
2. 앵커 `(c, r)`가 0~7 범위(벽은 판 밖으로 나갈 수 없음, 항상 정확히 2칸을 막음).
3. 기존 벽과 겹치지 않음: `H(c,r)`는 `H(c-1,r)`, `H(c,r)`, `H(c+1,r)`, `V(c,r)`와 충돌. `V(c,r)`는 `V(c,r-1)`, `V(c,r)`, `V(c,r+1)`, `H(c,r)`와 충돌(같은 교차점 십자 교차 금지).
4. 설치 후에도 게임에 남아 있는 모든 플레이어(설치한 본인 포함)가 각자 목표 줄까지 갈 수 있는 경로가 하나 이상 존재. 경로 탐색 시 말은 장애물로 보지 않고 벽만 본다.
- 경로 검사(BFS):
```ts
function hasPath(walls: WallSet, start: Cell, goal: (c: Cell) => boolean): boolean {
const seen = new Set<number>([idx(start)]);
const queue: Cell[] = [start];
while (queue.length) {
const cur = queue.shift()!;
if (goal(cur)) return true;
for (const d of [N, S, E, W]) {
const nxt = step(cur, d);
if (!onBoard(nxt) || blocked(walls, cur, d) || seen.has(idx(nxt))) continue;
seen.add(idx(nxt)); queue.push(nxt);
}
}
return false;
}
// 벽 후보 합법성 = 조건 1~3 && 모든 활성 플레이어 p에 대해 hasPath(walls ∪ {후보}, pos(p), goal(p))
```
- 설치한 벽은 이동·제거할 수 없다.
### 5.4 움직일 수 없는 경우
- 말 이동이 하나도 불가능하고(다른 말과 벽에 둘러싸임) 벽도 없으면 자동 패스(이벤트 `forcedPass`). 원작 규칙서에 명시되지 않은 극히 드문 상황에 대한 서비스 규칙.
## 6. 승패와 점수 계산
- 자기 목표 줄의 아무 칸에 말이 도착하는 즉시 승리(1등). 기본(`continueForRanking=false`)은 그 즉시 게임 종료.
- 4인 결과 순위: 1등 = 도착자. 나머지는 종료 시점의 최단 경로 길이(BFS 거리, 말 무시)가 짧은 순으로 2~4등, 같으면 공동 순위. 기권·실격자는 최하위(기권 순서가 늦을수록 높은 순위).
- `continueForRanking=true`(4인): 도착한 말은 판에서 제거하고(그 칸은 비게 됨, 벽은 남음) 남은 플레이어로 계속, 마지막 1명이 남으면 종료. 도착 순서대로 1~3등, 남은 1명 4등.
- 2인: 도착자 승리, 상대 패배.
- 기권: 2인은 상대 승. 4인은 기권자의 말을 판에서 제거, 남은 벽은 소멸, 설치한 벽은 유지. 남은 인원이 1명이면 그 사람 1등.
- 무승부: 남은 모든 플레이어 동의 시. 무승부면 남은 플레이어 모두 공동 1등.
- 점수 개념 없음(결과에 각자 남은 거리·남은 벽 수 기록).
## 7. 지역 룰 / 하우스 룰 (옵션으로 켜고 끄기)
- 대각 점프 조건: 원작 2인 규칙서는 "점프할 말 뒤에 벽이 있으면 좌우로" 라고만 적혀 있어 판 끝·다른 말의 경우 해석이 갈린다. 대부분의 온라인 구현과 영문 위키는 판 끝·다른 말도 포함하므로 기본값은 `wallEdgePawn`.
- 2인 벽 개수 줄이기(빠른 게임): 하우스 룰.
- 3인 플레이는 원작에 없으므로 미지원.
## 8. 엔진 설계
### 8.1 상태(State)
```ts
type Seat = 'S' | 'W' | 'N' | 'E';
type Cell = { c: number; r: number };
type Wall = { c: number; r: number; o: 'h' | 'v' };
interface QuoridorState {
options: QuoridorOptions;
seats: { seat: Seat; playerId: PlayerId; pos: Cell | null; // null = 제거(기권/도착 후 제외)
wallsLeft: number; status: 'playing' | 'finished' | 'resigned' | 'timedOut';
finishOrder: number | null }[];
turnIndex: number; // seats 배열 인덱스
walls: Wall[];
wallIndex: { h: boolean[]; v: boolean[] }; // 8x8 앵커별 점유 (빠른 충돌 검사)
moves: ({ kind: 'move'; seat: Seat; from: Cell; to: Cell; auto: boolean }
| { kind: 'wall'; seat: Seat; wall: Wall }
| { kind: 'pass'; seat: Seat })[];
phase: 'playing' | 'finished';
clock: { remainingMs: Record<Seat, number>; turnStartedAt: number; autoMoveStreak: Record<Seat, number> };
undo: { usedBy: Record<Seat, number>; pending: { by: Seat; accepted: Seat[] } | null };
draw: { pendingBy: Seat | null; accepted: Seat[]; lastOfferMove: Record<Seat, number> };
result: QuoridorResult | null;
rng: RngState;
}
```
### 8.2 액션
| type | payload | 누가 / 언제 | 검증 조건 |
|---|---|---|---|
| `move` | `{ to: Cell }` | 차례인 플레이어 | `to`가 5.2 규칙으로 계산한 이동 가능 칸 목록에 포함. 사유 예: "벽이 가로막고 있어요", "대각선으로는 상대 말을 넘을 때만 갈 수 있어요" |
| `placeWall` | `{ c, r, o }` | 차례인 플레이어 | 5.3 조건 1~4. 사유 예: "남은 벽이 없어요", "다른 벽과 겹쳐요", "누군가의 길을 완전히 막을 수 없어요" |
| `requestUndo` | `{}` | 직전 행동을 한 플레이어 | 횟수 남음, 대기 없음. 2인: 본인 직전 행동 1개(상대가 이미 행동했으면 2개). 4인: 본인이 방금 행동했고 다음 사람이 아직 행동하지 않았을 때만, 1개 |
| `respondUndo` | `{ accept }` | 요청자 외 활성 플레이어 전원 | 1명이라도 거절 → 취소. 전원 수락 → `moves` 재생으로 복원(벽 개수 포함), 시계 유지 |
| `offerDraw` / `respondDraw` / `cancelOffer` | | 활성 플레이어 | 전원 수락 시 무승부. 다음 사람이 행동하면 자동 취소 |
| `resign` | `{}` | 활성 플레이어 | 6장 처리 |
| `timeout` | `{}` | 시스템 | 8.5 |
- 이동 가능 칸 계산 `legalPawnMoves(state, seat)`: 4방향에 대해 5.2 규칙을 적용해 중복 없는 칸 목록 반환.
- 행동 후: 도착 판정 → 종료 또는 순위 기록 → 다음 활성 플레이어로 차례 이동(제거된 자리는 건너뜀) → 그 플레이어가 이동·벽 모두 불가하면 자동 패스.
- 이벤트: `pawnMoved`, `wallPlaced`, `forcedPass`, `playerFinished`, `playerResigned`, `gameEnded`.
### 8.3 공개/비공개 정보 (view)
- 공통 공개: 말 위치, 모든 벽, 각자 남은 벽 수, 차례, 기보, 시계, `deadline`, 대기 요청(누가 수락했는지 포함), 각 플레이어 최단 거리(관전 재미 요소, 공개 정보에서 계산 가능하므로 숨길 이유 없음), 결과.
- 차례인 플레이어에게만: `legalPawnMoves`, `legalWalls`(최대 128개 후보 중 합법 앵커 목록 — 클라이언트가 BFS를 구현하지 않아도 미리보기 가능), `showPath`면 `myShortestPath`.
- 제외: `rng`.
### 8.4 랜덤 요소 (시드 RNG 사용 지점)
1. `seatOrder=random`의 자리 배정.
2. `autoMove`에서 최단 경로의 첫 칸 후보가 여럿일 때 선택.
### 8.5 타이머·시간 초과·연결 끊김 시 자동 행동
- `deadline(state)`: `perMove` → `turnStartedAt + perMoveSec*1000`, `fischer` → `turnStartedAt + remainingMs[seat]`, `none` → `turnStartedAt + idleLimitSec*1000`, 종료 → `null`.
- `onTimeout(state, player)` → `{ type: 'timeout' }`. `apply`:
- `perMove + autoMove`: 이동 가능 칸 중 "목표까지 BFS 거리(말 무시)가 가장 짧아지는 칸"으로 이동(점프 포함). 동률은 RNG. 이동 불가면 자동 패스. streak 3회면 시간패.
- 그 외: 시간패. 2인은 상대 승. 4인은 기권과 같은 처리(말 제거, `timedOut`).
- 연결 끊김: 끊긴 플레이어 차례에 `min(deadline, 끊김 + disconnectGraceSec)`에 `onTimeout`. 4인에서 끊긴 플레이어가 무르기·무승부 응답자일 때는 "응답 없음 = 거절"로 30초 후 자동 취소(서버 타이머가 `respondUndo{accept:false}` 대신 요청 자체를 만료시키는 이벤트 처리).
### 8.6 종료 조건과 결과(GameResult)
```ts
interface QuoridorResult {
ranking: { playerId: PlayerId; rank: number }[]; // 공동 순위 허용
reason: 'reachedGoal' | 'lastPlayerStanding' | 'resign' | 'timeLoss' | 'drawAgreed';
perPlayer: { playerId: PlayerId; seat: Seat; distanceLeft: number; wallsLeft: number;
status: 'finished' | 'playing' | 'resigned' | 'timedOut' }[];
moveCount: number;
summaryKo: string; // "남(철수) 승리 — 23수 만에 도착"
}
```
## 9. UI/UX
- 모바일 세로: 정사각 판이 화면 폭 100%(폰 360px에서 칸 약 36px + 홈). 상단에 상대(들) 정보와 남은 벽(막대 아이콘), 하단에 내 정보와 모드 전환 큰 버튼 2개 "말 움직이기" / "벽 세우기"(각 48px 이상).
- 말 이동 모드: 이동 가능 칸이 밝게 표시, 탭으로 이동.
- 벽 세우기 모드: 홈의 교차점 근처를 탭하면 벽 미리보기(반투명), 가로/세로 전환 버튼(또는 같은 곳 다시 탭으로 회전), 놓을 수 없으면 빨간 미리보기와 이유 표시, "확인" 버튼으로 설치. 작은 화면에서 홈을 직접 터치하기 어렵기 때문에 "교차점 가장 가까운 곳 스냅 + 확인" 2단계 필수.
- 힌트: 내 최단 경로를 점선으로, 벽 미리보기 중에는 "이 벽을 놓으면 상대 길이 +4칸" 같은 변화량 표시(초보에게 벽의 효과를 알려줌, 옵션).
- 4인: 각 말 색과 목표 줄을 판 가장자리 색 띠로 표시.
- 규칙 보기: 점프·대각 점프 그림, "길을 완전히 막으면 안 돼요" 그림.
## 10. 테스트 체크리스트
- [ ] 2인/4인 시작 위치·목표·차례 순서, 3인 시작 거부.
- [ ] 기본 이동: 벽·판 끝·다른 말 때문에 막힘.
- [ ] 직선 점프 성공, 점프 뒤 칸이 벽/판 끝/말일 때 대각 점프 2곳, 그중 한쪽이 벽으로 막히면 1곳만.
- [ ] `wallOnly`에서 판 끝 뒤 대각 점프 불가.
- [ ] 직선 점프 가능할 때 대각 점프 거부, 말 2개 연속 점프 거부.
- [ ] 벽 겹침: 같은 방향 반 칸 겹침, 같은 앵커 십자 교차 거부. 서로 다른 앵커의 T자 접촉은 허용.
- [ ] 판 밖 앵커(0~7 범위 밖) 거부, 남은 벽 0개일 때 거부.
- [ ] 경로 차단: 상대 길을 완전히 막는 벽 거부, 자기 길을 막는 벽 거부, 4인에서 제3자의 길을 막는 벽 거부, 말 때문에만 막히는 경우는 허용.
- [ ] 목표 줄 도착 즉시 승리(점프로 도착 포함).
- [ ] 4인 기권: 말 제거, 벽 유지, 이후 경로 검사에서 제외, 1명 남으면 종료.
- [ ] `continueForRanking`: 도착자 제거 후 계속, 순위 기록.
- [ ] 4인 결과 순위가 남은 거리로 정렬되고 동률 공동 순위.
- [ ] `autoMove`: 최단 거리 감소 칸으로 이동, 같은 시드 동일 결과, 3연속 시간패.
- [ ] 4인 무르기: 다른 3명 전원 수락 시에만, 다음 사람이 행동한 뒤에는 요청 불가.
- [ ] 자동 패스: 말·벽 모두 불가한 인위적 국면.
- [ ] 정보 누출: 관전자·비차례 view에 `legalWalls`/`myShortestPath` 없음, `rng` 없음.
- [ ] 성능: 벽 후보 128개 × BFS 4회 계산이 1ms 수준인지(합법 벽 목록 생성).
## 11. 참고 자료
- Wikipedia "Quoridor": https://en.wikipedia.org/wiki/Quoridor (9×9, 2인 10개·4인 5개 벽, 점프와 대각 점프 조건: 판 끝·세 번째 말·벽, 길 차단 금지, 4인 시작 위치 e1·a5·e9·i5)
- Gigamic 공식 규칙서(Quoridor rules PDF, gigamic.com 제품 페이지): 2인 규칙의 대각 점프 문구("벽이 있으면 좌우로"), 벽은 2칸 길이·홈에 설치
- BoardGameGeek "Quoridor": https://boardgamegeek.com/boardgame/624/quoridor (출판사·인원·플레이 시간)
## 12. 메모 (상표·법적 주의 등)
- "Quoridor"(쿼리도/쿼리더)는 Gigamic의 등록 상표다. 게임 규칙 자체는 저작권 보호 대상이 아니지만 이름·로고·판 디자인은 사용하지 말 것. 서비스 표시 이름 후보: "벽 쌓기 길찾기", "길막기", "미로 탈출 보드". 최종 이름은 사용자 결정 사항. 규칙 보기에서 "쿼리도와 비슷한 규칙" 같은 비교 언급 여부도 사용자와 확인.
- 게임 id `quoridor`는 내부용이며 UI에 노출하지 않는다(필요하면 `wall-maze` 등으로 변경).
- 금전·베팅 요소 없음.