Files
MatchLiveTv/docs/TABELLONI_E_OVERLAY.md
Emiliano Frascaro 585332e32e Aggiunge app iOS nativa con parità multi-sport e test unitari.
Porting SwiftUI da Android (auth, hub, wizard, broadcast RTMP, overlay, scoring board-aware), reload hub al ritorno da diretta, decodifica score tollerante e documentazione allineata.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 21:37:32 +02:00

354 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 dellultimo 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` sullultimo 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 lengine:
| 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
Allavvio di ogni azione, lengine 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
Loverlay è **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 app mobile (Android e iOS)
Sullapp nativa loverlay è 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 sul flusso RTMP (OpenGL su Android, HaishinKit su iOS).
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 nelloverlay 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 allapertura 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.
Lapp 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 dellAPI, 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 dellengine
→ 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 sulloverlay (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/*` |
| Overlay iOS | `native/ios/MatchLiveTv/Streaming/Overlay/*` |
| Schermata broadcast Android | `native/android/.../ui/broadcast/BroadcastScreen.kt` |
| Schermata broadcast iOS | `native/ios/MatchLiveTv/UI/Broadcast/BroadcastScreen.swift` |
| 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`.