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>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/** Creates, finds, restores and garbage-collects rooms (docs/06 §11, docs/04 §3). */
|
||||
import { ulid } from 'ulid';
|
||||
import type { AnyGameDefinition } from '@bg/engine';
|
||||
import { isAllowedRoomCode, type PublicUser } from '@bg/shared';
|
||||
import { botsAllowed, isAllowedRoomCode, isBotId, type PublicUser } from '@bg/shared';
|
||||
import { Room, type RoomConfig, type RoomDeps, type RoomSnapshot } from './room';
|
||||
import type { RoomStore } from './store';
|
||||
import type { SettingsStore } from '../admin/settings';
|
||||
@@ -19,6 +19,7 @@ export interface ManagerDeps {
|
||||
log?: RoomDeps['log'];
|
||||
random?: () => number;
|
||||
settings?: SettingsStore;
|
||||
botDelay?: RoomDeps['botDelay'];
|
||||
}
|
||||
|
||||
export class RoomManager {
|
||||
@@ -40,7 +41,9 @@ export class RoomManager {
|
||||
now: this.now,
|
||||
log: this.deps.log,
|
||||
settings: this.deps.settings,
|
||||
botDelay: this.deps.botDelay,
|
||||
onSeatChange: (room, userId, seated) => {
|
||||
if (isBotId(userId)) return;
|
||||
const set = this.seatIndex.get(userId) ?? new Set<string>();
|
||||
if (seated) set.add(room.code);
|
||||
else set.delete(room.code);
|
||||
@@ -112,6 +115,23 @@ export class RoomManager {
|
||||
return room;
|
||||
}
|
||||
|
||||
/**
|
||||
* "컴퓨터랑 연습": a private room with just enough computer players to start right away (the host takes the
|
||||
* remaining seat on joining).
|
||||
*/
|
||||
createPractice(hostId: string, gameId: string): Room | string {
|
||||
if (!botsAllowed(gameId)) return '이 게임은 컴퓨터와 할 수 없어요.';
|
||||
const room = this.create(hostId, gameId, 'private');
|
||||
if (typeof room === 'string') return room;
|
||||
const def = this.deps.games[gameId]!;
|
||||
const { min, max } = room.playerRange();
|
||||
let bots = Math.max(1, min - 1);
|
||||
while (bots + 1 < max && def.playerCountError?.(room.config.options, bots + 1)) bots++;
|
||||
// Seat 0 stays free for the host.
|
||||
for (let i = 1; i <= Math.min(bots, room.config.maxPlayers - 1); i++) room.addBot(hostId, i);
|
||||
return room;
|
||||
}
|
||||
|
||||
/** Rooms where the user holds a seat in an unfinished game. */
|
||||
activeRoomFor(userId: string): string | null {
|
||||
for (const code of this.seatIndex.get(userId) ?? []) {
|
||||
@@ -132,7 +152,8 @@ export class RoomManager {
|
||||
|
||||
publicRooms(gameId: string | null): { code: string; gameId: string; host: string; seated: number; max: number; options: unknown }[] {
|
||||
return this.all()
|
||||
.filter((r) => (gameId === null || r.config.gameId === gameId) && r.config.visibility === 'public' && r.status === 'lobby')
|
||||
// Rooms with computer players are practice rooms: never listed, never filled by quick play.
|
||||
.filter((r) => (gameId === null || r.config.gameId === gameId) && r.config.visibility === 'public' && r.status === 'lobby' && !r.hasBots())
|
||||
.map((r) => ({
|
||||
code: r.code,
|
||||
gameId: r.config.gameId,
|
||||
|
||||
Reference in New Issue
Block a user