시간 제한 없음은 정말 무제한, 대기실 설정은 바꾸면 바로 적용, 모르는 사람 기다리기(빠른 시작·공개 방) 제거
- 오목에서 [시간 제한 없음]을 골라도 30초가 흐르던 문제: 대기실 설정이 [설정 저장]을 눌러야 서버에 반영되어, 누르지 않고 [게임 시작]하면 기본값(한 수 30초)으로 시작했다. 이제 설정은 바꾸는 즉시 서버에 보낸다(저장 버튼 없음, 휴대폰 시트는 [닫기]만). 빠르게 여러 번 눌러도 먼저 온 방 정보가 새 값을 되돌리지 않게 lib/option-sync.ts로 맞춘다. - 시간 제한 없음(오목·체스·장기·오델로·바둑·쿼리도): 무응답 한도(idleLimitSec, 5~10분 뒤 시간패)와 그 카운트다운을 없앴다. 체스는 첫 수 제한도 피셔일 때만. 연결이 끊긴 사람의 접속 유예는 그대로. - 홈의 [지금 열린 방] 탭, 게임 소개의 [아무나와 빠른 시작]·기다리는 방 목록, 대기실의 방 공개 설정을 뺐다(방은 모두 친구만). 서버 API는 남겨 두고 화면에서만 부르지 않는다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,7 @@
|
||||
- 게임: 사용자가 지정한 포커(여러 모드), 마작, 오목, 바둑, 체스, 뱅, 섯다(여러 모드) + 한국에서 인기 있는 보드게임. 전체 목록과 순서는 `games/README.md`.
|
||||
- 인원: 각 게임의 원래 최소/최대 인원을 그대로 따른다.
|
||||
- 계정: 디스코드 로그인 또는 닉네임만 정하는 게스트. 디스코드 로그인 사용자도 닉네임을 바꿀 수 있다.
|
||||
- 방: 6자리 숫자 코드, 초대 링크, 코드/링크 복사, QR, 모바일 공유 시트, 빠른 시작, 관전, 재접속.
|
||||
- 방: 6자리 숫자 코드, 초대 링크, 코드/링크 복사, QR, 모바일 공유 시트, 관전, 재접속.
|
||||
- 기기: **PC 우선**(사용자 결정, 2026-10-04). 태블릿·휴대폰도 같은 화면을 접어서 지원. 최신 Chrome/Edge/Safari/Firefox/Samsung Internet.
|
||||
|
||||
## 5. 범위 밖 (Non-goals)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
| 주소 | 화면 |
|
||||
|---|---|
|
||||
| `/` | 홈: 위쪽에 큰 버튼 두 개 **[방 만들기] [코드로 참여]**, 그 아래 "진행 중인 게임으로 돌아가기"(있을 때만), 그 아래 게임 카드 목록 |
|
||||
| `/g/:gameId` | 게임 소개: 한 줄 설명, 인원, 시간, 난이도, **[방 만들기] [빠른 시작] [규칙 보기]**, 열린 공개 방 목록 |
|
||||
| `/g/:gameId` | 게임 소개: 한 줄 설명, 인원, 시간, 난이도, **[방 만들기] [컴퓨터와 연습하기] [규칙 보기]** |
|
||||
| `/r/:code` | 방(대기실 ↔ 게임 화면 ↔ 결과 화면이 같은 주소에서 전환) |
|
||||
| `/j` | 코드 입력 전용 화면(숫자 키패드) |
|
||||
| `/me` | 계정 |
|
||||
@@ -26,7 +26,8 @@
|
||||
## 4. 방 만들기
|
||||
1. 게임 선택(홈 카드 또는 홈의 [방 만들기] → 게임 고르기 화면).
|
||||
2. 바로 대기실 생성. 모든 옵션은 **기본값으로 이미 정해져 있다**(인원은 게임 기본 인원, 공개 여부는 "친구만(비공개)").
|
||||
3. 대기실에서 방장이 바꿀 수 있는 것: 게임 모드/옵션(시간 제한 포함 — 게임마다 다름), 최대 인원(게임의 최소~최대 범위 안), 공개/비공개, 관전 허용, 채팅 허용.
|
||||
3. 대기실에서 방장이 바꿀 수 있는 것: 게임 모드/옵션(시간 제한 포함 — 게임마다 다름), 최대 인원(게임의 최소~최대 범위 안), 관전 허용, 채팅 허용. 바꾸는 즉시 서버에 저장된다(저장 버튼 없음). 그래서 [게임 시작]은 언제나 화면에 보이는 설정으로 시작한다.
|
||||
- 방은 모두 친구만(비공개)이다. 아는 사람끼리만 하므로 공개 설정은 화면에 두지 않는다.
|
||||
4. 옵션은 "기본 설정" 몇 개만 보이고 나머지는 "자세한 설정" 접기 안에 둔다.
|
||||
|
||||
## 5. 대기실 화면 (PC는 두 칸 배치 — `07-ui-ux.md` 3.4. 아래는 내용 순서이며 좁은 화면에서는 이 순서로 한 칸에 쌓인다)
|
||||
@@ -51,9 +52,8 @@
|
||||
3. 입장 → 빈자리가 있으면 **자동으로 자리에 앉는다**. 없으면 관전자로 들어가고 "자리가 나면 앉을 수 있어요".
|
||||
4. 방이 없거나 끝났으면: "이 방은 끝났어요" + [같은 게임 새 방 만들기] [홈으로].
|
||||
|
||||
## 7. 빠른 시작
|
||||
- 게임 상세의 [빠른 시작]: 같은 게임의 공개 대기실 중 사람이 가장 많은(가득 차지 않은) 방에 들어간다. 없으면 공개 방을 새로 만들고 기다린다.
|
||||
- 기다리는 동안 "다른 사람을 기다리는 중… (공개 방)" + [친구 초대하기] 버튼.
|
||||
## 7. 빠른 시작 (화면에서 뺌)
|
||||
- 아는 사람끼리만 하므로 [빠른 시작](모르는 사람 기다리기)은 화면에서 뺐다. 서버의 `POST /api/quick/:gameId`는 남아 있지만 화면에서 부르지 않는다.
|
||||
|
||||
## 8. 게임 중
|
||||
- 위쪽 상태 줄: 지금 할 일 문장("내 차례예요" / "OO님 차례예요"), 남은 시간 링.
|
||||
@@ -68,9 +68,8 @@
|
||||
- [한 판 더]는 투표: 자리에 앉은 사람 모두 누르면 같은 설정으로 즉시 새 게임(선/딜러는 규칙에 따라 교대). 안 누르는 사람이 있으면 그 사람이 [관전하기]로 일어나거나 방장이 [게임 시작]으로 바로 시작할 수 있다.
|
||||
- 기록: 끝난 게임은 `game_results`에 저장, 방 안에서 "이번 방 전적"(예: OO 3승 2패) 표시.
|
||||
|
||||
## 10. 공개 방 목록
|
||||
- 게임 상세 화면에서만 보인다(홈을 복잡하게 하지 않음). 정렬: 곧 시작할 방(빈자리 1개) 우선.
|
||||
- 각 줄: 방 이름(기본 "OO님의 방"), 인원, 모드, [참여].
|
||||
## 10. 공개 방 목록 (화면에서 뺌)
|
||||
- 홈의 [지금 열린 방] 탭과 게임 상세의 "기다리는 방" 목록은 뺐다. 서버의 `GET /api/public-rooms`, `GET /api/games/:gameId/rooms`는 남아 있지만 화면에서 부르지 않는다.
|
||||
|
||||
## 11. 서버 방 모델
|
||||
```ts
|
||||
|
||||
@@ -66,8 +66,7 @@
|
||||
│ [▶ 하던 게임으로 돌아가기] (있을 때만) │
|
||||
│ [+ 방 만들기 (파란 큰 버튼)] [🔢 코드로 참여 → /j] [🤖 컴퓨터와 연습] │
|
||||
├──────────────────────────────────────────────────────────────────────────┤
|
||||
│ [🎮 게임 고르기] [🚪 지금 열린 방 (n)] ← 탭, 한 번에 하나만 보임 │
|
||||
│ 게임 카드 격자 (4~5열, 카드에 인원·시간·난이도) 또는 공개 방 목록(게임·인원·참여) │
|
||||
│ 게임 카드 격자 (4~5열, 카드에 인원·시간·난이도) │
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
- 첫 화면에 다 넣지 않는다. 설명 글은 버튼 아래 한 줄만, 나머지는 탭이나 버튼을 눌러 들어간다.
|
||||
@@ -157,7 +156,7 @@
|
||||
- 홈(로그인 전/후, 진행 중 게임 있음)
|
||||
- 닉네임 입력(게스트 시작 / 링크 입장)
|
||||
- 코드 입력
|
||||
- 게임 상세 + 공개 방 목록
|
||||
- 게임 상세
|
||||
- 대기실(방장/참가자/관전자, 인원 부족/가득 참)
|
||||
- 게임 화면(내 차례/남의 차례/연결 끊김/자리 비움/일시 정지)
|
||||
- 결과 화면(승/패/무승부, 한 판 더 투표 중, 처음부터 다시 보기)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
| [03-realtime-protocol.md](03-realtime-protocol.md) | WebSocket 메시지, 순서 보장, 재접속, 타이머, 제한 |
|
||||
| [04-stability-security.md](04-stability-security.md) | 저장·복구, 장애 격리, 숨김 정보·공정성, 사행성·상표, 웹 보안 |
|
||||
| [05-accounts-auth.md](05-accounts-auth.md) | 게스트·디스코드 로그인, 닉네임, 세션, 계정 페이지 |
|
||||
| [06-lobby-rooms.md](06-lobby-rooms.md) | 방 만들기/참여, 코드·링크·QR·공유, 대기실, 빠른 시작, 한 판 더 |
|
||||
| [06-lobby-rooms.md](06-lobby-rooms.md) | 방 만들기/참여, 코드·링크·QR·공유, 대기실, 한 판 더 |
|
||||
| [07-ui-ux.md](07-ui-ux.md) | 디자인 원칙, 색·글자 토큰, 화면 틀, 컴포넌트, 접근성, 성능 예산 |
|
||||
| [08-data-model.md](08-data-model.md) | SQLite 스키마, 쓰기 시점, 정리, 개인정보 |
|
||||
| [09-game-engine.md](09-game-engine.md) | 게임 모듈 인터페이스, 결정적 난수, GameRunner, 공통 유틸, 공통 테스트 |
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
| `timeControl.periodSec` | 초읽기 1회 시간 | 10 / 20 / 30 / 60 | 30 |
|
||||
| `timeControl.periods` | 초읽기 횟수 | 1 ~ 5 | 3 |
|
||||
| `timeControl.incSec` | `fischer` 수당 추가 | 0 ~ 30 | 10 |
|
||||
| `idleLimitSec` | `none`일 때 무응답 한도 | 300 / 600 | 600 |
|
||||
| `undo` | 무르기 허용 횟수(1인당, 상대 동의) | `off` / `1` / `3` / `unlimited` | `3` |
|
||||
| `drawOffer` | 무승부(빅) 합의 제안 허용 | `true` / `false` | `true` |
|
||||
| `atariWarning` | **관리자 전용** 위협 가이드: 단수에 몰린 내 돌 표시(방장은 못 바꿈) | `true` / `false` | `false` |
|
||||
@@ -202,7 +201,7 @@ interface BadukState {
|
||||
- `deadline(state)`:
|
||||
- `playing` + `byoyomi`: `turnStartedAt + mainMs[turn] + periodsLeft[turn] * periodSec*1000` (현재 회차를 다 쓰면 다음 회차가 이어지므로 이 합이 최종 시간패 시각)
|
||||
- `playing` + `fischer`: `turnStartedAt + mainMs[turn]`
|
||||
- `playing` + `none`: `turnStartedAt + idleLimitSec*1000`
|
||||
- `playing` + `none`: `null` — 시간 제한 없음은 정말 마감이 없다(무응답 시간패 없음). 연결이 끊긴 사람만 방의 접속 유예(`graceSec`) 뒤에 처리한다.
|
||||
- `scoring`: `scoringMarks.startedAt + scoringDeadlineSec*1000`
|
||||
- `finished`: `null`
|
||||
- `activePlayers`: `playing`이면 차례인 1명. `scoring`이면 아직 `agreed`하지 않은 플레이어 전원(동시 행동 단계).
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
| `autoQueen` | 승진 시 퀸 자동 선택(설정 끄면 선택 창) | `true` / `false` | `false` (선택 창, 퀸이 첫 번째 큰 버튼) |
|
||||
| `undo` | 무르기 허용 횟수(1인당, 상대 동의) | `off` / `1` / `3` / `unlimited` | `1` |
|
||||
| `drawOffer` | 무승부 제안 | `true` / `false` | `true` |
|
||||
| `firstMoveSec` | 첫 수 제한(넘기면 대국 취소) | 15 / 30 / 60 | 30 |
|
||||
| `idleLimitSec` | `timeControl=none`일 때 무응답 한도 | 300 / 600 | 600 |
|
||||
| `firstMoveSec` | 첫 수 제한(넘기면 대국 취소, 피셔일 때만) | 15 / 30 / 60 | 30 |
|
||||
| `hints` | 초보 도움말(이동 가능 칸 표시) | `off` / `moves` | `moves` |
|
||||
| `threatHints` | **관리자 전용** 위협 가이드: 공격받고 방어 안 된 내 기물 표시(방장은 못 바꿈) | `true` / `false` | `false` |
|
||||
| `disconnectGraceSec` | 연결 끊김 유예 | 30 / 60 / 120 | 60 |
|
||||
@@ -154,9 +153,9 @@ interface ChessState {
|
||||
### 8.5 타이머·시간 초과·연결 끊김 시 자동 행동
|
||||
- 시계 시작: 각 색의 첫 수는 `firstMoveSec` 제한만 있고 본 시계를 차감하지 않는다(lichess 방식). 백이 첫 수를 두면 흑의 첫 수 제한이 시작되고, 흑이 첫 수를 두면 백의 본 시계가 시작된다.
|
||||
- `deadline(state)`:
|
||||
- 해당 색 첫 수 전: `turnStartedAt + firstMoveSec*1000`
|
||||
- `none`: `null` — 시간 제한 없음은 정말 마감이 없다(무응답 시간패 없음). 연결이 끊긴 사람만 방의 접속 유예(`graceSec`) 뒤에 처리한다. 첫 수 제한도 없다.
|
||||
- 피셔, 해당 색 첫 수 전: `turnStartedAt + firstMoveSec*1000`
|
||||
- 피셔: `turnStartedAt + remainingMs[turn]`
|
||||
- `none`: `turnStartedAt + idleLimitSec*1000`
|
||||
- 종료: `null`
|
||||
- `onTimeout(state, player)` → `{ type: 'timeout' }`. `apply`:
|
||||
- 첫 수 제한 초과 → `aborted`(승패 없음).
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
| `moveLimit` | 수 제한(도달 시 점수 판정) | `0`(없음) / 150 / 200 / 250 (양쪽 합산 수) | `0` |
|
||||
| `timeControl.kind` | 시간 방식 | `none` / `byoyomi` | `byoyomi` |
|
||||
| `timeControl.mainSec` / `periodSec` / `periods` | 기본 시간 / 초읽기 1회 / 횟수 | 5분·10분·20분 / 20·30·60초 / 1~5회 | 10분 / 30초 / 3회 |
|
||||
| `idleLimitSec` | `none`일 때 무응답 한도 | 300 / 600 | 600 |
|
||||
| `undo` | 무르기 허용 횟수(상대 동의) | `off` / `1` / `3` / `unlimited` | `3` |
|
||||
| `drawOffer` | 무승부 제안 | `true` / `false` | `true` |
|
||||
| `hints` | 이동 가능 칸 표시 | `off` / `moves` | `moves` |
|
||||
@@ -193,7 +192,7 @@ interface JanggiState {
|
||||
- `deadline(state)`:
|
||||
- `setup`: 단계 시작 + 60초(선택해야 할 사람 기준).
|
||||
- `playing` + `byoyomi`: `turnStartedAt + mainMs[turn] + periodsLeft[turn]*periodSec*1000`.
|
||||
- `playing` + `none`: `turnStartedAt + idleLimitSec*1000`.
|
||||
- `playing` + `none`: `null` — 시간 제한 없음은 정말 마감이 없다(무응답 시간패 없음). 연결이 끊긴 사람만 방의 접속 유예(`graceSec`) 뒤에 처리한다.
|
||||
- 종료: `null`.
|
||||
- `activePlayers`: `setup`에서 `hanFirst`면 순서에 맞는 1명, `simultaneous`면 아직 선택 안 한 전원. `playing`이면 차례인 1명.
|
||||
- `onTimeout(state, player)`:
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
| `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` |
|
||||
@@ -266,10 +265,10 @@ interface OmokState {
|
||||
- `deadline(state)`:
|
||||
- `perMove`: `turnStartedAt + perMoveSec*1000`
|
||||
- `fischer`: `turnStartedAt + remainingMs[turn]`
|
||||
- `none`: `turnStartedAt + idleLimitSec*1000`
|
||||
- `none`: `null` — 시간 제한 없음은 정말 마감이 없다(무응답 시간패 없음). 연결이 끊긴 사람만 방의 접속 유예(`graceSec`) 뒤에 처리한다.
|
||||
- `finished`: `null`
|
||||
- `onTimeout(state, player)`는 항상 `{ type: 'timeout' }`을 반환하고, `apply`가 정책을 적용한다:
|
||||
- `fischer` 또는 `none`의 무응답 한도 초과, `perMove + lose`: 해당 플레이어 시간패.
|
||||
- `fischer` 시간 다 씀, `perMove + lose`: 해당 플레이어 시간패.
|
||||
- `perMove + autoMove`: 8.4 규칙으로 자동 착수(`moves[].auto = true`), `autoMoveStreak +1`. 자동 착수 2번 뒤 세 번째로 연속 시간 초과하면 시간패. 정상 착수 시 streak 0으로 초기화.
|
||||
- 서버는 연결 끊긴 플레이어의 차례에 `min(deadline, 끊긴 시각 + 방의 graceSec)`에 `onTimeout`을 호출한다(유예 시간은 방 설정, 기본 60초). 끊긴 플레이어의 차례가 아닐 때는 아무것도 하지 않고, 차례가 오면 위 규칙을 따른다.
|
||||
- 시간 초과 직전에 도착한 `place`는 서버 도착 시각(`ctx.now`) 기준으로 판정한다.
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
| `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` |
|
||||
@@ -102,7 +101,7 @@ interface OthelloState {
|
||||
2. `timeoutPolicy=autoMove`의 자동 착수: 모서리(a1, h1, a8, h8)가 합법이면 그중 무작위, 아니면 전체 합법 수 중 균등 무작위. 모서리 우선은 시간 초과 당한 쪽이 지나치게 불리해지지 않게 하는 최소한의 배려.
|
||||
|
||||
### 8.5 타이머·시간 초과·연결 끊김 시 자동 행동
|
||||
- `deadline(state)`: `perMove` → `turnStartedAt + perMoveSec*1000`, `fischer` → `turnStartedAt + remainingMs[turn]`, `none` → `turnStartedAt + idleLimitSec*1000`, 종료 → `null`.
|
||||
- `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연속이면 시간패.
|
||||
- 그 외: 시간패.
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
| `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` |
|
||||
@@ -155,7 +154,7 @@ interface QuoridorState {
|
||||
2. `autoMove`에서 최단 경로의 첫 칸 후보가 여럿일 때 선택.
|
||||
|
||||
### 8.5 타이머·시간 초과·연결 끊김 시 자동 행동
|
||||
- `deadline(state)`: `perMove` → `turnStartedAt + perMoveSec*1000`, `fischer` → `turnStartedAt + remainingMs[seat]`, `none` → `turnStartedAt + idleLimitSec*1000`, 종료 → `null`.
|
||||
- `deadline(state)`: `perMove` → `turnStartedAt + perMoveSec*1000`, `fischer` → `turnStartedAt + remainingMs[seat]`, `none` → `null`(마감 없음, 무응답 시간패 없음), 종료 → `null`.
|
||||
- `onTimeout(state, player)` → `{ type: 'timeout' }`. `apply`:
|
||||
- `perMove + autoMove`: 이동 가능 칸 중 "목표까지 BFS 거리(말 무시)가 가장 짧아지는 칸"으로 이동(점프 포함). 동률은 RNG. 이동 불가면 자동 패스. streak 3회면 시간패.
|
||||
- 그 외: 시간패. 2인은 상대 승. 4인은 기권과 같은 처리(말 제거, `timedOut`).
|
||||
|
||||
Reference in New Issue
Block a user