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:
192
app/slots/forest_fortune.py
Normal file
192
app/slots/forest_fortune.py
Normal file
@@ -0,0 +1,192 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user