Files
cultivation-world-simulator/src/sim/simulator.py
2025-11-26 19:00:07 +08:00

280 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.
import random
import asyncio
from src.classes.calendar import Month, Year, MonthStamp
from src.classes.avatar import Avatar, Gender
from src.sim.new_avatar import create_random_mortal
from src.classes.age import Age
from src.classes.cultivation import Realm
from src.classes.world import World
from src.classes.event import Event, is_null_event
from src.classes.ai import llm_ai
from src.classes.name 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
from src.classes.celestial_phenomenon import get_random_celestial_phenomenon
from src.classes.long_term_objective import process_avatar_long_term_objective
from src.classes.death import handle_death
class Simulator:
def __init__(self, world: World):
self.world = world
self.birth_rate = CONFIG.game.npc_birth_rate_per_month # 从配置文件读取NPC每月出生率
async def _phase_decide_actions(self):
"""
决策阶段:仅对需要新计划的角色调用 AI当前无动作且无计划
将 AI 的决策结果加载为角色的计划链。
"""
avatars_to_decide = []
for avatar in list(self.world.avatar_manager.avatars.values()):
if avatar.current_action is None and not avatar.has_plans():
avatars_to_decide.append(avatar)
if not avatars_to_decide:
return
ai = llm_ai
decide_results = await ai.decide(self.world, avatars_to_decide)
for avatar, result in decide_results.items():
action_name_params_pairs, avatar_thinking, short_term_objective, _event = result
# 仅入队计划,不在此处添加开始事件,避免与提交阶段重复
avatar.load_decide_result_chain(action_name_params_pairs, avatar_thinking, short_term_objective)
def _phase_commit_next_plans(self):
"""
提交阶段:为空闲角色提交计划中的下一个可执行动作,返回开始事件集合。
"""
events = []
for avatar in list(self.world.avatar_manager.avatars.values()):
if avatar.current_action is None:
start_event = avatar.commit_next_plan()
if start_event is not None and not is_null_event(start_event):
events.append(start_event)
return events
async def _phase_execute_actions(self):
"""
执行阶段:推进当前动作,支持同月链式抢占即时结算,返回期间产生的事件。
"""
events = []
MAX_LOCAL_ROUNDS = 3
for _ in range(MAX_LOCAL_ROUNDS):
new_action_happened = False
for avatar_id, avatar in list(self.world.avatar_manager.avatars.items()):
# 本轮执行前若标记为新设,则清理,执行后由 Avatar 再统一清除
if getattr(avatar, "_new_action_set_this_step", False):
new_action_happened = True
new_events = await avatar.tick_action()
if new_events:
events.extend(new_events)
# 若在本次执行后产生了新的动作(被别人抢占设立),则标志位会在 commit_next_plan 时被置 True
if getattr(avatar, "_new_action_set_this_step", False):
new_action_happened = True
# 若本轮未检测到新动作产生,则结束本地循环
if not new_action_happened:
break
return events
def _phase_resolve_death(self):
"""
结算死亡:
- 战斗死亡已在 Action 中结算,此处不再重复(因为已从 avatars 中移除)
- 此时剩下的 avatars 都是存活的,只需检查非战斗因素(如老死、被动掉血)
"""
events = []
# 遍历时可能修改字典,使用 list() 复制
for avatar_id, avatar in list(self.world.avatar_manager.avatars.items()):
is_dead = False
reason = ""
# 优先判定重伤(可能是被动效果导致)
if avatar.hp <= 0:
is_dead = True
reason = f"{avatar.name} 因重伤不治身亡"
# 其次判定寿元
elif avatar.death_by_old_age():
is_dead = True
reason = f"{avatar.name} 老死了,时年{avatar.age.get_age()}"
if is_dead:
event = Event(self.world.month_stamp, reason, related_avatars=[avatar.id])
events.append(event)
handle_death(self.world, avatar)
return events
def _phase_update_age_and_birth(self):
"""
更新存活角色年龄,并以一定概率生成新修士,返回期间产生的事件集合。
"""
events = []
for avatar_id, avatar in self.world.avatar_manager.avatars.items():
avatar.update_age(self.world.month_stamp)
if random.random() < self.birth_rate:
age = random.randint(16, 60)
gender = random.choice(list(Gender))
name = get_random_name(gender)
new_avatar = create_random_mortal(self.world, self.world.month_stamp, name, Age(age, Realm.Qi_Refinement))
self.world.avatar_manager.avatars[new_avatar.id] = new_avatar
event = Event(self.world.month_stamp, f"{new_avatar.name}晋升为修士了。", related_avatars=[new_avatar.id])
events.append(event)
return events
async def _phase_passive_effects(self):
"""
被动结算阶段:
- 更新时间效果如HP回复
- 触发奇遇(非动作)
"""
events = []
for avatar in self.world.avatar_manager.avatars.values():
avatar.update_time_effect()
# 使用 gather 并行触发奇遇
tasks = [try_trigger_fortune(avatar) for avatar in self.world.avatar_manager.avatars.values()]
results = await asyncio.gather(*tasks)
for res in results:
if res:
events.extend(res)
return events
async def _phase_nickname_generation(self):
"""
绰号生成阶段
"""
from src.classes.nickname import process_avatar_nickname
# 并发执行
tasks = [process_avatar_nickname(avatar) for avatar in self.world.avatar_manager.avatars.values()]
results = await asyncio.gather(*tasks)
events = [e for e in results if e]
return events
async def _phase_long_term_objective_thinking(self):
"""
长期目标思考阶段
检查角色是否需要生成/更新长期目标
"""
# 并发执行
tasks = [process_avatar_long_term_objective(avatar) for avatar in self.world.avatar_manager.avatars.values()]
results = await asyncio.gather(*tasks)
events = [e for e in results if e]
return events
def _phase_update_celestial_phenomenon(self):
"""
更新天地灵机:
- 检查当前天象是否到期
- 如果到期,则随机选择新天象
- 生成世界事件记录天象变化
天象变化时机:
- 从游戏第二年101年开始
- 每5年或当前天象指定的持续时间变化一次
"""
events = []
current_year = self.world.month_stamp.get_year()
current_month = self.world.month_stamp.get_month()
# 第一年100年不触发天象
if current_year < 101:
return events
# 初次运行在101年1月设置初始天象
if self.world.current_phenomenon is None and current_month == Month.JANUARY:
new_phenomenon = get_random_celestial_phenomenon()
if new_phenomenon:
self.world.current_phenomenon = new_phenomenon
self.world.phenomenon_start_year = current_year
# 生成世界事件(不绑定任何角色)
event = Event(
self.world.month_stamp,
f"天降异象!{new_phenomenon.name}{new_phenomenon.desc}",
related_avatars=None # 世界事件,不绑定角色
)
events.append(event)
elif self.world.current_phenomenon is not None:
# 检查是否到期(每年一月检查)
if current_month == Month.JANUARY:
elapsed_years = current_year - self.world.phenomenon_start_year
if elapsed_years >= self.world.current_phenomenon.duration_years:
# 天象到期,更换新天象
old_phenomenon = self.world.current_phenomenon
new_phenomenon = get_random_celestial_phenomenon()
if new_phenomenon:
self.world.current_phenomenon = new_phenomenon
self.world.phenomenon_start_year = current_year
# 生成天象变化事件
event = Event(
self.world.month_stamp,
f"{old_phenomenon.name}消散,天地异象再现!{new_phenomenon.name}{new_phenomenon.desc}",
related_avatars=None # 世界事件
)
events.append(event)
return events
def _phase_log_events(self, events):
"""
将事件写入日志。
"""
logger = get_logger().logger
for event in events:
logger.info("EVENT: %s", str(event))
async def step(self):
"""
前进一步(每步模拟是一个月时间)
结算这个时间内的所有情况。
角色行为、世界变化、重大事件、etc。
先结算多个角色间互相交互的事件。
再去结算单个角色的事件。
"""
events = [] # list of Event
# 0.5 长期目标思考阶段(在决策之前)
events.extend(await self._phase_long_term_objective_thinking())
# 1. 决策阶段
await self._phase_decide_actions()
# 2. 提交阶段
events.extend(self._phase_commit_next_plans())
# 3. 执行阶段
events.extend(await self._phase_execute_actions())
# 4. 结算死亡
events.extend(self._phase_resolve_death())
# 5. 年龄与新生
events.extend(self._phase_update_age_and_birth())
# 6. 被动结算(时间效果+奇遇)
events.extend(await self._phase_passive_effects())
# 7. 绰号生成
events.extend(await self._phase_nickname_generation())
# 8. 更新天地灵机
events.extend(self._phase_update_celestial_phenomenon())
# 9. 日志
# 统一写入事件管理器
if hasattr(self.world, "event_manager") and self.world.event_manager is not None:
for e in events:
self.world.event_manager.add_event(e)
self._phase_log_events(events)
# 9. 时间推进
self.world.month_stamp = self.world.month_stamp + 1
return events