add fortunes

This commit is contained in:
bridge
2025-10-19 15:19:45 +08:00
parent bddc5028a0
commit af282ad206
4 changed files with 160 additions and 4 deletions

128
src/classes/fortune.py Normal file
View File

@@ -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",
]

View File

@@ -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。

View File

@@ -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)