05/23 Phase 5
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user