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:
46
packages/games/src/quoridor/bot.test.ts
Normal file
46
packages/games/src/quoridor/bot.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/** 벽 쌓기 길찾기 computer player: shortest path, winning step, walls a rival who is about to win. */
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { SeededRng } from '@bg/engine';
|
||||
import { decideBotAction } from '../bots/index';
|
||||
import { quoridor, type Cell, type QuoridorAction, type QuoridorState, type Seat } from './index';
|
||||
import { emptyWallIndex, pathLength, setWall, type Wall } from './board';
|
||||
|
||||
/** 'e5' → { c: 4, r: 4 }. */
|
||||
const at = (l: string): Cell => ({ c: 'abcdefghi'.indexOf(l[0]!), r: Number(l.slice(1)) - 1 });
|
||||
/** 'h:d4' → horizontal wall at anchor d4. */
|
||||
const wall = (w: string): Wall => ({ o: w[0] as 'h' | 'v', ...at(w.slice(2)) });
|
||||
|
||||
/** 2 players: A = 남 (the bot here, to move, goal row 9), B = 북 (goal row 1). */
|
||||
function position(pos: Partial<Record<Seat, string>>, walls: string[] = []): QuoridorState {
|
||||
const options = quoridor.optionsSchema.parse({ seatOrder: 'joinOrder' });
|
||||
const s = quoridor.setup({ players: ['A', 'B'], options, rng: SeededRng.fromSeed([1, 2, 3, 4]), now: 0 });
|
||||
for (const x of s.seats) if (pos[x.seat]) x.pos = at(pos[x.seat]!);
|
||||
s.walls = walls.map(wall);
|
||||
s.wallIndex = emptyWallIndex();
|
||||
for (const w of s.walls) setWall(s.wallIndex, w, true);
|
||||
s.turnIndex = s.seats.findIndex((x) => x.seat === 'S');
|
||||
return s;
|
||||
}
|
||||
const decide = (s: QuoridorState, random = () => 0.5) => decideBotAction(quoridor, s, 'A', { now: 1000, random }) as QuoridorAction;
|
||||
|
||||
describe('quoridor bot', () => {
|
||||
test('walks the shortest way around a wall', () => {
|
||||
// The wall above e2/f2 blocks the straight way; d2 → d3 is shortest (f2 is blocked above as well).
|
||||
const s = position({ S: 'e2', N: 'e9' }, ['h:e2']);
|
||||
expect(decide(s)).toEqual({ type: 'move', to: at('d2') });
|
||||
});
|
||||
|
||||
test('steps onto the goal row even when the rival is one step from winning', () => {
|
||||
const s = position({ S: 'e8', N: 'a2' });
|
||||
expect(decide(s)).toEqual({ type: 'move', to: at('e9') });
|
||||
});
|
||||
|
||||
test('walls off a rival who is about to win', () => {
|
||||
const s = position({ S: 'e5', N: 'e2' });
|
||||
const a = decide(s);
|
||||
expect(a.type).toBe('placeWall');
|
||||
const after = quoridor.apply(s, 'A', a, { rng: SeededRng.fromSeed([1, 1, 1, 1]), now: 1000 }).state;
|
||||
const north = after.seats.find((x) => x.seat === 'N')!;
|
||||
expect(pathLength(after.wallIndex, north.pos!, 'N')).toBeGreaterThan(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user