from __future__ import annotations from decimal import Decimal from app.slots.base import BaseSlot from app.slots.rng import Rng from app.slots.types import Grid, PaylineWin, SessionMetaState, SpinOutcome REELS = 5 ROWS = 5 LEAF = "leaf" MUSHROOM = "mushroom" PINE = "pine" DEER = "deer" FOX = "fox" OAK = "oak" WILD = "wild" SCATTER = "scatter" SYMBOLS = (LEAF, MUSHROOM, PINE, DEER, FOX, OAK, WILD, SCATTER) # Weights per reel (index aligns with SYMBOLS) REEL_WEIGHTS: list[list[float]] = [ [28, 24, 18, 12, 8, 5, 3, 2], [26, 24, 18, 12, 8, 6, 4, 2], [24, 22, 18, 14, 10, 6, 4, 2], [26, 24, 18, 12, 8, 6, 4, 2], [28, 24, 18, 12, 8, 5, 3, 2], ] # Multipliers of total spin bet PAYTABLE: dict[str, dict[int, Decimal]] = { LEAF: {3: Decimal("0.20"), 4: Decimal("0.50"), 5: Decimal("1.50")}, MUSHROOM: {3: Decimal("0.25"), 4: Decimal("0.60"), 5: Decimal("2.00")}, PINE: {3: Decimal("0.35"), 4: Decimal("0.80"), 5: Decimal("2.50")}, DEER: {3: Decimal("0.50"), 4: Decimal("1.20"), 5: Decimal("4.00")}, FOX: {3: Decimal("0.70"), 4: Decimal("1.80"), 5: Decimal("6.00")}, OAK: {3: Decimal("1.00"), 4: Decimal("3.00"), 5: Decimal("10.00")}, WILD: {3: Decimal("1.50"), 4: Decimal("4.00"), 5: Decimal("15.00")}, } SCATTER_TABLE: dict[int, Decimal] = { 3: Decimal("1.00"), 4: Decimal("3.00"), 5: Decimal("8.00"), } # Cap scatter pay at 5+ using the 5-table entry; more scatters still use 5 rate SCATTER_TRIGGER = 3 PAYLINES: list[list[int]] = [ [0, 0, 0, 0, 0], [1, 1, 1, 1, 1], [2, 2, 2, 2, 2], [3, 3, 3, 3, 3], [4, 4, 4, 4, 4], [0, 1, 2, 3, 4], [4, 3, 2, 1, 0], [0, 0, 1, 0, 0], [4, 4, 3, 4, 4], [1, 2, 3, 2, 1], [3, 2, 1, 2, 3], [0, 1, 0, 1, 0], [4, 3, 4, 3, 4], [2, 1, 0, 1, 2], [2, 3, 4, 3, 2], [0, 1, 2, 1, 0], [4, 3, 2, 3, 4], [1, 1, 2, 3, 3], [3, 3, 2, 1, 1], [0, 2, 4, 2, 0], ] def _line_symbols(grid: Grid, path: list[int]) -> list[str]: return [grid[reel][path[reel]] for reel in range(REELS)] def evaluate_payline(symbols: list[str]) -> tuple[str, int] | None: """Return (symbol, count) for best left-to-right win, or None.""" if not symbols: return None # All wilds if all(s == WILD for s in symbols): return WILD, len(symbols) # Find target symbol: first non-wild, non-scatter from the left target: str | None = None for s in symbols: if s == SCATTER: break if s != WILD: target = s break if target is None: # Leading wilds then scatter/end — pay as wild streak only if length >= 3 count = 0 for s in symbols: if s == WILD: count += 1 else: break if count >= 3: return WILD, count return None count = 0 for s in symbols: if s == SCATTER: break if s == target or s == WILD: count += 1 else: break if count >= 3: return target, count return None def scatter_pay_key(count: int) -> int | None: if count < 3: return None return min(count, 5) class ForestFortuneSlot(BaseSlot): slug = "forest-fortune" title = "Лесная Удача" description = "5×5 лесной слот. В пачке спинов духи поднимают множитель." status = "available" min_bet = Decimal("1.00") max_bet = Decimal("1000.00") min_spin_count = 1 max_spin_count = 1000 def build_grid(self, rng: Rng) -> Grid: grid: Grid = [] for reel in range(REELS): column = [ rng.choices(list(SYMBOLS), weights=REEL_WEIGHTS[reel], k=1)[0] for _ in range(ROWS) ] grid.append(column) return grid def evaluate(self, grid: Grid, bet: Decimal, multiplier: int) -> SpinOutcome: line_wins: list[PaylineWin] = [] for line_index, path in enumerate(PAYLINES): symbols = _line_symbols(grid, path) result = evaluate_payline(symbols) if result is None: continue symbol, count = result rate = PAYTABLE.get(symbol, {}).get(count) if rate is None: continue amount = (bet * rate * Decimal(multiplier)).quantize(Decimal("0.01")) line_wins.append( PaylineWin( line_index=line_index, symbol=symbol, count=count, amount=amount, path=list(path), ) ) scatter_count = sum(1 for reel in grid for cell in reel if cell == SCATTER) scatter_win = Decimal("0.00") key = scatter_pay_key(scatter_count) if key is not None: rate = SCATTER_TABLE[key] scatter_win = (bet * rate * Decimal(multiplier)).quantize(Decimal("0.01")) total = sum((w.amount for w in line_wins), Decimal("0.00")) + scatter_win return SpinOutcome( grid=grid, line_wins=line_wins, scatter_count=scatter_count, scatter_win=scatter_win, total_win=total.quantize(Decimal("0.01")), meta_triggered=scatter_count >= SCATTER_TRIGGER, ) def session_meta_on_spin(self, state: SessionMetaState, outcome: SpinOutcome) -> SessionMetaState: if outcome.meta_triggered: return state.after_trigger() return SessionMetaState(level=state.level, multiplier=state.multiplier, triggered=False)