- 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.
118 lines
3.8 KiB
Python
118 lines
3.8 KiB
Python
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,
|
|
}
|