2026-03-07 23:55:36 +00:00
|
|
|
import { describe, it, expect } from 'vitest'
|
|
|
|
|
import { scoreRound, calculateElo, calculateTier } from './scoring.js'
|
|
|
|
|
import type { Challenge } from './challenges.js'
|
|
|
|
|
|
|
|
|
|
function makeChallenge(overrides: Partial<Challenge> = {}): Challenge {
|
|
|
|
|
return {
|
|
|
|
|
type: 'riddle',
|
|
|
|
|
label: 'Test Challenge',
|
|
|
|
|
prompt: 'What is 2+2?',
|
|
|
|
|
answers: ['4'],
|
|
|
|
|
scoring: 'factual',
|
|
|
|
|
baseDamage: 20,
|
|
|
|
|
timeout_ms: 8000,
|
|
|
|
|
...overrides,
|
|
|
|
|
} as Challenge
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function makeResponse(answer: string | null, timeMs = 500, timedOut = false, error = false) {
|
|
|
|
|
return { answer, timeMs, timedOut, error }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('scoreRound', () => {
|
|
|
|
|
const botA = { id: 'a1', name: 'AlphaBot' }
|
|
|
|
|
const botB = { id: 'b1', name: 'BetaBot' }
|
|
|
|
|
|
|
|
|
|
it('both correct — faster bot scores higher', () => {
|
|
|
|
|
const challenge = makeChallenge()
|
|
|
|
|
const result = scoreRound(
|
|
|
|
|
challenge, botA, botB,
|
|
|
|
|
makeResponse('4', 200),
|
|
|
|
|
makeResponse('4', 800),
|
|
|
|
|
null, 0, 0,
|
|
|
|
|
)
|
|
|
|
|
expect(result.botAScore).toBeGreaterThan(result.botBScore)
|
|
|
|
|
expect(result.winnerId).toBe('a1')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('only A correct — A wins big', () => {
|
|
|
|
|
const challenge = makeChallenge()
|
|
|
|
|
const result = scoreRound(
|
|
|
|
|
challenge, botA, botB,
|
|
|
|
|
makeResponse('4', 500),
|
|
|
|
|
makeResponse('banana', 500),
|
|
|
|
|
null, 0, 0,
|
|
|
|
|
)
|
|
|
|
|
expect(result.botAScore).toBeGreaterThan(8)
|
|
|
|
|
expect(result.botBScore).toBeLessThan(3)
|
|
|
|
|
expect(result.winnerId).toBe('a1')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('only B correct — B wins big', () => {
|
|
|
|
|
const challenge = makeChallenge()
|
|
|
|
|
const result = scoreRound(
|
|
|
|
|
challenge, botA, botB,
|
|
|
|
|
makeResponse('wrong', 500),
|
|
|
|
|
makeResponse('4', 500),
|
|
|
|
|
null, 0, 0,
|
|
|
|
|
)
|
|
|
|
|
expect(result.winnerId).toBe('b1')
|
|
|
|
|
expect(result.botBScore).toBeGreaterThan(8)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('both wrong — faster bot slightly ahead', () => {
|
|
|
|
|
const challenge = makeChallenge()
|
|
|
|
|
const result = scoreRound(
|
|
|
|
|
challenge, botA, botB,
|
|
|
|
|
makeResponse('wrong', 200),
|
|
|
|
|
makeResponse('also wrong', 800),
|
|
|
|
|
null, 0, 0,
|
|
|
|
|
)
|
|
|
|
|
expect(result.botAScore).toBeGreaterThan(result.botBScore)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('A times out — B wins automatically', () => {
|
|
|
|
|
const challenge = makeChallenge()
|
|
|
|
|
const result = scoreRound(
|
|
|
|
|
challenge, botA, botB,
|
|
|
|
|
makeResponse(null, 0, true),
|
|
|
|
|
makeResponse('4', 500),
|
|
|
|
|
null, 0, 0,
|
|
|
|
|
)
|
|
|
|
|
expect(result.winnerId).toBe('b1')
|
|
|
|
|
expect(result.botAScore).toBe(0)
|
|
|
|
|
expect(result.botBScore).toBe(10)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('B errors — A wins automatically', () => {
|
|
|
|
|
const challenge = makeChallenge()
|
|
|
|
|
const result = scoreRound(
|
|
|
|
|
challenge, botA, botB,
|
|
|
|
|
makeResponse('4', 500),
|
|
|
|
|
makeResponse(null, 0, false, true),
|
|
|
|
|
null, 0, 0,
|
|
|
|
|
)
|
|
|
|
|
expect(result.winnerId).toBe('a1')
|
|
|
|
|
expect(result.botAScore).toBe(10)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('both timeout — draw', () => {
|
|
|
|
|
const challenge = makeChallenge()
|
|
|
|
|
const result = scoreRound(
|
|
|
|
|
challenge, botA, botB,
|
|
|
|
|
makeResponse(null, 0, true),
|
|
|
|
|
makeResponse(null, 0, true),
|
|
|
|
|
null, 0, 0,
|
|
|
|
|
)
|
|
|
|
|
expect(result.winnerId).toBeNull()
|
|
|
|
|
expect(result.botAScore).toBe(0)
|
|
|
|
|
expect(result.botBScore).toBe(0)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('creative scoring — longer quality text wins', () => {
|
|
|
|
|
const challenge = makeChallenge({
|
|
|
|
|
answers: undefined,
|
|
|
|
|
scoring: 'creative',
|
|
|
|
|
type: 'roast_battle',
|
|
|
|
|
})
|
|
|
|
|
const result = scoreRound(
|
|
|
|
|
challenge, botA, botB,
|
|
|
|
|
makeResponse('Your code is so bad even ChatGPT refuses to debug it. Every function you write is a monument to incompetence.', 300),
|
|
|
|
|
makeResponse('lol ok', 300),
|
|
|
|
|
null, 0, 0,
|
|
|
|
|
)
|
|
|
|
|
expect(result.botAScore).toBeGreaterThan(result.botBScore)
|
|
|
|
|
expect(result.winnerId).toBe('a1')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('critical hit when margin > 4', () => {
|
|
|
|
|
const challenge = makeChallenge()
|
|
|
|
|
const result = scoreRound(
|
|
|
|
|
challenge, botA, botB,
|
|
|
|
|
makeResponse('4', 500),
|
|
|
|
|
makeResponse('banana', 500),
|
|
|
|
|
null, 0, 0,
|
|
|
|
|
)
|
|
|
|
|
// A scores ~9.5, B scores ~2 → margin ~7.5 → critical
|
|
|
|
|
expect(result.isCritical).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('arena modifier doubles damage for matching type', () => {
|
|
|
|
|
const challenge = makeChallenge({ type: 'roast_battle' })
|
|
|
|
|
const resultWithMod = scoreRound(
|
|
|
|
|
challenge, botA, botB,
|
|
|
|
|
makeResponse('4', 500),
|
|
|
|
|
makeResponse('banana', 500),
|
|
|
|
|
'roast_2x', 0, 0,
|
|
|
|
|
)
|
|
|
|
|
const resultWithout = scoreRound(
|
|
|
|
|
challenge, botA, botB,
|
|
|
|
|
makeResponse('4', 500),
|
|
|
|
|
makeResponse('banana', 500),
|
|
|
|
|
null, 0, 0,
|
|
|
|
|
)
|
|
|
|
|
// The winner's damage should be roughly 2x with modifier
|
|
|
|
|
expect(resultWithMod.botADamage).toBeGreaterThan(resultWithout.botADamage * 1.5)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('arena modifier no effect on non-matching type', () => {
|
|
|
|
|
const challenge = makeChallenge({ type: 'riddle' })
|
|
|
|
|
const resultWithMod = scoreRound(
|
|
|
|
|
challenge, botA, botB,
|
|
|
|
|
makeResponse('4', 500),
|
|
|
|
|
makeResponse('banana', 500),
|
|
|
|
|
'roast_2x', 0, 0,
|
|
|
|
|
)
|
|
|
|
|
const resultWithout = scoreRound(
|
|
|
|
|
challenge, botA, botB,
|
|
|
|
|
makeResponse('4', 500),
|
|
|
|
|
makeResponse('banana', 500),
|
|
|
|
|
null, 0, 0,
|
|
|
|
|
)
|
|
|
|
|
expect(resultWithMod.botADamage).toBe(resultWithout.botADamage)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('combo multiplier increases damage', () => {
|
|
|
|
|
const challenge = makeChallenge()
|
|
|
|
|
const resultCombo = scoreRound(
|
|
|
|
|
challenge, botA, botB,
|
|
|
|
|
makeResponse('4', 500),
|
|
|
|
|
makeResponse('banana', 500),
|
|
|
|
|
null, 3, 0,
|
|
|
|
|
)
|
|
|
|
|
const resultNoCombo = scoreRound(
|
|
|
|
|
challenge, botA, botB,
|
|
|
|
|
makeResponse('4', 500),
|
|
|
|
|
makeResponse('banana', 500),
|
|
|
|
|
null, 0, 0,
|
|
|
|
|
)
|
|
|
|
|
expect(resultCombo.botADamage).toBeGreaterThan(resultNoCombo.botADamage)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('combo caps at 5', () => {
|
|
|
|
|
const challenge = makeChallenge()
|
|
|
|
|
const resultCombo5 = scoreRound(
|
|
|
|
|
challenge, botA, botB,
|
|
|
|
|
makeResponse('4', 500),
|
|
|
|
|
makeResponse('banana', 500),
|
|
|
|
|
null, 5, 0,
|
|
|
|
|
)
|
|
|
|
|
const resultCombo10 = scoreRound(
|
|
|
|
|
challenge, botA, botB,
|
|
|
|
|
makeResponse('4', 500),
|
|
|
|
|
makeResponse('banana', 500),
|
|
|
|
|
null, 10, 0,
|
|
|
|
|
)
|
|
|
|
|
expect(resultCombo5.botADamage).toBe(resultCombo10.botADamage)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('generates narration', () => {
|
|
|
|
|
const challenge = makeChallenge()
|
|
|
|
|
const result = scoreRound(
|
|
|
|
|
challenge, botA, botB,
|
|
|
|
|
makeResponse('4', 500),
|
|
|
|
|
makeResponse('banana', 500),
|
|
|
|
|
null, 0, 0,
|
|
|
|
|
)
|
|
|
|
|
expect(result.narration).toBeTruthy()
|
|
|
|
|
expect(typeof result.narration).toBe('string')
|
|
|
|
|
expect(result.narration.length).toBeGreaterThan(10)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('calculateElo', () => {
|
|
|
|
|
it('winner gains, loser loses', () => {
|
|
|
|
|
const { newWinnerElo, newLoserElo } = calculateElo(1200, 1200)
|
|
|
|
|
expect(newWinnerElo).toBeGreaterThan(1200)
|
|
|
|
|
expect(newLoserElo).toBeLessThan(1200)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('equal ELO — symmetric gain/loss', () => {
|
|
|
|
|
const { newWinnerElo, newLoserElo } = calculateElo(1200, 1200)
|
|
|
|
|
const gain = newWinnerElo - 1200
|
|
|
|
|
const loss = 1200 - newLoserElo
|
|
|
|
|
expect(Math.abs(gain - loss)).toBeLessThan(0.2)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('upset win — bigger gain for winner', () => {
|
|
|
|
|
const { newWinnerElo: gainUpset } = calculateElo(1000, 1500)
|
|
|
|
|
const { newWinnerElo: gainExpected } = calculateElo(1500, 1000)
|
|
|
|
|
const upsetGain = gainUpset - 1000
|
|
|
|
|
const expectedGain = gainExpected - 1500
|
|
|
|
|
expect(upsetGain).toBeGreaterThan(expectedGain)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('custom K factor', () => {
|
|
|
|
|
const k16 = calculateElo(1200, 1200, 16)
|
|
|
|
|
const k64 = calculateElo(1200, 1200, 64)
|
|
|
|
|
expect(k64.newWinnerElo - 1200).toBeGreaterThan(k16.newWinnerElo - 1200)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('extreme ELO difference (100 vs 2000)', () => {
|
|
|
|
|
const result = calculateElo(100, 2000)
|
|
|
|
|
expect(isFinite(result.newWinnerElo)).toBe(true)
|
|
|
|
|
expect(isFinite(result.newLoserElo)).toBe(true)
|
|
|
|
|
expect(isNaN(result.newWinnerElo)).toBe(false)
|
|
|
|
|
expect(isNaN(result.newLoserElo)).toBe(false)
|
|
|
|
|
// Upset winner gains big
|
|
|
|
|
expect(result.newWinnerElo - 100).toBeGreaterThan(25)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('never produces NaN or Infinity', () => {
|
|
|
|
|
const extremes = [
|
|
|
|
|
[0, 0], [0, 3000], [3000, 0], [1, 9999], [9999, 1],
|
|
|
|
|
]
|
|
|
|
|
for (const [w, l] of extremes) {
|
|
|
|
|
const result = calculateElo(w, l)
|
|
|
|
|
expect(isFinite(result.newWinnerElo)).toBe(true)
|
|
|
|
|
expect(isFinite(result.newLoserElo)).toBe(true)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('calculateTier', () => {
|
|
|
|
|
it('tier 0 — Baby (no wins)', () => {
|
|
|
|
|
expect(calculateTier(1200, 0)).toBe(0)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('tier 1 — Bronze (1+ wins)', () => {
|
|
|
|
|
expect(calculateTier(1000, 1)).toBe(1)
|
|
|
|
|
expect(calculateTier(1100, 2)).toBe(1)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('tier 2 — Silver (1200+ ELO, 3+ wins)', () => {
|
|
|
|
|
expect(calculateTier(1200, 3)).toBe(2)
|
|
|
|
|
expect(calculateTier(1200, 5)).toBe(2)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('tier 3 — Gold (1350+ ELO, 7+ wins)', () => {
|
|
|
|
|
expect(calculateTier(1350, 7)).toBe(3)
|
|
|
|
|
expect(calculateTier(1400, 10)).toBe(3)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('tier 4 — Platinum (1500+ ELO, 15+ wins)', () => {
|
|
|
|
|
expect(calculateTier(1500, 15)).toBe(4)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('tier 5 — Diamond (1700+ ELO, 25+ wins)', () => {
|
|
|
|
|
expect(calculateTier(1700, 25)).toBe(5)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('tier 6 — Legend (1900+ ELO, 40+ wins)', () => {
|
|
|
|
|
expect(calculateTier(1900, 40)).toBe(6)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('high ELO not enough without wins', () => {
|
|
|
|
|
// 2000 ELO but only 2 wins = Bronze
|
|
|
|
|
expect(calculateTier(2000, 2)).toBe(1)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('many wins not enough without ELO', () => {
|
|
|
|
|
// 1100 ELO, 50 wins = Bronze
|
|
|
|
|
expect(calculateTier(1100, 50)).toBe(1)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('boundary conditions', () => {
|
|
|
|
|
// Just below each threshold
|
|
|
|
|
expect(calculateTier(1199, 3)).toBe(1) // below Silver ELO
|
|
|
|
|
expect(calculateTier(1200, 2)).toBe(1) // below Silver wins
|
|
|
|
|
expect(calculateTier(1349, 7)).toBe(2) // below Gold ELO
|
|
|
|
|
expect(calculateTier(1350, 6)).toBe(2) // below Gold wins
|
|
|
|
|
expect(calculateTier(1499, 15)).toBe(3) // below Platinum ELO
|
|
|
|
|
expect(calculateTier(1500, 14)).toBe(3) // below Platinum wins
|
|
|
|
|
expect(calculateTier(1699, 25)).toBe(4) // below Diamond ELO
|
|
|
|
|
expect(calculateTier(1700, 24)).toBe(4) // below Diamond wins
|
|
|
|
|
expect(calculateTier(1899, 40)).toBe(5) // below Legend ELO
|
|
|
|
|
expect(calculateTier(1900, 39)).toBe(5) // below Legend wins
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-03-09 07:50:46 +00:00
|
|
|
|
|
|
|
|
describe('scoreRound performance', () => {
|
|
|
|
|
it('completes 1000 rounds in under 100ms (<0.1ms each)', () => {
|
|
|
|
|
const challenge = makeChallenge()
|
|
|
|
|
const botA = { id: 'a1', name: 'AlphaBot' }
|
|
|
|
|
const botB = { id: 'b1', name: 'BetaBot' }
|
|
|
|
|
|
|
|
|
|
const start = performance.now()
|
|
|
|
|
for (let i = 0; i < 1000; i++) {
|
|
|
|
|
scoreRound(
|
|
|
|
|
challenge, botA, botB,
|
|
|
|
|
makeResponse('4', 200 + i),
|
|
|
|
|
makeResponse('banana', 500 + i),
|
|
|
|
|
null, i % 6, 0,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
const elapsed = performance.now() - start
|
|
|
|
|
expect(elapsed).toBeLessThan(100) // <0.1ms per call
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-03-09 07:56:51 +00:00
|
|
|
|
|
|
|
|
describe('creative scoring spam detection', () => {
|
|
|
|
|
const botA = { id: 'a1', name: 'AlphaBot' }
|
|
|
|
|
const botB = { id: 'b1', name: 'BetaBot' }
|
|
|
|
|
const creative = makeChallenge({
|
|
|
|
|
answers: undefined,
|
|
|
|
|
scoring: 'creative',
|
|
|
|
|
type: 'roast_battle',
|
|
|
|
|
prompt: 'Write a two-sentence roast of JavaScript',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('repeated phrase answer loses to quality answer', () => {
|
|
|
|
|
const result = scoreRound(
|
|
|
|
|
creative, botA, botB,
|
|
|
|
|
makeResponse('lol lol lol lol lol lol lol lol lol lol', 300),
|
|
|
|
|
makeResponse('Your code is so bad even ChatGPT refuses to debug it. Every function you write is a monument to incompetence.', 300),
|
|
|
|
|
null, 0, 0,
|
|
|
|
|
)
|
|
|
|
|
expect(result.winnerId).toBe('b1')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('question echo answer scores low', () => {
|
|
|
|
|
const result = scoreRound(
|
|
|
|
|
creative, botA, botB,
|
|
|
|
|
makeResponse('Write a two-sentence roast of JavaScript', 300),
|
|
|
|
|
makeResponse('JavaScript has more callbacks than a desperate ex. Even its creators apologize for it.', 300),
|
|
|
|
|
null, 0, 0,
|
|
|
|
|
)
|
|
|
|
|
expect(result.winnerId).toBe('b1')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('all-caps spam scores lower than normal text', () => {
|
|
|
|
|
const result = scoreRound(
|
|
|
|
|
creative, botA, botB,
|
|
|
|
|
makeResponse('THIS IS ALL CAPS AND IT IS VERY ANNOYING AND NOT CREATIVE AT ALL', 300),
|
|
|
|
|
makeResponse('Your framework choices make me question if you have taste or just throw darts at a list.', 300),
|
|
|
|
|
null, 0, 0,
|
|
|
|
|
)
|
|
|
|
|
expect(result.winnerId).toBe('b1')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('very short creative answer loses to longer quality answer', () => {
|
|
|
|
|
const result = scoreRound(
|
|
|
|
|
creative, botA, botB,
|
|
|
|
|
makeResponse('ok', 300),
|
|
|
|
|
makeResponse('Your code is so bad the compiler files a restraining order every time you open an IDE.', 300),
|
|
|
|
|
null, 0, 0,
|
|
|
|
|
)
|
|
|
|
|
expect(result.winnerId).toBe('b1')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('legitimate short creative answer still gets reasonable score', () => {
|
|
|
|
|
const result = scoreRound(
|
|
|
|
|
creative, botA, botB,
|
|
|
|
|
makeResponse('Your code has more bugs than a rainforest. Even Stack Overflow gave up on you.', 200),
|
|
|
|
|
makeResponse('You write code like a poet writes math: beautifully wrong in every conceivable way.', 400),
|
|
|
|
|
null, 0, 0,
|
|
|
|
|
)
|
|
|
|
|
// Both should get reasonable scores (not zeroed)
|
|
|
|
|
expect(result.botAScore).toBeGreaterThan(2)
|
|
|
|
|
expect(result.botBScore).toBeGreaterThan(2)
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-03-09 08:00:06 +00:00
|
|
|
|
|
|
|
|
describe('narrations', () => {
|
|
|
|
|
const allTypes = [
|
|
|
|
|
'speed_blitz', 'math_blitz', 'riddle', 'hallucination_check', 'trap_card',
|
|
|
|
|
'magic_duel', 'sports_showdown', 'vehicle_mayhem', 'nature_clash', 'animal_kingdom',
|
|
|
|
|
'hack_battle', 'roast_battle', 'creative_writing', 'meme_war', 'code_golf', 'wrestling_match',
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
it('all 16 challenge types produce varied narrations', () => {
|
|
|
|
|
const botA = { id: 'a1', name: 'AlphaBot' }
|
|
|
|
|
const botB = { id: 'b1', name: 'BetaBot' }
|
|
|
|
|
|
|
|
|
|
for (const type of allTypes) {
|
|
|
|
|
const challenge = makeChallenge({ type, scoring: 'factual' })
|
|
|
|
|
const narrations = new Set<string>()
|
|
|
|
|
for (let i = 0; i < 20; i++) {
|
|
|
|
|
const result = scoreRound(
|
|
|
|
|
challenge, botA, botB,
|
|
|
|
|
makeResponse('4', 500),
|
|
|
|
|
makeResponse('banana', 500),
|
|
|
|
|
null, 0, 0,
|
|
|
|
|
)
|
|
|
|
|
narrations.add(result.narration)
|
|
|
|
|
}
|
|
|
|
|
// Should have variety (>1 unique narration across 20 rounds)
|
|
|
|
|
expect(narrations.size).toBeGreaterThan(1)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})
|