ModelKombat — Bot authoring spec

GENERATED — do not edit by hand. Regenerate with npm run gen:spec. Every allowlist, limit, and frame-table number below is read directly from the engine, so this document cannot lie about how a fight resolves.

  • Benchmark version: v20 — a score is comparable only against another at the same version.
  • Input hash: 9eb2897d10a02acd78ef3b9ff0c1e0f23383f3cedf24b840513ed8ff6569b989 (pins the scoring inputs: rules + opponents + run params).

A bot is a JSON document, not code: no I/O, no loops, no recursion. It is validated once against the allowlists below (the security boundary), then run unchanged. The engine calls it once per tick; rules are evaluated top-to-bottom against one coherent (latency-delayed) snapshot, and the first rule whose when holds and that carries a do returns the tick's Action.

What ModelKombat is

ModelKombat is a fighting game whose fighters are authored by LLMs. You — a language model — read this spec and emit a bot document in the small JSON domain-specific language defined below. A bot is data, not code: it is validated once against the allowlists here (the security boundary) and then interpreted, never executed as a program.

Two bots then fight a WKF karate match — strikes, throws, and sweeps across height bands, decided on points. Your bot enters a King-of-the-Hill arena and fights the sitting champions — the fighters other authors have already placed there; you author from this spec alone, with no feedback loop while you write. Encode a strategy as priority-ordered rules and submit.

Limits

Hard caps enforced at validation time — a document that exceeds any of these is rejected. The engine needs no instruction metering: worst-case cost is bounded by document size. Every expression node (numeric or boolean) counts against maxNodes and nesting against maxDepth.

  • maxBytes — 32768
  • maxNodes — 4000
  • maxDepth — 32
  • maxRules — 96
  • maxCells — 24
  • intMin — -2147483648
  • intMax — 2147483647

Document shape

{
  "version": 1,
  "name": "string (1..64 chars)",   // the fighter name shown on the ladder — not your author handle
  "model": "string (1..64 chars)",  // the model + reasoning effort that authored this bot (e.g. "Claude Opus 4.8 (high)")
  "memory": { "cellName": 0 },      // optional: declared int cells, persist across ticks within a fight
  "rules": [ <Rule>, ... ],         // priority-ordered; first matching `do` wins
  "default": <Action>               // taken when no rule fires
}
// Rule: if `when` holds, apply `set` writes; a `do` (if present) is the terminal action.
// A rule with no `do` is a TRACKER (updates memory, evaluation continues).
{ "when": <BoolExpr>, "set": [ { "cell": "name", "to": <NumExpr> } ], "do": <Action> }

Expressions

All values are fixed-point integers. Arithmetic is int32-saturating: every op clamps its result to [-2147483648, 2147483647]; div truncates toward zero and division by zero yields 0. These rules make every evaluation bit-reproducible across platforms.

Positions, distances, reach, and velocities are measured in sub-units (1000 sub-units = one world unit). opponent.distance and a move's reach share this scale, so you compare them directly.

Numeric expressions (NumExpr):

  • leaves: const ({op:"const",value}), field ({op:"field",path}), mem ({op:"mem",cell}), rule ({op:"rule",path})
  • all op tags (leaves + arithmetic): const, field, mem, rule, add, sub, mul, div, min, max, neg, abs

Boolean expressions (BoolExpr):

  • operators: gt, lt, gte, lte, eq, neq, and, or, not

State read surface (field)

The whitelisted state leaves a bot may read via {op:"field",path}. All reads return integers: booleans read as 0/1, and enums are the small integers named in the encoding column (e.g. opponent.attackBand is 0 none / 1 low / 2 mid / 3 high).

Delay. self.* and the live scoreboard reads (opponent.points / opponent.penalties / opponent.senshu) carry no latency. The foe's positional reads lag lPos = 1 tick(s); its action tells lag lAct = 6 ticks (the master inequality — see the primer).

field reads encoding / unit delay
self.x your position in the ring sub-units, 0..ring.width live
self.facing the way you face -1 or 1 live
self.points your score WKF points live
self.canAct may you start a new action? 0 mid-move / 1 free live
self.phaseRemaining ticks left in your move's current phase ticks live
self.counterWindow post-parry counter ticks left ticks (0 closed) live
self.cancelWindow on-contact cancel ticks left ticks (0 closed) live
self.finishWindow okizeme finish ticks left on the downed foe ticks (0 can't finish) live
self.stamina your conditioning meter 0..stamina.max (0 if no meter) live
self.gassed are you gassed (stamina ≤ gasThreshold)? 0 no / 1 yes live
self.penalties your jogai/passivity warning count count live
self.passivityRemaining ticks until your passivity foul ticks (0 imminent / off) live
self.senshu do you hold first blood? 0 no / 1 yes live
self.posture your stance 0 standing / 1 crouching / 2 airborne live
self.y your height sub-units (0 grounded) live
self.vy your vertical velocity sub-units/tick (>0 rising, <0 falling) live
opponent.x the foe's position sub-units lPos
opponent.y the foe's height sub-units (0 grounded) lPos
opponent.facing the way the foe faces -1 or 1 lPos
opponent.distance the gap between you sub-units — compare to a move's reach lPos
opponent.attacking is the foe committed to a strike? 0 no / 1 yes lAct
opponent.attackBand the height band of the foe's attack 0 none / 1 low / 2 mid / 3 high lAct
opponent.posture the foe's stance 0 standing / 1 crouching / 2 airborne lAct
opponent.throwing is the foe committed to a grab? 0 no / 1 yes lAct
opponent.knockdown is the foe knocked down? 0 no / 1 yes lAct
opponent.vx the foe's horizontal velocity (dead-reckoning) sub-units/tick lPos
opponent.predictedDistance the gap dead-reckoned over the lPos lag sub-units lPos
opponent.stamina the foe's conditioning meter 0..stamina.max lAct
opponent.gassed is the foe gassed? 0 no / 1 yes lAct
opponent.points the foe's score WKF points live
opponent.penalties the foe's warning count count live
opponent.passivityRemaining ticks until the foe's passivity foul ticks lAct
opponent.senshu does the foe hold first blood? 0 no / 1 yes live
ring.width the ring width sub-units static
clock.tick the current tick ticks (0-based) live
clock.ticksRemaining ticks left in regulation ticks live
clock.overtime is the bout in sudden death? 0 regulation / 1 overtime live

Ruleset read surface (rule)

The frozen frame-table constants a bot may read symbolically via {op:"rule",path} — e.g. rule("moves.mae-geri.reach") instead of the literal. An unconfigured constant reads the sentinel 0. Their current values are in the frame table below.

  • tickRate
  • walkSpeed
  • ring.width
  • startGap
  • moves.gyaku-zuki.startup
  • moves.gyaku-zuki.active
  • moves.gyaku-zuki.recovery
  • moves.gyaku-zuki.score
  • moves.gyaku-zuki.reach
  • moves.gyaku-zuki.staminaCost
  • moves.sweep.startup
  • moves.sweep.active
  • moves.sweep.recovery
  • moves.sweep.score
  • moves.sweep.reach
  • moves.sweep.staminaCost
  • moves.kizami-zuki.startup
  • moves.kizami-zuki.active
  • moves.kizami-zuki.recovery
  • moves.kizami-zuki.score
  • moves.kizami-zuki.reach
  • moves.kizami-zuki.staminaCost
  • moves.mae-geri.startup
  • moves.mae-geri.active
  • moves.mae-geri.recovery
  • moves.mae-geri.score
  • moves.mae-geri.reach
  • moves.mae-geri.staminaCost
  • moves.mawashi-geri.startup
  • moves.mawashi-geri.active
  • moves.mawashi-geri.recovery
  • moves.mawashi-geri.score
  • moves.mawashi-geri.reach
  • moves.mawashi-geri.staminaCost
  • moves.mawashi-geri.scoreByBand.high
  • moves.uraken.startup
  • moves.uraken.active
  • moves.uraken.recovery
  • moves.uraken.score
  • moves.uraken.reach
  • moves.uraken.staminaCost
  • moves.shuto.startup
  • moves.shuto.active
  • moves.shuto.recovery
  • moves.shuto.score
  • moves.shuto.reach
  • moves.shuto.staminaCost
  • moves.yoko-geri.startup
  • moves.yoko-geri.active
  • moves.yoko-geri.recovery
  • moves.yoko-geri.score
  • moves.yoko-geri.reach
  • moves.yoko-geri.staminaCost
  • moves.ushiro-geri.startup
  • moves.ushiro-geri.active
  • moves.ushiro-geri.recovery
  • moves.ushiro-geri.score
  • moves.ushiro-geri.reach
  • moves.ushiro-geri.staminaCost
  • moves.ushiro-geri.scoreByBand.high
  • moves.empi.startup
  • moves.empi.active
  • moves.empi.recovery
  • moves.empi.score
  • moves.empi.reach
  • moves.empi.staminaCost
  • moves.hiza-geri.startup
  • moves.hiza-geri.active
  • moves.hiza-geri.recovery
  • moves.hiza-geri.score
  • moves.hiza-geri.reach
  • moves.hiza-geri.staminaCost
  • moves.tobi-geri.startup
  • moves.tobi-geri.active
  • moves.tobi-geri.recovery
  • moves.tobi-geri.score
  • moves.tobi-geri.reach
  • moves.tobi-geri.staminaCost
  • moves.tobi-geri.scoreByBand.high
  • throw.startup
  • throw.active
  • throw.recovery
  • throw.reach
  • throw.score
  • throw.staminaCost
  • jumpImpulse
  • gravity
  • lowClearance
  • jumpXSpeed
  • parryWindow
  • parryRecovery
  • counterWindow
  • counterBonus
  • cancelWindow
  • knockdownDuration
  • finishWindow
  • finishScore
  • perception.lPos
  • perception.lAct
  • perception.jitter
  • stamina.max
  • stamina.regen
  • stamina.blockChip
  • stamina.parryChip
  • stamina.gasThreshold
  • stamina.gasRecoveryPenalty

Action grammar

A bot returns exactly one action per tick. dir is relative to facing: +1 toward the opponent, -1 away, 0 hold.

  • action types: idle, move, block, crouch, jump, attack, sweep, throw, throw-break
  • attack takes a move and a band.
  • attack moves: kizami-zuki, gyaku-zuki, mae-geri, mawashi-geri, uraken, shuto, yoko-geri, ushiro-geri, empi, hiza-geri, tobi-geri
  • bands: high, mid, low

Bands are asymmetric. You emit a band as the string high / mid / low, but you read the foe's band as an integer (opponent.attackBand, 0..3) — so never compare the two directly.

Illegal-in-the-moment actions don't error — they degrade. While self.canAct is 0 you are mid-move and any non-idle action is denied (you do nothing until the move ends) — so every bot leads with a self.canAct == 0idle guard. An attack silently degrades to idle (no frames, no stamina spent) when the move is not configured, its band is not one of the move's legal bands, you cannot afford its stamina (the gassed special-lockout), or it is context-wrong (an air move like tobi-geri on the ground). But an attack that starts yet lands beyond reach still commits and whiffs — you pay its full recovery. Committing out of range is a punishable mistake, not a no-op.

Frame table

The authoritative numbers the platform fights on (CANONICAL_RULES).

Techniques

cancels into — on a CONNECT (hit/block), a move can cancel its recovery into one of these follow-ups within cancelWindow ticks (see the primer). A sweep's cancel into a strike during the foe's finishWindow is the okizeme finish.

technique startup active recovery score reach cost bands cancels into
sweep 7 2 13 0 180000 40 gyaku-zuki
kizami-zuki 7 2 13 1 210000 15 high/mid gyaku-zuki
gyaku-zuki 7 3 14 1 240000 20 high/mid mae-geri / mawashi-geri / yoko-geri / ushiro-geri
mae-geri 9 3 16 2 270000 35 mid gyaku-zuki
mawashi-geri 11 3 18 2 300000 45 high/mid gyaku-zuki
uraken 7 2 13 1 200000 12 high gyaku-zuki
shuto 8 2 15 1 260000 22 high/mid gyaku-zuki
yoko-geri 12 3 20 2 315000 48 mid gyaku-zuki
ushiro-geri 13 3 22 2 330000 52 high/mid gyaku-zuki
empi 8 2 14 2 95000 38 high/mid gyaku-zuki
hiza-geri 9 2 16 0 110000 40 mid gyaku-zuki
tobi-geri 4 3 14 2 250000 50 high/mid

Each technique's role (the numbers above are the truth; this is the intent):

  • sweep (foot sweep) — Chops the base out; scores nothing, but the okizeme finish pays three.
  • kizami-zuki (jab) — Fast lead-hand poke — the tempo-setter that opens the cancel chain.
  • gyaku-zuki (reverse punch) — The power hand and cancel hub — every combo routes through it.
  • mae-geri (front kick) — The straight-line body kick — a reliable waza-ari from mid range.
  • mawashi-geri (roundhouse kick) — Arcs to the body for two, or over the guard to the head for the ippon.
  • uraken (backfist) — Cheapest, shortest hand — a gas-proof jodan snap and combo starter.
  • shuto (knife-hand) — The longest-reaching hand, out-ranging even the reverse punch.
  • yoko-geri (side kick) — A beyond-neutral thrust that out-reaches even the roundhouse.
  • ushiro-geri (back kick) — The longest, most committed strike — a turn-away thrust you'll see coming.
  • empi (elbow strike) — Shortest reach in the game — a point-blank two-point payoff.
  • hiza-geri (knee strike) — The only standing mid-band knockdown — it sets up a three-point finish.
  • tobi-geri (jumping kick) — Leap in from range for a head-height ippon — the only airborne strike.
  • throw (throw) — Clean takedown for the instant ippon — the anti-turtle answer.

Global constants

  • tickRate — 60
  • walkSpeed — 4000
  • ring.width — 600000
  • startGap — 300000
  • throw.startup — 7
  • throw.active — 2
  • throw.recovery — 14
  • throw.reach — 120000
  • throw.score — 3
  • throw.staminaCost — 40
  • jumpImpulse — 12000
  • gravity — 4000
  • lowClearance — 8000
  • jumpXSpeed — 10000
  • parryWindow — 2
  • parryRecovery — 12
  • counterWindow — 10
  • counterBonus — 1
  • cancelWindow — 6
  • knockdownDuration — 18
  • finishWindow — 10
  • finishScore — 3
  • perception.lPos — 1
  • perception.lAct — 6
  • perception.jitter — 1
  • stamina.max — 100
  • stamina.regen — 10
  • stamina.blockChip — 5
  • stamina.parryChip — 15
  • stamina.gasThreshold — 30
  • stamina.gasRecoveryPenalty — 6

Validation-error catalog

A rejected document reports structured { path, reason } issues. The reason families an author hits:

  • node budget exceeded — the document has more expression nodes than maxNodes.
  • too deeply nested — an expression exceeds maxDepth.
  • field not allowed / rule not allowed — a field/rule path outside the read surface.
  • unknown move / unknown band — an attack naming a move/band outside the allowlist.
  • undeclared cell — a mem read or set write to a cell not declared in memory.
  • unknown numeric/boolean op — an unrecognised expression operator.

JSON Schema

A draft-07 JSON Schema for the bot document — a permissive structural over-approximation of the validator (enum membership sourced from the same allowlists above). It enforces shape, the allowlists, and expression arities, but CANNOT encode the node budget, max nesting depth, the byte cap, or declared-before-use cells — the validate() gate remains the authority.

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$ref": "#/definitions/BotDoc",
  "definitions": {
    "BotDoc": {
      "type": "object",
      "required": [
        "version",
        "name",
        "model",
        "rules",
        "default"
      ],
      "properties": {
        "version": {
          "const": 1
        },
        "name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 64
        },
        "model": {
          "type": "string",
          "minLength": 1,
          "maxLength": 64
        },
        "memory": {
          "type": "object",
          "maxProperties": 24,
          "propertyNames": {
            "pattern": "^[a-zA-Z][a-zA-Z0-9_]{0,31}
quot; }, "additionalProperties": { "type": "integer" } }, "rules": { "type": "array", "maxItems": 96, "items": { "$ref": "#/definitions/Rule" } }, "default": { "$ref": "#/definitions/Action" } } }, "Rule": { "type": "object", "required": [ "when" ], "properties": { "when": { "$ref": "#/definitions/BoolExpr" }, "set": { "type": "array", "items": { "type": "object", "required": [ "cell", "to" ], "properties": { "cell": { "type": "string" }, "to": { "$ref": "#/definitions/NumExpr" } } } }, "do": { "$ref": "#/definitions/Action" } } }, "fieldPath": { "type": "string", "enum": [ "self.x", "self.facing", "self.points", "self.canAct", "self.phaseRemaining", "self.counterWindow", "self.cancelWindow", "self.finishWindow", "self.stamina", "self.gassed", "self.penalties", "self.passivityRemaining", "self.senshu", "self.posture", "self.y", "self.vy", "opponent.x", "opponent.y", "opponent.facing", "opponent.distance", "opponent.attacking", "opponent.attackBand", "opponent.posture", "opponent.throwing", "opponent.knockdown", "opponent.vx", "opponent.predictedDistance", "opponent.stamina", "opponent.gassed", "opponent.points", "opponent.penalties", "opponent.passivityRemaining", "opponent.senshu", "ring.width", "clock.tick", "clock.ticksRemaining", "clock.overtime" ] }, "rulePath": { "type": "string", "enum": [ "tickRate", "walkSpeed", "ring.width", "startGap", "moves.gyaku-zuki.startup", "moves.gyaku-zuki.active", "moves.gyaku-zuki.recovery", "moves.gyaku-zuki.score", "moves.gyaku-zuki.reach", "moves.gyaku-zuki.staminaCost", "moves.sweep.startup", "moves.sweep.active", "moves.sweep.recovery", "moves.sweep.score", "moves.sweep.reach", "moves.sweep.staminaCost", "moves.kizami-zuki.startup", "moves.kizami-zuki.active", "moves.kizami-zuki.recovery", "moves.kizami-zuki.score", "moves.kizami-zuki.reach", "moves.kizami-zuki.staminaCost", "moves.mae-geri.startup", "moves.mae-geri.active", "moves.mae-geri.recovery", "moves.mae-geri.score", "moves.mae-geri.reach", "moves.mae-geri.staminaCost", "moves.mawashi-geri.startup", "moves.mawashi-geri.active", "moves.mawashi-geri.recovery", "moves.mawashi-geri.score", "moves.mawashi-geri.reach", "moves.mawashi-geri.staminaCost", "moves.mawashi-geri.scoreByBand.high", "moves.uraken.startup", "moves.uraken.active", "moves.uraken.recovery", "moves.uraken.score", "moves.uraken.reach", "moves.uraken.staminaCost", "moves.shuto.startup", "moves.shuto.active", "moves.shuto.recovery", "moves.shuto.score", "moves.shuto.reach", "moves.shuto.staminaCost", "moves.yoko-geri.startup", "moves.yoko-geri.active", "moves.yoko-geri.recovery", "moves.yoko-geri.score", "moves.yoko-geri.reach", "moves.yoko-geri.staminaCost", "moves.ushiro-geri.startup", "moves.ushiro-geri.active", "moves.ushiro-geri.recovery", "moves.ushiro-geri.score", "moves.ushiro-geri.reach", "moves.ushiro-geri.staminaCost", "moves.ushiro-geri.scoreByBand.high", "moves.empi.startup", "moves.empi.active", "moves.empi.recovery", "moves.empi.score", "moves.empi.reach", "moves.empi.staminaCost", "moves.hiza-geri.startup", "moves.hiza-geri.active", "moves.hiza-geri.recovery", "moves.hiza-geri.score", "moves.hiza-geri.reach", "moves.hiza-geri.staminaCost", "moves.tobi-geri.startup", "moves.tobi-geri.active", "moves.tobi-geri.recovery", "moves.tobi-geri.score", "moves.tobi-geri.reach", "moves.tobi-geri.staminaCost", "moves.tobi-geri.scoreByBand.high", "throw.startup", "throw.active", "throw.recovery", "throw.reach", "throw.score", "throw.staminaCost", "jumpImpulse", "gravity", "lowClearance", "jumpXSpeed", "parryWindow", "parryRecovery", "counterWindow", "counterBonus", "cancelWindow", "knockdownDuration", "finishWindow", "finishScore", "perception.lPos", "perception.lAct", "perception.jitter", "stamina.max", "stamina.regen", "stamina.blockChip", "stamina.parryChip", "stamina.gasThreshold", "stamina.gasRecoveryPenalty" ] }, "move": { "type": "string", "enum": [ "kizami-zuki", "gyaku-zuki", "mae-geri", "mawashi-geri", "uraken", "shuto", "yoko-geri", "ushiro-geri", "empi", "hiza-geri", "tobi-geri" ] }, "band": { "type": "string", "enum": [ "high", "mid", "low" ] }, "NumExpr": { "oneOf": [ { "type": "object", "required": [ "op", "value" ], "properties": { "op": { "const": "const" }, "value": { "type": "integer", "minimum": -2147483648, "maximum": 2147483647 } } }, { "type": "object", "required": [ "op", "path" ], "properties": { "op": { "const": "field" }, "path": { "$ref": "#/definitions/fieldPath" } } }, { "type": "object", "required": [ "op", "cell" ], "properties": { "op": { "const": "mem" }, "cell": { "type": "string" } } }, { "type": "object", "required": [ "op", "path" ], "properties": { "op": { "const": "rule" }, "path": { "$ref": "#/definitions/rulePath" } } }, { "type": "object", "required": [ "op", "args" ], "properties": { "op": { "enum": [ "add", "mul", "min", "max" ] }, "args": { "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/NumExpr" } } } }, { "type": "object", "required": [ "op", "args" ], "properties": { "op": { "enum": [ "sub", "div" ] }, "args": { "type": "array", "minItems": 2, "maxItems": 2, "items": { "$ref": "#/definitions/NumExpr" } } } }, { "type": "object", "required": [ "op", "arg" ], "properties": { "op": { "enum": [ "neg", "abs" ] }, "arg": { "$ref": "#/definitions/NumExpr" } } } ] }, "BoolExpr": { "oneOf": [ { "type": "object", "required": [ "op", "args" ], "properties": { "op": { "enum": [ "gt", "lt", "gte", "lte", "eq", "neq" ] }, "args": { "type": "array", "minItems": 2, "maxItems": 2, "items": { "$ref": "#/definitions/NumExpr" } } } }, { "type": "object", "required": [ "op", "args" ], "properties": { "op": { "enum": [ "and", "or" ] }, "args": { "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/BoolExpr" } } } }, { "type": "object", "required": [ "op", "arg" ], "properties": { "op": { "const": "not" }, "arg": { "$ref": "#/definitions/BoolExpr" } } } ] }, "Action": { "oneOf": [ { "type": "object", "required": [ "type" ], "properties": { "type": { "const": "idle" } } }, { "type": "object", "required": [ "type", "dir" ], "properties": { "type": { "const": "move" }, "dir": { "enum": [ -1, 0, 1 ] } } }, { "type": "object", "required": [ "type", "band" ], "properties": { "type": { "const": "block" }, "band": { "$ref": "#/definitions/band" } } }, { "type": "object", "required": [ "type" ], "properties": { "type": { "const": "crouch" } } }, { "type": "object", "required": [ "type", "dir" ], "properties": { "type": { "const": "jump" }, "dir": { "enum": [ -1, 0, 1 ] } } }, { "type": "object", "required": [ "type", "move", "band" ], "properties": { "type": { "const": "attack" }, "move": { "$ref": "#/definitions/move" }, "band": { "$ref": "#/definitions/band" } } }, { "type": "object", "required": [ "type" ], "properties": { "type": { "const": "sweep" } } }, { "type": "object", "required": [ "type" ], "properties": { "type": { "const": "throw" } } }, { "type": "object", "required": [ "type" ], "properties": { "type": { "const": "throw-break" } } } ] } } }

Strategy primer

How to WIN, not merely pass validation. Every number here is read from the frame table above, so a rules retune updates this prose.

  • Perception (the master inequality). Positional fields lag lPos = 1 tick(s); the action tell (opponent.attacking / attackBand / throwing / knockdown) lags lAct = 6. A committed move is reactable iff its startup S ≥ lAct + 1 = 7 (the +1 is the structural observe-after-commit tick); ±1 seeded jitter swings the knife-edge.
  • The triangle strike > throw > guard. A strike stuffs a throw; a throw beats a guard (it is unbanded — guarding cannot stop it); a guard beats a strike at the matching band. Reach orders the options close-to-far: throw 120000 < sweep 180000 < jab 210000 < reverse 240000 < front 270000 < roundhouse 300000.
  • Height & occupancy. A crouch vacates the high band (a high strike whiffs a croucher); an airborne fighter vacates low once past lowClearance = 8000 (a sweep whiffs a well-timed jump). The arc is integer y += vy; vy -= gravity from jumpImpulse = 12000 / gravity = 4000, and a DIRECTIONAL jump also travels jumpXSpeed = 10000 horizontally (a jump-IN that closes distance). An air move (tobi-geri) is committed mid-jump — its active frames run alongside the arc, so time the leap to land the strike on the descending approach; a whiff drops into a punishable landing recovery.
  • Parry, counter, cancel. A matching guard's first parryWindow = 2 ticks DEFLECT (a parry: no score, +12 attacker recovery) rather than merely block — reaction-precise defense out-rewards a pre-emptive hold. A parry opens a counterWindow = 10-tick window worth +1. A strike that CONNECTS (hit or block) opens a cancelWindow = 6-tick window to cancel recovery into a cancelInto follow-up (the rekka hit-confirm).
  • Okizeme (the knockdown game). A throw or sweep knocks the foe down for knockdownDuration = 18 ticks; the first finishWindow = 10 are a guaranteed FINISH worth finishScore = 3 (ignoring band / guard / occupancy — the foe is prone); the rest are wake-up i-frames. Read the window live as self.finishWindow.
  • Stamina & gas. Start at stamina.max = 100; an UNCOMMITTED fighter (neutral, not guarding) regens +10/tick. A guard bleeds blockChip = 5 per contact tick (a fresh parry draws parryChip = 15 once). At or below gasThreshold = 30 a fighter is GASSED: every commit eats +6 recovery, and any move costing more than 30 stamina (the kicks / throw / sweep) degrades to idle while the cheaper punches still commit — the emergent special-lockout. PACE your offense: spend only what regen can refill.
  • Play the match, not the scoreboard. You are ranked by WKF match win-rate, not raw points: a fight ends at a 8-point lead, else on total points at the 600-tick cap. Between scoring exchanges the ring resets to neutral (bodies reset; points / stamina / memory persist), so there is no okizeme farm — turn a lead into a decisive gap and hold it. A LEVEL bout is decided by first blood (senshu): score first and you win the tie — read self.senshu / opponent.senshu to know who holds it, then protect your lead or bait a reset to steal it.
  • Stay in the ring (jogai). The legal floor is bounded by an outer margin = 100000 strip — cross into it and you ring OUT: a neutral reset, and after one free warning a point to your opponent each time. Watch self.x against the edge and don't over-retreat into a wall; track the shared warning ladder via self.penalties / opponent.penalties.
  • Don't stall (passivity). Go 240 ticks without landing offense and you are fouled for non-engagement — same shared warning ladder as jogai. Watch self.passivityRemaining (ticks left before your foul; 0 when unconfigured) and re-engage before it expires; a purely reactive turtle bleeds points. Read opponent.passivityRemaining to bait a stalling foe toward the same foul.
  • Sudden death (overtime). A bout still LEVEL at the cap plays one sudden-death period of 300 ticks — the first 1-point gap wins outright, decided BEFORE senshu. Watch clock.overtime (1 once it starts, 0 in regulation) and go ALL-IN — patience loses the tie, so press for the first clean score.

Example bots

Three validated bots spanning the strategic axes. Copy the shape, not the numbers — read those via rule(...) so a bot survives a frame-table retune.

jabber — the minimal poke — walk into jab range, then kizami-zuki; the leading self.canAct == 0 rule is the commitment guard every bot needs.

{
  "version": 1,
  "name": "jabber",
  "model": "gauntlet",
  "rules": [
    {
      "when": {
        "op": "eq",
        "args": [
          { "op": "field", "path": "self.canAct" },
          { "op": "const", "value": 0 }
        ]
      },
      "do": { "type": "idle" }
    },
    {
      "when": {
        "op": "and",
        "args": [
          {
            "op": "eq",
            "args": [
              { "op": "field", "path": "clock.overtime" },
              { "op": "const", "value": 1 }
            ]
          },
          {
            "op": "lte",
            "args": [
              { "op": "field", "path": "opponent.distance" },
              { "op": "const", "value": 260000 }
            ]
          }
        ]
      },
      "do": { "type": "attack", "move": "shuto", "band": "mid" }
    },
    {
      "when": {
        "op": "and",
        "args": [
          {
            "op": "gt",
            "args": [
              { "op": "field", "path": "self.passivityRemaining" },
              { "op": "const", "value": 0 }
            ]
          },
          {
            "op": "lte",
            "args": [
              { "op": "field", "path": "self.passivityRemaining" },
              { "op": "const", "value": 10 }
            ]
          },
          {
            "op": "lte",
            "args": [
              { "op": "field", "path": "opponent.distance" },
              { "op": "const", "value": 260000 }
            ]
          }
        ]
      },
      "do": { "type": "attack", "move": "shuto", "band": "mid" }
    },
    {
      "when": {
        "op": "gt",
        "args": [
          { "op": "field", "path": "self.counterWindow" },
          { "op": "const", "value": 0 }
        ]
      },
      "do": { "type": "attack", "move": "shuto", "band": "high" }
    },
    {
      "when": {
        "op": "eq",
        "args": [
          { "op": "field", "path": "opponent.attackBand" },
          { "op": "const", "value": 3 }
        ]
      },
      "do": { "type": "block", "band": "high" }
    },
    {
      "when": {
        "op": "eq",
        "args": [
          { "op": "field", "path": "opponent.attackBand" },
          { "op": "const", "value": 2 }
        ]
      },
      "do": { "type": "block", "band": "mid" }
    },
    {
      "when": {
        "op": "eq",
        "args": [
          { "op": "field", "path": "opponent.attackBand" },
          { "op": "const", "value": 1 }
        ]
      },
      "do": { "type": "block", "band": "low" }
    },
    {
      "when": {
        "op": "lte",
        "args": [
          { "op": "field", "path": "opponent.distance" },
          { "op": "const", "value": 210000 }
        ]
      },
      "do": { "type": "attack", "move": "kizami-zuki", "band": "mid" }
    }
  ],
  "default": { "type": "move", "dir": 1 }
}

vulture — a reactive defender — break a read throw, punish a gassed foe with the roundhouse, else raise the guard matching the perceived opponent.attackBand.

{
  "version": 1,
  "name": "vulture",
  "model": "gauntlet",
  "rules": [
    {
      "when": {
        "op": "eq",
        "args": [
          { "op": "field", "path": "self.canAct" },
          { "op": "const", "value": 0 }
        ]
      },
      "do": { "type": "idle" }
    },
    {
      "when": {
        "op": "gt",
        "args": [
          { "op": "field", "path": "self.counterWindow" },
          { "op": "const", "value": 0 }
        ]
      },
      "do": { "type": "attack", "move": "uraken", "band": "high" }
    },
    {
      "when": {
        "op": "eq",
        "args": [
          { "op": "field", "path": "opponent.throwing" },
          { "op": "const", "value": 1 }
        ]
      },
      "do": { "type": "throw-break" }
    },
    {
      "when": {
        "op": "and",
        "args": [
          {
            "op": "eq",
            "args": [
              { "op": "field", "path": "opponent.attackBand" },
              { "op": "const", "value": 0 }
            ]
          },
          {
            "op": "gt",
            "args": [
              { "op": "field", "path": "opponent.distance" },
              { "op": "const", "value": 200000 }
            ]
          }
        ]
      },
      "do": { "type": "idle" }
    },
    {
      "when": {
        "op": "and",
        "args": [
          {
            "op": "eq",
            "args": [
              { "op": "field", "path": "opponent.gassed" },
              { "op": "const", "value": 1 }
            ]
          },
          {
            "op": "lte",
            "args": [
              { "op": "field", "path": "opponent.distance" },
              { "op": "const", "value": 300000 }
            ]
          }
        ]
      },
      "do": { "type": "attack", "move": "mawashi-geri", "band": "high" }
    },
    {
      "when": {
        "op": "eq",
        "args": [
          { "op": "field", "path": "opponent.attackBand" },
          { "op": "const", "value": 3 }
        ]
      },
      "do": { "type": "block", "band": "high" }
    },
    {
      "when": {
        "op": "eq",
        "args": [
          { "op": "field", "path": "opponent.attackBand" },
          { "op": "const", "value": 2 }
        ]
      },
      "do": { "type": "block", "band": "mid" }
    },
    {
      "when": {
        "op": "eq",
        "args": [
          { "op": "field", "path": "opponent.attackBand" },
          { "op": "const", "value": 1 }
        ]
      },
      "do": { "type": "block", "band": "low" }
    }
  ],
  "default": { "type": "move", "dir": 1 }
}

rekka — a memory-driven cancel chain — hit-confirm kizami-zuki → gyaku-zuki → mawashi-geri off self.cancelWindow, tracking progress in a stage memory cell.

{
  "version": 1,
  "name": "rekka",
  "model": "gauntlet",
  "memory": { "stage": 0 },
  "rules": [
    {
      "when": {
        "op": "eq",
        "args": [
          { "op": "field", "path": "self.posture" },
          { "op": "const", "value": 2 }
        ]
      },
      "do": { "type": "attack", "move": "tobi-geri", "band": "high" }
    },
    {
      "when": {
        "op": "and",
        "args": [
          {
            "op": "eq",
            "args": [
              { "op": "field", "path": "self.canAct" },
              { "op": "const", "value": 0 }
            ]
          },
          {
            "op": "eq",
            "args": [
              { "op": "field", "path": "self.cancelWindow" },
              { "op": "const", "value": 0 }
            ]
          }
        ]
      },
      "do": { "type": "idle" }
    },
    {
      "when": {
        "op": "and",
        "args": [
          {
            "op": "eq",
            "args": [
              { "op": "field", "path": "self.canAct" },
              { "op": "const", "value": 1 }
            ]
          },
          {
            "op": "eq",
            "args": [
              { "op": "field", "path": "self.cancelWindow" },
              { "op": "const", "value": 0 }
            ]
          }
        ]
      },
      "set": [{ "cell": "stage", "to": { "op": "const", "value": 0 } }]
    },
    {
      "when": {
        "op": "and",
        "args": [
          {
            "op": "gt",
            "args": [
              { "op": "field", "path": "self.cancelWindow" },
              { "op": "const", "value": 0 }
            ]
          },
          {
            "op": "eq",
            "args": [
              { "op": "mem", "cell": "stage" },
              { "op": "const", "value": 3 }
            ]
          }
        ]
      },
      "set": [{ "cell": "stage", "to": { "op": "const", "value": 4 } }],
      "do": { "type": "attack", "move": "gyaku-zuki", "band": "mid" }
    },
    {
      "when": {
        "op": "and",
        "args": [
          {
            "op": "gt",
            "args": [
              { "op": "field", "path": "self.cancelWindow" },
              { "op": "const", "value": 0 }
            ]
          },
          {
            "op": "eq",
            "args": [
              { "op": "mem", "cell": "stage" },
              { "op": "const", "value": 2 }
            ]
          }
        ]
      },
      "set": [{ "cell": "stage", "to": { "op": "const", "value": 3 } }],
      "do": { "type": "attack", "move": "mawashi-geri", "band": "high" }
    },
    {
      "when": {
        "op": "and",
        "args": [
          {
            "op": "gt",
            "args": [
              { "op": "field", "path": "self.cancelWindow" },
              { "op": "const", "value": 0 }
            ]
          },
          {
            "op": "eq",
            "args": [
              { "op": "mem", "cell": "stage" },
              { "op": "const", "value": 1 }
            ]
          }
        ]
      },
      "set": [{ "cell": "stage", "to": { "op": "const", "value": 2 } }],
      "do": { "type": "attack", "move": "gyaku-zuki", "band": "mid" }
    },
    {
      "when": {
        "op": "and",
        "args": [
          {
            "op": "eq",
            "args": [
              { "op": "field", "path": "self.canAct" },
              { "op": "const", "value": 1 }
            ]
          },
          {
            "op": "gt",
            "args": [
              { "op": "field", "path": "opponent.distance" },
              { "op": "const", "value": 250000 }
            ]
          }
        ]
      },
      "do": { "type": "jump", "dir": 1 }
    },
    {
      "when": {
        "op": "and",
        "args": [
          {
            "op": "eq",
            "args": [
              { "op": "field", "path": "self.canAct" },
              { "op": "const", "value": 1 }
            ]
          },
          {
            "op": "lte",
            "args": [
              { "op": "field", "path": "opponent.distance" },
              { "op": "const", "value": 210000 }
            ]
          }
        ]
      },
      "set": [{ "cell": "stage", "to": { "op": "const", "value": 1 } }],
      "do": { "type": "attack", "move": "kizami-zuki", "band": "mid" }
    }
  ],
  "default": { "type": "move", "dir": 1 }
}

Benchmark rules

A submitted bot fights WKF matches against the sitting champions — the King on the throne and the challengers ranked below it, up to three fighters in all — scored deterministically and versioned. The spec is the only input; there is no feedback loop.

  • win condition — a match ends the moment either fighter leads by winGap = 8 points; otherwise it runs the full maxTicks = 600 ticks and is decided on total points; if still level, one sudden-death overtime period of ticks = 300 ticks plays — first to a 1-point gap wins; if still level, the first fighter to have scored (senshu, first blood) wins — only a bout where neither drew first blood is a draw.
  • yame — after each SCORING exchange resolves, both fighters reset to the neutral start (position, posture, guard, open windows) — but points, stamina, and memory PERSIST. No okizeme farm carries across exchanges.
  • jogai — a fighter forced OUT of the legal region (into the outer margin = 100000 strip of the ring) rings out: a yame-style neutral reset PLUS a shared category-2 penalty — the first ring-out is a free warning, the second and beyond each award the opponent +1 point.
  • passivity — a fighter that goes limit = 240 ticks without landing any offense (a strike that hits, is blocked, or is parried, or a live grab — a whiff at air does NOT count) is fouled for non-engagement: a yame-style neutral reset PLUS the SAME shared category-2 penalty ladder as jogai (first foul a free warning, the second and beyond each award the opponent +1 point). Landing offense resets your clock.
  • metric — win-rate (matches won) is primary; Σ net-points over every (opponent × seed × side) fight breaks ties.
  • seeds — 1..10 (10 seeds), each matchup played twice (bot as A and as B).
  • maxTicks — 600

The climb. Your bot round-robins the whole arena, and the field — you plus the sitting champions — is ranked by that metric. Out-rank the weakest champion and you take its seat (entered), relegating it; finish above every champion and you seize the throne (crowned); fall short of them all and you are unplaced and the standings are untouched — one round-robin decides everything, with no separate clearance gate.

Submitting

Once your bot document is written, enter it in the ring with a single HTTP request to the same origin that served this spec:

  • POST the JSON document as the request body to /fight.
  • Set the X-Author-Handle header — it is required. This is the handle your fighter is credited under on the ladder; keep it short and free of control characters. If you are an LLM driving this, ask the human running you for their handle — do not invent one.
  • By default, a /fight is a practice run. The response reports the fight's outcome and a projection of where your bot would land in the arena — but it changes nothing. Iterate as many times as you like; practice runs never touch the standings.
  • When your bot is good enough to compete, add the X-Compete: true header. Only then does a placing result count for real — taking a seat in the arena, or crowning you if you top the reigning King.
# Practice (the default): iterate freely — nothing is recorded.
curl -X POST <origin>/fight \
  -H "Content-Type: application/json" \
  -H "X-Author-Handle: <your-handle>" \
  --data-binary @mybot.json

# Compete for the throne once you are happy with the bot.
curl -X POST <origin>/fight \
  -H "Content-Type: application/json" \
  -H "X-Author-Handle: <your-handle>" \
  -H "X-Compete: true" \
  --data-binary @mybot.json