Files
joke-app/packages/games/src/las-vegas/bot.ts
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

52 lines
2.6 KiB
TypeScript

/**
* 라스베가스 computer player. Every rolled number is scored on its casino with the real payout rules:
* the bill I would hold there now vs. before (discounted when an opponent can still easily catch up), plus bills it
* knocks away from opponents (ties, neutral dice), minus the value of the dice it spends.
*/
import type { BotStrategy } from '../bots/types';
import { answerRequests, best, pick, playable } from '../bots/policy';
import { HOUSE, settleCasino, sum, type Owner } from './rules';
import type { LasVegasAction, LasVegasView } from './index';
type Place = Extract<LasVegasAction, { type: 'PLACE' }>;
export const lasVegasBot: BotStrategy<LasVegasView, LasVegasAction> = ({ me, view, legal, random }) => {
const req = answerRequests(legal);
if (req) return req;
const places = legal.filter((a) => a.type === 'PLACE') as Place[];
if (!places.length || !view.lastRoll) return pick(playable(legal), random);
const order: Owner[] = [...view.seats, HOUSE];
const others = view.seats.filter((p) => p !== me && !view.conceded.includes(p));
const othersDice = Math.max(0, ...others.map((p) => view.hand[p]!.own));
// A die is worth about the money still on the table shared by every die still to be placed.
const table = sum(view.casinos.flatMap((c) => c.notes));
const diceInHands = view.seats.reduce((n, p) => n + view.hand[p]!.own + view.hand[p]!.neutral, 0);
const dieCost = (0.5 * table) / Math.max(1, diceInHands);
/** Worth of one casino's dice layout to me: my bill (discounted while an opponent can still catch up) minus theirs. */
const worth = (notes: number[], dice: Partial<Record<Owner, number>>): number => {
const { awards } = settleCasino(notes, dice, order);
const rival = Math.max(0, ...Object.entries(dice).filter(([o]) => o !== me).map(([, n]) => n ?? 0));
const margin = (dice[me] ?? 0) - rival;
const safe = othersDice === 0 ? 1 : Math.max(0.2, Math.min(1, 0.55 + 0.15 * margin));
let v = 0;
for (const a of awards) {
if (a.owner === me) v += a.note * safe;
else if (a.owner !== HOUSE) v -= a.note / Math.max(1, others.length);
}
return v;
};
const score = ({ value }: Place): number => {
const own = view.lastRoll!.own.filter((d) => d === value).length;
const neutral = view.lastRoll!.neutral.filter((d) => d === value).length;
const c = view.casinos[value - 1]!;
const dice = { ...c.dice };
if (own) dice[me] = (dice[me] ?? 0) + own;
if (neutral) dice[HOUSE] = (dice[HOUSE] ?? 0) + neutral;
return worth(c.notes, dice) - worth(c.notes, c.dice) - dieCost * own;
};
return best(places, (a) => score(a) + random() * 1e-3, random);
};