152 lines
3.9 KiB
TypeScript
152 lines
3.9 KiB
TypeScript
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)
|
||
|
|
})
|
||
|
|
})
|