From 3047de0367a0fd4b587c8bff5c52867323e49799 Mon Sep 17 00:00:00 2001 From: bridge Date: Tue, 2 Sep 2025 00:35:07 +0800 Subject: [PATCH] add action chain --- README.md | 6 +- src/classes/action.py | 151 ++++++++++++++++++++++++++++++++----- src/classes/age.py | 21 +++--- src/classes/ai.py | 40 +++++++--- src/classes/avatar.py | 101 ++++++++++++++----------- src/classes/calendar.py | 25 ++++-- src/classes/cultivation.py | 17 ++--- src/classes/event.py | 32 ++++++-- src/classes/persona.py | 29 +++++++ src/classes/world.py | 5 +- src/front/front.py | 13 ++-- src/run/run.py | 20 +++-- src/sim/simulator.py | 16 ++-- src/utils/id_generator.py | 20 +++++ src/utils/llm.py | 4 +- static/templates/ai.txt | 2 + tests/test_basic.py | 11 ++- tests/test_simulator.py | 7 +- 18 files changed, 370 insertions(+), 150 deletions(-) create mode 100644 src/classes/persona.py create mode 100644 src/utils/id_generator.py diff --git a/README.md b/README.md index ef03d5d..bb9cb23 100644 --- a/README.md +++ b/README.md @@ -71,8 +71,10 @@ - [ ] 拍卖会 - [ ] 秘境探索 - [ ] 比武大会 -- [ ] 规则发起事件 -- [ ] NPC发起事件 +- [ ] 突发事件 + - [ ] 规则发起事件 + - [ ] NPC发起事件 + - [ ] 突发事件的小说化&CG化&影视化 - [ ] 自然事件: - [ ] 自然灾害 - [ ] 天灾 diff --git a/src/classes/action.py b/src/classes/action.py index 7c8035d..9607630 100644 --- a/src/classes/action.py +++ b/src/classes/action.py @@ -3,16 +3,45 @@ from abc import ABC, abstractmethod from typing import TYPE_CHECKING import random import json +import inspect from src.classes.essence import Essence, EssenceType from src.classes.root import Root, corres_essence_type from src.classes.tile import Region -from src.classes.event import Event, NullEvent +from src.classes.event import Event, NULL_EVENT if TYPE_CHECKING: from src.classes.avatar import Avatar from src.classes.world import World + +def long_action(step_month: int): + """ + 长态动作装饰器,用于为动作类自动添加时间管理功能 + + Args: + step_month: 动作需要的月份数 + """ + def decorator(cls): + # 设置类属性,供基类使用 + cls._step_month = step_month + + def is_finished(self, *args, **kwargs) -> bool: + """ + 根据时间差判断动作是否完成 + 接受但忽略额外的参数以保持与其他动作类型的兼容性 + """ + if self.start_monthstamp is None: + return False + return (self.world.month_stamp - self.start_monthstamp) >= self.step_month + + # 只添加 is_finished 方法 + cls.is_finished = is_finished + + return cls + + return decorator + class Action(ABC): """ 角色可以执行的动作。 @@ -28,11 +57,37 @@ class Action(ABC): self.world = world @abstractmethod - def execute(self) -> Event|NullEvent: + def execute(self) -> None: pass class DefineAction(Action): - pass + def __init__(self, avatar: Avatar, world: World): + """ + 初始化动作,处理长态动作的属性设置 + """ + super().__init__(avatar, world) + + # 如果是长态动作,初始化相关属性 + if hasattr(self.__class__, '_step_month'): + self.step_month = self.__class__._step_month + self.start_monthstamp = None + + def execute(self, *args, **kwargs) -> None: + """ + 执行动作,处理时间管理逻辑,然后调用具体的_execute实现 + """ + # 如果是长态动作且第一次执行,记录开始时间 + if hasattr(self, 'step_month') and self.start_monthstamp is None: + self.start_monthstamp = self.world.month_stamp + + self._execute(*args, **kwargs) + + @abstractmethod + def _execute(self, *args, **kwargs) -> None: + """ + 具体的动作执行逻辑,由子类实现 + """ + pass class LLMAction(Action): """ @@ -44,13 +99,39 @@ class LLMAction(Action): """ pass +class ChunkActionMixin(): + """ + 动作片,可以理解成只是一种切分出来的动作。 + 不能被avatar直接执行,而是成为avatar执行某个动作的步骤。 + """ + pass -class Move(DefineAction): +class ActualActionMixin(): + """ + 实际的可以被规则/LLM调用,让avatar去执行的动作。 + 不一定是多个step,也有可能就一个step + """ + @abstractmethod + def is_finished(self) -> bool: + """ + 判断动作是否完成 + """ + pass + + @abstractmethod + def get_event(self, *args, **kwargs) -> Event: + """ + 获取动作开始时的事件 + """ + pass + + +class Move(DefineAction, ChunkActionMixin): """ 最基础的移动动作,在tile之间进行切换。 """ COMMENT = "移动到某个相对位置" - def execute(self, delta_x: int, delta_y: int) -> Event|NullEvent: + def _execute(self, delta_x: int, delta_y: int) -> None: """ 移动到某个tile """ @@ -67,14 +148,13 @@ class Move(DefineAction): else: # 超出边界:不改变位置与tile pass - return NullEvent() -class MoveToRegion(DefineAction): +class MoveToRegion(DefineAction, ActualActionMixin): """ 移动到某个region """ COMMENT = "移动到某个区域" - def execute(self, region: Region|str) -> Event|NullEvent: + def _execute(self, region: Region|str) -> None: """ 移动到某个region """ @@ -88,14 +168,36 @@ class MoveToRegion(DefineAction): delta_x = max(-1, min(1, delta_x)) delta_y = max(-1, min(1, delta_y)) Move(self.avatar, self.world).execute(delta_x, delta_y) - return Event(self.world.year, self.world.month, f"{self.avatar.name} 移动向 {region.name}") -class Cultivate(DefineAction): + def is_finished(self, region: Region|str) -> bool: + """ + 判断动作是否完成 + """ + if isinstance(region, str): + region = self.world.map.region_names[region] + return self.avatar.is_in_region(region) + + def get_event(self, region: Region|str) -> Event: + """ + 获取移动动作开始时的事件 + """ + if isinstance(region, str): + region_name = region + if region in self.world.map.region_names: + region_name = self.world.map.region_names[region].name + elif hasattr(region, 'name'): + region_name = region.name + else: + region_name = str(region) + return Event(self.world.month_stamp, f"{self.avatar.name} 开始移动向 {region_name}") + +@long_action(step_month=10) +class Cultivate(DefineAction, ActualActionMixin): """ 修炼动作,可以增加修仙进度。 """ COMMENT = "修炼,增进修为" - def execute(self) -> Event|NullEvent: + def _execute(self) -> None: """ 修炼 获得的exp增加取决于essence的对应灵根的大小。 @@ -106,7 +208,6 @@ class Cultivate(DefineAction): essence_density = essence.get_density(essence_type) exp = self.get_exp(essence_density) self.avatar.cultivation_progress.add_exp(exp) - return Event(self.world.year, self.world.month, f"{self.avatar.name} 在 {self.avatar.tile.region.name} 修炼") def get_exp(self, essence_density: int) -> int: """ @@ -115,10 +216,17 @@ class Cultivate(DefineAction): """ 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} 开始修炼") # 突破境界class -class Breakthrough(DefineAction): +@long_action(step_month=1) +class Breakthrough(DefineAction, ActualActionMixin): """ 突破境界 """ @@ -129,19 +237,22 @@ class Breakthrough(DefineAction): """ return 0.5 - def execute(self) -> Event|NullEvent: + def _execute(self) -> None: """ 突破境界 """ - assert self.avatar.cultivation_progress.can_break_through() + # assert self.avatar.cultivation_progress.can_break_through() + if not self.avatar.cultivation_progress.can_break_through(): + print(f"警告,{self.avatar.name} 无法突破境界,其level为 {self.avatar.cultivation_progress.level},无法突破") success_rate = self.calc_success_rate() if random.random() < success_rate: self.avatar.cultivation_progress.break_through() - is_success = True - else: - is_success = False - res = "成功" if is_success else "失败" - return Event(self.world.year, self.world.month, f"{self.avatar.name} 突破境界{res}") + + def get_event(self) -> Event: + """ + 获取突破动作开始时的事件 + """ + return Event(self.world.month_stamp, f"{self.avatar.name} 开始尝试突破境界") ALL_ACTION_CLASSES = [Move, Cultivate, Breakthrough, MoveToRegion] diff --git a/src/classes/age.py b/src/classes/age.py index d755b20..4ab5283 100644 --- a/src/classes/age.py +++ b/src/classes/age.py @@ -1,5 +1,5 @@ import random -from src.classes.calendar import Month, Year +from src.classes.calendar import Month, Year, MonthStamp from src.classes.cultivation import Realm class Age: @@ -57,29 +57,26 @@ class Age: """ return random.random() < self.get_death_probability(realm) - def calculate_age(self, current_month: Month, current_year: Year, birth_month: Month, birth_year: Year) -> int: + + + def calculate_age(self, current_month_stamp: MonthStamp, birth_month_stamp: MonthStamp) -> int: """ 计算准确的年龄(整数年) Args: - current_month: 当前月份 - current_year: 当前年份 - birth_month: 出生月份 - birth_year: 出生年份 + current_month_stamp: 当前时间戳 + birth_month_stamp: 出生时间戳 Returns: 整数年龄 """ - age = current_year - birth_year - if current_month.value < birth_month.value: - age -= 1 - return max(0, age) + return max(0, (current_month_stamp - birth_month_stamp) // 12) - def update_age(self, current_month: Month, current_year: Year, birth_month: Month, birth_year: Year): + def update_age(self, current_month_stamp: MonthStamp, birth_month_stamp: MonthStamp): """ 更新年龄 """ - self.age = self.calculate_age(current_month, current_year, birth_month, birth_year) + self.age = self.calculate_age(current_month_stamp, birth_month_stamp) def __str__(self) -> str: """返回年龄的字符串表示""" diff --git a/src/classes/ai.py b/src/classes/ai.py index b1453da..249e789 100644 --- a/src/classes/ai.py +++ b/src/classes/ai.py @@ -3,25 +3,45 @@ NPC AI的类。 这里指的不是LLM或者Machine Learning,而是NPC的决策机制 分为两类:规则AI和LLM AI """ +from __future__ import annotations from abc import ABC, abstractmethod +from typing import TYPE_CHECKING from src.classes.world import World from src.classes.tile import Region 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 +if TYPE_CHECKING: + from src.classes.avatar import Avatar + class AI(ABC): """ AI的基类 """ - def __init__(self, avatar: 'Avatar'): + def __init__(self, avatar: Avatar): self.avatar = avatar - @abstractmethod - def decide(self, world: World) -> tuple[str, dict]: + def decide(self, world: World) -> tuple[str, dict, Event]: """ - 决定做什么 + 决定做什么,同时生成对应的事件 + """ + # 先决定动作和参数 + action_name, action_params = self._decide(world) + + # 获取动作对象并生成事件 + action = self.avatar.create_action(action_name) + event = action.get_event(**action_params) + + return action_name, action_params, event + + @abstractmethod + def _decide(self, world: World) -> tuple[str, dict]: + """ + 决策逻辑:决定执行什么动作和参数 + 由子类实现具体的决策逻辑 """ pass @@ -29,9 +49,9 @@ class RuleAI(AI): """ 规则AI """ - def decide(self, world: World) -> tuple[str, dict]: + def _decide(self, world: World) -> tuple[str, dict]: """ - 决定做什么 + 决策逻辑:决定执行什么动作和参数 先做一个简单的: 1. 找到自己灵根对应的最好的区域 2. 检测自己是否在最好的区域 @@ -69,17 +89,19 @@ class LLMAI(AI): 不能每个单步step都调用一次LLM来决定下一步做什么。这样子一方面动作一直乱变,另一方面也太费token了。 decide的作用是,拉取既有的动作链(如果没有了就call_llm),再根据动作链决定动作,以及动作之间的衔接。 """ - def decide(self, world: World) -> tuple[str, dict]: + def _decide(self, world: World) -> tuple[str, dict]: """ - 决定做什么 + 决策逻辑:通过LLM决定执行什么动作和参数 """ action_space_str = ACTION_SPACE_STR avatar_infos_str = str(self.avatar) regions_str = "\n".join([str(region) for region in world.map.regions.values()]) + avatar_persona = self.avatar.persona.prompt dict_info = { "action_space": action_space_str, "avatar_infos": avatar_infos_str, - "regions": regions_str + "regions": regions_str, + "avatar_persona": avatar_persona } res = get_ai_prompt_and_call_llm(dict_info) action_name, action_params = res["action_name"], res["action_params"] diff --git a/src/classes/avatar.py b/src/classes/avatar.py index bdd7de7..eae8778 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -1,18 +1,20 @@ import random -import uuid from dataclasses import dataclass, field from enum import Enum from typing import Optional -from src.classes.calendar import Month, Year +from src.classes.calendar import Month, Year, 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.root import Root from src.classes.age import Age -from src.utils.strings import to_snake_case +from src.classes.event import NULL_EVENT + from src.classes.ai import AI, RuleAI, LLMAI +from src.classes.persona import Persona, personas_by_id +from src.utils.id_generator import get_avatar_id class Gender(Enum): MALE = "male" @@ -35,26 +37,27 @@ class Avatar: world: World name: str id: str - birth_month: Month - birth_year: Year + birth_month_stamp: MonthStamp age: Age gender: Gender cultivation_progress: CultivationProgress = field(default_factory=lambda: CultivationProgress(0)) pos_x: int = 0 pos_y: int = 0 tile: Optional[Tile] = None - actions: dict[str, Action] = field(default_factory=dict) + 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 def __post_init__(self): """ - 在Avatar创建后自动绑定基础动作和AI + 在Avatar创建后自动初始化tile和AI """ self.tile = self.world.map.get_tile(self.pos_x, self.pos_y) - self.ai = LLMAI(self) - # self.ai = RuleAI(self) - self._bind_basic_actions() + # self.ai = LLMAI(self) + self.ai = RuleAI(self) def __str__(self) -> str: """ @@ -63,38 +66,48 @@ 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 _bind_basic_actions(self): + def create_action(self, action_name: str) -> Action: """ - 绑定基础动作,如移动等 + 根据动作名称创建新的action实例 + + Args: + action_name: 动作类的名称(如 'Cultivate', 'Breakthrough' 等) + + Returns: + 新创建的Action实例 + + Raises: + ValueError: 如果找不到对应的动作类 """ - for action in ALL_ACTION_CLASSES: - self.bind_action(action) - - - def bind_action(self, action_class: type[Action]): - """ - 绑定一个action到avatar - """ - # 以类名为键保存实例,保持可追踪性 - self.actions[action_class.__name__] = action_class(self, self.world) - - # 同时挂载一个便捷方法,名称为蛇形(MoveFast -> move_fast),并转发参数 - method_name = to_snake_case(action_class.__name__) - - def _wrapper(*args, **kwargs): - return self.actions[action_class.__name__].execute(*args, **kwargs) - - setattr(self, method_name, _wrapper) + # 在所有动作类中查找对应的类 + for action_class in ALL_ACTION_CLASSES: + if action_class.__name__ == action_name: + return action_class(self, self.world) + + raise ValueError(f"未找到名为 '{action_name}' 的动作类") def act(self): """ 角色执行动作。 - 实际上分为两步:决定做什么(decide)和实习上去做(do) + 实际上分为两步:决定做什么(decide)和实际去做(do) + 事件只在决定动作时产生,执行过程不产生事件 """ - action_name, action_args = self.ai.decide(self.world) - action = self.actions[action_name] - event = action.execute(**action_args) + event = NULL_EVENT + + if self.action_be_executed 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 + + # 纯粹执行动作,不产生事件 + self.action_be_executed.execute(**self.action_parmas_be_executed) + + if self.action_be_executed.is_finished(**self.action_parmas_be_executed): + self.action_be_executed = None + self.action_parmas_be_executed = None + return event def update_cultivation(self, new_level: int): @@ -118,11 +131,11 @@ class Avatar: """ return self.age.death_by_old_age(self.cultivation_progress.realm) - def update_age(self, current_month: Month, current_year: Year): + def update_age(self, current_month_stamp: MonthStamp): """ 更新年龄 """ - self.age.update_age(current_month, current_year, self.birth_month, self.birth_year) + self.age.update_age(current_month_stamp, self.birth_month_stamp) def get_age_info(self) -> dict: """ @@ -145,27 +158,25 @@ class Avatar: def is_in_region(self, region: Region) -> bool: return self.tile.region == region -def get_new_avatar_from_ordinary(world: World, current_year: Year, name: str, age: Age): +def get_new_avatar_from_ordinary(world: World, current_month_stamp: MonthStamp, name: str, age: Age): """ 从凡人中来的新修士 这代表其境界为最低 """ - # 利用uuid功能生成id - avatar_id = str(uuid.uuid4()) + # 生成短ID,替代UUID4 + avatar_id = get_avatar_id() - birth_year = current_year - age.age - birth_month = random.choice(list(Month)) + birth_month_stamp = current_month_stamp - age.age * 12 + random.randint(0, 11) # 在出生年内随机选择月份 cultivation_progress = CultivationProgress(0) - pos_x = random.randint(0, world.map.width) - pos_y = random.randint(0, world.map.height) + pos_x = random.randint(0, world.map.width - 1) + pos_y = random.randint(0, world.map.height - 1) gender = random.choice(list(Gender)) return Avatar( world=world, name=name, id=avatar_id, - birth_month=birth_month, - birth_year=birth_year, + birth_month_stamp=MonthStamp(birth_month_stamp), age=age, gender=gender, cultivation_progress=cultivation_progress, diff --git a/src/classes/calendar.py b/src/classes/calendar.py index 94ac969..1bd11da 100644 --- a/src/classes/calendar.py +++ b/src/classes/calendar.py @@ -24,8 +24,23 @@ class Year(int): def __add__(self, other: int) -> 'Year': return Year(int(self) + other) -def next_month(month: Month, year: Year) -> tuple[Month, Year]: - if month == Month.DECEMBER: - return Month.JANUARY, year + 1 - else: - return Month(month.value + 1), year \ No newline at end of file +class MonthStamp(int): + """ + 0年1月 = 0 + 之后依次递增 + """ + def get_month(self) -> Month: + month_value = (self % 12) + 1 + return Month(month_value if month_value <= 12 else 12) + + def get_year(self) -> Year: + return Year(self // 12) + + def __add__(self, other: int) -> 'MonthStamp': + return MonthStamp(int(self) + other) + + + +def create_month_stamp(year: Year, month: Month) -> MonthStamp: + """从年和月创建MonthStamp""" + return MonthStamp(int(year) * 12 + month.value - 1) \ No newline at end of file diff --git a/src/classes/cultivation.py b/src/classes/cultivation.py index f8ed0a3..d90427c 100644 --- a/src/classes/cultivation.py +++ b/src/classes/cultivation.py @@ -92,16 +92,6 @@ class CultivationProgress: return exp_required + realm_bonus - def can_level_up(self) -> bool: - """ - 检查是否可以升级 - - 返回: - 如果经验值足够升级则返回True - """ - required_exp = self.get_exp_required() - return self.exp >= required_exp - def get_exp_progress(self) -> tuple[int, int]: """ 获取当前经验值进度 @@ -150,5 +140,12 @@ class CultivationProgress: """ return self.level in level_to_break_through.keys() + def can_level_up(self) -> bool: + """ + 检查是否可以升级 + 可以突破,说明到顶了,说明不能升级。 + """ + return not self.can_break_through() + def __str__(self) -> str: return f"{self.realm.value}{self.stage.value}({self.level}级)。可以突破:{self.can_break_through()}" \ No newline at end of file diff --git a/src/classes/event.py b/src/classes/event.py index 31b3b33..a9030b5 100644 --- a/src/classes/event.py +++ b/src/classes/event.py @@ -3,17 +3,39 @@ event class """ from dataclasses import dataclass -from src.classes.calendar import Month, Year +from src.classes.calendar import Month, Year, MonthStamp @dataclass class Event: - year: Year - month: Month + month_stamp: MonthStamp content: str def __str__(self) -> str: - return f"{self.year}年{self.month}月: {self.content}" + year = self.month_stamp.get_year() + month = self.month_stamp.get_month() + return f"{year}年{month}月: {self.content}" class NullEvent: + """ + 空事件单例类 + """ + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + def __str__(self) -> str: - return "" \ No newline at end of file + return "" + + def __bool__(self) -> bool: + """使NullEvent实例在布尔上下文中为False""" + return False + +# 全局单例实例 +NULL_EVENT = NullEvent() + +def is_null_event(event) -> bool: + """检查事件是否为空事件的便捷函数""" + return event is NULL_EVENT \ No newline at end of file diff --git a/src/classes/persona.py b/src/classes/persona.py new file mode 100644 index 0000000..a3a44c1 --- /dev/null +++ b/src/classes/persona.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass, field + +# TODO: 配表化 +@dataclass +class Persona: + """ + 角色个性 + """ + id: int + name: str + prompt: str + +personas_by_id: dict[int, Persona] = {} +personas_by_name: dict[str, Persona] = {} +p1 = Persona(id=1, name="理性", prompt="你是一个理性的人,你总是会用逻辑来思考问题,做事会谋定而后动。") +p2 = Persona(id=2, name="无常", prompt="你是一个无常的人,你总是会随机应变,性子到哪里了就是哪里。") +p3 = Persona(id=3, name="怠惰", prompt="你是一个怠惰的人,你总是会拖延,不想努力,更热衷于享受人生。") +p4 = Persona(id=4, name="冒险", prompt="你是一个冒险的人,你总是会冒险,喜欢刺激,总想放手一搏。") + +personas_by_id[p1.id] = p1 +personas_by_id[p2.id] = p2 +personas_by_id[p3.id] = p3 +personas_by_id[p4.id] = p4 +personas_by_name[p1.name] = p1 +personas_by_name[p2.name] = p2 +personas_by_name[p3.name] = p3 +personas_by_name[p4.name] = p4 + + diff --git a/src/classes/world.py b/src/classes/world.py index c01bb38..c7d6dae 100644 --- a/src/classes/world.py +++ b/src/classes/world.py @@ -1,10 +1,9 @@ from dataclasses import dataclass from src.classes.tile import Map -from src.classes.calendar import Year, Month +from src.classes.calendar import Year, Month, MonthStamp @dataclass class World(): map: Map - year: Year - month: Month \ No newline at end of file + month_stamp: MonthStamp \ No newline at end of file diff --git a/src/front/front.py b/src/front/front.py index 4c19188..68bd10d 100644 --- a/src/front/front.py +++ b/src/front/front.py @@ -206,8 +206,8 @@ class Front: def _draw_year_month_info(self, y_pos: int, padding: int): """绘制年月信息""" # 获取年月数据 - year = int(self.simulator.world.year) - month_num = self._get_month_number() + year = int(self.simulator.world.month_stamp.get_year()) + month_num = self.simulator.world.month_stamp.get_month().value # 构建年月文本 ym_text = f"{year}年{month_num:02d}月" @@ -220,12 +220,8 @@ class Front: self.screen.blit(ym_surf, (x_pos, y_pos)) def _get_month_number(self) -> int: - """获取月份数字""" - try: - month_num = list(type(self.simulator.world.month)).index(self.simulator.world.month) + 1 - return month_num - except Exception: - return 1 + """获取月份数字(已弃用,保留向后兼容)""" + return self.simulator.world.month_stamp.get_month().value @@ -420,6 +416,7 @@ class Front: f"年龄: {avatar.age}", f"境界: {str(avatar.cultivation_progress)}", f"灵根: {avatar.root.value}", + f"个性: {avatar.persona.name}", f"位置: ({avatar.pos_x}, {avatar.pos_y})", ] self._draw_tooltip(lines, *self.pygame.mouse.get_pos(), self.tooltip_font) diff --git a/src/run/run.py b/src/run/run.py index 2af4650..275f2b2 100644 --- a/src/run/run.py +++ b/src/run/run.py @@ -1,5 +1,4 @@ import random -import uuid from typing import List, Tuple, Dict, Any # 依赖项目内部模块 @@ -8,7 +7,7 @@ from src.sim.simulator import Simulator from src.classes.world import World from src.classes.tile import Map, TileType from src.classes.avatar import Avatar, Gender -from src.classes.calendar import Month, Year +from src.classes.calendar import Month, Year, MonthStamp, create_month_stamp from src.classes.action import Move from src.classes.essence import Essence, EssenceType from src.classes.cultivation import CultivationProgress @@ -16,6 +15,7 @@ from src.classes.root import Root from src.classes.age import Age from src.run.create_map import create_cultivation_world_map from src.utils.names import get_random_name +from src.utils.id_generator import get_avatar_id def clamp(value: int, lo: int, hi: int) -> int: @@ -35,15 +35,14 @@ def random_gender() -> Gender: return Gender.MALE if random.random() < 0.5 else Gender.FEMALE -def make_avatars(world: World, count: int = 12, current_year: Year = Year(100)) -> dict[str, Avatar]: +def make_avatars(world: World, count: int = 12, current_month_stamp: MonthStamp = MonthStamp(100 * 12)) -> dict[str, Avatar]: avatars: dict[str, Avatar] = {} width, height = world.map.width, world.map.height for i in range(count): # 随机生成年龄,范围从16到60岁 age_years = random.randint(16, 60) - # 根据当前年份和年龄计算出生年份 - birth_year = current_year - age_years - birth_month = random.choice(list(Month)) + # 根据当前时间戳和年龄计算出生时间戳 + birth_month_stamp = current_month_stamp - age_years * 12 + random.randint(0, 11) # 在出生年内随机选择月份 gender = random_gender() # 使用仙侠风格的随机名字 name = get_random_name(gender) @@ -68,9 +67,8 @@ def make_avatars(world: World, count: int = 12, current_year: Year = Year(100)) avatar = Avatar( world=world, name=name, - id=str(uuid.uuid4()), - birth_month=birth_month, - birth_year=birth_year, + id=get_avatar_id(), + birth_month_stamp=MonthStamp(birth_month_stamp), age=age, gender=gender, cultivation_progress=cultivation_progress, @@ -87,13 +85,13 @@ def main(): # 为了每次更丰富,使用随机种子;如需复现可将 seed 固定 game_map = create_cultivation_world_map() - world = World(map=game_map, year=Year(100), month=Month.JANUARY) + world = World(map=game_map, month_stamp=create_month_stamp(Year(100), Month.JANUARY)) # 创建模拟器 sim = Simulator(world) # 创建角色,传入当前年份确保年龄与生日匹配 - sim.avatars.update(make_avatars(world, count=2, current_year=world.year)) + sim.avatars.update(make_avatars(world, count=2, current_month_stamp=world.month_stamp)) front = Front( simulator=sim, diff --git a/src/sim/simulator.py b/src/sim/simulator.py index 978ebe9..6b0d97d 100644 --- a/src/sim/simulator.py +++ b/src/sim/simulator.py @@ -1,10 +1,10 @@ import random -from src.classes.calendar import Month, Year, next_month +from src.classes.calendar import Month, Year, MonthStamp from src.classes.avatar import Avatar, get_new_avatar_from_ordinary, Gender from src.classes.age import Age from src.classes.world import World -from src.classes.event import Event, NullEvent +from src.classes.event import Event, is_null_event from src.utils.names import get_random_name class Simulator: @@ -27,13 +27,13 @@ class Simulator: # 结算角色行为 for avatar_id, avatar in self.avatars.items(): event = avatar.act() - if event is not NullEvent: + if not is_null_event(event): events.append(event) if avatar.death_by_old_age(): death_avatar_ids.append(avatar_id) - event = Event(self.world.year, self.world.month, f"{avatar.name} 老死了,时年{avatar.age.get_age()}岁") + event = Event(self.world.month_stamp, f"{avatar.name} 老死了,时年{avatar.age.get_age()}岁") events.append(event) - avatar.update_age(self.world.month, self.world.year) + avatar.update_age(self.world.month_stamp) # 删除死亡的角色 for avatar_id in death_avatar_ids: @@ -44,12 +44,12 @@ class Simulator: age = random.randint(16, 60) gender = random.choice(list(Gender)) name = get_random_name(gender) - new_avatar = get_new_avatar_from_ordinary(self.world, self.world.year, name, Age(age)) + new_avatar = get_new_avatar_from_ordinary(self.world, self.world.month_stamp, name, Age(age)) self.avatars[new_avatar.id] = new_avatar - event = Event(self.world.year, self.world.month, f"{new_avatar.name}晋升为修士了。") + event = Event(self.world.month_stamp, f"{new_avatar.name}晋升为修士了。") events.append(event) # 最后结算年月 - self.world.month, self.world.year = next_month(self.world.month, self.world.year) + self.world.month_stamp = self.world.month_stamp + 1 return events diff --git a/src/utils/id_generator.py b/src/utils/id_generator.py new file mode 100644 index 0000000..5091e51 --- /dev/null +++ b/src/utils/id_generator.py @@ -0,0 +1,20 @@ +""" +简化的ID生成器,替代UUID4 +""" + +import random +import string + + +def base62_id(length: int = 8) -> str: + """ + 生成base62编码的短ID(数字+大小写字母) + 默认8位,比UUID4的36位短很多 + """ + charset = string.ascii_letters + string.digits # 0-9, a-z, A-Z (62个字符) + return ''.join(random.choices(charset, k=length)) + + +def get_avatar_id() -> str: + """获取Avatar ID的默认函数""" + return base62_id(8) diff --git a/src/utils/llm.py b/src/utils/llm.py index 30a061d..6d79e35 100644 --- a/src/utils/llm.py +++ b/src/utils/llm.py @@ -44,8 +44,8 @@ def get_prompt_and_call_llm(template_path: Path, infos: dict) -> str: prompt = get_prompt(template, infos) res = call_llm(prompt) json_res = json.loads(res) - print(f"prompt = {prompt}") - print(f"res = {res}") + # print(f"prompt = {prompt}") + # print(f"res = {res}") return json_res def get_ai_prompt_and_call_llm(infos: dict) -> dict: diff --git a/static/templates/ai.txt b/static/templates/ai.txt index 663b53c..d035a1f 100644 --- a/static/templates/ai.txt +++ b/static/templates/ai.txt @@ -8,6 +8,8 @@ {regions} 你需要进行决策的NPC的基本信息为: {avatar_infos} +其个性为:{avatar_persona} +决策时需参考这个角色的个性。 注意,只返回json格式的动作 返回格式: diff --git a/tests/test_basic.py b/tests/test_basic.py index 7a8ae07..706a9d1 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1,6 +1,6 @@ -import uuid +from src.utils.id_generator import get_avatar_id from src.classes.avatar import Avatar, Gender -from src.classes.calendar import Month, Year +from src.classes.calendar import Month, Year, MonthStamp, create_month_stamp from src.classes.world import World from src.classes.tile import Map, TileType from src.classes.age import Age @@ -15,14 +15,13 @@ def test_basic(): for y in range(2): map.create_tile(x, y, TileType.PLAIN) - world = World(map=map, year=Year(1), month=Month.JANUARY) + world = World(map=map, month_stamp=create_month_stamp(Year(1), Month.JANUARY)) avatar = Avatar( world=world, name=get_random_name(Gender.MALE), - id=str(uuid.uuid4()), - birth_month=Month.JANUARY, - birth_year=Year(2000), + id=get_avatar_id(), + birth_month_stamp=create_month_stamp(Year(2000), Month.JANUARY), age=Age(20), gender=Gender.MALE ) diff --git a/tests/test_simulator.py b/tests/test_simulator.py index 7fc2e85..2615cd1 100644 --- a/tests/test_simulator.py +++ b/tests/test_simulator.py @@ -2,7 +2,7 @@ import random from src.sim.simulator import Simulator from src.classes.avatar import Avatar, Gender -from src.classes.calendar import Month, Year +from src.classes.calendar import Month, Year, MonthStamp, create_month_stamp from src.classes.world import World from src.classes.tile import Map, TileType from src.classes.action import Move @@ -19,15 +19,14 @@ def test_simulator_step_moves_avatar_and_sets_tile(): for y in range(3): game_map.create_tile(x, y, TileType.PLAIN) - world = World(map=game_map, year=Year(1), month=Month.JANUARY) + world = World(map=game_map, month_stamp=create_month_stamp(Year(1), Month.JANUARY)) # 将角色放在地图中心,避免越界 avatar = Avatar( world=world, name=get_random_name(Gender.MALE), id="1", - birth_month=Month.JANUARY, - birth_year=Year(2000), + birth_month_stamp=create_month_stamp(Year(2000), Month.JANUARY), age=20, gender=Gender.MALE, pos_x=1,