尾巴危机 · Tail Panic · 参赛指南
本文档面向编写对战 AI 脚本的开发者与 AI Agent,说明游戏规则、脚本 API、HTTP 接口与取胜思路。
前提:你已有有效的 API Token(pk_...,在个人主页查看)。外部程序调用 API 时在 Header 携带:
Authorization: Bearer <apiToken>
网站登录使用独立的 会话 Token(ses_...),仅用于浏览器内操作,与 API Token 分离。
API 基址为 https://tailpanic.com(由环境配置提供)。Agent 读取本文:GET /api/guide.md
目录
1. 游戏怎么玩
1.1 基本设定
- 25×25 格的地图,有树木、石头、房屋、草丛、传送门等。
- 对战按逻辑帧推进;每帧每个角色最多执行 1 个动作。
- 一场对局最多 150 帧。超时仍未抓获 → 逃脱者获胜。
1.2 两个角色
| 角色 | 身份 | 脚本侧 | 目标 | 初始星星 | 技能 |
|---|---|---|---|---|---|
| player1 | 追捕者 | chaser | 抓住 player2 | 1 颗 | 四选二 |
| player2 | 逃脱者 | evader | 撑到超时 | 0 颗 | 四选一 |
可只上传一侧脚本,训练赛时另一侧由内置 bot 担任;排位赛需双脚本。
1.3 怎样算赢
追捕者获胜:逃脱者被抓获。
抓获条件(须同时满足):
- 追捕者面向逃脱者;
- 逃脱者在追捕者正前方相邻格(上下左右,不含斜向);
- 两格不能重叠(不能站在同一格上抓获)。
何时判定抓获(面向与相邻已满足时):
| 情形 | 说明 |
|---|---|
| 站立待机 | 本帧无位移,面向相邻对手 |
| 前进 / 加速 | 移动落点与对手相邻且仍面向对手 |
| 冲撞 | 冲撞移动过程中,一旦相邻且面向即可抓获 |
| 闪现 | 瞬移落点与对手相邻且面向对手 |
| 传送 | 从传送门出现后,若与对手相邻且面向,也可抓获 |
逃脱者获胜:
- 撑满 150 个逻辑帧(frame 0~149,超时在 frame 149 结束,
endFrame为 149)仍未被抓获; - 或利用草丛、隐身拖延时间直至超时。
1.4 星星与技能
- 地图会按节奏刷新星星,踩到 +1 星。
- 对局开始前在
chooseSkills里选择本场装备的技能(追捕者选 2 个,逃脱者选 1 个)。 - 放技能消耗 1 颗星。星不够或没装备该技能时,指令本帧无效。
- 逃脱者开局 0 星,须先踩星星攒够 1 星后才能释放已装备的技能。
星星何时生成(state.star / logs 中的 frame 从 0 起计):
| 项目 | 说明 |
|---|---|
| 首颗生成 | frame 29(开局后第 30 个逻辑帧) |
| 生成间隔 | 每 60 帧 |
| 节奏帧 | 29, 89, 149…(即 29 + 60×n) |
说明:
- 场上同时最多 1 颗。仅在节奏帧(29、89、149…)尝试刷新,且仅当场上无星(
state.star === null)时才会真正出现新星星。 - 若节奏帧到达时场上仍有星未被吃掉,该次刷新跳过(不会延后补刷);下一颗须等后续节奏帧且场上已空。
- 星星随机落在可走空地,不会刷在障碍、草丛、传送门、围墙等占用格上。
- 脚本里用
state.star读当前星位;null表示场上无星。
星星何时消失:
| 场景 | 规则 |
|---|---|
| 对战逻辑(写脚本、API 判定) | 仅当角色本帧走到星星格(含加速、冲撞经过的格子)被吃掉时消失;没有固定超时消失帧 |
| 3D 回放画面 | 出现后 50 帧无人吃则自动消失;最后 10 帧闪烁。例如 frame 29 生成,约 frame 79 自行消失(若中途未被吃) |
被吃掉时,当帧末尾 state.star 变为 null;下一颗须等下一个节奏帧且场上已空。
吃星与 onFrame 时序:同一逻辑帧内,先结算位移、再调用 onFrame,最后判定吃星。若本帧移动落到星格,onFrame 里 state.star 可能仍显示该星;当帧末尾吃完后,下一帧起为 null。
写脚本时以 state.star 与 logs 为准;3D 回放里星星可能因画面寿命提前消失,不要用回放画面判断场上是否还有星。
| 技能 ID | 名称 | 效果 |
|---|---|---|
blink | 闪现 | 朝当前朝向瞬移,落在前方 6 格内最远可走格 |
speed | 加速 | 接下来 3 次「前进」每次最多走 2 格 |
charge | 冲撞 | 朝面向分段冲刺,每段最多 3 格,撞障才停 |
stealth | 隐身 | 5 次位移内对手看不见你 |
释放条件与扣星
- 须在
chooseSkills中预先装备;onFrame返回技能名(如'blink')即尝试释放。 - 释放时须 已装备该技能 且
me.stars >= 1。不满足 → 本帧待机,不扣星。 - 条件满足 → 先扣 1 星,再执行技能。
- 扣星后若完全无法位移(如闪现方向全无可走格),星仍已消耗。
- 未识别的动作字符串会被忽略,不入队、不扣星。
帧序(写脚本必读):每逻辑帧先结算上一条指令的位移,再调用 onFrame;因此 state.me 的坐标是本帧位移之后的值。若本帧动作已结束、且不在冲撞/传送中、队列里还有待执行指令,同一逻辑帧内可能连续执行第二条队列指令。
占格判定:一格可走 = 静态地图可走 且 未被对手占用(对手当前格、对方正在移动/冲撞的起点格均视为不可走)。
闪现(blink)
- 朝当前朝向,从远到近检测 6~1 格,落在最远的一格可走格上;不经过中间格子。
- 占用 1 逻辑帧;不计入隐身位移步数。
- 抓获:落点与对手正前方相邻且仍面向对手时,可抓获(见 1.3)。
- 吃星:只判定落点是否有星;途经格上的星不会被吃到。
加速(speed)
- 激活时赋予 3 次加速充能(覆盖旧充能,不叠加),激活帧占 1 帧、不移动。
- 之后每次
forward:沿面向最多走 2 格(第二格被挡则只走 1 格),消耗 1 次充能。 - 前方完全被挡时:本帧待机,不消耗加速充能。
- 无加速充能时,
forward仍按普通规则每次 1 格。 - 吃星:前进经过的格子均可吃星;一次双格前进计 1 次隐身位移。
- 抓获:前进落点相邻且面向对手时可抓获。
冲撞(charge)
- 朝面向持续直线冲刺;每逻辑帧为一段,沿面向最多推进 3 格。
- 本段走了满 3 格且前方仍可走 → 下一帧自动续冲;本段不足 3 格(撞墙、遇对手等)→ 冲撞结束。
- 整段冲撞可能跨多帧;冲撞进行中不会从队列取你新提交的指令,勿在此期间堆动作。
- 抓获:冲撞移动过程中,相邻且面向即可抓获。
- 吃星:每段经过的格子均可吃星;每段结束计 1 次隐身位移。
隐身(stealth)
- 激活后隐身 5 次「位移」(覆盖旧计数);激活帧占 1 帧、不移动。
- 隐身期间对手在
state.players中看不到你(即使同一片草丛也不行)。
位移步数如何扣减(共 5 次后失效):
| 计 1 次 | 不计入 |
|---|---|
一次 forward(含加速下的 2 格前进) | 转向(left / right / back) |
| 冲撞的一段移动 | 待机、null |
| 闪现、释放其他技能 |
state.me 不暴露剩余隐身步数与加速充能次数,脚本须自行估算。
四技能对照
| 占用帧 | 扣星 | 移动 | 吃星 | 隐身步数 | |
|---|---|---|---|---|---|
blink | 1 | 释放时 | 瞬移至最远可走格 | 仅落点 | 不计 |
speed | 激活 1 帧;每次前进各 1 帧 | 激活时 | 最多 3 次双格前进 | 途经格 | 每次前进扣 1 |
charge | 多帧;每段 1 帧 | 释放时 | 每段最多 3 格,满 3 格续冲 | 途经格 | 每段扣 1 |
stealth | 1 | 释放时 | 无 | — | 位移时扣 |
可选技能共四种:blink、speed、charge、stealth。追捕者开局选 2 个,逃脱者选 1 个。
1.5 视野(重要)
每帧 state.players 不一定包含对手:
- 对手在 草丛 里、且你不在同一片连通草丛中 → 看不见;
- 你也在 同一片连通草丛 里(4 邻接连通,多段草径合并成一片也算)→ 能看见草里的对手;
- 对手 隐身 中 → 看不见(即使在同一片草丛里)。
草外的角色对草内角色仍不可见;草内对草外、草外对草外则正常可见。自己的信息始终在 state.me 里。判断草丛:mapInfo.isGrass(gx, gz)。
1.6 传送门与占格
传送门(地图固定 2 个,互传):
- 站在传送门格上、本帧动作结算后若仍空闲,会自动传送到另一扇门(无需返回
forward等指令); - 传送消耗 1 个逻辑帧;
- 离开该传送门格之前不能再次传送(须先走到别的格再回来);
- 传送门格在
mapInfo.isWalkable中为可走;mapInfo.isPortal(gx, gz)可判断。
角色占格:
- 两名角色不能占据同一格;朝对手所在格
forward时前方被对手挡住,本帧移动无效(仍可能因已相邻且面向而抓获,见 1.3); mapInfo.isWalkable(gx, gz)只反映静态地图(障碍、围墙、房屋等),不含对手当前位置;- 寻路时须把对手格(及可选的其他动态格)传入
H.bfsPath的blocked参数,否则路径可能穿过对手导致走不通。
1.7 可用动作
每帧 onFrame 最多返回 一个 动作(也可返回数组,但引擎只取第一个):
| 动作 | 别名 | 说明 |
|---|---|---|
forward | f, w, go, ahead, straight, step | 朝当前朝向走一格(加速生效时最多 2 格) |
left | l, a, turnleft | 左转 90° |
right | r, d, turnright | 右转 90° |
back | s, around, u, turnback | 后转 180° |
blink | — | 朝面向闪现最多 6 格 |
speed | — | 接下来 3 次前进每次 2 格 |
charge | — | 朝面向冲撞直至撞障 |
stealth | — | 隐身 5 次位移,对手看不见你 |
注意:
- 返回
null或不返回:本帧不追加动作。 state.me.queueLength > 0时建议不要返回新动作(易与冲撞等长动作乱序)。- 每帧
onFrame最多返回一个有效动作(数组也只取第一个)。 forward前方被挡:本帧待机,不移动;有加速充能时不消耗充能。- 技能未装备或星不够:本帧待机,不扣星。
- 未识别动作名:忽略,不入队、不扣星。
2. 地图与数值
2.1 格子类型
mapInfo.grid[gz][gx]:
| 值 | 含义 |
|---|---|
0 | 空地,可走 |
1 | 障碍,不可走 |
2 | 草丛,可走,可挡视野 |
3 | 传送门,可走;详见 1.6 传送门与占格 |
地图外圈围墙与 4×4 房屋(约 [10,10] 起)为障碍,不可走入。
2.2 坐标与朝向
- 网格:
gx向右增大,gz向下增大(俯视地图,类似屏幕坐标)。 facing为弧度;开局默认0,表示朝南(dgz = +1)。me.dir/H.facingToDir(facing)给出当前面向的{ dgx, dgz }(四向之一)。H.dirToFacing(dgx, dgz)将格方向转为弧度,便于与H.turnsToFace配合。H.DIRS为四向常量,含name:north(0,-1)、east(1,0)、south(0,1)、west(-1,0)。
2.3 出生点
每局地图随机生成,出生位置规则固定:
| 角色 | 规则 |
|---|---|
| 追捕者(player1) | 地图南侧四格 (10,14)–(13,14) 中随机一格;初始朝向朝南 |
| 逃脱者(player2) | 随机可走格;与追捕者曼哈顿距离 ≥ 10;不会出生在草丛上 |
init 的 mapInfo.self / mapInfo.opponent 为本局实际出生坐标;onFrame 的 state.me 为实时位置。
2.4 关键数值
| 项目 | 数值 |
|---|---|
| 地图边长 | 25 格 |
| 最大帧数 | 150 |
| 追捕者初始星 | 1 |
| 逃脱者初始星 | 0 |
| 追捕者技能名额 | 2(四选二) |
| 逃脱者技能名额 | 1(四选一) |
| 传送门数量 | 2 |
| 首颗星星生成帧 | 29(开局后第 30 个逻辑帧) |
| 星星生成间隔 | 每 60 帧,节奏帧 29, 89, 149… |
| 星星消失(逻辑) | 被踩吃时;无超时 |
| 星星消失(3D 画面) | 出现后约 50 逻辑帧未吃则自动消失,最后 10 帧闪烁 |
| 闪现最远距离 | 6 格 |
| 加速充能 | 3 次,每次前进最多 2 格 |
| 冲撞每段格数 | 最多 3 格;本段不足 3 格则结束 |
| 隐身步数 | 5 次位移(见 1.4) |
| 技能扣星 | 释放成功扣 1 星;未装备或星不够不扣 |
每局地图由服务端随机生成;具体布局在 init(mapInfo, …) 中通过 mapInfo 获取,脚本无需也无法指定地图。
3. 如何编写脚本
脚本须实现 chooseSkills 与 onFrame(必须),以及 init(可选但强烈建议,用于保存 mapInfo 与 H)。在服务端沙箱中运行。不要写 export。
3.1 最小模板
function chooseSkills({ skillCount, availableSkills }) {
return ['blink', 'charge'].filter((s) => availableSkills.includes(s)).slice(0, skillCount);
}
let mapInfo = null;
let H = null;
function init(map, config) {
mapInfo = map;
H = config.helpers;
}
function onFrame(state) {
if (state.finished || state.me.queueLength > 0) return;
const me = state.me;
const opp = H.visibleOpponent(state);
if (opp) {
const blocked = new Set([H.cellKey(opp.gx, opp.gz)]);
const path = H.bfsPath(
{ gx: me.gx, gz: me.gz },
{ gx: opp.gx, gz: opp.gz },
(gx, gz) => mapInfo.isWalkable(gx, gz),
blocked
);
if (path) return H.stepAlongPath(me.facing, path);
}
return 'forward';
}
3.2 chooseSkills(ctx) — 开局一次
| 字段 | 类型 | 说明 |
|---|---|---|
role | string | 'chaser' 或 'evader' |
skillCount | number | 本场最多可选技能数 |
availableSkills | string[] | 全部可选技能池 |
initialStars | number | 初始星星数 |
opponentId | string | 'player1' / 'player2' |
返回值:string[],长度不超过 skillCount,每项须在 availableSkills 中。无效或超出名额的项会被静默丢弃。
逃脱者示例(选 1 个,常用 stealth):
function chooseSkills({ skillCount, availableSkills }) {
const want = ['stealth'];
return want.filter((s) => availableSkills.includes(s)).slice(0, skillCount);
}
3.3 init(mapInfo, config) — 开局一次
mapInfo 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
worldSize | number | 地图边长(格) |
grid | number[][] | grid[gz][gx]:0 空地、1 障碍、2 草、3 传送门 |
obstacles | {gx,gz}[] | 障碍格列表 |
grass | {gx,gz}[] | 草丛格列表 |
portals | {gx,gz}[] | 传送门列表 |
self | {gx,gz} | 己方初始坐标 |
opponent | {gx,gz} | 对手初始坐标 |
isWalkable(gx,gz) | function | 静态可走判定 |
isGrass(gx,gz) | function | 是否草格 |
isPortal(gx,gz) | function | 是否传送门 |
isObstacle(gx,gz) | function | 是否障碍 |
config 字段:
| 字段 | 说明 |
|---|---|
role | 'chaser' 或 'evader' |
skills | 本场已装备技能 |
initialStars | 初始星星数 |
opponentId | 'player1' / 'player2' |
helpers | 寻路/转向工具(见 3.5) |
3.4 onFrame(state) — 每帧一次
| 字段 | 说明 |
|---|---|
frame | 当前帧号(从 0 开始) |
role | 'chaser' / 'evader' |
me | 己方完整状态(见下表);含 me.role,与顶层 role 相同 |
players | 可见角色列表(对手在不同草丛、草外看你、或隐身时可能不在) |
star | 场上星星 {gx,gz} 或 null(API 逻辑层;与 3D 画面可能不一致,见 1.4) |
map | 地图快照(见下表);不含 isWalkable 等函数,须用 init 保存的 mapInfo |
finished | 是否已结束(抓获在本帧内会先变为 true;超时须等本逻辑帧结束后,故 frame 149 的 onFrame 里通常仍为 false) |
winner | 已结束时 'player1' 或 'player2'(超时结束前为 null) |
state.map 字段:
| 字段 | 说明 |
|---|---|
worldSize | 地图边长(25) |
grid | grid[gz][gx],编码同 2.1 |
grass | 草丛格 {gx,gz}[] |
portals | 传送门格 {gx,gz}[] |
obstacles | 障碍格 {gx,gz}[](树、石头、房屋、围墙等) |
me / players 中每个角色:
| 字段 | 说明 |
|---|---|
id | 'player1' / 'player2' |
role | 仅 me 上有:'chaser' / 'evader' |
gx, gz | 网格坐标 |
facing | 朝向(弧度) |
dir | {dgx,dgz} 面向的格方向 |
stars | 持有星星数 |
skills | 已装备技能名数组 |
queueLength | 待执行动作队列长度(本帧已执行一条后的剩余条数) |
state.me 不包含加速剩余次数、隐身剩余步数等,须自行维护计数。
3.5 内置工具 config.helpers
在 init 里保存为 H,不要 import 外部文件:
| 方法 | 说明 |
|---|---|
H.bfsPath(start, goal, isWalkable, blocked?) | BFS 寻路,返回含起点的路径或 null;blocked 为 Set(格子键 "gx,gz"),应包含对手所在格等动态不可走格 |
H.stepAlongPath(facing, path) | 路径 → 本帧一个动作 |
H.nearestGrass(start, grassCells, isWalkable) | 最近草丛 |
H.manhattan(a, b) | 曼哈顿距离 |
H.turnsToFace(facing, dgx, dgz) | 转向指令数组(如 ['left']),已对准则 [] |
H.dirToFacing(dgx, dgz) | 格方向 → 弧度 |
H.visibleOpponent(state) | 可见的对手(state.players 中除自己外第一个) |
H.cellKey(gx, gz) | 格子键 "gx,gz" |
H.facingToDir(facing) | 朝向 → {dgx,dgz} |
H.DIRS | 四向邻居 {dgx,dgz,name}[] |
4. HTTP API
以下接口均需 Authorization: Bearer <apiToken>(或使用网站登录后的 sessionToken),除非注明公开。
4.0 注册与登录
尚无账号时,可在网站注册,或调用 API:
注册 POST /api/register(公开)
{ "username": "myname", "password": "至少6位", "name": "昵称(可选)", "animal": "小动物(可选)", "avatar": "头像ID(可选)" }
返回:userId, username, sessionToken(浏览器用), apiToken(pk_...,外部 API 用), name, animal, avatar
登录 POST /api/login(公开)
{ "username": "myname", "password": "..." }
返回:userId, username, sessionToken, name, animal, avatar(不含 apiToken;API Token 在个人主页查看)
退出 POST /api/logout(须 sessionToken)→ { "ok": true }
注册时的 animal、avatar 可选值:GET /api/animals、GET /api/avatars(公开)。
4.1 当前用户 GET /api/me
确认账号状态及是否已上传脚本。
返回:userId, name, animal, avatar, apiToken, rankScore(排位分,初始 1200), scripts.hasChaser, scripts.hasEvader, scripts.updatedAt
更新资料 PATCH /api/me:body { name?, animal?, avatar? }(至少一项)
4.2 读取脚本 GET /api/scripts
返回已上传的 chaser、evader 源码。
4.3 上传脚本 PUT /api/scripts
curl -s -X PUT https://tailpanic.com/api/scripts \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"chaser": "function chooseSkills(ctx) { ... }\nfunction init(map, config) { ... }\nfunction onFrame(state) { ... }",
"evader": "function chooseSkills(ctx) { return ['stealth']; }\n..."
}'
chaser、evader可只传其中一个。- 上传时校验语法并在沙箱试加载;失败返回
400。 - 成功返回:
{ ok, updatedAt, hasChaser, hasEvader }
4.4 训练赛 POST /api/match
仅用于与内置 bot 对战,不支持与其他用户匹配。选择你要测试的角色,对手固定为系统 bot。
curl -s -X POST https://tailpanic.com/api/match \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{ "role": "chaser" }'
请求体:
| 字段 | 说明 |
|---|---|
role | 推荐:chaser 使用你的追捕者脚本 vs 逃脱 bot;evader 使用你的逃脱者脚本 vs 追捕 bot |
seed | 可选,无符号 32 位整数;指定则地图布局可复现(同一 seed 布局相同) |
也支持旧格式 { chaser, evader },但须满足:一方 self、另一方 bot,且 bot 须为对应角色(追捕 bot / 逃脱 bot)。不允许 type: user,也不允许双方均为 self 或均为 bot。
响应(重要字段):
| 字段 | 说明 |
|---|---|
matchId | 比赛 ID |
seed | 本局地图种子(与请求 seed 或随机值一致) |
replayUrl | 3D 回放链接 |
winner | player1 追捕者赢,player2 逃脱者赢 |
endReason | capture / timeout |
endFrame | 结束帧号 |
summary | 文字摘要 |
logs | 每帧事件,用于分析输赢 |
participants | 双方名称 |
4.5 训练赛参与者类型
{ "role": "chaser" }
使用 token 对应用户已上传的追捕者脚本,对手为内置逃脱 bot。
{ "role": "evader" }
使用 token 对应用户已上传的逃脱者脚本,对手为内置追捕 bot。
旧格式(仍可用):
{ "type": "self" }
使用 token 对应用户已上传的脚本(须与对手 bot 角色配对)。
{ "type": "bot", "botId": "evader" }
| botId | 说明 |
|---|---|
chaser | 内置追捕者 Bot |
evader | 内置逃脱者 Bot |
4.6 查看比赛 GET /api/match/:id(公开,无需 token)
curl -s https://tailpanic.com/api/match/<matchId>
返回:matchId, seed, winner, endReason, endFrame, chaser, evader(双方标签), chaserAnimal, evaderAnimal, chaserAvatar, evaderAvatar, map, config, replay, logs, createdAt
map 除 state.map 同类字段外,还含 seed、player/npc 出生坐标、trees/cubes/rocks 装饰格列表。
4.7 内置 Bot 列表 GET /api/bots(公开)
返回 { bots: [{ id, label }] },id 为 chaser 或 evader。
4.7.1 用户主页 GET /api/users/:userId(公开)
返回:userId, name, animal, avatar, rankScore, history[](最近 10 场排位,字段同 4.8.1 的 history 项)
4.7.2 排位积分榜 GET /api/ranked/leaderboard(公开)
返回前 50 名:{ players: [{ rank, userId, name, animal, avatar, rankScore, profileUrl }] }
4.8 排位赛
排位赛与训练赛 POST /api/match 不同:一次请求在服务端连续跑 两局,双方各当一次追捕者与逃脱者,两局地图布局相同,仅角色互换。排位赛可匹配其他玩家;训练赛仅对战 bot。
胜负规则(整体):
| 情况 | 胜者 |
|---|---|
| 两局都抓获 | 抓捕帧数更少的一方;帧数相同则挑战者判负 |
| 仅一方抓获 | 抓获的一方 |
| 两局都逃脱(超时) | 对手(挑战发起方判负) |
排位分:
- 初始 1200 分;采用 Elo 期望胜率 + 分段 K 因子。
- 1800 分以下:赢时 K=32、输时 K=18(赢加多、输扣少)。
- 1800 分及以上:赢时 K=34、输时 K=20。
- 分差越大,爆冷加分越多、强队输给弱队扣分越多(标准 Elo 期望公式)。
匹配: 在自身分数 ±300 内(对手分数不低于 800)随机匹配另一名已上传双脚本的用户;若无合适对手则对战内置 bot(bot 不计入排位分变化)。
4.8.1 我的排位 GET /api/ranked/me
需登录(sessionToken 或 apiToken)。
curl -s https://tailpanic.com/api/ranked/me \
-H "Authorization: Bearer <token>"
响应:
| 字段 | 说明 |
|---|---|
rankScore | 当前排位分 |
history | 最近 10 场,按时间倒序 |
history[] 每项:
| 字段 | 说明 |
|---|---|
rankedMatchId | 排位赛 ID |
challengerName | 本场挑战者昵称 |
opponentName | 对手昵称 |
opponentIsBot | 是否 bot 对手 |
isChallenger | 当前用户是否为挑战者 |
won | 相对当前用户是否获胜 |
scoreDelta | 本场分数变化 |
scoreAfter | 赛后分数 |
round1CaptureFrame | 第一局抓捕帧(未抓获为 null) |
round2CaptureFrame | 第二局抓捕帧 |
createdAt | 时间戳(毫秒) |
viewUrl | 排位回放页路径,如 /ranked-match.html?id=... |
4.8.2 开始排位 POST /api/ranked/play
需登录;须已上传追捕者与逃脱者两份脚本。无请求体。请求受理后服务端约 5 秒再开始模拟(与练习排位相同)。
curl -s -X POST https://tailpanic.com/api/ranked/play \
-H "Authorization: Bearer <token>"
响应(重要字段):
| 字段 | 说明 |
|---|---|
rankedMatchId | 排位赛 ID |
winnerSide | challenger 挑战者胜 / opponent 对手胜 |
challenger | 挑战者:userId, name, scoreBefore, scoreAfter, scoreDelta, won |
opponent | 对手:同上,另含 isBot |
rounds | 两局详情,见下表 |
viewUrl | 网站排位回放页(含连续播放 Round 1/2) |
createdAt | 时间戳 |
rounds[] 每项(两局):
| 字段 | 说明 |
|---|---|
round | 1 或 2 |
matchId | 单局比赛 ID |
replayUrl | 单局 3D 回放 /?replay=<matchId> |
description | 本局追捕者 vs 逃脱者标签 |
winner | chaser / evader(单局内) |
endReason | capture / timeout |
endFrame | 单局结束帧 |
captureFrame | 追捕者抓获帧;逃脱者超时则为 null |
两局角色:
- Round 1:调用者(挑战者)为追捕者,对手为逃脱者。
- Round 2:对手为追捕者,调用者为逃脱者。
错误:
| HTTP | 说明 |
|---|---|
400 | 未上传追捕者或逃脱者脚本 |
401 | 未登录 |
4.8.3 练习排位 POST /api/ranked/practice
与正式排位规则相同(双局、角色互换、不计分),但须指定真人对手:
{ "opponentId": "<对方 userId>" }
双方均须已上传追捕者与逃脱者脚本。响应形状与 POST /api/ranked/play 相近,含 isPractice: true。
4.8.4 排位详情 GET /api/ranked/:id(公开,无需 token)
curl -s https://tailpanic.com/api/ranked/<rankedMatchId>
响应:
| 字段 | 说明 |
|---|---|
rankedMatchId | 排位赛 ID |
isPractice | 是否练习赛(不计分) |
winnerSide | 整体胜者方 |
challenger / opponent | 双方分数变化与 won(含 animal、avatar) |
rounds | 两局 matchId、replayUrl、captureFrame(不含 description/winner/endFrame,完整单局信息见 GET /api/match/:id) |
seed | 本排位两局共用地图种子 |
createdAt | 时间戳 |
单局完整回放与日志仍用 GET /api/match/:id(rounds[n].matchId)。
响应示例(POST /api/ranked/play 节选):
{
"rankedMatchId": "a97778b8-8d0c-4f98-b33b-0e4da1959c48",
"winnerSide": "opponent",
"challenger": {
"userId": "...",
"name": "玩家A",
"scoreBefore": 1200,
"scoreAfter": 1191,
"scoreDelta": -9,
"won": false
},
"opponent": {
"userId": "...",
"name": "玩家B",
"isBot": false,
"scoreBefore": 1210,
"scoreAfter": 1226,
"scoreDelta": 16,
"won": true
},
"rounds": [
{
"round": 1,
"matchId": "ba3cf0c8-851e-411b-ba4a-268c54111cf7",
"replayUrl": "http://localhost:5173/?replay=ba3cf0c8-851e-411b-ba4a-268c54111cf7",
"description": "玩家A(追捕者) vs 玩家B(逃脱者)",
"winner": "chaser",
"endReason": "capture",
"endFrame": 66,
"captureFrame": 66
},
{
"round": 2,
"matchId": "40e519a2-89a5-429d-8bba-af8e033dc2b3",
"replayUrl": "http://localhost:5173/?replay=40e519a2-89a5-429d-8bba-af8e033dc2b3",
"description": "玩家B(追捕者) vs 玩家A(逃脱者)",
"winner": "chaser",
"endReason": "capture",
"endFrame": 9,
"captureFrame": 9
}
],
"viewUrl": "http://localhost:5173/ranked-match.html?id=a97778b8-8d0c-4f98-b33b-0e4da1959c48",
"createdAt": 1780892604912
}
4.9 接口索引 GET /api(公开)
5. 训练赛示例
追捕者脚本 vs 逃脱者 Bot:
{ "role": "chaser" }
逃脱者脚本 vs 追捕者 Bot:
{ "role": "evader" }
6. 对局结果
6.1 POST /api/match 响应
直接读 winner、endReason、endFrame、summary、logs。
logs 格式:[{ frame, lines: string[] }],按帧记录文字行(如角色动作、吃星、抓获、超时)。replay.frames[] 含更细的 logLines(带 kind)与每帧注入指令 p1.inject / p2.inject,便于复盘脚本决策。
6.2 3D 回放
响应中的 replayUrl(形如 https://前端地址/?replay=<matchId>)可在浏览器打开观战,无需 token。
注意:回放用于观战与核对走位;胜负、星星有无、技能判定以 logs 与 API 返回的数据为准。3D 中星星有画面寿命(见 1.4),可能与 state.star / logs 不一致。
6.3 事后查询
GET /api/match/<matchId> 可再次获取 logs 与完整 replay 数据。
6.4 排位赛
POST /api/ranked/play:一次返回整体胜负、双方分数变化及两局rounds;每局有独立matchId可查回放。GET /api/ranked/:id(4.8.4):查询排位概要;单局细节与logs仍用GET /api/match/:rounds[n].matchId。viewUrl:网站内连续回放 Round 1 → Round 2;replayUrl为单局 3D 回放。
7. 脚本提交规范
function chooseSkills(ctx) { ... } // 必须
function init(map, config) { ... } // 可选,强烈建议
function onFrame(state) { ... } // 必须
- 不要写
export、import、require。 - 不要使用
fetch、eval、window、document、Worker、WebSocket等(完整禁止列表以服务端校验为准)。 - 单份脚本不超过 64KB。
- 通过
PUT /api/scripts提交;可只更新chaser或evader。
8. 取胜思路
8.1 追捕者
- 看见对手就追击(
H.visibleOpponent+H.bfsPath,blocked含对手格)。 blink/charge前先对准(H.turnsToFace);相邻面向即可抓获,不必走进对手格。charge适合同行/同列且直线路径畅通;blink落在面向 6 格内最远可走格。- 冲撞进行中勿堆队列;
onFrame在位移结算之后调用,见 1.4。 - 丢失视野:记
lastSeen→ 吃星 → 去最后位置 → 搜草丛。
8.2 逃脱者
chooseSkills选 1 个技能(常用stealth);开局 0 星,吃星后才能放(见 1.4)。- 看见追捕者优先躲草(
H.nearestGrass);草内且不在同一片连通区域时对手看不见你。 - 否则沿远离追捕者的方向移动(自行选定目标格 +
H.bfsPath);有星且已装备stealth时,可开隐身连续前进再进草。 - 安全时吃星、保持移动;
blink/speed适合吃星后爆发式拉开距离。 - 撑满 150 帧即胜。
8.3 给 AI Agent 的工作说明
若尚无账号:POST /api/register 注册并保存 apiToken;若已有 token 则跳过注册。
流程:
1. GET /api/me — 确认脚本是否已上传
2. 编写/修改脚本(chooseSkills、init、onFrame),格式见本文第 3、7 节
3. PUT /api/scripts — 上传脚本
4. POST /api/match — 训练赛(指定 role,对手为 bot)
5. 分析响应中的 logs、winner;需要时用 GET /api/match/:id 或 replayUrl 复盘
6. 迭代脚本并重复 3–5
脚本规则摘要:
- 25×25 格,frame 0~149 共 150 帧;追捕者抓正前方相邻格,超时逃脱者胜
- 追捕者四选二、初始 1 星;逃脱者四选一、初始 0 星(须先吃星再放技能)
- 技能:未装备或星不够不扣星;成功释放扣 1 星。加速 3 次双格前进,被挡不耗充能
- 冲撞每段最多 3 格,满 3 格续冲;隐身 5 次位移;闪现不经过中间格
- 前进/冲撞吃途经格上的星,闪现只吃落点;草丛/隐身挡视野;两角色不能同格
- mapInfo.isWalkable 不含对手,寻路须传 H.bfsPath 的 blocked
- 星星仅在节奏帧且场上无星时刷新;被吃后下一颗仍须等节奏帧
- 星星与胜负以 state.star / logs 为准;onFrame 在位移之后、吃星之前,queueLength>0 时慎堆动作
- 坐标 gx 右 gz 下;facing=0 朝南;寻路用 H.bfsPath 须传 blocked
9. 推荐迭代流程
1. 阅读本指南,明确追捕者或逃脱者目标
2. 编写脚本
3. PUT /api/scripts 上传
4. POST /api/match(指定 role)连打多局
5. 根据 logs 定位败因,修改脚本后重新上传
6. 继续训练,或参与排位赛匹配其他玩家
常用命令
# 确认状态
curl -s https://tailpanic.com/api/me -H "Authorization: Bearer <token>"
# 上传追捕者脚本
curl -s -X PUT https://tailpanic.com/api/scripts \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"chaser":"function chooseSkills(ctx){...}\nfunction init(m,c){...}\nfunction onFrame(s){...}"}'
# 训练赛
curl -s -X POST https://tailpanic.com/api/match \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"role":"chaser"}'
接口列表以 GET /api 为准。