# Tabelloni, overlay e regolamenti sportivi Questo documento descrive come Match Live TV modella gli sport, applica i regolamenti di punteggio e li traduce in **tabelloni** (logica + controlli) e **overlay** (grafica sul video in diretta). --- ## Panoramica Il sistema separa tre livelli che spesso vengono confusi: | Livello | Cosa rappresenta | Dove vive | Esempio basket | |--------|-------------------|-----------|------------------| | **Sport** (`sport_key`) | La disciplina scelta per squadra/partita | `config/sports.yml`, DB | `basket` | | **Board** (`board_type`) | Il *tipo di tabellone* — logica di punteggio e controlli | Derivato dal catalogo, `score_states.board_type` | `basket` | | **Overlay** (`overlay_kind`) | Il *layout grafico* sul video | Per partita, validato dal catalogo | `basket` o `none` | In sintesi: - Il **regolamento** definisce *come si segna* (set da vincere, punti per set, durata quarti…). - Il **board** è il motore che applica quel regolamento. - L’**overlay** è solo la resa visiva sullo stream; può essere disattivato (`none`) anche quando il board resta attivo in regia. ```mermaid flowchart LR subgraph catalogo [Catalogo sport] SK[sport_key] BR[board_type] OV[overlay default] DR[default_rules] end subgraph partita [Partita] M[Match] OK[overlay_kind opzionale] SR[scoring_rules opzionale] end subgraph runtime [Sessione live] SS[ScoreState] ENG[Scoring Engine] OL[Overlay video] end SK --> BR SK --> OV SK --> DR M --> SK M --> OK M --> SR BR --> ENG DR --> ENG SR --> ENG ENG --> SS SS --> OL OK --> OL ``` --- ## Catalogo sport e regolamenti Il catalogo è definito in [`backend/config/sports.yml`](../backend/config/sports.yml) e caricato da `Sports::Catalog`. Non esiste una tabella `sports` nel database: lo sport è una **configurazione versionata** esposta via API (`GET /api/v1/sports`). Per ogni disciplina il catalogo specifica: | Campo | Significato | |-------|-------------| | `label` | Nome visualizzato (es. «Pallavolo») | | `board` | Tipo tabellone implicito | | `overlay` | Overlay video predefinito | | `allowed_overlays` | Overlay ammessi sulla partita | | `default_rules` | Regolamento standard di quello sport | ### Tipi di regolamento (`default_rules`) Gli sport si raggruppano in tre famiglie di regole, validate da `Sports::RulesSchema`: #### Sport a set e punti (`board: volley` o `racket`) | Chiave | Significato | Esempio pallavolo | |--------|-------------|-------------------| | `sets_to_win` | Set necessari per vincere la partita | 3 (best of 5) | | `points_per_set` | Punti per vincere un set «normale» | 25 | | `points_deciding_set` | Punti dell’ultimo set (tie-break) | 15 | | `min_point_lead` | Vantaggio minimo per chiudere il set | 2 | **Sport che usano queste regole:** pallavolo, beach volley, sitting volley, tennis, tennis tavolo, badminton, padel, pickleball. La logica è in `Scoring::Rules` (`set_winner_from_points`, `match_winner`): un set si vince al raggiungimento del target (`points_per_set` o `points_deciding_set` sull’ultimo set possibile) **con almeno** `min_point_lead` punti di distacco. #### Sport a tempi e periodi (`board: basket` o `timed`) | Chiave | Significato | Esempio basket | Esempio calcio a 5 | |--------|-------------|----------------|---------------------| | `periods` | Numero di periodi regolamentari | 4 quarti | 2 tempi | | `period_duration_secs` | Durata iniziale del cronometro per periodo | 600 (10 min) | 1200 (20 min) | | `overtime_duration_secs` | Durata supplementare / OT | 300 (5 min) | 300 | **Sport `timed`:** calcio a 5, pallamano, hockey indoor, floorball. Il cronometro è gestito dal motore (`clock_toggle`, `clock_reset`, `clock_tick`, `advance_period`) e memorizzato in `score_states.data` (JSONB). #### Sport senza punteggio a referto (`board: timer` o `generic`) | Board | Uso | Regole | Overlay default | |-------|-----|--------|-----------------| | `timer` | Ginnastica, danza, cheerleading, pattinaggio | Nessuna (`{}`) | `none` | | `generic` | «Altro» | Nessuna | `none` | Per questi sport la regia può usare un cronometro (`TimerEngine`) ma **non** viene sovrapposto un tabellone sul video. --- ## Board type: il motore di punteggio Il `board_type` non si sceglie manualmente in creazione partita: deriva sempre dallo `sport_key` tramite `Match#effective_board_type` → `Sports::Catalog.board_for`. `Scoring::Engine.for_match(match)` seleziona l’engine: | Board | Classe engine | Azioni principali | |-------|---------------|-------------------| | `volley` | `VolleyEngine` | `home_point`, `away_point`, `home_undo`, `away_undo`, `close_set` | | `racket` | `RacketEngine` | Come volley (fase 1 — stessa logica a set) | | `basket` | `BasketEngine` | `+1/+2/+3`, undo, `advance_period`, cronometro | | `timed` | `TimedEngine` | `+1`, undo, periodi, cronometro | | `timer` | `TimerEngine` | Solo cronometro (conteggio crescente) | | `generic` | `GenericEngine` | `+1` / undo senza chiusura set | ### Come il regolamento entra nel motore All’avvio di ogni azione, l’engine costruisce le regole effettive con: ```ruby match.effective_scoring_rules # => default_rules del catalogo deep_merge scoring_rules della partita ``` Quindi: 1. **Default dello sport** — es. pallavolo: 25 punti, set decisivo a 15. 2. **Override partita** — campo `matches.scoring_rules` (JSON) per deroghe locali (categoria giovanile, torneo ridotto…). 3. **Legacy** — per il volley, `matches.sets_to_win` viene fuso in `effective_scoring_rules` se presente. Il volley applica automaticamente la vittoria di set quando il punteggio soddisfa `Rules#set_winner_from_points`; `close_set` forza la chiusura manuale (utile se si preferisce confermare a bordo campo). --- ## Overlay kind: la grafica sul video L’overlay è **indipendente** dal motore di scoring: descrive *cosa disegnare* sul frame, non *come calcolare* i punti. ### Valori ammessi Definiti in `Sports::OverlayKind`: `none`, `volley`, `basket`, `timed`, `racket`. > Il valore legacy `timer` come overlay è mappato a `none` in `Match#effective_overlay_kind`. ### Scelta per partita - Campo opzionale `matches.overlay_kind`. - Se assente → default del catalogo (`Sports::Catalog.overlay_for`). - Deve essere incluso in `allowed_overlays` dello sport, altrimenti si ripiega sul default. Esempio: il basket ammette `[basket, none]` — si può trasmettere con tabellone o solo watermark. ### Layout Android (pipeline video) Sull’app nativa l’overlay è composito in GPU senza ricodifica aggiuntiva: 1. `BroadcastScreen` costruisce un `OverlayState` a partire da `effectiveOverlayKind` e `ScoreState`. 2. `OverlayRenderer` → `OverlayCanvasRenderer` disegna gli elementi su bitmap trasparente. 3. Il bitmap viene applicato come filtro OpenGL sul flusso RTMP. Elementi grafici (`OverlayCanvasRenderer`): | Elemento | Quando è attivo | |----------|-----------------| | `WatermarkElement` | Sempre tranne `overlay_kind = none` | | `ScoreboardElement` | `volley`, `racket` — tabellone a colonne per set | | `CompactScoreboardElement` | `basket`, `timed` — squadre ai lati, punteggio centrale, etichetta periodo | | `SponsorElement` | Se configurato | **Nota:** il cronometro non è più renderizzato nell’overlay video (basket/timed mostrano solo periodo, es. `Q2` o `2° tempo`). Il cronometro resta disponibile nella **regia** e nei controlli operatore, non sullo stream. ### Mappatura overlay ← punteggio | Overlay | Struttura dati overlay | Fonte dati | |---------|------------------------|------------| | `volley` / `racket` | `ScoreboardState` (colonne set) | `set_partials`, `current_set`, punti correnti | | `basket` / `timed` | `CompactScoreboardState` | `home_score` / `away_score` in `data`, `periodLabel` | | `none` | Solo watermark / stato LIVE opzionale | — | --- ## Stato tabellone (`ScoreState`) Ogni `StreamSession` ha al massimo uno `ScoreState`, creato all’apertura sessione (`Sessions::Create` → `Scoring::Engine.ensure_score_for`). ### Campi condivisi | Campo | Uso | |-------|-----| | `board_type` | Snapshot del board al momento della creazione | | `home_sets` / `away_sets` | Set vinti (volley/racket) | | `home_points` / `away_points` | Punti set corrente **oppure** mirror del punteggio basket/timed | | `current_set` | Set in corso | | `set_partials` | Storico parziali `[{set, home, away}, …]` | | `data` (JSONB) | Stato esteso basket/timed/timer | ### Payload WebSocket `ScoreState#as_cable_payload` arricchisce il JSON in base al board: - **volley / racket:** set, punti, parziali. - **basket / timed:** `home_points`/`away_points` come punteggio totale, `period`, `clock_secs`, `clock_running`. - **timer:** solo cronometro. L’app mobile e la regia web si sottoscrivono al canale `SessionChannel` e ricevono `score_update` dopo ogni azione. --- ## Regia web (controlli operatore) La pagina regia (`public/regia`) usa lo stesso motore di scoring dell’API, con partial diversi per board: | Partial | Board | Controlli tipici | |---------|-------|------------------| | `_volley.html.erb` | volley | +1, undo, chiudi set, hint punti target | | `_racket.html.erb` | racket | Come volley | | `_basket.html.erb` | basket | +1/+2/+3, cronometro, avanza quarto | | `_timed.html.erb` | timed | +1, cronometro, avanza tempo | | `_timer.html.erb` | timer | Solo cronometro | | `_generic.html.erb` | generic | Contatore punti semplice | Il partial viene scelto in base a `data-board` sul container `#regia-app`. I pulsanti inviano azioni al controller regia, che chiama `Scoring::ApplyAction`. --- ## Flusso end-to-end ``` 1. Creazione partita team.sport_key → match.sport_key opzionale: match.overlay_kind, match.scoring_rules 2. Avvio sessione (POST /sessions) → ScoreState inizializzato con default_data dell’engine → Per basket/timed: clock_secs = period_duration_secs del regolamento 3. Azione operatore (app, regia, API) POST score_action / WebSocket → Scoring::ApplyAction → Engine.apply → ScoreState aggiornato + broadcast Action Cable 4. Rendering overlay (solo app camera / relay grafico) ScoreState + Match.effectiveOverlayKind → OverlayState → bitmap → filtro GL sul video 5. Spettatori Vedono il video con overlay già composito (non ricevono il tabellone come layer separato) ``` --- ## Tabella sport → board → overlay → regolamento | sport_key | Board | Overlay default | Regolamento chiave | |-----------|-------|-----------------|-------------------| | pallavolo | volley | volley | 3 set, 25 pt, 5° set a 15, +2 | | beach_volley | volley | volley | 2 set, 21 pt, 3° set a 15 | | sitting_volley | volley | volley | Come pallavolo | | basket | basket | basket | 4×10 min, OT 5 min | | calcio_a_5 | timed | timed | 2×20 min, suppl. 5 min | | pallamano | timed | timed | 2×30 min | | hockey_indoor | timed | timed | 2×20 min | | floorball | timed | timed | 3×20 min | | tennis | racket | racket | 2 set, 6 game, tie-break 7 | | tennis_tavolo | racket | racket | 3 set, 11 pt | | badminton | racket | racket | 2 set, 21 pt | | padel | racket | racket | Come tennis | | pickleball | racket | racket | 2 set, 11 pt | | ginnastica_* / danza / cheer / pattinaggio | timer | none | Nessun punteggio | | altro | generic | none | Contatore libero | --- ## Personalizzazione per partita | Campo DB | Effetto | |----------|---------| | `sport_key` | Determina board, overlay default e regole base | | `overlay_kind` | Cambia solo la grafica sul video (entro `allowed_overlays`) | | `scoring_rules` | Sovrascrive i numeri del regolamento (es. `points_per_set: 21` su pallavolo giovanile) | | `sets_to_win` | Legacy; fuso in `effective_scoring_rules` per il volley | Esempio API partita: ```json { "sport_key": "pallavolo", "overlay_kind": "none", "scoring_rules": { "points_per_set": 21, "sets_to_win": 2 } } ``` → Regia e motore usano regole da 21 punti; lo stream non mostra tabellone (`none`). --- ## Relazione regolamento ↔ UI ↔ overlay | Aspetto | Governato da | Visibile in overlay? | |---------|--------------|----------------------| | Vittoria set (25+2) | `Scoring::Rules` + `VolleyEngine` | Sì — colonne set / parziali | | Vittoria partita (3 set) | `match_winner` | Sì — conteggio set | | Punti basket (+2/+3) | `BasketEngine` | Sì — punteggio centrale | | Durata quarto | `period_duration_secs` | No — solo etichetta periodo (`Q1`…) | | Cronometro | `score_states.data` | No sull’overlay (sì in regia) | | Sport ginnico | `TimerEngine` | No overlay punteggio | Questa separazione è intenzionale: il regolamento alimenta sempre il **modello dati** (`ScoreState`); l’**overlay** sceglie quali campi mostrare sul video. --- ## File di riferimento | Area | Percorso | |------|----------| | Catalogo sport | `backend/config/sports.yml` | | API catalogo | `backend/app/domain/sports/catalog.rb` | | Validazione regole | `backend/app/domain/sports/rules_schema.rb` | | Logica regolamento | `backend/app/services/scoring/rules.rb` | | Engine | `backend/app/services/scoring/engine.rb`, `engines/*` | | Modello partita | `backend/app/models/match.rb` | | Stato punteggio | `backend/app/models/score_state.rb` | | Overlay Android | `native/android/.../streaming/overlay/*` | | Schermata broadcast | `native/android/.../ui/broadcast/BroadcastScreen.kt` | | Regia web | `backend/app/views/public/regia/_*.html.erb` | Documento correlato: [`MULTI_SPORT.md`](./MULTI_SPORT.md) (panoramica più sintetica della migrazione multi-sport). --- ## Limitazioni attuali 1. **`racket` = `volley` a livello motore** — tennis/padel usano logica a set+punti numerici; non c’è ancora scoring «15-30-40» o tie-break automatico stile tennis. 2. **Cronometro non in overlay** — per basket/timed il tempo è gestito in regia ma non bruciato sul video. 3. **Overlay solo lato publisher** — lo spettatore vede il grafico composito nel flusso; non c’è tabellone HTML separato sulla pagina live pubblica. 4. **Regolamenti = parametri numerici** — il sistema non incorpora testi di regolamento FIPAV/FIBA; implementa le regole standard dichiarate in `sports.yml`, personalizzabili per partita. Per aggiungere uno sport nuovo: estendere `sports.yml`, verificare che `board` e `allowed_overlays` siano coerenti, e — se serve — aggiungere un partial regia e un ramo in `BroadcastScreen` / `OverlayCanvasRenderer`.