Files
joke-app/docs/06-lobby-rooms.md
EJClaw a3f81ded37 M9: 컴퓨터 상대 — 게임 22종 컴퓨터 전략, 서버 구동(BotDriver), 연습 방
- packages/shared/src/bots.ts: 컴퓨터 id(bot:<n>:<ulid>)·프로필(컴퓨터 n)·BOT_GAMES
  (도블·할리갈리·그림 맞히기·마피아 제외)
- packages/games/src/bots: 전략 계약(자기 view + 합법 수만), decideBotAction(validate 재검사),
  게임별 <게임>/bot.ts 22개 + 행동 테스트, 전 게임 판 끝까지 돌리는 공통 테스트
- 서버: Room.addBot/removeBot(방장·대기실만, 방은 친구만), RoomManager.createPractice
  (POST /api/rooms {practice:true}), BotDriver(사람 같은 지연, 사람과 같은 창이면 더 천천히,
  재시작 후 다시 예약), 연습 게임은 전적 제외(result_json.practice), 사람이 모두 나가면 무효
- 프로토콜 addBot/removeBot, 문서 03·06 §12·08·09 §8

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 13:32:28 +09:00

107 lines
8.9 KiB
Markdown

# 06. 로비와 방
## 1. 목표
- 방 만들기: 첫 화면에서 **게임 카드 누르기 → "방 만들기"** 두 번이면 대기실, 코드와 링크가 바로 보인다.
- 방 참여: 링크를 누르면 바로 대기실(처음이면 닉네임 한 칸). 코드를 받았으면 첫 화면 "코드로 참여"에 숫자 6개.
- 글을 몰라도 쓸 수 있게 큰 아이콘 + 짧은 글자.
## 2. 화면과 주소
| 주소 | 화면 |
|---|---|
| `/` | 홈: 위쪽에 큰 버튼 두 개 **[방 만들기] [코드로 참여]**, 그 아래 "진행 중인 게임으로 돌아가기"(있을 때만), 그 아래 게임 카드 목록 |
| `/g/:gameId` | 게임 소개: 한 줄 설명, 인원, 시간, 난이도, **[방 만들기] [빠른 시작] [규칙 보기]**, 열린 공개 방 목록 |
| `/r/:code` | 방(대기실 ↔ 게임 화면 ↔ 결과 화면이 같은 주소에서 전환) |
| `/j` | 코드 입력 전용 화면(숫자 키패드) |
| `/me` | 계정 |
| `/rules/:gameId` | 규칙 설명(그림 포함) |
초대 링크는 `https://<도메인>/r/123456`. 짧고 읽기 쉬워서 말로 불러 줘도 된다.
## 3. 방 코드
- 6자리 숫자(000000 제외, 같은 숫자 6개·연속 숫자 제외). 숫자만 쓰는 이유: 어르신·어린이가 키패드로 입력하기 가장 쉽고, 말로 전달하기 쉽다.
- 화면에는 `123 456`처럼 3자리씩 띄어 보여 준다. 입력은 띄어쓰기·하이픈 무시.
- 사용 중인 코드와 겹치지 않게 무작위 발급. 방이 끝나고 24시간 동안은 같은 코드 재사용 금지(옛 링크로 엉뚱한 방에 들어가는 것 방지).
- 코드 입력 화면: 6칸 큰 숫자 칸, 휴대폰 숫자 키패드(`inputmode="numeric"`), 6자리가 채워지면 자동으로 입장 시도. 클립보드에 6자리 숫자나 초대 링크가 있으면 "붙여넣기" 버튼을 크게 표시.
## 4. 방 만들기
1. 게임 선택(홈 카드 또는 홈의 [방 만들기] → 게임 고르기 화면).
2. 바로 대기실 생성. 모든 옵션은 **기본값으로 이미 정해져 있다**(인원은 게임 기본 인원, 공개 여부는 "친구만(비공개)").
3. 대기실에서 방장이 바꿀 수 있는 것: 게임 모드/옵션(시간 제한 포함 — 게임마다 다름), 최대 인원(게임의 최소~최대 범위 안), 공개/비공개, 관전 허용, 채팅 허용.
4. 옵션은 "기본 설정" 몇 개만 보이고 나머지는 "자세한 설정" 접기 안에 둔다.
## 5. 대기실 화면 (PC는 두 칸 배치 — `07-ui-ux.md` 3.4. 아래는 내용 순서이며 좁은 화면에서는 이 순서로 한 칸에 쌓인다)
1. **초대 카드**(가장 눈에 띄게): 큰 글씨 코드 `123 456`
- [코드 복사] [링크 복사] [공유하기](모바일: 공유 시트 → 카카오톡 등) [QR 보기](같은 공간의 가족이 폰으로 찍어서 입장)
- 복사하면 버튼이 1.5초간 "복사했어요 ✓"로 바뀜 + 진동(지원 기기).
- 공유 문구: "OO님이 오목 방에 초대했어요! 코드 123 456 · https://<도메인>/r/123456"
2. **자리 목록**: 게임 인원만큼 자리 칸. 각 칸: 프로필, 닉네임, 방장 왕관, 준비 체크, 연결 상태 점(초록/노랑/회색). 빈자리는 점선 "+ 초대하기"(누르면 초대 카드로 스크롤).
3. **게임 설정 요약**: "오목 · 렌주룰 · 한 수 30초" + 방장에게만 [설정 바꾸기].
4. **아래 고정 버튼**(엄지 닿는 곳):
- 방장: [게임 시작] — 최소 인원이 안 되면 비활성 + "1명 더 필요해요" 안내. 모두 준비가 아니어도 시작 가능하되 "준비 안 된 사람이 있어요. 시작할까요?" 확인.
- 나머지: [준비하기] / [준비 취소].
5. 채팅(접을 수 있음), 관전자 수.
방장 기능: 내보내기, 방장 넘기기, 자리 순서 섞기(선 정하기), 게임 바꾸기(같은 방 유지 — 친구들끼리 여러 게임 이어서 하기). 방장이 나가면 가장 먼저 들어온 사람이 방장.
## 6. 링크로 들어오기
1. `/r/123456` 접속.
2. 로그인 안 됨 → 같은 화면 위에 작은 창: 방 정보("오목 · 방장 OO · 1/2명") + 닉네임 칸(추천 이름 채워짐) + [이 이름으로 입장] + 작게 [디스코드로 로그인].
3. 입장 → 빈자리가 있으면 **자동으로 자리에 앉는다**. 없으면 관전자로 들어가고 "자리가 나면 앉을 수 있어요".
4. 방이 없거나 끝났으면: "이 방은 끝났어요" + [같은 게임 새 방 만들기] [홈으로].
## 7. 빠른 시작
- 게임 상세의 [빠른 시작]: 같은 게임의 공개 대기실 중 사람이 가장 많은(가득 차지 않은) 방에 들어간다. 없으면 공개 방을 새로 만들고 기다린다.
- 기다리는 동안 "다른 사람을 기다리는 중… (공개 방)" + [친구 초대하기] 버튼.
## 8. 게임 중
- 위쪽 상태 줄: 지금 할 일 문장("내 차례예요" / "OO님 차례예요"), 남은 시간 링.
- 메뉴(⋯): 규칙 보기, 소리, 글자 크기, 무르기 요청(지원 게임), 무승부 제안(지원 게임), 기권, 나가기.
- 나가기를 누르면: "게임 중에 나가면 OO 처리돼요. 정말 나갈까요?"(게임별: 2인 대전은 기권패, 카드 게임은 다이 후 자리 비움 등).
- 관전자는 [자리 나면 참가] 대기 가능(다음 판부터).
## 9. 게임 끝
- 결과 화면: 순위, 점수, 핵심 장면 요약(예: "흑 5목 완성"), [한 판 더] [게임 바꾸기] [나가기].
- [한 판 더]는 투표: 자리에 앉은 사람 모두 누르면 같은 설정으로 즉시 새 게임(선/딜러는 규칙에 따라 교대). 안 누르는 사람이 있으면 그 사람이 [관전하기]로 일어나거나 방장이 [게임 시작]으로 바로 시작할 수 있다.
- 기록: 끝난 게임은 `game_results`에 저장, 방 안에서 "이번 방 전적"(예: OO 3승 2패) 표시.
## 10. 공개 방 목록
- 게임 상세 화면에서만 보인다(홈을 복잡하게 하지 않음). 정렬: 곧 시작할 방(빈자리 1개) 우선.
- 각 줄: 방 이름(기본 "OO님의 방"), 인원, 모드, [참여].
## 11. 서버 방 모델
```ts
type RoomStatus = 'lobby' | 'playing' | 'finished' | 'paused' | 'broken';
interface Room {
code: string; // 6자리
id: string; // 내부 ULID
hostId: UserId;
gameId: GameId;
options: unknown; // 게임 optionsSchema로 검증된 값
visibility: 'private' | 'public';
maxPlayers: number; // 게임 min~max 범위
graceSec: number; // 연결 끊김 유예(기본 60초). 턴 시간은 게임 옵션
allowSpectators: boolean;
chat: { enabled: boolean; filter: boolean };
seats: Array<{ userId: UserId; ready: boolean } | null>;
spectators: Set<UserId>;
presence: Map<UserId, { connected: boolean; since: number; autoCount: number }>;
status: RoomStatus;
seq: number;
game?: { state: unknown; rng: RngState; deadline: number | null; startedAt: number; seedHash: string };
rematchVotes: Set<UserId>;
sessionStats: Map<UserId, { wins: number; played: number }>;
createdAt: number; lastActiveAt: number;
}
```
- 한 사용자는 동시에 한 방에만 플레이어로 있을 수 있다(다른 방에 들어가면 이전 방 대기실 자리에서 빠짐. 이전 방이 게임 중이면 "진행 중인 게임이 있어요. 그래도 이동할까요?").
- 제한: 서버 전체 방 2,000개, 사용자당 동시에 만든 방 3개.
## 12. 컴퓨터 상대 (혼자 연습)
- 들어가는 곳: 게임 상세와 홈의 [🤖 컴퓨터와 연습하기]. 친구만 방이 열리고, 바로 시작할 수 있을 만큼 컴퓨터가 앉아 있다(예: 오목 1명, 마작 2명, 뱅 3명).
- 대기실: 방장은 첫 빈자리의 [🤖 컴퓨터 넣기]로 넣고, 컴퓨터 자리의 [✕]로 뺀다(대기실에서만). 컴퓨터 이름은 "컴퓨터 1", "컴퓨터 2"…, 아바타는 🤖. 늘 준비 완료이고, 방장은 컴퓨터에게 넘어가지 않는다. 사람이 한 명은 앉아야 시작된다.
- 컴퓨터가 앉은 방은 연습 방이다: 공개로 바꿀 수 없고, 공개 방 목록·빠른 시작에 나오지 않는다. 결과는 남지만 전적(`user_stats`)에는 넣지 않는다. 화면에 "연습 게임이에요. 기록에는 남지 않아요." 표시.
- 컴퓨터가 할 수 있는 게임: 실시간·대화 게임(도블, 할리갈리, 그림 맞히기, 마피아)을 뺀 전부(`packages/shared/src/bots.ts`의 `BOT_GAMES`).
- 움직임: 서버의 `BotDriver`(`apps/server/src/rooms/bots.ts`)가 게임이 컴퓨터를 기다릴 때(차례, 반응 창, 동시 선택, 다음 판 준비) 0.4~1.5초 쉬었다가 한 수를 둔다. 사람과 같은 행동 경로(검사·기록)를 거치고, 서버가 다시 켜져도 이어서 둔다.
- 수 고르기: `packages/games/src/<게임>/bot.ts`. 자기 화면(`view(state, 나)`)과 둘 수 있는 수만 보고 고르므로 남의 패를 볼 수 없다. 강하지 않지만 엉뚱하지 않게(오목은 4를 막고, 오셀로는 귀를 잡고, 카드 게임은 패 세기에 맞게).
- 사람이 모두 나가면 게임은 무효로 끝난다.