{
  "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:<your fastest punish>}}. 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:<hard punish>}}. 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"
}