/** 오셀로 computer player: corners first, no X-squares next to an empty corner, no corner handed over. */ import { describe, expect, test } from 'bun:test'; import { SeededRng } from '@bg/engine'; import { decideBotAction } from '../bots/index'; import { othello, type OthelloAction, type OthelloState } from './index'; import type { Cell } from './rules'; const at = (l: string) => ({ x: 'abcdefgh'.indexOf(l[0]!), y: Number(l.slice(1)) - 1 }); /** A = black (the bot here). 8 rows of '.', 'B', 'W' (row 1 first). */ function fromRows(rows: string[]): OthelloState { const options = othello.optionsSchema.parse({ colorAssignment: 'hostBlack' }); const s = othello.setup({ players: ['A', 'B'], options, rng: SeededRng.fromSeed([1, 2, 3, 4]), now: 0 }); s.board = rows.join('').split('').map((c) => (c === 'B' ? 1 : c === 'W' ? 2 : 0)) as Cell[]; s.turn = 'black'; return s; } const decide = (s: OthelloState, seed = 1) => { const r = SeededRng.fromSeed([seed, 4, 4, 4]); return decideBotAction(othello, s, 'A', { now: 1000, random: () => r.next() }) as OthelloAction; }; const legalLabels = (s: OthelloState) => othello.view(s, 'A').legalMoves!.map((m) => 'abcdefgh'[m.x]! + (m.y + 1)).sort(); describe('othello bot', () => { test('takes a corner when available', () => { // a1 (corner, flips b2) and f4 (flips e4 + more) are both legal. const s = fromRows(['........', '.W......', '..B.....', '...WWB..', '...BW...', '........', '........', '........']); expect(legalLabels(s)).toContain('a1'); for (let seed = 1; seed <= 4; seed++) expect(decide(s, seed)).toEqual({ type: 'place', ...at('a1') }); }); test('avoids the X-square next to an empty corner even when it flips more', () => { // b2 flips two discs (c3, d4); g5 flips one. const s = fromRows(['........', '........', '..W.....', '...W....', '....BW..', '........', '........', '........']); const legal = legalLabels(s); expect(legal).toContain('b2'); for (let seed = 1; seed <= 4; seed++) expect(decide(s, seed)).not.toEqual({ type: 'place', ...at('b2') }); }); test("accepts the opponent's undo request", () => { let s = othello.setup({ players: ['A', 'B'], options: othello.optionsSchema.parse({ colorAssignment: 'hostBlack' }), rng: SeededRng.fromSeed([1, 2, 3, 4]), now: 0 }); const step = (who: string, a: OthelloAction) => (s = othello.apply(s, who, a, { rng: SeededRng.fromSeed([1, 1, 1, 1]), now: 1000 }).state); step('A', { type: 'place', ...at('d3') }); step('B', { type: 'place', ...at('c3') }); step('B', { type: 'requestUndo' }); expect(decide(s)).toEqual({ type: 'respondUndo', accept: true }); }); });