Files
cultivation-world-simulator/src/sim/simulator.py
4thfever 63fc2f828e Feat/auction (#30)
Add gathering events, in which multiple avatars participate
Add auction event

Closes #24
2026-01-14 02:33:13 +08:00

461 lines
19 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.misfortune import try_trigger_misfortune
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
from src.classes.death_reason import DeathReason
class Simulator:
def __init__(self, world: World):
self.world = world
self.awakening_rate = CONFIG.game.npc_awakening_rate_per_month # 从配置文件读取NPC每月觉醒率凡人晋升修士
def _phase_update_perception_and_knowledge(self):
"""
感知更新阶段:
1. 基于感知范围更新 known_regions
2. 自动占据无主洞府(如果自己没有洞府)
"""
from src.classes.observe import get_avatar_observation_radius
from src.classes.region import CultivateRegion
# 1. 缓存当前有洞府的角色ID
avatars_with_home = set()
# 注意:这里我们只关心 CultivateRegion 的 host
# map.cultivate_regions 可能需要确保被正确初始化,如果没有,可以回退到遍历所有 regions
# 为了稳妥,遍历所有 Region 筛选
cultivate_regions = [
r for r in self.world.map.regions.values()
if isinstance(r, CultivateRegion)
]
for r in cultivate_regions:
if r.host_avatar:
avatars_with_home.add(r.host_avatar.id)
# 2. 遍历所有存活角色
for avatar in self.world.avatar_manager.get_living_avatars():
# 计算感知半径(曼哈顿距离)
radius = get_avatar_observation_radius(avatar)
# 扫描范围内的坐标
# 优化只扫描半径内的坐标可能比遍历所有region快也可能慢取决于地图大小和半径
# 地图可能很大,半径通常很小(<10所以基于坐标扫描更优
# 获取范围内的有效坐标
start_x = max(0, avatar.pos_x - radius)
end_x = min(self.world.map.width - 1, avatar.pos_x + radius)
start_y = max(0, avatar.pos_y - radius)
end_y = min(self.world.map.height - 1, avatar.pos_y + radius)
# 收集感知到的区域
observed_regions = set()
for x in range(start_x, end_x + 1):
for y in range(start_y, end_y + 1):
# 距离判定:曼哈顿距离
if abs(x - avatar.pos_x) + abs(y - avatar.pos_y) <= radius:
tile = self.world.map.get_tile(x, y)
if tile.region:
observed_regions.add(tile.region)
# 更新认知与自动占据
for region in observed_regions:
# 更新 known_regions
avatar.known_regions.add(region.id)
# 自动占据逻辑
# 只有当:是修炼区域 + 无主 + 自己无洞府 时触发
if isinstance(region, CultivateRegion):
if region.host_avatar is None:
if avatar.id not in avatars_with_home:
# 占据
region.host_avatar = avatar
avatars_with_home.add(avatar.id)
# 记录事件
event = Event(
self.world.month_stamp,
f"{avatar.name} 路过 {region.name},发现无主,将其占据。",
related_avatars=[avatar.id]
)
avatar.add_event(event)
async def _phase_decide_actions(self):
"""
决策阶段:仅对需要新计划的角色调用 AI当前无动作且无计划
将 AI 的决策结果加载为角色的计划链。
"""
avatars_to_decide = []
for avatar in self.world.avatar_manager.get_living_avatars():
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 self.world.avatar_manager.get_living_avatars():
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):
"""
执行阶段:推进当前动作,支持同月链式抢占即时结算,返回期间产生的事件。
TODO: 为单个角色的 tick_action() 添加 try-except 处理。
"""
events = []
MAX_LOCAL_ROUNDS = 3
# Round 1: 全员执行一次
avatars_needing_retry = set()
for avatar in self.world.avatar_manager.get_living_avatars():
new_events = await avatar.tick_action()
if new_events:
events.extend(new_events)
# 检查是否有新动作产生(抢占/连招),如果有则加入下一轮
# 注意tick_action 内部已处理标记清除逻辑,仅当动作发生切换时才会保留 True
if getattr(avatar, "_new_action_set_this_step", False):
avatars_needing_retry.add(avatar)
# Round 2+: 仅执行有新动作的角色,避免无辜角色重复执行
round_count = 1
while avatars_needing_retry and round_count < MAX_LOCAL_ROUNDS:
current_avatars = list(avatars_needing_retry)
avatars_needing_retry.clear()
for avatar in current_avatars:
new_events = await avatar.tick_action()
if new_events:
events.extend(new_events)
# 再次检查
if getattr(avatar, "_new_action_set_this_step", False):
avatars_needing_retry.add(avatar)
round_count += 1
return events
def _phase_resolve_death(self):
"""
结算死亡:
- 战斗死亡已在 Action 中结算
- 此时剩下的 avatars 都是存活的,只需检查非战斗因素(如老死、被动掉血)
"""
from src.classes.death_reason import DeathReason, DeathType
events = []
for avatar in self.world.avatar_manager.get_living_avatars():
is_dead = False
death_reason: DeathReason | None = None
# 优先判定重伤(可能是被动效果导致)
if avatar.hp.cur <= 0: # 注意:这里应该是 avatar.hp.cur 或者 avatar.hp <= 0 取决于 HP 类的实现,原代码是 avatar.hp <= 0
is_dead = True
death_reason = DeathReason(DeathType.SERIOUS_INJURY)
# 其次判定寿元
elif avatar.death_by_old_age():
is_dead = True
death_reason = DeathReason(DeathType.OLD_AGE)
if is_dead and death_reason:
event = Event(self.world.month_stamp, f"{avatar.name}{death_reason}", related_avatars=[avatar.id])
events.append(event)
handle_death(self.world, avatar, death_reason)
return events
def _phase_update_age_and_birth(self):
"""
更新存活角色年龄,并以一定概率生成新修士,返回期间产生的事件集合。
"""
events = []
for avatar in self.world.avatar_manager.get_living_avatars():
avatar.update_age(self.world.month_stamp)
if random.random() < self.awakening_rate:
age = random.randint(16, 60)
gender = random.choice(list(Gender))
name = get_random_name(gender)
# create_random_mortal 内部会获取 existing_avatars需要确保它处理活人
new_avatar = create_random_mortal(self.world, self.world.month_stamp, name, Age(age, Realm.Qi_Refinement))
self.world.avatar_manager.register_avatar(new_avatar, is_newly_born=True)
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 = []
living_avatars = self.world.avatar_manager.get_living_avatars()
for avatar in living_avatars:
# 1. 处理丹药过期
avatar.process_elixir_expiration(int(self.world.month_stamp))
# 2. 更新被动效果 (如HP回复)
avatar.update_time_effect()
# 使用 gather 并行触发奇遇和霉运
tasks_fortune = [try_trigger_fortune(avatar) for avatar in living_avatars]
tasks_misfortune = [try_trigger_misfortune(avatar) for avatar in living_avatars]
results = await asyncio.gather(*(tasks_fortune + tasks_misfortune))
events.extend([e for res in results if res for e in res])
return events
async def _phase_nickname_generation(self):
"""
绰号生成阶段
"""
from src.classes.nickname import process_avatar_nickname
# 并发执行
living_avatars = self.world.avatar_manager.get_living_avatars()
tasks = [process_avatar_nickname(avatar) for avatar in living_avatars]
results = await asyncio.gather(*tasks)
events = [e for e in results if e]
return events
async def _phase_long_term_objective_thinking(self):
"""
长期目标思考阶段
检查角色是否需要生成/更新长期目标
"""
# 并发执行
living_avatars = self.world.avatar_manager.get_living_avatars()
tasks = [process_avatar_long_term_objective(avatar) for avatar in living_avatars]
results = await asyncio.gather(*tasks)
events = [e for e in results if e]
return events
async def _phase_process_gatherings(self):
"""
Gathering 结算阶段:
检查并执行注册的多人聚集事件(如拍卖会、大比等)。
"""
return await self.world.gathering_manager.check_and_run_all(self.world)
def _phase_update_celestial_phenomenon(self):
"""
更新天地灵机:
- 检查当前天象是否到期
- 如果到期,则随机选择新天象
- 生成世界事件记录天象变化
天象变化时机:
- 初始年份如100年1月立即开始第一个天象
- 每N年当前天象指定的持续时间变化一次
"""
events = []
current_year = self.world.month_stamp.get_year()
current_month = self.world.month_stamp.get_month()
# 检查是否需要初始化或更新天象
# 1. 如果没有天象 (初始化)
# 2. 如果有天象且到期 (每年一月检查)
should_update = False
is_init = False
if self.world.current_phenomenon is None:
should_update = True
is_init = True
elif current_month == Month.JANUARY:
elapsed_years = current_year - self.world.phenomenon_start_year
if elapsed_years >= self.world.current_phenomenon.duration_years:
should_update = True
if should_update:
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
desc = ""
if is_init:
desc = f"世界初开,天降异象!{new_phenomenon.name}{new_phenomenon.desc}"
else:
desc = f"{old_phenomenon.name}消散,天地异象再现!{new_phenomenon.name}{new_phenomenon.desc}"
event = Event(
self.world.month_stamp,
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 _phase_evolve_relations(self):
"""
关系演化阶段:检查并处理满足条件的角色关系变化
"""
from src.classes.relation_resolver import RelationResolver
pairs_to_resolve = []
processed_pairs = set() # (id1, id2) id1 < id2
living_avatars = self.world.avatar_manager.get_living_avatars()
for avatar in living_avatars:
target_ids = list(avatar.relation_interaction_states.keys())
for target_id in target_ids:
state = avatar.relation_interaction_states[target_id]
target = self.world.avatar_manager.get_avatar(target_id)
if target is None or target.is_dead:
continue
# 判定是否触发
count = state["count"]
should_trigger = False
threshold = CONFIG.social.relation_check_threshold
if count >= threshold:
should_trigger = True
if should_trigger:
# 确保唯一性
id1, id2 = sorted([str(avatar.id), str(target.id)])
pair_key = (id1, id2)
if pair_key not in processed_pairs:
processed_pairs.add(pair_key)
pairs_to_resolve.append((avatar, target))
# 重置双方的计数器,防止重复触发
# 1. 重置 A 侧
state["count"] = 0
state["checked_times"] += 1
# 2. 重置 B 侧 (如果 B 也有状态记录)
if hasattr(target, "relation_interaction_states"):
# target 对 avatar 的记录
t_state = target.relation_interaction_states[str(avatar.id)]
t_state["count"] = 0
t_state["checked_times"] += 1
events = []
if pairs_to_resolve:
# 批量并发处理,并直接收集返回的事件
relation_events = await RelationResolver.run_batch(pairs_to_resolve)
if relation_events:
events.extend(relation_events)
return events
async def step(self):
"""
前进一步(每步模拟是一个月时间)
结算这个时间内的所有情况。
角色行为、世界变化、重大事件、etc。
先结算多个角色间互相交互的事件。
再去结算单个角色的事件。
"""
events = [] # list of Event
# 1. 感知与认知更新阶段(包括自动占据洞府)
# 在思考和决策之前,先让角色感知世界
self._phase_update_perception_and_knowledge()
# 2. 长期目标思考阶段(在决策之前)
events.extend(await self._phase_long_term_objective_thinking())
# 3. Gathering 结算阶段
events.extend(await self._phase_process_gatherings())
# 4. 决策阶段
await self._phase_decide_actions()
# 5. 提交阶段
events.extend(self._phase_commit_next_plans())
# 6. 执行阶段
events.extend(await self._phase_execute_actions())
# 7. 关系演化阶段
events.extend(await self._phase_evolve_relations())
# 8. 结算死亡
events.extend(self._phase_resolve_death())
# 9. 年龄与新生
events.extend(self._phase_update_age_and_birth())
# 10. 被动结算(时间效果+奇遇)
events.extend(await self._phase_passive_effects())
# 11. 绰号生成
events.extend(await self._phase_nickname_generation())
# 12. 更新天地灵机
events.extend(self._phase_update_celestial_phenomenon())
# 13. 日志
# 去重:基于 ID 去重防止同一个事件对象或相同ID的事件被多次添加
# 常见情况Gathering 既返回了事件,又将其加入了 Avatar 的 pending_events
unique_events = {}
for e in events:
if e.id not in unique_events:
unique_events[e.id] = e
# 保持原有顺序Python 3.7+ dict 保持插入序)
events = list(unique_events.values())
# 统一写入事件管理器
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)
# 14. 时间推进
self.world.month_stamp = self.world.month_stamp + 1
return events