Add spin session functionality and update game mechanics
- Introduced a new `spin_sessions` table to track user spin activities. - Updated the API to include a new endpoint for spinning games, allowing users to place bets and specify spin counts. - Enhanced game information structure and added validation for bets and spin counts. - Implemented the `Forest Fortune` slot game mechanics, including payline evaluation and scatter functionality. - Updated dependencies in `pyproject.toml` for development and added `httpx` for HTTP requests. - Added unit tests for the new slot mechanics and API endpoints to ensure functionality and reliability.
This commit is contained in:
149
tests/test_forest_fortune.py
Normal file
149
tests/test_forest_fortune.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user