"""Unit tests for Forest Fortune slot mechanics and session meta.""" from __future__ import annotations import unittest from decimal import Decimal from app.slots.forest_fortune import ( FOX, OAK, SCATTER, WILD, ForestFortuneSlot, evaluate_payline, ) from app.slots.rng import seeded_rng from app.slots.types import SessionMetaState, SpinOutcome class PaylineTests(unittest.TestCase): def test_three_of_kind(self) -> None: self.assertEqual(evaluate_payline([OAK, OAK, OAK, FOX, OAK]), (OAK, 3)) def test_break_stops_chain(self) -> None: self.assertIsNone(evaluate_payline([OAK, OAK, FOX, OAK, OAK])) def test_wild_extends(self) -> None: self.assertEqual(evaluate_payline([OAK, WILD, OAK, FOX, FOX]), (OAK, 3)) def test_scatter_breaks_line(self) -> None: self.assertIsNone(evaluate_payline([OAK, OAK, SCATTER, OAK, OAK])) def test_all_wilds(self) -> None: self.assertEqual(evaluate_payline([WILD, WILD, WILD, WILD, WILD]), (WILD, 5)) class SessionMetaTests(unittest.TestCase): def setUp(self) -> None: self.slot = ForestFortuneSlot() def _forced_outcome(self, scatter_count: int, multiplier: int = 1) -> SpinOutcome: grid = [["leaf"] * 5 for _ in range(5)] placed = 0 for reel in range(5): for row in range(5): if placed < scatter_count: grid[reel][row] = SCATTER placed += 1 return self.slot.evaluate(grid, Decimal("10.00"), multiplier) def test_single_spin_no_level_up_effect(self) -> None: # Even with scatters, spin_count=1 keeps meta disabled in play_session class ForceRng: def __init__(self) -> None: self.n = 0 def random(self) -> float: return 0.5 def choices(self, population, weights=None, *, k=1): # Force many scatters return [SCATTER] * k result = self.slot.play_session(Decimal("1.00"), 1, rng=ForceRng()) self.assertEqual(result.spin_count, 1) self.assertEqual(result.max_level, 1) self.assertEqual(result.trigger_count, 0) self.assertFalse(result.spins[0].meta_triggered) def test_pack_trigger_applies_to_next_spins(self) -> None: scatters_on = {4} # 0-based index 4 => 5th spin class ScriptedRng: def __init__(self) -> None: self.spin = -1 self.cell = 0 def random(self) -> float: return 0.5 def choices(self, population, weights=None, *, k=1): # New spin starts every 25 cells if self.cell % 25 == 0: self.spin += 1 self.cell += 1 if self.spin in scatters_on: # first 3 cells of that spin are scatters, rest leaf pos = (self.cell - 1) % 25 return [SCATTER if pos < 3 else "leaf"] * k return ["leaf"] * k result = self.slot.play_session(Decimal("1.00"), 10, rng=ScriptedRng()) self.assertEqual(result.trigger_count, 1) self.assertEqual(result.max_level, 2) # spins 0..4 at level 1; spin 4 triggers; spins 5..9 at level 2 self.assertEqual(result.spins[4].level, 1) self.assertTrue(result.spins[4].meta_triggered) self.assertEqual(result.spins[5].level, 2) self.assertEqual(result.spins[5].multiplier, 2) self.assertEqual(result.spins[9].multiplier, 2) def test_second_trigger_triples(self) -> None: scatters_on = {2, 5} class ScriptedRng: def __init__(self) -> None: self.spin = -1 self.cell = 0 def random(self) -> float: return 0.5 def choices(self, population, weights=None, *, k=1): if self.cell % 25 == 0: self.spin += 1 self.cell += 1 if self.spin in scatters_on: pos = (self.cell - 1) % 25 return [SCATTER if pos < 3 else "leaf"] * k return ["leaf"] * k result = self.slot.play_session(Decimal("1.00"), 8, rng=ScriptedRng()) self.assertEqual(result.trigger_count, 2) self.assertEqual(result.max_level, 3) self.assertEqual(result.spins[3].multiplier, 2) self.assertEqual(result.spins[6].multiplier, 3) def test_evaluate_scatter_pay_and_trigger_flag(self) -> None: outcome = self._forced_outcome(3, multiplier=2) self.assertTrue(outcome.meta_triggered) self.assertEqual(outcome.scatter_count, 3) self.assertEqual(outcome.scatter_win, Decimal("20.00")) # 10 * 1.00 * 2 def test_meta_state_after_trigger(self) -> None: state = SessionMetaState() outcome = self._forced_outcome(3) nxt = self.slot.session_meta_on_spin(state, outcome) self.assertEqual(nxt.level, 2) self.assertEqual(nxt.multiplier, 2) self.assertTrue(nxt.triggered) def test_seeded_session_runs(self) -> None: result = self.slot.play_session(Decimal("5.00"), 5, rng=seeded_rng(42)) self.assertEqual(len(result.spins), 5) self.assertEqual(result.total_bet, Decimal("25.00")) if __name__ == "__main__": unittest.main()