fix bugs
This commit is contained in:
@@ -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,不直接由调度器执行
|
||||
|
||||
33
src/classes/action_runtime.py
Normal file
33
src/classes/action_runtime.py
Normal 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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
# 保持一次性效果,不参与新的调度接口
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user