From af282ad20691e7295ca15586f96aced4bb4df30f Mon Sep 17 00:00:00 2001 From: bridge Date: Sun, 19 Oct 2025 15:19:45 +0800 Subject: [PATCH] add fortunes --- src/classes/fortune.py | 128 +++++++++++++++++++++++++++++++++++++++ src/classes/technique.py | 22 +++++++ src/sim/simulator.py | 13 ++-- static/config.yml | 1 + 4 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 src/classes/fortune.py diff --git a/src/classes/fortune.py b/src/classes/fortune.py new file mode 100644 index 0000000..b3c7ffb --- /dev/null +++ b/src/classes/fortune.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +import random +from typing import Optional + +from src.utils.config import CONFIG +from src.classes.avatar import Avatar +from src.classes.event import Event +from src.classes.story_teller import StoryTeller +from src.classes.technique import TechniqueGrade, get_random_upper_technique_for_avatar +from src.classes.treasure import Treasure, treasures_by_id + + +F_TREASURE_THEMES: list[str] = [ + "误入洞府", + "巧捡奇物", + "误入试炼", + "异象出世", + "高人指点", +] + +F_TECHNIQUE_THEMES: list[str] = [ + "误入洞府", + "巧捡奇物", + "误入试炼", + "高人指点", + "玄妙感悟", +] + + +def _is_rogue_and_under_equipped(avatar: Avatar) -> bool: + # 必须散修;法宝为空 或 功法非上品 + if avatar.sect is not None: + return False + has_no_treasure = avatar.treasure is None + is_tech_lower = (avatar.technique is None) or (avatar.technique.grade is not TechniqueGrade.UPPER) + return has_no_treasure or is_tech_lower + + +def _choose_kind(avatar: Avatar) -> str: + # 如果无法宝,偏向法宝;否则若功法非上品,偏向功法;否则随机 + no_treasure = avatar.treasure is None + tech_not_upper = (avatar.technique is None) or (avatar.technique.grade is not TechniqueGrade.UPPER) + if no_treasure and tech_not_upper: + return random.choice(["treasure", "technique"]) # 两者都缺,随机其一 + if no_treasure: + return "treasure" + if tech_not_upper: + return "technique" + return random.choice(["treasure", "technique"]) + + +def _pick_theme(kind: str) -> str: + if kind == "treasure": + return random.choice(F_TREASURE_THEMES) + return random.choice(F_TECHNIQUE_THEMES) + + +def _get_unique_treasure_for_world(avatar: Avatar) -> Optional[Treasure]: + # 世界唯一法宝:从全量里挑选一个未被任何人持有的 + owned_ids: set[int] = set() + for other in avatar.world.avatar_manager.avatars.values(): + if other.treasure is not None: + owned_ids.add(other.treasure.id) + candidates = [t for t in treasures_by_id.values() if t.id not in owned_ids] + if not candidates: + return None + return random.choice(candidates) + + +def try_trigger_fortune(avatar: Avatar) -> list[Event]: + """ + 在月度结算阶段尝试触发奇遇。 + 规则: + - 奇遇不是一个 action;仅在条件满足时以概率触发。 + - 触发条件:散修,且(无法宝 或 功法非上品)。 + - 结果:先决定奖励类型(法宝/功法),法宝世界唯一且不可重复;功法可重复但优先上品且需与灵根兼容。 + - 故事:仅给出主旨主题,由 LLM 自由发挥生成短故事。 + """ + prob = float(getattr(CONFIG.game, "fortune_probability", 0.0)) + if prob <= 0.0: + return [] + if not _is_rogue_and_under_equipped(avatar): + return [] + if random.random() >= prob: + return [] + + kind = _choose_kind(avatar) + theme = _pick_theme(kind) + + res_text: str = "" + + if kind == "treasure": + tr = _get_unique_treasure_for_world(avatar) + if tr is None: + # 回退到功法 + kind = "technique" + else: + avatar.treasure = tr + res_text = f"{avatar.name} 获得法宝『{tr.name}』" + + if kind == "technique": + tech = get_random_upper_technique_for_avatar(avatar) + if tech is None: + # 若无可用上品,则不奖励 + return [] + avatar.technique = tech + res_text = f"{avatar.name} 得到上品功法『{tech.name}』" + + # 生成故事(仅给出主旨,留白由LLM发挥) + event_text = f"{avatar.name} 遭遇奇遇({theme})" + story_prompt = ( + f"主旨:{theme}。请据此自由发挥,写不超过120字的小故事,收束于获得之物。" + ) + story = StoryTeller.tell_from_actors(event_text, res_text, avatar, prompt=story_prompt) + + events: list[Event] = [ + Event(avatar.world.month_stamp, event_text), + Event(avatar.world.month_stamp, story), + ] + return events + + +__all__ = [ + "try_trigger_fortune", +] + + diff --git a/src/classes/technique.py b/src/classes/technique.py index e0edfdc..f4a4795 100644 --- a/src/classes/technique.py +++ b/src/classes/technique.py @@ -174,6 +174,28 @@ def get_random_technique_for_avatar(avatar) -> Technique: return random.choices(candidates, weights=weights, k=1)[0] +def get_random_upper_technique_for_avatar(avatar) -> Technique | None: + """ + 返回一个与 avatar 灵根/阵营/条件相容的上品功法;若无则返回 None。 + 仅用于奇遇奖励优先挑选上品功法。 + """ + import random + candidates: List[Technique] = [] + for t in techniques_by_id.values(): + if t.grade is not TechniqueGrade.UPPER: + continue + 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: + return None + weights = [max(0.0, t.weight) for t in candidates] + return random.choices(candidates, weights=weights, k=1)[0] + def get_technique_by_sect(sect) -> Technique: """ 简化版:仅按宗门筛选并按权重抽样,不考虑灵根与 condition。 diff --git a/src/sim/simulator.py b/src/sim/simulator.py index 00867dc..64ae4fc 100644 --- a/src/sim/simulator.py +++ b/src/sim/simulator.py @@ -11,6 +11,7 @@ from src.classes.ai import llm_ai, rule_ai from src.utils.names import get_random_name from src.utils.config import CONFIG from src.run.log import get_logger +from src.classes.fortune import try_trigger_fortune class Simulator: def __init__(self, world: World): @@ -109,13 +110,17 @@ class Simulator: events.append(event) return events - def _phase_update_time_effect(self): + def _phase_passive_effects(self): """ - 更新时间效果(如HP回复)。 + 被动结算阶段: + - 更新时间效果(如HP回复) + - 触发奇遇(非动作) """ events = [] for avatar in self.world.avatar_manager.avatars.values(): avatar.update_time_effect() + for avatar in list(self.world.avatar_manager.avatars.values()): + events.extend(try_trigger_fortune(avatar)) return events def _phase_log_events(self, events): @@ -152,8 +157,8 @@ class Simulator: # 5. 年龄与新生 events.extend(self._phase_update_age_and_birth()) - # 6. 时间效果(如HP回复) - events.extend(self._phase_update_time_effect()) + # 6. 被动结算(时间效果+奇遇) + events.extend(self._phase_passive_effects()) # 7. 日志 self._phase_log_events(events) diff --git a/static/config.yml b/static/config.yml index 8f373c9..013e8a7 100644 --- a/static/config.yml +++ b/static/config.yml @@ -18,6 +18,7 @@ game: init_npc_num: 4 sect_num: 2 # init_npc_num大于sect_num时,会随机选择sect_num个宗门 npc_birth_rate_per_month: 0.001 + fortune_probability: 0.001 df: ids_separator: ";"