import { describe, it, expect } from 'vitest' import { eloProbability, calculateOdds, oddsToFractional, oddsToAmerican, calculatePayout, validateBet, } from './odds.js' describe('eloProbability', () => { it('equal ELO gives 50/50', () => { expect(eloProbability(1200, 1200)).toBeCloseTo(0.5, 5) }) it('higher ELO favored', () => { expect(eloProbability(1400, 1200)).toBeGreaterThan(0.7) }) it('lower ELO underdog', () => { expect(eloProbability(1000, 1400)).toBeLessThan(0.15) }) it('probabilities are complementary', () => { const pA = eloProbability(1300, 1100) const pB = eloProbability(1100, 1300) expect(pA + pB).toBeCloseTo(1, 5) }) it('extreme ELO difference stays finite', () => { const p = eloProbability(100, 2500) expect(p).toBeGreaterThan(0) expect(p).toBeLessThan(0.001) expect(isFinite(p)).toBe(true) }) }) describe('calculateOdds', () => { it('equal ELO gives near-even odds', () => { const odds = calculateOdds(1200, 1200) expect(odds.botAWinProb).toBeCloseTo(0.5, 1) expect(odds.botBWinProb).toBeCloseTo(0.5, 1) expect(odds.botADecimalOdds).toBeCloseTo(2.0, 0) }) it('house edge reduces payout below fair odds', () => { const odds = calculateOdds(1200, 1200, { houseEdge: 0.05 }) expect(odds.botAPayoutMultiplier).toBeLessThan(odds.botADecimalOdds) expect(odds.botBPayoutMultiplier).toBeLessThan(odds.botBDecimalOdds) }) it('favorite has lower payout multiplier', () => { const odds = calculateOdds(1500, 1200) expect(odds.botAPayoutMultiplier).toBeLessThan(odds.botBPayoutMultiplier) }) it('streak adjusts probability', () => { const noStreak = calculateOdds(1200, 1200) const withStreak = calculateOdds(1200, 1200, { streakA: 5 }) expect(withStreak.botAWinProb).toBeGreaterThan(noStreak.botAWinProb) }) it('probabilities always sum to 1', () => { const odds = calculateOdds(1600, 1000, { streakA: 3, streakB: 1 }) expect(odds.botAWinProb + odds.botBWinProb).toBeCloseTo(1, 2) }) it('probabilities capped between 2% and 98%', () => { const odds = calculateOdds(3000, 500, { streakA: 5 }) expect(odds.botAWinProb).toBeLessThanOrEqual(0.98) expect(odds.botBWinProb).toBeGreaterThanOrEqual(0.02) }) it('spread reflects ELO difference', () => { const odds = calculateOdds(1500, 1200) expect(odds.spread).toBe(300) }) }) describe('oddsToFractional', () => { it('even money', () => { expect(oddsToFractional(2.0)).toBe('1/1') }) it('3/1', () => { expect(oddsToFractional(4.0)).toBe('3/1') }) it('1/2', () => { expect(oddsToFractional(1.5)).toBe('1/2') }) }) describe('oddsToAmerican', () => { it('underdog shows positive', () => { expect(oddsToAmerican(3.0)).toBe('+200') }) it('favorite shows negative', () => { expect(oddsToAmerican(1.5)).toBe('-200') }) it('even money', () => { expect(oddsToAmerican(2.0)).toBe('+100') }) }) describe('calculatePayout', () => { it('100 sats at 2x = 200', () => { expect(calculatePayout(100, 2.0)).toBe(200) }) it('floors to integer sats', () => { expect(calculatePayout(100, 1.94)).toBe(194) }) }) describe('validateBet', () => { it('valid bet', () => { expect(validateBet(1000).valid).toBe(true) }) it('below minimum', () => { const r = validateBet(50) expect(r.valid).toBe(false) expect(r.error).toContain('Minimum') }) it('above maximum', () => { const r = validateBet(200_000) expect(r.valid).toBe(false) expect(r.error).toContain('Maximum') }) it('zero is invalid', () => { expect(validateBet(0).valid).toBe(false) }) it('negative is invalid', () => { expect(validateBet(-100).valid).toBe(false) }) it('non-integer is invalid', () => { expect(validateBet(100.5).valid).toBe(false) }) it('custom limits', () => { expect(validateBet(50, 10, 100).valid).toBe(true) expect(validateBet(5, 10, 100).valid).toBe(false) }) })