JSON Schema (v1)

Canonical schema: https://opencoachingformat.org/schema/v1.json

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://opencoachingformat.org/schema/v1.json",
  "$comment": "Schema version 1.4.0. The $id stays v1 across all backwards-compatible 1.x releases; version-pinned copies live at /<version>/ocf-action-v1.json.",
  "x-ocf-version": "1.4.0",
  "title": "Open Coaching Format",
  "description": "Open standard for team-sport coaching diagrams and animations (invasion team sports; basketball first). Semantic action model.",
  "type": "object",
  "required": [
    "meta",
    "court",
    "entities",
    "frames"
  ],
  "definitions": {
    "ruleset": {
      "type": "string",
      "enum": [
        "fiba",
        "nba",
        "ncaa",
        "nfhs",
        "custom"
      ],
      "description": "Basketball ruleset. Determines unit, field dimensions and named position coordinates."
    },
    "unit": {
      "type": "string",
      "enum": [
        "m",
        "ft"
      ],
      "description": "Length unit used for all coordinates in this document. Derived from ruleset unless custom."
    },
    "coordinate_free": {
      "type": "object",
      "description": "Absolute coordinate in court units. Origin at center of court. x: negative=viewer's left, positive=viewer's right. y: positive=frontcourt (offense basket), negative=backcourt.",
      "required": [
        "x",
        "y"
      ],
      "properties": {
        "x": {
          "type": "number"
        },
        "y": {
          "type": "number"
        }
      },
      "additionalProperties": false
    },
    "coordinate_named": {
      "type": "object",
      "description": "Reference to a named court position resolved via the active ruleset. Custom positions use 'custom.' prefix.",
      "required": [
        "named"
      ],
      "properties": {
        "named": {
          "type": "string"
        }
      },
      "additionalProperties": false
    },
    "coordinate_relative": {
      "type": "object",
      "description": "Offset from a named court position, in court units.",
      "required": [
        "relative_to",
        "dx",
        "dy"
      ],
      "properties": {
        "relative_to": {
          "type": "string"
        },
        "dx": {
          "type": "number"
        },
        "dy": {
          "type": "number"
        }
      },
      "additionalProperties": false
    },
    "coordinate": {
      "description": "A single point: absolute, named, or relative.",
      "oneOf": [
        {
          "$ref": "#/definitions/coordinate_free"
        },
        {
          "$ref": "#/definitions/coordinate_named"
        },
        {
          "$ref": "#/definitions/coordinate_relative"
        }
      ]
    },
    "entity_ref": {
      "type": "string",
      "description": "Reference to a player, coach, cone or station. Format '{offense|defense}_{1-9}', 'coach', 'cone_N', 'station_N'.",
      "pattern": "^(offense|defense)_[1-9]$|^coach$|^(cone|station)_[1-9][0-9]*$"
    },
    "ball_ref": {
      "type": "string",
      "description": "Reference to a ball by id, e.g. 'ball_1'.",
      "pattern": "^ball_[1-9][0-9]*$"
    },
    "color_role": {
      "type": "string",
      "enum": [
        "offense",
        "defense",
        "black",
        "grey",
        "yellow",
        "green",
        "red",
        "blue",
        "white"
      ],
      "description": "Semantic color role. Resolved to hex via color_scheme."
    },
    "entity_offense": {
      "type": "object",
      "description": "Offensive player.",
      "required": [
        "type",
        "nr",
        "x",
        "y"
      ],
      "properties": {
        "type": {
          "type": "string",
          "const": "offense"
        },
        "nr": {
          "type": "integer",
          "minimum": 1,
          "maximum": 9
        },
        "x": {
          "type": "number"
        },
        "y": {
          "type": "number"
        },
        "rotation": {
          "type": "number",
          "minimum": 0,
          "maximum": 360,
          "default": 0
        },
        "color": {
          "$ref": "#/definitions/color_role",
          "default": "offense"
        },
        "label": {
          "type": "string"
        }
      },
      "additionalProperties": false
    },
    "entity_defense": {
      "type": "object",
      "description": "Defensive player.",
      "required": [
        "type",
        "nr",
        "x",
        "y"
      ],
      "properties": {
        "type": {
          "type": "string",
          "const": "defense"
        },
        "nr": {
          "type": "integer",
          "minimum": 1,
          "maximum": 9
        },
        "x": {
          "type": "number"
        },
        "y": {
          "type": "number"
        },
        "rotation": {
          "type": "number",
          "minimum": 0,
          "maximum": 360,
          "default": 0
        },
        "color": {
          "$ref": "#/definitions/color_role",
          "default": "defense"
        },
        "label": {
          "type": "string"
        }
      },
      "additionalProperties": false
    },
    "entity_coach": {
      "type": "object",
      "description": "Coach position marker. May carry a ball.",
      "required": [
        "type",
        "x",
        "y"
      ],
      "properties": {
        "type": {
          "type": "string",
          "const": "coach"
        },
        "x": {
          "type": "number"
        },
        "y": {
          "type": "number"
        }
      },
      "additionalProperties": false
    },
    "entity_cone": {
      "type": "object",
      "description": "Training cone / marker.",
      "required": [
        "type",
        "nr",
        "x",
        "y"
      ],
      "properties": {
        "type": {
          "type": "string",
          "const": "cone"
        },
        "nr": {
          "type": "integer",
          "minimum": 1
        },
        "x": {
          "type": "number"
        },
        "y": {
          "type": "number"
        }
      },
      "additionalProperties": false
    },
    "entity_station": {
      "type": "object",
      "description": "Numbered station for circuit training.",
      "required": [
        "type",
        "nr",
        "x",
        "y"
      ],
      "properties": {
        "type": {
          "type": "string",
          "const": "station"
        },
        "nr": {
          "type": "integer",
          "minimum": 1
        },
        "label": {
          "type": "string"
        },
        "x": {
          "type": "number"
        },
        "y": {
          "type": "number"
        }
      },
      "additionalProperties": false
    },
    "entity": {
      "description": "Any non-ball entity on the court.",
      "oneOf": [
        {
          "$ref": "#/definitions/entity_offense"
        },
        {
          "$ref": "#/definitions/entity_defense"
        },
        {
          "$ref": "#/definitions/entity_coach"
        },
        {
          "$ref": "#/definitions/entity_cone"
        },
        {
          "$ref": "#/definitions/entity_station"
        }
      ]
    },
    "ball": {
      "type": "object",
      "description": "A ball. Holds exactly one lifecycle state: carried_by, at, or dead.",
      "required": [
        "id"
      ],
      "properties": {
        "id": {
          "$ref": "#/definitions/ball_ref"
        },
        "carried_by": {
          "$ref": "#/definitions/entity_ref"
        },
        "at": {
          "$ref": "#/definitions/coordinate"
        },
        "dead": {
          "type": "boolean",
          "const": true
        }
      },
      "additionalProperties": false,
      "oneOf": [
        {
          "required": [
            "carried_by"
          ],
          "not": {
            "anyOf": [
              {
                "required": [
                  "at"
                ]
              },
              {
                "required": [
                  "dead"
                ]
              }
            ]
          }
        },
        {
          "required": [
            "at"
          ],
          "not": {
            "anyOf": [
              {
                "required": [
                  "carried_by"
                ]
              },
              {
                "required": [
                  "dead"
                ]
              }
            ]
          }
        },
        {
          "required": [
            "dead"
          ],
          "not": {
            "anyOf": [
              {
                "required": [
                  "carried_by"
                ]
              },
              {
                "required": [
                  "at"
                ]
              }
            ]
          }
        }
      ]
    },
    "ball_state": {
      "type": "object",
      "description": "A ball's lifecycle state inside a frame state, keyed by ball ref. Exactly one of carried_by/at/dead.",
      "properties": {
        "carried_by": {
          "$ref": "#/definitions/entity_ref"
        },
        "at": {
          "$ref": "#/definitions/coordinate"
        },
        "dead": {
          "type": "boolean",
          "const": true
        }
      },
      "additionalProperties": false,
      "oneOf": [
        {
          "required": [
            "carried_by"
          ],
          "not": {
            "anyOf": [
              {
                "required": [
                  "at"
                ]
              },
              {
                "required": [
                  "dead"
                ]
              }
            ]
          }
        },
        {
          "required": [
            "at"
          ],
          "not": {
            "anyOf": [
              {
                "required": [
                  "carried_by"
                ]
              },
              {
                "required": [
                  "dead"
                ]
              }
            ]
          }
        },
        {
          "required": [
            "dead"
          ],
          "not": {
            "anyOf": [
              {
                "required": [
                  "carried_by"
                ]
              },
              {
                "required": [
                  "at"
                ]
              }
            ]
          }
        }
      ]
    },
    "state": {
      "type": "object",
      "description": "Positional + ball anchor. Entity refs map to a coordinate; the optional 'balls' map keys ball refs to ball states.",
      "properties": {
        "balls": {
          "type": "object",
          "propertyNames": {
            "$ref": "#/definitions/ball_ref"
          },
          "additionalProperties": {
            "$ref": "#/definitions/ball_state"
          }
        }
      },
      "patternProperties": {
        "^(offense|defense)_[1-9]$|^coach$|^(cone|station)_[1-9][0-9]*$": {
          "$ref": "#/definitions/coordinate"
        }
      },
      "additionalProperties": false
    },
    "action_ref": {
      "type": "string",
      "description": "Reference to another action in the same frame: '<entity_ref>.<action_type>'.",
      "pattern": "^((offense|defense)_[1-9]|coach|(cone|station)_[1-9][0-9]*)\\.(move|cut|screen|defend|dribble|pass|shoot|rebound|pickup|tackle|clear|faceoff|check)$"
    },
    "movement_intensity": {
      "type": "string",
      "description": "Relative tempo over distance for move/cut/dribble. Does not imply a concrete duration — a downstream application derives timing from this plus actual player speed.",
      "enum": [
        "slow",
        "normal",
        "fast",
        "explosive"
      ]
    },
    "ball_intensity": {
      "type": "string",
      "description": "Relative ball speed for pass/shoot. Independent of the pass 'variant' (technique/flight path), e.g. variant 'lob' + intensity 'soft' are separate axes.",
      "enum": [
        "soft",
        "normal",
        "hard",
        "bullet"
      ]
    },
    "physicality": {
      "type": "string",
      "description": "Contact/collision style for screen/defend/rebound/pickup, used by a renderer to pick the right animation. Independent of intensity — no effect on distance/timing.",
      "enum": [
        "passive",
        "normal",
        "aggressive",
        "hard"
      ]
    },
    "move_step": {
      "type": "object",
      "description": "One step in a movement sequence. Without 'to' = a move on the spot. Reference fields override the action-level ones.",
      "properties": {
        "variant": {
          "type": "string"
        },
        "to": {
          "$ref": "#/definitions/coordinate"
        },
        "around_player": {
          "$ref": "#/definitions/entity_ref"
        },
        "off_screen_by": {
          "$ref": "#/definitions/entity_ref"
        },
        "intensity": {
          "$ref": "#/definitions/movement_intensity"
        },
        "side": {
          "type": "string",
          "enum": [
            "left",
            "right"
          ],
          "description": "Which side to pass an around_player obstacle on, relative to the moving player's direction of travel. Optional; renderer chooses when absent."
        },
        "arc": {
          "type": "string",
          "enum": [
            "tight",
            "normal",
            "wide"
          ],
          "description": "How closely the path wraps an around_player obstacle: tight (curl), normal, wide (flare). Optional; renderer default when absent."
        }
      },
      "additionalProperties": false
    },
    "action_move": {
      "type": "object",
      "required": [
        "player",
        "type",
        "moves"
      ],
      "properties": {
        "player": {
          "$ref": "#/definitions/entity_ref"
        },
        "type": {
          "const": "move"
        },
        "moves": {
          "type": "array",
          "minItems": 1,
          "items": {
            "$ref": "#/definitions/move_step"
          }
        },
        "intensity": {
          "$ref": "#/definitions/movement_intensity"
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "after": {
          "$ref": "#/definitions/action_ref"
        },
        "with": {
          "$ref": "#/definitions/action_ref"
        },
        "on_catch": {
          "type": "boolean"
        }
      },
      "additionalProperties": false
    },
    "action_cut": {
      "type": "object",
      "required": [
        "player",
        "type",
        "moves"
      ],
      "properties": {
        "player": {
          "$ref": "#/definitions/entity_ref"
        },
        "type": {
          "const": "cut"
        },
        "moves": {
          "type": "array",
          "minItems": 1,
          "items": {
            "$ref": "#/definitions/move_step"
          }
        },
        "variant": {
          "type": "string",
          "enum": [
            "backdoor",
            "give_and_go",
            "flash",
            "v_cut",
            "l_cut",
            "curl",
            "flare",
            "fade",
            "basket"
          ]
        },
        "around_player": {
          "$ref": "#/definitions/entity_ref"
        },
        "off_screen_by": {
          "$ref": "#/definitions/entity_ref"
        },
        "side": {
          "type": "string",
          "enum": [
            "left",
            "right"
          ],
          "description": "Default side for this cut's around_player steps; overridden per move_step."
        },
        "arc": {
          "type": "string",
          "enum": [
            "tight",
            "normal",
            "wide"
          ],
          "description": "Default arc for this cut's around_player steps; overridden per move_step."
        },
        "intensity": {
          "$ref": "#/definitions/movement_intensity"
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "after": {
          "$ref": "#/definitions/action_ref"
        },
        "with": {
          "$ref": "#/definitions/action_ref"
        },
        "on_catch": {
          "type": "boolean"
        }
      },
      "additionalProperties": false
    },
    "action_screen": {
      "type": "object",
      "required": [
        "player",
        "type",
        "for_player"
      ],
      "properties": {
        "player": {
          "$ref": "#/definitions/entity_ref"
        },
        "type": {
          "const": "screen"
        },
        "for_player": {
          "$ref": "#/definitions/entity_ref"
        },
        "on_player": {
          "$ref": "#/definitions/entity_ref"
        },
        "at": {
          "$ref": "#/definitions/coordinate"
        },
        "variant": {
          "type": "string",
          "enum": [
            "ball_screen",
            "back_screen",
            "down_screen",
            "flare_screen",
            "cross_screen",
            "pin_down"
          ]
        },
        "physicality": {
          "$ref": "#/definitions/physicality"
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "after": {
          "$ref": "#/definitions/action_ref"
        },
        "with": {
          "$ref": "#/definitions/action_ref"
        },
        "on_catch": {
          "type": "boolean"
        }
      },
      "additionalProperties": false
    },
    "action_defend": {
      "type": "object",
      "required": [
        "player",
        "type",
        "guards_player"
      ],
      "properties": {
        "player": {
          "$ref": "#/definitions/entity_ref"
        },
        "type": {
          "const": "defend"
        },
        "guards_player": {
          "$ref": "#/definitions/entity_ref"
        },
        "variant": {
          "type": "string",
          "enum": [
            "on_ball",
            "deny",
            "help",
            "hedge",
            "switch",
            "box_out"
          ]
        },
        "physicality": {
          "$ref": "#/definitions/physicality"
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "after": {
          "$ref": "#/definitions/action_ref"
        },
        "with": {
          "$ref": "#/definitions/action_ref"
        },
        "on_catch": {
          "type": "boolean"
        }
      },
      "additionalProperties": false
    },
    "action_dribble": {
      "type": "object",
      "required": [
        "player",
        "type",
        "moves"
      ],
      "properties": {
        "player": {
          "$ref": "#/definitions/entity_ref"
        },
        "type": {
          "const": "dribble"
        },
        "moves": {
          "type": "array",
          "minItems": 1,
          "items": {
            "$ref": "#/definitions/move_step"
          }
        },
        "ball_id": {
          "$ref": "#/definitions/ball_ref"
        },
        "intensity": {
          "$ref": "#/definitions/movement_intensity"
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "after": {
          "$ref": "#/definitions/action_ref"
        },
        "with": {
          "$ref": "#/definitions/action_ref"
        },
        "on_catch": {
          "type": "boolean"
        }
      },
      "additionalProperties": false
    },
    "action_pass": {
      "type": "object",
      "required": [
        "player",
        "type",
        "to_player"
      ],
      "properties": {
        "player": {
          "$ref": "#/definitions/entity_ref"
        },
        "type": {
          "const": "pass"
        },
        "to_player": {
          "$ref": "#/definitions/entity_ref"
        },
        "ball_id": {
          "$ref": "#/definitions/ball_ref"
        },
        "variant": {
          "type": "string",
          "enum": [
            "chest",
            "bounce",
            "overhead",
            "lob",
            "baseball",
            "hand_off",
            "outlet"
          ]
        },
        "intensity": {
          "$ref": "#/definitions/ball_intensity"
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "after": {
          "$ref": "#/definitions/action_ref"
        },
        "with": {
          "$ref": "#/definitions/action_ref"
        },
        "on_catch": {
          "type": "boolean"
        }
      },
      "additionalProperties": false
    },
    "action_shoot": {
      "type": "object",
      "required": [
        "player",
        "type"
      ],
      "properties": {
        "player": {
          "$ref": "#/definitions/entity_ref"
        },
        "type": {
          "const": "shoot"
        },
        "ball_id": {
          "$ref": "#/definitions/ball_ref"
        },
        "variant": {
          "type": "string",
          "enum": [
            "jumper",
            "three",
            "layup",
            "floater",
            "dunk",
            "hook",
            "free_throw"
          ]
        },
        "result": {
          "type": "string",
          "enum": [
            "make",
            "miss"
          ]
        },
        "intensity": {
          "$ref": "#/definitions/ball_intensity"
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "after": {
          "$ref": "#/definitions/action_ref"
        },
        "with": {
          "$ref": "#/definitions/action_ref"
        },
        "on_catch": {
          "type": "boolean"
        }
      },
      "additionalProperties": false
    },
    "action_rebound": {
      "type": "object",
      "required": [
        "player",
        "type"
      ],
      "properties": {
        "player": {
          "$ref": "#/definitions/entity_ref"
        },
        "type": {
          "const": "rebound"
        },
        "ball_id": {
          "$ref": "#/definitions/ball_ref"
        },
        "variant": {
          "type": "string",
          "enum": [
            "offensive",
            "defensive"
          ]
        },
        "physicality": {
          "$ref": "#/definitions/physicality"
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "after": {
          "$ref": "#/definitions/action_ref"
        },
        "with": {
          "$ref": "#/definitions/action_ref"
        },
        "on_catch": {
          "type": "boolean"
        }
      },
      "additionalProperties": false
    },
    "action_pickup": {
      "type": "object",
      "required": [
        "player",
        "type",
        "ball_id"
      ],
      "properties": {
        "player": {
          "$ref": "#/definitions/entity_ref"
        },
        "type": {
          "const": "pickup"
        },
        "ball_id": {
          "$ref": "#/definitions/ball_ref"
        },
        "physicality": {
          "$ref": "#/definitions/physicality"
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "after": {
          "$ref": "#/definitions/action_ref"
        },
        "with": {
          "$ref": "#/definitions/action_ref"
        },
        "on_catch": {
          "type": "boolean"
        }
      },
      "additionalProperties": false
    },
    "action_tackle": {
      "type": "object",
      "$comment": "Reserved for invasion sports (soccer/futsal/hockey). No variants defined yet; must be specified before promotion.",
      "required": [
        "player",
        "type"
      ],
      "properties": {
        "player": {
          "$ref": "#/definitions/entity_ref"
        },
        "type": {
          "const": "tackle"
        },
        "physicality": {
          "$ref": "#/definitions/physicality"
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "after": {
          "$ref": "#/definitions/action_ref"
        },
        "with": {
          "$ref": "#/definitions/action_ref"
        },
        "on_catch": {
          "type": "boolean"
        }
      },
      "additionalProperties": false
    },
    "action_clear": {
      "type": "object",
      "$comment": "Reserved for invasion sports (soccer/futsal/hockey). No variants defined yet.",
      "required": [
        "player",
        "type"
      ],
      "properties": {
        "player": {
          "$ref": "#/definitions/entity_ref"
        },
        "type": {
          "const": "clear"
        },
        "ball_id": {
          "$ref": "#/definitions/ball_ref"
        },
        "intensity": {
          "$ref": "#/definitions/ball_intensity"
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "after": {
          "$ref": "#/definitions/action_ref"
        },
        "with": {
          "$ref": "#/definitions/action_ref"
        },
        "on_catch": {
          "type": "boolean"
        }
      },
      "additionalProperties": false
    },
    "action_faceoff": {
      "type": "object",
      "$comment": "Reserved for hockey. No variants defined yet.",
      "required": [
        "player",
        "type"
      ],
      "properties": {
        "player": {
          "$ref": "#/definitions/entity_ref"
        },
        "type": {
          "const": "faceoff"
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "after": {
          "$ref": "#/definitions/action_ref"
        },
        "with": {
          "$ref": "#/definitions/action_ref"
        },
        "on_catch": {
          "type": "boolean"
        }
      },
      "additionalProperties": false
    },
    "action_check": {
      "type": "object",
      "$comment": "Reserved for hockey. No variants defined yet.",
      "required": [
        "player",
        "type"
      ],
      "properties": {
        "player": {
          "$ref": "#/definitions/entity_ref"
        },
        "type": {
          "const": "check"
        },
        "on_player": {
          "$ref": "#/definitions/entity_ref"
        },
        "physicality": {
          "$ref": "#/definitions/physicality"
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "after": {
          "$ref": "#/definitions/action_ref"
        },
        "with": {
          "$ref": "#/definitions/action_ref"
        },
        "on_catch": {
          "type": "boolean"
        }
      },
      "additionalProperties": false
    },
    "action": {
      "description": "Any action. Discriminated by 'type'.",
      "oneOf": [
        {
          "$ref": "#/definitions/action_move"
        },
        {
          "$ref": "#/definitions/action_cut"
        },
        {
          "$ref": "#/definitions/action_screen"
        },
        {
          "$ref": "#/definitions/action_defend"
        },
        {
          "$ref": "#/definitions/action_dribble"
        },
        {
          "$ref": "#/definitions/action_pass"
        },
        {
          "$ref": "#/definitions/action_shoot"
        },
        {
          "$ref": "#/definitions/action_rebound"
        },
        {
          "$ref": "#/definitions/action_pickup"
        },
        {
          "$ref": "#/definitions/action_tackle"
        },
        {
          "$ref": "#/definitions/action_clear"
        },
        {
          "$ref": "#/definitions/action_faceoff"
        },
        {
          "$ref": "#/definitions/action_check"
        }
      ]
    },
    "custom_position": {
      "type": "object",
      "required": [
        "x",
        "y"
      ],
      "properties": {
        "x": {
          "type": "number"
        },
        "y": {
          "type": "number"
        },
        "description": {
          "type": "string"
        }
      },
      "additionalProperties": false
    },
    "formation_adjustment": {
      "type": "object",
      "description": "Per-entity delta from the resolved formation position. dx/dy are in court units, anchored to that entity's registry position (not a named position, so no relative_to).",
      "required": [
        "entity",
        "dx",
        "dy"
      ],
      "properties": {
        "entity": {
          "$ref": "#/definitions/entity_ref"
        },
        "dx": {
          "type": "number"
        },
        "dy": {
          "type": "number"
        },
        "note": {
          "type": "string"
        }
      },
      "additionalProperties": false
    },
    "based_on_formation": {
      "type": "object",
      "description": "External reference to a starting formation in a versioned registry, resolved to entities at authoring time; pure provenance thereafter. Resolve by id, never by title.",
      "required": [
        "id"
      ],
      "properties": {
        "id": {
          "type": "string",
          "description": "Stable formation id in the registry. Only field with normative meaning."
        },
        "title": {
          "type": "string",
          "description": "Human-readable label, convenience only."
        },
        "source": {
          "type": "string",
          "format": "uri",
          "description": "Stable URI of the formation registry index. Any URI; not host-restricted."
        },
        "source_version": {
          "type": "string",
          "description": "SemVer of the referenced registry. Optional; when present MUST be valid SemVer.",
          "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
        },
        "adjustments": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/formation_adjustment"
          }
        }
      },
      "additionalProperties": false
    },
    "based_on_play": {
      "type": "object",
      "description": "External reference to another play this one derives from. The referenced play is always the base; this document is the derivative. Resolve by id, never by title.",
      "required": [
        "id"
      ],
      "properties": {
        "id": {
          "type": "string",
          "description": "Stable play id in the referenced playbook index. Only field with normative meaning."
        },
        "title": {
          "type": "string",
          "description": "Human-readable label, convenience only."
        },
        "source": {
          "type": "string",
          "format": "uri",
          "description": "Stable URI of the playbook index/manifest. Any URI; not host-restricted."
        },
        "source_version": {
          "type": "string",
          "description": "SemVer of the referenced playbook. Optional; when present MUST be valid SemVer.",
          "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
        },
        "relationship": {
          "type": "string",
          "enum": [
            "variant",
            "progression",
            "counter"
          ],
          "description": "How this play relates to the base. Direction is fixed: this play derives from the referenced base."
        }
      },
      "additionalProperties": false
    },
    "color_scheme": {
      "type": "object",
      "description": "Hex color values for each semantic role.",
      "properties": {
        "offense_fill": {
          "type": "string",
          "pattern": "^#[0-9a-fA-F]{6}$",
          "default": "#003366"
        },
        "offense_stroke": {
          "type": "string",
          "pattern": "^#[0-9a-fA-F]{6}$",
          "default": "#ffffff"
        },
        "defense_fill": {
          "type": "string",
          "pattern": "^#[0-9a-fA-F]{6}$",
          "default": "#58001d"
        },
        "defense_stroke": {
          "type": "string",
          "pattern": "^#[0-9a-fA-F]{6}$",
          "default": "#ffffff"
        },
        "black": {
          "type": "string",
          "pattern": "^#[0-9a-fA-F]{6}$",
          "default": "#000000"
        },
        "grey": {
          "type": "string",
          "pattern": "^#[0-9a-fA-F]{6}$",
          "default": "#7f7f7f"
        },
        "yellow": {
          "type": "string",
          "pattern": "^#[0-9a-fA-F]{6}$",
          "default": "#ffff00"
        },
        "green": {
          "type": "string",
          "pattern": "^#[0-9a-fA-F]{6}$",
          "default": "#7ce86a"
        },
        "red": {
          "type": "string",
          "pattern": "^#[0-9a-fA-F]{6}$",
          "default": "#ff0000"
        },
        "blue": {
          "type": "string",
          "pattern": "^#[0-9a-fA-F]{6}$",
          "default": "#5dd5ff"
        },
        "white": {
          "type": "string",
          "pattern": "^#[0-9a-fA-F]{6}$",
          "default": "#ffffff"
        }
      },
      "additionalProperties": false
    },
    "area": {
      "type": "object",
      "required": [
        "form",
        "x",
        "y"
      ],
      "properties": {
        "form": {
          "type": "string",
          "enum": [
            "rectangle",
            "ellipse",
            "triangle"
          ]
        },
        "color": {
          "$ref": "#/definitions/color_role",
          "default": "yellow"
        },
        "opacity": {
          "type": "number",
          "minimum": 0,
          "maximum": 1,
          "default": 0.4
        },
        "x": {
          "type": "number"
        },
        "y": {
          "type": "number"
        },
        "width": {
          "type": "number"
        },
        "height": {
          "type": "number"
        },
        "rotation": {
          "type": "number",
          "minimum": 0,
          "maximum": 360,
          "default": 0
        },
        "coords": {
          "type": "array",
          "minItems": 3,
          "maxItems": 3,
          "items": {
            "$ref": "#/definitions/coordinate"
          }
        }
      },
      "additionalProperties": false
    },
    "label": {
      "type": "object",
      "required": [
        "text",
        "x",
        "y"
      ],
      "properties": {
        "text": {
          "type": "string"
        },
        "x": {
          "type": "number"
        },
        "y": {
          "type": "number"
        },
        "color": {
          "$ref": "#/definitions/color_role",
          "default": "black"
        }
      },
      "additionalProperties": false
    },
    "outcome": {
      "type": "string",
      "enum": [
        "make",
        "miss",
        "turnover",
        "steal",
        "foul"
      ],
      "description": "Result that selects a branch target frame."
    },
    "frame": {
      "type": "object",
      "description": "A coaching phase: explicit anchors (start_state/end_state) plus semantic actions, with optional outcome branches.",
      "required": [
        "id",
        "actions",
        "end_state"
      ],
      "properties": {
        "id": {
          "type": "string",
          "description": "Unique frame id within the drill."
        },
        "label": {
          "type": "string"
        },
        "description": {
          "type": "string",
          "description": "Coaching instruction; LLMs should generate this."
        },
        "duration_ms": {
          "type": "integer",
          "minimum": 0,
          "description": "Deprecated: use per-action 'intensity' instead. Suggested animation duration for the phase."
        },
        "start_state": {
          "$ref": "#/definitions/state",
          "description": "Optional. Defaults to the previous frame's end_state."
        },
        "actions": {
          "type": "array",
          "description": "Semantic actions in this phase. May be empty.",
          "items": {
            "$ref": "#/definitions/action"
          }
        },
        "end_state": {
          "$ref": "#/definitions/state",
          "description": "Explicit anchor at the end of the phase."
        },
        "branches": {
          "type": "object",
          "description": "Optional outcome -> target frame id. Without branches, the next frame in the array follows.",
          "propertyNames": {
            "$ref": "#/definitions/outcome"
          },
          "additionalProperties": {
            "type": "string"
          },
          "minProperties": 1
        }
      },
      "additionalProperties": false
    }
  },
  "properties": {
    "$schema": {
      "type": "string",
      "format": "uri"
    },
    "sport": {
      "type": "string",
      "enum": [
        "basketball",
        "soccer",
        "handball",
        "hockey",
        "futsal"
      ],
      "default": "basketball",
      "$comment": "Optional, default basketball (back-compat). Intended to become required in v2.0.0. basketball is fully defined; soccer/handball/hockey/futsal carry provisional minimal action whitelists pending sport-expert review."
    },
    "meta": {
      "type": "object",
      "required": [
        "id",
        "title"
      ],
      "properties": {
        "id": {
          "type": "string",
          "format": "uuid"
        },
        "title": {
          "type": "string"
        },
        "description": {
          "type": "string"
        },
        "author": {
          "type": "string"
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "difficulty": {
          "type": "string",
          "enum": [
            "beginner",
            "intermediate",
            "advanced"
          ]
        },
        "created": {
          "type": "string",
          "format": "date-time"
        },
        "modified": {
          "type": "string",
          "format": "date-time"
        },
        "source_format": {
          "type": "string",
          "enum": [
            "fiba",
            "fastdraw",
            "open",
            "custom"
          ]
        },
        "source_url": {
          "type": "string",
          "format": "uri"
        },
        "based_on_formation": {
          "$ref": "#/definitions/based_on_formation"
        },
        "based_on_play": {
          "$ref": "#/definitions/based_on_play"
        },
        "min_schema_version": {
          "type": "string",
          "description": "Minimum OCF schema version required to validate this document correctly. Optional; when present MUST be valid SemVer. A validator whose bundled schema is older SHOULD warn that it may be out of date rather than treat unknown newer fields as errors.",
          "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
        }
      },
      "additionalProperties": false
    },
    "court": {
      "type": "object",
      "required": [
        "ruleset",
        "type"
      ],
      "properties": {
        "ruleset": {
          "$ref": "#/definitions/ruleset"
        },
        "type": {
          "type": "string",
          "enum": [
            "half_court",
            "full_court"
          ]
        },
        "drill_focus": {
          "type": "string",
          "enum": [
            "offense",
            "defense",
            "transition",
            "neutral"
          ],
          "default": "offense"
        },
        "wheelchair": {
          "type": "boolean",
          "default": false
        },
        "custom_dimensions": {
          "type": "object",
          "required": [
            "unit",
            "length",
            "width",
            "basket_from_baseline",
            "three_point_distance",
            "paint_width",
            "paint_depth",
            "free_throw_distance"
          ],
          "properties": {
            "unit": {
              "$ref": "#/definitions/unit"
            },
            "length": {
              "type": "number"
            },
            "width": {
              "type": "number"
            },
            "basket_from_baseline": {
              "type": "number"
            },
            "three_point_distance": {
              "type": "number"
            },
            "paint_width": {
              "type": "number"
            },
            "paint_depth": {
              "type": "number"
            },
            "free_throw_distance": {
              "type": "number"
            }
          },
          "additionalProperties": false
        }
      },
      "additionalProperties": false
    },
    "color_scheme": {
      "$ref": "#/definitions/color_scheme"
    },
    "named_positions": {
      "type": "object",
      "properties": {
        "custom": {
          "type": "object",
          "additionalProperties": {
            "$ref": "#/definitions/custom_position"
          }
        }
      },
      "additionalProperties": false
    },
    "entities": {
      "type": "array",
      "description": "All non-ball entities and their initial (frame 0) positions.",
      "items": {
        "$ref": "#/definitions/entity"
      }
    },
    "balls": {
      "type": "array",
      "description": "Balls present at the start of the drill. Each holds exactly one lifecycle state.",
      "items": {
        "$ref": "#/definitions/ball"
      }
    },
    "frames": {
      "type": "array",
      "minItems": 1,
      "items": {
        "$ref": "#/definitions/frame"
      }
    },
    "areas": {
      "type": "array",
      "items": {
        "$ref": "#/definitions/area"
      }
    },
    "labels": {
      "type": "array",
      "items": {
        "$ref": "#/definitions/label"
      }
    }
  },
  "additionalProperties": false,
  "allOf": [
    {
      "if": {
        "type": "object",
        "properties": {
          "court": {
            "type": "object",
            "properties": {
              "ruleset": {
                "const": "custom"
              }
            }
          }
        }
      },
      "then": {
        "type": "object",
        "properties": {
          "court": {
            "type": "object",
            "required": [
              "custom_dimensions"
            ]
          }
        }
      }
    },
    {
      "$comment": "sport absent OR basketball -> basketball action whitelist (default is a non-validating annotation, so absence must be handled explicitly for back-compat)",
      "if": {
        "anyOf": [
          {
            "not": {
              "required": [
                "sport"
              ]
            }
          },
          {
            "properties": {
              "sport": {
                "const": "basketball"
              }
            }
          }
        ]
      },
      "then": {
        "type": "object",
        "properties": {
          "frames": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "actions": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "type": {
                        "enum": [
                          "move",
                          "cut",
                          "screen",
                          "defend",
                          "dribble",
                          "pass",
                          "shoot",
                          "rebound",
                          "pickup"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    {
      "if": {
        "required": [
          "sport"
        ],
        "properties": {
          "sport": {
            "const": "soccer"
          }
        }
      },
      "then": {
        "type": "object",
        "properties": {
          "frames": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "actions": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "type": {
                        "enum": [
                          "move",
                          "pass",
                          "shoot",
                          "defend",
                          "dribble",
                          "tackle",
                          "clear"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    {
      "if": {
        "required": [
          "sport"
        ],
        "properties": {
          "sport": {
            "const": "handball"
          }
        }
      },
      "then": {
        "type": "object",
        "properties": {
          "frames": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "actions": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "type": {
                        "enum": [
                          "move",
                          "pass",
                          "shoot",
                          "defend",
                          "cut",
                          "screen",
                          "pickup"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    {
      "if": {
        "required": [
          "sport"
        ],
        "properties": {
          "sport": {
            "const": "hockey"
          }
        }
      },
      "then": {
        "type": "object",
        "properties": {
          "frames": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "actions": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "type": {
                        "enum": [
                          "move",
                          "pass",
                          "shoot",
                          "defend",
                          "dribble",
                          "clear",
                          "faceoff",
                          "check"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    {
      "if": {
        "required": [
          "sport"
        ],
        "properties": {
          "sport": {
            "const": "futsal"
          }
        }
      },
      "then": {
        "type": "object",
        "properties": {
          "frames": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "actions": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "type": {
                        "enum": [
                          "move",
                          "pass",
                          "shoot",
                          "defend",
                          "dribble",
                          "tackle",
                          "clear"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  ]
}