193 lines
7.4 KiB
Python
193 lines
7.4 KiB
Python
"""
|
|
tests/test_predictor.py
|
|
------------------------
|
|
Tests for core/predictor.py.
|
|
Verifies that every strategy:
|
|
• returns exactly main_count unique numbers
|
|
• all numbers within 1..main_max
|
|
• numbers are sorted
|
|
• bonus within 1..bonus_max (or None when bonus_count == 0)
|
|
• works on an empty DB (random fallback)
|
|
• works on a populated DB
|
|
"""
|
|
|
|
import pytest
|
|
from db.models import get_game_by_name, insert_draw
|
|
from core.predictor import (
|
|
hot_numbers,
|
|
due_numbers,
|
|
weighted_random,
|
|
monte_carlo,
|
|
positional_pick,
|
|
)
|
|
|
|
# ── Shared helpers ────────────────────────────────────────────────────────────
|
|
|
|
_DRAWS = [
|
|
("2024-01-01", [1, 13, 36, 61, 69], 7),
|
|
("2024-01-03", [1, 2, 13, 45, 69], 15),
|
|
("2024-01-05", [2, 13, 22, 36, 55], 3),
|
|
("2024-01-08", [5, 18, 33, 50, 65], 22),
|
|
("2024-01-10", [7, 14, 28, 42, 60], 11),
|
|
]
|
|
|
|
|
|
@pytest.fixture
|
|
def pb_game(tmp_db):
|
|
game = get_game_by_name("Powerball")
|
|
for date, nums, bonus in _DRAWS:
|
|
insert_draw(game["id"], date, nums, bonus=bonus, source="test")
|
|
return game
|
|
|
|
|
|
def _assert_valid(result, game):
|
|
"""Shared validity assertions for any strategy output."""
|
|
nums = result["numbers"]
|
|
bonus = result["bonus"]
|
|
|
|
assert isinstance(nums, list), "numbers must be a list"
|
|
assert len(nums) == game["main_count"], f"expected {game['main_count']} numbers, got {len(nums)}"
|
|
assert nums == sorted(nums), "numbers must be sorted"
|
|
assert len(set(nums)) == len(nums), "numbers must be unique"
|
|
assert all(1 <= n <= game["main_max"] for n in nums), "all numbers must be in 1..main_max"
|
|
|
|
if game["bonus_count"] > 0:
|
|
assert bonus is not None, "bonus must not be None"
|
|
assert 1 <= bonus <= game["bonus_max"], "bonus out of range"
|
|
else:
|
|
assert bonus is None, "bonus should be None when bonus_count == 0"
|
|
|
|
|
|
# ── Hot Numbers ───────────────────────────────────────────────────────────────
|
|
|
|
def test_hot_numbers_valid(pb_game):
|
|
result = hot_numbers(pb_game["id"])
|
|
_assert_valid(result, get_game_by_name("Powerball"))
|
|
|
|
|
|
def test_hot_numbers_picks_most_frequent(pb_game):
|
|
result = hot_numbers(pb_game["id"])
|
|
# 13 appears in all 5 draws — must be included
|
|
assert 13 in result["numbers"]
|
|
|
|
|
|
def test_hot_numbers_last_n_respected(pb_game):
|
|
# last_n=1 → only draw 5: [7,14,28,42,60]
|
|
result = hot_numbers(pb_game["id"], last_n=1)
|
|
assert set(result["numbers"]) == {7, 14, 28, 42, 60}
|
|
|
|
|
|
def test_hot_numbers_empty_db_fallback(tmp_db):
|
|
game = get_game_by_name("Powerball")
|
|
result = hot_numbers(game["id"])
|
|
_assert_valid(result, game)
|
|
|
|
|
|
# ── Due Numbers ───────────────────────────────────────────────────────────────
|
|
|
|
def test_due_numbers_valid(pb_game):
|
|
result = due_numbers(pb_game["id"])
|
|
_assert_valid(result, get_game_by_name("Powerball"))
|
|
|
|
|
|
def test_due_numbers_picks_high_gap(pb_game):
|
|
result = due_numbers(pb_game["id"])
|
|
# Numbers that never appeared have gap = total draws (5)
|
|
# and should be favoured; at minimum, recently appearing numbers
|
|
# (gap=0) should NOT all dominate the ticket.
|
|
# Verify the ticket is valid (structure is the key assertion here).
|
|
assert len(result["numbers"]) == 5
|
|
|
|
|
|
def test_due_numbers_empty_db_fallback(tmp_db):
|
|
game = get_game_by_name("Powerball")
|
|
result = due_numbers(game["id"])
|
|
_assert_valid(result, game)
|
|
|
|
|
|
# ── Weighted Random ───────────────────────────────────────────────────────────
|
|
|
|
def test_weighted_random_valid(pb_game):
|
|
result = weighted_random(pb_game["id"])
|
|
_assert_valid(result, get_game_by_name("Powerball"))
|
|
|
|
|
|
def test_weighted_random_empty_db_still_valid(tmp_db):
|
|
# No history → all weights equal to 1; should still produce valid ticket
|
|
game = get_game_by_name("Powerball")
|
|
result = weighted_random(game["id"])
|
|
_assert_valid(result, game)
|
|
|
|
|
|
def test_weighted_random_different_runs(pb_game):
|
|
# Two runs are almost certainly different (1-in-C(69,5) ≈ 1-in-11M chance of collision)
|
|
r1 = weighted_random(pb_game["id"])
|
|
r2 = weighted_random(pb_game["id"])
|
|
# Validate both; don't assert inequality (astronomically unlikely to collide)
|
|
_assert_valid(r1, get_game_by_name("Powerball"))
|
|
_assert_valid(r2, get_game_by_name("Powerball"))
|
|
|
|
|
|
# ── Monte Carlo ───────────────────────────────────────────────────────────────
|
|
|
|
def test_monte_carlo_valid(pb_game):
|
|
result = monte_carlo(pb_game["id"], simulations=200)
|
|
_assert_valid(result, get_game_by_name("Powerball"))
|
|
|
|
|
|
def test_monte_carlo_empty_db_still_valid(tmp_db):
|
|
game = get_game_by_name("Powerball")
|
|
result = monte_carlo(game["id"], simulations=100)
|
|
_assert_valid(result, game)
|
|
|
|
|
|
def test_monte_carlo_favours_frequent_numbers(pb_game):
|
|
# 13 appears in 4/5 draws — over many simulations it should be selected often.
|
|
# Run with enough simulations to make this deterministic.
|
|
result = monte_carlo(pb_game["id"], simulations=5000)
|
|
assert 13 in result["numbers"], "Monte Carlo should pick 13 (appears in 4/5 draws)"
|
|
|
|
|
|
# ── Positional Pick ───────────────────────────────────────────────────────────
|
|
|
|
def test_positional_pick_valid(pb_game):
|
|
result = positional_pick(pb_game["id"])
|
|
_assert_valid(result, get_game_by_name("Powerball"))
|
|
|
|
|
|
def test_positional_pick_no_duplicates(pb_game):
|
|
# Each position contributes a unique number even if the same number
|
|
# is the most frequent at multiple positions.
|
|
result = positional_pick(pb_game["id"])
|
|
assert len(set(result["numbers"])) == len(result["numbers"])
|
|
|
|
|
|
def test_positional_pick_empty_db_fallback(tmp_db):
|
|
game = get_game_by_name("Powerball")
|
|
result = positional_pick(game["id"])
|
|
_assert_valid(result, game)
|
|
|
|
|
|
# ── Mega Millions (no-bonus_count check is N/A; both games have bonus) ────────
|
|
|
|
def test_all_strategies_valid_for_megamillions(tmp_db):
|
|
mm = get_game_by_name("Mega Millions")
|
|
for date, nums, bonus in _DRAWS:
|
|
insert_draw(mm["id"], date, nums, bonus=bonus, source="test")
|
|
|
|
for fn in (hot_numbers, due_numbers, weighted_random,
|
|
lambda gid: monte_carlo(gid, simulations=100),
|
|
positional_pick):
|
|
result = fn(mm["id"])
|
|
_assert_valid(result, mm)
|
|
|
|
|
|
# ── Multiple tickets ──────────────────────────────────────────────────────────
|
|
|
|
def test_generate_multiple_tickets(pb_game):
|
|
game = get_game_by_name("Powerball")
|
|
tickets = [hot_numbers(pb_game["id"]) for _ in range(5)]
|
|
assert len(tickets) == 5
|
|
for t in tickets:
|
|
_assert_valid(t, game)
|