Files
cultivation-world-simulator/src/classes/fortune.py
2025-11-03 00:16:45 +08:00

339 lines
11 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 random
from enum import Enum
from typing import Optional
import asyncio
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.action.event_helper import EventHelper
from src.utils.asyncio_utils import schedule_background
from src.classes.technique import (
TechniqueGrade,
get_random_upper_technique_for_avatar,
techniques_by_id,
Technique,
is_attribute_compatible_with_root,
TechniqueAttribute,
)
from src.classes.treasure import Treasure, treasures_by_id
from src.classes.relation import Relation
from src.classes.alignment import Alignment
class FortuneKind(Enum):
"""奇遇类型"""
TREASURE = "treasure"
TECHNIQUE = "technique"
FIND_MASTER = "find_master"
F_TREASURE_THEMES: list[str] = [
"误入洞府",
"巧捡奇物",
"误入试炼",
"异象出世",
"高人指点",
]
F_TECHNIQUE_THEMES: list[str] = [
"误入洞府",
"巧捡奇物",
"误入试炼",
"高人指点",
"玄妙感悟",
]
F_FIND_MASTER_THEMES: list[str] = [
"危难相救",
"品行打动",
"展露天赋",
"机缘巧合",
"通过考验",
]
def _has_master(avatar: Avatar) -> bool:
"""检查是否已有师傅"""
for other, rel in avatar.relations.items():
if rel == Relation.APPRENTICE:
return True
return False
def _is_alignment_compatible(avatar: Avatar, other: Avatar) -> bool:
"""检查两个角色的阵营是否兼容(不是敌对关系)"""
from src.classes.alignment import Alignment
if avatar.alignment is None or other.alignment is None:
return True
# 正邪不相容
if avatar.alignment == Alignment.RIGHTEOUS and other.alignment == Alignment.EVIL:
return False
if avatar.alignment == Alignment.EVIL and other.alignment == Alignment.RIGHTEOUS:
return False
return True
def _find_potential_master(avatar: Avatar) -> Optional[Avatar]:
"""
在世界中寻找潜在的师傅。
规则:
1. 等级 > avatar.level + 20
2. 优先选择同宗门的高手
3. 如果没有同宗门的,选择阵营兼容的其他人
4. 不能拜敌对阵营的人为师
"""
same_sect_candidates: list[Avatar] = []
other_candidates: list[Avatar] = []
for other in avatar.world.avatar_manager.avatars.values():
if other is avatar:
continue
# 等级差检查
level_diff = other.cultivation_progress.level - avatar.cultivation_progress.level
if level_diff < 20:
continue
# 阵营兼容性检查
if not _is_alignment_compatible(avatar, other):
continue
# 同宗门优先
if avatar.sect is not None and other.sect == avatar.sect:
same_sect_candidates.append(other)
else:
other_candidates.append(other)
# 优先从同宗门选择
if same_sect_candidates:
return random.choice(same_sect_candidates)
# 没有同宗门的,从其他候选中选择
if other_candidates:
return random.choice(other_candidates)
return None
def _can_get_treasure(avatar: Avatar) -> bool:
"""检查是否可以获得法宝奇遇"""
return avatar.treasure is None
def _can_get_technique(avatar: Avatar) -> bool:
"""
检查是否可以获得功法奇遇
- 任何人功法非上品都可以触发
- 但实际能否获得功法,在获取时会有额外检查(宗门弟子有限制)
"""
tech_not_upper = (avatar.technique is None) or (avatar.technique.grade is not TechniqueGrade.UPPER)
return tech_not_upper
def _can_get_master(avatar: Avatar) -> bool:
"""检查是否可以获得拜师奇遇"""
if _has_master(avatar):
return False
return _find_potential_master(avatar) is not None
def _choose_kind(avatar: Avatar) -> FortuneKind:
"""
从所有可能的奇遇中随机选择一个。
可能的奇遇取决于角色当前状态。
"""
possible_kinds: list[FortuneKind] = []
# 法宝奇遇:任何人无法宝都可以
if _can_get_treasure(avatar):
possible_kinds.append(FortuneKind.TREASURE)
# 功法奇遇:任何人功法非上品都可以(实际获得时会有限制)
if _can_get_technique(avatar):
possible_kinds.append(FortuneKind.TECHNIQUE)
# 拜师奇遇:无师傅且世界中有合适的师傅
if _can_get_master(avatar):
possible_kinds.append(FortuneKind.FIND_MASTER)
if not possible_kinds:
return None
return random.choice(possible_kinds)
def _pick_theme(kind: FortuneKind) -> str:
if kind == FortuneKind.TREASURE:
return random.choice(F_TREASURE_THEMES)
elif kind == FortuneKind.TECHNIQUE:
return random.choice(F_TECHNIQUE_THEMES)
elif kind == FortuneKind.FIND_MASTER:
return random.choice(F_FIND_MASTER_THEMES)
return ""
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 _get_fortune_technique_for_avatar(avatar: Avatar) -> Optional[Technique]:
"""
为奇遇获取功法。
规则:
1. 散修:可以获得任何上品功法(与灵根/阵营/condition兼容
2. 宗门弟子:只能获得本宗门或无宗门的上品功法
"""
candidates: list[Technique] = []
# 确定允许的宗门范围
allowed_sects: set[Optional[str]] = {None, ""}
if avatar.sect is not None:
sect_name = avatar.sect.name.strip() if avatar.sect.name else None
if sect_name:
allowed_sects.add(sect_name)
# 筛选功法
for t in techniques_by_id.values():
# 必须是上品
if t.grade != TechniqueGrade.UPPER:
continue
# 宗门限制:宗门弟子只能获得本宗门或无宗门的功法
tech_sect = t.sect.strip() if t.sect else None
if tech_sect not in allowed_sects:
continue
# condition 检查
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 try_trigger_fortune(avatar: Avatar) -> list[Event]:
"""
在月度结算阶段尝试触发奇遇。
规则:
- 奇遇不是一个 action仅在条件满足时以概率触发。
- 触发条件:
* 法宝奇遇:无法宝(不限散修/宗门)
* 功法奇遇:功法非上品(不限散修/宗门,但宗门弟子只能获得本宗门或无宗门功法)
* 拜师奇遇:无师傅且世界中有合适的师傅(优先同宗门,不能拜敌对阵营)
- 结果:
* 法宝:世界唯一且不可重复
* 功法:可重复,优先上品,需与灵根兼容,宗门弟子受宗门限制
* 拜师:建立师徒关系
- 故事:仅给出主旨主题,由 LLM 自由发挥生成短故事。
"""
base_prob = float(getattr(CONFIG.game, "fortune_probability", 0.0))
extra_prob = float(avatar.effects.get("extra_fortune_probability", 0.0))
prob = base_prob + extra_prob
if prob <= 0.0:
return []
if random.random() >= prob:
return []
# 从所有可能的奇遇中选择
kind = _choose_kind(avatar)
if kind is None:
return []
theme = _pick_theme(kind)
res_text: str = ""
related_avatars = [avatar.id]
actors_for_story = [avatar] # 用于生成故事的角色列表
if kind == FortuneKind.TREASURE:
tr = _get_unique_treasure_for_world(avatar)
if tr is None:
# 回退到功法
kind = FortuneKind.TECHNIQUE
theme = _pick_theme(kind)
else:
avatar.treasure = tr
res_text = f"{avatar.name} 获得法宝『{tr.name}"
if kind == FortuneKind.TECHNIQUE:
tech = _get_fortune_technique_for_avatar(avatar)
if tech is None:
# 若无可用上品功法(宗门弟子可能因宗门限制而找不到),则不奖励
return []
avatar.technique = tech
res_text = f"{avatar.name} 得到上品功法『{tech.name}"
elif kind == FortuneKind.FIND_MASTER:
master = _find_potential_master(avatar)
if master is None:
# 找不到合适的师傅
return []
# 建立师徒关系avatar 是徒弟master 是师傅
avatar.set_relation(master, Relation.APPRENTICE)
res_text = f"{avatar.name}{master.name} 为师"
related_avatars.append(master.id)
actors_for_story = [avatar, master] # 拜师奇遇需要两个人的信息
# 生成故事(异步避免阻塞)
event_text = f"遭遇奇遇({theme}{res_text}"
story_prompt = "请据此写100~150字小故事。"
month_at_finish = avatar.world.month_stamp
base_event = Event(month_at_finish, event_text, related_avatars=related_avatars)
async def _gen_and_push_story():
# 拜师奇遇传入两个角色,其他奇遇传入一个角色
story = await StoryTeller.tell_from_actors_async(event_text, res_text, *actors_for_story, prompt=story_prompt)
story_event = Event(month_at_finish, story, related_avatars=related_avatars)
# 根据涉及角色数量推送事件
if len(actors_for_story) == 1:
EventHelper.push_self(story_event, avatar, to_sidebar=True)
else:
# 拜师奇遇涉及两个角色
EventHelper.push_pair(story_event, initiator=avatar, target=actors_for_story[1], to_sidebar_once=True)
def _fallback_sync():
story = StoryTeller.tell_from_actors(event_text, res_text, *actors_for_story, prompt=story_prompt)
story_event = Event(month_at_finish, story, related_avatars=related_avatars)
if len(actors_for_story) == 1:
EventHelper.push_self(story_event, avatar, to_sidebar=True)
else:
EventHelper.push_pair(story_event, initiator=avatar, target=actors_for_story[1], to_sidebar_once=True)
schedule_background(_gen_and_push_story(), fallback=_fallback_sync)
return [base_event]
__all__ = [
"try_trigger_fortune",
]