add action chain
This commit is contained in:
@@ -71,8 +71,10 @@
|
||||
- [ ] 拍卖会
|
||||
- [ ] 秘境探索
|
||||
- [ ] 比武大会
|
||||
- [ ] 规则发起事件
|
||||
- [ ] NPC发起事件
|
||||
- [ ] 突发事件
|
||||
- [ ] 规则发起事件
|
||||
- [ ] NPC发起事件
|
||||
- [ ] 突发事件的小说化&CG化&影视化
|
||||
- [ ] 自然事件:
|
||||
- [ ] 自然灾害
|
||||
- [ ] 天灾
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
"""返回年龄的字符串表示"""
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
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)
|
||||
@@ -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()}"
|
||||
@@ -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 ""
|
||||
return ""
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
"""使NullEvent实例在布尔上下文中为False"""
|
||||
return False
|
||||
|
||||
# 全局单例实例
|
||||
NULL_EVENT = NullEvent()
|
||||
|
||||
def is_null_event(event) -> bool:
|
||||
"""检查事件是否为空事件的便捷函数"""
|
||||
return event is NULL_EVENT
|
||||
29
src/classes/persona.py
Normal file
29
src/classes/persona.py
Normal file
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
month_stamp: MonthStamp
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
20
src/utils/id_generator.py
Normal file
20
src/utils/id_generator.py
Normal file
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
{regions}
|
||||
你需要进行决策的NPC的基本信息为:
|
||||
{avatar_infos}
|
||||
其个性为:{avatar_persona}
|
||||
决策时需参考这个角色的个性。
|
||||
|
||||
注意,只返回json格式的动作
|
||||
返回格式:
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user