diff --git a/README.md b/README.md index bb9cb23..03adb03 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ ### 🏗️ 基础系统 - ✅ 基础世界地图系统 - ✅ 多样化地形类型(平原、山脉、森林、沙漠、水域等) -- ✅ 时间系统(年月日历) +- ✅ 时间系统(年月时间戳) - ✅ 前端显示界面(pygame) - ✅ 基础模拟器框架 - ✅ 项目文档(README) @@ -64,6 +64,9 @@ - ✅ 基础移动动作 - ✅ 动作执行框架 - ✅ 有明确规则的定义动作(Defined Action) +- ✅ 长动作执行和结算系统 + - ✅ 支持多月份持续的动作(如修炼、突破、游戏等) + - ✅ 动作完成时的自动结算机制 - [ ] 影响人际关系的LLM动作(LLM Action) ### 🎭 事件系统 diff --git a/src/classes/ai.py b/src/classes/ai.py index d129998..26c7694 100644 --- a/src/classes/ai.py +++ b/src/classes/ai.py @@ -14,6 +14,7 @@ from src.classes.root import corres_essence_type from src.classes.action import ACTION_SPACE_STR from src.classes.event import Event, NULL_EVENT from src.utils.llm import get_ai_prompt_and_call_llm +from src.classes.typings import ACTION_NAME, ACTION_PARAMS, ACTION_PAIR if TYPE_CHECKING: from src.classes.avatar import Avatar @@ -25,7 +26,7 @@ class AI(ABC): def __init__(self, avatar: Avatar): self.avatar = avatar - def decide(self, world: World) -> tuple[str, dict, Event]: + def decide(self, world: World) -> tuple[ACTION_NAME, ACTION_PARAMS, Event]: """ 决定做什么,同时生成对应的事件 """ @@ -39,7 +40,7 @@ class AI(ABC): return action_name, action_params, event @abstractmethod - def _decide(self, world: World) -> tuple[str, dict]: + def _decide(self, world: World) -> ACTION_PAIR: """ 决策逻辑:决定执行什么动作和参数 由子类实现具体的决策逻辑 @@ -50,7 +51,7 @@ class RuleAI(AI): """ 规则AI """ - def _decide(self, world: World) -> tuple[str, dict]: + def _decide(self, world: World) -> ACTION_PAIR: """ 决策逻辑:决定执行什么动作和参数 先做一个简单的: @@ -92,7 +93,7 @@ class LLMAI(AI): 不能每个单步step都调用一次LLM来决定下一步做什么。这样子一方面动作一直乱变,另一方面也太费token了。 decide的作用是,拉取既有的动作链(如果没有了就call_llm),再根据动作链决定动作,以及动作之间的衔接。 """ - def _decide(self, world: World) -> tuple[str, dict]: + def _decide(self, world: World) -> ACTION_PAIR: """ 决策逻辑:通过LLM决定执行什么动作和参数 """ diff --git a/src/classes/avatar.py b/src/classes/avatar.py index eae8778..8d3c82b 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -3,14 +3,15 @@ from dataclasses import dataclass, field from enum import Enum from typing import Optional -from src.classes.calendar import Month, Year, MonthStamp +from src.classes.calendar import MonthStamp from src.classes.action import Action, ALL_ACTION_CLASSES from src.classes.world import World from src.classes.tile import Tile, Region -from src.classes.cultivation import CultivationProgress, Realm +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 +from src.classes.typings import ACTION_NAME, ACTION_PARAMS, ACTION_PAIR from src.classes.ai import AI, RuleAI, LLMAI from src.classes.persona import Persona, personas_by_id @@ -28,6 +29,9 @@ gender_strs = { Gender.FEMALE: "女", } +# 历史动作对的最大数量 +MAX_HISTORY_ACTIONS = 3 + @dataclass class Avatar: """ @@ -48,8 +52,8 @@ class Avatar: root: Root = field(default_factory=lambda: random.choice(list(Root))) persona: Persona = field(default_factory=lambda: random.choice(list(personas_by_id.values()))) ai: AI = None - action_be_executed: Optional[Action] = None - action_parmas_be_executed: Optional[dict] = None + cur_action_pair: Optional[ACTION_PAIR] = None + history_action_pairs: list[ACTION_PAIR] = field(default_factory=list) def __post_init__(self): """ @@ -66,7 +70,7 @@ class Avatar: """ return f"Avatar(id={self.id}, 性别={self.gender}, 年龄={self.age}, name={self.name}, 区域={self.tile.region.name}, 灵根={self.root.value}, 境界={self.cultivation_progress})" - def create_action(self, action_name: str) -> Action: + def create_action(self, action_name: ACTION_NAME) -> Action: """ 根据动作名称创建新的action实例 @@ -95,21 +99,41 @@ class Avatar: """ event = NULL_EVENT - if self.action_be_executed is None: + if self.cur_action_pair is None: # 决定动作时生成事件 action_name, action_args, event = self.ai.decide(self.world) - self.action_be_executed = self.create_action(action_name) - self.action_parmas_be_executed = action_args + action = self.create_action(action_name) + self.cur_action_pair = (action, action_args) # 纯粹执行动作,不产生事件 - self.action_be_executed.execute(**self.action_parmas_be_executed) + action, action_params = self.cur_action_pair + action.execute(**action_params) - if self.action_be_executed.is_finished(**self.action_parmas_be_executed): - self.action_be_executed = None - self.action_parmas_be_executed = None + if action.is_finished(**action_params): + # 将完成的动作对添加到历史记录中 + self._add_to_history(self.cur_action_pair) return event + def _add_to_history(self, action_pair: ACTION_PAIR) -> None: + """ + 将完成的动作对添加到历史记录中 + + Args: + action_pair: 要添加的动作对 + + 注意: + - 如果历史记录达到上限,会丢弃最老的记录 + - 新的记录会被添加到列表末尾 + """ + # 添加新的动作对到历史记录 + self.history_action_pairs.append(action_pair) + self.cur_action_pair = None + + # 如果超过上限,移除最老的记录 + if len(self.history_action_pairs) > MAX_HISTORY_ACTIONS: + self.history_action_pairs.pop(0) + def update_cultivation(self, new_level: int): """ 更新修仙进度,并在境界提升时更新寿命 diff --git a/src/classes/typings.py b/src/classes/typings.py new file mode 100644 index 0000000..38f6da1 --- /dev/null +++ b/src/classes/typings.py @@ -0,0 +1,5 @@ +from src.classes.action import Action + +ACTION_NAME = str +ACTION_PARAMS = dict +ACTION_PAIR = tuple[Action, ACTION_PARAMS] \ No newline at end of file diff --git a/src/front/front.py b/src/front/front.py index 4d68e7f..447ffe5 100644 --- a/src/front/front.py +++ b/src/front/front.py @@ -419,6 +419,14 @@ class Front: f"个性: {avatar.persona.name}", f"位置: ({avatar.pos_x}, {avatar.pos_y})", ] + + # 添加历史动作信息 + lines.append("") # 空行分隔 + lines.append("历史动作:") + for i, (action, params) in enumerate(avatar.history_action_pairs): + action_name = action.__class__.__name__ + lines.append(f" {i+1}. {action_name}") + self._draw_tooltip(lines, *self.pygame.mouse.get_pos(), self.tooltip_font) def _draw_tooltip_for_region(self, region, mouse_x: int, mouse_y: int):