Files
timandClaude Fable 5 3710a68e37 RL: encoding observacii, akcne masky a Round prostredie
Zaklad RL vrstvy (rl/DESIGN.md): egocentricky rotovana observacia
(ruka, videne karty, tipy, kopka, dedukovane voidy -- 233 dim), masky
legalnych tipov/kariet zrkadliace pravidla enginu a RoundEnv (jedno kolo
= jedna self-play epizoda). Fuzz-testy vynucuju zhodu masiek s enginom.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 18:49:55 +02:00

9.7 KiB

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