/** * 라스베가스 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; export const lasVegasBot: BotStrategy = ({ 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>): 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); };