- 방 화면을 벗어나면(홈·규칙) detach: 자리·접속 상태는 유지, 방 접속 수에서 빠짐 - 컴퓨터가 있는 방: 사람이 모두 [방 나가기]하면 바로 닫고, 아무도 안 보면 대기실·끝난 방 1분, 게임 중 30분 뒤 정리 - 게스트 로그아웃 = 계정 삭제(자리·전적·세션·소켓), 사이트를 닫고 24시간 지난 게스트도 1분 주기로 삭제 - users.last_seen_at을 요청·소켓 종료 때 갱신(이전엔 갱신 안 됨), 003 마이그레이션에서 기존 게스트 시계 초기화 - 테스트: 무작위 대국 시드를 playoutSeeds()로 평소 1/10, test:full은 전부. bun test --parallel=4, 테스트 서버 종료 대기 1s→50ms Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
9.6 KiB
Markdown
109 lines
9.6 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개.
|
|
- 방 정리(1분마다): 접속이 하나도 없는 방 중 대기실은 10분, 끝난 방·멈춘 방은 30분 동안 활동이 없으면 닫는다. 컴퓨터가 있는 방(연습 방)은 마지막 사람이 나간 때부터 재서 대기실·끝난 방은 1분, 게임 중인 방은 30분 뒤 닫는다. 사람이 [방 나가기]로 모두 나가 컴퓨터만 남으면 바로 닫는다.
|
|
- 방 화면을 벗어나면(홈, 규칙 등) 클라이언트가 `detach`를 보낸다. 자리는 그대로이고 '접속 중'으로 남지만, 방 메시지를 받지 않고 방의 접속 수에서도 빠진다. 그 상태로 사이트를 닫으면 연결 끊김으로 처리한다.
|
|
|
|
## 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를 막고, 오셀로는 귀를 잡고, 카드 게임은 패 세기에 맞게).
|
|
- 사람이 모두 나가면 게임은 무효로 끝난다.
|