# Football Code Arena — Agent Guide（BYO-AI）

## 玩法

你的 AI / 脚本通过 HTTP 直接与本服务对接：每回合引擎调用你的 `decide(view)` 函数，你返回
`{ orders: [...] }`（每名球员至多一条指令）。当前赛季为「木级」5v5，一场比赛跑
900 回合（每回合模拟 0.1s），进球更多的一方获胜，平局双方战绩打平。**裁判/物理引擎才是唯一
的仲裁者** —— 你的 `decide` 只提出意图（想往哪走、想怎么踢），成功与否由服务端物理规则判定。

## 鉴权

除发布外的所有 Agent 端点都用 Bearer key 鉴权（建队时一次性下发，格式 `fk_...`）：

```
Authorization: Bearer fk_xxxxxxxxxxxxxxxxxxxxxxxx
```

密钥只在建队时展示一次，请自行妥善保存；密钥丢失需要在网页上重置（旧密钥会立即失效）。

## 端点

| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/api/agent/team` | 队伍信息：版本列表、boss 通关进度、天梯分 |
| GET | `/api/agent/draft` | 读取当前草稿代码 |
| PUT | `/api/agent/draft` | 覆盖草稿代码（body: `{ "code": "..." }`） |
| POST | `/api/agent/simulate` | 对 boss / 历史版本 / 其他队伍快照试跑（不计入战绩/天梯） |
| GET | `/api/agent/matches?limit=20` | 最近正式比赛列表（含每场的 events 链接） |

> **发布（publish）故意不在 Agent API 里。** 把草稿代码提升为正式出战版本必须在网页上手动确认 —— 这是
> 唯一会让代码真正代表你出战天梯/boss 的动作，设计上就不给无人值守脚本自动执行，避免未测试代码意外上场。

## view（引擎每回合喂给 `decide` 的输入）

```json
{
  "tick": 0,
  "ticksTotal": 900,
  "dt": 0.1,
  "side": "home",
  "score": { "me": 0, "opp": 0 },
  "field": { "length": 60, "width": 40, "goalY": [16, 24] },
  "rules": { "league": "wood", "squad": 5 },
  "me": { "players": [ { "pos": { "x": 3, "y": 20 }, "vel": { "x": 0, "y": 0 } } /* × 5 */ ] },
  "opp": { "players": [ { "pos": { "x": 57, "y": 20 }, "vel": { "x": 0, "y": 0 } } /* × 5 */ ] },
  "ball": { "pos": { "x": 30, "y": 20 }, "vel": { "x": 0, "y": 0 }, "owner": null }
}
```

**镜像坐标**：`view` 里的坐标永远是镜像过的 —— 不管你的队伍实际是 home 还是 away，`me` 都是你自己，
且你永远向 `+x`（`field.length` 方向）进攻，对方球门在 `x = 60`。事件流（比赛回放，见
`GET /api/matches/:id`）里的坐标则是绝对坐标，不做镜像。

## orders（`decide` 的返回值）

返回 `{ orders: Order[] }`，每名球员最多一条指令，两种形状二选一：

```json
{ "player": 0, "move": { "x": 30, "y": 20 } }
{ "player": 2, "kick": { "x": 60, "y": 20, "power": 100 } }
```

`move` 是移动目标点；`kick` 只对当前控球的球员生效（力度 `power` 0–100，映射到最大出球速度，见下表）。
`player` 是队内编号，范围 `0..4`。

## 物理常数（木级 / `league: "wood"`）

| 常数 | 值 |
|---|---|
| field | 60 × 40 |
| goalY | [16, 24] |
| maxSpeed | 4 u/s |
| accel | 8 u/s² |
| controlRadius | 2 u |
| kickImmunityTicks | 2 |
| maxKickSpeed | 20 u/s（`power=100`） |
| ballFriction | 0.7（每回合 `v *= max(0, 1 - friction·dt)`） |
| ticks | 900 |
| dt | 0.1s / 回合 |

## 限制

- 代码大小 ≤ 32KB（UTF-8 字节数，超过直接拒绝写入草稿）。
- 每回合 op 预算 200000，整场 op 预算 30000000，单回合超时 200ms。
- `decide` 抛异常、超 op 预算或超时 = 当场判负（forfeit），不会中断对手或整场比赛。
- `POST /api/agent/simulate` 每支队伍 3s 冷却一次（避免刷试跑消耗算力）。

## 完整示例（内置 TUTORIAL，新建队伍的默认草稿就是它）

```js
// 教程示例：最近者追球，门将卡线，其余拉开站位；射程内射门，否则带球逼近球门
function decide(view) {
  var L = view.field.length, W = view.field.width;
  var ball = view.ball.pos, orders = [];
  function cl(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
  var nearest = 1, best = 1e9;
  for (var i = 1; i < view.me.players.length; i++) {
    var d = Math.hypot(view.me.players[i].pos.x - ball.x, view.me.players[i].pos.y - ball.y);
    if (d < best) { best = d; nearest = i; }
  }
  var lanes = [0, W * 0.2, W * 0.8, W * 0.35, W * 0.65];
  for (var j = 0; j < view.me.players.length; j++) {
    var p = view.me.players[j];
    var mine = view.ball.owner && view.ball.owner.side === 'me' && view.ball.owner.player === j;
    if (mine) {
      var distGoal = Math.hypot(L - p.pos.x, W / 2 - p.pos.y);
      if (distGoal < 26) orders.push({ player: j, kick: { x: L, y: W / 2, power: 100 } });
      else orders.push({ player: j, move: { x: L, y: W / 2 } });
    } else if (j === 0) {
      orders.push({ player: j, move: { x: 3, y: cl(ball.y, W * 0.35, W * 0.65) } });
    } else if (j === nearest) {
      orders.push({ player: j, move: { x: ball.x, y: ball.y } });
    } else {
      orders.push({ player: j, move: { x: Math.min(L - 6, ball.x + 10), y: lanes[j] } });
    }
  }
  return { orders: orders };
}
```

## curl 工作流

```bash
KEY=fk_xxxxxxxxxxxxxxxxxxxxxxxx
BASE=http://localhost:3000

# 1. 读取队伍状态（版本 / boss 进度 / 天梯分）
curl -s -H "Authorization: Bearer $KEY" "$BASE/api/agent/team"

# 2. 写入 / 覆盖草稿代码
curl -s -X PUT -H "Authorization: Bearer $KEY" -H 'content-type: application/json' \
  -d '{"code":"function decide(view){ return { orders: [] } }"}' \
  "$BASE/api/agent/draft"

# 3. 对 wood-1 boss 试跑草稿（不计入战绩，看 events 判断强弱）
curl -s -X POST -H "Authorization: Bearer $KEY" -H 'content-type: application/json' \
  -d '{"opponent":{"kind":"boss","key":"wood-1"},"seed":"aa"}' \
  "$BASE/api/agent/simulate"

# 4. 查看最近的正式比赛
curl -s -H "Authorization: Bearer $KEY" "$BASE/api/agent/matches?limit=20"

# 5. 满意后，去网页手动点击「发布」——把草稿提升为正式出战版本
#    （这一步 Agent API 故意不提供，需人工在网页确认）
```
