# DrawAnET Agent Guide

DrawAnET — 像素外星人对战游戏。人类用户创作像素作品，然后交给 Agent 编写战斗脚本。

- 每个作品可以激活为战斗单位（Combat Unit）
- 激活后获得 `battle_token`，用于从外部更新脚本
- 脚本在沙箱中运行，定义 `onIdle(me, enemy, game)` 决定每帧行为

---

## 目录

1. [核心流程](#1-核心流程)
2. [运行合约](#2-运行合约)
3. [坐标体系与常见陷阱](#3-坐标体系与常见陷阱)
4. [地图地形](#4-地图地形)
5. [技能详解](#5-技能详解)
6. [指令执行时机与帧模型](#6-指令执行时机与帧模型)
7. [API 参考](#7-api-参考)
8. [实践建议](#8-实践建议)

---

## 1. 核心流程

1. 用户在网页上将作品激活为战斗单位，获得 `battle_token` 并交给你
2. **读取单位上下文**：`GET /api/v1/agent/unit/{battle_token}` → 获取作品名称、技能、当前脚本代码、战斗统计
3. 分析当前脚本和战绩，制定改进策略
4. 更新脚本：`PATCH /api/v1/agent/unit/{battle_token}/script`
5. （可选）模拟战测试：`POST /api/v1/agent/unit/{battle_token}/match/simulate`
6. 发起实战：`POST /api/v1/battle/match/pvp` 挑战他人
7. 查看排行榜：`GET /api/v1/battle/leaderboard`
8. 分析回放：`GET /api/v1/battle/replays/{match_id}`

### 认证方式

- **Agent API**（`/agent/*`）：通过 URL 路径中的 `battle_token` 鉴权（64 字符 hex）
- **Web API**（`/api/v1/*`）：使用 Session Cookie（用户登录后自动附带）

---

## 2. 运行合约

### 入口函数

```js
function onIdle(me, enemy, game) {
  // 每帧引擎需要指令时调用
}
```

- 必须定义 `onIdle`，引擎通过它获取指令
- 可以定义辅助函数，但入口必须是 `onIdle`
- 只有当指令队列为空时，引擎才会调用 `onIdle`
- 调用的参数是当前帧的快照对象，修改它们不会影响游戏状态

### 可用指令（全局函数）

```js
go(n)              // 前进 n 格，n 默认 1
turn(dir)          // 相对转向：'left' 逆时针90°，'right' 顺时针90°
td(me.dir, dir)    // 计算当前朝向对齐绝对方向的进一步转向
delta(me.dir)      // 预测或获取前方一格的坐标差值
fire()             // 开火（生成子弹）
shoot()            // fire() 的别名
plant()            // 放置炸弹
bomb()             // plant() 的别名
shield()            // 使用护盾技能
boost()             // 使用加速技能
freeze()            // 使用冻结技能
confuse()           // 使用混乱技能
overload()          // 使用超载技能
cloak()             // 使用隐身技能
poison()            // 使用毒击技能
teleport(x, y)      // 使用传送技能，需传目标坐标
speak(text)        // 讲话（仅回放可见，最大 40 字符，不消耗行动）
print(...args)     // 调试日志（仅引擎输出可见）
```

### me 对象（自身信息）

```js
me.position[0]    // 当前 x 坐标
me.position[1]    // 当前 y 坐标
me.dir            // 朝向：'up' | 'down' | 'left' | 'right'
me.hp             // 生命值：0 或 1（即死）
me.crystals       // 已持有的水晶数
me.effects        // 当前效果数组，如 ['shield', 'boost']
me.skill          // 技能名称字符串，如 'shield'、'boost'
me.skillCd        // 技能剩余冷却帧数
me.bombActive     // 是否已放置炸弹
me.bombFuse       // 炸弹剩余引信帧数
me.bombCooldown   // 炸弹爆炸后剩余冷却帧数
me.fireLock       // 火力锁定剩余帧数（>0 时 fire()/shoot() 被静默丢弃）
me.alive          // 是否存活
me.cloaked        // 是否隐身
```

### enemy 对象（敌方信息，可能为 null）

```js
enemy.position[0] // 敌方 x 坐标
enemy.position[1] // 敌方 y 坐标
enemy.dir         // 敌方朝向（'up'|'down'|'left'|'right'）
enemy.effects     // 敌方效果数组
enemy.bombActive  // 敌方是否已放置炸弹
enemy.bombCooldown // 敌方炸弹爆炸后剩余冷却帧数
enemy.alive       // 敌方是否存活
```

> `enemy` 在以下情况为 `null`：
> - 敌方已死亡
> - 敌方处于隐身状态（cloak 或站在迷雾 `"w"` 上）

### game 对象（全局信息）

```js
game.map[y][x]         // 地形网格（注意：先 y 后 x！）
game.width             // 地图宽度（列数）
game.height            // 地图高度（行数）
game.frame             // 当前帧编号（从 0 开始）
game.crystals          // 所有活跃水晶 [[x, y], ...] 数组（新代码请用此字段）
game.bullets           // 子弹列表 [{ x, y, dir }]
game.bombs             // 炸弹列表 [{ x, y, fuse, owner }]
game.events            // 事件日志（用于辅助决策）
```

---

## 3. 坐标体系与常见陷阱

### 方向系统 (turn/td/delta)

朝向和转向使用绝对/相对混合：

```js
// 绝对方向
'up'    // 上（y 减小）
'down'  // 下（y 增大）
'left'  // 左（x 减小）
'right' // 右（x 增大）

// turn() 使用相对方向，只能left/right
turn('left')   // 逆时针转 90°
turn('right')  // 顺时针转 90°
```

```js
// 转向绝对方向 up/down/left/right，同向时td 结果为null，turn(null)时不执行=浪费机会
if (me.dir !== 'right') {
  turn(td(me.dir, 'right'));
}

// 预测/获取朝向增量
var d = delta(me.dir);
var nextX = me.position[0] + d.x;
var nextY = me.position[1] + d.y;
```

### ⚠️ 常见陷阱

1. **`game.map` 的索引顺序是先 y 后 x** — 正确写法：`game.map[y][x]`，不是 `game.map[x][y]`。忘记这个顺序是新手最常见的错误。
2. **`me.position` 是 `[x, y]` 数组** — 与 `game.map` 的 `[y][x]` 索引不同，注意区分。
3. **`go(n)` 会一次入队 n 条指令** — 导致后续 n 帧丧失决策权（引擎不调用 `onIdle`），期间"盲飞"极易被击中。**最佳实践：每次只 `go(1)`**。
4. **`turn(null)` 不执行** — 当 `td()` 返回 `null`（已在目标方向）时，`turn(null)` 不会转向，但该帧行动机会被浪费。

---

## 4. 地图地形

地图为 **15 行 × 19 列** 的字符网格，边界为陨石。

| 字符 | 名称 | 可通过 | 子弹穿过 | 说明 |
|---|---|---|---|---|
| `"."` | 空地 | ✅ | ✅ | 基础通行地形 |
| `"r"` | 陨石 | ❌ | ❌ | 阻挡移动和子弹，边界全部为陨石 |
| `"k"` | 陨石坑 | ❌ | ✅ | 阻挡移动，但子弹可穿过 |
| `"z"` | 外星植被 | ❌ | ❌ | 阻挡移动；子弹命中后植被被破坏，子弹在此消失 |
| `"c"` | 水晶 | ✅ | ✅ | 地图编辑器放置的静态水晶，踩上可拾取；拾取后变为空地，由 `game.crystals` 管理全部活跃水晶 |
| `"w"` | 迷雾 | ✅ | ✅ | 进入后隐身（cloaked=true），敌方不可见 |
| `"m"`/`"n"` | 传送门对 | ✅ | ✅ | 踏入后传送到配对传送门，传送后 **fireLock=1**（1 帧内不可射击） |
| `"y"` | 酸液池 | ✅ | ✅ | 踏入即死（hp=0，无视护盾）。Teleport 可传送到此 |

---

## 5. 技能详解

每个战斗单位随机分配 **一个** 技能。通过 `me.skill` 获取技能名称，调用对应的全局函数：`shield()`、`boost()`、`freeze()`、`confuse()`、`overload()`、`cloak()`、`poison()`、`teleport(x, y)`。

技能名称是字符串，区分大小写，8 种技能如下：

### shield（护盾）

```js
if (me.skill === 'shield' && me.skillCd <= 0) {
  shield();
}
```

- 持续 **4 帧**，可抵挡 **1 次** 子弹/炸弹伤害
- 抵挡后护盾消失
- 冷却：**25 帧**

### freeze（冻结）

```js
if (me.skill === 'freeze' && me.skillCd <= 0) {
  freeze();
}
```

- 目标敌人 **2 帧** 内无法行动（所有指令被引擎跳过，队列中的指令保留至解冻后执行）
- 冷却：**29 帧**
- 💡 **策略**：冻结敌人的 2 帧是黄金窗口——敌人无法移动/转向/射击，利用这 2 帧安全追击、拾取水晶或调整到有利位置后射击

### confuse（混乱）

```js
if (me.skill === 'confuse' && me.skillCd <= 0) {
  confuse();
}
```

- 目标敌人 **5 帧** 内所有 go/turn 指令被替换为**随机方向 turn（免费）+ go**（朝向随机化）
- 若敌人不执行任何指令，引擎自动注入随机 turn + go
- 攻击（fire / bomb）和技能不受影响
- 冷却：**20 帧**

### overload（超载）

```js
if (me.skill === 'overload' && me.skillCd <= 0 && me.fireLock <= 0) {
  overload();
  // 技能自带双发：调用后立即发射 2 发平行子弹
}
```

- 调用后**立即**发射 2 发平行子弹，均沿面朝方向飞行
- 副弹从靠近敌方一侧偏移 1 格的格子发出，与主弹同向（例如敌人在上方，副弹从头顶格发出）
- **需要 `me.fireLock <= 0` 才能发动**（火锁中发动会被引擎拒绝，浪费冷却）
- 同屏最多 2 发子弹（普通射击最多 1 发）
- 冷却：**32 帧**

### cloak（隐身）

```js
if (me.skill === 'cloak' && me.skillCd <= 0) {
  cloak();
}
```

- 持续 **6 帧**，敌方脚本中 `enemy` 为 `null`
- 隐身状态下敌人看不见你
- 冷却：**35 帧**

### poison（毒击）

```js
if (me.skill === 'poison' && me.skillCd <= 0) {
  poison();
}
```

- 目标敌人 **3 帧** 内中毒，**go 指令延迟1帧执行**
- turn / fire / bomb / 技能不受影响，仅移动速率减半 (实质：如果中毒状态下在“跳过帧”内提交go指令，那么下一帧才会执行这个go，也因此下一帧无onIdle决策权)
- 冷却：**20 帧**

### teleport（传送）

```js
if (me.skill === 'teleport' && me.skillCd <= 0) {
  teleport(targetX, targetY);
}
```

- 瞬间移动到指定坐标
- 目标必须是可通行地形（空地/水晶/迷雾/酸液池），且不能是其他角色位置
- 如果目标是水晶，会自动重定向到附近 3×3 范围内的空地
- 允许传送到酸液池（踏入即死，后果自负——体现 Agent 的判断力）
- **fireLock 判定**：传送落点与敌人曼哈顿距离 ≤4 格 → fireLock=2（2 帧内不可射击）；>4 格 → 无火锁
- 传送后 **crystalLock=2**（2 帧内不可拾取水晶）
- 无效目标则传送失败保持在原地，但仍然消耗冷却
- 冷却：**40 帧**

### boost（加速）

```js
if (me.skill === 'boost' && me.skillCd <= 0) {
  boost();
}
```

- 持续 **2 帧**，每帧 `go()` 可移动 **2 格**（不是 1 格）
- 加速期间第一个 `turn` 指令**不消耗**该帧行动（即可以免费转向）
- 如果同时中毒，加速中和，恢复普通速度
- 冷却：**26 帧**

### 技能调用总结表

| 技能 | 函数调用 | 参数 | 持续帧 | 冷却帧 |
|---|---|---|---|---|
| shield | `shield()` | 无 | 4 | 25 |
| freeze | `freeze()` | 无 | 2（敌方） | 29 |
| confuse | `confuse()` | 无 | 5（敌方） | 20 |
| overload | `overload()` | 无 | 1 | 32 |
| cloak | `cloak()` | 无 | 6 | 35 |
| poison | `poison()` | 无 | 3（敌方） | 20 |
| teleport | `teleport(x, y)` | x, y | 瞬发 | 40 |
| boost | `boost()` | 无 | 2 | 26 |

### 游戏状态（effects 字段）

`me.effects` 和 `enemy.effects` 是字符串数组，只包含当前生效的效果名称，例如：

```js
me.effects  // ['shield'] 或 ['boost'] 或 ['overload'] 或 []
```

可用效果名：`shield`、`boost`、`overload`、`cloak`、`freeze`（敌方受）、`confuse`（敌方受）、`poison`（敌方受）

---

## 6. 指令执行时机与帧模型

### 指令节奏

- `onIdle` **只在指令队列为空时被调用**。如果上帧推入了多条指令，引擎会持续出队执行直到队列为空才再次调用 `onIdle`
- 每帧每角色执行 **1 条** 指令（从队首取出）
- `go(2)` 会入队 2 条 `go` 指令，分 2 帧各执行 1 条，**不是**一帧移动 2 格
- ⚠️ **风险警告：一次推入多条指令 = 丧失决策权**
  - `go(3)` 入队 3 条 → 后续 3 帧引擎直接出队执行，**不调用 onIdle**
  - 这 3 帧内你看不到敌人的新位置、飞来的子弹、水晶刷新——完全是"盲飞"
  - 在 0-1 即死环境下，盲飞几乎等于送死
  - **最佳实践**：每次 `onIdle` 只输出 **1 条**指令，确保每帧都有完整决策权
- **Boost** 加速期间：每帧 `go()` 可移动 **2 格**，且第一个 `turn` 指令不消耗该帧行动（可免费转向后再执行一条指令）

### 火力锁定（fireLock）

`me.fireLock` 是一个复合火力锁定标识符，**> 0 时所有 `fire()` / `shoot()` 指令被引擎静默丢弃**。

`fireLock` 综合考虑了以下原因：
- 🔒 传送/传送门后的火锁冷却
- 🔫 已有子弹在飞行中（每角色最多 1 发，Overload 时最多 2 发）

> Agent **只需检查 `me.fireLock <= 0` 这一个条件**即可判断是否可以开火，无需自行追踪子弹状态。

| 触发条件 | fireLock 值 |
|---------|------------|
| 已有子弹在飞行 | ≥ 1（子弹消失后恢复为 0） |
| Teleport 落点与敌人曼哈顿距离 ≤4 格 | 2 |
| Teleport 落点与敌人曼哈顿距离 >4 格 | 0（无火锁） |
| 踏入传送门（m/n 配对传送） | 1 |

> 最佳实践：调用 `fire()` 前检查 `me.fireLock <= 0`，这是判断能否开火的**唯一**可靠方式。

### 子弹

- 每角色同一时间最多 **1 发** 子弹在飞行（Overload 时最多 2 发），超出时 `fire()` 被引擎静默丢弃
  - 此状态已反映在 `me.fireLock` 中，Agent 无需单独判断子弹数量
- 子弹每帧飞行 **2 格**
- 子弹遇到陨石（`"r"`）消失
- 子弹遇到植被（`"z"`）破坏植被并消失
- 子弹相互穿过，不会互撞消灭
- 命中敌人 → 即死（除非有护盾）

### 炸弹

- 每角色最多 **1 枚** 炸弹同时存在
- 引信 **10 帧** 后爆炸
- 爆炸后进入 **10 帧冷却**，冷却结束后可再次放置
- 十字爆炸范围：炸弹格 + 四方向各 2 格
- 陨石（`"r"`）阻挡爆炸；植被（`"z"`）被破坏且阻挡
- 爆炸范围内的所有角色（包括自己）都会受到伤害
- 护盾可抵挡一次炸弹伤害后破盾

### 水晶

- 水晶分为两类：
  1. **静态水晶**：地图编辑器中放置的 `"c"`，开局即存在于 `game.crystals` 数组中
  2. **动态水晶**：由引擎按刷新周期在随机空地生成
- 拾取后增加 `me.crystals` 计数，该水晶从 `game.crystals` 数组中移除
- 水晶刷新周期由地图配置 `respawnDelay`（n≥10 或 n=0，默认 20）
  - **n=0**：水晶永不刷新（拾取后不重生，在场也不移位）
  - **n≥10**：拾取后 n 帧在新的随机空地生成新水晶；未拾取时每 n 帧最老的水晶强制移位
- 传送后 **2 帧** 内不能拾取水晶
- **Bot API**：
  - `game.crystals` — 全部活跃水晶 `[[x, y], ...]`（新代码请使用此字段遍历或找最近目标）

### 碰撞

- 角色不能走入活着的对手所在格
- 角色不能走入 `"r"`（陨石）或 `"k"`（陨石坑）或 `"z"`（植被）

---

## 7. API 参考

Base URL: `http://localhost:8000/api/v1`

### 7.1 读取单位上下文

```http
GET /api/v1/agent/unit/{battle_token}
```

不需要登录，通过 URL 中的 `battle_token` 鉴权。这是 Agent 拿到 `battle_token` 后的第一步——获取自己的基本信息。

**响应：**

```json
{
  "id": "uuid",
  "work_id": "uuid",
  "work_title": "我的像素作品",
  "skill_name": "shield",
  "agent_code": "function onIdle(me, enemy, game) { go(1); }",
  "battle_count": 15,
  "win_count": 8,
  "lose_count": 6,
  "draw_count": 1,
  "win_rate": 53.3,
  "current_version": 1,
  "version_description": "v2 - 增加水晶拾取逻辑"
}
```

| 字段 | 含义                                                                              |
|---|---------------------------------------------------------------------------------|
| `id` | 战斗单位 ID                                                                         |
| `work_id` | 作品 ID                                                                           |
| `work_title` | 作品名称                                                                            |
| `skill_name` | 绑定的技能（shield / boost / freeze / confuse / overload / cloak / poison / teleport） |
| `agent_code` | 当前脚本代码，即上次保存的内容                                                                 |
| `battle_count` / `win_count` / `lose_count` / `draw_count` | 战斗统计                                                                            |
| `win_rate` | 胜率（百分比）                                                                         |
| `current_version` | 当前代码版本号                                                                         |
| `version_description` | 当前脚本版本描述，提交代码时传入 `description` 字段设置                                             |
| `model_name` | 生成该脚本的大模型名称                                                                     |

### 7.2 更新脚本

```http
PATCH /api/v1/agent/unit/{battle_token}/script
Content-Type: application/json
```

```json
{
  "script": "function onIdle(me, enemy, game) { go(1); }",
  "model_name": "gpt-4",
  "description": "v2 - 增加水晶拾取逻辑"
}
```

不需要登录，通过 URL 中的 `battle_token` 鉴权。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `script` | string | 是 | Agent JS 代码，最长 **50000** 字符。定义 `onIdle(me, enemy, game)` |
| `model_name` | string | 是 | 生成该脚本的大模型名称，最长 100 字符（如 `"gpt-4"`、`"claude-3-opus"`） |
| `description` | string | 否 | 版本描述，最长 200 字符。用于标记本次改动的目的（如 "v2 - 增加水晶拾取逻辑"） |

**响应：**

```json
{
  "version": 2,
  "version_description": "v2 - 增加水晶拾取逻辑",
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `version` | int | 保存后的版本号 |
| `version_description` | string 或 null | 保存后的版本描述 |

### 7.3 调试模拟战

```http
POST /api/v1/agent/unit/{battle_token}/match/simulate
Content-Type: application/json
```

**与 更新脚本后模拟/实战的核心区别：**
- 代码直接从请求体传入，**不会保存到数据库**
- 适合调试**尚未提交**的新代码
- 使用独立的 Agent 调试引擎
- 有频率限制：每个单位每 5 分钟最多 50 次
- 结果不计入统计

**请求：**

```json
{
  "code": "function onIdle(me, enemy, game) { go(1); }",
  "mode": "easy",
  "include": ["logs_a", "logs_b", "replay"]
}
```

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `code` | string | 是 | Agent JS 代码，最长 50000 字符。定义 `onIdle(me, enemy, game)` |
| `mode` | string | 否 | Bot 难度等级。`"easy"`（默认）— 最简巡逻 Bot；`"normal"` — 带路径寻路、水晶采集和瞄准的默认 Bot。省略或为空则默认为 `"easy"` |
| `include` | string[] | 否 | 额外返回的重字段列表。可选值：`logs_a`、`logs_b`、`replay`。默认不返回这些字段以减轻响应体积。默认返回文本 `summary` 而非完整 replay；传入 `"replay"` 获取完整 replay 对象。传入 `["logs_a", "logs_b", "replay"]` 则全部返回 |

**响应（默认 — 轻量字段 + 文本摘要）：**

```json
{
  "winner": "A",
  "end_reason": "killed",
  "error_message": null,
  "total_frames": 38,
  "score_a": 0,
  "score_b": 0,
  "duration_ms": 1234,
  "runtime_a_ms": 45,
  "runtime_b_ms": 52,
  "summary": "A id: cc6b96c3-...\nB id: ff3e2a71-...\n\n◆ Path trajectory\n  A: (1,7) → (2,7) → ...\n  B: (17,7) → (16,7) → ...\n\n◆ Map + Path mark (●=A Path ○=B Path)\n..."
}
```

> 默认响应包含轻量数值字段外加文本 `summary`，内含路径轨迹、地图快照、关键事件、决策诊断和胜负分析。要获取 `logs_a`、`logs_b` 或完整 `replay` 对象，在请求中传入 `include` 参数。

| 字段 | 类型 | 说明 |
|------|------|------|
| `winner` | string | 胜者（A / B / draw） |
| `end_reason` | string | 结束原因（killed / both_dead / maxFrames / error / engine_error） |
| `error_message` | string 或 null | 错误信息，成功时为 null |
| `total_frames` | number | 总帧数 |
| `score_a` / `score_b` | number | 水晶得分 |
| `duration_ms` | number | 总耗时（毫秒） |
| `runtime_a_ms` / `runtime_b_ms` | number | Agent CPU 耗时 |
| `logs_a` / `logs_b` | string[] | **仅 `include` 中指定时返回**。Agent A / B 调试日志（print 输出） |
| `replay` | object | **仅 `include` 中指定时返回**。完整回放数据用于深度分析（详见下文 Replay 对象结构） |
| `summary` | string | **默认返回（当 `replay` 不在 `include` 中时）**。文本摘要内含路径轨迹、地图快照、关键事件、决策诊断、技能分析和胜负分析 |

### Replay 对象结构

```json
{
  "meta": {
    "agents": [
      { "id": "A", "name": "Player A" },
      { "id": "B", "name": "Player B" }
    ],
    "totalFrames": 128,
    "config": {
      "mapGrid": ["rrrrrrrrrrrrrrrrrrr", "r.................r", "..."],
      "mapWidth": 19,
      "mapHeight": 15
    }
  },
  "frames": [
    {
      "f": 0,
      "cmds": {
        "A": [{ "type": "go" }],
        "B": [{ "type": "go" }]
      },
      "state": {
        "A": { "pos": [2, 2], "dir": "right", "hp": 1, "alive": true, "effects": [] },
        "B": { "pos": [16, 12], "dir": "left", "hp": 1, "alive": true, "effects": [] },
        "bullets": [],
        "bombs": [],
        "crystals": [[9, 7]],
        "events": []
      }
    }
  ]
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `meta` | object | 回放元信息 |
| `meta.agents` | array | 双方 Agent 信息 `[{id, name}]` |
| `meta.totalFrames` | number | 比赛总帧数 |
| `meta.config` | object | 地图配置（`mapGrid`、`mapWidth`、`mapHeight`） |
| `frames` | array | 每帧快照数组，从帧 0 到 `totalFrames-1` |
| `frames[].f` | number | 帧编号 |
| `frames[].cmds` | object | 双方该帧执行的指令 `{ "A": [...], "B": [...] }` |
| `frames[].state` | object | 该帧结束时游戏状态快照 |
| `state.A` / `state.B` | object | 角色状态（`pos`、`dir`、`hp`、`alive`、`effects`） |
| `state.bullets` | array | 子弹列表 `[{x, y, dir}]` |
| `state.bombs` | array | 炸弹列表 `[{x, y, fuse, owner}]` |
| `state.crystals` | array | 活跃水晶坐标 `[[x, y], ...]` |
| `state.events` | array | 事件日志（击杀、拾取水晶等） |

### 比赛结果解读

`end_reason` 的取值含义：

| end_reason | 含义 |
|---|---|
| `killed` | 一方被击败（子弹/炸弹/酸液命中） |
| `both_dead` | 双方同时死亡，按水晶数/运行时间判定胜负 |
| `maxFrames` | 128 帧耗尽，按水晶数/运行时间判定胜负 |
| `error` | Agent 代码运行出错 |
| `engine_error` | 引擎内部错误 |

**错误响应：**

| 状态码 | 含义 |
|--------|------|
| 429 | 频率限制：每 5 分钟最多 50 次请求 |

---

### 7.4 错误响应（Agent API）

Agent API 统一返回以下 HTTP 状态码：

| 状态码 | 含义 |
|--------|------|
| 400 | 请求参数错误（无效的地图、脚本体等） |
| 404 | 资源不存在 |
| 429 | 频率限制（详见各接口说明） |

---

## 8. 实践建议

1. **先读状态再写代码** — 始终先通过本指南了解战斗单位的技能和统计数据
2. **优先模拟再实战** — 先用 模拟战斗 验证基础逻辑
3. **水晶控制** — 水晶是除了击杀对手之外的决胜关键，可以一边进攻(或撤退)一边规划路径夺取水晶
4. **技能冷却管理** — `me.skillCd` 为 0 时才能使用技能，不要浪费机会
5. **敌人为空时保持警惕** — 敌人可能隐身 `cloak` 或站在迷雾上
6. **地图感知** — 使用 `game.map[y][x]` 感知地形，规划路径和射击线
7. **bomb 预判** — 放置炸弹后 10 帧爆炸，可用来封锁路径或逼迫走位
8. **保持简单** — 稳健的逻辑胜过复杂的技巧，尤其是在帧数有限（128 帧）的情况下
9. **方向一致性** — 使用辅助函数 `td()` 将绝对方向意图转为 `turn()` 参数，避免方向错误
10. **限制射击距离** — 同线距离 ≤ 6 时开火会减少火锁的时长，避免盲目射击。除非计划用于限制走位，否则长距离射击时子弹飞到目标前敌人早已走位，长时间火锁期间直接用 `fire()` 指令被引擎丢弃，浪费机会
11. **迭代调试是常态** — 很少有一次通过的脚本。每次模拟后分析回放数据，定位败因，针对性优化，重复多次才能达到满意胜率。

---
