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>
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_numberje aj tak v observacii, takze default = uniformne samplovatround_number0-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=7drzat 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
Stashobjektov vself.stashes - aktualna kopka v procese — 4 sloty (karta alebo prazdne) +
first_playeraktualnej 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 sposobilaBridzikException(sucet tipov = pocet kopiek) — vypocitatelne vopred zself.guesses. Zakazana hodnota =(8-round_number) - sum(3 tipov); ak vyjde mimo0..(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 alebo10+guess) ako terminalnu odmenu za VSETKY svoje rozhodnutia v danom kole (guess + vsetkyplay_cardtahy). 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+guessje v rozsahu 10-18 a lisi sa per kolo; pri miesanychround_numberto zvysuje varianciu policy gradientu. Standardizovat advantage per batch (odcitat priemer, delit std) — bezna PPO praktika, tu je nutnejsia kvoli rozne velkym odmenam. - Paralelizacia:
Roundinstancie 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:
- 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_cardszahodí prvych4*round_numberkariet (round_cards[4*round_number:]), takze v kole NIE su rozdane vsetky karty. MC teda z32 - vlastna_rukakariet 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. - 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
rl/encoding.py— cistě funkcie observation + mask (nadRoundobjektom), testovatelne izolovane. Uz tu zapracovat bod 1 (videne karty) a bod 2 (egocentricka rotacia). Unit/property testy: maska NIKDY nepovoli tah, ktoryRound.play_card/add_player_guessodmietne (fuzz-test proti enginu); rotacia je konzistentna pre vsetky 4 sedadla.rl/env.py— step/reset wrapper okolo jednehoRound(nie celehoBridzik)- 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).
- sieť (PyTorch, trup + 3 hlavy) —
input_dimpodla realnej velkosti obs (~200) - self-play generator (paralelne
Roundepizody) - PPO update krok (advantage standardizovat per batch — bod 7)
- (neskor, volitelne) rozsirit na cely
Series/Bridzikself-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 zakladnehorequirements.txtprojektu - 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