Progetto:BridgeShape API
Documentazione BridgeShape: Motore e frontend · Ricette DSL
BridgeShape — API per Generatori di Smazzate
Documentazione tecnica per chi sviluppa un generatore di smazzate di test che legge i vincoli di distribuzione (shape constraints) dalle convenzioni di BridgePedia.
Panoramica
Ogni pagina wiki di convenzione su BridgePedia può definire nel template
{{Convenzione}} un campo shape_constraints che descrive, in un formato
DSL compatto, i vincoli di distribuzione delle mani dei 4 giocatori al tavolo.
L'estensione BridgeShape espone un'API REST/JSON per leggere questi vincoli in modo strutturato.
API Endpoint
<syntaxhighlight lang="text"> GET /api.php?action=bridgeshape&shapeaction=ACTION&format=json </syntaxhighlight>
Azioni disponibili
| Azione | Descrizione | Parametri |
|---|---|---|
get
|
Vincoli per una singola convenzione | pageid oppure title
|
list
|
Elenco convenzioni con vincoli definiti | — |
listall
|
Elenco tutte le convenzioni (con/senza vincoli) | — |
validate
|
Valida una stringa DSL senza bisogno di una pagina wiki | dsl
|
Esempi di chiamata
<syntaxhighlight lang="bash">
- Per titolo pagina
curl "https://bridgepedia.it/api.php?action=bridgeshape&shapeaction=get&title=Splinter&format=json"
- Per page ID
curl "https://bridgepedia.it/api.php?action=bridgeshape&shapeaction=get&pageid=42&format=json"
- Elenco pagine con vincoli
curl "https://bridgepedia.it/api.php?action=bridgeshape&shapeaction=list&format=json"
- Validare una stringa DSL
curl "https://bridgepedia.it/api.php?action=bridgeshape&shapeaction=validate&dsl=opener+H:5%2B+hcp:12-21&format=json" </syntaxhighlight>
Formato risposta JSON
shapeaction=get
<syntaxhighlight lang="json"> {
"shapeconstraints": {
"pageId": 42,
"raw": "1H-4C: opener H:5+ hcp:12-21; responder H:4+ C:0-1 Chcp:0 hcp:10-15 // ...",
"scenarios": [
{
"label": "1H-4C",
"positions": {
"opener": {
"suits": {
"H": { "min": 5, "max": null }
},
"hcp": { "min": 12, "max": 21 }
},
"responder": {
"suits": {
"H": { "min": 4, "max": null },
"C": { "min": 0, "max": 1 }
},
"suit_hcp": {
"C": { "min": 0, "max": 0 }
},
"hcp": { "min": 10, "max": 15 }
}
},
"fit": []
}
]
}
} </syntaxhighlight>
Descrizioni leggibili. Insieme a scenarios, get e validate restituiscono
descriptions (stesso ordine): per ogni scenario label, positions
(per posizione, elenco di frasi già tradotte, es. "♠: ha K J 10 (e non A Q 9)"),
positionLabels (nome del ruolo, es. "Attaccante" se la pagina ha shape_roles;
altrimenti "Apertore (chi usa la convenzione)", "Compagno", ...) e fit.
Parametro opzionale lang (it, en, ...; default lingua dell'API, di solito it).
Serve a mostrare nell'interfaccia di Play perché la mano è fatta così.
Nella posizione compare anche quiet: 1 se è stata usata la macro quiet.
shapeaction=list
<syntaxhighlight lang="json"> {
"pages": [
{
"pageId": 42,
"title": "Splinter",
"scenarioCount": 6,
"labels": ["1H-4C", "1H-4D", "1H-4S", "1S-4C", "1S-4D", "1S-4H"]
},
{
"pageId": 15,
"title": "Stayman",
"scenarioCount": 2,
"labels": ["fit a cuori", "fit a picche"]
}
]
} </syntaxhighlight>
Formato DSL
Il DSL (Domain Specific Language) è la stringa scritta nel campo
shape_constraints del template wiki.
Struttura
<syntaxhighlight lang="text"> etichetta: vincoli posizione; vincoli posizione // etichetta: vincoli // ... </syntaxhighlight>
//separa gli scenari (il generatore ne sceglie uno random o li presenta all'utente);separa le posizioni dentro uno scenario- Ogni scenario ha un'etichetta opzionale seguita da
: - Ogni posizione inizia col nome del ruolo, seguito da vincoli separati da spazi
Posizioni
| Nome | Ruolo |
|---|---|
opener
|
Chi apre o fa la prima licita convenzionale |
responder
|
Il compagno dell'opener |
opp1
|
Avversario a sinistra dell'opener |
opp2
|
Avversario a destra dell'opener (compagno di opp1) |
Vincoli
Lunghezza seme (specifico)
<syntaxhighlight lang="text"> S:5+ — almeno 5 picche H:0-1 — 0 o 1 cuori D:3 — esattamente 3 quadri C:2-5 — da 2 a 5 fiori </syntaxhighlight>
Semi: S = Spades/Picche, H = Hearts/Cuori, D = Diamonds/Quadri, C = Clubs/Fiori
Lunghezza seme (wildcard)
<syntaxhighlight lang="text"> x:0-1 — un seme qualsiasi (tra quelli non specificati) con 0-1 carte x:6+ — un seme non specificato con almeno 6 carte </syntaxhighlight>
Il generatore sceglie casualmente quale seme assegnare a x tra quelli
non già vincolati esplicitamente (S/H/D/C).
HCP totali
<syntaxhighlight lang="text"> hcp:15-17 — da 15 a 17 punti onore hcp:12+ — almeno 12 punti hcp:0-7 — da 0 a 7 punti </syntaxhighlight>
HCP nel seme (specifico)
<syntaxhighlight lang="text"> Shcp:3+ — almeno 3 HCP a picche Chcp:0 — 0 HCP a fiori (nessun onore) Hhcp:0-2 — da 0 a 2 HCP a cuori </syntaxhighlight>
HCP nel seme (wildcard)
<syntaxhighlight lang="text"> xhcp:0 — 0 HCP nel seme wildcard (x) xhcp:0-1 — 0-1 HCP nel seme x </syntaxhighlight>
Shape class
<syntaxhighlight lang="text"> shape:balanced — distribuzione bilanciata (4333, 4432, 5332) shape:one_suiter — monocolore shape:two_suiter — bicolore shape:three_suiter — tricolore shape:any — qualsiasi (default) </syntaxhighlight>
Onori esatti in un seme (S=KJT)
<syntaxhighlight lang="text"> S=KJT — in picche ha K, J e 10; NON ha A, Q, 9 (le carte basse 8-2 completano) S=KT9 — sequenza interna: K, 10, 9; NON ha A, Q, J H=Q — a cuori ha la Q come unico onore tra A K Q J 10 D=x — nessun onore a quadri (solo carte basse) </syntaxhighlight>
Le lettere ammesse sono A K Q J T 9 (anche 10). La regola è: le carte elencate sono possedute, le altre tra A K Q J 10 9 NON lo sono; le carte
dall'8 in giù sono libere e riempiono fino alla lunghezza del seme. Serve a
descrivere attacchi (sequenza interrotta KJT, interna KT, sotto onore K o
Q) e le carte del compagno. Si combina con la lunghezza (S:5 S=KJT).
Nel JSON compare suit_cards nella posizione, es. {"S": "KJT", "H": ""}
(stringa vuota = nessuna carta alta). Il generatore deve:
- assegnare esattamente quegli onori alla mano e escluderli dalle altre mani del seme; gli onori non nominati da nessuna posizione sono distribuiti a caso, ma mai a una posizione con
suit_cardsper quel seme che non li elenca; - rispettare comunque
suitsesuit_hcp(la validazione segnala le contraddizioni, es.S=KQ Shcp:0-2o lo stesso onore a due mani).
Pattern di mano in un seme (S~..., stile regex)
Descrive l'intera mano in quel seme, dall'alto in basso, come una piccola espressione regolare ancorata (inizio e fine):
<syntaxhighlight lang="text"> S~AKQ. — A, K, Q + una carta bassa (4 carte) S~A... — A + tre carte basse (4 carte) S~AJT9. — A, J, 10, 9 + una bassa S~AKQ.|A... — alternative: una OPPURE l'altra S~[KQ]x{2,3} — K o Q (come unico onore) + 2 o 3 carte basse S~KJT.* — K, J, 10 + qualunque numero di basse (anche zero) </syntaxhighlight>
Elementi:
- Carte alte
A K Q J T 9(anche10), sempre in ordine decrescente, ciascuna posseduta; quelle non nominate tra A K Q J 10 9 NON sono possedute. - Classe
[KQ]= una carta tra quelle (si espande in alternative). - Carta bassa
.(ox) = una carta qualsiasi tra 8 e 2; compare solo in fondo. Quantificatori:?(0-1),*(0 o più),+(1 o più),{n},{n,m}. Più punti si sommano (...= tre basse). - Alternative
|: il generatore ne sceglie una a caso per ogni mano.
È la forma più espressiva di S=KJT (che resta valida): il pattern fissa anche
la lunghezza (S~A... = 4 carte). Se c'è anche S:N i due vincoli devono
essere compatibili (la validazione API segnala le incompatibilità).
Nel JSON compare suit_patterns nella posizione, già normalizzato (classi
espanse, quantificatori risolti), così il generatore non deve interpretare la
regex:
<syntaxhighlight lang="json"> "suit_patterns": {
"S": [
{"cards": "AKQ", "low_min": 1, "low_max": 1},
{"cards": "A", "low_min": 3, "low_max": 3}
]
} </syntaxhighlight>
cards = carte alte possedute (ordine A K Q J T 9); low_min/low_max =
numero di carte basse (2-8), low_max: null = nessun limite. La lunghezza del
seme è len(cards) + numero di basse. Il generatore sceglie una alternativa e
rispetta anche suits e suit_hcp. Gli onori di cards vanno esclusi dalle altre
mani; quelli non nominati nella mano vanno ad altri a caso.
Avversari "quiet" (nessun intervento)
<syntaxhighlight lang="text"> opp1 quiet — solo per opp1/opp2 opp2 quiet:0-7 — come quiet, ma con HCP personalizzati </syntaxhighlight>
quiet è una macro espansa dall'API: nel JSON non compare mai la parola
quiet, ma i vincoli già risolti. Chi legge l'API (il generatore) non deve
implementare nulla di nuovo. Si espande in:
<syntaxhighlight lang="text"> S:0-4 H:0-4 D:0-5 C:0-5 hcp:0-9 Shcp:0-3 Hhcp:0-3 Dhcp:0-4 Chcp:0-4 (con quiet:0-7 → hcp:0-7) </syntaxhighlight>
Significato: gli avversari possono avere colori lunghi (così le mani sono
varie), ma poveri di onori e con pochi punti: nessun colore da 6+ carte
(niente apertura di sbarramento), nessun contro informativo, nessun intervento
naturale credibile. Le distribuzioni ammesse sono quindi molte, ma con punti
ridotti: il generatore ha meno margine sugli HCP della coppia dichiarante
(40 punti totali: apertore+rispondente restano sopra ~22).
I token espliciti scritti sulla stessa posizione sostituiscono i default,
in qualsiasi ordine: opp1 quiet S:0-4 lascia le picche 0-4 e tiene il resto.
Su opener/responder la macro viene ignorata.
Fit (vincolo cross-posizione)
<syntaxhighlight lang="text"> fit:H8+ — almeno 8 cuori totali tra i compagni fit:S9+ — almeno 9 picche totali tra i compagni fit:M8+ — almeno 8 carte in un seme maggiore (H o S) fit:m8+ — almeno 8 carte in un seme minore (D o C) fit:8+ — almeno 8 carte in un seme qualsiasi </syntaxhighlight>
Il fit si applica tra compagni:
- Se appare su
openeroresponder, o a livello di scenario → coppia dichiarante - Se appare su
opp1oopp2→ coppia difensiva
Formato range
| Sintassi | Significato |
|---|---|
5+
|
Almeno 5 (min=5, max=null) |
0-1
|
Da 0 a 1 (min=0, max=1) |
3
|
Esattamente 3 (min=3, max=3) |
12-21
|
Da 12 a 21 (min=12, max=21) |
Nella risposta JSON, max: null significa "nessun limite superiore".
Semi non menzionati
I semi non vincolati e non coperti da x: sono distribuiti casualmente
dal generatore, rispettando il vincolo che il totale delle carte per mano sia 13.
DSL versione 2 (dslVersion: 2)
Tutto ciò che sopra resta valido e invariato: una pagina che usa solo la v1
produce lo stesso JSON di prima. La v2 aggiunge sintassi nuova; ogni scenario che
la usa ha nel JSON il campo requires (elenco di tag, vedi sotto) così un motore
parziale può rifiutarlo con un errore esplicito invece di generare mani sbagliate.
Le definizioni normative (cosa è una fermata, i punti totali, le perdenti...) e
l'architettura del motore sono in DEAL_ENGINE_SPEC.md.
Macro di scrittura (@NOME=v1,v2) — risolte dal parser, il motore non le vede
<syntaxhighlight lang="text">
@M=H,S 1${M}: opener ${M}:5+ hcp:12-21; responder ${M}:3+
</syntaxhighlight>
Genera due scenari (1H, 1S) sostituendo ${M} ovunque, etichetta compresa.
Più prefissi fanno il prodotto cartesiano (max 64 scenari): @M=H,S @N=15-17,12-14.
I valori possono essere intervalli (hcp:${N}): servono anche come parametri del
sistema (SA 15-17 contro 14-16).
Semi variabili $a $b $c $d
Un seme "da scegliere" condiviso da più posizioni dello stesso scenario. Ogni variabile è un seme diverso dalle altre. Dominio dichiarato a livello di scenario (prima delle posizioni):
<syntaxhighlight lang="text">
Landy: $a=M $b=any opener hcp:12-21 ; opp1 $a:5+ $b:4+ ; responder $a:0-2
</syntaxhighlight>
$a=M nobili, $a=m minori, $a=any (default) qualunque, $a=SH insieme esplicito.
Si usano al posto di S H D C in: $a:5+ (lunghezza), $ahcp:, $a=KQ, $a~...,
stop:$a, has:$aQ, kc:$a3, confronti ($a>$b), longest:$a.
Differisce da x: (che vale per una sola posizione).
JSON: lo stesso dizionario suits, suit_hcp, ... ma con chiave "$a";
a livello di scenario "vars": {"$a": ["S","H"]}.
Controlli, carte chiave, carte precise
| Token | Significato |
|---|---|
aces:2 kings:1-2 queens:0
|
numero di assi / re / dame nella mano |
controls:6+
|
controlli (A=2, K=1) |
kc:H3 kc:H2-3 kc:$a3
|
carte chiave con atout H: i 4 assi + il Re di H (0-5) |
has:HQ has:S10
|
possiede quella carta precisa |
lacks:HQ
|
NON possiede quella carta |
stop:S nostop:S
|
ha / non ha una fermata nel seme (definizione in SPEC) |
JSON: aces, kings, queens, controls (Range); keycards: {"H": Range};
cards_held: ["HQ"], cards_not_held: ["SK"]; stoppers: {"S": 1, "H": 0}
(1 = deve averla, 0 = non deve averla).
Punti e perdenti
tp: punti totali (PO + 1 per ogni carta oltre la quarta in ogni seme),
dp: punti distribuzione (PO + 3 chicane + 2 singolo + 1 doubleton),
ltc: perdenti (Losing Trick Count). A livello di scenario: pairhcp:25+
(PO della coppia dichiarante), defhcp:0-14 (coppia avversaria), pairtp:26+.
JSON: tp, dp, ltc (Range) nella posizione; pair_hcp: {"declarer": R, "defense": R},
pair_tp: {"declarer": R} nello scenario.
Confronti tra semi e forme precise
<syntaxhighlight lang="text">
H>S H>=S H<S H<=S H==S $a>$b — confronto di lunghezze nella stessa mano
longest:H — H è tra i semi più lunghi (pari merito ammesso)
shape:5-4-x-x shape:5422 shape:54xx — lunghezze ordinate in modo decrescente, x = qualsiasi
shape:semi_balanced — 5422 e 6322 (si aggiunge alle classi esistenti)
</syntaxhighlight>
JSON: compare: [{"left":"H","op":">=","right":"S"}], longest: ["H"],
shape_pattern: "54xx".
Alternative e negazioni nella stessa mano: any(...) e not(...)
<syntaxhighlight lang="text">
opener any(S:4+ hcp:12+, H:4+) not(D:6+)
</syntaxhighlight>
any(A, B): la mano soddisfa almeno una delle alternative (separate da virgola;
ogni alternativa è una congiunzione di token). not(A, B): la mano non soddisfa
nessuna delle alternative. Si possono scrivere più gruppi (si combinano in AND);
niente annidamenti.
JSON: any: [[altA, altB], ...] (lista di gruppi; ogni alternativa è un oggetto
con gli stessi campi di una posizione), not: [altA, altB].
Contesto dello scenario
Token prima delle posizioni: weight:3 (frequenza relativa, 1-100, default 1),
dealer:opener, vul:none|us|them|both (us = apertore+rispondente),
auction:1SA,P,2C,P (licite, notazione BridgeCards; SA = NT), contract:4H,
declarer:responder, lead:SK.
JSON: weight (int) e context: {"dealer":..,"vul":..,"auction":[..],"contract":"4H","declarer":..,"lead":"SK"}.
Il motore deve usare dealer/vul quando li espone (per la numerazione del tavolo) e,
se c'è lead, la carta deve stare nella mano dell'attaccante (a sinistra del
dichiarante) e rispettare i vincoli di quella mano.
Tag requires
suit_cards, suit_patterns, vars, stoppers, card_presence (has/lacks),
controls (aces/kings/queens/controls/kc), points (tp/dp/ltc), pair_points,
compare (confronti, longest), shape_pattern, any_not, context, weight.
Le macro @ e quiet non compaiono: sono già espanse.
Avvisi di parsing (warnings)
get e validate restituiscono warnings quando un frammento del DSL non è stato
capito e quindi scartato (nome di posizione sbagliato, token fuori posto). Un
motore non deve generare se ci sono warnings o errors.
Struttura JSON di uno scenario
<syntaxhighlight lang="typescript"> interface Scenario {
label: string; // Etichetta (es. "1H-4C"), vuota se non specificata
positions: {
[posName: string]: Position; // "opener", "responder", "opp1", "opp2"
};
fit: FitConstraint[]; // Vincoli di fit (array, può essere vuoto)
}
interface Position {
suit_patterns?: { // Pattern regex-like normalizzati: {"S":[{"cards":"AKQ","low_min":1,"low_max":1}]}
[suit: string]: { cards: string; low_min: number; low_max: number | null }[];
};
suit_cards?: { [suit: string]: string }; // Onori esatti, es. {"S":"KJT"}; "" = nessuna carta alta
suits?: { // Vincoli lunghezza seme (solo semi esplicitati)
[suit: string]: Range; // "S", "H", "D", "C"
};
hcp?: Range; // HCP totali
suit_hcp?: { // HCP per seme specifico
[suit: string]: Range;
};
x?: Range; // Lunghezza seme wildcard
x_hcp?: Range; // HCP seme wildcard
shape?: string; // Shape class: "balanced", "one_suiter", etc.
}
interface Range {
min: number; max: number | null; // null = nessun limite superiore
}
interface FitConstraint {
suit: string | null; // "S","H","D","C" o null suit_class: string | null; // "major", "minor" o null min: number; // Numero minimo di carte (es. 8) pair: string; // "declarer" o "defense"
} </syntaxhighlight>
Logica per il generatore
Algoritmo base
Per ogni scenario:
- Scegli lo scenario: random tra quelli disponibili, oppure lascia scegliere all'utente
- Risolvi wildcard:
- Se una posizione ha
x:, scegli un seme casuale tra quelli non esplicitamente vincolati xhcp:si applica allo stesso seme scelto perx:
- Se una posizione ha
- Risolvi fit:
- Se
fit.suitè specificato, entrambi i compagni devono avere un totale >=fit.minin quel seme - Se
fit.suit_classè "major", scegli tra H e S; se "minor", tra D e C - Se entrambi sono null, scegli un seme qualsiasi
fit.pairindica quale coppia: "declarer" (opener+responder) o "defense" (opp1+opp2)
- Se
- Genera le mani:
- Per ogni posizione, genera una mano che rispetti tutti i vincoli
- I semi senza vincoli vengono riempiti casualmente
- Il totale delle carte per mano deve essere esattamente 13
- Il totale delle carte per seme (tutte e 4 le mani) deve essere esattamente 13
- Posizioni non menzionate: se uno scenario non menziona
opp1/opp2, distribuisci le carte rimanenti casualmente tra gli avversari
Priorità dei vincoli
- Vincoli di fit (cross-posizione) — determinano la distribuzione di base
- Vincoli di seme specifico (S:, H:, D:, C:) — limitano i singoli semi
- Vincoli wildcard (x:) — dopo aver fissato i semi specifici
- Vincoli HCP (hcp:, Shcp:, xhcp:) — filtro finale
- Shape class (shape:) — validazione della forma complessiva
Gestione di max: null
Quando max è null, non c'è limite superiore esplicito. I limiti fisici
del bridge si applicano comunque:
- Massimo 13 carte per seme (tutte e 4 le mani)
- Massimo 13 carte per mano
- Massimo 10 HCP per seme (A+K+Q+J = 4+3+2+1)
- Massimo 37 HCP per mano (raro ma teoricamente possibile con tutti gli onori)
Esempi completi
Splinter (tutti gli scenari specifici)
DSL nel template wiki: <syntaxhighlight lang="text"> 1H-4C: opener H:5+ hcp:12-21; responder H:4+ C:0-1 Chcp:0 hcp:10-15 // 1H-4D: opener H:5+ hcp:12-21; responder H:4+ D:0-1 Dhcp:0 hcp:10-15 // 1H-4S: opener H:5+ hcp:12-21; responder H:4+ S:0-1 Shcp:0 hcp:10-15 // 1S-4C: opener S:5+ hcp:12-21; responder S:4+ C:0-1 Chcp:0 hcp:10-15 // 1S-4D: opener S:5+ hcp:12-21; responder S:4+ D:0-1 Dhcp:0 hcp:10-15 // 1S-4H: opener S:5+ hcp:12-21; responder S:4+ H:0-1 Hhcp:0 hcp:10-15 </syntaxhighlight>
Splinter (compatto con fit e wildcard)
<syntaxhighlight lang="text"> dopo 1H: fit:H8+ opener H:5+ hcp:12-21; responder x:0-1 xhcp:0 hcp:10-15 // dopo 1S: fit:S8+ opener S:5+ hcp:12-21; responder x:0-1 xhcp:0 hcp:10-15 </syntaxhighlight>
Stayman
<syntaxhighlight lang="text"> fit a cuori: fit:H8+ opener hcp:15-17 S:2-4 H:2-4 D:2-5 C:2-5; responder H:4+ hcp:8+ // fit a picche: fit:S8+ opener hcp:15-17 S:2-4 H:2-4 D:2-5 C:2-5; responder S:4+ hcp:8+ </syntaxhighlight>
Contro informativo
<syntaxhighlight lang="text"> base: opener hcp:12-21 x:5+; opp1 hcp:12+ S:3+ H:3+ D:3+ C:3+; responder hcp:0-8 // con intervento: opener hcp:12-21 x:5+; opp1 hcp:12+ S:3+ H:3+ D:3+ C:3+; opp2 hcp:8+ x:5+ </syntaxhighlight>
Preempt 3-a-livello
<syntaxhighlight lang="text"> opener H:7+ x:0-1 hcp:5-10 </syntaxhighlight>
Overcall con fit avversari
<syntaxhighlight lang="text"> overcall semplice: opener hcp:12-21 x:5+; opp1 hcp:10+ x:5+; opp2 fit:8+ // con barrage: opener hcp:12-21 x:5+; opp1 hcp:6-10 x:6+; opp2 fit:9+ </syntaxhighlight>
Note implementative
- L'API è read-only, non richiede token CSRF
- L'accesso anonimo è abilitato di default (
BridgeShapeAllowAnonymous: true) perché i dati sono pubblici e i tool esterni ne hanno bisogno - Il campo
rawnella rispostagetcontiene la stringa DSL originale, utile per debug - L'azione
validatepermette di testare stringhe DSL senza creare pagine wiki - I calcoli HCP usano la scala standard: A=4, K=3, Q=2, J=1