""" ligging_analyse.py ================== Hoeveel van de slagen die de leider (dubbeldummy) maakt zijn LIGGINGS-AFHANKELIJK (snit / dwang / ingooi) en dus NIET voorspelbaar tijdens het bieden? Methode 2 ("verwachting over de liggingen"), contrafeitelijk: 1. Genereer een willekeurig spel. 2. Kies het beste NZ-contract (kleur + leider) uit de DD-tabel. 3. Houd de NZ-handen VAST; herverdeel de 26 OW-kaarten K keer willekeurig. (Willekeurig herdelen = weging met de a-priori-kans van elke ligging.) 4. Bereken DD-slagen voor HETZELFDE contract bij elke herverdeling. Per spel: DD-werkelijk = slagen op de echte ligging (solver bankt elke snit) verwachting = gemiddelde over de herverdelingen (eerlijke, biedbare schatting) bodem (P10) = 10e percentiel (~gegarandeerd, slechte ligging) liggings-afh. = verwachting - bodem (de snit/dwang/ingooi-zone) Afhankelijkheden: endplay (pip install endplay). Schaalbaar via N en K onderaan. """ #!/usr/bin/env python3 import random, statistics as st from collections import defaultdict from endplay.types import Deal, Player, Denom from endplay.dds import calc_all_tables RANKS = "AKQJT98765432"; RO = {r: i for i, r in enumerate(RANKS)}; SUITS = "SHDC" NS = [Player.north, Player.south] STRAINS = [Denom.clubs, Denom.diamonds, Denom.hearts, Denom.spades, Denom.nt] ALL = [Denom.spades, Denom.hearts, Denom.diamonds, Denom.clubs, Denom.nt] DEN_NAME = {Denom.clubs: "♣", Denom.diamonds: "♦", Denom.hearts: "♥", Denom.spades: "♠", Denom.nt: "SA"} # ---------- spel-representatie (kaart = (kleur, hoogte)) ---------- def hand_to_str(cards): return ".".join("".join(sorted([r for (cs, r) in cards if cs == s], key=lambda r: RO[r])) for s in SUITS) def deal_to_pbn(h): return "N:" + " ".join(hand_to_str(h[p]) for p in "NESW") def random_deal(): deck = [(s, r) for s in SUITS for r in RANKS]; random.shuffle(deck) return {'N': deck[0:13], 'E': deck[13:26], 'S': deck[26:39], 'W': deck[39:52]} def redistribute_ew(h): ew = h['E'] + h['W']; random.shuffle(ew) return {'N': h['N'], 'E': ew[:13], 'S': h['S'], 'W': ew[13:]} def chunked(lst, n): for i in range(0, len(lst), n): yield lst[i:i + n] def pct(xs, p): xs = sorted(xs); k = (len(xs) - 1) * p / 100; f = int(k) if f + 1 >= len(xs): return xs[-1] return xs[f] + (xs[f + 1] - xs[f]) * (k - f) def best_ns_contract(table): best = (-1, None, None) for den in STRAINS: for dec in NS: tr = table[den, dec] if tr > best[0]: best = (tr, den, dec) return best # (slagen, kleur, leider) # ====================== HOOFDPROGRAMMA ====================== def run(N=80, K=30, seed=2026): random.seed(seed) originals = [random_deal() for _ in range(N)] # 1) volledige DD-tabel per origineel spel -> contractkeuze deals = [Deal(deal_to_pbn(h)) for h in originals] tables = [] for ch in chunked(deals, 20): tables.extend(calc_all_tables(ch)) contracts = [best_ns_contract(t) for t in tables] # (T_actual, den, dec) # 2) herverdelingen, getagd per strain (voor efficiente batching) jobs = defaultdict(list) # den -> [(orig_idx, Deal), ...] for i, h in enumerate(originals): _, den, _ = contracts[i] for _ in range(K): jobs[den].append((i, Deal(deal_to_pbn(redistribute_ew(h))))) # 3) DD-slagen per herverdeling (alleen de gekozen strain rekenen) results = defaultdict(list) # orig_idx -> [slagen, ...] for den, js in jobs.items(): excl = [d for d in ALL if d != den] ds = [d for _, d in js] tabs = [] for ch in chunked(ds, 20): tabs.extend(calc_all_tables(ch, exclude=excl)) for (i, _), t in zip(js, tabs): results[i].append(t[den, contracts[i][2]]) # 4) aggregeren rows = [] for i in range(N): T_act, den, dec = contracts[i] xs = results[i] verw = st.mean(xs); bodem = pct(xs, 10); top = pct(xs, 90) rows.append(dict(T=T_act, den=den, verw=verw, bodem=bodem, top=top, lieafh=verw - bodem, xs=xs)) print(f"\n=== Liggings-analyse (N={N} spellen, K={K} herverdelingen elk) ===") print(f" {N*K} DD-herberekeningen, beste NZ-contract per spel.\n") gT = st.mean(r['T'] for r in rows) gV = st.mean(r['verw'] for r in rows) gB = st.mean(r['bodem'] for r in rows) gL = st.mean(r['lieafh'] for r in rows) print(f" DD-werkelijk (echte ligging) ........ {gT:5.2f} slagen") print(f" Verwachting over de liggingen ....... {gV:5.2f} slagen (eerlijke biedbare schatting)") print(f" Gegarandeerde bodem (P10) ........... {gB:5.2f} slagen (slechte ligging)") print(f" --> LIGGINGS-AFHANKELIJK (verw-bodem) {gL:5.2f} slagen (snit/dwang/ingooi-zone)") print(f" = {100*gL/gV:4.1f}% van de slagen leunt op een ligging die je tijdens " f"het bieden niet kent.\n") print(f" Controle: DD-werkelijk ({gT:.2f}) ~ verwachting ({gV:.2f}); " f"het echte spel is een eerlijke trekking, geen geluksligging.") # 5) 'manche op snit': contracten die DD op de echte ligging maakt games = [r for r in rows if r['T'] >= 10] if games: # kans dat een DD-makende manche ook over willekeurige liggingen maakt ps = [sum(1 for x in r['xs'] if x >= 10) / len(r['xs']) for r in games] robuust = [p for p in ps if p >= 0.90] opsnit = [p for p in ps if p < 0.60] print(f"\n --- Manches (DD maakt >=10 op de echte ligging): {len(games)} stuks ---") print(f" Gem. kans dat zo'n manche ook over willekeurige liggingen maakt: " f"{100*st.mean(ps):4.1f}%") print(f" robuust (>=90% maakt) : {len(robuust):3d} ({100*len(robuust)/len(games):4.1f}%)") print(f" 'op snit' (<60% maakt): {len(opsnit):3d} ({100*len(opsnit)/len(games):4.1f}%)") # 6) histogram van liggings-afhankelijke slagen per spel print("\n Verdeling liggings-afhankelijke slagen per spel (verw - bodem):") buckets = defaultdict(int) for r in rows: buckets[round(r['lieafh'] * 2) / 2] += 1 # halve-slag bins for b in sorted(buckets): bar = "#" * buckets[b] print(f" {b:4.1f} | {bar} {buckets[b]}") return rows if __name__ == "__main__": run(N=80, K=30)