"""Baseline hraci pre evaluaciu a neskorsi warm-start siete (viz rl/DESIGN.md). Spolocne rozhranie: `guess(rnd, seat) -> int` (tip 0..8) a `play(rnd, seat) -> int` (index karty 0..31). Hrac vidi len to, co by videl pri stole -- vlastnu ruku, tipy, dokoncene kopky a rozohranu kopku; do cudzich ruk nesiaha. """ from collections import Counter from random import Random from bridzik import cards, Card_colors, Stash from rl.encoding import ( N_GUESS_ACTIONS, N_PLAY_ACTIONS, card_index, deduce_voids, guess_mask, legal_cards, play_mask, ) class RandomPlayer: """Uniformne nahodny legalny tah -- najslabsi mozny baseline.""" def __init__(self, rng: Random = None): self.rng = rng if rng is not None else Random() def guess(self, rnd, seat: int) -> int: mask = guess_mask(rnd) return self.rng.choice([g for g in range(N_GUESS_ACTIONS) if mask[g]]) def play(self, rnd, seat: int) -> int: mask = play_mask(rnd, seat) return self.rng.choice([i for i in range(N_PLAY_ACTIONS) if mask[i]]) def _strength(card) -> tuple: """Absolutna sila karty: kazda cervena (tromf) bije kazdu necervenu.""" return (card.color == Card_colors['HEARTS'], card.value.value) def _current_best(stash): """Zatial vitazna karta rozohranej kopky (None ak sa este nevynieslo).""" first = stash.get_first_card() if stash is not None else None if first is None: return None best = first for card in stash.get_cards().values(): if _beats(card, best): best = card return best def _beats(card, best) -> bool: """Ci `card` prebije `best` (karta drziaca kopku; jej farba je smerodajna).""" if card.color == best.color: return card.value > best.value return card.color == Card_colors['HEARTS'] def simulate_tricks(hands: dict, leader: int, rng: Random) -> list: """Dohra kopky s nahodnou legalnou strategiou; vrati pocty vyhier hracov. `hands` je dict seat -> zoznam kariet (rovnako velke ruky); zoznamy sa spotrebuju. Vitaza kopky urcuje enginovy Stash.get_winner() -- pravidla sa tu neduplikuju. """ tricks = [0] * 4 for _ in range(len(hands[leader])): stash = Stash(leader) for i in range(4): seat = (leader + i) % 4 card = rng.choice(legal_cards(hands[seat], stash.get_first_card())) hands[seat].remove(card) stash.add_card(seat, card) leader = stash.get_winner() tricks[leader] += 1 return tricks def deal_consistent(unknown: list, hand_sizes: dict, voids: dict, rng: Random, max_tries: int = 20) -> dict: """Nahodne rozdanie neznamych kariet superom respektujuce voidy. Greedy priradenie po zamiesani (najviac obmedzeni hraci prvi); ak sa konzistentne rozdanie nepodari za `max_tries`, padne na rozdanie bez voidov (zriedkave, radsej mierne skreslena vzorka nez ziadna). Zvysok kariet ostava v odlozenej kope mimo hry. """ seats = sorted(hand_sizes, key=lambda s: len(voids.get(s, ())), reverse=True) pool = list(unknown) for _ in range(max_tries): rng.shuffle(pool) remaining = list(pool) hands = {} for seat in seats: hand, rest, banned = [], [], voids.get(seat, set()) for card in remaining: if len(hand) < hand_sizes[seat] and card.color not in banned: hand.append(card) else: rest.append(card) if len(hand) < hand_sizes[seat]: break hands[seat] = hand remaining = rest else: return hands rng.shuffle(pool) idx = 0 hands = {} for seat in seats: hands[seat] = pool[idx:idx + hand_sizes[seat]] idx += hand_sizes[seat] return hands def mc_guess_distribution(rnd, seat: int, n_samples: int, rng: Random) -> Counter: """Monte Carlo odhad rozdelenia poctu vlastnych kopiek v kole. Nezname karty sa v kazdej vzorke nahodne rozdelia ostatnym trom hracom -- kazdemu len (8 - round_number) kariet, zvysok ostava v odlozenej kope mimo hry (pozri DESIGN.md, pasca "discard pile"). Leader prvej kopky je v case tipovania neznamy (najvyssi tip), sampluje sa uniformne. """ hand = rnd.player_cards[seat] hand_size = 8 - rnd.round_number unknown = [c for c in cards if c not in hand] counts = Counter() for _ in range(n_samples): rng.shuffle(unknown) sim_hands = {seat: list(hand)} others = [s for s in range(4) if s != seat] for i, other in enumerate(others): sim_hands[other] = unknown[i * hand_size:(i + 1) * hand_size] tricks = simulate_tricks(sim_hands, rng.randrange(4), rng) counts[tricks[seat]] += 1 return counts def finish_round(hands: dict, current_cards: dict, first_player: int, me: int, my_card, tricks: list, rng: Random) -> list: """Dohra kolo od mojho tahu: dokonci rozohranu kopku (ja hram `my_card`, dalsi nahodne legalne) a zvysne kopky dohra nahodnou legalnou strategiou. `tricks` su uz vyhrane kopky (mutuje sa kopia volajuceho); vrati final.""" stash = Stash(first_player) for seat, card in current_cards.items(): stash.add_card(seat, card) stash.add_card(me, my_card) while not stash.is_completed(): seat = stash.get_active_player() card = rng.choice(legal_cards(hands[seat], stash.get_first_card())) hands[seat].remove(card) stash.add_card(seat, card) leader = stash.get_winner() tricks[leader] += 1 while hands[leader]: stash = Stash(leader) for i in range(4): seat = (leader + i) % 4 card = rng.choice(legal_cards(hands[seat], stash.get_first_card())) hands[seat].remove(card) stash.add_card(seat, card) leader = stash.get_winner() tricks[leader] += 1 return tricks class HeuristicPlayer: """MC tipper + jednoducha hracia heuristika riadena vlastnym tipom.""" def __init__(self, rng: Random = None, n_samples: int = 100): self.rng = rng if rng is not None else Random() self.n_samples = n_samples def guess(self, rnd, seat: int) -> int: counts = mc_guess_distribution(rnd, seat, self.n_samples, self.rng) mask = guess_mask(rnd) # najcastejsi LEGALNY pocet kopiek (mod rozdelenia, nie priemer -- # boduje sa len presna zhoda); pri nule vzoriek pre legalny tip # rozhodne blizkost k celkovemu modu mode = counts.most_common(1)[0][0] legal = [g for g in range(N_GUESS_ACTIONS) if mask[g]] return max(legal, key=lambda g: (counts[g], -abs(g - mode))) def play(self, rnd, seat: int) -> int: hand = rnd.player_cards[seat] stash = rnd.get_last_stash() allowed = legal_cards(hand, stash.get_first_card() if stash else None) need = rnd.guesses[seat] - rnd.get_stashes_winner_summary()[seat] best = _current_best(stash) if best is None: # vynasam: chcem kopku -> najsilnejsia karta; nechcem -> najslabsia chosen = max(allowed, key=_strength) if need > 0 else min(allowed, key=_strength) else: winning = [c for c in allowed if _beats(c, best)] if need > 0 and winning: # ber kopku co najlacnejsie chosen = min(winning, key=_strength) elif need <= 0 and len(winning) < len(allowed): # kopku nechcem: zbav sa najsilnejsej neberucej karty chosen = max((c for c in allowed if not _beats(c, best)), key=_strength) elif need <= 0: # vsetko berie -> ber co najlacnejsie (setri silne karty netreba, # ale nizka karta drzi sancu, ze ma este niekto prebije) chosen = min(allowed, key=_strength) else: # kopku chcem, ale nic neberie -> odhod najslabsiu chosen = min(allowed, key=_strength) return card_index(chosen) class McPlayer(HeuristicPlayer): """Heuristika s MC hracou fazou: kazdy kandidatsky tah sa ohodnoti simulaciami zvysku kola nad rozdaniami neznamych kariet konzistentnymi s dedukovanymi voidmi (`use_voids=False` = ablacia bez dedukcie). Tipovanie ostava MC tipper z HeuristicPlayer (pred prvou kartou niet z coho voidy dedukovat).""" def __init__(self, rng: Random = None, n_samples: int = 100, play_samples: int = 24, use_voids: bool = True): super().__init__(rng, n_samples) self.play_samples = play_samples self.use_voids = use_voids def play(self, rnd, seat: int) -> int: hand = rnd.player_cards[seat] stash = rnd.get_last_stash() first_card = stash.get_first_card() if stash else None candidates = legal_cards(hand, first_card) if len(candidates) == 1: return card_index(candidates[0]) target = rnd.guesses[seat] base_tricks = rnd.get_stashes_winner_summary() voids = deduce_voids(rnd) if self.use_voids else {} seen = set() played_count = Counter() for st in rnd.stashes: for other, card in st.get_cards().items(): seen.add(card) played_count[other] += 1 unknown = [c for c in cards if c not in seen and c not in hand] hand_size0 = 8 - rnd.round_number hand_sizes = {s: hand_size0 - played_count[s] for s in range(4) if s != seat} current_cards = stash.get_cards() # spolocne rozdanie pre vsetkych kandidatov (common random numbers -- # porovnavame tahy na tych istych svetoch, mensia variancia) scores = {card_index(c): 0 for c in candidates} for _ in range(self.play_samples): world = deal_consistent(unknown, hand_sizes, voids, self.rng) for candidate in candidates: sim_hands = {s: list(h) for s, h in world.items()} sim_hands[seat] = [c for c in hand if c != candidate] tricks = finish_round(sim_hands, dict(current_cards), stash.first_player, seat, candidate, list(base_tricks), self.rng) if tricks[seat] == target: scores[card_index(candidate)] += 1 best_index = max(scores, key=scores.get) if scores[best_index] == 0: # tip uz je (takmer) nedosiahnutelny -> aspon rozumny pravidlovy tah return super().play(rnd, seat) return best_index