# 벽 쌓기 길찾기 (쿼리도 방식) (`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([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; turnStartedAt: number; autoMoveStreak: Record }; undo: { usedBy: Record; pending: { by: Seat; accepted: Seat[] } | null }; draw: { pendingBy: Seat | null; accepted: Seat[]; lastOfferMove: Record }; 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` 등으로 변경). - 금전·베팅 요소 없음.