first commit

This commit is contained in:
2026-09-21 19:21:35 +08:00
commit 6c944c4ffd
2733 changed files with 296956 additions and 0 deletions
+169
View File
@@ -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()