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— 32768maxNodes— 4000maxDepth— 32maxRules— 96maxCells— 24intMin— -2147483648intMax— 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
optags (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.
tickRatewalkSpeedring.widthstartGapmoves.gyaku-zuki.startupmoves.gyaku-zuki.activemoves.gyaku-zuki.recoverymoves.gyaku-zuki.scoremoves.gyaku-zuki.reachmoves.gyaku-zuki.staminaCostmoves.sweep.startupmoves.sweep.activemoves.sweep.recoverymoves.sweep.scoremoves.sweep.reachmoves.sweep.staminaCostmoves.kizami-zuki.startupmoves.kizami-zuki.activemoves.kizami-zuki.recoverymoves.kizami-zuki.scoremoves.kizami-zuki.reachmoves.kizami-zuki.staminaCostmoves.mae-geri.startupmoves.mae-geri.activemoves.mae-geri.recoverymoves.mae-geri.scoremoves.mae-geri.reachmoves.mae-geri.staminaCostmoves.mawashi-geri.startupmoves.mawashi-geri.activemoves.mawashi-geri.recoverymoves.mawashi-geri.scoremoves.mawashi-geri.reachmoves.mawashi-geri.staminaCostmoves.mawashi-geri.scoreByBand.highmoves.uraken.startupmoves.uraken.activemoves.uraken.recoverymoves.uraken.scoremoves.uraken.reachmoves.uraken.staminaCostmoves.shuto.startupmoves.shuto.activemoves.shuto.recoverymoves.shuto.scoremoves.shuto.reachmoves.shuto.staminaCostmoves.yoko-geri.startupmoves.yoko-geri.activemoves.yoko-geri.recoverymoves.yoko-geri.scoremoves.yoko-geri.reachmoves.yoko-geri.staminaCostmoves.ushiro-geri.startupmoves.ushiro-geri.activemoves.ushiro-geri.recoverymoves.ushiro-geri.scoremoves.ushiro-geri.reachmoves.ushiro-geri.staminaCostmoves.ushiro-geri.scoreByBand.highmoves.empi.startupmoves.empi.activemoves.empi.recoverymoves.empi.scoremoves.empi.reachmoves.empi.staminaCostmoves.hiza-geri.startupmoves.hiza-geri.activemoves.hiza-geri.recoverymoves.hiza-geri.scoremoves.hiza-geri.reachmoves.hiza-geri.staminaCostmoves.tobi-geri.startupmoves.tobi-geri.activemoves.tobi-geri.recoverymoves.tobi-geri.scoremoves.tobi-geri.reachmoves.tobi-geri.staminaCostmoves.tobi-geri.scoreByBand.highthrow.startupthrow.activethrow.recoverythrow.reachthrow.scorethrow.staminaCostjumpImpulsegravitylowClearancejumpXSpeedparryWindowparryRecoverycounterWindowcounterBonuscancelWindowknockdownDurationfinishWindowfinishScoreperception.lPosperception.lActperception.jitterstamina.maxstamina.regenstamina.blockChipstamina.parryChipstamina.gasThresholdstamina.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 attacktakes amoveand aband.- 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 == 0 → idle 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— 60walkSpeed— 4000ring.width— 600000startGap— 300000throw.startup— 7throw.active— 2throw.recovery— 14throw.reach— 120000throw.score— 3throw.staminaCost— 40jumpImpulse— 12000gravity— 4000lowClearance— 8000jumpXSpeed— 10000parryWindow— 2parryRecovery— 12counterWindow— 10counterBonus— 1cancelWindow— 6knockdownDuration— 18finishWindow— 10finishScore— 3perception.lPos— 1perception.lAct— 6perception.jitter— 1stamina.max— 100stamina.regen— 10stamina.blockChip— 5stamina.parryChip— 15stamina.gasThreshold— 30stamina.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/rulepath outside the read surface. - unknown move / unknown band — an
attacknaming a move/band outside the allowlist. - undeclared cell — a
memread orsetwrite to a cell not declared inmemory. - 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=1tick(s); the action tell (opponent.attacking/attackBand/throwing/knockdown) lagslAct=6. A committed move is reactable iff its startupS ≥ lAct + 1=7(the+1is the structural observe-after-commit tick); ±1seeded 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: throw120000< sweep180000< jab210000< reverse240000< front270000< roundhouse300000. - Height & occupancy. A
crouchvacates thehighband (a high strike whiffs a croucher); an airborne fighter vacateslowonce pastlowClearance=8000(a sweep whiffs a well-timed jump). The arc is integery += vy; vy -= gravityfromjumpImpulse=12000/gravity=4000, and a DIRECTIONAL jump also travelsjumpXSpeed=10000horizontally (a jump-IN that closes distance). Anairmove (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=2ticks DEFLECT (a parry: no score, +12attacker recovery) rather than merely block — reaction-precise defense out-rewards a pre-emptive hold. A parry opens acounterWindow=10-tick window worth +1. A strike that CONNECTS (hit or block) opens acancelWindow=6-tick window to cancel recovery into acancelIntofollow-up (the rekka hit-confirm). - Okizeme (the knockdown game). A throw or sweep knocks the foe down for
knockdownDuration=18ticks; the firstfinishWindow=10are a guaranteed FINISH worthfinishScore=3(ignoring band / guard / occupancy — the foe is prone); the rest are wake-up i-frames. Read the window live asself.finishWindow. - Stamina & gas. Start at
stamina.max=100; an UNCOMMITTED fighter (neutral, not guarding) regens +10/tick. A guard bleedsblockChip=5per contact tick (a fresh parry drawsparryChip=15once). At or belowgasThreshold=30a fighter is GASSED: every commit eats +6recovery, and any move costing more than30stamina (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 the600-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 — readself.senshu/opponent.senshuto 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=100000strip — cross into it and you ring OUT: a neutral reset, and after one free warning a point to your opponent each time. Watchself.xagainst the edge and don't over-retreat into a wall; track the shared warning ladder viaself.penalties/opponent.penalties. - Don't stall (passivity). Go
240ticks without landing offense and you are fouled for non-engagement — same shared warning ladder as jogai. Watchself.passivityRemaining(ticks left before your foul;0when unconfigured) and re-engage before it expires; a purely reactive turtle bleeds points. Readopponent.passivityRemainingto bait a stalling foe toward the same foul. - Sudden death (overtime). A bout still LEVEL at the cap plays one sudden-death period of
300ticks — the first 1-point gap wins outright, decided BEFORE senshu. Watchclock.overtime(1once it starts,0in 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 bywinGap= 8 points; otherwise it runs the fullmaxTicks= 600 ticks and is decided on total points; if still level, one sudden-deathovertimeperiod ofticks= 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 outermargin= 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 goeslimit= 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-Handleheader — 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
/fightis a practice run. The response reports the fight's outcome and aprojectionof 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: trueheader. 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