Skip to content

Commit 85aa0ac

Browse files
authored
RL9 (#1383)
* 1 * 2
1 parent 35ad13e commit 85aa0ac

9 files changed

Lines changed: 489 additions & 0 deletions

File tree

docs/contests/RL9/pve/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
title: THUAI9
3+
slug: ./
4+
---
5+
## 赛事名称
6+
7+
AI 工厂模拟(THUAI9)
8+
9+
## PVE 概要
10+
11+
强化学习竞技环境。选手训练智能体在地图上移动、低买高卖、采集资源,在有限时间内最大化累计得分。提供 easy / medium / hard 三级难度,支持 MaskablePPO 训练。比赛以多 seed 平均得分排名。
12+
13+
---
14+
15+
## 选手包使用说明
16+
17+
不熟悉强化学习的可以访问https://github.com/konpoku/THUAI9-RL,在小项目中学习一下
18+
19+
详细的使用说明参照选手包里的logic\pve\docs\CONTESTANT_GUIDE.md
20+
21+
---
22+
23+
## 相关链接
24+
25+
+ THUAI9 GitHub 仓库:[https://github.com/lch24/THUAI9](https://github.com/lch24/THUAI9)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# 常见问题(PVE / Python / RL)
2+
3+
## 环境相关
4+
5+
> Q: 需要什么 Python 版本?
6+
>
7+
> A: Python 3.9+。依赖见 `logic/pve/requirements.txt`
8+
9+
> Q: 运行 `import GameLogic` 报错?
10+
>
11+
> A: 确保在 `logic/pve/` 目录下运行,或将 `logic/pve/` 加入 `PYTHONPATH`
12+
13+
> Q: 如何切换难度?
14+
>
15+
> A: `GameConfig.easy()` / `.medium()` / `.hard()` 预设,或传入 YAML 文件路径。
16+
17+
## 训练相关
18+
19+
> Q: PPO 训练不收敛?
20+
>
21+
> A: 尝试:(1) 使用 `MaskablePPO`;(2) 降低 `price_volatility`;(3) easy 地图上先训练。
22+
23+
> Q: reward 和 score 的区别?
24+
>
25+
> A: reward 是训练辅助信号(含塑形奖励和惩罚),score 是最终排名依据(仅卖出得分×10)。比赛看 score。
26+
27+
> Q: 动作掩码有什么用?
28+
>
29+
> A: 在训练时告诉 PPO 哪些动作当前无效,避免探索无效方向。对规则策略同样有用。
30+
31+
## 接口相关
32+
33+
> Q: 能直接读 `env.unit` 吗?
34+
>
35+
> A: **不能**。评测机只暴露 `reset/step/action_masks` 三个标准接口。所有信息通过 `obs``info` 获取。
36+
37+
> Q: 观测向量怎么理解?
38+
>
39+
> A: 32 维 float32,包含位置、HP、背包、现金、市场价格相位、最近资源点/市场/算力中心相对位置等。详见 [公开接口](../interface/gym.md)
40+
41+
> Q: 怎么写规则策略(不用 RL)?
42+
>
43+
> A: 读取 `info` 字典做 if-else 或状态机决策,直接调 `env.step()`
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# 动作空间
2+
3+
PVE 使用 **8 个离散动作**
4+
5+
| 编号 | 动作 | 含义 | 有效性条件 |
6+
|:----:|:----:|------|------------|
7+
| 0 | `WAIT` | 等待一个 tick | 始终有效 |
8+
| 1 | `MOVE_UP` | 向上移动 (x−1) | 目标格可通行,单位不 busy |
9+
| 2 | `MOVE_DOWN` | 向下移动 (x+1) | 同上 |
10+
| 3 | `MOVE_LEFT` | 向左移动 (y−1) | 同上 |
11+
| 4 | `MOVE_RIGHT` | 向右移动 (y+1) | 同上 |
12+
| 5 | `BUY` | 在相邻市场买最便宜的可负担商品 | Manhattan ≤1 有市场,背包有空间,现金充足 |
13+
| 6 | `SELL` | 在相邻市场卖出背包内所有商品 | Manhattan ≤1 有市场,背包有商品 |
14+
| 7 | `HARVEST` | 从附近资源点采集原材料 | Manhattan ≤2 有未耗尽资源,背包有空间 |
15+
16+
- **BUY**:自动购买当前市场价格最低的可负担商品
17+
- **SELL**:一次性卖出背包中所有商品,获得当前市场价
18+
- **HARVEST**:采集范围 2 格(Manhattan 距离)
19+
- 执行无效动作不会报错,但受到 **-0.05 分惩罚**并浪费步数
20+
21+
## 动作掩码
22+
23+
环境提供 `action_masks()` 方法,返回 `(8,)` 布尔数组,`True` 表示该动作当前有效。使用 `MaskablePPO` 可以自动过滤无效动作:
24+
25+
```python
26+
from sb3_contrib import MaskablePPO
27+
from sb3_contrib.common.wrappers import ActionMasker
28+
29+
def mask_fn(env):
30+
return env.unwrapped.action_masks()
31+
32+
masked_env = ActionMasker(env, mask_fn)
33+
model = MaskablePPO("MlpPolicy", masked_env)
34+
```
35+
36+
建议所有策略先查询 `action_masks()` 再决定动作。
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# 奖励与得分
2+
3+
## 得分(Score)
4+
5+
最终排名依据的指标。只在 **SELL 动作成功**时增加:
6+
7+
```
8+
score += revenue × score_factor(默认 × 10)
9+
```
10+
11+
## 单步奖励(RL Reward)
12+
13+
训练时的辅助信号,由以下部分叠加:
14+
15+
| 来源 || 说明 |
16+
|------|:--:|------|
17+
| 现金变化 Δmoney | × 0.01 | 正负均有 |
18+
| 得分变化 Δscore | × 0.01 | 仅卖出时为正 |
19+
| 时间惩罚(每步) | −0.002 | 鼓励高效路径 |
20+
| 采集奖励(每单位) | +0.001 | 采集塑形 |
21+
| 算力中心解锁(一次性) | +0.5 | 进度奖励 |
22+
| 无效动作惩罚 | −0.05 | 每步 |
23+
| 破产惩罚(terminated 时) | −10.0 | 终端惩罚 |
24+
25+
> 奖励是训练辅助信号。**最终排名以 `info["score"]` 为准**,不是累计奖励。
26+
27+
## `step()` 返回的 info 字典
28+
29+
| 字段 | 类型 | 含义 |
30+
|------|------|------|
31+
| `step` | `int` | 当前步数 |
32+
| `time` | `float` | 游戏时间(秒) |
33+
| `money` | `float` | 当前现金 |
34+
| `score` | `float` | 当前累计得分 |
35+
| `compute` | `float` | 当前算力 |
36+
| `action_valid` | `bool` | 上一步动作是否有效 |
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# 游戏状态
2+
3+
## 单位属性
4+
5+
| 属性 ||
6+
|:----:|:--:|
7+
| 血量 | 300 |
8+
| 背包容量 | 30 |
9+
| 采集速率 | 10/s |
10+
| 算力中心占领时间 | 10s |
11+
12+
- 背包分为**原材料****成品**两部分
13+
- `busy_ticks > 0` 时单位忙碌,忽略新指令
14+
15+
## 工厂
16+
17+
| 属性 ||
18+
|:----:|:--:|
19+
| 位置 | (0, 0) |
20+
| 仓储上限 | 300 |
21+
| 初始生产线数 | 3 |
22+
23+
## 算力产出
24+
25+
- 每个已占领算力中心:**1 算力/秒**
26+
- 可花费算力招募新单位(Phase 2)
27+
28+
## 资源再生
29+
30+
资源点库存会随时间缓慢再生:
31+
32+
```
33+
regen(t) = rate × (1 + sin(2π·t / period)) / 2
34+
```
35+
36+
- 再生倍率:easy=2.0, medium=1.0, hard=0.5
37+
- 资源耗尽(`depleted=True`)后停止再生
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# 公开接口
2+
3+
PVE 算法**只能**通过标准 Gymnasium 接口与环境交互。不得直接访问内部对象。
4+
5+
## 环境初始化
6+
7+
```python
8+
from GameLogic import GameEnvironment, GameConfig
9+
10+
# 内置难度
11+
env = GameEnvironment(cfg=GameConfig.easy())
12+
env = GameEnvironment(cfg=GameConfig.medium())
13+
env = GameEnvironment(cfg=GameConfig.hard())
14+
15+
# 自定义配置
16+
env = GameEnvironment(cfg=GameConfig.from_dict({
17+
"map_width": 8, "map_height": 8,
18+
"num_markets": 4, "initial_money": 100.0,
19+
}))
20+
```
21+
22+
## 接口方法
23+
24+
### `reset()`
25+
26+
```python
27+
obs, info = env.reset(seed=0)
28+
```
29+
30+
重置环境,返回初始观测和 info。
31+
32+
### `step()`
33+
34+
```python
35+
obs, reward, terminated, truncated, info = env.step(action)
36+
# action: int, 0-7
37+
# obs: np.ndarray, shape (32,), dtype float32
38+
# reward: float
39+
# terminated: bool (money < 0,破产)
40+
# truncated: bool (步数耗尽,正常结束)
41+
# info: dict
42+
```
43+
44+
### `action_masks()`
45+
46+
```python
47+
mask = env.action_masks() # np.ndarray[bool], shape (8,)
48+
```
49+
50+
返回当前有效的动作掩码。
51+
52+
## 观测向量(32 维 float32)
53+
54+
| 索引 | 含义 | 归一化 |
55+
|:----:|------|--------|
56+
| 0–1 | 单位位置 (x, y) | / (H, W) |
57+
| 2 | 单位 HP | / max_hp |
58+
| 3 | 原材料背包占比 | raw_inv / capacity |
59+
| 4 | 成品背包占比 | prod_inv / capacity |
60+
| 5 | busy 倒计时 | / 10,截断到 1 |
61+
| 6 | 现金 | log10(money+1) / 5 |
62+
| 7 | 算力 | / 100,截断到 2 |
63+
| 8 | 游戏进度 | time / max_time |
64+
| 9 | 价格相位 sin | sin(2π·t / period) |
65+
| 10 | 价格相位 cos | cos(2π·t / period) |
66+
| 11 | 工厂原料库存 | / storage_cap |
67+
| 12 | 工厂成品库存 | / storage_cap |
68+
| 13 | 生产队列长度 | / 10,截断到 1 |
69+
| 14–16 | 资源点 0 | 相对位置 (dx/H, dy/W) + 库存比 |
70+
| 17–19 | 资源点 1 | 同上 |
71+
| 20–22 | 算力中心 0 | 相对位置 (dx/H, dy/W) + is_open |
72+
| 23–25 | 算力中心 1 | 同上 |
73+
| 26–28 | 市场 0 | 相对位置 (dx/H, dy/W) + best_price |
74+
| 29–31 | 市场 1 | 同上 |
75+
76+
> 如果实体数量不足(如只有 2 个市场),多余索引保持 0。
77+
78+
## 禁止访问的对象
79+
80+
以下为内部实现,选手算法**不得依赖**
81+
82+
- `env.unit`(Unit 内部字段)
83+
- `env.factory`(Factory 内部字段)
84+
- `env.board`(Board / 地图内部字段)
85+
- `env.markets`(Market 列表)
86+
- `env.money``env.compute``env.score`(通过 `info` 获取)
87+
- 任何以下划线开头的方法或属性
88+
89+
> 评测机只会暴露 `reset``step``action_masks` 三个公开接口。
90+
91+
## 编写规则型策略
92+
93+
如果不需要 RL 训练,可以直接读取 `info` 字典做规则决策:
94+
95+
```python
96+
obs, info = env.reset()
97+
while True:
98+
# 规则策略基于 info 做决策
99+
if info["money"] > 50:
100+
action = 5 # BUY
101+
else:
102+
action = 7 # HARVEST
103+
obs, reward, terminated, truncated, info = env.step(action)
104+
if terminated or truncated:
105+
break
106+
```
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# PVE 赛制与规则
2+
3+
## 概述
4+
5+
PVE(PvE-RL)赛道是一个**强化学习**竞技环境。选手需要训练一个智能体,在有限时间内通过买卖商品和采集资源来最大化累计得分。
6+
7+
与 PVP 不同的是,PVE 选手**不直接连接游戏服务器**,而是通过标准 **Gymnasium** 接口与一个本地仿真环境交互。选手的算法通过 `reset()` / `step(action)` / `action_masks()` 三个接口控制一个单位,在地图上移动、买卖、采集。
8+
9+
## 核心目标
10+
11+
**最大化多 seed 下的平均总得分**。得分 = 所有卖出收入之和 × 10。
12+
13+
每局从初始资金开始,通过在市场低买高卖、采集资源、解锁算力中心来积累财富。资金归零(破产)或步数耗尽时游戏结束。
14+
15+
## 地图与实体
16+
17+
| 难度 | 地图 | 市场 | 资源点 | 算力中心 | 初始资金 | 初始算力 | 时长 |
18+
|:----:|:----:|:----:|:------:|:--------:|:--------:|:--------:|:----:|
19+
| **easy** | 5×5 | 3 | 2 | 1 | 200 | 60 | 300s |
20+
| **medium** | 10×10 | 3 | 2 | 2 | 50 | 30 | 300s |
21+
| **hard** | 15×15 | 4 | 4 | 3 | 30 | 20 | 500s |
22+
23+
- **工厂**位于 `(0, 0)`,是智能体的出生点
24+
- **市场**:3~4 个,买卖商品
25+
- **资源点**:2~4 个,可采集原材料(有再生)
26+
- **算力中心**:1~3 个,占领后提供算力加成
27+
- **障碍物**:地图中随机分布
28+
29+
## 商品
30+
31+
| ID | 名称 | 购买成本 | 市场价格范围 | 生产时间 |
32+
|:--:|:----:|:--------:|:------------:|:--------:|
33+
| 0 | 半导体 | 10 | 40–120 | 5.0s |
34+
| 1 | 药品 | 5 | 20–60 | 4.0s |
35+
| 2 | 小商品 | 1 | 4–12 | 2.0s |
36+
| 3 | 服饰 | 8 | 32–96 | 6.0s |
37+
| 4 | 食品 | 3 | 12–24 | 1.0s |
38+
39+
## 市场价格
40+
41+
每个市场对每种商品有独立的正弦价格函数:
42+
43+
```
44+
price(t) = base + amplitude × (1 + sin(2π·t / period + phase)) / 2
45+
```
46+
47+
- 不同市场的价格**相位随机不同步**,套利窗口随时间移动
48+
- `price_volatility` 控制波动幅度(easy=0.3, medium=1.0, hard=2.0)
49+
50+
## 终止条件
51+
52+
| 条件 | 类型 |
53+
|------|------|
54+
| `money < 0`(破产) | `terminated = True` |
55+
| `step >= max_steps`(时间耗尽) | `truncated = True` |
56+
57+
## 与 PVP 的关键区别
58+
59+
| | PVP | PVE |
60+
|:--|:---|:---|
61+
| 交互方式 | gRPC 客户端连接服务器 | 本地 Gymnasium 环境 |
62+
| 编程语言 | C++ | Python |
63+
| 动作 | 连续移动 + 多种操作 | 8 个离散动作 |
64+
| 市场定价 | 衰减机制 | 正弦周期波动 |
65+
| 单位数 | 1 Team + 3 Character | 1 个可控单位 |
66+
| 对手 | 其他人类队伍 | 无(纯经济环境) |
67+
| 评测 | 单局得分 | 多 seed 平均得分 |

0 commit comments

Comments
 (0)