add technique

This commit is contained in:
bridge
2025-10-04 23:06:39 +08:00
parent e17c9e35a9
commit aea1923ba6
7 changed files with 374 additions and 47 deletions

View File

@@ -30,6 +30,7 @@ from .battle import Battle
from .plunder_mortals import PlunderMortals
from .help_mortals import HelpMortals
from .talk import Talk
from .devour_mortals import DevourMortals
# 注册到 ActionRegistry标注是否为实际可执行动作
register_action(actual=False)(Action)
@@ -56,6 +57,7 @@ register_action(actual=False)(Battle)
register_action(actual=True)(PlunderMortals)
register_action(actual=True)(HelpMortals)
register_action(actual=True)(Talk)
register_action(actual=True)(DevourMortals)
__all__ = [
# 基类
@@ -84,6 +86,7 @@ __all__ = [
"PlunderMortals",
"HelpMortals",
"Talk",
"DevourMortals",
]

View File

@@ -0,0 +1,44 @@
from __future__ import annotations
from src.classes.action import TimedAction
from src.classes.event import Event
from src.classes.region import CityRegion
from src.classes.alignment import Alignment
class DevourMortals(TimedAction):
"""
吞噬凡人:仅邪阵营可在城市区域执行,获得大量修炼经验。
与普通修炼相比,经验获取显著更高。
"""
COMMENT = "在城镇吞噬凡人,获得大量修行经验(邪修)"
DOABLES_REQUIREMENTS = "仅限城市区域,且角色阵营为‘邪’,且未处于瓶颈"
PARAMS = {}
duration_months = 2
EXP_GAIN = 2000
def _execute(self) -> None:
region = self.avatar.tile.region
if not isinstance(region, CityRegion):
return
if self.avatar.cultivation_progress.is_in_bottleneck():
return
self.avatar.cultivation_progress.add_exp(self.EXP_GAIN)
def can_start(self) -> bool:
region = self.avatar.tile.region
if not isinstance(region, CityRegion):
return False
if self.avatar.alignment != Alignment.EVIL:
return False
return not self.avatar.cultivation_progress.is_in_bottleneck()
def start(self) -> Event:
return Event(self.world.month_stamp, f"{self.avatar.name} 在城镇开始吞噬凡人")
def finish(self) -> list[Event]:
return []

View File

@@ -13,6 +13,7 @@ from src.classes.tile import Tile
from src.classes.region import Region
from src.classes.cultivation import CultivationProgress
from src.classes.root import Root
from src.classes.technique import Technique, get_random_technique_for_avatar
from src.classes.age import Age
from src.classes.event import NULL_EVENT, Event
from src.classes.typings import ACTION_NAME, ACTION_PARAMS, ACTION_NAME_PARAMS_PAIRS, ACTION_NAME_PARAMS_PAIR
@@ -64,6 +65,7 @@ class Avatar:
root: Root = field(default_factory=lambda: random.choice(list(Root)))
personas: List[Persona] = field(default_factory=list)
technique: Technique | None = None
history_events: List[Event] = field(default_factory=list)
_pending_events: List[Event] = field(default_factory=list)
current_action: Optional[ActionInstance] = None
@@ -97,6 +99,10 @@ class Avatar:
if not self.personas:
self.personas = get_random_compatible_personas(persona_num, avatar=self)
# 出生即随机赋予功法(与灵根/阵营/条件兼容)
if self.technique is None:
self.technique = get_random_technique_for_avatar(self)
def __hash__(self) -> int:
return hash(self.id)
@@ -106,7 +112,8 @@ class Avatar:
尽量多打一些因为会用来给LLM进行决策
"""
personas_str = ", ".join([persona.name for persona in self.personas])
return f"Avatar(id={self.id}, 性别={self.gender}, 年龄={self.age}, name={self.name}, 阵营={self.alignment.get_info()}, 区域={self.tile.region.name}, 灵根={str(self.root)}, 境界={self.cultivation_progress}, HP={self.hp}, MP={self.mp}, 个性={personas_str})"
technique_str = self.technique.name if self.technique is not None else ""
return f"Avatar(id={self.id}, 性别={self.gender}, 年龄={self.age}, name={self.name}, 阵营={self.alignment.get_info()}, 区域={self.tile.region.name}, 灵根={str(self.root)}, 功法={technique_str}, 境界={self.cultivation_progress}, HP={self.hp}, MP={self.mp}, 个性={personas_str})"
def __str__(self) -> str:
return self.get_info()

View File

@@ -2,8 +2,10 @@ from __future__ import annotations
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
if TYPE_CHECKING:
from src.classes.avatar import Avatar
@@ -24,13 +26,26 @@ def _realm_order(realm: Realm) -> int:
def calc_win_rate(attacker: "Avatar", defender: "Avatar") -> float:
"""
根据双方境界粗略计算进攻方胜率。
基准50%,每高一个大境界+15%,限制在[0.1, 0.9]。
胜率计算(返回进攻方胜率 p ∈ [0.1, 0.9]
- 基准50%
- 境界差:每高一大境界 +15%
- 功法品阶差:按品阶差的相对加成(可正可负)
- 属性克制:若进攻方克制防守方,再 +10%
最后夹紧到 [0.1, 0.9]
"""
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))
@@ -58,8 +73,37 @@ def get_escape_success_rate(attacker: "Avatar", defender: "Avatar") -> float:
def get_damage(winner: "Avatar", loser: "Avatar") -> int:
"""
根据胜负双方境界差距估算伤害基础100差一大境界+100上限500。
伤害计算(返回单次战斗伤害值,整数):
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))
# return min(500, 100 + 100 * gap)
return 500
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)

View File

@@ -2,15 +2,13 @@
灵根
五行元素与灵根组合:
- RootElement金、木、水、火、土恒定不变
- Root从 CSV 配置加载(单/双/天灵根等),每个成员包含(中文名, 元素列表)
- Root硬编码定义(单/双/天灵根等),每个成员包含(中文名, 元素列表)
"""
from enum import Enum
from typing import List, Tuple, Dict
from collections import defaultdict
from src.classes.essence import EssenceType
from src.utils.df import game_configs
from src.utils.config import CONFIG
class RootElement(Enum):
@@ -44,30 +42,41 @@ class _RootMixin:
return f"{self.value}({', '.join(str(e) for e in self.elements)})"
def _build_root_members_from_csv() -> Dict[str, tuple]:
class Root(_RootMixin, Enum):
"""
从 CSV 读取 Root 定义,返回用于创建枚举的 members 映射
CSV 列id,key,name,element_list其中 element_list 用分号分隔中文名(如:金;木)。
灵根(硬编码):成员值为 (中文名, 元素元组)
数据来源原为 CSV现改为内置
- GOLD: 金灵根 -> 金
- WOOD: 木灵根 -> 木
- WATER: 水灵根 -> 水
- FIRE: 火灵根 -> 火
- EARTH: 土灵根 -> 土
- THUNDER: 雷灵根 -> 水;土
- ICE: 冰灵根 -> 金;水
- WIND: 风灵根 -> 木;水
- DARK: 暗灵根 -> 火;土
- HEAVEN: 天灵根 -> 金;木;水;火;土(额外突破+0.1
"""
df = game_configs.get("root")
sep = CONFIG.df.ids_separator
members: Dict[str, tuple] = {}
for _, row in df.iterrows():
key = str(row["key"]).strip()
cn_name = str(row["name"]).strip()
elements_field = str(row["element_list"]).strip()
element_names = [s.strip() for s in elements_field.split(sep) if str(s).strip()]
element_members: List[RootElement] = []
for en in element_names:
element_members.append(RootElement(en))
members[key] = (cn_name, tuple(element_members))
return members
# 动态创建 Root 枚举(使用 mixin 作为 type使 __new__ 生效)
Root = Enum("Root", _build_root_members_from_csv(), type=_RootMixin)
# 某些环境下函数式创建的 Enum 可能未正确采用 mixin 的 __str__这里显式绑定确保生效
Root.__str__ = _RootMixin.__str__
GOLD = ("金灵根", (RootElement.GOLD,))
WOOD = ("木灵根", (RootElement.WOOD,))
WATER = ("水灵根", (RootElement.WATER,))
FIRE = ("火灵根", (RootElement.FIRE,))
EARTH = ("土灵根", (RootElement.EARTH,))
THUNDER = ("雷灵根", (RootElement.WATER, RootElement.EARTH))
ICE = ("冰灵根", (RootElement.GOLD, RootElement.WATER))
WIND = ("风灵根", (RootElement.WOOD, RootElement.WATER))
DARK = ("暗灵根", (RootElement.FIRE, RootElement.EARTH))
HEAVEN = (
"天灵根",
(
RootElement.GOLD,
RootElement.WOOD,
RootElement.WATER,
RootElement.FIRE,
RootElement.EARTH,
),
)
# 元素到灵气类型的一一对应
@@ -87,23 +96,10 @@ def get_essence_types_for_root(root: "Root") -> List[EssenceType]:
return [_essence_by_element[e] for e in root.elements]
def _load_extra_breakthrough_success_rate_from_csv() -> Dict["Root", float]:
"""
从 root.csv 载入各灵根的额外突破成功率默认0。
列名extra_breakthrough_success_rate
"""
df = game_configs["root"]
bonuses: Dict["Root", float] = {}
for _, row in df.iterrows():
key = str(row["key"]).strip()
root_member = getattr(Root, key)
bonus = float(row.get("extra_breakthrough_success_rate", 0) or 0)
bonuses[root_member] = bonus
return bonuses
# 从配置构造带默认值的加成表
# 额外突破成功率(默认 0.0),根据原 CSV 保留天灵根 0.1
extra_breakthrough_success_rate = defaultdict(
lambda: 0.0,
_load_extra_breakthrough_success_rate_from_csv(),
{
Root.HEAVEN: 0.1,
},
)

200
src/classes/technique.py Normal file
View File

@@ -0,0 +1,200 @@
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Optional, Dict, List
from src.utils.df import game_configs
from src.classes.alignment import Alignment
from src.classes.root import Root, RootElement
class TechniqueAttribute(Enum):
GOLD = ""
WOOD = ""
WATER = ""
FIRE = ""
EARTH = ""
ICE = ""
WIND = ""
DARK = ""
THUNDER = ""
EVIL = ""
def __str__(self) -> str:
return self.value
class TechniqueGrade(Enum):
LOWER = "下品"
MIDDLE = "中品"
UPPER = "上品"
@staticmethod
def from_str(s: str) -> "TechniqueGrade":
s = str(s).strip()
if s == "上品":
return TechniqueGrade.UPPER
if s == "中品":
return TechniqueGrade.MIDDLE
return TechniqueGrade.LOWER
@dataclass
class Technique:
id: int
name: str
attribute: TechniqueAttribute
grade: TechniqueGrade
prompt: str
weight: float
condition: str
def is_allowed_for(self, avatar) -> bool:
if not self.condition:
return True
return bool(eval(self.condition, {"__builtins__": {}}, {"avatar": avatar, "Alignment": Alignment}))
# 五行与扩展属性的克制关系
# - 五行:金克木,木克土,土克水,水克火,火克金
# - 雷克邪;邪、冰、风、暗不克任何人
SUPPRESSION: dict[TechniqueAttribute, set[TechniqueAttribute]] = {
TechniqueAttribute.GOLD: {TechniqueAttribute.WOOD},
TechniqueAttribute.WOOD: {TechniqueAttribute.EARTH},
TechniqueAttribute.EARTH: {TechniqueAttribute.WATER},
TechniqueAttribute.WATER: {TechniqueAttribute.FIRE},
TechniqueAttribute.FIRE: {TechniqueAttribute.GOLD},
TechniqueAttribute.THUNDER: {TechniqueAttribute.EVIL},
TechniqueAttribute.ICE: set(),
TechniqueAttribute.WIND: set(),
TechniqueAttribute.DARK: set(),
TechniqueAttribute.EVIL: set(),
}
def loads() -> tuple[dict[int, Technique], dict[str, Technique]]:
techniques_by_id: dict[int, Technique] = {}
techniques_by_name: dict[str, Technique] = {}
df = game_configs["technique"]
for _, row in df.iterrows():
attr = TechniqueAttribute(str(row["technique_root"]).strip())
name = str(row["name"]).strip()
grade = TechniqueGrade.from_str(row.get("grade", "下品"))
cond_val = row.get("condition", "")
condition = "" if str(cond_val) == "nan" else str(cond_val).strip()
weight_val = row.get("weight", 1)
weight = float(str(weight_val)) if str(weight_val) != "nan" else 1.0
t = Technique(
id=int(row["id"]),
name=name,
attribute=attr,
grade=grade,
prompt=str(row.get("prompt", "")),
weight=weight,
condition=condition,
)
techniques_by_id[t.id] = t
techniques_by_name[t.name] = t
return techniques_by_id, techniques_by_name
techniques_by_id, techniques_by_name = loads()
def is_attribute_compatible_with_root(attr: TechniqueAttribute, root: Root) -> bool:
if attr == TechniqueAttribute.EVIL:
# 邪功法仅由阵营约束,这里视为与灵根无关
return True
# 天灵根:除邪外全系可修
if root == Root.HEAVEN:
return attr != TechniqueAttribute.EVIL
# 单属性灵根:只能修行对应属性
single_map = {
Root.GOLD: TechniqueAttribute.GOLD,
Root.WOOD: TechniqueAttribute.WOOD,
Root.WATER: TechniqueAttribute.WATER,
Root.FIRE: TechniqueAttribute.FIRE,
Root.EARTH: TechniqueAttribute.EARTH,
}
if root in single_map:
return attr == single_map[root]
# 复合/扩展灵根:根名属性 + 其元素列表中的属性
complex_map: dict[Root, set[TechniqueAttribute]] = {
Root.ICE: {TechniqueAttribute.ICE, TechniqueAttribute.GOLD, TechniqueAttribute.WATER},
Root.WIND: {TechniqueAttribute.WIND, TechniqueAttribute.WOOD, TechniqueAttribute.WATER},
Root.DARK: {TechniqueAttribute.DARK, TechniqueAttribute.FIRE, TechniqueAttribute.EARTH},
Root.THUNDER: {TechniqueAttribute.THUNDER, TechniqueAttribute.WATER, TechniqueAttribute.EARTH},
}
if root in complex_map:
return attr in complex_map[root]
return False
def get_random_technique_for_avatar(avatar) -> Technique:
import random
candidates: List[Technique] = []
for t in techniques_by_id.values():
if not t.is_allowed_for(avatar):
continue
if t.attribute == TechniqueAttribute.EVIL and avatar.alignment != Alignment.EVIL:
continue
if not is_attribute_compatible_with_root(t.attribute, avatar.root):
continue
candidates.append(t)
if not candidates:
# 回退:不考虑条件,仅按灵根兼容挑选(若仍为空,则全量)
fallback = [
t for t in techniques_by_id.values()
if (t.attribute != TechniqueAttribute.EVIL) and is_attribute_compatible_with_root(t.attribute, avatar.root)
]
candidates = fallback or list(techniques_by_id.values())
weights = [max(0.0, t.weight) for t in candidates]
return random.choices(candidates, weights=weights, k=1)[0]
def get_grade_bonus(grade: TechniqueGrade) -> float:
if grade is TechniqueGrade.UPPER:
return 0.10
if grade is TechniqueGrade.MIDDLE:
return 0.05
return 0.0
def get_suppression_bonus(att_attr: TechniqueAttribute, def_attr: TechniqueAttribute) -> float:
return 0.10 if def_attr in SUPPRESSION.get(att_attr, set()) else 0.0
# 相对品阶优势加成:按“品阶差×步进”的方式计算
# - 品阶映射:下品=0中品=1上品=2
# - 每级差距步进5%最大±10%
_GRADE_RANK: dict[TechniqueGrade, int] = {
TechniqueGrade.LOWER: 0,
TechniqueGrade.MIDDLE: 1,
TechniqueGrade.UPPER: 2,
}
def get_grade_advantage_bonus(attacker_grade: Optional[TechniqueGrade], defender_grade: Optional[TechniqueGrade]) -> float:
"""
根据双方品阶差计算进攻方的相对加成:
- diff = rank(att) - rank(def)
- bonus = 0.05 × diff夹紧到 [-0.10, 0.10]
- 任一为空则视为无加成
返回:进攻方概率或伤害的相对加成(可为负)。
"""
if attacker_grade is None or defender_grade is None:
return 0.0
diff = _GRADE_RANK[attacker_grade] - _GRADE_RANK[defender_grade]
bonus = 0.05 * diff
if bonus > 0.10:
bonus = 0.10
if bonus < -0.10:
bonus = -0.10
return bonus