feat: add avatar metrics tracking feature (#111)
* feat: add avatar metrics tracking feature (#110) Add AvatarMetrics dataclass for tracking avatar state snapshots - Add AvatarMetrics dataclass for recording monthly snapshots - Add metrics_history field to Avatar with opt-in tracking - Implement automatic monthly snapshot recording in Simulator - Add backward compatibility support for existing save files - Set default tracking limit to 1200 months (100 years) - Add comprehensive tests with 100% coverage - Move documentation to specs directory with simplified chinese * fix: convert Traditional Chinese comments to Simplified Chinese 修正程式碼中的繁體中文註解為簡體中文,以符合專案規範。 Fix Traditional Chinese comments to Simplified Chinese in codebase.
This commit is contained in:
278
docs/specs/avatar-metrics-tracking.md
Normal file
278
docs/specs/avatar-metrics-tracking.md
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
# Avatar 状态追踪功能
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
新增可选的 Avatar 状态追踪功能,用于记录角色成长轨迹。该功能默认关闭,不影响现有游戏逻辑。
|
||||||
|
|
||||||
|
## 功能特点
|
||||||
|
|
||||||
|
- **可选性**:默认关闭,不影响现有功能
|
||||||
|
- **轻量**:仅记录关键指标(修为、资源、社交等)
|
||||||
|
- **不可变**:快照一旦创建不修改
|
||||||
|
- **持久化**:支持存档/读档
|
||||||
|
- **自动清理**:默认最多保留 1200 笔记录(100 年)
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
### 启用追踪
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 启用 Avatar 状态追踪
|
||||||
|
avatar.enable_metrics_tracking = True
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自动记录
|
||||||
|
|
||||||
|
追踪启用后,模拟器会在每月自动调用 `record_metrics()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 在 simulator.py 的 _finalize_step() 中自动执行
|
||||||
|
# avatar.record_metrics() # 每月自动调用
|
||||||
|
```
|
||||||
|
|
||||||
|
### 手动记录并标记事件
|
||||||
|
|
||||||
|
```python
|
||||||
|
from src.classes.avatar_metrics import MetricTag
|
||||||
|
|
||||||
|
# 手动记录状态(可选事件标记)
|
||||||
|
avatar.record_metrics(tags=["breakthrough"])
|
||||||
|
avatar.record_metrics(tags=[MetricTag.INJURED.value, MetricTag.BATTLE.value])
|
||||||
|
avatar.record_metrics(tags=["custom_event"]) # 支持自定义标签
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查看摘要
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 获取状态追踪摘要
|
||||||
|
summary = avatar.get_metrics_summary()
|
||||||
|
print(summary)
|
||||||
|
# 输出示例:
|
||||||
|
# {
|
||||||
|
# "enabled": True,
|
||||||
|
# "count": 120,
|
||||||
|
# "first_record": 100,
|
||||||
|
# "latest_record": 220,
|
||||||
|
# "cultivation_growth": 5
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 访问历史记录
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 直接访问历史记录列表
|
||||||
|
for metrics in avatar.metrics_history:
|
||||||
|
print(f"Month {metrics.timestamp}: Level {metrics.cultivation_level}, HP {metrics.hp}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 设计原则
|
||||||
|
|
||||||
|
### 1. 可选性
|
||||||
|
|
||||||
|
- 默认关闭(`enable_metrics_tracking = False`)
|
||||||
|
- 不影响现有 API 和逻辑
|
||||||
|
- 可随时启用或禁用
|
||||||
|
|
||||||
|
### 2. 轻量级
|
||||||
|
|
||||||
|
仅记录关键指标:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `timestamp` | `MonthStamp` | 记录时间 |
|
||||||
|
| `age` | `int` | 年龄 |
|
||||||
|
| `cultivation_level` | `int` | 修为等级 |
|
||||||
|
| `cultivation_progress` | `int` | 修为进度 |
|
||||||
|
| `hp` | `float` | 当前生命值 |
|
||||||
|
| `hp_max` | `float` | 最大生命值 |
|
||||||
|
| `spirit_stones` | `int` | 灵石数量 |
|
||||||
|
| `relations_count` | `int` | 关系数量 |
|
||||||
|
| `known_regions_count` | `int` | 已知区域数量 |
|
||||||
|
| `tags` | `List[str]` | 事件标签 |
|
||||||
|
|
||||||
|
### 3. 不可变性
|
||||||
|
|
||||||
|
- 快照一旦创建不修改
|
||||||
|
- 使用 dataclass 保证结构清晰
|
||||||
|
- 支持序列化/反序列化
|
||||||
|
|
||||||
|
### 4. 向后兼容
|
||||||
|
|
||||||
|
- 使用 `default_factory` 避免破坏旧代码
|
||||||
|
- 存档/读档完全兼容
|
||||||
|
- 旧存档会使用默认值(空列表、False)
|
||||||
|
|
||||||
|
## 性能影响
|
||||||
|
|
||||||
|
### 关闭时
|
||||||
|
|
||||||
|
- 零影响(不占用额外内存或 CPU)
|
||||||
|
- `record_metrics()` 直接返回 `None`
|
||||||
|
|
||||||
|
### 开启时
|
||||||
|
|
||||||
|
- 每月新增约 200 bytes 快照
|
||||||
|
- 100 年约 240 KB
|
||||||
|
- **有上限**:默认最多保留 1200 笔记录(可通过 `max_metrics_history` 调整)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 调整历史记录上限
|
||||||
|
avatar.max_metrics_history = 600 # 改为 50 年
|
||||||
|
```
|
||||||
|
|
||||||
|
## 默认标签
|
||||||
|
|
||||||
|
建议使用 `MetricTag` 枚举中的默认标签:
|
||||||
|
|
||||||
|
| 标签 | 值 | 说明 |
|
||||||
|
|------|-----|------|
|
||||||
|
| `BREAKTHROUGH` | `"breakthrough"` | 突破 |
|
||||||
|
| `INJURED` | `"injured"` | 受伤 |
|
||||||
|
| `RECOVERED` | `"recovered"` | 康复 |
|
||||||
|
| `SECT_JOIN` | `"sect_join"` | 加入宗门 |
|
||||||
|
| `SECT_LEAVE` | `"sect_leave"` | 离开宗门 |
|
||||||
|
| `TECHNIQUE_LEARN` | `"technique_learn"` | 学习功法 |
|
||||||
|
| `DEATH` | `"death"` | 死亡 |
|
||||||
|
| `BATTLE` | `"battle"` | 战斗 |
|
||||||
|
| `DUNGEON` | `"dungeon"` | 探索秘境 |
|
||||||
|
|
||||||
|
### 使用标签
|
||||||
|
|
||||||
|
```python
|
||||||
|
from src.classes.avatar_metrics import MetricTag
|
||||||
|
|
||||||
|
# 使用枚举(推荐)
|
||||||
|
avatar.record_metrics(tags=[MetricTag.BREAKTHROUGH.value])
|
||||||
|
|
||||||
|
# 多个标签
|
||||||
|
avatar.record_metrics(tags=[
|
||||||
|
MetricTag.INJURED.value,
|
||||||
|
MetricTag.BATTLE.value
|
||||||
|
])
|
||||||
|
|
||||||
|
# 自定义标签(也支持)
|
||||||
|
avatar.record_metrics(tags=["custom_event", "special_occurrence"])
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据结构
|
||||||
|
|
||||||
|
### AvatarMetrics
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class AvatarMetrics:
|
||||||
|
timestamp: MonthStamp
|
||||||
|
age: int
|
||||||
|
cultivation_level: int
|
||||||
|
cultivation_progress: int
|
||||||
|
hp: float
|
||||||
|
hp_max: float
|
||||||
|
spirit_stones: int
|
||||||
|
relations_count: int
|
||||||
|
known_regions_count: int
|
||||||
|
tags: List[str]
|
||||||
|
|
||||||
|
def to_save_dict(self) -> dict:
|
||||||
|
"""转换为可序列化的字典"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_save_dict(cls, data: dict) -> "AvatarMetrics":
|
||||||
|
"""从字典重建"""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## 序列化
|
||||||
|
|
||||||
|
### 存档
|
||||||
|
|
||||||
|
状态追踪数据会自动包含在存档中:
|
||||||
|
|
||||||
|
```python
|
||||||
|
save_dict = avatar.to_save_dict()
|
||||||
|
# 包含:
|
||||||
|
# - "metrics_history": [...]
|
||||||
|
# - "enable_metrics_tracking": True
|
||||||
|
```
|
||||||
|
|
||||||
|
### 读档
|
||||||
|
|
||||||
|
读档时自动恢复:
|
||||||
|
|
||||||
|
```python
|
||||||
|
avatar = Avatar.from_save_dict(data, world)
|
||||||
|
# metrics_history 和 enable_metrics_tracking 自动恢复
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
### 1. 标签的可变性
|
||||||
|
|
||||||
|
`tags` 字段使用 `List[str]` 而非 `List[MetricTag]`,提供灵活性:
|
||||||
|
- 支持默认标签(使用 `MetricTag.value`)
|
||||||
|
- 支持自定义标签
|
||||||
|
- 允许混合使用
|
||||||
|
|
||||||
|
### 2. 自动清理
|
||||||
|
|
||||||
|
历史记录超过 `max_metrics_history` 时会自动清理旧记录:
|
||||||
|
```python
|
||||||
|
# 保留最新的 N 笔记录
|
||||||
|
if len(self.metrics_history) > self.max_metrics_history:
|
||||||
|
self.metrics_history = self.metrics_history[-self.max_metrics_history:]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 不可变性
|
||||||
|
|
||||||
|
快照对象本身是可变的(Python dataclass 默认),但设计上应视为不可变:
|
||||||
|
- 创建后不修改 `AvatarMetrics` 对象
|
||||||
|
- 如需更新,创建新快照
|
||||||
|
|
||||||
|
## 使用场景
|
||||||
|
|
||||||
|
### 追踪修为成长
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 启用追踪
|
||||||
|
avatar.enable_metrics_tracking = True
|
||||||
|
|
||||||
|
# 模拟运行...
|
||||||
|
# 自动记录每月状态
|
||||||
|
|
||||||
|
# 分析成长
|
||||||
|
first = avatar.metrics_history[0]
|
||||||
|
latest = avatar.metrics_history[-1]
|
||||||
|
print(f"修为增长: {latest.cultivation_level - first.cultivation_level}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 标记重大事件
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 突破时标记
|
||||||
|
if avatar.cultivation_progress.realm != old_realm:
|
||||||
|
avatar.record_metrics(tags=[MetricTag.BREAKTHROUGH.value])
|
||||||
|
|
||||||
|
# 受伤时标记
|
||||||
|
if avatar.hp.value < avatar.hp.max_value * 0.3:
|
||||||
|
avatar.record_metrics(tags=[MetricTag.INJURED.value])
|
||||||
|
```
|
||||||
|
|
||||||
|
### 分析游戏数据
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 导出数据到 CSV/Pandas
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
data = []
|
||||||
|
for metrics in avatar.metrics_history:
|
||||||
|
data.append({
|
||||||
|
"timestamp": metrics.timestamp,
|
||||||
|
"age": metrics.age,
|
||||||
|
"level": metrics.cultivation_level,
|
||||||
|
"hp": metrics.hp,
|
||||||
|
"spirit_stones": metrics.spirit_stones,
|
||||||
|
})
|
||||||
|
|
||||||
|
df = pd.DataFrame(data)
|
||||||
|
df.plot(x="timestamp", y="level")
|
||||||
|
```
|
||||||
@@ -40,6 +40,7 @@ from src.classes.nickname_data import Nickname
|
|||||||
from src.classes.emotions import EmotionType
|
from src.classes.emotions import EmotionType
|
||||||
from src.utils.config import CONFIG
|
from src.utils.config import CONFIG
|
||||||
from src.classes.elixir import ConsumedElixir, Elixir
|
from src.classes.elixir import ConsumedElixir, Elixir
|
||||||
|
from src.classes.avatar_metrics import AvatarMetrics
|
||||||
|
|
||||||
# Mixin 导入
|
# Mixin 导入
|
||||||
from src.classes.effect import EffectsMixin
|
from src.classes.effect import EffectsMixin
|
||||||
@@ -124,6 +125,11 @@ class Avatar(
|
|||||||
|
|
||||||
known_regions: set[int] = field(default_factory=set)
|
known_regions: set[int] = field(default_factory=set)
|
||||||
|
|
||||||
|
# 状态追踪(可选)
|
||||||
|
metrics_history: List[AvatarMetrics] = field(default_factory=list)
|
||||||
|
enable_metrics_tracking: bool = False
|
||||||
|
max_metrics_history: int = 1200 # 最多 100 年
|
||||||
|
|
||||||
# 关系交互计数器: key=target_id, value={"count": 0, "checked_times": 0}
|
# 关系交互计数器: key=target_id, value={"count": 0, "checked_times": 0}
|
||||||
relation_interaction_states: dict[str, dict[str, int]] = field(default_factory=lambda: defaultdict(lambda: {"count": 0, "checked_times": 0}))
|
relation_interaction_states: dict[str, dict[str, int]] = field(default_factory=lambda: defaultdict(lambda: {"count": 0, "checked_times": 0}))
|
||||||
|
|
||||||
@@ -260,6 +266,58 @@ class Avatar(
|
|||||||
"""检查是否老死"""
|
"""检查是否老死"""
|
||||||
return self.age.death_by_old_age(self.cultivation_progress.realm)
|
return self.age.death_by_old_age(self.cultivation_progress.realm)
|
||||||
|
|
||||||
|
# ========== 状态追踪 ==========
|
||||||
|
|
||||||
|
def record_metrics(self, tags: Optional[List[str]] = None) -> Optional[AvatarMetrics]:
|
||||||
|
"""
|
||||||
|
记录当前状态快照。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tags: 可选的事件标记
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
创建的快照,如果追踪未启用则返回 None
|
||||||
|
"""
|
||||||
|
if not self.enable_metrics_tracking:
|
||||||
|
return None
|
||||||
|
|
||||||
|
metrics = AvatarMetrics(
|
||||||
|
timestamp=self.world.month_stamp,
|
||||||
|
age=self.age.value,
|
||||||
|
cultivation_level=self.cultivation_progress.level,
|
||||||
|
cultivation_progress=self.cultivation_progress.progress,
|
||||||
|
hp=self.hp.value,
|
||||||
|
hp_max=self.hp.max_value,
|
||||||
|
spirit_stones=self.magic_stone.amount,
|
||||||
|
relations_count=len(self.relations),
|
||||||
|
known_regions_count=len(self.known_regions),
|
||||||
|
tags=tags or [],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.metrics_history.append(metrics)
|
||||||
|
|
||||||
|
# 自动清理旧记录
|
||||||
|
if len(self.metrics_history) > self.max_metrics_history:
|
||||||
|
self.metrics_history = self.metrics_history[-self.max_metrics_history:]
|
||||||
|
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
def get_metrics_summary(self) -> dict:
|
||||||
|
"""获取状态追踪摘要"""
|
||||||
|
if not self.metrics_history:
|
||||||
|
return {"enabled": self.enable_metrics_tracking, "count": 0}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"enabled": self.enable_metrics_tracking,
|
||||||
|
"count": len(self.metrics_history),
|
||||||
|
"first_record": self.metrics_history[0].timestamp,
|
||||||
|
"latest_record": self.metrics_history[-1].timestamp,
|
||||||
|
"cultivation_growth": (
|
||||||
|
self.metrics_history[-1].cultivation_level -
|
||||||
|
self.metrics_history[0].cultivation_level
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
# ========== 年龄与修为 ==========
|
# ========== 年龄与修为 ==========
|
||||||
|
|
||||||
def update_age(self, current_month_stamp: MonthStamp):
|
def update_age(self, current_month_stamp: MonthStamp):
|
||||||
|
|||||||
56
src/classes/avatar_metrics.py
Normal file
56
src/classes/avatar_metrics.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
from typing import List, Optional
|
||||||
|
from enum import Enum
|
||||||
|
from src.classes.calendar import MonthStamp
|
||||||
|
|
||||||
|
|
||||||
|
class MetricTag(Enum):
|
||||||
|
"""预定义的度量标签"""
|
||||||
|
BREAKTHROUGH = "breakthrough"
|
||||||
|
INJURED = "injured"
|
||||||
|
RECOVERED = "recovered"
|
||||||
|
SECT_JOIN = "sect_join"
|
||||||
|
SECT_LEAVE = "sect_leave"
|
||||||
|
TECHNIQUE_LEARN = "technique_learn"
|
||||||
|
DEATH = "death"
|
||||||
|
BATTLE = "battle"
|
||||||
|
DUNGEON = "dungeon"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AvatarMetrics:
|
||||||
|
"""
|
||||||
|
Avatar 状态快照,用于追踪角色成长轨迹。
|
||||||
|
|
||||||
|
设计原则:
|
||||||
|
- 轻量:仅记录关键指标
|
||||||
|
- 不可变:快照一旦创建不修改
|
||||||
|
- 可选:不影响现有功能
|
||||||
|
"""
|
||||||
|
timestamp: MonthStamp
|
||||||
|
age: int
|
||||||
|
|
||||||
|
# 修为相关
|
||||||
|
cultivation_level: int
|
||||||
|
cultivation_progress: int
|
||||||
|
|
||||||
|
# 资源相关
|
||||||
|
hp: float
|
||||||
|
hp_max: float
|
||||||
|
spirit_stones: int
|
||||||
|
|
||||||
|
# 社会相关
|
||||||
|
relations_count: int
|
||||||
|
known_regions_count: int
|
||||||
|
|
||||||
|
# 标记
|
||||||
|
tags: List[str]
|
||||||
|
|
||||||
|
def to_save_dict(self) -> dict:
|
||||||
|
"""转换为可序列化的字典(用于存档)"""
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_save_dict(cls, data: dict) -> "AvatarMetrics":
|
||||||
|
"""从字典重建(用于读档)"""
|
||||||
|
return cls(**data)
|
||||||
@@ -53,6 +53,7 @@ class AvatarLoadMixin:
|
|||||||
from src.classes.magic_stone import MagicStone
|
from src.classes.magic_stone import MagicStone
|
||||||
from src.classes.action_runtime import ActionPlan
|
from src.classes.action_runtime import ActionPlan
|
||||||
from src.classes.elixir import elixirs_by_id, ConsumedElixir
|
from src.classes.elixir import elixirs_by_id, ConsumedElixir
|
||||||
|
from src.classes.avatar_metrics import AvatarMetrics
|
||||||
|
|
||||||
# 重建基本对象
|
# 重建基本对象
|
||||||
gender = Gender(data["gender"])
|
gender = Gender(data["gender"])
|
||||||
@@ -226,6 +227,14 @@ class AvatarLoadMixin:
|
|||||||
# 恢复临时效果
|
# 恢复临时效果
|
||||||
avatar.temporary_effects = data.get("temporary_effects", [])
|
avatar.temporary_effects = data.get("temporary_effects", [])
|
||||||
|
|
||||||
|
# 重建 metrics_history
|
||||||
|
metrics_history_data = data.get("metrics_history", [])
|
||||||
|
avatar.metrics_history = [
|
||||||
|
AvatarMetrics.from_save_dict(metrics_data)
|
||||||
|
for metrics_data in metrics_history_data
|
||||||
|
]
|
||||||
|
avatar.enable_metrics_tracking = data.get("enable_metrics_tracking", False)
|
||||||
|
|
||||||
# 加载完成后重新计算effects(确保数值正确)
|
# 加载完成后重新计算effects(确保数值正确)
|
||||||
avatar.recalc_effects()
|
avatar.recalc_effects()
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,14 @@ class AvatarSaveMixin:
|
|||||||
} if self.long_term_objective else None,
|
} if self.long_term_objective else None,
|
||||||
"_action_cd_last_months": self._action_cd_last_months,
|
"_action_cd_last_months": self._action_cd_last_months,
|
||||||
"known_regions": list(self.known_regions),
|
"known_regions": list(self.known_regions),
|
||||||
|
|
||||||
|
# 状态追踪
|
||||||
|
"metrics_history": [
|
||||||
|
metrics.to_save_dict()
|
||||||
|
for metrics in self.metrics_history
|
||||||
|
] if self.enable_metrics_tracking else [],
|
||||||
|
"enable_metrics_tracking": self.enable_metrics_tracking,
|
||||||
|
|
||||||
# 丹药
|
# 丹药
|
||||||
"elixirs": [
|
"elixirs": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -480,6 +480,11 @@ class Simulator:
|
|||||||
"""
|
"""
|
||||||
本轮步进的最终归档:去重、入库、打日志、推进时间。
|
本轮步进的最终归档:去重、入库、打日志、推进时间。
|
||||||
"""
|
"""
|
||||||
|
# 0. 为启用追踪的 Avatar 记录每月快照
|
||||||
|
for avatar in self.world.avatar_manager.avatars.values():
|
||||||
|
if avatar.enable_metrics_tracking:
|
||||||
|
avatar.record_metrics()
|
||||||
|
|
||||||
# 1. 基于 ID 去重(防止同一个事件对象被多次添加)
|
# 1. 基于 ID 去重(防止同一个事件对象被多次添加)
|
||||||
unique_events: dict[str, Event] = {}
|
unique_events: dict[str, Event] = {}
|
||||||
for e in events:
|
for e in events:
|
||||||
|
|||||||
146
tests/test_avatar_metrics.py
Normal file
146
tests/test_avatar_metrics.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
"""
|
||||||
|
测试 Avatar 状态追踪功能
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
from src.classes.avatar_metrics import AvatarMetrics, MetricTag
|
||||||
|
from src.classes.calendar import MonthStamp
|
||||||
|
|
||||||
|
|
||||||
|
def test_avatar_metrics_creation():
|
||||||
|
"""测试状态快照创建"""
|
||||||
|
metrics = AvatarMetrics(
|
||||||
|
timestamp=MonthStamp(100),
|
||||||
|
age=20,
|
||||||
|
cultivation_level=5,
|
||||||
|
cultivation_progress=100,
|
||||||
|
hp=100.0,
|
||||||
|
hp_max=100.0,
|
||||||
|
spirit_stones=50,
|
||||||
|
relations_count=3,
|
||||||
|
known_regions_count=2,
|
||||||
|
tags=["breakthrough"],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert metrics.age == 20
|
||||||
|
assert metrics.cultivation_level == 5
|
||||||
|
assert metrics.cultivation_progress == 100
|
||||||
|
assert metrics.hp == 100.0
|
||||||
|
assert metrics.hp_max == 100.0
|
||||||
|
assert metrics.spirit_stones == 50
|
||||||
|
assert metrics.relations_count == 3
|
||||||
|
assert metrics.known_regions_count == 2
|
||||||
|
assert "breakthrough" in metrics.tags
|
||||||
|
assert metrics.timestamp == MonthStamp(100)
|
||||||
|
|
||||||
|
|
||||||
|
def test_avatar_metrics_serialization():
|
||||||
|
"""测试序列化与反序列化"""
|
||||||
|
original = AvatarMetrics(
|
||||||
|
timestamp=MonthStamp(200),
|
||||||
|
age=30,
|
||||||
|
cultivation_level=10,
|
||||||
|
cultivation_progress=500,
|
||||||
|
hp=150.0,
|
||||||
|
hp_max=200.0,
|
||||||
|
spirit_stones=1000,
|
||||||
|
relations_count=5,
|
||||||
|
known_regions_count=10,
|
||||||
|
tags=["injured", "battle"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# 序列化
|
||||||
|
data = original.to_save_dict()
|
||||||
|
assert isinstance(data, dict)
|
||||||
|
assert data["age"] == 30
|
||||||
|
assert data["cultivation_level"] == 10
|
||||||
|
assert "injured" in data["tags"]
|
||||||
|
assert "battle" in data["tags"]
|
||||||
|
|
||||||
|
# 反序列化
|
||||||
|
restored = AvatarMetrics.from_save_dict(data)
|
||||||
|
assert restored.age == original.age
|
||||||
|
assert restored.cultivation_level == original.cultivation_level
|
||||||
|
assert restored.hp == original.hp
|
||||||
|
assert restored.tags == original.tags
|
||||||
|
assert restored.timestamp == original.timestamp
|
||||||
|
|
||||||
|
|
||||||
|
def test_metric_tag_enum():
|
||||||
|
"""测试 MetricTag 枚举"""
|
||||||
|
assert MetricTag.BREAKTHROUGH.value == "breakthrough"
|
||||||
|
assert MetricTag.INJURED.value == "injured"
|
||||||
|
assert MetricTag.RECOVERED.value == "recovered"
|
||||||
|
assert MetricTag.SECT_JOIN.value == "sect_join"
|
||||||
|
assert MetricTag.SECT_LEAVE.value == "sect_leave"
|
||||||
|
assert MetricTag.TECHNIQUE_LEARN.value == "technique_learn"
|
||||||
|
assert MetricTag.DEATH.value == "death"
|
||||||
|
assert MetricTag.BATTLE.value == "battle"
|
||||||
|
assert MetricTag.DUNGEON.value == "dungeon"
|
||||||
|
|
||||||
|
|
||||||
|
def test_avatar_metrics_with_standard_tags():
|
||||||
|
"""测试使用标准标签"""
|
||||||
|
metrics = AvatarMetrics(
|
||||||
|
timestamp=MonthStamp(50),
|
||||||
|
age=25,
|
||||||
|
cultivation_level=7,
|
||||||
|
cultivation_progress=300,
|
||||||
|
hp=80.0,
|
||||||
|
hp_max=100.0,
|
||||||
|
spirit_stones=200,
|
||||||
|
relations_count=4,
|
||||||
|
known_regions_count=5,
|
||||||
|
tags=[MetricTag.BREAKTHROUGH.value, MetricTag.BATTLE.value],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "breakthrough" in metrics.tags
|
||||||
|
assert "battle" in metrics.tags
|
||||||
|
assert len(metrics.tags) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_avatar_metrics_empty_tags():
|
||||||
|
"""测试空标签列表"""
|
||||||
|
metrics = AvatarMetrics(
|
||||||
|
timestamp=MonthStamp(0),
|
||||||
|
age=0,
|
||||||
|
cultivation_level=0,
|
||||||
|
cultivation_progress=0,
|
||||||
|
hp=100.0,
|
||||||
|
hp_max=100.0,
|
||||||
|
spirit_stones=0,
|
||||||
|
relations_count=0,
|
||||||
|
known_regions_count=0,
|
||||||
|
tags=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert metrics.tags == []
|
||||||
|
assert len(metrics.tags) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_avatar_metrics_multiple_tags():
|
||||||
|
"""测试多个标签"""
|
||||||
|
tags = [
|
||||||
|
MetricTag.BREAKTHROUGH.value,
|
||||||
|
MetricTag.INJURED.value,
|
||||||
|
MetricTag.BATTLE.value,
|
||||||
|
"custom_event", # 允许自定义标签
|
||||||
|
]
|
||||||
|
|
||||||
|
metrics = AvatarMetrics(
|
||||||
|
timestamp=MonthStamp(1000),
|
||||||
|
age=100,
|
||||||
|
cultivation_level=15,
|
||||||
|
cultivation_progress=1000,
|
||||||
|
hp=50.0,
|
||||||
|
hp_max=500.0,
|
||||||
|
spirit_stones=10000,
|
||||||
|
relations_count=20,
|
||||||
|
known_regions_count=50,
|
||||||
|
tags=tags,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(metrics.tags) == 4
|
||||||
|
assert "breakthrough" in metrics.tags
|
||||||
|
assert "injured" in metrics.tags
|
||||||
|
assert "battle" in metrics.tags
|
||||||
|
assert "custom_event" in metrics.tags
|
||||||
Reference in New Issue
Block a user