first commit
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
from decimal import Decimal
|
||||
from typing import Iterable, Mapping, Sequence
|
||||
|
||||
from .config import (
|
||||
DEFAULT_MODE,
|
||||
MAX_SYMBOL_COUNT,
|
||||
MAX_WIN_MULTIPLIER,
|
||||
MIN_WIN_COUNT,
|
||||
MODES,
|
||||
MULTIPLIER_VALUES,
|
||||
SYMBOL_RULES,
|
||||
GameMode,
|
||||
)
|
||||
from .models import Bomb, CascadeResult, Elimination, SessionState, SpinResult
|
||||
|
||||
|
||||
class RuleViolation(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class OlympusEngine:
|
||||
def __init__(
|
||||
self,
|
||||
bet: Decimal | int | str = Decimal("1"),
|
||||
mode: str = DEFAULT_MODE,
|
||||
) -> None:
|
||||
if mode not in MODES:
|
||||
raise RuleViolation(f"未知模式: {mode},可选: {sorted(MODES)}")
|
||||
self.mode: GameMode = MODES[mode]
|
||||
self.bet = Decimal(str(bet))
|
||||
if self.bet <= 0:
|
||||
raise RuleViolation("bet 必须大于 0")
|
||||
# 档位校验:整数档位必须在模式允许列表内(非整数 bet 仅用于单测/模拟,不校验)
|
||||
if self.bet == self.bet.to_integral_value() and int(self.bet) not in self.mode.base_bets:
|
||||
raise RuleViolation(
|
||||
f"模式 {self.mode.code} 不支持下注档位 {int(self.bet)},可选: {self.mode.base_bets}"
|
||||
)
|
||||
self.cost = self.bet * self.mode.cost_multiplier # 实际扣费
|
||||
self.max_win = self.bet * MAX_WIN_MULTIPLIER # 单局封顶 1000x 基础下注
|
||||
self.state = SessionState()
|
||||
self._cascades: list[CascadeResult] | None = None
|
||||
self._symbol_counts: Counter[str] = Counter()
|
||||
self._spin_base_win = Decimal("0")
|
||||
self._bombs: dict[str, Bomb] = {}
|
||||
self._is_free_spin = False
|
||||
|
||||
def buy_free_spins_allowed(self) -> bool:
|
||||
"""当前模式是否允许购买免费旋转(底注与购买互斥)。"""
|
||||
return self.mode.can_buy_free_spins
|
||||
|
||||
def ante_boost_active(self) -> bool:
|
||||
"""当前模式是否启用底注效果(购买免费旋转模式下强制关闭)。"""
|
||||
return self.mode.ante_boost
|
||||
|
||||
def begin_spin(self, is_free_spin: bool = False) -> int:
|
||||
"""is_free_spin=True 表示免费旋转:炸弹进入累计器;基础旋转炸弹仅对本旋生效。"""
|
||||
if self._cascades is not None:
|
||||
raise RuleViolation("当前回合尚未结束")
|
||||
self._cascades = []
|
||||
self._symbol_counts.clear()
|
||||
self._spin_base_win = Decimal("0")
|
||||
self._bombs.clear()
|
||||
self._is_free_spin = is_free_spin
|
||||
return self.state.next_spin_index
|
||||
|
||||
def record_cascade(
|
||||
self,
|
||||
symbols: Mapping[str, Sequence[int]] | None = None,
|
||||
bombs: Iterable[Bomb] = (),
|
||||
) -> CascadeResult:
|
||||
if self._cascades is None:
|
||||
raise RuleViolation("请先调用 begin_spin")
|
||||
|
||||
eliminations = []
|
||||
cascade_base_win = Decimal("0")
|
||||
for symbol, raw_positions in (symbols or {}).items():
|
||||
if symbol not in SYMBOL_RULES:
|
||||
raise RuleViolation(f"未知符号: {symbol}")
|
||||
positions = tuple(raw_positions)
|
||||
count = len(positions)
|
||||
if count < MIN_WIN_COUNT or count > MAX_SYMBOL_COUNT:
|
||||
raise RuleViolation(f"{symbol} 消除数量必须在 {MIN_WIN_COUNT}-{MAX_SYMBOL_COUNT} 之间")
|
||||
if len(set(positions)) != count:
|
||||
raise RuleViolation(f"{symbol} 的消除位置不能重复")
|
||||
|
||||
payout_multiplier = SYMBOL_RULES[symbol].payout_for(count)
|
||||
elimination = Elimination(
|
||||
symbol=symbol,
|
||||
count=count,
|
||||
positions=positions,
|
||||
payout_multiplier=payout_multiplier,
|
||||
)
|
||||
eliminations.append(elimination)
|
||||
cascade_base_win += payout_multiplier * self.bet
|
||||
self._symbol_counts[symbol] += count
|
||||
|
||||
current_bombs = []
|
||||
for bomb in bombs:
|
||||
if bomb.value not in MULTIPLIER_VALUES:
|
||||
raise RuleViolation(f"不支持的炸弹倍数: {bomb.value}")
|
||||
existing = self._bombs.get(bomb.bomb_id)
|
||||
if existing is not None and existing != bomb:
|
||||
raise RuleViolation(f"炸弹 {bomb.bomb_id} 的数据前后不一致")
|
||||
if existing is None:
|
||||
self._bombs[bomb.bomb_id] = bomb
|
||||
current_bombs.append(bomb)
|
||||
|
||||
self._spin_base_win += cascade_base_win
|
||||
result = CascadeResult(
|
||||
cascade_index=len(self._cascades) + 1,
|
||||
eliminations=tuple(eliminations),
|
||||
bombs=tuple(current_bombs),
|
||||
base_win=cascade_base_win,
|
||||
accumulated_symbols=dict(self._symbol_counts),
|
||||
)
|
||||
self._cascades.append(result)
|
||||
return result
|
||||
|
||||
def end_spin(self) -> SpinResult:
|
||||
if self._cascades is None:
|
||||
raise RuleViolation("请先调用 begin_spin")
|
||||
|
||||
multiplier_before = self.state.accumulated_multiplier
|
||||
has_win = self._spin_base_win > 0
|
||||
bombs_added = sum(bomb.value for bomb in self._bombs.values()) if has_win else 0
|
||||
if self._is_free_spin:
|
||||
# 免费旋转:炸弹并入累计器,赢额乘“累计+本旋新炸弹”
|
||||
multiplier_after = multiplier_before + bombs_added
|
||||
effective_multiplier = max(1, multiplier_after) if has_win else 0
|
||||
else:
|
||||
# 基础旋转:无累计器,炸弹仅乘本旋赢额,不留存
|
||||
multiplier_after = multiplier_before
|
||||
effective_multiplier = max(1, bombs_added) if has_win else 0
|
||||
total_win = self._spin_base_win * effective_multiplier
|
||||
|
||||
# 封顶:整局累计赢奖不超过 1000 倍基础下注(最高 1000x 饰品)
|
||||
remaining = self.max_win - self.state.total_win
|
||||
if remaining < 0:
|
||||
remaining = Decimal("0")
|
||||
capped = total_win > remaining
|
||||
if capped:
|
||||
total_win = remaining
|
||||
|
||||
result = SpinResult(
|
||||
spin_index=self.state.next_spin_index,
|
||||
cascades=tuple(self._cascades),
|
||||
base_win=self._spin_base_win,
|
||||
multiplier_before=multiplier_before,
|
||||
bombs_added=bombs_added,
|
||||
multiplier_after=multiplier_after,
|
||||
effective_multiplier=effective_multiplier,
|
||||
total_win=total_win,
|
||||
accumulated_symbols=dict(self._symbol_counts),
|
||||
win_capped=capped,
|
||||
)
|
||||
self.state.spins.append(result)
|
||||
self.state.next_spin_index += 1
|
||||
self.state.accumulated_multiplier = multiplier_after
|
||||
self.state.total_win += total_win
|
||||
self._cascades = None
|
||||
return result
|
||||
|
||||
def reset_session(self) -> None:
|
||||
if self._cascades is not None:
|
||||
raise RuleViolation("当前回合尚未结束")
|
||||
self.state.reset()
|
||||
Reference in New Issue
Block a user