From b58baa7ef8f7ab3a05bc3351e6a9be761b9df4bc Mon Sep 17 00:00:00 2001 From: lch Date: Tue, 12 May 2026 13:02:23 +0800 Subject: [PATCH 1/2] 1 --- docs/contests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contests/README.md b/docs/contests/README.md index a29730cc..b4d31ac8 100644 --- a/docs/contests/README.md +++ b/docs/contests/README.md @@ -15,4 +15,4 @@ slug: ./ + [THUAI6](THUAI6/README.md):清华大学第六届人工智能挑战赛电子系赛道(原电子系第 24 届队式程序设计大赛 teamstyle24) + [THUAI7](THUAI7/README.md):清华大学第七届人工智能挑战赛电子系赛道(原电子系第 25 届队式程序设计大赛 teamstyle25) + [THUAI8](THUAI8/README.md):清华大学第八届人工智能挑战赛电子系赛道(原电子系第 26 届队式程序设计大赛 teamstyle26) -+ [THUAI9](THUAI8/README.md):清华大学第九届人工智能挑战赛电子系赛道(原电子系第 27 届队式程序设计大赛 teamstyle27) ++ [THUAI9](THUAI9/README.md):清华大学第九届人工智能挑战赛电子系赛道(原电子系第 27 届队式程序设计大赛 teamstyle27) From dd645f52bb1939938211ecd0094cc818e697bb2d Mon Sep 17 00:00:00 2001 From: lch Date: Tue, 12 May 2026 13:48:23 +0800 Subject: [PATCH 2/2] 2 --- docs/contests/RL9/pve/README.md | 25 +++++ docs/contests/RL9/pve/faq/README.md | 43 +++++++++ docs/contests/RL9/pve/game/actions.md | 36 +++++++ docs/contests/RL9/pve/game/reward.md | 36 +++++++ docs/contests/RL9/pve/game/state.md | 37 +++++++ docs/contests/RL9/pve/interface/gym.md | 106 +++++++++++++++++++++ docs/contests/RL9/pve/intro/rule.md | 67 +++++++++++++ docs/contests/RL9/pve/training/training.md | 103 ++++++++++++++++++++ sidebars.js | 36 +++++++ 9 files changed, 489 insertions(+) create mode 100644 docs/contests/RL9/pve/README.md create mode 100644 docs/contests/RL9/pve/faq/README.md create mode 100644 docs/contests/RL9/pve/game/actions.md create mode 100644 docs/contests/RL9/pve/game/reward.md create mode 100644 docs/contests/RL9/pve/game/state.md create mode 100644 docs/contests/RL9/pve/interface/gym.md create mode 100644 docs/contests/RL9/pve/intro/rule.md create mode 100644 docs/contests/RL9/pve/training/training.md diff --git a/docs/contests/RL9/pve/README.md b/docs/contests/RL9/pve/README.md new file mode 100644 index 00000000..df73aba7 --- /dev/null +++ b/docs/contests/RL9/pve/README.md @@ -0,0 +1,25 @@ +--- +title: THUAI9 +slug: ./ +--- +## 赛事名称 + +AI 工厂模拟(THUAI9) + +## PVE 概要 + +强化学习竞技环境。选手训练智能体在地图上移动、低买高卖、采集资源,在有限时间内最大化累计得分。提供 easy / medium / hard 三级难度,支持 MaskablePPO 训练。比赛以多 seed 平均得分排名。 + +--- + +## 选手包使用说明 + +不熟悉强化学习的可以访问https://github.com/konpoku/THUAI9-RL,在小项目中学习一下 + +详细的使用说明参照选手包里的logic\pve\docs\CONTESTANT_GUIDE.md + +--- + +## 相关链接 + ++ THUAI9 GitHub 仓库:[https://github.com/lch24/THUAI9](https://github.com/lch24/THUAI9) diff --git a/docs/contests/RL9/pve/faq/README.md b/docs/contests/RL9/pve/faq/README.md new file mode 100644 index 00000000..5e8d990d --- /dev/null +++ b/docs/contests/RL9/pve/faq/README.md @@ -0,0 +1,43 @@ +# 常见问题(PVE / Python / RL) + +## 环境相关 + +> Q: 需要什么 Python 版本? +> +> A: Python 3.9+。依赖见 `logic/pve/requirements.txt`。 + +> Q: 运行 `import GameLogic` 报错? +> +> A: 确保在 `logic/pve/` 目录下运行,或将 `logic/pve/` 加入 `PYTHONPATH`。 + +> Q: 如何切换难度? +> +> A: `GameConfig.easy()` / `.medium()` / `.hard()` 预设,或传入 YAML 文件路径。 + +## 训练相关 + +> Q: PPO 训练不收敛? +> +> A: 尝试:(1) 使用 `MaskablePPO`;(2) 降低 `price_volatility`;(3) easy 地图上先训练。 + +> Q: reward 和 score 的区别? +> +> A: reward 是训练辅助信号(含塑形奖励和惩罚),score 是最终排名依据(仅卖出得分×10)。比赛看 score。 + +> Q: 动作掩码有什么用? +> +> A: 在训练时告诉 PPO 哪些动作当前无效,避免探索无效方向。对规则策略同样有用。 + +## 接口相关 + +> Q: 能直接读 `env.unit` 吗? +> +> A: **不能**。评测机只暴露 `reset/step/action_masks` 三个标准接口。所有信息通过 `obs` 和 `info` 获取。 + +> Q: 观测向量怎么理解? +> +> A: 32 维 float32,包含位置、HP、背包、现金、市场价格相位、最近资源点/市场/算力中心相对位置等。详见 [公开接口](../interface/gym.md)。 + +> Q: 怎么写规则策略(不用 RL)? +> +> A: 读取 `info` 字典做 if-else 或状态机决策,直接调 `env.step()`。 diff --git a/docs/contests/RL9/pve/game/actions.md b/docs/contests/RL9/pve/game/actions.md new file mode 100644 index 00000000..037c4ede --- /dev/null +++ b/docs/contests/RL9/pve/game/actions.md @@ -0,0 +1,36 @@ +# 动作空间 + +PVE 使用 **8 个离散动作**: + +| 编号 | 动作 | 含义 | 有效性条件 | +|:----:|:----:|------|------------| +| 0 | `WAIT` | 等待一个 tick | 始终有效 | +| 1 | `MOVE_UP` | 向上移动 (x−1) | 目标格可通行,单位不 busy | +| 2 | `MOVE_DOWN` | 向下移动 (x+1) | 同上 | +| 3 | `MOVE_LEFT` | 向左移动 (y−1) | 同上 | +| 4 | `MOVE_RIGHT` | 向右移动 (y+1) | 同上 | +| 5 | `BUY` | 在相邻市场买最便宜的可负担商品 | Manhattan ≤1 有市场,背包有空间,现金充足 | +| 6 | `SELL` | 在相邻市场卖出背包内所有商品 | Manhattan ≤1 有市场,背包有商品 | +| 7 | `HARVEST` | 从附近资源点采集原材料 | Manhattan ≤2 有未耗尽资源,背包有空间 | + +- **BUY**:自动购买当前市场价格最低的可负担商品 +- **SELL**:一次性卖出背包中所有商品,获得当前市场价 +- **HARVEST**:采集范围 2 格(Manhattan 距离) +- 执行无效动作不会报错,但受到 **-0.05 分惩罚**并浪费步数 + +## 动作掩码 + +环境提供 `action_masks()` 方法,返回 `(8,)` 布尔数组,`True` 表示该动作当前有效。使用 `MaskablePPO` 可以自动过滤无效动作: + +```python +from sb3_contrib import MaskablePPO +from sb3_contrib.common.wrappers import ActionMasker + +def mask_fn(env): + return env.unwrapped.action_masks() + +masked_env = ActionMasker(env, mask_fn) +model = MaskablePPO("MlpPolicy", masked_env) +``` + +建议所有策略先查询 `action_masks()` 再决定动作。 diff --git a/docs/contests/RL9/pve/game/reward.md b/docs/contests/RL9/pve/game/reward.md new file mode 100644 index 00000000..83612c1c --- /dev/null +++ b/docs/contests/RL9/pve/game/reward.md @@ -0,0 +1,36 @@ +# 奖励与得分 + +## 得分(Score) + +最终排名依据的指标。只在 **SELL 动作成功**时增加: + +``` +score += revenue × score_factor(默认 × 10) +``` + +## 单步奖励(RL Reward) + +训练时的辅助信号,由以下部分叠加: + +| 来源 | 值 | 说明 | +|------|:--:|------| +| 现金变化 Δmoney | × 0.01 | 正负均有 | +| 得分变化 Δscore | × 0.01 | 仅卖出时为正 | +| 时间惩罚(每步) | −0.002 | 鼓励高效路径 | +| 采集奖励(每单位) | +0.001 | 采集塑形 | +| 算力中心解锁(一次性) | +0.5 | 进度奖励 | +| 无效动作惩罚 | −0.05 | 每步 | +| 破产惩罚(terminated 时) | −10.0 | 终端惩罚 | + +> 奖励是训练辅助信号。**最终排名以 `info["score"]` 为准**,不是累计奖励。 + +## `step()` 返回的 info 字典 + +| 字段 | 类型 | 含义 | +|------|------|------| +| `step` | `int` | 当前步数 | +| `time` | `float` | 游戏时间(秒) | +| `money` | `float` | 当前现金 | +| `score` | `float` | 当前累计得分 | +| `compute` | `float` | 当前算力 | +| `action_valid` | `bool` | 上一步动作是否有效 | diff --git a/docs/contests/RL9/pve/game/state.md b/docs/contests/RL9/pve/game/state.md new file mode 100644 index 00000000..8955d676 --- /dev/null +++ b/docs/contests/RL9/pve/game/state.md @@ -0,0 +1,37 @@ +# 游戏状态 + +## 单位属性 + +| 属性 | 值 | +|:----:|:--:| +| 血量 | 300 | +| 背包容量 | 30 | +| 采集速率 | 10/s | +| 算力中心占领时间 | 10s | + +- 背包分为**原材料**和**成品**两部分 +- `busy_ticks > 0` 时单位忙碌,忽略新指令 + +## 工厂 + +| 属性 | 值 | +|:----:|:--:| +| 位置 | (0, 0) | +| 仓储上限 | 300 | +| 初始生产线数 | 3 | + +## 算力产出 + +- 每个已占领算力中心:**1 算力/秒** +- 可花费算力招募新单位(Phase 2) + +## 资源再生 + +资源点库存会随时间缓慢再生: + +``` +regen(t) = rate × (1 + sin(2π·t / period)) / 2 +``` + +- 再生倍率:easy=2.0, medium=1.0, hard=0.5 +- 资源耗尽(`depleted=True`)后停止再生 diff --git a/docs/contests/RL9/pve/interface/gym.md b/docs/contests/RL9/pve/interface/gym.md new file mode 100644 index 00000000..6435ad12 --- /dev/null +++ b/docs/contests/RL9/pve/interface/gym.md @@ -0,0 +1,106 @@ +# 公开接口 + +PVE 算法**只能**通过标准 Gymnasium 接口与环境交互。不得直接访问内部对象。 + +## 环境初始化 + +```python +from GameLogic import GameEnvironment, GameConfig + +# 内置难度 +env = GameEnvironment(cfg=GameConfig.easy()) +env = GameEnvironment(cfg=GameConfig.medium()) +env = GameEnvironment(cfg=GameConfig.hard()) + +# 自定义配置 +env = GameEnvironment(cfg=GameConfig.from_dict({ + "map_width": 8, "map_height": 8, + "num_markets": 4, "initial_money": 100.0, +})) +``` + +## 接口方法 + +### `reset()` + +```python +obs, info = env.reset(seed=0) +``` + +重置环境,返回初始观测和 info。 + +### `step()` + +```python +obs, reward, terminated, truncated, info = env.step(action) +# action: int, 0-7 +# obs: np.ndarray, shape (32,), dtype float32 +# reward: float +# terminated: bool (money < 0,破产) +# truncated: bool (步数耗尽,正常结束) +# info: dict +``` + +### `action_masks()` + +```python +mask = env.action_masks() # np.ndarray[bool], shape (8,) +``` + +返回当前有效的动作掩码。 + +## 观测向量(32 维 float32) + +| 索引 | 含义 | 归一化 | +|:----:|------|--------| +| 0–1 | 单位位置 (x, y) | / (H, W) | +| 2 | 单位 HP | / max_hp | +| 3 | 原材料背包占比 | raw_inv / capacity | +| 4 | 成品背包占比 | prod_inv / capacity | +| 5 | busy 倒计时 | / 10,截断到 1 | +| 6 | 现金 | log10(money+1) / 5 | +| 7 | 算力 | / 100,截断到 2 | +| 8 | 游戏进度 | time / max_time | +| 9 | 价格相位 sin | sin(2π·t / period) | +| 10 | 价格相位 cos | cos(2π·t / period) | +| 11 | 工厂原料库存 | / storage_cap | +| 12 | 工厂成品库存 | / storage_cap | +| 13 | 生产队列长度 | / 10,截断到 1 | +| 14–16 | 资源点 0 | 相对位置 (dx/H, dy/W) + 库存比 | +| 17–19 | 资源点 1 | 同上 | +| 20–22 | 算力中心 0 | 相对位置 (dx/H, dy/W) + is_open | +| 23–25 | 算力中心 1 | 同上 | +| 26–28 | 市场 0 | 相对位置 (dx/H, dy/W) + best_price | +| 29–31 | 市场 1 | 同上 | + +> 如果实体数量不足(如只有 2 个市场),多余索引保持 0。 + +## 禁止访问的对象 + +以下为内部实现,选手算法**不得依赖**: + +- `env.unit`(Unit 内部字段) +- `env.factory`(Factory 内部字段) +- `env.board`(Board / 地图内部字段) +- `env.markets`(Market 列表) +- `env.money`、`env.compute`、`env.score`(通过 `info` 获取) +- 任何以下划线开头的方法或属性 + +> 评测机只会暴露 `reset`、`step`、`action_masks` 三个公开接口。 + +## 编写规则型策略 + +如果不需要 RL 训练,可以直接读取 `info` 字典做规则决策: + +```python +obs, info = env.reset() +while True: + # 规则策略基于 info 做决策 + if info["money"] > 50: + action = 5 # BUY + else: + action = 7 # HARVEST + obs, reward, terminated, truncated, info = env.step(action) + if terminated or truncated: + break +``` diff --git a/docs/contests/RL9/pve/intro/rule.md b/docs/contests/RL9/pve/intro/rule.md new file mode 100644 index 00000000..55b5afd2 --- /dev/null +++ b/docs/contests/RL9/pve/intro/rule.md @@ -0,0 +1,67 @@ +# PVE 赛制与规则 + +## 概述 + +PVE(PvE-RL)赛道是一个**强化学习**竞技环境。选手需要训练一个智能体,在有限时间内通过买卖商品和采集资源来最大化累计得分。 + +与 PVP 不同的是,PVE 选手**不直接连接游戏服务器**,而是通过标准 **Gymnasium** 接口与一个本地仿真环境交互。选手的算法通过 `reset()` / `step(action)` / `action_masks()` 三个接口控制一个单位,在地图上移动、买卖、采集。 + +## 核心目标 + +**最大化多 seed 下的平均总得分**。得分 = 所有卖出收入之和 × 10。 + +每局从初始资金开始,通过在市场低买高卖、采集资源、解锁算力中心来积累财富。资金归零(破产)或步数耗尽时游戏结束。 + +## 地图与实体 + +| 难度 | 地图 | 市场 | 资源点 | 算力中心 | 初始资金 | 初始算力 | 时长 | +|:----:|:----:|:----:|:------:|:--------:|:--------:|:--------:|:----:| +| **easy** | 5×5 | 3 | 2 | 1 | 200 | 60 | 300s | +| **medium** | 10×10 | 3 | 2 | 2 | 50 | 30 | 300s | +| **hard** | 15×15 | 4 | 4 | 3 | 30 | 20 | 500s | + +- **工厂**位于 `(0, 0)`,是智能体的出生点 +- **市场**:3~4 个,买卖商品 +- **资源点**:2~4 个,可采集原材料(有再生) +- **算力中心**:1~3 个,占领后提供算力加成 +- **障碍物**:地图中随机分布 + +## 商品 + +| ID | 名称 | 购买成本 | 市场价格范围 | 生产时间 | +|:--:|:----:|:--------:|:------------:|:--------:| +| 0 | 半导体 | 10 | 40–120 | 5.0s | +| 1 | 药品 | 5 | 20–60 | 4.0s | +| 2 | 小商品 | 1 | 4–12 | 2.0s | +| 3 | 服饰 | 8 | 32–96 | 6.0s | +| 4 | 食品 | 3 | 12–24 | 1.0s | + +## 市场价格 + +每个市场对每种商品有独立的正弦价格函数: + +``` +price(t) = base + amplitude × (1 + sin(2π·t / period + phase)) / 2 +``` + +- 不同市场的价格**相位随机不同步**,套利窗口随时间移动 +- `price_volatility` 控制波动幅度(easy=0.3, medium=1.0, hard=2.0) + +## 终止条件 + +| 条件 | 类型 | +|------|------| +| `money < 0`(破产) | `terminated = True` | +| `step >= max_steps`(时间耗尽) | `truncated = True` | + +## 与 PVP 的关键区别 + +| | PVP | PVE | +|:--|:---|:---| +| 交互方式 | gRPC 客户端连接服务器 | 本地 Gymnasium 环境 | +| 编程语言 | C++ | Python | +| 动作 | 连续移动 + 多种操作 | 8 个离散动作 | +| 市场定价 | 衰减机制 | 正弦周期波动 | +| 单位数 | 1 Team + 3 Character | 1 个可控单位 | +| 对手 | 其他人类队伍 | 无(纯经济环境) | +| 评测 | 单局得分 | 多 seed 平均得分 | diff --git a/docs/contests/RL9/pve/training/training.md b/docs/contests/RL9/pve/training/training.md new file mode 100644 index 00000000..8a7dfc5f --- /dev/null +++ b/docs/contests/RL9/pve/training/training.md @@ -0,0 +1,103 @@ +# 训练与评测 + +## 环境安装 + +```bash +pip install -r requirements.txt +``` + +## 单元测试 + +验证环境正常: + +```bash +python -m pytest tests/ -v +``` + +## 基础训练 + +```bash +# easy 难度,10 万步 +python TrainingDemo/train_basic.py --config easy --timesteps 100000 + +# medium 难度,20 万步 +python TrainingDemo/train_basic.py --config medium --timesteps 200000 + +# 自定义 YAML 配置 +python TrainingDemo/train_basic.py --config TrainingDemo/configs/medium.yaml --timesteps 200000 +``` + +## 评测模型 + +```bash +python TrainingDemo/evaluate.py --model models/ppo_thuai9_best --config medium --episodes 50 +``` + +输出多 seed 下的平均得分和方差。 + +## MaskablePPO + +使用 `sb3-contrib` 的 `MaskablePPO` 可以自动利用动作掩码: + +```python +from sb3_contrib import MaskablePPO +from sb3_contrib.common.wrappers import ActionMasker + +def mask_fn(env): + return env.unwrapped.action_masks() + +env = ActionMasker(env, mask_fn) +model = MaskablePPO("MlpPolicy", env, verbose=1) +model.learn(total_timesteps=100000) +model.save("ppo_thuai9") +``` + +## 难度对比 + +| 参数 | easy | medium | hard | +|------|:----:|:------:|:----:| +| 地图 | 5×5 | 10×10 | 15×15 | +| 市场数 | 3 | 3 | 4 | +| 资源点数 | 2 | 2 | 4 | +| 算力中心数 | 1 | 2 | 3 | +| 初始资金 | 200 | 50 | 30 | +| 初始算力 | 60 | 30 | 20 | +| 价格波动 | 0.3x | 1.0x | 2.0x | +| 资源再生 | 2.0x | 1.0x | 0.5x | +| 初始资源 | 200 | 100 | 50 | +| 游戏时长 | 300s | 300s | 500s | + +## 项目结构 + +``` +logic/pve/ +├── GameLogic/ # 游戏规则与状态管理(算法不可直接修改) +│ ├── config.py # 全局配置(难度参数化) +│ ├── board.py # 地图、资源点、算力中心 +│ ├── character.py # 单位(HP、背包、状态机) +│ ├── market.py # 市场动态价格函数 +│ ├── action_space.py # 动作空间与动作掩码 +│ ├── reward_calculator.py # 奖励计算 +│ └── game_env.py # 主环境(Gymnasium 接口) +├── RLInterfaces/ # RL 算法接口层 +│ ├── base_agent.py # 抽象基类 +│ ├── ppo_agent.py # PPO 实现(支持 MaskablePPO) +│ └── training_loop.py # 训练循环 +├── TrainingDemo/ # 训练与评测脚本 +│ ├── train_basic.py # 训练入口 +│ ├── evaluate.py # 评测脚本 +│ ├── visualization.py # ASCII 渲染 + 奖励曲线 +│ └── configs/ # YAML 配置 +├── tests/ # 单元测试 +└── docs/ # 选手/开发者文档 +``` + +## 建议方向 + +- 使用 `action_masks()` 过滤无效动作 +- BFS / A* 路径规划,找最优路线 +- 建模市场价格正弦周期(利用观测中的 sin/cos 相位) +- 选择高利润商品(半导体 40–120、服饰 32–96) +- 规则策略 + RL 混合(Hybrid Policy) +- 课程学习(easy → medium → hard) +- Recurrent Policy 识别价格周期 diff --git a/sidebars.js b/sidebars.js index 51ad7bec..d55936aa 100644 --- a/sidebars.js +++ b/sidebars.js @@ -254,6 +254,42 @@ module.exports = { }, ] }, + { + "RL9": [ + { + "PVE": [ + "contests/RL9/pve/README", + { + "引入": [ + "contests/RL9/pve/intro/rule", + ], + }, + { + "游戏": [ + "contests/RL9/pve/game/actions", + "contests/RL9/pve/game/state", + "contests/RL9/pve/game/reward", + ], + }, + { + "接口": [ + "contests/RL9/pve/interface/gym", + ], + }, + { + "训练": [ + "contests/RL9/pve/training/training", + ], + }, + { + "常见问题": [ + "contests/RL9/pve/faq/README", + ], + }, + ] + }, + ] + }, { "THUAI9": [ "contests/THUAI9/README",