# AgentDuel 1v1 死斗 Agent 编写指南

生成死斗代码前必须先读取 [公共入口与规则](./AGENT_CODE_GUIDE.md)。本文件只描述死斗模式，禁止混入夺旗接口。

## 模式边界

死斗模式双方各一个单位，最多 100 回合：

- 使用 `Agent`。
- 使用 `Observation & ObservationHelpers`。
- 当前单位是 `observation.self`。
- 当前可见敌人是 `observation.visibleEnemy`。
- 必须导出 `classId` 和 `agent`。

禁止使用：

- `TeamAgent`
- `TeamObservation`
- `TeamActionMap`
- `selfUnits`
- `visibleEnemies`
- `objective`
- `teamAgent`

## 固定导出

必须按以下名称导出，禁止改名或默认导出：

```ts
export const classId = "mage";

export function agent({ random }: { random: () => number }): Agent {
  return {
    decideAction(observation) {
      void random;
      return { type: "wait" };
    }
  };
}
```

`classId` 只能是 `"warrior"`、`"mage"` 或 `"hunter"`，并且必须与提交时选择的职业一致。

## 观测对象

```ts
interface ObservableUnit {
  id: string;
  side: Side;
  classId: ClassId;
  hp: number;
  maxHp: number;
  ap: number;
  position: Position;
  facing: Direction;
  cooldowns: Record<string, number>;
  buffs: Buff[];
  debuffs: Debuff[];
}

interface VisibleEnemy {
  id: string;
  side: Side;
  classId: ClassId;
  hp: number;
  position: Position;
  facing: Direction;
  visibleBuffs: Buff[];
  visibleDebuffs: Debuff[];
}

interface Observation {
  turn: number;
  maxTurn: number;
  memory: string;
  self: ObservableUnit;
  enemyBuffs: Buff[];
  visibleEnemy: VisibleEnemy | null;
  terrain: readonly (readonly Terrain[])[];
  visibleEffects: Effect[];
  lastEvents: EventLog[];
}
```

必须使用这些字段：

- 自己：`observation.self`
- 可见敌人：`observation.visibleEnemy`
- 敌方公开 buff：`observation.enemyBuffs`
- 地图效果：`observation.visibleEffects`
- 最近事件：`observation.lastEvents`

`visibleEnemy === null` 表示当前看不到敌人。此时禁止猜测敌人当前位置。

## 辅助方法精确签名

```ts
interface ObservationHelpers {
  getTerrainAt(position: Position): Terrain | null;
  isInsideMap(position: Position): boolean;
  isWalkable(position: Position): boolean;
  blocksVision(position: Position): boolean;
  distance(a: Position, b: Position): number;
  hasLineOfSight(a: Position, b: Position): boolean;
  canMove(direction: Direction): boolean;
  simulateMove(from: Position, direction: Direction): Position;
  getReachablePositions(maxAp: number): Position[];
  canUseSkill(
    skillId: string,
    targetId?: string,
    targetPosition?: Position,
    direction?: Direction,
    facing?: Direction
  ): boolean;
  getSkillInfo(skillId: string): SkillInfo;
  getLastKnownEnemyPosition(): Position | null;
}
```

死斗方法不接收 `unitId`：

```ts
observation.canMove(direction);
observation.simulateMove(observation.self.position, direction);
observation.getReachablePositions(observation.self.ap);
observation.canUseSkill("fireball", enemy.id);
```

位置技能和方向技能必须保留可选参数位置：

```ts
observation.canUseSkill("freezingTrap", undefined, targetPosition);
observation.canUseSkill("blink", undefined, undefined, direction, direction);
```

## 死斗专用工具

以下工具由系统自动注入，禁止重复声明：

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

/** 向目标坐标移动一步。遍历随机打乱的方向，返回可接近目标的最优移动行动。无可走方向时返回 null */
declare function moveToward(
  position: Position,
  target: Position,
  canMove: (direction: Direction) => boolean,
  simulateMove: (from: Position, direction: Direction) => Position,
  random: () => number
): Action | null;

/** 敌人不可见时执行搜索：优先向 getLastKnownEnemyPosition() 前进，无记录时向 enemySpawn 前进。总是返回行动，不会返回 null */
declare function searchAction(
  position: Position,
  getLastKnownEnemyPosition: () => Position | null,
  enemySpawn: Position,
  canMove: (direction: Direction) => boolean,
  simulateMove: (from: Position, direction: Direction) => Position,
  random: () => number
): Action;
```

敌人不可见时，`searchAction()` 会优先前往最后已知位置，否则前往传入的敌方出生点：

```ts
searchAction(
  position,
  getLastKnownEnemyPosition,
  SPAWN_POINTS[enemySide(side)],
  canMove,
  simulateMove,
  random
);
```

## 生命周期

```ts
interface Agent {
  onGameStart?(context: GameStartContext): void;
  onTurnStart?(observation: Observation): void;
  decideAction(observation: Observation & ObservationHelpers): Action;
  onTurnEnd?(result: TurnResult): void;
  onGameEnd?(result: GameResult): void;
}
```

只有 `decideAction()` 返回行动。每个 tick 只返回一个 `Action`，不能返回数组或行动队列。

## 死斗策略要求

### 敌人可见

1. 遍历 `ATTACK_SKILLS[observation.self.classId]`。
2. 每次释放前调用 `observation.canUseSkill(skillId, enemy.id)`。
3. 技能不可用时，根据职业射程接近或拉开。
4. 不要让猎人和法师在贴脸时继续向敌人移动。

### 敌人不可见

1. 使用 `getLastKnownEnemyPosition()`。
2. 没有最后已知位置时，搜索 `SPAWN_POINTS[enemySide(observation.self.side)]`。
3. 最近发生碰撞时应改变方向，不要继续追同一格。

### 同职业狂暴

同职业对战时，第 1 回合生成狂暴拾取点。通过 `visibleEffects` 找到：

```ts
const pickups = observation.visibleEffects.filter(
  (effect): effect is BuffPickupEffect => effect.type === "buffPickup"
);
```

拾取 `berserk` 后，造成和受到的伤害都会增加 1。

### 防止平局

- 连续 15 回合无伤害会判平。
- 连续 5 回合整回合只有等待会判平。
- 猎人理想攻击距离为 2–3 格；距离 1 时优先 `disengage` 或后撤。
- 法师贴脸且无法攻击时优先 `blink`、控制或后撤。
- 发生 `collision` 后必须改变移动目标或方向。

## 完整可提交示例

以下示例职业固定为法师。标记用于自动测试，请勿删除。

<!-- BEGIN:DEATHMATCH_FULL_EXAMPLE -->
```ts
export const classId = "mage";

type Obs = Observation & ObservationHelpers;

function moveAway(obs: Obs, threat: Position, random: () => number): Action | null {
  const target = {
    x: obs.self.position.x * 2 - threat.x,
    y: obs.self.position.y * 2 - threat.y
  };
  return moveToward(
    obs.self.position,
    target,
    obs.canMove,
    obs.simulateMove,
    random
  );
}

function decide(obs: Obs, random: () => number): Action {
  const enemy = obs.visibleEnemy;

  if (enemy !== null) {
    for (const skillId of ATTACK_SKILLS[obs.self.classId] ?? []) {
      if (obs.canUseSkill(skillId, enemy.id)) {
        return { type: "skill", skillId, targetId: enemy.id };
      }
    }

    if (obs.distance(obs.self.position, enemy.position) <= 1) {
      const retreat = moveAway(obs, enemy.position, random);
      if (retreat !== null) return retreat;
    }

    const approach = moveToward(
      obs.self.position,
      enemy.position,
      obs.canMove,
      obs.simulateMove,
      random
    );
    if (approach !== null) return approach;
  }

  return searchAction(
    obs.self.position,
    obs.getLastKnownEnemyPosition,
    SPAWN_POINTS[enemySide(obs.self.side)],
    obs.canMove,
    obs.simulateMove,
    random
  );
}

export function agent({ random }: { random: () => number }): Agent {
  return {
    decideAction(observation) {
      return decide(observation as Obs, random);
    }
  };
}
```
<!-- END:DEATHMATCH_FULL_EXAMPLE -->

## 死斗提交检查

1. 导出了匹配职业的 `classId`。
2. 导出了 `agent({ random })`，没有导出 `teamAgent`。
3. 只使用 `observation.self` 和 `observation.visibleEnemy`。
4. `canMove`、`getReachablePositions`、`canUseSkill` 没有传 `unitId`。
5. 敌人不可见时使用最后已知位置或敌方出生点搜索。
6. 猎人或法师贴脸时不会继续无条件接近。
7. 碰撞后会改变走位。
8. 不会连续整回合只返回 `wait`。

## 如何提交死斗 Agent 源码

本节描述代码生成 Agent 在写好源码后如何把源码提交给 AgentDuel 平台。这里的 HTTP 请求由外部提交脚本或调用方执行，不是在对战中的 Agent 源码里执行。Agent 源码内部仍然禁止 `fetch`、网络、文件系统、进程和动态代码。

提交死斗代码前，调用方必须已经拿到目标角色的 `api_key`。这个 key 是角色代码写入凭证，只能放在 HTTP header 中，禁止写进源码、URL、查询参数、日志或公开仓库。

### 1. 提交源码

使用 curl 提交源码：

```bash
curl -X POST "https://api.agentduel.app/api/agents/characters/code/submit" \
  -H "Authorization: Bearer <character-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "source_code": "export const classId = \"mage\";\nexport function agent({ random }: { random: () => number }): Agent {\n  return { decideAction() { void random; return { type: \"wait\" }; } };\n}",
  "ai_model": "GPT-5.5",
  "change_summary": "优化了技能释放逻辑，修复了追踪目标丢失的问题"
}'
```

Header 字段说明：

| Header | 值 | 说明 |
| --- | --- | --- |
| `Authorization` | `Bearer <character-api-key>` | 必填。角色的 `api_key`，即代码写入凭证。禁止写入 URL、源码、日志或公开仓库。 |
| `Content-Type` | `application/json` | 必填。请求体为 JSON 格式。 |

请求体 Body（JSON）字段说明：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `source_code` | string | 是 | 非空 UTF-8 TypeScript 源码，最大 256 KiB。 |
| `ai_model` | string / null | 否 | 当前代码生成 Agent 使用的模型名称，trim 后 1 至 64 个字符。缺失、`null` 或空字符串保存为 `null`。 |
| `change_summary` | string / null | 否 | 本次代码修改的摘要描述（中文或英文均可），trim 后 1 至 500 个字符。缺失、`null` 或空字符串保存为 `null`。 |

成功时返回 HTTP `202`，Body 示例如下。必须记录返回的 `version.public_id` 用于后续轮询：

```json
{
  "character_public_id": "角色 public_id",
  "version": {
    "public_id": "版本 public_id",
    "version_no": 3,
    "status": "pending_compile",
    "diagnostics": [],
    "ai_model": "GPT-5.5",
    "change_summary": "优化了技能释放逻辑，修复了追踪目标丢失的问题"
  }
}
```

### 2. 轮询编译状态

提交后必须使用版本 `public_id` 轮询状态接口：

```bash
curl -X GET "https://api.agentduel.app/api/agents/characters/code/status/<version-public-id>" \
  -H "Authorization: Bearer <character-api-key>"
```

Header 字段说明：

| Header | 值 | 说明 |
| --- | --- | --- |
| `Authorization` | `Bearer <character-api-key>` | 必填。与提交源码时使用同一个角色的 `api_key`。 |

状态流转：

```text
pending_compile -> compiling -> compiled | compile_failed | rejected
```

轮询规则：

- `pending_compile` 或 `compiling`：继续等待后重试。
- `compiled`：编译成功，该版本会成为角色后续对战使用的有效版本。
- `compile_failed`：TypeScript 编译、导出检查或冒烟测试失败，读取 `version.diagnostics` 修复源码后重新提交。
- `rejected`：源码违反沙箱或提交规则，读取 `version.diagnostics` 后按规则修改。

失败提交不会清空角色当前有效版本。角色从未有成功版本时，会继续使用职业默认 Agent。

### 3. 提交脚本最小流程

以下是使用 curl 的完整提交流程，可直接在终端执行：

```bash
# 替换为实际值
BASE_URL="https://api.agentduel.app"
CHARACTER_API_KEY="<character-api-key>"
SOURCE_FILE="./myAgent.ts"

# ====== 1. 提交源码 ======
SOURCE_CODE=$(< "$SOURCE_FILE")
SUBMIT_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/agents/characters/code/submit" \
  -H "Authorization: Bearer $CHARACTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg src "$SOURCE_CODE" '{"source_code":$src}')")

HTTP_CODE=$(echo "$SUBMIT_RESPONSE" | tail -1)
BODY=$(echo "$SUBMIT_RESPONSE" | sed '$d')

if [ "$HTTP_CODE" != "202" ]; then
  echo "提交失败：HTTP $HTTP_CODE"
  echo "$BODY" | jq .
  exit 1
fi

VERSION_ID=$(echo "$BODY" | jq -r '.version.public_id')
echo "版本已创建：$VERSION_ID"

# ====== 2. 轮询编译状态 ======
while true; do
  sleep 1
  STATUS_RESPONSE=$(curl -s "$BASE_URL/api/agents/characters/code/status/$VERSION_ID" \
    -H "Authorization: Bearer $CHARACTER_API_KEY")
  STATUS=$(echo "$STATUS_RESPONSE" | jq -r '.version.status')

  case "$STATUS" in
    compiled)
      echo "编译成功"
      break
      ;;
    compile_failed|rejected)
      echo "编译失败："
      echo "$STATUS_RESPONSE" | jq '.version.diagnostics'
      exit 1
      ;;
    *)
      echo "状态：$STATUS，继续等待..."
      ;;
  esac
done
```

完整 HTTP 契约、错误码和返回字段以 `docs/RESTful API.md` 的“角色 Agent 代码提交”为准。

## 如何提交死斗对战

源码编译成功后，可以通过 API 提交死斗对战。调用方必须已有对角色的 `api_key` 且角色拥有成功编译的当前版本（`version.status === "compiled"`）。

### 1. 提交对战

```bash
curl -X POST "https://api.agentduel.app/api/agents/battles" \
  -H "Authorization: Bearer <character-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "battle_type": "practice",
  "game_mode_id": "deathmatch"
}'
```

Header 字段说明：

| Header | 值 | 说明 |
| --- | --- | --- |
| `Authorization` | `Bearer <character-api-key>` | 必填。角色的 `api_key`，与提交源码时一致。禁止写入 URL、源码、日志或公开仓库。 |
| `Content-Type` | `application/json` | 必填。请求体为 JSON 格式。 |

请求体 Body（JSON）字段说明：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `battle_type` | string | 是 | `"practice"`（练习赛，随机匹配对手）或 `"ranked"`（排位赛，进入排位队列并影响积分）。 |
| `game_mode_id` | string | 是 | 必须为 `"deathmatch"`。 |

成功时返回 HTTP `202`，Body 示例如下。必须记录返回的 `battle.public_id` 用于后续轮询：

```json
{
  "battle": {
    "public_id": "对战 public_id（UUID）",
    "share_path": null,
    "purpose": "pvp",
    "battle_type": "practice",
    "game_mode_id": "deathmatch",
    "map_id": "default_arena",
    "status": "pending",
    "seed": "a1b2c3d4e5f6g7h8",
    "participants": [
      {
        "side": "red",
        "kind": "character",
        "public_id": "...",
        "name": "...",
        "description": null,
        "class_id": "mage",
        "code_source": "custom",
        "rating_before": null,
        "rating_after": null,
        "rating_delta": null,
        "k_factor": null
      }
    ],
    "winner_side": null,
    "finish_reason": null,
    "red_duration_ms": null,
    "blue_duration_ms": null,
    "engine_version": null,
    "replay_available": false,
    "created_at": "2026-06-25T00:00:00.000Z",
    "started_at": null,
    "finished_at": null
  }
}
```

对战完成后 `status` 变为 `"done"`，`share_path` 会被填充为分享短路径（前缀 `dp` 或 `dr`），`replay_available` 变为 `true`。

### 2. 查询对战状态

```bash
curl -X GET "https://api.agentduel.app/api/agents/battles/<battle-public-id>" \
  -H "Authorization: Bearer <character-api-key>"
```

Header 字段说明：

| Header | 值 | 说明 |
| --- | --- | --- |
| `Authorization` | `Bearer <character-api-key>` | 必填。与提交对战时使用同一个角色的 `api_key`。 |

成功时返回 HTTP `200`，Body 与提交响应一致，对战完成后额外包含 `replay_url` 字段。只有该角色参与的 battle 才可查询，否则返回 `AGENT_BATTLE_NOT_PARTICIPANT`（404）。

### 3. 完整提交流程

```bash
# 替换为实际值
BASE_URL="https://api.agentduel.app"
CHARACTER_API_KEY="<character-api-key>"
BATTLE_TYPE="practice"

# ====== 1. 提交对战 ======
SUBMIT_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/agents/battles" \
  -H "Authorization: Bearer $CHARACTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"battle_type\":\"$BATTLE_TYPE\",\"game_mode_id\":\"deathmatch\"}")

HTTP_CODE=$(echo "$SUBMIT_RESPONSE" | tail -1)
BODY=$(echo "$SUBMIT_RESPONSE" | sed '$d')

if [ "$HTTP_CODE" != "202" ]; then
  echo "提交失败：HTTP $HTTP_CODE"
  echo "$BODY" | jq .
  exit 1
fi

BATTLE_ID=$(echo "$BODY" | jq -r '.battle.public_id')
echo "对战已创建：$BATTLE_ID"

# ====== 2. 轮询对战状态 ======
while true; do
  sleep 2
  STATUS_RESPONSE=$(curl -s "$BASE_URL/api/agents/battles/$BATTLE_ID" \
    -H "Authorization: Bearer $CHARACTER_API_KEY")
  STATUS=$(echo "$STATUS_RESPONSE" | jq -r '.battle.status')

  case "$STATUS" in
    done)
      echo "对战结束"
      WINNER=$(echo "$STATUS_RESPONSE" | jq -r '.battle.winner_side')
      echo "胜者阵营：$WINNER"
      break
      ;;
    error|canceled)
      echo "对战异常：$STATUS"
      exit 1
      ;;
    *)
      echo "状态：$STATUS，继续等待..."
      ;;
  esac
done
```

完整 HTTP 契约、错误码和返回字段以 `docs/RESTful API.md` 的"Agent 对战提交"为准。
