# RL bot pre bridzik — navrh (2026-07-01)

Ciel: naucit sa principy self-play reinforcement learningu (v duchu AlphaGo Zero)
na praktickom priklade — natrenovat sietovy policy pre hranie bridziku. Zamerne
zjednodusene oproti AlphaGo Zero: bez MCTS (skryta informacia neumoznuje priamy
prehladavaci strom), cisty self-play policy gradient (PPO/REINFORCE) nad `bridzik.py`
enginom.

## Kluc: `Round` je nezavisla epizoda

Bodovanie (`Round.get_points_summary`) je cisto lokalne pre jedno kolo — nezavisi
od predoslych ani nasledujucich kol, len od tipu a poctu kopiek v danom kole.
Netreba teda simulovat cely `Bridzik`/`Series` state machine na trening — staci
instanciovat `Round(round_number, first_player, shuffler)` priamo, opakovane,
s roznymi `round_number` (0-7, teda 8 az 1 karta v ruke). Kazdy `Round` je
samostatna self-play epizoda.

Vyhody:
- jednoduchsi self-play loop (ziadne series/game bookkeeping)
- vela nezavislych epizod, jednoduchá paralelizacia

**Curriculum vs. uniformne samplovanie (bod 4).** Povodny napad "zacat na
`round_number=7`" je zavadzajuci: pri 1 karte je tip masked na {0,1} a jedina
karta je vynutena — nula card-play rozhodnuti, ziadne ucenie, len smoke-test.
Realne ucenie je v kolach `round_number` 0-3 (6-8 kariet). Preto:
- `round_number` je aj tak v observacii, takze **default = uniformne samplovat
  `round_number` 0-7** a nechat siet zdielat vahy naprieč velkostami ruk.
- Ak curriculum, tak **od tazkych (viac kariet) k lahsim**, nie naopak; alebo
  aspon uniformne s miernym zvyhodnenim tazsich kol.
- `round_number=7` drzat len ako sanity/smoke test pipeline, nie ako trening.

## 1. State encoding (observation)

Spolocne pre guess aj play fazu, budovane z `Round` objektu pre daneho hraca:

- **vlastna ruka** — 32-dim multi-hot (4 farby x 8 hodnot)
- **round_number** — one-hot (8) alebo normalizovane cislo (urcuje velkost ruk)
- **tipy vsetkych 4 hracov** — 4x (flag "uz tipoval" + normalizovana hodnota)
- **vlastny tip** (po tipnuti) — kriticke pre play fazu (viem, ci este potrebujem
  vyhrat kopku, alebo sa jej mam vyhybat)
- **kolko kopiek uz kazdy hrac vyhral v tomto kole** — 4 scalars, odvodene
  z dokoncenych `Stash` objektov v `self.stashes`
- **aktualna kopka v procese** — 4 sloty (karta alebo prazdne) + `first_player`
  aktualnej kopky
- **uz odohrane/videne karty v tomto kole** — 32-dim multi-hot (bod 1). KRITICKE:
  bez toho observacia NIE JE Markovovska. Pri viac kartach (round_number 0-2)
  su dve rovnake ruky s rovnakou aktualnou kopkou, ale roznou historiou uz
  odohranych kariet, rozne stavy s roznym optimalnym tahom (vies, ci este visi
  eso/cerven). Bez tejto zlozky sa siet nemoze naucit card-counting a strop hry
  ostane nizky. Kodovat karty odohrane v predoslych dokoncenych `Stash`-och
  (mimo tvojej ruky a mimo aktualnej rozohranej kopky).
- **(volitelne, neskor — bod 8) znama neúčasť supperov vo farbe (voids)** —
  ked supper neprizna vynasanu farbu, prezradi void → per-hrac x per-farba
  flag. Silna informacia, ale nechat na neskorsie rozsirenie.

**Egocentricka rotacia (bod 2) — povinny invariant.** Aby parameter sharing
medzi 4 sedadlami fungoval, VSETKY 4-hracske vektory (tipy, pocty vyhranych
kopiek, sloty aktualnej kopky, `first_player`) musia byt rotovane tak, ze
"ja" = index 0 a ostatni relativne (+1, +2, +3 v smere hry). Toto zapisat do
`encoding.py` ako tvrdy invariant + unit test — je to najpravdepodobnejsie
miesto tichej chyby, ktora pokazi ucenie.

Zamerne vynechane: priebezne skore/standings naprieč hrou — kedze odmena je
per-round nezavisla, optimalne rozhodnutie v danom kole na standings nezavisi.

**Velkost observacie (bod 5).** Povodny odhad ~80-100 floatov je podstrelený.
Ak sa 4 sloty aktualnej kopky koduju one-hot (4x32=128) + ruka 32 + videne
karty 32 + tipy/pocty/round_number/first_player, realny `input_dim` je skor
~200. Nie je to problem, len podla toho nastavit vstupnu vrstvu siete.

## 2. Akcny priestor + maskovanie

- **Guess**: 9 kategorii (0-8), maskovane na `0..(8-round_number)`; pre 4.
  (posledneho) tipujuceho naviac zamaskovat hodnotu, ktora by sposobila
  `BridzikException` (sucet tipov = pocet kopiek) — vypocitatelne vopred
  z `self.guesses`. Zakazana hodnota = `(8-round_number) - sum(3 tipov)`;
  ak vyjde mimo `0..(8-round_number)`, je uz aj tak nelegalna a nemaskuje sa
  nic navyse (osetrit rozsah).
- **Play card**: 32 kategorii (rovnaka indexacia farba+hodnota ako hand-encoding),
  maskovane na karty, ktore hrac realne ma A splnaju follow-suit pravidlo
  (rovnaka logika ako v `Round.play_card`: farba prvej karty v kopke, inak
  povinna cervena ak ju hrac ma).

## 3. Sieť

Zdielany "trup" (2 hidden layers, ~128-256 neuronov, ReLU) nad observation
vektorom (~80-100 floatov), s troma vystupmi:

- guess head (9 logitov)
- play head (32 logitov)
- value head (1 scalar) — odhad ocakavanej odmeny do konca kola (baseline
  pre actor-critic)

`input_dim` nastavit podla realnej velkosti observacie (~200, viz bod 5
v sekcii 1), nie podla povodneho ~80-100.

Fazovy flag v observacii + maskovanie urcuje, ktora hlava je pouzitelna
v danom kroku (guess a play fazy sa nikdy neprelinaju).

## 4. Odmena a trening

- Odmena = 0 pocas kola; na konci kola kazdy hrac dostane
  `points_summary[player]` (0 alebo `10+guess`) ako terminalnu odmenu za
  VSETKY svoje rozhodnutia v danom kole (guess + vsetky `play_card` tahy).
  Sparse terminal reward, ziadne discountovanie netreba — `gamma=1` (bod 9),
  kolo ma max 9 rozhodnuti na hraca: round 0 = 1 tip + 8 kariet.
- Algoritmus: self-play PPO (prip. najprv jednoduchsie REINFORCE + baseline),
  jedna zdielana siet hra vsetkych 4 hracov v kazdom `Round` (parameter
  sharing, rovnaky princip ako AlphaGo Zero).
- **Normalizacia odmeny (bod 7).** `10+guess` je v rozsahu 10-18 a lisi sa
  per kolo; pri miesanych `round_number` to zvysuje varianciu policy gradientu.
  Standardizovat advantage per batch (odcitat priemer, delit std) — bezna
  PPO praktika, tu je nutnejsia kvoli rozne velkym odmenam.
- Paralelizacia: `Round` instancie su nezavisle bez shared state, self-play
  generovanie sa da paralelizovat cez multiprocessing naprieč jadrami CPU
  (pripadne batchovanim viacerych epizod naraz cez sietovy forward).

**Caveat: hra nie je zero-sum (bod 3).** Je to 4-hracska general-sum hra —
viacero hracov moze naraz trafit tip a vsetci skoruju, zaroven sa o kopky
sutazi (`sum(kopky) = pocet kopiek`). Self-play so zdielanymi vahami preto
NEMA konvergencne zaruky ako AlphaGo Zero (2-hracska zero-sum); skonverguje
k *nejakemu* equilibriu, nie nutne k optimu, a moze oscilovat. Na ucebny
ciel to staci, ale: (a) nepredavat si to ako "AlphaZero-grade optimalitu",
(b) sledovat progres proti FIXNYM baseline-om (sekcia 5), nie len podla
self-play reward, ktory sa hybe s protihracom.

## 5. Vyhodnotenie

- priemerne body/kolo oproti baseline (nahodny legalny hrac, Monte Carlo
  heuristicky tipper — pozri sekciu nizsie)
- presnost tipu (% kôl, kde sa tip presne trafil) — interpretovatelnejsia
  metrika nez surove body

## Alternativa/doplnok pre tipovaciu fazu: Monte Carlo namiesto siete

Kedze tipovanie je v podstate odhad pravdepodobnosti pri neznamom rozdeleni
zvysnych kariet, da sa riesit aj bez siete:

1. **Naivna MC simulacia** — vygenerovat vela nahodnych rozdeleni zvysnych
   kariet medzi ostatnych 3 hracov, odsimulovat kolo s jednoduchou heuristickou
   hracou strategiou, spocitat rozdelenie poctu vlastnych kopiek. POZOR: kedze
   bodujeme len presnu zhodu, spravny cieľ je **mod** rozdelenia, nie priemer.
   POZOR 2 (bod 6 — "discard pile"): `deal_starting_cards` zahodí prvych
   `4*round_number` kariet (`round_cards[4*round_number:]`), takze v kole NIE
   su rozdane vsetky karty. MC teda z `32 - vlastna_ruka` kariet rozdá kazdemu
   z 3 supperov len `(8-round_number)` kariet a **zvysok necha v neznamej kope
   mimo hru** — nerozdavat vsetko medzi supperov, inak nadhodnotis, kolko
   vysokych kariet/cervene supperi drzia.
2. **Silnejsia verzia** — rovnaky MC rollout, ale simulovat zvysok kola
   s uz natrenovanou card-play sietou namiesto naivnej heuristiky (analogia
   MCTS + value network v AlphaZero namiesto ciste nahodnych rolloutov).

Toto sa da pouzit ako rychly heuristicky baseline bez trenovania siete na
tipovanie vobec, alebo ako silnejsi hybrid s uz existujucou play sietou.

## Poradie implementacie

1. `rl/encoding.py` — cistě funkcie observation + mask (nad `Round` objektom),
   testovatelne izolovane. Uz tu zapracovat bod 1 (videne karty) a bod 2
   (egocentricka rotacia). Unit/property testy: maska NIKDY nepovoli tah, ktory
   `Round.play_card`/`add_player_guess` odmietne (fuzz-test proti enginu);
   rotacia je konzistentna pre vsetky 4 sedadla.
2. `rl/env.py` — step/reset wrapper okolo jedneho `Round` (nie celeho `Bridzik`)
3. **baseline hraci + evaluacny harness UZ TU** (nahodny legalny hrac, MC
   heuristicky tipper podla sekcie vyssie) — nech je metrika k dispozicii od
   prvej trenovacej epochy a da sa sledovat progres (bod 3: proti fixnym
   baseline-om, nie len self-play reward).
4. sieť (PyTorch, trup + 3 hlavy) — `input_dim` podla realnej velkosti obs (~200)
5. self-play generator (paralelne `Round` epizody)
6. PPO update krok (advantage standardizovat per batch — bod 7)
7. (neskor, volitelne) rozsirit na cely `Series`/`Bridzik` self-play, ak by
   sa ukazalo, ze cross-round dynamika (rotacia first_player a pod.) predsa
   len nieco mení — podla bodu 1 to nie je ocakavane

## Technologie

- **PyTorch** — samostatny `requirements-rl.txt`, oddeleny od zakladneho
  `requirements.txt` projektu
- vlastna mensia implementacia PPO (v duchu CleanRL) namiesto Stable-Baselines3/
  RLlib — nas pripad (multi-agent self-play so zdielanymi vahami, maskovanie
  akcii) sa bije s ich single-agent Gym abstrakciou viac, nez by pomohla
