refactor battle

This commit is contained in:
bridge
2025-10-15 23:28:19 +08:00
parent 5deb75d881
commit 837cb539fc
5 changed files with 127 additions and 98 deletions

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from src.classes.action import InstantAction
from src.classes.event import Event
from src.classes.battle import decide_battle
from src.classes.battle import decide_battle, get_effective_strength_pair
from src.classes.story_teller import StoryTeller
@@ -35,7 +35,9 @@ class Battle(InstantAction):
def start(self, avatar_name: str) -> Event:
target = self._get_target(avatar_name)
target_name = target.name if target is not None else avatar_name
event = Event(self.world.month_stamp, f"{self.avatar.name}{target_name} 发起战斗")
# 展示双方折算战斗力(基于对手、含克制)
s_att, s_def = get_effective_strength_pair(self.avatar, target)
event = Event(self.world.month_stamp, f"{self.avatar.name}{target_name} 发起战斗(战斗力:{self.avatar.name} {int(s_att)} vs {target_name} {int(s_def)}")
# 记录开始事件内容,供故事生成使用
self._start_event_content = event.content
return event

View File

@@ -1,120 +1,150 @@
from __future__ import annotations
import math
import random
from typing import Tuple, TYPE_CHECKING
import random
from src.classes.cultivation import Realm
from src.classes.technique import get_suppression_bonus, get_grade_advantage_bonus
from src.classes.technique import TechniqueGrade, get_suppression_bonus
if TYPE_CHECKING:
from src.classes.avatar import Avatar
def _realm_order(realm: Realm) -> int:
# 战斗力参数参考文明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 # 胜率逻辑函数斜率
_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 _combat_strength_vs(opponent: "Avatar", self_avatar: "Avatar") -> float:
"""
将境界映射为数值顺序,用于胜率计算。
计算对某个对手的有效战斗力:
= 10×ln(1+修为等级) + 品阶点数(0/3/6) + 克制点数(若克制则+3)
说明:
- 修为使用总等级1..120)并做对数缩放,避免过大;
- 品阶加点为线性,避免与修为重复放大;
- 克制只在“对某个对手”时生效,因此放在此函数处理。
"""
order_map = {
Realm.Qi_Refinement: 1,
Realm.Foundation_Establishment: 2,
Realm.Core_Formation: 3,
Realm.Nascent_Soul: 4,
}
return order_map.get(realm, 1)
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)
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 strength_from_level + grade_points + 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:
"""
胜率计算(返回进攻方胜率 p ∈ [0.1, 0.9]
- 基准50%
- 境界差:每高一大境界 +15%
- 功法品阶差:按品阶差的相对加成(可正可负)
- 属性克制:若进攻方克制防守方,再 +10%
最后夹紧到 [0.1, 0.9]
胜率 = sigmoid(β×战斗力差),并夹紧到 [0.1, 0.9]
- 战斗力差 = 有效战斗力(att) - 有效战斗力(def)
- β 默认 0.15使差值≈10时胜率≈0.82
"""
atk_order = _realm_order(attacker.cultivation_progress.realm)
def_order = _realm_order(defender.cultivation_progress.realm)
delta = atk_order - def_order
base = 0.5 + 0.15 * delta
# 功法品阶差相对加成
atk_grade = getattr(getattr(attacker, "technique", None), "grade", None)
def_grade = getattr(getattr(defender, "technique", None), "grade", None)
base += get_grade_advantage_bonus(atk_grade, def_grade)
# 属性克制:若进攻方克制防守方,再+10%
atk_attr = getattr(getattr(attacker, "technique", None), "attribute", None)
def_attr = getattr(getattr(defender, "technique", None), "attribute", None)
if atk_attr is not None and def_attr is not None:
base += get_suppression_bonus(atk_attr, def_attr)
return max(0.1, min(0.9, base))
diff = _strength_diff(attacker, defender)
p = 1.0 / (1.0 + math.exp(-_WIN_BETA * diff))
if p < 0.1:
return 0.1
if p > 0.9:
return 0.9
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]:
"""
结算一场战斗,返回(胜者, 败者, 败者掉血, 赢家掉血)。
规则:
- 先按 calc_win_rate 判定胜负;
- 以 get_damage 计算基准伤害,再让败者“多掉一点血”(适度上调,例如 +15%
- 赢家也会受伤,但伤害不超过败者伤害的一半(随机 15%~40% 区间)。
结算战斗,返回 (胜者, 败者, 败者掉血, 赢家掉血)。
- 胜率由战斗力差的逻辑函数给出;
- 双方伤害均按 Civ6 风格由同一差值决定对称公式HP 与战斗力独立。
"""
p = calc_win_rate(attacker, defender)
print(f"胜率: {p}")
if random.random() < p:
winner, loser = attacker, defender
else:
winner, loser = defender, attacker
base_damage = get_damage(winner, loser)
# 败者多掉一点血:适度上调,保持上限由 HP.reduce 自然处理
loser_damage = max(1, int(base_damage * 1.15))
# 赢家也掉血,但不超过败者的一半:在 15%~40% 的范围取随机值
rnd_ratio = random.uniform(0.15, 0.40)
winner_damage = int(loser_damage * rnd_ratio)
winner_damage = max(0, min(winner_damage, loser_damage // 2))
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:
"""
逃跑成功率:临时返回常量值,后续可基于双方能力细化。
attacker: 追击方(通常为进攻者)
defender: 逃跑方(通常为被攻击者)
"""
return 0.1
def get_damage(winner: "Avatar", loser: "Avatar") -> int:
"""
伤害计算(返回单次战斗伤害值,整数):
1) 先计算“期望伤害” expected
- 境界差base = 100 + 80 × gap其中 gap = max(0, winnerRealmOrder - loserRealmOrder)
- 功法品阶差:按品阶差 bonus 调整 expected *= (1 + bonus)
- 属性克制:若胜者克制败者,再乘 1.15
- 夹紧:期望伤害最终限制在 [30, 500]
2) 再生成随机区间:[low, high] = [0.85×expected, 1.15×expected]
3) 下限与比例保护不低于败者最大HP的 10%,并至少为 15 的硬下限
4) 返回区间内的随机整数
"""
gap = max(0, _realm_order(winner.cultivation_progress.realm) - _realm_order(loser.cultivation_progress.realm))
expected = 100 + 80 * gap
win_grade = getattr(getattr(winner, "technique", None), "grade", None)
lose_grade = getattr(getattr(loser, "technique", None), "grade", None)
expected *= (1.0 + get_grade_advantage_bonus(win_grade, lose_grade))
win_attr = getattr(getattr(winner, "technique", None), "attribute", None)
lose_attr = getattr(getattr(loser, "technique", None), "attribute", None)
if win_attr is not None and lose_attr is not None:
if get_suppression_bonus(win_attr, lose_attr) > 0:
expected *= 1.15
# 期望伤害夹紧
expected = max(30.0, min(500.0, expected))
# 设定伤害区间并随机
low = int(expected * 0.85)
high = int(expected * 1.15)
# 与最大HP挂钩的最低保护
loser_max_hp = getattr(getattr(loser, "hp", None), "max", 0) or 0
hp_floor = int(max(15, loser_max_hp * 0.10))
low = max(low, hp_floor)
high = max(high, low + 1)
return random.randint(low, high)
"""逃跑成功率:后续可基于双方能力细化。"""
return 0.1

View File

@@ -38,10 +38,7 @@ class StoryTeller:
"""
infos: Dict[str, dict] = {}
for av in avatars:
try:
infos[av.name] = av.get_info(detailed=True)
except Exception:
infos[av.name] = getattr(av, "name", "未知角色")
infos[av.name] = av.get_info(detailed=True)
return infos
@staticmethod

View File

@@ -15,7 +15,7 @@ ai:
max_decide_num: 4
game:
init_npc_num: 9
init_npc_num: 2
sect_num: 3 # init_npc_num大于sect_num时会随机选择sect_num个宗门
npc_birth_rate_per_month: 0.001

View File

@@ -11,7 +11,7 @@ id,name,exclusion_ids,desc,weight,condition
9,沉思,2,你总是会深思熟虑,思考问题比较有哲理。,1,
10,惜命,4;24;25,你总是会珍惜自己的生命,不会轻易冒险。,1,
11,友爱,13;14;15;12;24;25,你重视同伴与和谐,乐于助人,倾向通过协作与沟通化解矛盾。,1,
12,复仇,11;14,你绝不轻易放下仇怨,为了复仇愿意付出代价与时间。你必须立刻进行复仇行动。,1,
12,复仇,11;14,你绝不轻易放下仇怨,为了复仇愿意付出代价与时间。你必须立刻进行复仇行动。,1000,
13,孤僻,11,你喜欢独处,避免与人深交,更信赖自己的判断与行动。,1,
14,淡漠,11;12;15;24;25,你情感克制,对外界冷静疏离,不轻易被他人或事件影响。,1,
15,好斗,11;14;10;17,你直面冲突,偏好以力量与对抗解决问题,越挫越勇。,1,
@@ -25,7 +25,7 @@ id,name,exclusion_ids,desc,weight,condition
23,热情,13;14;22,你待人友好、积极回应,倾向用鼓励与赞美拉近距离;你更愿意主动展开善意的交流,乐于合作与分享,1,
24,极端正义,20;25,你对邪恶深恶痛绝,对正义的理想抱有近乎偏执的追求。,1,avatar.alignment == "正"
25,极端邪恶,20;24,你推崇权力与恐惧,为达目的不择手段,对善良嗤之以鼻。,1,avatar.alignment == "邪"
26,开放,27,你对待和他人结为道侣或者双修比较随意,1000,
26,开放,27,你对待和他人结为道侣或者双修比较随意,1,
27,腼腆,26,你对待和他人结为道侣或者双修比较谨慎,1,
28,舔狗,13;14;22;27,你对异性中外貌出众者格外友善,倾向主动接近、帮助与合作。,1,
29,嫉妒,11;23,你对在修为、外貌或财富等方面远超于你的人容易产生敌意,更倾向对其冷淡、挑衅或打压。,1,
Can't render this file because it contains an unexpected character in line 26 and column 121.