Open Coaching Format — Open Standard for Team-Sport Coaching Diagrams

1. Abstract

{ocf} v1.4.0 is an open, ruleset-aware JSON schema for representing team-sport coaching diagrams and multi-step animations — targeting invasion team sports, with basketball fully defined first. It defines a court coordinate system based on real-world units (meters or feet), a registry of named court positions per ruleset, a semantic entity model, and a frame-based animation structure.

The format is designed to be:

  • Human-readable — coaching terminology maps directly to named positions

  • LLM-friendly — structured enough for text-to-diagram generation

  • Renderer-agnostic — SVG, Canvas, PDF, mobile

  • Ruleset-aware — FIBA, NBA, NCAA, NFHS, custom

  • FIBA-compatible — import/export path for existing FIBA Europe diagrams


2. Introduction

2.1. Motivation

No open standard exists for team-sport coaching diagrams. All major tools (FIBA Europe Coaching App, FastDraw, Sportplan.net) use proprietary formats with image-only export. This prevents programmatic drill creation, LLM-assisted coaching tools, and cross-platform interoperability.

{ocf} addresses this gap by providing:

  1. A well-defined JSON Schema (Draft-7)

  2. A coordinate system grounded in real-world units

  3. A named position registry aligned with official ruleset geometry

  4. A frame model for multi-step animated drills

  5. A clear import path from the FIBA Europe internal format

2.2. Design Principles

Principle Decision

Court units = real units

Coordinates in meters (FIBA) or feet (NBA/NCAA). No arbitrary pixel grids.

Origin = midcourt center

Positive y = frontcourt (offense). Negative y = backcourt (defense). Zero = midline.

Viewer perspective

"Left" and "right" are always from the viewer’s top-down perspective.

Named positions over raw coordinates

Standard court landmarks (e.g. top_of_the_key) are first-class citizens.

Entity keys = team + number

offense_1, defense_3. No synthetic IDs for player entities.

Frames are anchored

Each frame carries explicit start_state/end_state anchors plus semantic actions. Any frame can render as a still without simulating history.

Colors are semantic roles

"offense", "defense", "black". Hex values are configured per document.

Registry over fixed enum

Named positions use a known-good standard catalog + user-extensible custom block.

JSON Schema draft-07

The schema targets draft-07 for the v1 line. OCF’s variance is conditional (if/then on sport), not recursive, so newer-dialect features like $dynamicRef/unevaluatedProperties add no expressive value here — while draft-07 maximizes tooling support and keeps the TypeScript and Python reference validators in lockstep. Reconsider only for a genuinely recursive-polymorphic feature, or at a v2 major bump.


3. Coordinate System

3.1. Overview

The {ocf} coordinate system places the origin at the center of the court (midcourt center circle).

                    viewer's left         viewer's right
                         |                     |
    defense basket  ─────┼─────────────────────┼─────  y = -max
    (backcourt)          │                     │
                         │    BACKCOURT        │
                         │    y < 0            │
                    ─────┼─────────────────────┼─────  y = 0  (midline / origin)
                    x=-max         x=0        x=+max
                         │    FRONTCOURT       │
                         │    y > 0            │
    offense basket  ─────┼─────────────────────┼─────  y = +max
    (frontcourt)         │                     │
Axis Definition

x

Horizontal. 0 = court center line. Negative = viewer’s left. Positive = viewer’s right.

y

Vertical. 0 = midline. Positive = toward offense basket (frontcourt). Negative = toward defense basket (backcourt).

3.2. Coordinate Ranges per Ruleset

Ruleset Unit x range y range (half / full)

fiba

m

−7.5 … +7.5

0 … +14.0 / −14.0 … +14.0

nba

ft

−25.0 … +25.0

0 … +47.0 / −47.0 … +47.0

ncaa

ft

−25.0 … +25.0

0 … +47.0 / −47.0 … +47.0

nfhs

ft

−25.0 … +25.0

0 … +42.0 / −42.0 … +42.0

For half court diagrams, only the frontcourt (y ≥ 0) is rendered. Entities placed at negative y coordinates in a half_court document are outside the visible area.

3.3. Field Dimensions Reference

FIBA Half Court — values in meters, origin at midline center
┌─────────────────────────────────────────────────────────┐  y = 0.0
│                         x=0                             │
│              midcourt.left  midcourt.center  midcourt.right
│           (-7.5,0)          (0,0)             (7.5,0)  │
│                                                         │
│                     TOP OF THE KEY                      │
│                      (0.0, 5.68)                        │
│       left_wing                          right_wing     │
│      (-6.75, 8.60)                    (6.75, 8.60)      │
│                                                         │
│   left_corner   ─ ─ FREE THROW LINE ─ ─  right_corner  │
│                        (0, 8.20)                        │
│  left_elbow                              right_elbow    │
│  (-2.45, 8.20)                          (2.45, 8.20)   │
│                 ┌───────────────────┐                   │
│                 │   PAINT AREA      │                   │
│   left_block    │                   │   right_block     │
│  (-2.45, 11.00) │   paint_center    │  (2.45, 11.00)   │
│                 │    (0, 10.50)     │                   │
│                 └───────────────────┘                   │
│  left_corner                            right_corner    │
│  (-7.5, 13.98)       basket          (7.5, 13.98)       │
│                     (0, 12.43)                          │
└─────────────────────────────────────────────────────────┘  y = +14.0

3.4. Coordinate Types

A coordinate in {ocf} can be one of three forms:

3.4.1. Absolute

{ "x": 2.45, "y": 8.20 }

A fixed point in court units. Always valid.

3.4.2. Named

{ "named": "left_elbow" }

References a standard named position. Resolved to absolute coordinates at render time using the document’s ruleset. Custom positions use the custom. prefix:

{ "named": "custom.dunker_spot" }

3.4.3. Relative

{
  "relative_to": "left_elbow",
  "dx": 1.0,
  "dy": -0.5
}

An offset from a named position. dx and dy are in the document’s court unit. Positive dx = viewer’s right. Positive dy = toward offense basket.


4. Named Court Positions

4.1. Standard Positions (FIBA)

All coordinates in meters (m). Backcourt positions are the exact y-negation of their frontcourt counterparts.

The machine-readable source of truth for every ruleset’s named-position coordinates is positions/{fiba,nba,ncaa,nfhs}-v1.json, exported from @opencoachingformat/spec via resolveNamedPosition(name, ruleset). The tables below are the human-readable FIBA reference; tools MUST read the JSON, not parse these tables. A test (test/positions-anchors.test.mjs) guards the JSON against the cross-ruleset anchor values in the difference table.
Name x (m) y (m) Description

Paint / Near Basket

basket

0.000

12.425

Center of basket ring

left_block

-2.450

11.000

Low post, left side

right_block

2.450

11.000

Low post, right side

paint_center

0.000

10.500

Center of the lane

left_short_corner

-7.500

11.500

Baseline region, outside paint left

right_short_corner

7.500

11.500

Baseline region, outside paint right

Mid-Range / Elbow

left_elbow

-2.450

8.200

Junction of free throw line and paint, left

right_elbow

2.450

8.200

Junction of free throw line and paint, right

free_throw_line

0.000

8.200

Center of free throw line

high_post_left

-2.450

7.000

Above the elbow, left side

high_post_right

2.450

7.000

Above the elbow, right side

3-Point Area

top_of_the_key

0.000

5.680

Apex of 3-point arc

left_wing

-6.750

8.600

3-point line, ~45° left

right_wing

6.750

8.600

3-point line, ~45° right

left_corner

-7.500

13.980

Corner 3, left

right_corner

7.500

13.980

Corner 3, right

Midcourt

midcourt.center

0.000

0.000

Center circle, origin

midcourt.left

-7.500

0.000

Left sideline at midline

midcourt.right

7.500

0.000

Right sideline at midline

Backcourt (Full Court)

backcourt.basket

0.000

-12.425

Defense basket

backcourt.left_block

-2.450

-11.000

Defense left block

backcourt.right_block

2.450

-11.000

Defense right block

backcourt.left_elbow

-2.450

-8.200

Defense left elbow

backcourt.right_elbow

2.450

-8.200

Defense right elbow

backcourt.free_throw_line

0.000

-8.200

Defense free throw line

backcourt.top_of_the_key

0.000

-5.680

Defense top of the key

backcourt.left_wing

-6.750

-8.600

Defense left wing

backcourt.right_wing

6.750

-8.600

Defense right wing

backcourt.left_corner

-7.500

-13.980

Defense left corner

backcourt.right_corner

7.500

-13.980

Defense right corner

Inbound Positions

inbound.baseline_left

-3.000

14.000

Baseline inbound, left of basket

inbound.baseline_right

3.000

14.000

Baseline inbound, right of basket

inbound.baseline_center

0.000

14.000

Baseline inbound, center

inbound.sideline_left_fc

-7.500

8.200

Sideline inbound, frontcourt left

inbound.sideline_right_fc

7.500

8.200

Sideline inbound, frontcourt right

inbound.sideline_left_mid

-7.500

0.000

Sideline inbound, at midline left

inbound.sideline_right_mid

7.500

0.000

Sideline inbound, at midline right

inbound.sideline_left_bc

-7.500

-8.200

Sideline inbound, backcourt left

inbound.sideline_right_bc

7.500

-8.200

Sideline inbound, backcourt right

4.2. Ruleset Differences

Key position differences across rulesets, as centered coordinates (origin at court center), matching the values in positions/{ruleset}-v1.json:

Position FIBA (m) NBA (ft) NCAA (ft) NFHS (ft)

top_of_the_key y

5.68

20.75

20.75

19.75

left_wing x

±6.75

±22.15

±20.75

±19.75

left_corner x

±7.50

±25.0

±25.0

±25.0

left_corner y

13.98

43.305

43.305

38.305

free_throw_line y

8.20

28.0

28.0

23.0

left_elbow x

±2.45

±8.0

±6.0

±6.0

These are centered-coordinate values (y measured from the court center, positive toward the offense basket), not distances from the baseline. For example NBA free_throw_line y = 28.0 is the free-throw line’s position in the centered system (half-court 47 − 19.0 ft from baseline), not the familiar "19 ft from the baseline" figure.
NBA corners are defined by a straight sideline section, not an arc. The left_corner and right_corner positions mark the junction point where the straight section meets the arc.

4.3. Custom Positions (Registry)

User-defined named positions are stored in the named_positions.custom block:

"named_positions": {
  "custom": {
    "dunker_spot": {
      "x": 1.5,
      "y": 12.8,
      "description": "Dunker spot, weak side baseline"
    },
    "nail": {
      "x": 0.0,
      "y": 8.5,
      "description": "Free throw line extended, center"
    },
    "short_roll": {
      "x": -2.0,
      "y": 9.5
    }
  }
}

Reference them in coordinates with the custom. prefix:

{ "named": "custom.dunker_spot" }
Custom positions use the same unit as the document ruleset. No separate unit declaration is needed.

5. Entities

Entities represent all non-ball objects that appear on the court. Their initial position is defined in the top-level entities array. Balls are stored separately in the top-level balls array (see Balls).

5.1. Entity Reference Keys

Each entity is uniquely identified by a natural key string:

Entity Key Format

Offensive player nr. 1

"offense_1"

Defensive player nr. 3

"defense_3"

Coach

"coach"

Cone nr. 2

"cone_2"

Station nr. 4

"station_4"

offense_1 and defense_1 are distinct entities. In transition drills, both can appear simultaneously.

5.2. Distinct Positions

Two distinct entities MUST NOT occupy the same position. Within any single state (the top-level entities array, or a frame’s start_state / end_state), no two entities may share coordinates that resolve closer than a minimum separation to each other.

The minimum separation is one player diameter in the document’s court unit — the space a player symbol occupies — so that rendered symbols never overlap and each player remains individually identifiable:

Ruleset Minimum separation

fiba

0.5 m

nba / ncaa / nfhs

1.6 ft

A document that places two entities closer than this is invalid; a validator reports it. Two players genuinely "at the same spot" in a coach’s mind (e.g. a hand-off) are still distinct bodies and must be given distinct coordinates a symbol-width apart.

A renderer SHOULD additionally apply a small visual nudge if symbols still touch after a valid layout (a rendering safety net), but this does not relax the data constraint above — the underlying coordinates remain the source of truth.

5.3. Entity Types

5.3.1. Offensive Player

{
  "type": "offense",
  "nr": 1,
  "x": 0.0,
  "y": 5.68,
  "rotation": 0,
  "color": "offense"
}

Rendered as a filled circle with player number. rotation is accepted for symmetry but is not rendered for offense (the filled circle has no facing direction); it is typically 0. The field is meaningful for defenders — see the defender rotation convention below. color defaults to "offense" (resolved via color_scheme.offense_fill).

5.3.2. Defensive Player

{
  "type": "defense",
  "nr": 1,
  "x": 0.5,
  "y": 5.5,
  "rotation": 45,
  "color": "defense"
}

Rendered as a filled shape (an arc — the defender’s outstretched arms — above an inner circle carrying the player number). rotation indicates the direction the defender is facing, i.e. the direction the arms point.

Defender rotation convention

rotation is measured in degrees, clockwise on screen, from a defined zero:

  • (default) — arms point in the −y direction, i.e. away from the attacking basket. This is the natural defensive stance: the defender’s back is toward their own basket and they face an attacker coming from +y (the direction of attack).

  • Increasing values rotate the arms clockwise as seen on the rendered image: 90° → arms point −x (viewer’s left), 180°+y (toward the attacking basket), 270°+x (viewer’s right).

A renderer applies rotation exactly as given; it does not infer a facing direction from nearby players. When rotation is absent it defaults to (arms toward −y).

5.3.3. Balls

Balls are no longer entities. They live in the top-level balls array and each holds exactly one lifecycle state:

State Meaning

carried_by

An entity ref (player or coach) currently holding the ball.

at

A coordinate where the ball rests (floor, rim after a miss). Still in play — can be pickup or rebound.

dead

true. Out of play (made basket with no rebound, or out of bounds).

"balls": [
  { "id": "ball_1", "carried_by": "offense_1" },
  { "id": "ball_2", "carried_by": "coach" },
  { "id": "ball_3", "at": { "named": "right_corner" } }
]

Ball possession moves automatically: pass, shoot, pickup and rebound actions transfer the ball. After a shoot with no following rebound/pickup on the same ball, the ball becomes dead.

5.3.4. Coach

{
  "type": "coach",
  "x": -7.5,
  "y": 6.0
}

Rendered as a circle labeled "C".

5.3.5. Cone

{
  "type": "cone",
  "nr": 1,
  "x": 2.0,
  "y": 7.0
}

Rendered as a small triangle. Used as training markers or obstacles.

5.3.6. Station

{
  "type": "station",
  "nr": 3,
  "label": "Rebound jumps",
  "x": -4.0,
  "y": 11.0
}

Rendered as a numbered marker with optional description. Used for circuit training drills.


6. Sport

OCF targets invasion team sports — two teams sharing a field, ball/puck possession, and off-ball movement (basketball, soccer, handball, hockey, futsal). Basketball is fully defined; the others are reserved with provisional action vocabularies.

The optional sport field declares the sport:

"sport": "basketball"

sport is optional and defaults to basketball for backwards compatibility; it is planned to become required in v2. The declared sport determines which action types (and variants) are valid — e.g. screen/rebound are valid under basketball, tackle/clear under soccer. Documents with no sport field are treated as basketball.


7. Actions

Movement and ball events are expressed as semantic actions, not geometric lines. A renderer derives the drawn arrows/paths from the actions. Every action has a required player and type; everything else is optional, so a coach can sketch quickly or annotate in full detail.

7.1. Action Types

Type Ball? Required Optional (variant enum / references)

move

no

moves[]

intensity; per-move intensity

cut

no

moves[]

variant (backdoor, give_and_go, flash, v_cut, l_cut, curl, flare, fade, basket); intensity; action-level side, arc (defaults); per-move around_player, off_screen_by, intensity, side, arc

screen

no

for_player

on_player, at, variant (ball_screen, back_screen, down_screen, flare_screen, cross_screen, pin_down), physicality

defend

no

guards_player

variant (on_ball, deny, help, hedge, switch, box_out), physicality

dribble

yes

moves[]

ball_id, intensity; per-move variant (speed, hesitation, crossover, behind_back, between_legs, spin, retreat), intensity

pass

yes

to_player

ball_id, variant (chest, bounce, overhead, lob, baseball, hand_off, outlet), intensity

shoot

yes

player

ball_id, variant (jumper, three, layup, floater, dunk, hook, free_throw), result (make/miss), intensity

rebound

becomes carrier

player

ball_id, variant (offensive, defensive), physicality

pickup

becomes carrier

ball_id

physicality

Any action may also carry a free tags array (e.g. eurostep, reverse). Curated variant values are what a renderer can distinguish visually; most tags are open coaching metadata that need no schema change.

Curated tags: left_handed / right_handed (ball-handling side)

Two tags are curated because a renderer interprets them visually: on a ball-handling action (dribble, pass, shoot) the tags left_handed and right_handed state which hand handles the ball.

This drives where the ball is drawn relative to its carrier:

  • A carried ball is drawn slightly ahead of and to one side of the carrier — not on top of them — so the player’s number stays visible and the handling side is legible. "Ahead" is the direction of the ball-handling action (dribble / pass / shoot) in that frame.

  • Default (no tag, or right_handed): the ball is on the right of the direction of travel.

  • left_handed: the ball is on the left.

These tags remain ordinary entries in the free tags array (no schema change); they are simply the agreed vocabulary for ball side. A renderer that does not model the ball side may ignore them. This convention also lets a future animated renderer show a hand change (e.g. a crossover, or dribble→shoot) correctly.

A loose ball (at: a coordinate, not carried) is drawn on its coordinate and is not offset — the offset applies only to a carried ball.

7.2. Movement Sequences

move, cut and dribble carry a moves[] sequence. A move without to is a move on the spot (ball handling / fake); a move with to carries the player (and ball, for dribble) to a waypoint. The final to must match the player’s position in the frame end_state. Reference fields (around_player, off_screen_by) may sit on an individual move and override the action level.

For dribble (and move), each move’s variant describes that step’s character — a single dribble action can chain e.g. crossover then speed — whereas cut also has an action-level variant for the overall cut type (e.g. backdoor).

{
  "player": "offense_1",
  "type": "dribble",
  "ball_id": "ball_1",
  "moves": [
    { "variant": "crossover" },
    { "variant": "speed", "to": { "named": "basket" }, "around_player": "defense_1" }
  ]
}

When a movement step goes around_player, two optional fields shape the detour: side (left/right, relative to the player’s direction of travel) chooses which side to pass on, and arc (tight/normal/wide) how closely the path wraps the obstacle — tight reads as a curl, wide as a flare. Both may be set on the cut action as defaults and overridden per step. When omitted, the renderer chooses (for a cut, it may derive the arc from the variantcurl→tight, flare/fade→wide). arc is a semantic enum, not a radius; the renderer owns the enum-to-distance mapping and may expose it as configuration.

7.3. Intensity & Physicality

Two optional, independent enums express relative execution style. Neither implies a concrete duration — a downstream application combines them with actual player/roster data to derive timing or pick renderer animations.

Field Applies to Enum

intensity (movement_intensity)

move, cut, dribble (action-level and per-move, move overrides action)

slow, normal (default), fast, explosive

intensity (ball_intensity)

pass, shoot

soft, normal (default), hard, bullet

physicality

screen, defend, rebound, pickup

passive, normal (default), aggressive, hard

intensity describes tempo over distance (how fast a player/ball moves). physicality describes contact/collision style and has no bearing on timing — it exists purely to help a renderer pick the right animation (e.g. a clean screen vs. a moving-screen-style rammed one). The two are deliberately separate so a distance→time calculation built on intensity never has to reason about physicality values.

{
  "player": "offense_1",
  "type": "cut",
  "intensity": "slow",
  "moves": [
    { "to": { "x": -2.0, "y": 4.5 } },
    { "to": { "named": "basket" }, "intensity": "explosive" }
  ]
}

duration_ms (see Frame Model) is deprecated in favor of intensity. It remains valid for backward compatibility with existing play files, but new play files should express tempo via intensity instead.

7.4. Timing

Actions express ordering semantically, not in seconds (omitted = at frame start):

Field Meaning

after

"<player>.<type>" — starts after the referenced action begins.

with

"<player>.<type>" — simultaneous with the referenced action.

on_catch

true — straight out of the catch (catch-and-shoot).


8. Frames

A frame is a coaching phase. It combines explicit anchors with semantic actions.

8.1. Frame Model

Field Required Meaning

id

yes

Unique frame id within the drill.

label

no

Short display label.

description

no

Coaching instruction. LLMs should generate this.

duration_ms

no

Deprecated — use per-action intensity instead. Suggested animation duration for the phase.

start_state

no

Positional + ball anchor at phase start. Defaults to the previous frame’s end_state.

actions

yes

Semantic actions in this phase. May be empty.

end_state

yes

Positional + ball anchor at phase end.

branches

no

Outcome → target frame id.

8.2. States

start_state and end_state map entity refs to coordinates, plus an optional balls map of ball ref → ball state. Because each frame carries an explicit anchor, any frame can be rendered as a still without simulating history — which is what enables PDF export, editor scrubbing and video overlay.

start_state is optional and inherits the previous frame’s end_state. Set it explicitly only at jumps, steals, rebounds or continuum resets.

"end_state": {
  "offense_1": { "x": -1.0, "y": 9.0 },
  "offense_4": { "named": "right_block" },
  "balls": { "ball_1": { "carried_by": "offense_4" } }
}

8.3. Branches

Without branches, the next frame in the array follows. With branches, the outcome selects the next frame. Outcomes are make, miss, turnover, steal, foul. A branch may point at an earlier frame to build a continuum loop. For a single set play, omit branches and let the final frame set the ball dead.

"branches": { "make": "frame_reset", "miss": "frame_oreb" }

9. Areas and Labels

9.1. Areas

Highlighted zones for marking training areas, defensive zones, or emphasis regions.

{
  "form": "rectangle",
  "color": "yellow",
  "opacity": 0.4,
  "x": 0.0,
  "y": 10.0,
  "width": 4.9,
  "height": 4.0
}
{
  "form": "ellipse",
  "color": "green",
  "opacity": 0.3,
  "x": 0.0,
  "y": 8.2,
  "width": 5.0,
  "height": 3.0
}
{
  "form": "triangle",
  "color": "red",
  "opacity": 0.25,
  "x": 0,
  "y": 0,
  "coords": [
    { "named": "left_block" },
    { "named": "right_block" },
    { "named": "basket" }
  ]
}

9.2. Labels

Free text annotations on the court:

{
  "text": "Pick & Roll",
  "x": -7.0,
  "y": 3.0,
  "color": "black"
}

10. Color Scheme

10.1. Design

Colors are defined as semantic roles, not as direct hex values in entities or lines. This allows editors to customize the appearance globally without modifying individual elements.

Role FIBA Default Usage

offense_fill

#003366

Offensive player symbol fill

offense_stroke

#ffffff

Offensive player symbol outline + number

defense_fill

#58001d

Defensive player symbol fill

defense_stroke

#ffffff

Defensive player symbol outline + number

black

#000000

Lines, cones, text (default)

grey

#7f7f7f

Subtle highlights, background areas

yellow

#ffff00

Highlighted zones

green

#7ce86a

Secondary highlights

red

#ff0000

Warning zones, alternative team color

blue

#5dd5ff

Alternative team color

white

#ffffff

Backgrounds, inverted labels

10.2. WCAG Accessibility

All color combinations used for rendered symbols must meet at minimum WCAG AA (4.5:1 contrast ratio). WCAG AAA (7:1) is recommended for printed materials.

Validators should emit a warning when color_scheme values produce contrast below 4.5:1 against the court background used by the target renderer.

FIBA default — contrast analysis:

Combination Foreground Background Ratio WCAG

Offense number on fill

#ffffff

#003366

12.6:1

✅ AAA

Defense number on fill

#ffffff

#58001d

11.2:1

✅ AAA

Black line on court

#000000

#0089cf

4.6:1

✅ AA

Offense fill on court

#003366

#0089cf

3.8:1

⚠️ Fail (use stroke)

Defense fill on court

#58001d

#0089cf

3.1:1

⚠️ Fail (use stroke)

Renderers should apply a 2px white stroke around player symbols to compensate for the low contrast between symbol fill and court background. This matches the FIBA SVG renderer behavior.

10.3. Overriding Colors

"color_scheme": {
  "offense_fill": "#1a4f8a",
  "offense_stroke": "#ffffff",
  "defense_fill": "#8a1a1a",
  "defense_stroke": "#ffffff"
}

Only fields that differ from the FIBA defaults need to be included.


11. Court Configuration

11.1. Supported Rulesets

ruleset unit Court (L × W) Notes

fiba

m

28.0 × 15.0

International standard

nba

ft

94.0 × 50.0

NBA-specific 3-point geometry (straight corners)

ncaa

ft

94.0 × 50.0

Shorter 3-point line than NBA

nfhs

ft

84.0 × 50.0

US high school, shorter court

custom

*

defined

Requires custom_dimensions block

11.2. Half Court vs Full Court

type Rendered region

half_court

y = 0 to +max. Offense basket at top. Midline forms the baseline.

full_court

y = -max to +max. Both baskets rendered.

11.3. Drill Focus

drill_focus Meaning

offense

Offensive drill. Positive y = direction of attack. Default.

defense

Defensive drill. Positive y = basket being defended.

transition

Full court drill with directional change. Implies full_court.

neutral

No directional focus (e.g. conditioning drill, station circuit).

11.4. Custom Ruleset

"court": {
  "ruleset": "custom",
  "type": "half_court",
  "drill_focus": "offense",
  "custom_dimensions": {
    "unit": "m",
    "length": 24.0,
    "width": 13.0,
    "basket_from_baseline": 1.575,
    "three_point_distance": 5.77,
    "paint_width": 3.6,
    "paint_depth": 5.8,
    "free_throw_distance": 5.8
  }
}

12. Ruleset Conversion

Converting a drill between rulesets is an explicit, lossless operation for named positions and a best-effort proportional mapping for free coordinates.

12.1. Named Position Conversion

Named positions are looked up independently in each ruleset’s coordinate table. The semantic meaning is preserved exactly.

"left_wing" in FIBA  →  (-6.75m, 8.60m)
"left_wing" in NBA   →  (-22.15ft, 28.5ft)   ← independent lookup, no math

12.2. Free Coordinate Conversion

Free absolute coordinates are converted proportionally:

x_target = x_source / (source.half_width)  * target.half_width
y_target = y_source / (source.half_depth)  * target.half_depth

Proportional conversion of free coordinates may place entities at positions that are semantically incorrect (e.g. inside or outside the 3-point line) relative to the target ruleset’s geometry.

Editors should surface a warning: "X free coordinates were proportionally scaled. Please verify their positions against the target ruleset."

12.3. FIBA Legacy Import

Existing FIBA Europe diagrams use a proprietary pixel coordinate system (384 × 540). Conversion to {ocf} FIBA coordinates:

x_m = (fiba_x - 192) / 192.0 * 7.5
y_m = (270 - fiba_y) / 270.0 * 14.0
The FIBA coordinate origin is at the top-left of the SVG canvas. The y-axis is inverted (y increases downward in FIBA, upward in {ocf}).

13. Complete Example

13.1. Pick & Roll — Half Court, FIBA

{
  "$schema": "https://opencoachingformat.org/schema/v1.json",
  "meta": {
    "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "title": "Basic Pick & Roll",
    "description": "Ball handler uses a ball screen at the top of the key. Screener rolls to the basket for the finish.",
    "author": "OCF Examples",
    "tags": ["pick-and-roll", "offense", "half-court", "2-player", "beginner"],
    "difficulty": "beginner",
    "created": "2025-02-27T10:00:00Z",
    "source_format": "open"
  },
  "court": { "ruleset": "fiba", "type": "half_court", "drill_focus": "offense" },
  "entities": [
    { "type": "offense", "nr": 1, "x": 0.0, "y": 5.68 },
    { "type": "offense", "nr": 4, "x": -2.45, "y": 8.20 },
    { "type": "defense", "nr": 1, "x": 0.5, "y": 5.20 },
    { "type": "defense", "nr": 4, "x": -2.80, "y": 7.80 }
  ],
  "balls": [ { "id": "ball_1", "carried_by": "offense_1" } ],
  "frames": [
    {
      "id": "frame_1",
      "label": "Step 1 — Screen",
      "description": "offense_4 moves up from the left elbow to set a ball screen for offense_1 at the top of the key.",
      "duration_ms": 1500,
      "actions": [
        { "player": "offense_4", "type": "screen", "for_player": "offense_1", "on_player": "defense_1", "variant": "ball_screen", "at": { "x": -0.5, "y": 6.7 } }
      ],
      "end_state": {
        "offense_1": { "named": "top_of_the_key" },
        "offense_4": { "x": -0.5, "y": 6.2 },
        "balls": { "ball_1": { "carried_by": "offense_1" } }
      }
    },
    {
      "id": "frame_2",
      "label": "Step 2 — Attack",
      "description": "offense_1 uses the screen and dribbles hard left toward the elbow; offense_4 rolls to the right block.",
      "duration_ms": 2000,
      "actions": [
        {
          "player": "offense_1",
          "type": "dribble",
          "ball_id": "ball_1",
          "moves": [
            { "variant": "hesitation" },
            { "variant": "speed", "to": { "named": "left_elbow" }, "around_player": "defense_1" }
          ]
        },
        { "player": "offense_4", "type": "cut", "variant": "basket", "moves": [ { "to": { "named": "right_block" } } ], "tags": ["roll"] }
      ],
      "end_state": {
        "offense_1": { "named": "left_elbow" },
        "offense_4": { "named": "right_block" },
        "balls": { "ball_1": { "carried_by": "offense_1" } }
      }
    },
    {
      "id": "frame_3",
      "label": "Step 3 — Finish",
      "description": "offense_1 throws a bounce pass to the rolling offense_4, who finishes with a layup.",
      "duration_ms": 1500,
      "actions": [
        { "player": "offense_1", "type": "pass", "to_player": "offense_4", "ball_id": "ball_1", "variant": "bounce" },
        { "player": "offense_4", "type": "shoot", "ball_id": "ball_1", "variant": "layup", "result": "make", "on_catch": true }
      ],
      "end_state": {
        "offense_1": { "named": "left_elbow" },
        "offense_4": { "named": "right_block" },
        "balls": { "ball_1": { "dead": true } }
      }
    }
  ],
  "areas": [
    { "form": "rectangle", "color": "yellow", "opacity": 0.2, "x": 0.0, "y": 10.5, "width": 4.9, "height": 4.0, "rotation": 0 }
  ],
  "labels": [ { "text": "Roll!", "x": 3.2, "y": 9.5, "color": "black" } ]
}

14. External References

Some meta fields point at content that lives outside the document — in a versioned registry or playbook. They all share one shape, the External Reference:

Field Meaning

id

Stable identifier in the external source. The only field with normative meaning — tools resolve by id, never by matching title.

title

Human-readable label, convenience only (useful when source is unreachable).

source

Stable URI of the external collection (a registry index or playbook manifest), not a document path. Any URI; the official registry under opencoachingformat.org is recommended, third-party registries are first-class.

source_version

SemVer of that collection. Optional; when present it MUST be valid SemVer so resolvers can compare compatibility.

Resolution is best-effort, not a validity requirement: a document whose source is unreachable is still valid — id/title remain useful. Validators check reference shape only and never fetch source.

Where a reference stands in for concrete data the renderer needs, it is resolved into the document’s normal fields at authoring time, and the reference remains as pure provenance. Renderers never resolve references.

14.1. meta.based_on_formation

Records the standard starting formation a play begins from, looked up from a versioned formation registry instead of being hand-authored. Resolved to concrete entities[].x/y at authoring time.

adjustments is an optional list of per-entity deltas from the registry’s stored position. dx/dy are in court units and are anchored to that entity’s resolved formation position — this is a different anchor from a relative coordinate (relative_to), so adjustments carry no relative_to.

"based_on_formation": {
  "id": "4_out_1_in",
  "title": "4 Out 1 In",
  "source": "https://opencoachingformat.org/registry/formations/basketball-v1.json",
  "source_version": "1.0.0",
  "adjustments": [
    { "entity": "offense_5", "dx": -0.5, "dy": 0.0, "note": "tighter to the block" }
  ]
}

14.2. meta.based_on_play

Records that this play is a variant, progression, or counter of another play. Direction is fixed: the referenced play is always the base; this document is the derivative. A single optional reference (not an array).

relationship Meaning (read as "this play is …")

variant

a tactical variation of the base play

progression

a training step built from the base play

counter

a response to a specific defense against the base play

"based_on_play": {
  "id": "7c9e4f2a-1b3d-4a6e-8f0c-2d5e9a1b3c7f",
  "title": "Pick the Picker BLOB",
  "source": "https://opencoachingformat.org/playbooks/hoopsgeek-classics/index.json",
  "relationship": "variant"
}

14.3. meta.min_schema_version

min_schema_version (optional, SemVer) declares the minimum schema version needed to validate the document correctly. Set it when the document uses a field introduced in a specific minor, so a validator whose bundled schema is older can warn that it may be out of date rather than reporting the newer fields as errors. Omit it otherwise. It never changes how a current validator validates a document — it is a signal for version-skew diagnostics.

"meta": { "id": "...", "title": "...", "min_schema_version": "1.4.0" }

15. Schema Reference

15.1. Top-Level Structure

Drillmeta: Metacourt: Courtentities: Entity[]balls: Ball[]frames: Frame[]color_scheme: ColorScheme?named_positions: NamedPositions?areas: Area[]?labels: Label[]?Metaid: uuidtitle: stringdescription: string?author: string?tags: string[]?difficulty: enum?created: datetime?source_format: enum?min_schema_version: semver?Courtruleset: enumtype: enumdrill_focus: enumwheelchair: booleancustom_dimensions: CustomDimensions?Entitytype: enumx: numbery: numbernr: integer?rotation: number?color: ColorRole?label: string?Frameid: stringactions: Action[]end_state: Statelabel: string?description: string?duration_ms: integer?start_state: State?branches: map?Actionplayer: EntityReftype: enumball_id: string?moves: Move[]?variant: string?tags: string[]?Coordinate«oneOf»FreeCoordinateNamedCoordinateRelativeCoordinate

15.1.1. Top-Level Fields

Field Required Description

meta

yes

Drill metadata (id, title, author, tags, etc.).

court

yes

Ruleset, court type, drill focus.

entities

yes

Non-ball entities: offense, defense, coach, cones, stations.

balls

yes

Array of balls; each ball has id + one of carried_by, at, or dead.

frames

yes

Ordered coaching phases; each carries actions and end_state.

color_scheme

no

Override FIBA default color roles.

named_positions

no

Custom named court positions.

areas

no

Highlighted zones on the court.

labels

no

Free text annotations on the court.

15.2. Coordinate oneOf

«oneOf»CoordinateFreeCoordinatex: numbery: numberNamedCoordinatenamed: stringRelativeCoordinaterelative_to: stringdx: numberdy: number

15.3. Enums Summary

Enum Values

ruleset

fiba, nba, ncaa, nfhs, custom

court.type

half_court, full_court

drill_focus

offense, defense, transition, neutral

difficulty

beginner, intermediate, advanced

source_format

fiba, fastdraw, open, custom

entity.type

offense, defense, coach, cone, station

area.form

rectangle, ellipse, triangle

color_role

offense, defense, black, grey, yellow, green, red, blue, white

unit

m, ft

Action type

move, cut, screen, defend, dribble, pass, shoot, rebound, pickup, tackle, clear, faceoff, check (valid subset depends on sport — see the Sport section)

Cut variant

backdoor, give_and_go, flash, v_cut, l_cut, curl, flare, fade, basket

Screen variant

ball_screen, back_screen, down_screen, flare_screen, cross_screen, pin_down

Defend variant

on_ball, deny, help, hedge, switch, box_out

Dribble per-move variant

speed, hesitation, crossover, behind_back, between_legs, spin, retreat

Pass variant

chest, bounce, overhead, lob, baseball, hand_off, outlet

Shoot variant

jumper, three, layup, floater, dunk, hook, free_throw

Rebound variant

offensive, defensive

Frame outcome

make, miss, turnover, steal, foul

movement_intensity (move/cut/dribble intensity)

slow, normal, fast, explosive

ball_intensity (pass/shoot intensity)

soft, normal, hard, bullet

physicality (screen/defend/rebound/pickup)

passive, normal, aggressive, hard

Per-move variant values are recommended vocabulary; the schema accepts any string for a move’s variant so renderers can fall back gracefully.

16. LLM Generation Guide

16.1. Prompt Pattern

{ocf} is designed so that LLMs can generate valid drill JSON from natural language descriptions. The key is the named position vocabulary — it grounds spatial references in a controlled set of terms.

Example prompt structure:

Generate an Open Coaching Format v1 JSON for the following drill:

Ruleset: FIBA, Half Court, Offense
Title: "3-Man Weave"

Description:
offense_1 starts at midcourt.center with the ball.
offense_2 starts at inbound.sideline_left_mid.
offense_3 starts at inbound.sideline_right_mid.

Step 1: offense_1 passes to offense_2, cuts behind toward the right sideline.
Step 2: offense_2 passes to offense_3 who has filled the center lane.
Step 3: offense_3 finishes at the basket.

Output only valid JSON matching https://opencoachingformat.org/schema/v1.json

16.2. LLM Output Tips

  • Actions express intent, not geometry — prefer named positions in end_state over raw coordinates wherever a standard name applies

  • Leave optional action fields out when the description is vague; add variant, ball_id, and timing only when the description implies them

  • Use relative_to for positions defined as offsets ("2 meters right of the elbow")

  • Every frame requires an end_state; list only the entities whose positions change in that frame (others inherit)

  • Ball state in end_state.balls must reflect the result of the actions: use carried_by after a pass/pickup, dead after a made shot with no rebound

  • Set the ball dead in end_state.balls after a shoot with result: "make" if no rebound or pickup follows in that frame or the next

  • Frame description should mirror the coaching instruction in natural language — LLMs should always generate this field

16.3. Validation

After LLM generation, validate against the JSON Schema:

npx ajv-cli validate \
  -s opencoachingformat-v1.schema.json \
  -d my_drill.json

Appendix A: FIBA Import Mapping

FIBA Field OCF Field Notes

id (GUID)

meta.id

GUID → UUID

fullCourt

court.type

truefull_court

wheelchair

court.wheelchair

Direct

dynamicElement.type

entity.type

See type map below

dynamicElement.nr

entity.nr

Direct

dynamicElement.color

entity.color

See color map below

dynamicElement.x

entity.x

(fiba_x - 192) / 192 * 7.5

dynamicElement.y

entity.y

(270 - fiba_y) / 270 * 14.0

lineElement.type

frame.actions[].type

See action type map below

lineElement.curved

— (derived from action type)

Renderer infers path style

lineElement.coords

frame.actions[].moves[].to

Coordinate conversion each point

staticElement (area)

areas[]

form, color, position

staticElement (text)

labels[]

text, position

A.1. FIBA Dynamic Element Type Map

FIBA type OCF entity.type

0 (offence)

offense

1 (defence)

defense

2 (ball)

— (map to balls[] entry with carried_by or at)

3 (coach)

coach

4 (cone)

cone

5 (shooting)

— (map to shoot action)

6 (handoff)

— (map to pass action with variant: "hand_off")

A.2. FIBA Line Type Map

FIBA type OCF action type

0 (movement)

move or cut

1 (passing)

pass

2 (dribbling)

dribble

3 (screen)

screen

4 (line)

— (annotate with tags)

5 (free)

— (annotate with tags)

A.3. FIBA Color Map

FIBA color FIBA name OCF color_role

-1

unknown

black

0

yellow

yellow

1

green

green

2

red

red

3

grey

grey

4

black

black

5

blue

blue

6

offence

offense

7

defence

defense

Appendix B: Changelog

B.1. v1.4.0

Adds schema version signals (RFC 0005): optional min_schema_version in meta, a machine-readable x-ocf-version keyword, and a published versions.json manifest. All additive and backwards-compatible; documents without the new field are unchanged.

B.2. v1.3.0

Adds optional side (left/right) and arc (tight/normal/wide) fields to movement steps (and the cut action) to make an around_player detour’s side and tightness explicit instead of renderer-guessed (RFC 0004). Additive and backwards-compatible.

B.3. v1.2.0

Adds the optional sport field and per-sport action-type scoping (RFC 0003). Basketball is fully defined; soccer/handball/hockey/futsal are reserved with provisional vocabularies. Additive and backwards-compatible — documents without a sport field validate as basketball.

B.4. v1.1.1

Tooling and distribution fixes only; no schema shape change. Publishes the schema to its canonical $id URL (https://opencoachingformat.org/schema/v1.json), which previously 404’d; records the schema version in a $comment; and adds provenance/repository metadata for npm trusted publishing. Documents remain valid against v1.1.0 unchanged.

B.5. v1.1.0

Adds the External References convention and applies it twice under meta: based_on_formation (starting-formation registry reference with optional per-entity adjustments) and based_on_play (play lineage — variant/progression/counter). Promotes the named-position catalog to structured data (positions/{fiba,nba,ncaa,nfhs}-v1.json) exported via resolveNamedPosition. Additive and backwards-compatible.

B.6. v1.0.0

The first stable Open Coaching Format. Semantic action model: per-player actions (move, cut, screen, defend, dribble, pass, shoot, rebound, pickup) replace the earlier line-based frames. The ball entity is replaced by a top-level balls array with a carried_by/at/dead lifecycle and automatic possession transfer. Frames gain explicit start_state/end_state anchors and outcome branches. Actions carry optional intensity (movement_intensity for move/cut/dribble, ball_intensity for pass/shoot) and physicality (screen/defend/rebound/pickup) enums; frame-level duration_ms is deprecated in favor of per-action intensity but remains valid. Retains the FIBA-compatible entity model, the real-unit coordinate system (meters/feet, origin at midcourt), the named-position registry (FIBA, NBA, NCAA, NFHS) with a custom escape hatch, and semantic color roles.

B.7. v0.1.0 (2025-02-27)

Initial pre-stable draft — a line-based geometric format (movement/passing/ screen lines, entity delta states per frame). Superseded by the v1.0.0 semantic action model; retained here for historical context only.