Files
cultivation-world-simulator/src/classes/battle.py
2025-10-19 01:33:22 +08:00

154 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import math
import random
from typing import Tuple, TYPE_CHECKING
from src.classes.technique import TechniqueGrade, get_suppression_bonus
if TYPE_CHECKING:
from src.classes.avatar import Avatar
# 战斗力参数参考文明6思想但适配本项目数值体系
_STRENGTH_LOG_SCALE: float = 10.0 # 修为强度的对数缩放10×ln(1+level)
_GRADE_POINTS = {
TechniqueGrade.LOWER: 0.0,
TechniqueGrade.MIDDLE: 3.0,
TechniqueGrade.UPPER: 6.0,
}
_SUPPRESSION_POINTS: float = 3.0 # 属性克制即加固定战斗力点数
_CIV6_K: float = 0.04 # 伤害指数系数e^(K×差值)
_WIN_BETA: float = 0.15 # 胜率逻辑函数斜率
_MIN_WIN_RATE: float = 0.01 # 最小胜率
_MAX_WIN_RATE: float = 0.99 # 最大胜率
_BASE_DAMAGE_LOW: int = 24 # 基础伤害下限(按 defender.maxHP/100 缩放)
_BASE_DAMAGE_HIGH: int = 36 # 基础伤害上限(按 defender.maxHP/100 缩放)
_MIN_RATIO: float = 1.05 # 最小相对优势比,确保赢家伤害严格更低
_PAIR_BIAS: float = 1.1 # 成对偏置:让败者再多一点、赢家再少一点
def get_base_strength(self_avatar: "Avatar") -> float:
"""
基础战斗力:与对手无关。
= 10×ln(1+修为等级) + 品阶点数(0/3/6)
"""
level = max(1, self_avatar.cultivation_progress.level)
strength_from_level = _STRENGTH_LOG_SCALE * math.log1p(level)
grade_points = 0.0
if self_avatar.technique is not None:
grade_points = _GRADE_POINTS.get(self_avatar.technique.grade, 0.0)
# 来自效果的额外战斗力点数(例如法宝带来的被动加成)
extra_raw = self_avatar.effects.get("extra_battle_strength_points", 0)
extra_points = float(extra_raw or 0.0)
return strength_from_level + grade_points + extra_points
def _combat_strength_vs(opponent: "Avatar", self_avatar: "Avatar") -> float:
"""
相对战斗力:= 基础战斗力 + 克制点数(若克制则+3)
"""
base = get_base_strength(self_avatar)
suppression_points = 0.0
if self_avatar.technique is not None and opponent.technique is not None:
if get_suppression_bonus(self_avatar.technique.attribute, opponent.technique.attribute) > 0.0:
suppression_points = _SUPPRESSION_POINTS
return base + suppression_points
def _strength_diff(attacker: "Avatar", defender: "Avatar") -> float:
return _combat_strength_vs(defender, attacker) - _combat_strength_vs(attacker, defender)
def get_effective_strength(self_avatar: "Avatar", opponent: "Avatar") -> float:
"""
对外公开:返回 self_avatar 面对 opponent 时的折算战斗力。
用于事件展示与调试,不参与状态修改。
"""
return _combat_strength_vs(opponent, self_avatar)
def get_effective_strength_pair(a: "Avatar", b: "Avatar") -> tuple[float, float]:
"""
一次性返回双方a 面对 bb 面对 a的折算战斗力。
顺序:(a_strength, b_strength)
"""
return _combat_strength_vs(b, a), _combat_strength_vs(a, b)
def calc_win_rate(attacker: "Avatar", defender: "Avatar") -> float:
"""
胜率 = sigmoid(β×战斗力差),并夹紧到 [0.1, 0.9]
- 战斗力差 = 有效战斗力(att) - 有效战斗力(def)
- β 默认 0.15使差值≈10时胜率≈0.82
"""
diff = _strength_diff(attacker, defender)
p = 1.0 / (1.0 + math.exp(-_WIN_BETA * diff))
if p < _MIN_WIN_RATE:
return _MIN_WIN_RATE
if p > _MAX_WIN_RATE:
return _MAX_WIN_RATE
return p
def _base_damage_scale(defender: "Avatar") -> float:
# 以 100 HP 为基准,将 Civ6 的 24~36 损伤映射到不同境界的 HP 档位
max_hp = defender.hp.max
return max(1.0, max_hp / 100.0)
def _damage_from_to(attacker: "Avatar", defender: "Avatar") -> int:
"""
使用 Civ6 风格伤害damage = U(24,36)×scale × e^(K×差值)
- scale = defender.maxHP / 100使不同境界下伤害相对一致
- 差值 = strength(att) - strength(def)
"""
diff = _strength_diff(attacker, defender)
base = random.randint(_BASE_DAMAGE_LOW, _BASE_DAMAGE_HIGH) * _base_damage_scale(defender)
dmg = base * math.exp(_CIV6_K * diff)
return max(1, int(dmg))
def _damage_pair(winner: "Avatar", loser: "Avatar") -> tuple[int, int]:
"""
成对伤害:使用同一基础与对称比值,保证赢家伤害严格小于败者伤害。
- ratio = max(exp(K×|diff|), MIN_RATIO)
- 中间尺度 = 几何均值 sqrt(scale_winner × scale_loser)
- 败者伤害 = base × 中间尺度 × ratio
- 赢家伤害 = base × 中间尺度 ÷ ratio
"""
abs_diff = abs(_strength_diff(winner, loser))
ratio = math.exp(_CIV6_K * abs_diff)
ratio *= _PAIR_BIAS
if ratio < _MIN_RATIO:
ratio = _MIN_RATIO
base = random.randint(_BASE_DAMAGE_LOW, _BASE_DAMAGE_HIGH)
scale_w = _base_damage_scale(winner)
scale_l = _base_damage_scale(loser)
mid_scale = math.sqrt(scale_w * scale_l)
loser_damage = max(1, int(base * mid_scale * ratio))
winner_damage = max(1, int(base * mid_scale / ratio))
return loser_damage, winner_damage
def decide_battle(attacker: "Avatar", defender: "Avatar") -> Tuple["Avatar", "Avatar", int, int]:
"""
结算战斗,返回 (胜者, 败者, 败者掉血, 赢家掉血)。
- 胜率由战斗力差的逻辑函数给出;
- 双方伤害均按 Civ6 风格由同一差值决定对称公式HP 与战斗力独立。
"""
p = calc_win_rate(attacker, defender)
if random.random() < p:
winner, loser = attacker, defender
else:
winner, loser = defender, attacker
loser_damage, winner_damage = _damage_pair(winner, loser)
return winner, loser, loser_damage, winner_damage
def get_escape_success_rate(attacker: "Avatar", defender: "Avatar") -> float:
"""逃跑成功率:后续可基于双方能力细化。"""
return 0.1