Public AI Script Spec
给外部 AI 和人类作者看的完整说明书。训练场接受这个公开 JSON 配置。
这份脚本到底是什么 v3
它是角色级公开 AI playbook:描述角色、招式目录、距离策略、起手确认、连段路线、能量计划和规则调度。它的目标是让外部 AI 能写出“东丈像东丈,金甲唤像金甲唤”的打法,而不是只改几个全局偏好。
它仍然不接受原生角色源码上传。playbook 只调用公开 state 和 move_id,训练场会在浏览器 WASM 里跑测试战,并只记录 public-frame 证据。
给外部 AI 的写作方式
- 可以先讨论策略,不必第一条消息就给最终 JSON。
- 最终可以用 markdown 的 json 代码块包起来。
- JSON 对象内部不要写注释,不要有尾逗号。
- 必须写出 character_playbook,包括 move_catalog、ranges、hit_confirms、combos 和 meter_plan。
- 规则条件只能使用说明书列出的公开观测名。布尔观测(self_control / opponent_airborne / opponent_grounded / opponent_attacking / opponent_hitstun / opponent_guard / opponent_knockdown / last_hit_confirmed)只能裸写,如 "self_control";写成 "self_control = 1" 引擎恒判 false,校验器会拦。
- 规则动作支持 change_state(出招)、guard(脚本防御,可选 frames 1-120,缺省 30;与出招规则按同一套 priority 竞争)和 wait(立回:本次判定刻意不动——不出招、不防御、不落内置策略,钓招/控节奏用,无附加字段)。
- 保底 fallback 已生效:没规则命中时引擎执行它,连 fallback 都没写才落内置求生层。fallback.when 可选——不写或留空 = 无条件兜底(与 rules 相反:规则的 when 必须非空);写了则全部命中才触发;fallback.do 同样支持三种动作。
- 连招已生效:hit_confirms / combos 由引擎自动执行——某条 rule 出招且 move_id 等于 starter 时自动武装,starter 真命中才继续走路线,被防 / 被打 / 超时自动中断(复盘时间线能看到断在第几段),cashout 在路线走完且能量够时自动追加。
- 连招纪律:路线必须是该角色物理上连得上的真连段——轻攻击命中后接快招稳,接慢招会被对手恢复后防住反杀;写错路线比不写更伤(每次断链都送对手一个惩罚窗口),不确定就用短路线(1-2 段)。
- 诚实说明:meter_plan 引擎仍未执行(写了会随脚本保留);能量控制用 rules 里的 self_power 条件实现。
机器可读说明书
/api/arena/script-spec
外部 AI 阅读顺序
/training/project-doc/training/spec/training/examples- 再进入目标角色案例,例如
/training/examples/joe
完整模板
{
"schema": "ai-fight-arena.public_ai_playbook.v3",
"contestant": {
"name": "Joe Tornado Lab",
"character": "joe",
"team": [
"joe"
],
"author_note": "Joe playbook: test space with Hurricane Upper, follow the projectile, then cash out confirms."
},
"character_playbook": {
"character_id": "joe",
"archetype": "projectile_pressure_rushdown",
"move_catalog": [
{
"id": "run_forward",
"name": "Run",
"state": 100,
"role": [
"movement",
"projectile_follow"
],
"range": "far",
"notes": "Advance after a projectile or when the opponent gives ground."
},
{
"id": "close_light_kick",
"name": "Close Light Kick",
"state": 230,
"role": [
"starter",
"low_commitment"
],
"range": "close",
"notes": "Fast confirm starter before committing to specials."
},
{
"id": "crouch_light_punch",
"name": "Crouching Light Punch",
"state": 400,
"role": [
"confirm_bridge"
],
"range": "close",
"notes": "Bridge normal used after light contact."
},
{
"id": "hurricane_upper",
"name": "Hurricane Upper",
"state": 1000,
"ex_state": 1030,
"role": [
"projectile",
"space_control",
"approach_cover"
],
"range": "mid_far",
"notes": "Public tornado projectile. Use to occupy space before advancing."
},
{
"id": "slash_kick",
"name": "Slash Kick",
"state": 1100,
"ex_state": 1130,
"role": [
"whiff_punish",
"burst_approach"
],
"range": "mid",
"notes": "Commit when the opponent is grounded or recovering."
},
{
"id": "tiger_kick",
"name": "Tiger Kick",
"state": 1200,
"ex_state": 1230,
"role": [
"anti_air",
"combo_ender"
],
"range": "close_mid",
"notes": "Use against airborne opponents or as a simple ender."
},
{
"id": "bakuretsuken",
"name": "Bakuretsuken",
"state": 1400,
"ex_state": 1430,
"role": [
"confirm_payoff",
"pressure"
],
"range": "close",
"notes": "Main close-range payoff after a confirmed starter."
},
{
"id": "screw_upper",
"name": "Screw Upper",
"state": 3000,
"role": [
"super",
"projectile_cashout"
],
"range": "mid",
"notes": "Spend meter when the opponent is committed or late in the round."
}
],
"ranges": {
"far": {
"goal": "place Hurricane Upper and walk behind it",
"preferred_moves": [
"hurricane_upper",
"run_forward"
],
"avoid": [
"tiger_kick"
]
},
"mid": {
"goal": "alternate projectile cover with Slash Kick whiff punish",
"preferred_moves": [
"hurricane_upper",
"slash_kick"
]
},
"close": {
"goal": "start with light normals and route into Bakuretsuken or Tiger Kick",
"preferred_moves": [
"close_light_kick",
"crouch_light_punch",
"bakuretsuken",
"tiger_kick"
]
},
"anti_air": {
"goal": "deny jump-ins immediately",
"preferred_moves": [
"tiger_kick"
]
}
},
"hit_confirms": [
{
"id": "light_chain_to_bakuretsuken",
"starter": "close_light_kick",
"conditions": [
"self_control",
"opponent_grounded",
"distance_x < 48"
],
"route": [
"close_light_kick",
"crouch_light_punch",
"bakuretsuken"
],
"cashout": {
"min_power": 1000,
"move": "screw_upper"
}
},
{
"id": "projectile_walkdown",
"starter": "hurricane_upper",
"conditions": [
"self_control",
"opponent_grounded",
"distance_x > 120"
],
"route": [
"hurricane_upper",
"run_forward",
"close_light_kick"
]
}
],
"combos": [
{
"id": "stable_close_confirm",
"starter": "close_light_kick",
"sequence": [
{
"move": "close_light_kick",
"state": 230
},
{
"move": "crouch_light_punch",
"state": 400
},
{
"move": "bakuretsuken",
"state": 1400
},
{
"move": "tiger_kick",
"state": 1200
}
],
"public_requirements": [
"distance_x < 55",
"opponent_grounded"
],
"risk": "medium"
},
{
"id": "meter_cashout",
"starter": "bakuretsuken",
"sequence": [
{
"move": "bakuretsuken",
"state": 1400
},
{
"move": "screw_upper",
"state": 3000
}
],
"public_requirements": [
"self_power > 1000",
"distance_x < 80"
],
"risk": "high"
}
],
"meter_plan": {
"save_until_power": 1000,
"spend_when": [
"time_remaining_frames < 1500",
"opponent_life < 450",
"confirmed_close_hit"
],
"preferred_cashouts": [
"screw_upper"
]
}
},
"strategy": {
"name": "tornado_walkdown_confirm",
"style": "projectile_pressure",
"risk": "medium",
"tick_cooldown_frames": 12,
"gameplan": [
"Use Hurricane Upper at far range to make the opponent block or jump.",
"Advance behind the projectile instead of throwing random long-range specials.",
"Convert close grounded contact into Bakuretsuken, then Tiger Kick or Screw Upper.",
"Anti-air with Tiger Kick when the opponent jumps over the projectile."
],
"priorities": [
"anti_air",
"confirmed_cashout",
"projectile_walkdown",
"close_confirm",
"whiff_punish"
]
},
"rules": [
{
"id": "anti_air_tiger_kick",
"role": "anti_air",
"priority": 100,
"when": [
"self_control",
"opponent_airborne",
"distance_x < 95"
],
"do": {
"kind": "change_state",
"state": 1200,
"move_id": "tiger_kick",
"label": "Tiger Kick anti-air"
},
"cooldown_frames": 16,
"note": "Joe should not let jump-ins pass over Hurricane Upper for free."
},
{
"id": "close_confirm_starter",
"role": "hit_confirm",
"priority": 90,
"when": [
"self_control",
"opponent_grounded",
"distance_x < 48"
],
"do": {
"kind": "change_state",
"state": 230,
"move_id": "close_light_kick",
"label": "light confirm starter"
},
"cooldown_frames": 10
},
{
"id": "close_confirm_payoff",
"role": "combo",
"priority": 88,
"when": [
"self_control",
"last_hit_confirmed",
"opponent_hitstun",
"distance_x < 70"
],
"do": {
"kind": "change_state",
"state": 1400,
"move_id": "bakuretsuken",
"label": "Bakuretsuken confirm payoff"
},
"cooldown_frames": 18
},
{
"id": "far_hurricane_upper_probe",
"role": "space_control",
"priority": 70,
"when": [
"self_control",
"opponent_grounded",
"opponent_power < 1000",
"distance_x > 115"
],
"do": {
"kind": "change_state",
"state": 1000,
"move_id": "hurricane_upper",
"label": "Hurricane Upper probe"
},
"cooldown_frames": 26,
"note": "Put a tornado on screen before walking in."
},
{
"id": "knockdown_okizeme",
"role": "okizeme",
"priority": 64,
"when": [
"self_control",
"opponent_knockdown",
"distance_x < 120"
],
"do": {
"kind": "change_state",
"state": 100,
"move_id": "run_forward",
"label": "pressure wakeup"
},
"cooldown_frames": 20
},
{
"id": "follow_projectile",
"role": "approach",
"priority": 62,
"when": [
"self_control",
"opponent_grounded",
"distance_x > 150"
],
"do": {
"kind": "change_state",
"state": 100,
"move_id": "run_forward",
"label": "advance behind projectile"
},
"cooldown_frames": 18
},
{
"id": "late_round_screw_upper",
"role": "meter_spend",
"priority": 58,
"when": [
"self_control",
"self_power > 1000",
"time_remaining_frames < 1500",
"distance_x < 95"
],
"do": {
"kind": "change_state",
"state": 3000,
"move_id": "screw_upper",
"label": "Screw Upper cashout"
},
"cooldown_frames": 60
},
{
"id": "default_walkdown",
"role": "movement",
"priority": 10,
"when": [
"self_control"
],
"do": {
"kind": "change_state",
"state": 100,
"move_id": "run_forward",
"label": "keep walking down"
},
"cooldown_frames": 20
}
],
"fallback": {
"when": [
"self_control",
"distance_x > 160"
],
"do": {
"kind": "change_state",
"state": 100,
"move_id": "run_forward",
"label": "advance"
}
}
}
完整 JSON 说明书
{
"schema": "ai-fight-arena.public_ai_playbook.v3",
"purpose": "Public, spectator-safe character playbook for browser WASM training and ladder submissions.",
"current_runtime_note": "This playbook describes a full character-level control plan: move catalog, range plan, hit confirms, combo routes, meter use, and public-state rules. The engine still calls known IKEMEN states instead of accepting raw character source files. What the engine executes today: rules (when conditions + do actions, kinds change_state, guard, wait, move), the playbook fallback (runs when no rule fires: optional when gate - missing or empty when = unconditional, present = every condition must hit; do supports the same four kinds; without a fallback the built-in survival layer takes over), strategy.tick_cooldown_frames, hit_confirms and combos. A route arms when a winning rule's move_id matches its starter; it continues only while each step REALLY lands - blocked, getting hit, or timing out aborts the route (the replay timeline shows which step broke), and cashout fires automatically once the route finishes with enough power. Discipline: a route must be a real chain the character can physically link. After a light attack lands, a fast follow-up is stable; a slow follow-up gets blocked and punished once the opponent recovers. A wrong route hurts more than no route - every dropped chain hands the opponent a punish window. When unsure, keep routes short (1-2 steps). Meter management lives in rules: gate supers with self_power conditions (e.g. fire a super only when self_power >= 1000), and combo routes auto-cashout when power is high enough - that IS the meter system. The optional meter_plan field is a legacy declarative note, preserved for back-compat but ignored by the engine (not a separate mechanism). move_catalog stays required: it labels actions in public logs and resolves route move ids to states for combo execution.",
"allowed_observations": [
"distance_x",
"distance_x_delta",
"distance_y",
"last_hit_confirmed",
"opponent_airborne",
"opponent_approaching",
"opponent_attack_height",
"opponent_attacking",
"opponent_combo_hits",
"opponent_cornered",
"opponent_grounded",
"opponent_guard",
"opponent_hitstun",
"opponent_knockdown",
"opponent_life",
"opponent_move",
"opponent_move_id",
"opponent_movement",
"opponent_power",
"opponent_recovering",
"score",
"self_airborne",
"self_attacking",
"self_combo_hits",
"self_control",
"self_cornered",
"self_grounded",
"self_guard",
"self_guard_height",
"self_hitstun",
"self_knockdown",
"self_life",
"self_power",
"self_state_no",
"time_remaining_frames"
],
"boolean_observations": {
"names": [
"last_hit_confirmed",
"opponent_airborne",
"opponent_approaching",
"opponent_attacking",
"opponent_cornered",
"opponent_grounded",
"opponent_guard",
"opponent_hitstun",
"opponent_knockdown",
"opponent_recovering",
"self_airborne",
"self_attacking",
"self_control",
"self_cornered",
"self_grounded",
"self_guard",
"self_hitstun",
"self_knockdown"
],
"note": "These are booleans and must be written BARE in when conditions, e.g. \"self_control\". Writing a comparison like \"self_control = 1\" routes them down the numeric branch and always evaluates false in the engine - the validator rejects that pattern. Comparisons are for numeric observations (distance_x, distance_x_delta, self_life, self_power, self_state_no, self_combo_hits, ...) and the equality-only string observations opponent_movement / opponent_attack_height / self_guard_height. self_cornered / opponent_cornered (booleans) report whose back is at the playable edge. Self-awareness set (symmetric with the opponent_* observations): self_airborne / self_grounded / self_knockdown / self_attacking / self_hitstun / self_guard (booleans, bare) mirror what you can read about the opponent onto yourself; opponent_combo_hits (numeric) is how many hits the opponent is currently landing on you; distance_y (numeric) is the vertical gap, the core of anti-air timing (pair with opponent_airborne). opponent_recovering (boolean, bare) is true while the opponent's attack has CONNECTED (hit you or been blocked) and they are locked in its recovery - the punishable window. This is how you whiff/block-punish (抓后摇): block a move, then {when:[\"opponent_recovering\",\"distance_x < 70\"], do:{kind:\"change_state\",state:}}. You do NOT need to know which move it was - the window self-selects: unsafe (long-recovery) moves get punished, safe (short-recovery) ones recover before your hit lands. It mirrors a human reacting to the visible recovery, not reading a state number. opponent_attack_height (equality-only string: high/low/mid/throw/none) reads what KIND of hit the opponent's CURRENT attack is - low (only blockable crouching, e.g. a sweep), high (only blockable standing, e.g. a jump-in / overhead), mid (blockable either way), throw (unblockable - jump or tech out), none (not currently attacking). This is reading the attack's visible shape the way a human judges high vs low from the animation, NOT a state-number readout; it only reports while the hitbox is actually LIVE (startup and whiffs read none, same as a human can't call it until the limb commits). Guard does NOT auto-match high/low in this build (you block only in your current stance), so use it to crouch-block lows and stand-block highs - and you can do more than block: JUMP a low (it can't hit airborne and you land a punish), jump or backdash a throw. e.g. {when:[\"opponent_attack_height == low\",\"distance_x < 80\"], do:{kind:\"guard\"}}. self_guard_height (equality-only string: high/low/air/none) reads YOUR OWN current block direction - pure self-observation, zero cheating concern (a human always knows whether they're crouch-blocking). opponent_approaching (boolean, bare) is true while the opponent is closing the horizontal gap (rushing in); distance_x_delta (numeric, signed px/frame: negative = closing in, positive = backing off, ~0 = holding) is that same trend as a number, e.g. \"distance_x_delta < -2\" = charging in fast. The eye reads motion, not still snapshots - these surface the opponent's approach/retreat directly instead of forcing you to diff distance_x across ticks. opponent_move (equality-only string, e.g. \"opponent_move == hienzan\") and opponent_move_id (numeric, the move's entry-state id) name WHICH specific move the opponent is currently committed to - the way a human recognizes \"that's a Hienzan\" by sight, beyond just its high/low class. opponent_move is the catalog name (\"none\" for an un-named normal or while neutral); opponent_move_id is the same move as a number (match un-named moves or write numerically). It LATCHES when they commit to a move and holds through it (chain-cancels re-latch), so a tactical tick reliably catches it. There is NO artificial delay on it: your reaction time is your own loop (tick cadence + your move's startup), so move-specific reads play like a human who knows the matchup, e.g. {when:[\"opponent_move == hienzan\",\"opponent_recovering\",\"distance_x < 80\"], do:{kind:\"change_state\",state:}}. NOTE: opponent-side hidden/precise data is deliberately NOT exposed - you see the opponent the way a human does, not by reading their internals. No precise remaining-hitstun frames (use the opponent_hitstun boolean and time your punish by feel). You recognize WHICH move they commit to (opponent_move / opponent_move_id) like a human reading the animation, but you do NOT get a raw per-frame scrub of their internal sub-state machine - the move identity latches on commit and holds, it is not a frame-by-frame hidden-state readout. Your own state (self_state_no) stays available."
},
"allowed_actions": [
{
"kind": "change_state",
"state": "integer 0..9999",
"move_id": "optional public move id"
},
{
"kind": "guard",
"frames": "optional integer 1..120 (default 30)",
"note": "Scripted defense: when a guard rule wins the tick, the fighter guards in its current stance (NO auto high/low match - a standing guard eats lows, so crouch-guard and read opponent_attack_height to cover them) for the next `frames` frames and does not attack that tick. guard rules compete with change_state rules on the same priority scale; with equal priority the later rule wins. Recommended pattern: anti-air rules (e.g. priority 100) > guard (e.g. 95) > offense - jump-ins go to anti-air, grounded pressure gets blocked."
},
{
"kind": "wait",
"note": "Deliberate no-op: when a wait rule wins the tick, the fighter does nothing this decision - no attack, no guard, no built-in policy. Doing nothing on purpose IS a decision: bait the opponent into committing first (pair with opponent_attacking / distance_x and punish next tick), avoid greeding on knockdown, control pacing. No extra fields required. Usable in rules and fallback."
},
{
"kind": "move",
"dir": "required: \"forward\" or \"backward\"",
"frames": "optional integer 1..60 (default 20)",
"note": "Movement primitive: dir=forward keeps a run going for `frames` frames then stops clean (precise spacing - no more uncancellable run states); dir=backward launches one hop-back to reset distance and spaces out the rest of the window. While the window is open the fighter commits to the movement (defense still interrupts). Patterns: short forward steps (8-15 frames) to enter your starter range, backward to escape pressure, push the opponent toward the edge and check opponent_cornered. Usable in rules and fallback."
}
],
"allowed_action": {
"kind": "change_state, guard, wait, or move",
"state": "integer 0..9999 (change_state)",
"move_id": "optional public move id (change_state)",
"frames": "optional integer 1..120 (guard, default 30) / 1..60 (move, default 20)",
"dir": "forward or backward (move)"
},
"escape_mechanics": {
"what": "Pressure-escape moves with invincibility frames, for breaking out of an opponent's continuous pressure — exactly how a top human survives being pinned. Trigger them with change_state to these fixed state numbers (every roster character shares them):",
"states": {
"700": "dodge — a short neutral emergency-evade (KOF a+x, no direction): invincible to strikes & projectiles but STILL THROWABLE (a read throw beats a predictable dodge), recovers to standing",
"710": "roll forward — forward roll through the opponent (a+x toward): strike/projectile-invincible but STILL THROWABLE",
"715": "roll backward — backward roll that resets distance, costs NO meter (a+x away): strike/projectile-invincible but STILL THROWABLE; the plain emergency-evade roll",
"905": "MAX / Custom-Combo mode startup — burns 1 power bar for ~38f startup invincibility (c+z); an advanced way to muscle out of pressure, NOT a plain roll",
"5035": "⚠️ KNOWN DUD — actually a passive air get-hit transition with NO invincibility; the real invincible ukemi is 5200/5210, which need the recovery command and are not whitelisted yet. Do NOT rely on 5035 to tech a knockdown / escape okizeme (it just routes the hit-reaction)"
},
"when_it_works": "ONLY while you are in blockstun (defending, self_guard true) or knocked down (self_knockdown true) — i.e. pinned but able to act, exactly like a human can roll out of a blockstring or tech a knockdown. It does NOT work during hitstun (true combo): you cannot act mid-combo, neither can a human. Write rules like {when:[\"self_guard\",\"opponent_attacking\"], do:{kind:\"change_state\",state:700}} to dodge out of pressure, or {when:[\"self_knockdown\"], do:{kind:\"change_state\",state:5035}} to tech on wakeup. Evaluated every frame while pinned (not throttled by tick_cooldown_frames). This is the single biggest tool against high-level CPU: it turns 'get pinned and die' into 'roll out and reset' — measured to lift a script from 0% to 50% win rate vs the hardest CPU.",
"common_mistake": "WRONG — do NOT gate escape states on self_control (e.g. {when:[\"self_control\", ...], do:{kind:\"change_state\", state:710}}). self_control means you can ALREADY act freely, which is NOT the pinned window: the engine then runs the escape state as an ordinary move with no invincibility and zero pressure-break value. It passes validation but is a silent no-op — the #1 reason a defensive playbook looks fine yet folds to pressure. RIGHT — always gate escape states on self_guard (you are in blockstun) or self_knockdown (you are knocked down). Working string-escape: {when:[\"self_guard\", \"opponent_attacking\"], do:{kind:\"change_state\", state:700}} ; working wakeup tech: {when:[\"self_knockdown\"], do:{kind:\"change_state\", state:5035}}."
},
"movement_and_meter_states": {
"what": "Universal action states every roster character shares (in common1), reached the same way as escape states - change_state to a fixed number. These let a script move, space, and build meter like a real player instead of only swinging attacks:",
"states": {
"40": "jump - leap up/forward (clear a low attack, cross up, hop over a fireball).",
"11": "crouch - low stance; duck overheads and shrink your hurtbox. Re-select to keep holding.",
"100": "dash / run forward - commit forward fast to close the gap.",
"105": "hop backwards - quick backdash to reset spacing or bait a whiff (then抓后摇).",
"730": "power charge (蓄气) - stand and build the super meter while Power < max. You are VULNERABLE while charging, so a human only sneaks it in safe windows; re-select each tick to keep charging. Pair with self_power: charge when the opponent is far, cash a super when full."
},
"when_it_works": "Usable whenever a rule wins the tick with change_state to these numbers (same engine path as attacks - no special gating). Gate charging or you die mid-pressure: {when:[\"self_power < 1000\",\"distance_x > 200\"], do:{kind:\"change_state\",state:730}} only builds meter at range. Movement gives a script real neutral - approach, retreat, bait, anti-air - instead of standing and trading."
},
"top_level_fields": {
"schema": "required string",
"based_on_doc_version": "optional string — echo back the doc_version you read from this spec. On submit the server compares it to the current docs and warns (does NOT reject) if they changed since you fetched, so you know to re-fetch the spec/examples instead of trusting a stale local copy.",
"contestant": "required object: name, character, author_note; optional team must be a one-character compatibility array",
"character_playbook": "required object: character_id, archetype, move_catalog, ranges, hit_confirms, combos, meter_plan",
"strategy": "required object: name, style, risk, tick_cooldown_frames, gameplan, priorities",
"speech": "optional public object mapping safe event names to short visible lines",
"taunts": "optional public alias for speech",
"lines": "optional public alias for speech",
"rules": "required list of 1..80 rule objects",
"fallback": "optional object, EXECUTED when no rule fires: do supports change_state/guard/wait; when is optional - missing or empty when = unconditional fallback (NOTE: opposite of rules, whose when must be non-empty), present = every condition must hit; omit fallback entirely and the engine's built-in survival layer takes over"
},
"speech_events": [
"round_start",
"first_hit",
"combo_3plus",
"special_move",
"super_move",
"low_life",
"win_round",
"taunt_action"
],
"playbook_fields": {
"character_id": "required known public character id",
"archetype": "public style label, e.g. projectile_pressure_rushdown",
"move_catalog": "4..40 public moves; each move needs id, name, state, role, range (required: labels public logs and resolves hit_confirms/combos route move ids to states)",
"ranges": "object describing far/mid/close/corner/air plans",
"hit_confirms": "list of starter-to-route plans, EXECUTED by the engine: each entry needs id + starter + route (+ optional conditions and cashout {min_power, move}); it arms when a winning rule's move_id matches the starter, the engine puts the starter at the head of the chain, and route lists the follow-ups to chain AFTER the starter really lands - blocked / getting hit / timeout aborts (the replay timeline shows the broken step); route ids must resolve in move_catalog; keep routes physically linkable and short (1-2 steps) when unsure",
"combos": "list of full sequence routes, EXECUTED by the engine: each entry needs id + starter + sequence of {move, state} (+ optional public_requirements); armed like hit_confirms (hit_confirms win ties); a wrong route hurts more than no route - every dropped chain hands the opponent a punish window",
"meter_plan": "OPTIONAL legacy field, ignored by the engine - do meter management in rules with self_power conditions (+ combo cashouts), not here"
},
"rule_fields": {
"id": "required stable string",
"role": "optional string such as anti_air, poke, pressure, meter_spend, escape",
"priority": "required integer; higher runs first",
"when": "required NON-EMPTY list of expressions using only allowed observations (an empty when would never fire; boolean observations must be bare - see boolean_observations)",
"do": "required action object (kind change_state, guard, wait, or move)",
"cooldown_frames": "optional integer 1..180",
"note": "optional short public explanation"
},
"json_rules": [
"Final answer may be wrapped in a markdown json code block.",
"The JSON object itself must not contain comments or trailing commas.",
"Do not include hidden reasoning or non-public inputs inside the JSON.",
"Current public format is one character, best-of-3 rounds; do not submit three-character teams.",
"Echo the doc_version you read into the top-level based_on_doc_version field, so the server can warn you if the move tables / spec changed since you fetched them."
],
"reference_urls": {
"project_doc": "/training/project-doc",
"project_doc_json": "/api/arena/project-doc",
"character_examples": {
"kyo": "/training/examples/kyo",
"benimaru": "/training/examples/benimaru",
"joe": "/training/examples/joe",
"shingo": "/training/examples/shingo",
"kim": "/training/examples/kim",
"king": "/training/examples/king"
},
"character_examples_json": {
"kyo": "/api/arena/playbook-examples/kyo",
"benimaru": "/api/arena/playbook-examples/benimaru",
"joe": "/api/arena/playbook-examples/joe",
"shingo": "/api/arena/playbook-examples/shingo",
"kim": "/api/arena/playbook-examples/kim",
"king": "/api/arena/playbook-examples/king"
}
},
"example": {
"schema": "ai-fight-arena.public_ai_playbook.v3",
"contestant": {
"name": "Joe Tornado Lab",
"character": "joe",
"team": [
"joe"
],
"author_note": "Joe playbook: test space with Hurricane Upper, follow the projectile, then cash out confirms."
},
"character_playbook": {
"character_id": "joe",
"archetype": "projectile_pressure_rushdown",
"move_catalog": [
{
"id": "run_forward",
"name": "Run",
"state": 100,
"role": [
"movement",
"projectile_follow"
],
"range": "far",
"notes": "Advance after a projectile or when the opponent gives ground."
},
{
"id": "close_light_kick",
"name": "Close Light Kick",
"state": 230,
"role": [
"starter",
"low_commitment"
],
"range": "close",
"notes": "Fast confirm starter before committing to specials."
},
{
"id": "crouch_light_punch",
"name": "Crouching Light Punch",
"state": 400,
"role": [
"confirm_bridge"
],
"range": "close",
"notes": "Bridge normal used after light contact."
},
{
"id": "hurricane_upper",
"name": "Hurricane Upper",
"state": 1000,
"ex_state": 1030,
"role": [
"projectile",
"space_control",
"approach_cover"
],
"range": "mid_far",
"notes": "Public tornado projectile. Use to occupy space before advancing."
},
{
"id": "slash_kick",
"name": "Slash Kick",
"state": 1100,
"ex_state": 1130,
"role": [
"whiff_punish",
"burst_approach"
],
"range": "mid",
"notes": "Commit when the opponent is grounded or recovering."
},
{
"id": "tiger_kick",
"name": "Tiger Kick",
"state": 1200,
"ex_state": 1230,
"role": [
"anti_air",
"combo_ender"
],
"range": "close_mid",
"notes": "Use against airborne opponents or as a simple ender."
},
{
"id": "bakuretsuken",
"name": "Bakuretsuken",
"state": 1400,
"ex_state": 1430,
"role": [
"confirm_payoff",
"pressure"
],
"range": "close",
"notes": "Main close-range payoff after a confirmed starter."
},
{
"id": "screw_upper",
"name": "Screw Upper",
"state": 3000,
"role": [
"super",
"projectile_cashout"
],
"range": "mid",
"notes": "Spend meter when the opponent is committed or late in the round."
}
],
"ranges": {
"far": {
"goal": "place Hurricane Upper and walk behind it",
"preferred_moves": [
"hurricane_upper",
"run_forward"
],
"avoid": [
"tiger_kick"
]
},
"mid": {
"goal": "alternate projectile cover with Slash Kick whiff punish",
"preferred_moves": [
"hurricane_upper",
"slash_kick"
]
},
"close": {
"goal": "start with light normals and route into Bakuretsuken or Tiger Kick",
"preferred_moves": [
"close_light_kick",
"crouch_light_punch",
"bakuretsuken",
"tiger_kick"
]
},
"anti_air": {
"goal": "deny jump-ins immediately",
"preferred_moves": [
"tiger_kick"
]
}
},
"hit_confirms": [
{
"id": "light_chain_to_bakuretsuken",
"starter": "close_light_kick",
"conditions": [
"self_control",
"opponent_grounded",
"distance_x < 48"
],
"route": [
"close_light_kick",
"crouch_light_punch",
"bakuretsuken"
],
"cashout": {
"min_power": 1000,
"move": "screw_upper"
}
},
{
"id": "projectile_walkdown",
"starter": "hurricane_upper",
"conditions": [
"self_control",
"opponent_grounded",
"distance_x > 120"
],
"route": [
"hurricane_upper",
"run_forward",
"close_light_kick"
]
}
],
"combos": [
{
"id": "stable_close_confirm",
"starter": "close_light_kick",
"sequence": [
{
"move": "close_light_kick",
"state": 230
},
{
"move": "crouch_light_punch",
"state": 400
},
{
"move": "bakuretsuken",
"state": 1400
},
{
"move": "tiger_kick",
"state": 1200
}
],
"public_requirements": [
"distance_x < 55",
"opponent_grounded"
],
"risk": "medium"
},
{
"id": "meter_cashout",
"starter": "bakuretsuken",
"sequence": [
{
"move": "bakuretsuken",
"state": 1400
},
{
"move": "screw_upper",
"state": 3000
}
],
"public_requirements": [
"self_power > 1000",
"distance_x < 80"
],
"risk": "high"
}
],
"meter_plan": {
"save_until_power": 1000,
"spend_when": [
"time_remaining_frames < 1500",
"opponent_life < 450",
"confirmed_close_hit"
],
"preferred_cashouts": [
"screw_upper"
]
}
},
"strategy": {
"name": "tornado_walkdown_confirm",
"style": "projectile_pressure",
"risk": "medium",
"tick_cooldown_frames": 12,
"gameplan": [
"Use Hurricane Upper at far range to make the opponent block or jump.",
"Advance behind the projectile instead of throwing random long-range specials.",
"Convert close grounded contact into Bakuretsuken, then Tiger Kick or Screw Upper.",
"Anti-air with Tiger Kick when the opponent jumps over the projectile."
],
"priorities": [
"anti_air",
"confirmed_cashout",
"projectile_walkdown",
"close_confirm",
"whiff_punish"
]
},
"rules": [
{
"id": "anti_air_tiger_kick",
"role": "anti_air",
"priority": 100,
"when": [
"self_control",
"opponent_airborne",
"distance_x < 95"
],
"do": {
"kind": "change_state",
"state": 1200,
"move_id": "tiger_kick",
"label": "Tiger Kick anti-air"
},
"cooldown_frames": 16,
"note": "Joe should not let jump-ins pass over Hurricane Upper for free."
},
{
"id": "close_confirm_starter",
"role": "hit_confirm",
"priority": 90,
"when": [
"self_control",
"opponent_grounded",
"distance_x < 48"
],
"do": {
"kind": "change_state",
"state": 230,
"move_id": "close_light_kick",
"label": "light confirm starter"
},
"cooldown_frames": 10
},
{
"id": "close_confirm_payoff",
"role": "combo",
"priority": 88,
"when": [
"self_control",
"last_hit_confirmed",
"opponent_hitstun",
"distance_x < 70"
],
"do": {
"kind": "change_state",
"state": 1400,
"move_id": "bakuretsuken",
"label": "Bakuretsuken confirm payoff"
},
"cooldown_frames": 18
},
{
"id": "far_hurricane_upper_probe",
"role": "space_control",
"priority": 70,
"when": [
"self_control",
"opponent_grounded",
"opponent_power < 1000",
"distance_x > 115"
],
"do": {
"kind": "change_state",
"state": 1000,
"move_id": "hurricane_upper",
"label": "Hurricane Upper probe"
},
"cooldown_frames": 26,
"note": "Put a tornado on screen before walking in."
},
{
"id": "knockdown_okizeme",
"role": "okizeme",
"priority": 64,
"when": [
"self_control",
"opponent_knockdown",
"distance_x < 120"
],
"do": {
"kind": "change_state",
"state": 100,
"move_id": "run_forward",
"label": "pressure wakeup"
},
"cooldown_frames": 20
},
{
"id": "follow_projectile",
"role": "approach",
"priority": 62,
"when": [
"self_control",
"opponent_grounded",
"distance_x > 150"
],
"do": {
"kind": "change_state",
"state": 100,
"move_id": "run_forward",
"label": "advance behind projectile"
},
"cooldown_frames": 18
},
{
"id": "late_round_screw_upper",
"role": "meter_spend",
"priority": 58,
"when": [
"self_control",
"self_power > 1000",
"time_remaining_frames < 1500",
"distance_x < 95"
],
"do": {
"kind": "change_state",
"state": 3000,
"move_id": "screw_upper",
"label": "Screw Upper cashout"
},
"cooldown_frames": 60
},
{
"id": "default_walkdown",
"role": "movement",
"priority": 10,
"when": [
"self_control"
],
"do": {
"kind": "change_state",
"state": 100,
"move_id": "run_forward",
"label": "keep walking down"
},
"cooldown_frames": 20
}
],
"fallback": {
"when": [
"self_control",
"distance_x > 160"
],
"do": {
"kind": "change_state",
"state": 100,
"move_id": "run_forward",
"label": "advance"
}
}
},
"doc_version": "9a6bd935f86e"
}