import { describe, expect, it } from 'vitest'; import { Card } from '@core/cards/Card'; import { ActionType } from '@core/rules/Action'; import { Phase } from '@core/rules/GameState'; import { apply, currentActor, legalActions } from '@core/rules/DurakRules'; import { intentFor } from '@core/rules/intent'; import { makeState } from '../../support/fixtures'; const YOU = 'you'; const BOT = 'bot'; const tap = (state: Parameters[0], code: string) => intentFor(state, YOU, Card.parse(code)); describe('intentFor when opening a turn', () => { const position = () => makeState({ hands: { [YOU]: '6C 7D KH', [BOT]: '8C 9D QH' }, trump: 'S', deck: '10C 10D', attacker: YOU, }); it('plays any card from hand', () => { expect(tap(position(), '6C')).toMatchObject({ type: ActionType.Attack }); expect(tap(position(), 'KH')).toMatchObject({ type: ActionType.Attack }); }); it('refuses a card that is not in hand', () => { expect(tap(position(), 'AS')).toBeNull(); }); it('refuses everything when it is not your move', () => { const state = position(); expect(intentFor(state, BOT, Card.parse('8C'))).toBeNull(); }); }); describe('intentFor when defending', () => { const position = () => makeState({ hands: { [YOU]: '8D 6S KC', [BOT]: 'AH AD' }, trump: 'S', deck: '10C 10D', attacker: BOT, defender: YOU, table: [{ attack: '7D' }], phase: Phase.Defence, }); it('works out which attack the card beats, with no second tap', () => { expect(tap(position(), '8D')).toMatchObject({ type: ActionType.Defend, slot: 0 }); }); it('allows a trump', () => { expect(tap(position(), '6S')).toMatchObject({ type: ActionType.Defend, slot: 0 }); }); it('refuses a card that beats nothing', () => { expect(tap(position(), 'KC')).toBeNull(); }); it('picks the lowest slot the card can answer', () => { const state = makeState({ hands: { [YOU]: 'AD', [BOT]: 'AH AC' }, trump: 'S', deck: '10C 10D', attacker: BOT, defender: YOU, table: [{ attack: '7C' }, { attack: '7D' }], phase: Phase.Defence, }); // The ace of diamonds cannot answer the clubs, so slot 1 is the answer. expect(tap(state, 'AD')).toMatchObject({ slot: 1 }); }); }); describe('intentFor when adding to an attack', () => { const position = () => makeState({ hands: { [YOU]: '7H 9C KD', [BOT]: 'AH AD' }, trump: 'S', deck: '10C 10D', attacker: YOU, table: [{ attack: '7D', defence: '9D' }], phase: Phase.Reinforce, }); it('adds a card whose rank is on the table', () => { expect(tap(position(), '7H')).toMatchObject({ type: ActionType.Attack }); expect(tap(position(), '9C')).toMatchObject({ type: ActionType.Attack }); }); it('refuses a rank that is not on the table', () => { expect(tap(position(), 'KD')).toBeNull(); }); }); describe('intentFor agrees with the rules', () => { it('never proposes an action the rules would reject', () => { // Play many matches by tapping cards, and check every proposal is legal. for (let seed = 0; seed < 400; seed++) { let state = makeState({ hands: { [YOU]: '6C 7D 8H 9S 10C JD', [BOT]: '6D 7H 8S 9C 10D JH' }, trump: 'S', deck: 'QC QD QH QS KC KD KH KS AC AD AH', attacker: seed % 2 === 0 ? YOU : BOT, turnNumber: 1 + (seed % 3), }); for (let step = 0; step < 40 && state.phase !== Phase.Finished; step++) { const actor = currentActor(state); if (actor === null) break; if (actor === YOU) { const hand = state.players[0].hand; for (const card of hand) { const action = intentFor(state, YOU, card); if (!action) continue; // Whatever it proposes, the rules must accept it. expect(() => apply(state, action)).not.toThrow(); } } const options = legalActions(state, actor); state = apply(state, options[seed % options.length]).state; } } }); it('proposes something for every card the rules would accept', () => { const state = makeState({ hands: { [YOU]: '6C 7D 8H 9S 10C JD', [BOT]: '6D 7H 8S' }, trump: 'S', deck: 'QC QD QH', attacker: YOU, }); const playable = state.players[0].hand.filter((c) => intentFor(state, YOU, c) !== null); const legalCards = legalActions(state, YOU) .filter((a) => a.type === ActionType.Attack) .map((a) => (a as { card: Card }).card); expect(new Set(playable)).toEqual(new Set(legalCards)); }); });