Open Challenge Protocol (OCP) | Ride Cave

Specifica

Open Challenge Protocol

Un formato JSON dichiarativo per descrivere sfide di fitness tra piattaforme diverse.

BozzaVersione 0.1.0

Panoramica

L'Open Challenge Protocol (OCP) è un formato JSON dichiarativo per descrivere sfide di fitness. Definisce cosa costituisce il successo, non come un'applicazione host debba implementare l'esperienza.

Una sfida è un obiettivo strutturato che valuta la prestazione del ciclista rispetto a criteri definiti, con fasi del ciclo di vita, direttive di runtime e logica di iterazione/loop opzionali.

Sfide e allenamenti a confronto

OCP è pensato specificamente per le Sfide, non per gli allenamenti. Capire questa distinzione è fondamentale.

Sfide

  • Dinamico e orientato all'obiettivo: Il successo si ottiene soddisfacendo criteri specifici
  • Basati sulla logica: Costruito su condizioni, soglie e criteri di uscita
  • Durata spesso indeterminata: Molte terminano quando non riesci a mantenere una condizione
  • Pass/fail o record: O raggiungi l'obiettivo o gareggi contro il tuo record personale

Esempi

  • Tieni 5.0 w/kg per 10 secondi
  • Sprinta sui 200 m il più veloce possibile
  • Percorri la distanza più lunga possibile tenendo la FC sotto la zona 4
  • Accelera da fermo a 40 km/h

Allenamenti

  • Strutturati e predeterminati: Segui una sequenza fissa di intervalli
  • Basati su tempo/durata: Gli intervalli hanno durate precise
  • Completamento = successo: Se completi tutti gli intervalli, hai "completato" l'allenamento
  • Orientati all'allenamento: Pensato per l'adattamento fisiologico

Non adatto a OCP

  • 5x (30 secondi on / 30 secondi off)
  • Tabata: 8x (20 secondi / 10 secondi)
  • Intervalli a scala 1-2-3-2-1 minuti
  • 2x20 minuti a FTP

Nel dubbio: Chiediti: "Il successo dipende da come si comporta il ciclista, o solo dal fatto che completi la struttura?" Se il successo dipende dal mantenere delle condizioni → Sfida. Se il successo è solo completare gli intervalli → Allenamento.

Struttura della sfida

Una sfida è un oggetto JSON con i seguenti campi di primo livello:

{
  "id": "string",
  "version": "0.1.0",
  "name": "string",
  "description": "string (optional)",
  "sport": "cycling | running | rowing | any (optional)",

  "mode": "record | pass-fail",

  "config": { },
  "phases": [ ],
  "criteria": { },
  "loops": [ ],
  "directives": [ ],
  "record": { },
  "completion": { },

  "metadata": { }
}

Campi obbligatori

CampoTipoDescrizione
idstringIdentificatore univoco di questa sfida
versionstringVersione del protocollo (semver)
namestringNome della sfida leggibile
phasesarrayElenco ordinato delle fasi della sfida (OPPURE criteria per le sfide semplici)

Campi facoltativi

CampoTipoDescrizione
descriptionstringDescrizione più lunga della sfida
sportstringTipo di sport (cycling, running, rowing, any)
modestringrecord (miglior risultato) oppure pass-fail (successo binario). Predefinito: pass-fail
configobjectConfigurazione e valori calcolati a livello di sfida
criteriaobjectCriteri di successo (e facoltativamente di fallimento) per le sfide semplici
loopsarrayBlocchi di valutazione ripetuti
directivesarrayComportamenti di runtime (notifiche, suggerimenti)
recordobjectQuali metriche registrare come risultato della sfida
completionobjectCome termina la sfida e cosa mostrare
metadataobjectSuggerimenti di visualizzazione, difficoltà, tag, ecc.

Modalità della sfida

pass-fail (predefinito): sfida tradizionale con criteri di successo/fallimento. Esito binario.

record: sfida in cui l'obiettivo è ottenere il miglior risultato possibile. Nessuno stato di fallimento: la sfida registra la tua prestazione.

Metriche

Le metriche sono i dati canonici che le sfide valutano. Le applicazioni host mappano i propri dati grezzi su questi percorsi.

Formato del percorso metrico

Le metriche usano la notazione a punti: category.metric

power.instant     → Current power in watts
power.wkg         → Current power in watts per kilogram
lap.index         → Current lap number (0-indexed or 1-indexed per host)
position.current  → Race position (1 = first place)

Metriche di base

Tempo

PercorsoTipoDescrizione
time.elapsednumberSecondi dall'inizio della sfida
time.sessionnumberSecondi dall'inizio della sessione

Potenza

PercorsoTipoDescrizione
power.instantnumberPotenza attuale (watt)
power.avgnumberPotenza media della sessione
power.normalizednumberNormalized Power
power.wkgnumberWatt per chilo attuali
power.zonenumberZona di potenza attuale (1-7)
power.percentFtpnumberPotenza attuale in % della FTP

Velocità, cadenza, frequenza cardiaca

PercorsoTipoDescrizione
speed.instantnumberVelocità attuale (m/s o km/h a seconda dell'host)
cadence.instantnumberCadenza attuale (RPM)
heartRate.instantnumberFrequenza cardiaca attuale (BPM)
heartRate.zonenumberZona FC attuale (1-5)

Distanza, giro, pista

PercorsoTipoDescrizione
distance.totalnumberDistanza totale (metri)
lap.indexnumberNumero del giro attuale
lap.countnumberGiri totali completati
track.lengthnumberLunghezza della pista (metri)

Metriche di gara/multiplayer

Sono disponibili solo nei contesti di gara:

PercorsoTipoDescrizione
position.currentnumberPosizione attuale in gara (1 = primo)
gap.aheadnumberDistacco dal ciclista davanti
gap.behindnumberDistacco dal ciclista dietro
overtakes.countnumberNumero di sorpassi effettuati

Fasi

Le fasi definiscono la struttura di una sfida come sequenza ordinata di segmenti. Ogni fase ha la sua durata, i suoi obiettivi, le sue condizioni di uscita e le sue direttive.

Struttura della fase

{
  "id": "effort",
  "name": "Max Effort",
  "duration": 30,
  "description": "Give everything for 30 seconds",
  "targets": { },
  "exitConditions": { },
  "directives": [ ]
}
CampoTipoObbligatorioDescrizione
idstringSìIdentificatore univoco di questa fase
namestringSìNome visualizzato
durationnumber/stringNoDurata della fase in secondi, oppure "unlimited"
distancenumberNoDistanza della fase in metri (alternativa alla durata)
targetsobjectNoMetriche obiettivo per questa fase
exitConditionsobjectNoCondizioni che chiudono prima questa fase

Obiettivi

Gli obiettivi definiscono le metriche previste o richieste durante una fase:

{
  "targets": {
    "power": { "min": 80, "max": 150 },
    "cadence": { "min": 90, "max": 100 }
  }
}

Obiettivi a rampa

{
  "power": {
    "mode": "ramp",
    "start": 100,
    "increment": 1,
    "per": "second"
  }
}

Condizioni

Una condizione è una singola espressione verificabile che restituisce vero o falso.

Struttura della condizione

{
  "metric": "power.wkg",
  "operator": ">=",
  "value": 5.0,
  "duration": 10,
  "window": "sustained"
}
CampoTipoObbligatorioDescrizione
metricstringSìPercorso metrico da valutare
operatorstringSìOperatore di confronto
valuenumber/arraySìValore/i obiettivo
durationnumberNoVincolo di tempo in secondi
windowstringNoCome viene valutata la durata

Operatori

Confronto

  • == Uguale a
  • != Diverso da
  • > Maggiore di
  • < Minore di
  • >= Maggiore o uguale a
  • <= Minore o uguale a

Intervallo

  • between Intervallo inclusivo
  • outside Fuori intervallo

Cambia

  • increasedBy Aumento delta
  • decreasedBy Diminuzione delta

Finestre di durata

FinestraDescrizione
sustainedLa condizione deve essere vera in modo continuo per la durata (predefinito)
cumulativeLa condizione deve essere vera per la durata totale (anche non continua)
rollingValutato su una finestra mobile della durata indicata
anyLa condizione deve essere vera in un momento qualsiasi per la durata

Porte logiche

Le condizioni si possono combinare con porte logiche.

Tipi di porta

CancelloDescrizione
allTutte le condizioni devono essere vere (AND)
anyAlmeno una condizione deve essere vera (OR)
noneNessuna condizione può essere vera (NOT ANY)
sequenceLe condizioni devono diventare vere in ordine

Esempio: porte annidate

{
  "all": [
    { "metric": "time.elapsed", "operator": ">=", "value": 60 },
    {
      "any": [
        { "metric": "power.wkg", "operator": ">=", "value": 5.0 },
        { "metric": "position.current", "operator": "==", "value": 1 }
      ]
    }
  ]
}

"Dopo 60 secondi, raggiungi 5.0 w/kg OPPURE sii in prima posizione"

Ciclo di vita

Il ciclo di vita definisce quando inizia e quando finisce la valutazione della sfida.

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   PENDING   │ ──▶ │   ARMING    │ ──▶ │   ACTIVE    │ ──▶ │  COMPLETE   │
└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘
                          │                    │
                          │                    ▼
                          │              ┌─────────────┐
                          └────────────▶ │   FAILED    │
                                         └─────────────┘
FaseDescrizione
pendingSfida caricata ma non avviata
armingIn attesa delle condizioni di partenza
activeValutazione dei criteri di successo/fallimento
completeCriteri di successo raggiunti
failedCriteri di fallimento raggiunti o sfida abbandonata

Struttura del ciclo di vita

{
  "lifecycle": {
    "arming": {
      "all": [
        { "metric": "time.elapsed", "operator": ">=", "value": 120 }
      ]
    },
    "timeout": 600,
    "maxAttempts": 3
  }
}

Criteri di successo e fallimento

Struttura dei criteri

{
  "criteria": {
    "success": {
      "all": [
        { "metric": "power.wkg", "operator": ">=", "value": 5.0, "duration": 10 }
      ]
    },
    "failure": {
      "any": [
        { "metric": "power.instant", "operator": "==", "value": 0, "duration": 5 }
      ]
    }
  }
}

Sfide con solo criteri di successo

La maggior parte delle sfide definisce solo criteri di successo. La sfida continua fino al successo o finché l'utente non la abbandona.

Sfide con condizioni di fallimento

{
  "criteria": {
    "success": {
      "all": [
        { "metric": "lap.count", "operator": ">=", "value": 5 }
      ]
    },
    "failure": {
      "any": [
        { "metric": "position.current", "operator": ">", "value": 3 }
      ]
    }
  }
}

"Completa 5 giri restando nei primi 3. Scendi al 4° posto o peggio = fallito."

Loop

I loop permettono di ripetere blocchi di valutazione con una logica che tiene conto dell'iterazione. Sono essenziali per sfide con più intervalli, difficoltà progressiva o azioni ripetute.

Struttura del loop

{
  "loops": [
    {
      "id": "sprint_intervals",
      "count": 5,
      "trigger": {
        "metric": "lap.index",
        "operator": "increasedBy",
        "value": 1
      },
      "onIteration": {
        "directives": [ ],
        "criteria": { }
      },
      "onComplete": {
        "directives": [ ]
      }
    }
  ]
}

Variabili di contesto dell'iterazione

VariabileDescrizione
{{loopIndex}}Iterazione attuale (a partire da 0)
{{loopCount}}Iterazioni totali configurate
{{loopRemaining}}Iterazioni rimanenti

Esempio di difficoltà progressiva

{
  "loops": [
    {
      "id": "progressive_power",
      "count": 5,
      "trigger": {
        "metric": "time.elapsed",
        "operator": ">=",
        "value": "{{loopIndex * 60}}"
      },
      "onIteration": {
        "directives": [
          {
            "type": "notify",
            "message": "Interval {{loopIndex + 1}}/{{loopCount}}: Hold {{3.0 + loopIndex * 0.5}} w/kg"
          }
        ]
      }
    }
  ]
}

"5 intervalli a potenza crescente: 3.0, 3.5, 4.0, 4.5, 5.0 w/kg"

Direttive

Le direttive sono comportamenti di runtime facoltativi che l'applicazione host può eseguire. Sono consultivo: è l'host a decidere se applicarle.

Struttura delle direttive

{
  "directives": [
    {
      "id": "gap_warning",
      "type": "notify",
      "when": {
        "metric": "gap.ahead",
        "operator": ">",
        "value": 3.0
      },
      "params": {
        "message": "Gap widening! Push harder.",
        "priority": "high"
      },
      "cooldown": 10
    }
  ]
}

Tipi di direttiva

notify

Mostra un messaggio al ciclista

{
  "type": "notify",
  "params": {
    "message": "Push harder!",
    "priority": "normal | high | critical"
  }
}

sound

Riproduci un segnale audio

{
  "type": "sound",
  "params": {
    "sound": "bell | chime | warning | success"
  }
}

suggest

Suggerisci un obiettivo al ciclista (consultivo)

{
  "type": "suggest",
  "params": {
    "metric": "power.instant",
    "target": 250
  }
}

highlight

Evidenzia una metrica nell'interfaccia

{
  "type": "highlight",
  "params": {
    "metric": "power.wkg",
    "style": "pulse | glow | flash"
  }
}

Record (sfide al miglior risultato)

Per le sfide mode: "record", l'oggetto record definisce quali metriche registrare come risultato della sfida.

Struttura del record

{
  "record": {
    "primary": {
      "metric": "power.max",
      "label": "Peak Power",
      "unit": "watts",
      "phase": "effort"
    },
    "secondary": [
      { "metric": "power.peak", "label": "Peak Power", "unit": "watts" },
      { "metric": "cadence.max", "label": "Max Cadence", "unit": "rpm" }
    ]
  }
}

Metriche principali comuni

Tipo di sfidaMetrica principaleEsempio
Sforzo massimopower.maxPotenza di picco
Cronometroduration per la faseTempo sui 200 m lanciati
Tenuta di fondoduration per la faseTempo tenuto in target
Distanzadistance.totalMetri percorsi

Completamento

L'oggetto completion definisce come termina una sfida e cosa mostrare nel riepilogo.

Struttura di completamento

{
  "completion": {
    "condition": "phases-complete | exit-or-phases-complete | criteria-met",
    "summary": {
      "headline": "{{record.primary.value}}W",
      "subheadline": "Peak Power"
    }
  }
}

Condizioni di completamento

CondizioneDescrizione
phases-completeLa sfida termina quando tutte le fasi sono completate
exit-or-phases-completeLa sfida termina alla condizione di uscita di una fase OPPURE quando tutte le fasi sono completate
criteria-metLa sfida termina quando i criteri di successo sono soddisfatti (modalità pass-fail)

Metadati

I metadati forniscono suggerimenti di visualizzazione e categorie per le applicazioni host.

{
  "metadata": {
    "difficulty": 4,
    "estimatedDuration": 120,
    "tags": ["sprint", "power", "short"],
    "author": "Ride Cave",
    "icon": "bolt",
    "color": "#FF6B00",
    "shortDescription": "Hold 5.0 w/kg for 10 seconds",
    "requirements": {
      "powerMeter": true,
      "heartRateMonitor": false
    }
  }
}
CampoTipoDescrizione
difficultynumberValutazione di difficoltà da 1 a 5
estimatedDurationnumberDurata prevista in secondi
tagsarrayTag di categoria
authorstringAutore della sfida
iconstringIdentificatore dell'icona
colorstringColore di accento (hex)
requirementsobjectSensori/attrezzatura necessari

Esempio completo

{
  "id": "power-surge",
  "version": "0.1.0",
  "name": "Power Surge",
  "description": "After a 2-minute warmup, hold 5.0 w/kg for 10 continuous seconds.",
  "sport": "cycling",

  "lifecycle": {
    "arming": {
      "all": [
        { "metric": "time.elapsed", "operator": ">=", "value": 120 }
      ]
    },
    "timeout": 300
  },

  "criteria": {
    "success": {
      "all": [
        {
          "metric": "power.wkg",
          "operator": ">=",
          "value": 5.0,
          "duration": 10,
          "window": "sustained"
        }
      ]
    }
  },

  "directives": [
    {
      "type": "notify",
      "params": {
        "message": "Warmup complete. Go for 5.0 w/kg!"
      },
      "once": true
    },
    {
      "type": "notify",
      "when": {
        "metric": "power.wkg",
        "operator": ">=",
        "value": 4.5
      },
      "params": {
        "message": "Almost there! Push to 5.0!",
        "priority": "high"
      },
      "cooldown": 5
    }
  ],

  "metadata": {
    "difficulty": 4,
    "estimatedDuration": 180,
    "tags": ["sprint", "power", "short"],
    "author": "Ride Cave",
    "shortDescription": "Hold 5.0 w/kg for 10 seconds"
  }
}

Modello di valutazione

Ciclo di valutazione

Il valutatore delle sfide gira a frequenza fissa (consigliata: 10Hz).

┌─────────────────────────────────────────────────────────────┐
│                      EVALUATION TICK                        │
├─────────────────────────────────────────────────────────────┤
│  1. Collect current metrics from host                       │
│  2. Check lifecycle phase                                   │
│     - If PENDING: check arming conditions                   │
│     - If ARMING: check arming conditions → transition       │
│     - If ACTIVE: evaluate criteria + loops + directives     │
│  3. Update loop states                                      │
│  4. Check success criteria                                  │
│  5. Check failure criteria (if defined)                     │
│  6. Fire applicable directives                              │
│  7. Return evaluation result                                │
└─────────────────────────────────────────────────────────────┘

Risultato della valutazione

{
  "phase": "active",
  "success": false,
  "failed": false,
  "progress": {
    "percent": 75,
    "conditions": [
      {
        "metric": "power.wkg",
        "target": 5.0,
        "current": 5.2,
        "durationElapsed": 7.5,
        "durationRequired": 10,
        "met": false
      }
    ]
  },
  "loops": [
    {
      "id": "sprint_intervals",
      "iteration": 2,
      "count": 5,
      "status": "active"
    }
  ],
  "firedDirectives": ["gap_warning"],
  "elapsed": 145.3
}

Appendici

A. Ambiente e portabilità

Le sfide possono fare riferimento a metriche di ambiente, valori forniti dall'applicazione host che descrivono il contesto in cui si svolge la sfida.

Spettro di portabilità

  • Completamente portabile: Le sfide che usano solo metriche di base (potenza, cadenza, frequenza cardiaca, tempo, distanza) funzionano su qualsiasi host conforme
  • Dipendenti dall'ambiente: Le sfide che usano metriche di ambiente richiedono che l'host fornisca quei valori
  • Specifici della piattaforma: Alcune sfide possono fare riferimento a funzioni esclusive di una piattaforma specifica

Valori di riserva

{
  "config": {
    "approachDistance": {
      "value": "{{track.length}}",
      "fallback": 250
    }
  }
}

B. Percorsi metrici riservati

I seguenti percorsi sono riservati per usi futuri:

  • rowing.* - Metriche specifiche del canottaggio
  • running.* - Metriche specifiche della corsa
  • strength.* - Metriche dell'allenamento di forza
  • custom.* - Metriche personalizzate definite dall'host

C. Note di implementazione per l'host

Gli host devono implementare un adattatore di metriche che mappa i propri dati interni sui percorsi metrici OCP:

interface MetricAdapter {
  resolve(path: string): number | undefined;
  getProfile(): { ftp?: number; weight?: number; maxHr?: number };
}

D. Cronologia delle versioni

VersioneDataModifiche
0.1.0-draft2025-01Bozza iniziale della specifica