import { describe, expect, it } from 'vitest'; import { ActionType } from '@core/rules/Action'; import { Phase } from '@core/rules/GameState'; import { projectView } from '@core/bot/PlayerView'; import { BasicBot } from '@core/bot/BasicBot'; import { makeState } from '../../support/fixtures'; const YOU = 'you'; const BOT = 'bot'; const bot = new BasicBot(); function decideFor(state: Parameters[0], player: string) { return bot.decide(projectView(state, player)); } describe('BasicBot opening a turn', () => { it('leads with its cheapest non-trump', () => { const state = makeState({ hands: { [BOT]: '9C 6D KH 7S', [YOU]: 'AH AD' }, trump: 'S', deck: '10C 10D', attacker: BOT, }); const action = decideFor(state, BOT); expect(action.type).toBe(ActionType.Attack); expect(action).toMatchObject({ card: expect.objectContaining({ code: '6♦' }) }); }); it('keeps its trumps back even when they are cheaper', () => { const state = makeState({ hands: { [BOT]: '6S 9C', [YOU]: 'AH AD' }, trump: 'S', deck: '10C 10D', attacker: BOT, }); expect(decideFor(state, BOT)).toMatchObject({ card: expect.objectContaining({ code: '9♣' }), }); }); it('leads with its cheapest trump when that is all it holds', () => { const state = makeState({ hands: { [BOT]: 'KS 7S', [YOU]: 'AH AD' }, trump: 'S', deck: '10C 10D', attacker: BOT, }); expect(decideFor(state, BOT)).toMatchObject({ card: expect.objectContaining({ code: '7♠' }), }); }); }); describe('BasicBot defending', () => { it('beats with the cheapest card that works', () => { const state = makeState({ hands: { [YOU]: 'AC', [BOT]: '10D 8D QD 6S' }, trump: 'S', deck: '10C 10H', attacker: YOU, table: [{ attack: '7D' }], phase: Phase.Defence, }); expect(decideFor(state, BOT)).toMatchObject({ type: ActionType.Defend, card: expect.objectContaining({ code: '8♦' }), }); }); it('prefers any non-trump over a cheap trump', () => { const state = makeState({ hands: { [YOU]: 'AC', [BOT]: 'AD 6S' }, trump: 'S', deck: '10C 10H', attacker: YOU, table: [{ attack: '7D' }], phase: Phase.Defence, }); expect(decideFor(state, BOT)).toMatchObject({ card: expect.objectContaining({ code: 'A♦' }), }); }); it('spends a trump when nothing else beats the attack', () => { const state = makeState({ hands: { [YOU]: 'AC', [BOT]: '6D KS 7S' }, trump: 'S', deck: '10C 10H', attacker: YOU, table: [{ attack: '7D' }], phase: Phase.Defence, }); expect(decideFor(state, BOT)).toMatchObject({ card: expect.objectContaining({ code: '7♠' }), }); }); it('takes when it cannot beat the attack', () => { const state = makeState({ hands: { [YOU]: 'AC', [BOT]: '6D 6H' }, trump: 'S', deck: '10C 10H', attacker: YOU, table: [{ attack: '7D' }], phase: Phase.Defence, }); expect(decideFor(state, BOT).type).toBe(ActionType.Take); }); }); describe('BasicBot adding to an attack', () => { it('throws in every eligible non-trump, cheapest first', () => { const state = makeState({ hands: { [BOT]: '7H 7C KD', [YOU]: 'AH AD AC' }, trump: 'S', deck: '10C 10D', attacker: BOT, table: [{ attack: '7D', defence: '9D' }], phase: Phase.Reinforce, }); expect(decideFor(state, BOT)).toMatchObject({ type: ActionType.Attack, card: expect.objectContaining({ code: '7♣' }), }); }); it('holds its trumps back while the deck still has cards', () => { const state = makeState({ hands: { [BOT]: '7S KD', [YOU]: 'AH AD AC' }, trump: 'S', deck: '10C 10D', attacker: BOT, table: [{ attack: '7D', defence: '9D' }], phase: Phase.Reinforce, }); expect(decideFor(state, BOT).type).toBe(ActionType.Pass); }); it('holds its trumps back at the end if it still has other cards', () => { const state = makeState({ hands: { [BOT]: '7S KD', [YOU]: 'AH AD AC' }, trump: 'S', attacker: BOT, table: [{ attack: '7D', defence: '9D' }], phase: Phase.Reinforce, }); expect(state.deck).toHaveLength(0); expect(decideFor(state, BOT).type).toBe(ActionType.Pass); }); it('throws in a trump once the deck is empty and trumps are all it has', () => { const state = makeState({ hands: { [BOT]: '7S KS', [YOU]: 'AH AD AC' }, trump: 'S', attacker: BOT, table: [{ attack: '7D', defence: '9D' }], phase: Phase.Reinforce, }); expect(decideFor(state, BOT)).toMatchObject({ type: ActionType.Attack, card: expect.objectContaining({ code: '7♠' }), }); }); }); describe('BasicBot contract', () => { it('refuses to move out of turn', () => { const state = makeState({ hands: { [YOU]: '6C', [BOT]: '8C' }, trump: 'S', deck: '10C', attacker: YOU, }); expect(() => decideFor(state, BOT)).toThrow(/out of turn/); }); });