diff --git a/src/classes/ai.py b/src/classes/ai.py index 7c5b117..7d991e5 100644 --- a/src/classes/ai.py +++ b/src/classes/ai.py @@ -26,21 +26,21 @@ class AI(ABC): def __init__(self, avatar: Avatar): self.avatar = avatar - async def decide(self, world: World) -> tuple[ACTION_NAME, ACTION_PARAMS, Event]: + async def decide(self, world: World) -> tuple[ACTION_NAME, ACTION_PARAMS, str, Event]: """ 决定做什么,同时生成对应的事件 """ # 先决定动作和参数 - action_name, action_params = await self._decide(world) + action_name, action_params, avatar_thinking = await self._decide(world) # 获取动作对象并生成事件 action = self.avatar.create_action(action_name) event = action.get_event(**action_params) - return action_name, action_params, event + return action_name, action_params, avatar_thinking, event @abstractmethod - async def _decide(self, world: World) -> ACTION_PAIR: + async def _decide(self, world: World) -> tuple[ACTION_NAME, ACTION_PARAMS, str]: """ 决策逻辑:决定执行什么动作和参数 由子类实现具体的决策逻辑 @@ -51,7 +51,7 @@ class RuleAI(AI): """ 规则AI """ - async def _decide(self, world: World) -> ACTION_PAIR: + async def _decide(self, world: World) -> tuple[ACTION_NAME, ACTION_PARAMS, str]: """ 决策逻辑:决定执行什么动作和参数 先做一个简单的: @@ -62,15 +62,15 @@ class RuleAI(AI): 5. 如果需要突破境界了,则突破境界 """ if random.random() < 0.1: - return "Play", {} + return "Play", {}, "" best_region = self.get_best_region(list(world.map.regions.values())) if self.avatar.is_in_region(best_region): if self.avatar.cultivation_progress.can_break_through(): - return "Breakthrough", {} + return "Breakthrough", {}, "" else: - return "Cultivate", {} + return "Cultivate", {}, "" else: - return "MoveToRegion", {"region": best_region.name} + return "MoveToRegion", {"region": best_region.name}, "" def get_best_region(self, regions: list[Region]) -> Region: """ @@ -93,7 +93,7 @@ class LLMAI(AI): 不能每个单步step都调用一次LLM来决定下一步做什么。这样子一方面动作一直乱变,另一方面也太费token了。 decide的作用是,拉取既有的动作链(如果没有了就call_llm),再根据动作链决定动作,以及动作之间的衔接。 """ - async def _decide(self, world: World) -> ACTION_PAIR: + async def _decide(self, world: World) -> tuple[ACTION_NAME, ACTION_PARAMS, str]: """ 异步决策逻辑:通过LLM决定执行什么动作和参数 """ @@ -108,5 +108,5 @@ class LLMAI(AI): "avatar_persona": avatar_persona } res = await get_ai_prompt_and_call_llm_async(dict_info) - action_name, action_params = res["action_name"], res["action_params"] - return action_name, action_params \ No newline at end of file + action_name, action_params, avatar_thinking = res["action_name"], res["action_params"], res["avatar_thinking"] + return action_name, action_params, avatar_thinking \ No newline at end of file diff --git a/src/classes/avatar.py b/src/classes/avatar.py index 18ba429..36a1b50 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -54,6 +54,7 @@ class Avatar: ai: AI = None cur_action_pair: Optional[ACTION_PAIR] = None history_action_pairs: list[ACTION_PAIR] = field(default_factory=list) + thinking: str = "" def __post_init__(self): """ @@ -101,8 +102,9 @@ class Avatar: if self.cur_action_pair is None: # 决定动作时生成事件 - action_name, action_args, event = await self.ai.decide(self.world) + action_name, action_args, avatar_thinking, event = await self.ai.decide(self.world) action = self.create_action(action_name) + self.thinking = avatar_thinking self.cur_action_pair = (action, action_args) # 纯粹执行动作,不产生事件 diff --git a/src/front/front.py b/src/front/front.py index 4a90fd3..a066e0d 100644 --- a/src/front/front.py +++ b/src/front/front.py @@ -8,6 +8,7 @@ from src.classes.world import World from src.classes.tile import TileType from src.classes.avatar import Avatar, Gender from src.classes.event import Event +from src.utils.text_wrap import wrap_text class Front: @@ -155,10 +156,7 @@ class Front: pygame.quit() - def _step_once(self): - """执行一步模拟(同步版本,已弃用)""" - print("警告:_step_once已弃用,请使用异步版本") - pass + def _render(self): """渲染主画面""" @@ -239,11 +237,6 @@ class Front: # 计算位置:放在操作指南右边,留适当间距 x_pos = self.margin + self._guide_width + padding * 3 self.screen.blit(ym_surf, (x_pos, y_pos)) - - def _get_month_number(self) -> int: - """获取月份数字(已弃用,保留向后兼容)""" - return self.simulator.world.month_stamp.get_month().value - def _draw_map(self): @@ -445,6 +438,15 @@ class Front: lines.append("") # 空行分隔 lines.append("历史动作:") lines.extend(avatar.get_history_action_pairs_str().split("\n")) + + # 添加thinking信息 + if avatar.thinking: + lines.append("") # 空行分隔 + lines.append("思考:") + # 使用wrap_text函数将thinking信息按20字符换行 + thinking_lines = wrap_text(avatar.thinking, 20) + lines.extend(thinking_lines) + 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): @@ -545,11 +547,7 @@ class Front: self.avatar_images[avatar_id] = random.choice(self.female_avatars) # 如果没有可用的头像,则使用None,后续会画圆点作为fallback - def _create_fallback_surface(self, tile_type): - """创建默认的fallback surface""" - fallback_surface = self.pygame.Surface((self.tile_size, self.tile_size)) - fallback_surface.fill((128, 128, 128)) # 灰色 - self.tile_images[tile_type] = fallback_surface + def _create_font(self, size: int): """创建字体""" diff --git a/src/utils/llm.py b/src/utils/llm.py index d17a0a7..7c958ca 100644 --- a/src/utils/llm.py +++ b/src/utils/llm.py @@ -71,8 +71,6 @@ def get_prompt_and_call_llm(template_path: Path, infos: dict) -> str: prompt = get_prompt(template, infos) res = call_llm(prompt) json_res = parse_llm_response(res) - # print(f"prompt = {prompt}") - # print(f"res = {res}") return json_res async def get_prompt_and_call_llm_async(template_path: Path, infos: dict) -> str: @@ -82,7 +80,7 @@ async def get_prompt_and_call_llm_async(template_path: Path, infos: dict) -> str template = read_txt(template_path) prompt = get_prompt(template, infos) res = await call_llm_async(prompt) - print(f"res = {res}") + # print(f"res = {res}") json_res = parse_llm_response(res) return json_res diff --git a/src/utils/text_wrap.py b/src/utils/text_wrap.py new file mode 100644 index 0000000..6cb3a5e --- /dev/null +++ b/src/utils/text_wrap.py @@ -0,0 +1,53 @@ +def wrap_text(text: str, max_width: int = 20) -> list[str]: + """ + 将长文本按指定宽度换行 + + Args: + text: 要换行的文本 + max_width: 每行的最大字符数,默认20 + + Returns: + 换行后的文本行列表 + """ + if not text: + return [] + + lines = [] + current_line = "" + + # 按换行符分割,处理已有的换行 + paragraphs = text.split('\n') + + for paragraph in paragraphs: + if not paragraph.strip(): + lines.append("") + continue + + words = paragraph.split() + + for word in words: + # 如果当前行加上新词会超过限制 + if len(current_line) + len(word) + 1 > max_width: + if current_line: + lines.append(current_line.strip()) + current_line = word + else: + # 如果单个词就超过限制,强制换行 + if len(word) > max_width: + # 长词强制切分 + for i in range(0, len(word), max_width): + lines.append(word[i:i + max_width]) + else: + lines.append(word) + else: + if current_line: + current_line += " " + word + else: + current_line = word + + # 处理段落的最后一行 + if current_line: + lines.append(current_line.strip()) + current_line = "" + + return lines