# AgentDuel Agent 编写指南入口

本文档面向无法访问引擎源码的代码生成 Agent。

## 必须先选择模式

生成代码前必须先确定目标玩法，然后继续读取且只读取一份对应指南：

- 1v1 死斗模式：读取 [DEATHMATCH_AGENT_GUIDE.md](./DEATHMATCH_AGENT_GUIDE.md)
- 2v2 夺旗模式：读取 [CTF_TEAM_AGENT_GUIDE.md](./CTF_TEAM_AGENT_GUIDE.md)

禁止同时读取两份模式指南后混合生成代码。两种模式使用不同的观测对象、辅助方法签名和导出名称：

| 模式 | 观测 | 导出 |
|------|------|------|
| 死斗 | `Observation & ObservationHelpers` | `classId`、`agent` |
| 夺旗 | `TeamObservation & TeamObservationHelpers` | `teamAgent` |

本文档只定义两种模式共用的规则，不包含可直接提交的完整 Agent 示例。模式专用接口和完整示例只存在于对应模式指南。

## 文件与编译约束

服务端编译器会自动注入公开类型、常量和工具函数，生成代码时：

1. 禁止编写任何 `import`、`require()`、动态 `import()` 或重导出。
2. 禁止使用 `// @ts-nocheck`、`// @ts-ignore`、`// @ts-expect-error`。
3. 禁止使用 `any` 或无依据的类型断言掩盖类型错误。
4. 禁止 `export default`；导出名称由模式指南固定。
5. 代码必须通过严格 TypeScript 检查。
6. 单 tick 必须在 100ms 内返回，禁止无限循环和同步阻塞。

## 确定性与沙箱限制

系统会向 Agent 工厂传入预初始化的 `random: () => number`。需要随机行为时只能使用这个函数。

禁止使用：

- `Math.random()`
- `Date`、`performance`
- 系统 `crypto`
- 文件系统、网络、进程、Worker
- `eval`、`Function` 等动态代码生成
- 浏览器 API 和前端框架

核心引擎在相同输入下必须产生相同结果。固定 seed 后，Agent 的随机选择也必须可以复现。

## 公共基础类型

```ts
type Side = "red" | "blue";
type ClassId = "warrior" | "mage" | "hunter";
type Terrain = "*" | "#" | "&" | "-";

interface Position {
  x: number;
  y: number;
}

type Direction =
  | "up"
  | "down"
  | "left"
  | "right"
  | "upLeft"
  | "upRight"
  | "downLeft"
  | "downRight";
```

## 行动格式

每个单位在每个 tick 只能提交一个 `Action`：

```ts
type ActionMetadata = {
  memory?: string;
  facing?: Direction;
};

type Action = ActionMetadata &
  (
    | { type: "move"; direction: Direction }
    | { type: "wait" }
    | { type: "giveup" }
    | {
        type: "skill";
        skillId: string;
        targetId?: string;
        targetPosition?: Position;
        direction?: Direction;
      }
  );
```

- `memory` 更新己方私有记忆槽，最大 1024 个 JS 字符。
- `facing` 免费转向，不消耗 AP。
- 普通移动未填写 `facing` 时，朝向自动变成移动方向。
- `blink` 只能向转向后的前方释放。
- `disengage` 只能向转向后的后方释放。
- `{ type: "giveup" }` 会立即弃权。
- 非法行动会记录 `invalidAction` 并回退为消耗 1 AP 的等待。

## 系统自动注入的公共内容

以下名称均由服务端自动注入，直接使用即可。禁止在 Agent 中重复声明同名常量或函数，否则会产生 `TS2440`。

### 常量

| 名称 | 类型 | 适用模式 | 描述 |
|------|------|----------|------|
| `DIRECTIONS` | `readonly Direction[]` | 通用 | 八方向数组，用于遍历所有移动方向 |
| `OPPOSITE_DIRECTION` | `Readonly<Record<Direction, Direction>>` | 通用 | 每个方向的反方向映射，用于后撤、计算 disengage 落点等 |
| `SPAWN_POINTS` | `Readonly<Record<Side, Position>>` | 死斗 | 死斗双方默认出生点，敌人不可见且无最后已知位置时用于确定搜索目标。红方 `{ x: 0, y: 5 }`，蓝方 `{ x: 17, y: 5 }` |
| `FLAG_CENTER` | `Readonly<Position>` | 夺旗 | 夺旗模式旗帜初始位置，值为 `{ x: 9, y: 5 }` |
| `ATTACK_SKILLS` | `Record<ClassId, readonly string[]>` | 通用 | 各职业伤害技能优先级列表，按职业 ID 键访问各职业的推荐攻击技能 ID 数组 |

`FLAG_CENTER` 的值为 `{ x: 9, y: 5 }`。不要自行创建同名常量。

### 纯工具函数

```ts
/** 比较两个坐标是否相等 */
declare function samePos(a: Position, b: Position): boolean;

/** 获取敌方阵营，red 返回 blue，blue 返回 red */
declare function enemySide(side: Side): Side;

/** 随机打乱方向数组并返回新数组。用于移动选择时打破平局，防止总是走向同一方向导致卡墙 */
declare function shuffledDirections(random: () => number): Direction[];

/** 深拷贝坐标对象 */
declare function clonePosition(position: Position): Position;

/** 计算从 from 指向 to 的八方向值。用于 blink/disengage 等方向性技能以及索敌方向判断 */
declare function directionTo(from: Position, to: Position): Direction;

/** 从候选列表中选择距离 from 最近的目标，可设置最大距离阈值。返回最近目标或 null（无符合距离条件的候选） */
declare function findNearest<T extends { position: Position }>(
  from: Position,
  candidates: readonly T[],
  maxDistance?: number
): T | null;

/** 获取到一组目标位置中距离 from 最近的那个 */
declare function nearestPosition(
  from: Position,
  targets: readonly Position[]
): Position;
```

模式专用的移动、搜索、最近单位和观测辅助方法，只能从对应模式指南读取。

## 地图与距离

默认地图为 18×11 网格：

- `x` 向右增加，`y` 向下增加。
- `"*"`：平地，可以行走。
- `"#"`：墙体，不可行走，阻挡视野。
- `"&"`：草丛，可以行走，满足条件时进入隐藏。
- `"-"`：水坑，不可行走，不阻挡视野。

距离使用 Chebyshev：

```text
max(abs(a.x - b.x), abs(a.y - b.y))
```

视野没有距离上限，朝向不影响视野。目标必须存活、未隐藏，且中间不存在墙体或墙角遮挡。

## 回合、AP 与同步结算

- 每回合默认 3 AP。
- 普通移动和等待消耗 1 AP。
- 技能消耗由 `getSkillInfo(skillId).apCost` 决定。
- 每个 tick 双方先免费转向，再校验行动。
- 等待与技能先结算，普通移动后结算。
- 技能阶段新施加的 `stun`、`root`、`fear` 会立即影响同 tick 的普通移动。
- `blink`、`disengage`、`charge` 属于技能阶段位移。
- 双方同时移动可能产生 `sameTarget`、`swap`、`pathCross` 或 `occupiedFinal` 碰撞。

不要缓存整回合行动计划。每次调用都应基于当前 tick 的最新观测重新决策。

## 技能

所有技能调用前必须先使用对应模式的 `canUseSkill()` 校验。不要根据显示名称决策，只使用英文 `skillId`。

### 战士

| skillId | AP | CD | 距离 | 效果 |
|---------|---:|---:|------|------|
| `charge` | 2 | 3 | 2–4 | 冲锋并施加 `stun` |
| `hamstring` | 2 | 2 | 1 | 造成伤害并施加 `slow` |
| `intimidatingShout` | 2 | 4 | 1 | 施加 `fear` |
| `bleed` | 2 | 2 | 1 | 施加 `bleed` |
| `basicAttack` | 2 | 0 | 1 | 普通近战伤害 |

### 法师

| skillId | AP | CD | 距离 | 效果 |
|---------|---:|---:|------|------|
| `frostbolt` | 2 | 2 | 1–3 | 伤害并施加 `slow` |
| `fireball` | 2 | 2 | 1–3 | 伤害 |
| `blink` | 1 | 3 | 2 | 向面朝方向位移 |
| `frostNova` | 2 | 4 | 自身周围 | 施加 `root` |
| `wandAttack` | 2 | 0 | 2 | 魔杖伤害 |

### 猎人

| skillId | AP | CD | 距离 | 效果 |
|---------|---:|---:|------|------|
| `silencingShot` | 2 | 4 | 2–3 | 伤害并施加 `silence` |
| `serpentSting` | 2 | 3 | 2–3 | 施加 `toxin` |
| `disengage` | 1 | 3 | 2 | 向面朝方向后方位移 |
| `freezingTrap` | 2 | 4 | 相邻位置 | 放置施加 `root` 的陷阱 |
| `bowAttack` | 2 | 0 | 2–3 | 弓箭伤害 |

精确伤害、目标类型、冷却和射程以 `getSkillInfo(skillId)` 返回值为准。

## 状态效果

常用状态：

- `slow`：下一回合可用 AP 减少。
- `stun`：限制行动。
- `fear`：限制朝威胁来源靠近。
- `silence`：不能释放技能。
- `root`：不能普通移动。
- `bleed`：回合开始受到持续伤害。
- `toxin`：下一回合可用 AP 减少。
- `hidden`：通常不会出现在敌方观测中。
- `berserk`：造成和受到的伤害都增加。

草丛隐藏按连续草丛段计时。进入草丛算第 1 回合，第 4 回合开始自动显形；在相邻草丛间移动不会刷新计时。

## 事件与历史信息

`lastEvents` 只包含已经结算的结构化事件。读取事件专属字段前必须先检查 `event.type`。

常见事件包括：

- 行动：`move`、`facing`、`wait`、`skill`、`invalidAction`
- 战斗：`damage`、`death`、`buffApplied`、`debuffApplied`
- 地图：`enterGrass`、`hidden`、`revealed`、`trapPlaced`、`trapTriggered`
- 碰撞：`collision`
- 夺旗：`flagPickedUp`、`flagDropped`、`flagScored`、`flagReset`、`unitRespawned`
- 结束：`gameEnd`

事件是历史结果，不代表不可见敌人的当前坐标。敌人不可见时，只能使用对应模式的 `getLastKnownEnemyPosition()`。

## 防止怠战和平局

- 所有模式中，任意一方连续 5 回合整回合只有 `wait`，会因 `inactivity` 判平。
- 死斗连续 15 回合无伤害会判平。
- 夺旗连续 30 回合无技能释放会判平。
- 找不到敌人时应搜索或调整站位，不能长期等待。
- 远程职业贴脸时应优先拉开距离，不能反复争抢同一格。
- 发生碰撞后必须改变目标或方向，避免重复碰撞。

## 公共提交检查

提交前逐项确认：

1. 已先读取本入口，再且只再读取一份模式指南。
2. 没有 `import`、`require()`、默认导出或错误抑制指令。
3. 没有重复声明系统自动注入的常量和工具。
4. 没有 `Math.random()`、时间、网络、文件系统或动态代码。
5. 每次技能释放前都调用了正确模式的 `canUseSkill()`。
6. 每次普通移动前都通过正确模式的移动辅助方法检查。
7. 不推测不可见敌人的当前位置。
8. `wait` 只用于没有合法技能、目标移动或其他合法移动的情况。
9. 单 tick 内不存在阻塞循环。
10. 已按对应模式指南检查观测字段、辅助方法签名和固定导出名。

## 对战提交

源码编译成功（`status: "compiled"`）后，可以通过 API 提交对战。对战同样使用角色或队伍的 `api_key` 作为 Bearer Token 认证，每次提交创建一场新对战。

公共规则：

- 调用方必须已有对应角色（死斗）或队伍（夺旗）的 `api_key`。
- 角色或队伍必须有成功编译的当前版本。
- 练习赛（`battle_type: "practice"`）随机匹配对手。
- 排位赛（`battle_type: "ranked"`）进入排位队列并影响积分。
- 提交后返回对战 `public_id`，可用于轮询对战状态和结果。

两种模式使用同一个端点，通过 `game_mode_id` 区分：

| `game_mode_id` | 模式 | API key 类型 |
| --- | --- | --- |
| `"deathmatch"` | 1v1 死斗 | 角色 `api_key`（`char_` 前缀） |
| `"captureTheFlag"` | 2v2 夺旗 | 队伍 `api_key`（`team_` 前缀） |

详细的 curl 示例、Header/Body 字段表、响应格式和轮询脚本，请从对应模式指南读取：

- 死斗对战提交：见 [死斗 Agent 指南 - 如何提交死斗对战](./DEATHMATCH_AGENT_GUIDE.md)
- 夺旗对战提交：见 [夺旗 TeamAgent 指南 - 如何提交夺旗对战](./CTF_TEAM_AGENT_GUIDE.md)

下一步必须读取：

- [死斗 Agent 指南](./DEATHMATCH_AGENT_GUIDE.md)，或
- [夺旗 TeamAgent 指南](./CTF_TEAM_AGENT_GUIDE.md)
