from __future__ import annotations from abc import ABC, abstractmethod from decimal import Decimal from app.slots.rng import Rng, system_rng from app.slots.types import Grid, SessionMetaState, SessionResult, SpinOutcome, SpinResult class BaseSlot(ABC): slug: str title: str description: str status: str = "coming_soon" min_bet: Decimal = Decimal("1.00") max_bet: Decimal = Decimal("1000.00") min_spin_count: int = 1 max_spin_count: int = 1000 @abstractmethod def build_grid(self, rng: Rng) -> Grid: raise NotImplementedError @abstractmethod def evaluate(self, grid: Grid, bet: Decimal, multiplier: int) -> SpinOutcome: raise NotImplementedError @abstractmethod def session_meta_on_spin(self, state: SessionMetaState, outcome: SpinOutcome) -> SessionMetaState: raise NotImplementedError def validate_bet(self, bet: Decimal) -> None: if bet < self.min_bet or bet > self.max_bet: raise ValueError(f"Bet must be between {self.min_bet} and {self.max_bet}") def validate_spin_count(self, spin_count: int) -> None: if spin_count < self.min_spin_count or spin_count > self.max_spin_count: raise ValueError( f"spin_count must be between {self.min_spin_count} and {self.max_spin_count}" ) def play_session( self, bet: Decimal, spin_count: int, rng: Rng | None = None, ) -> SessionResult: if self.status != "available": raise RuntimeError(f"Game '{self.slug}' is not available") bet = bet.quantize(Decimal("0.01")) self.validate_bet(bet) self.validate_spin_count(spin_count) rng = rng or system_rng() meta = SessionMetaState() spins: list[SpinResult] = [] total_win = Decimal("0.00") trigger_count = 0 max_level = 1 meta_enabled = spin_count > 1 for index in range(spin_count): level = meta.level multiplier = meta.multiplier grid = self.build_grid(rng) outcome = self.evaluate(grid, bet, multiplier) if meta_enabled and outcome.meta_triggered: next_meta = self.session_meta_on_spin(meta, outcome) if next_meta.triggered: trigger_count += 1 meta = SessionMetaState( level=next_meta.level, multiplier=next_meta.multiplier, triggered=False, ) max_level = max(max_level, meta.level) triggered = True else: triggered = False spin = SpinResult( index=index, grid=outcome.grid, line_wins=outcome.line_wins, scatter_count=outcome.scatter_count, scatter_win=outcome.scatter_win, total_win=outcome.total_win, level=level, multiplier=multiplier, meta_triggered=triggered, ) spins.append(spin) total_win += outcome.total_win return SessionResult( bet=bet, spin_count=spin_count, total_bet=(bet * spin_count).quantize(Decimal("0.01")), total_win=total_win.quantize(Decimal("0.01")), max_level=max_level, trigger_count=trigger_count, spins=spins, ) def info(self) -> dict: return { "slug": self.slug, "title": self.title, "description": self.description, "status": self.status, "min_bet": self.min_bet, "max_bet": self.max_bet, "min_spin_count": self.min_spin_count, "max_spin_count": self.max_spin_count, }