Merge branch 'm9/bots' into codex/owner/grp_discord_joke-app
This commit is contained in:
@@ -28,6 +28,8 @@
|
||||
| `start` | — | 방장: 시작 |
|
||||
| `kick` | `userId` | 방장: 내보내기 |
|
||||
| `host` | `userId` | 방장 넘기기 |
|
||||
| `addBot` | `seat?: number` | 방장: 빈자리에 컴퓨터 넣기(대기실에서만, 06 §12) |
|
||||
| `removeBot` | `userId` | 방장: 컴퓨터 빼기(대기실에서만) |
|
||||
| `act` | `cs: number`, `a: Action` | 게임 행동. `cs`는 클라이언트 행동 번호(1부터 증가) |
|
||||
| `chat` | `text`(1~200자) | 채팅 |
|
||||
| `emote` | `id` | 빠른 반응(👍, 😂, "잘했어요!" 등 고정 목록) |
|
||||
|
||||
@@ -95,3 +95,12 @@ interface Room {
|
||||
```
|
||||
- 한 사용자는 동시에 한 방에만 플레이어로 있을 수 있다(다른 방에 들어가면 이전 방 대기실 자리에서 빠짐. 이전 방이 게임 중이면 "진행 중인 게임이 있어요. 그래도 이동할까요?").
|
||||
- 제한: 서버 전체 방 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를 막고, 오셀로는 귀를 잡고, 카드 게임은 패 세기에 맞게).
|
||||
- 사람이 모두 나가면 게임은 무효로 끝난다.
|
||||
|
||||
@@ -86,13 +86,13 @@ CREATE TABLE games (
|
||||
started_at INTEGER NOT NULL,
|
||||
ended_at INTEGER,
|
||||
end_reason TEXT, -- 'normal' | 'resign' | 'abandoned' | 'void'
|
||||
result_json TEXT
|
||||
result_json TEXT -- 컴퓨터와 한 연습 게임은 "practice": true
|
||||
);
|
||||
CREATE INDEX games_room ON games(room_id);
|
||||
|
||||
CREATE TABLE game_players (
|
||||
game_pk TEXT NOT NULL REFERENCES games(id) ON DELETE CASCADE,
|
||||
user_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL, -- 컴퓨터는 'bot:<번호>:<ULID>'(users에 없음)
|
||||
seat INTEGER NOT NULL,
|
||||
rank INTEGER, -- 1 = 1등, 무승부는 같은 순위
|
||||
score REAL,
|
||||
@@ -117,7 +117,7 @@ CREATE TABLE user_stats (
|
||||
| 대기실 변경(자리, 옵션, 준비) | `room_state` update(+ `room_log`) |
|
||||
| 게임 시작 | `games` insert, `room_state`, `room_log` |
|
||||
| 게임 행동 / 시간 초과 | `room_log` insert + `room_state` update (한 트랜잭션) |
|
||||
| 게임 끝 | `games` update(결과, 시드 공개), `game_players`, `user_stats` upsert (한 트랜잭션) |
|
||||
| 게임 끝 | `games` update(결과, 시드 공개), `game_players`, `user_stats` upsert (한 트랜잭션). 무효·연습 게임은 `user_stats` 제외 |
|
||||
| 방 닫힘 | `rooms.status='closed'`, `room_state` 삭제 |
|
||||
|
||||
## 4. 정리 작업(매시간)
|
||||
|
||||
@@ -129,3 +129,8 @@ export interface GameUI<V> {
|
||||
- **API**: `GET /api/me/games?before=&limit=`(내가 끝까지 한 게임, 최신순), `GET /api/replays/:gamePk`. 다시 만든 결과는 잠깐(10분, 32개) 캐시하고, 한 번에 하나씩 조금씩 끊어 만들어 방 처리를 막지 않는다.
|
||||
- **화면**: 게임의 `Board`/`Side`를 그대로 쓰되 읽기 전용(`act`는 아무것도 안 함, 판 안의 누르기·키 입력은 막음). 처음 / 이전 / 재생·멈춤 / 다음 / 끝 버튼, 슬라이더, "n번째 / 전체 n번", 장면 설명(누가 움직였는지, 자동 진행). 키보드 ←/→/Home/End. 들어가는 곳: 결과 화면 [처음부터 다시 보기](`RoomView.lastGamePk`), 내 정보 › 내 게임 기록 [다시 보기].
|
||||
- **테스트**: 모든 게임을 실제 `Room`으로 끝까지(무작위 행동, 시간 초과, 나가기) 둔 뒤 다시 만든 상태·결과가 같은지, 모든 장면이 그 플레이어가 받은 view와 같은지(`replay.test.ts`), 권한(`replay-api.test.ts`), 브라우저(`e2e/replay.e2e.ts`).
|
||||
|
||||
## 9. 컴퓨터 상대 (`packages/games/src/bots/`, 06 §12)
|
||||
- 게임마다 `<게임>/bot.ts`의 전략 하나: `(입력: { me, view, legal, random }) => 행동`. `view`는 `view(state, me)`, `legal`은 `legalActions(state, me)`. 게임 상태는 넘기지 않는다(비공개 정보 차단).
|
||||
- `decideBotAction`이 고른 수를 `validate`로 다시 검사하고, 틀리면 다른 합법 수로 바꾼다. 서버는 그래도 실패하면 게임의 `onTimeout` 행동을 쓴다.
|
||||
- 테스트(`bots/bots.test.ts`): 게임마다 "사람 1명은 시간 초과만, 나머지는 컴퓨터"와 "컴퓨터끼리" 판을 끝까지 돌려, 모든 수가 합법이고 컴퓨터 때문에 멈추지 않으며 한 수가 충분히 빠른지 본다.
|
||||
|
||||
Reference in New Issue
Block a user