This commit is contained in:
bridge
2025-09-24 22:05:08 +08:00
parent e5d8bf9bf4
commit 30f91d264c
8 changed files with 332 additions and 284 deletions

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from enum import Enum
from typing import TYPE_CHECKING
import random
import json
@@ -17,6 +18,8 @@ from src.classes.battle import decide_battle
if TYPE_CHECKING:
from src.classes.avatar import Avatar
from src.classes.world import World
from src.classes.animal import Animal
from src.classes.plant import Plant
def long_action(step_month: int):
@@ -125,28 +128,24 @@ class ChunkActionMixin():
class ActualActionMixin():
"""
实际的可以被规则/LLM调用让avatar去执行的动作。
不一定是多个step也有可能就一个step
不一定是多个step也有可能就一个step
新接口:子类必须实现 can_start/start/step/finish。
"""
@abstractmethod
def is_finished(self) -> bool:
"""
判断动作是否完成
"""
pass
@abstractmethod
def get_event(self, *args, **kwargs) -> Event:
"""
获取动作开始时的事件
"""
def can_start(self, **params) -> bool:
pass
@property
@abstractmethod
def is_doable(self) -> bool:
"""
判断动作是否可以执行
"""
def start(self, **params) -> Event | None:
pass
@abstractmethod
def step(self, **params) -> tuple[str, list[Event]]: # status: "running" | "completed" | "failed" | "blocked"
pass
@abstractmethod
def finish(self, **params) -> list[Event]:
pass
@@ -203,19 +202,10 @@ class MoveToRegion(DefineAction, ActualActionMixin):
delta_y = max(-step, min(step, delta_y))
Move(self.avatar, self.world).execute(delta_x, delta_y)
def is_finished(self, region: Region|str) -> bool:
"""
判断动作是否完成
"""
if isinstance(region, str):
from src.classes.region import regions_by_name
region = regions_by_name[region]
return self.avatar.is_in_region(region)
def get_event(self, region: Region|str) -> Event:
"""
获取移动动作开始时的事件
"""
def can_start(self, region: Region|str|None = None) -> bool:
return True
def start(self, region: Region|str) -> Event:
if isinstance(region, str):
region_name = region
from src.classes.region import regions_by_name
@@ -227,12 +217,17 @@ class MoveToRegion(DefineAction, ActualActionMixin):
region_name = str(region)
return Event(self.world.month_stamp, f"{self.avatar.name} 开始移动向 {region_name}")
@property
def is_doable(self) -> bool:
"""
判断移动到区域动作是否可以执行
"""
return True
def step(self, region: Region|str) -> tuple[str, list[Event]]:
self.execute(region=region)
# 完成条件:到达目标区域
if isinstance(region, str):
from src.classes.region import regions_by_name
region = regions_by_name[region]
done = self.avatar.is_in_region(region)
return ("completed" if done else "running"), []
def finish(self, region: Region|str) -> list[Event]:
return []
class MoveToAvatar(DefineAction, ActualActionMixin):
@@ -265,20 +260,28 @@ class MoveToAvatar(DefineAction, ActualActionMixin):
delta_y = max(-step, min(step, delta_y))
Move(self.avatar, self.world).execute(delta_x, delta_y)
def is_finished(self, avatar_name: str) -> bool:
target = self._get_target(avatar_name)
if target is None:
return True
return self.avatar.pos_x == target.pos_x and self.avatar.pos_y == target.pos_y
def can_start(self, avatar_name: str|None = None) -> bool:
return True
def get_event(self, avatar_name: str) -> Event:
def start(self, avatar_name: str) -> Event:
target = self._get_target(avatar_name)
target_name = target.name if target is not None else avatar_name
return Event(self.world.month_stamp, f"{self.avatar.name} 开始移动向 {target_name}")
@property
def is_doable(self) -> bool:
return True
def step(self, avatar_name: str) -> tuple[str, list[Event]]:
self.execute(avatar_name=avatar_name)
target = None
try:
target = self._get_target(avatar_name)
except Exception:
target = None
if target is None:
return "completed", []
done = self.avatar.pos_x == target.pos_x and self.avatar.pos_y == target.pos_y
return ("completed" if done else "running"), []
def finish(self, avatar_name: str) -> list[Event]:
return []
@long_action(step_month=10)
class Cultivate(DefineAction, ActualActionMixin):
@@ -306,20 +309,12 @@ class Cultivate(DefineAction, ActualActionMixin):
根据essence的密度计算获得的exp。
公式为base * essence_density
"""
if self.avatar.cultivation_progress.is_in_bottleneck():
return 0
base = 100
return base * essence_density
def get_event(self) -> Event:
"""
获取修炼动作开始时的事件
"""
return Event(self.world.month_stamp, f"{self.avatar.name}{self.avatar.tile.region.name} 开始修炼")
@property
def is_doable(self) -> bool:
"""
判断修炼动作是否可以执行
"""
def can_start(self) -> bool:
root = self.avatar.root
region = self.avatar.tile.region
essence_types = get_essence_types_for_root(root)
@@ -331,6 +326,18 @@ class Cultivate(DefineAction, ActualActionMixin):
return False
return True
def start(self) -> Event:
return Event(self.world.month_stamp, f"{self.avatar.name}{self.avatar.tile.region.name} 开始修炼")
def step(self) -> tuple[str, list[Event]]:
self.execute()
# 使用 long_action 注入的 is_finished
done = getattr(self, "is_finished")()
return ("completed" if done else "running"), []
def finish(self) -> list[Event]:
return []
# 突破境界class
@long_action(step_month=1)
class Breakthrough(DefineAction, ActualActionMixin):
@@ -392,18 +399,19 @@ class Breakthrough(DefineAction, ActualActionMixin):
self.avatar.mp.add_max(mp_increase)
self.avatar.mp.recover(mp_increase) # 突破时完全恢复MP
def get_event(self) -> Event:
"""
获取突破动作开始时的事件
"""
def can_start(self) -> bool:
return self.avatar.cultivation_progress.can_break_through()
def start(self) -> Event:
return Event(self.world.month_stamp, f"{self.avatar.name} 开始尝试突破境界")
@property
def is_doable(self) -> bool:
"""
判断突破动作是否可以执行
"""
return self.avatar.cultivation_progress.can_break_through()
def step(self) -> tuple[str, list[Event]]:
self.execute()
done = getattr(self, "is_finished")()
return ("completed" if done else "running"), []
def finish(self) -> list[Event]:
return []
@long_action(step_month=6)
class Play(DefineAction, ActualActionMixin):
@@ -422,15 +430,19 @@ class Play(DefineAction, ActualActionMixin):
# 比如增加心情值、减少压力等
pass
def get_event(self) -> Event:
"""
获取游戏娱乐动作开始时的事件
"""
def can_start(self) -> bool:
return True
def start(self) -> Event:
return Event(self.world.month_stamp, f"{self.avatar.name} 开始玩耍")
@property
def is_doable(self) -> bool:
return True
def step(self) -> tuple[str, list[Event]]:
self.execute()
done = getattr(self, "is_finished")()
return ("completed" if done else "running"), []
def finish(self) -> list[Event]:
return []
@long_action(step_month=6)
@@ -474,18 +486,7 @@ class Hunt(DefineAction, ActualActionMixin):
"""
return 1.0 # 100%成功率
def get_event(self) -> Event:
"""
获取狩猎动作开始时的事件
"""
region = self.avatar.tile.region
return Event(self.world.month_stamp, f"{self.avatar.name}{region.name} 开始狩猎")
@property
def is_doable(self) -> bool:
"""
判断是否可以狩猎必须在有动物的普通区域且avatar的境界必须大于等于动物的境界
"""
def can_start(self) -> bool:
region = self.avatar.tile.region
if not isinstance(region, NormalRegion):
return False
@@ -494,6 +495,18 @@ class Hunt(DefineAction, ActualActionMixin):
return False
return True
def start(self) -> Event:
region = self.avatar.tile.region
return Event(self.world.month_stamp, f"{self.avatar.name}{region.name} 开始狩猎")
def step(self) -> tuple[str, list[Event]]:
self.execute()
done = getattr(self, "is_finished")()
return ("completed" if done else "running"), []
def finish(self) -> list[Event]:
return []
@long_action(step_month=6)
class Harvest(DefineAction, ActualActionMixin):
@@ -536,18 +549,7 @@ class Harvest(DefineAction, ActualActionMixin):
"""
return 1.0 # 100%成功率
def get_event(self) -> Event:
"""
获取采集动作开始时的事件
"""
region = self.avatar.tile.region
return Event(self.world.month_stamp, f"{self.avatar.name}{region.name} 开始采集")
@property
def is_doable(self) -> bool:
"""
判断是否可以采集必须在有植物的普通区域且avatar的境界必须大于等于植物的境界
"""
def can_start(self) -> bool:
region = self.avatar.tile.region
if not isinstance(region, NormalRegion):
return False
@@ -556,6 +558,18 @@ class Harvest(DefineAction, ActualActionMixin):
return False
return True
def start(self) -> Event:
region = self.avatar.tile.region
return Event(self.world.month_stamp, f"{self.avatar.name}{region.name} 开始采集")
def step(self) -> tuple[str, list[Event]]:
self.execute()
done = getattr(self, "is_finished")()
return ("completed" if done else "running"), []
def finish(self) -> list[Event]:
return []
@long_action(step_month=1)
class Sold(DefineAction, ActualActionMixin):
@@ -593,14 +607,28 @@ class Sold(DefineAction, ActualActionMixin):
self.avatar.magic_stone = self.avatar.magic_stone + total_gain
def get_event(self, item_name: str) -> Event:
def can_start(self, item_name: str|None = None) -> bool:
region = self.avatar.tile.region
if not isinstance(region, CityRegion):
return False
if item_name is None:
# 用于动作空间:只要背包非空即可
return bool(self.avatar.items)
item = items_by_name.get(item_name)
if item is None:
return False
return self.avatar.get_item_quantity(item) > 0
def start(self, item_name: str) -> Event:
return Event(self.world.month_stamp, f"{self.avatar.name} 在城镇出售 {item_name}")
@property
def is_doable(self) -> bool:
# 只允许在城镇且背包非空时出现在动作空间
region = self.avatar.tile.region
return isinstance(region, CityRegion) and bool(self.avatar.items)
def step(self, item_name: str) -> tuple[str, list[Event]]:
self.execute(item_name=item_name)
# 一次性动作
return "completed", []
def finish(self, item_name: str) -> list[Event]:
return []
class Battle(DefineAction, ChunkActionMixin):
@@ -619,10 +647,4 @@ class Battle(DefineAction, ChunkActionMixin):
# 简化失败者HP小额扣减
if hasattr(loser, "hp"):
loser.hp.reduce(10)
def is_finished(self, avatar_name: str) -> bool:
return True
def get_event(self, avatar_name: str) -> Event:
return Event(self.world.month_stamp, f"{self.avatar.name}{avatar_name} 进行对战")
@property
def is_doable(self) -> bool:
return True
# Battle 作为 ChunkAction不直接由调度器执行

View File

@@ -0,0 +1,33 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional, Dict, Any
# 运行时动作状态(字符串,便于与现有实现对接)
ActionStatus = str # "running" | "completed" | "failed" | "blocked"
@dataclass
class ActionPlan:
"""
计划中的动作项:尚未提交执行。
仅包含 class 名与参数,外加可选的调度策略字段。
"""
action_name: str
params: Dict[str, Any]
priority: int = 0
expiry_month: Optional[int] = None # 到期月戳None 为不过期
max_retries: int = 0
attempted: int = 0
@dataclass
class ActionInstance:
"""
已提交并开始执行的动作实例。
"""
action: Any # src.classes.action.Action
params: Dict[str, Any]
status: ActionStatus = "running"

View File

@@ -49,10 +49,7 @@ ALL_ACTUAL_ACTION_CLASSES = [
Hunt,
Harvest,
Sold,
DriveAway,
AttackInteract,
MoveAwayFromAvatar,
MoveAwayFromRegion,
# 互动类/反馈类作为即时落地,不进入可选择动作空间
]
ALL_ACTION_NAMES = [action.__name__ for action in ALL_ACTION_CLASSES]

View File

@@ -49,11 +49,8 @@ class AI(ABC):
else:
action_name, action_params, avatar_thinking, objective = result # type: ignore
action_name_params_pairs = [(action_name, action_params)]
# 只为队列中的第一个动作生成事件
first_action_name, first_action_params = action_name_params_pairs[0]
action = avatar.create_action(first_action_name)
event = action.get_event(**first_action_params)
results[avatar] = (action_name_params_pairs, avatar_thinking, objective, event)
# 不在决策阶段生成开始事件,提交阶段统一触发
results[avatar] = (action_name_params_pairs, avatar_thinking, objective, NULL_EVENT)
return results

View File

@@ -14,7 +14,8 @@ from src.classes.cultivation import CultivationProgress
from src.classes.root import Root
from src.classes.age import Age
from src.classes.event import NULL_EVENT, Event
from src.classes.typings import ACTION_NAME, ACTION_PARAMS, ACTION_PAIR, ACTION_NAME_PARAMS_PAIRS, ACTION_NAME_PARAMS_PAIR
from src.classes.typings import ACTION_NAME, ACTION_PARAMS, ACTION_NAME_PARAMS_PAIRS, ACTION_NAME_PARAMS_PAIR
from src.classes.action_runtime import ActionPlan, ActionInstance
from src.classes.persona import Persona, personas_by_id, get_random_compatible_personas
from src.classes.item import Item
@@ -60,10 +61,10 @@ class Avatar:
root: Root = field(default_factory=lambda: random.choice(list(Root)))
personas: List[Persona] = field(default_factory=list)
cur_action_pair: Optional[ACTION_PAIR] = None
history_events: List[Event] = field(default_factory=list)
_pending_events: List[Event] = field(default_factory=list)
next_actions: ACTION_NAME_PARAMS_PAIRS = field(default_factory=list)
current_action: Optional[ActionInstance] = None
planned_actions: List[ActionPlan] = field(default_factory=list)
thinking: str = ""
objective: str = ""
magic_stone: MagicStone = field(default_factory=lambda: MagicStone(0)) # 灵石,即货币
@@ -130,88 +131,101 @@ class Avatar:
"""
if not action_name_params_pairs:
return
first_action_name, first_action_params = action_name_params_pairs[0]
action = self.create_action(first_action_name)
self.thinking = avatar_thinking
self.objective = objective
self.cur_action_pair = (action, first_action_params)
# 余下的动作进入队列
if len(action_name_params_pairs) > 1:
self.next_actions.extend(action_name_params_pairs[1:])
# 转为计划并入队(不立即提交,交由提交阶段统一触发开始事件)
plans: List[ActionPlan] = [ActionPlan(name, params) for name, params in action_name_params_pairs]
self.planned_actions.extend(plans)
def clear_next_actions(self) -> None:
"""
清空后续动作队列(不影响当前动作)。
"""
self.next_actions.clear()
def clear_plans(self) -> None:
self.planned_actions.clear()
def has_next_actions(self) -> bool:
return len(self.next_actions) > 0
def has_plans(self) -> bool:
return len(self.planned_actions) > 0
def pop_next_action_and_set_current(self) -> Optional[Event]:
def commit_next_plan(self) -> Optional[Event]:
"""
从队列中取出下一个动作并设置为当前动作,同时返回开始事件。
若队列为空则返回None。
提交下一个可启动的计划为当前动作返回开始事件(若有)
"""
if not self.next_actions:
if self.current_action is not None:
return None
action_name, action_params = self.next_actions.pop(0)
action = self.create_action(action_name)
while not action.is_doable and self.next_actions:
action_name, action_params = self.next_actions.pop(0)
action = self.create_action(action_name)
while self.planned_actions:
plan = self.planned_actions.pop(0)
action = self.create_action(plan.action_name)
# 再验证
can_start = True
try:
can_start = bool(action.can_start(**plan.params))
except TypeError:
can_start = bool(action.can_start())
if not can_start:
continue
# 启动
try:
start_event = action.start(**plan.params)
except TypeError:
start_event = action.start()
self.current_action = ActionInstance(action=action, params=plan.params, status="running")
return start_event
return None
if not action.is_doable:
def peek_next_plan(self) -> Optional[ActionPlan]:
if not self.planned_actions:
return None
return self.planned_actions[0]
self.cur_action_pair = (action, action_params)
async def tick_action(self) -> List[Event]:
"""
推进当前动作一步;返回过程中由动作内部产生的事件(通过 add_event 收集)。
"""
if self.current_action is None:
return []
action = self.current_action.action
params = self.current_action.params
try:
event = action.get_event(**action_params)
status, mid_events = action.step(**params)
except TypeError:
# 兼容无参数的 get_event 定义
event = action.get_event()
return event
def peek_next_action(self) -> Optional[ACTION_NAME_PARAMS_PAIR]:
"""
查看下一个动作但不弹出。
"""
if not self.next_actions:
return None
return self.next_actions[0]
def is_next_action_doable(self) -> bool:
"""
判断队列中的下一个动作当前是否可执行。
若没有下一个动作返回False。
"""
pair = self.peek_next_action()
if pair is None:
return False
action_name, _ = pair
action = self.create_action(action_name)
doable = action.is_doable
assert isinstance(doable, bool)
del action
return doable
async def act(self) -> List[Event]:
"""
角色执行动作。
注意这里只负责执行,不负责决定做什么动作。
事件只在决定动作时产生,执行过程不产生事件
"""
# 纯粹执行动作。具体事件由决定阶段或动作内部通过 add_event 添加
action, action_params = self.cur_action_pair
action.execute(**action_params)
if action.is_finished(**action_params):
# 完成后清空当前动作
self.cur_action_pair = None
# 返回并清空待派发事件
status, mid_events = action.step()
if status == "completed":
try:
finish_events = action.finish(**params)
except TypeError:
finish_events = action.finish()
self.current_action = None
if finish_events:
# 允许 finish 直接返回事件(极少用),统一并入 pending
for e in finish_events:
self._pending_events.append(e)
# 合并动作返回的事件(通常为空)
if mid_events:
for e in mid_events:
self._pending_events.append(e)
events, self._pending_events = self._pending_events, []
return events
def _commit_specific_plan(self, plan: ActionPlan) -> bool:
"""
尝试提交指定计划为当前动作,不返回事件(用于内部/立即触发)。
"""
if self.current_action is not None:
# 已有当前动作则直接入队
self.planned_actions.insert(0, plan)
return False
action = self.create_action(plan.action_name)
try:
can_start = bool(action.can_start(**plan.params))
except TypeError:
can_start = bool(action.can_start())
if not can_start:
# 无法启动则丢入计划队列等待
self.planned_actions.insert(0, plan)
return False
try:
_ = action.start(**plan.params)
except TypeError:
_ = action.start()
self.current_action = ActionInstance(action=action, params=plan.params, status="running")
return True
def update_cultivation(self, new_level: int):
"""
@@ -353,7 +367,15 @@ class Avatar:
获取动作空间
"""
actual_actions = [self.create_action(action_cls_name) for action_cls_name in ALL_ACTUAL_ACTION_NAMES]
doable_actions = [action for action in actual_actions if action.is_doable]
doable_actions: list[Action] = []
for action in actual_actions:
# 用 can_start 的无参形式,用于“是否在动作空间中显示”
try:
if action.can_start():
doable_actions.append(action)
except TypeError:
# 无参展示判定失败时,简单跳过(避免在空间中展示需要参数才能判定的动作)
continue
action_space = [action.name for action in doable_actions]
return action_space

View File

@@ -40,24 +40,6 @@ class Stage(Enum):
LEVELS_PER_REALM = 30
LEVELS_PER_STAGE = 10
LEVEL_TO_REALM = {
0: Realm.Qi_Refinement,
30: Realm.Foundation_Establishment,
60: Realm.Core_Formation,
90: Realm.Nascent_Soul,
}
LEVEL_TO_STAGE = {
0: Stage.Early_Stage,
10: Stage.Middle_Stage,
20: Stage.Late_Stage,
}
LEVEL_TO_BREAK_THROUGH = {
30: Realm.Foundation_Establishment,
60: Realm.Core_Formation,
90: Realm.Nascent_Soul,
}
REALM_TO_MOVE_STEP = {
Realm.Qi_Refinement: 1,
Realm.Foundation_Establishment: 2,
@@ -83,20 +65,30 @@ class CultivationProgress:
self.realm = self.get_realm(level)
self.stage = self.get_stage(level)
def get_realm(self, level: int) -> str:
"""获取境界"""
for level_threshold, realm in reversed(list(LEVEL_TO_REALM.items())):
if level >= level_threshold:
return realm
return Realm.Qi_Refinement
def get_realm(self, level: int) -> Realm:
"""获取境界(算术推导,不依赖映射表)"""
if level <= 0:
return Realm.Qi_Refinement
realm_index = (level - 1) // LEVELS_PER_REALM # 0-based index
order: tuple[Realm, ...] = (
Realm.Qi_Refinement,
Realm.Foundation_Establishment,
Realm.Core_Formation,
Realm.Nascent_Soul,
)
return order[min(realm_index, len(order) - 1)]
def get_stage(self, level: int) -> Stage:
"""获取阶段"""
_level = level % LEVELS_PER_REALM
for level_threshold, stage in reversed(list(LEVEL_TO_STAGE.items())):
if _level >= level_threshold:
return stage
return Stage.Early_Stage
"""获取阶段算术推导1-10前期11-20中期21-30后期"""
if level <= 0:
return Stage.Early_Stage
stage_index = ((level - 1) % LEVELS_PER_REALM) // LEVELS_PER_STAGE
order: tuple[Stage, ...] = (
Stage.Early_Stage,
Stage.Middle_Stage,
Stage.Late_Stage,
)
return order[min(stage_index, len(order) - 1)]
def get_move_step(self) -> int:
"""
@@ -136,8 +128,9 @@ class CultivationProgress:
# 基础经验值计算
exp_required = base_exp + (next_level - 1) * increment
# 境界加成:每跨越一个境界额外增加1000点经验值
realm_bonus = (next_level // 30) * 1000
# 境界加成:按 next_level 所处境界(延后入境界,算术推导)增加
realm_index = (max(1, next_level) - 1) // LEVELS_PER_REALM
realm_bonus = realm_index * 1000
return exp_required + realm_bonus
@@ -162,18 +155,25 @@ class CultivationProgress:
如果升级了则返回True
"""
self.exp += exp_amount
# 检查是否可以升级
if self.is_level_up():
leveled_up = False
# 支持多级升级但在瓶颈30/60/90…停下等待突破
while True:
# 瓶颈位level > 0 且 level % LEVELS_PER_REALM == 0
if self.is_in_bottleneck():
break
if not self.is_level_up():
break
required_exp = self.get_exp_required()
self.exp -= required_exp
self.level += 1
# 更新境界和阶段
self.realm = self.get_realm(self.level)
self.stage = self.get_stage(self.level)
return True
return False
leveled_up = True
if self.is_in_bottleneck():
break
return leveled_up
def break_through(self):
"""
@@ -186,12 +186,9 @@ class CultivationProgress:
def is_in_bottleneck(self) -> bool:
"""
是否处于瓶颈期。
如果级别在LEVEL_TO_BREAK_THROUGH中同时realm不是该级别对应的realm则处于瓶颈期
处于每个大境界的第 30、60、90…级时level > 0 且 level % LEVELS_PER_REALM == 0
"""
for level_threshold, realm in LEVEL_TO_BREAK_THROUGH.items():
if self.level == level_threshold and self.realm != realm:
return True
return False
return self.level > 0 and (self.level % LEVELS_PER_REALM == 0)
def can_break_through(self) -> bool:
"""

View File

@@ -59,8 +59,14 @@ class MutualAction(DefineAction, LLMAction):
"""
将反馈决定落地为目标角色的立即动作(清空后加载单步动作链)。
"""
# 使用已有的加载动作链接口,立即设置为当前动作
# 先加载为计划
target_avatar.load_decide_result_chain([(action_name, action_params)], target_avatar.thinking, "")
# 立即提交为当前动作,触发开始事件
start_event = target_avatar.commit_next_plan()
if start_event is not None:
# 事件广播到双方(进入侧边栏与历史)
self.avatar.add_event(start_event)
target_avatar.add_event(start_event)
def _settle_feedback(self, target_avatar: "Avatar", feedback_name: str) -> None:
"""
@@ -93,12 +99,12 @@ class MutualAction(DefineAction, LLMAction):
# 挂到目标的thinking上面向UI/日志),并执行反馈落地
target_avatar.thinking = thinking
# 发起事件(进入侧边栏与双方历史)
start_event = self.get_event(target_avatar)
start_event = Event(self.world.month_stamp, f"{self.avatar.name}{target_avatar.name} 发起 {getattr(self, 'ACTION_NAME', self.name)}")
self.avatar.add_event(start_event)
target_avatar.add_event(start_event)
# 1) 先清空目标后续动作(仅清空队列,不动当前动作)
if hasattr(target_avatar, "clear_next_actions"):
target_avatar.clear_next_actions()
# 1) 先清空目标后续计划(仅清空队列,不动当前动作)
if hasattr(target_avatar, "clear_plans"):
target_avatar.clear_plans()
# 2) 再结算反馈映射为对应动作
self._settle_feedback(target_avatar, feedback)
# 3) 反馈事件(进入侧边栏与双方历史)
@@ -109,17 +115,8 @@ class MutualAction(DefineAction, LLMAction):
self._apply_feedback(target_avatar, feedback)
# 互动力一般是一次性的即时动作
def is_finished(self, target_avatar: "Avatar") -> bool: # type: ignore[override]
return True
def get_event(self, target_avatar: "Avatar|str") -> Event: # type: ignore[override]
target_name = target_avatar if isinstance(target_avatar, str) else target_avatar.name
return Event(self.world.month_stamp, f"{self.avatar.name}{target_name} 发起 {getattr(self, 'ACTION_NAME', self.name)}")
@property
def is_doable(self) -> bool: # type: ignore[override]
# 一般来讲必须和对象avatar在同一区域
return True
# 互动类行为仍保持一次性效果,由自身执行时发事件
# 不接入新的调度器接口
class DriveAway(MutualAction, ActualActionMixin):
@@ -177,13 +174,7 @@ class MoveAwayFromAvatar(DefineAction, ActualActionMixin):
self.avatar.pos_x = nx
self.avatar.pos_y = ny
self.avatar.tile = self.world.map.get_tile(nx, ny)
def is_finished(self, avatar_name: str) -> bool:
return True
def get_event(self, avatar_name: str) -> Event:
return Event(self.world.month_stamp, f"{self.avatar.name} 远离 {avatar_name}")
@property
def is_doable(self) -> bool:
return True
# 保持一次性效果,不参与新的调度接口
class MoveAwayFromRegion(DefineAction, ActualActionMixin):
@@ -200,10 +191,4 @@ class MoveAwayFromRegion(DefineAction, ActualActionMixin):
self.avatar.pos_x = nx
self.avatar.pos_y = ny
self.avatar.tile = self.world.map.get_tile(nx, ny)
def is_finished(self, region: str) -> bool:
return True
def get_event(self, region: str) -> Event:
return Event(self.world.month_stamp, f"{self.avatar.name} 离开 {region}")
@property
def is_doable(self) -> bool:
return True
# 保持一次性效果,不参与新的调度接口

View File

@@ -26,37 +26,32 @@ class Simulator:
events = [] # list of Event
death_avatar_ids = [] # list of str
# 决定动作行为
# 决策阶段:仅对需要新计划的角色调用 AI当前无动作且无计划
avatars_to_decide = []
for avatar in list(self.world.avatar_manager.avatars.values()):
if avatar.cur_action_pair is None:
# 若有排队动作但当前不可执行:丢弃之后的所有动作
if avatar.has_next_actions():
if not avatar.is_next_action_doable():
avatar.next_actions.clear()
avatars_to_decide.append(avatar)
else:
event = avatar.pop_next_action_and_set_current()
if event is not None and not is_null_event(event):
events.append(event)
else:
avatars_to_decide.append(avatar)
if avatar.current_action is None and not avatar.has_plans():
avatars_to_decide.append(avatar)
if CONFIG.ai.mode == "llm":
ai = llm_ai
else:
ai = rule_ai
if avatars_to_decide:
if avatars_to_decide:
decide_results = await ai.decide(self.world, avatars_to_decide)
for avatar, result in decide_results.items():
action_name_params_pairs, avatar_thinking, objective, event = result
action_name_params_pairs, avatar_thinking, objective, _event = result
# 仅入队计划,不在此处添加开始事件,避免与提交阶段重复
avatar.load_decide_result_chain(action_name_params_pairs, avatar_thinking, objective)
if not is_null_event(event):
events.append(event)
# 结算角色行为
# 提交阶段:为空闲角色提交计划中的下一个可执行动作
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)
# 执行阶段:推进所有有当前动作的角色
for avatar_id, avatar in self.world.avatar_manager.avatars.items():
# 只在当前有动作时执行当前动作,不再检查“下一个动作”的可执行性
new_events = await avatar.act()
new_events = await avatar.tick_action()
if new_events:
events.extend(new_events)