""" trump_vs_nt_advantage.py ======================== Empirische meting van het troefvoordeel: hoeveel slagen levert spelen in troef gemiddeld meer op dan spelen in SA, gegeven dat NS een 8+-kaartsfit heeft? Werkwijze: 1. Genereer N willekeurige deals 2. Filter op deals waar NS minimaal een 8+-fit heeft 3. Kies de langste fit als troefkleur (bij meerdere willekeurig) 4. Bereken DD-tricks in troef en in SA (gemiddeld over N als leider en S als leider) 5. Aggregeer: gemiddelde, mediaan, percentielen, histogram 6. Uitsplitsing naar fitlengte (8, 9, 10, 11+) Vereist: endplay (pip install endplay) Draait op Windows met multiprocessing. Stel het aantal workers en deals onderaan in. """ from endplay.dealer import generate_deal from endplay.dds import calc_dd_table from endplay.types import Player, Denom import multiprocessing as mp from collections import Counter import random import statistics import time import sys # Robuuste HCP-functie: probeer endplay.evaluate.hcp, # val terug op string-tellen als die niet bestaat try: from endplay.evaluate import hcp as _endplay_hcp def calc_hcp(hand): return _endplay_hcp(hand) except (ImportError, AttributeError): def calc_hcp(hand): """Bereken HCP door A/H/V/B te tellen in de string-representatie.""" s = str(hand) return s.count('A') * 4 + s.count('K') * 3 + s.count('Q') * 2 + s.count('J') * 1 def analyze_one_deal(seed, min_ns_hcp=20): """ Analyseer één deal en retourneer (fitlengte, ns_hcp, troef_tricks, sa_tricks) of None als er geen 8+-fit voor NS is, of als NS te zwak is om te declareren. """ random.seed(seed) deal = generate_deal() north = deal.north south = deal.south # Filter 1: NS moet voldoende HCP hebben om realistisch te declareren ns_hcp = calc_hcp(north) + calc_hcp(south) if ns_hcp < min_ns_hcp: return None # Bereken fitlengtes voor NS in elke kleur fits = {} for denom in [Denom.spades, Denom.hearts, Denom.diamonds, Denom.clubs]: n_count = len(north[denom]) s_count = len(south[denom]) total = n_count + s_count if total >= 8: fits[denom] = total # Filter 2: NS moet minstens één 8+-fit hebben if not fits: return None # Kies langste fit (bij gelijke lengte willekeurig) max_len = max(fits.values()) longest_fits = [d for d, l in fits.items() if l == max_len] trump_suit = random.choice(longest_fits) # Bereken DD-tabel (alle 5 denominaties, alle 4 leiders) dd_table = calc_dd_table(deal) # Trick counts voor NS-leider in troef # Voor zuivere meting: gemiddelde van N en S als leider # Indexering is tuple (Denom, Player) — niet dubbele [][] troef_n = dd_table[trump_suit, Player.north] troef_s = dd_table[trump_suit, Player.south] troef_tricks = (troef_n + troef_s) / 2.0 sa_n = dd_table[Denom.nt, Player.north] sa_s = dd_table[Denom.nt, Player.south] sa_tricks = (sa_n + sa_s) / 2.0 return (max_len, ns_hcp, troef_tricks, sa_tricks) def worker_chunk(args): """Verwerk een chunk van seeds en retourneer resultaten.""" chunk_seeds, min_ns_hcp = args results = [] for seed in chunk_seeds: r = analyze_one_deal(seed, min_ns_hcp=min_ns_hcp) if r is not None: results.append(r) return results def main(n_deals=10000, n_workers=8, min_ns_hcp=20): print(f"Genereer {n_deals} deals met {n_workers} workers...") print(f"Filter: NS minstens {min_ns_hcp} HCP, minstens één 8+-fit\n") t0 = time.time() seeds = list(range(n_deals)) chunk_size = max(1, n_deals // (n_workers * 4)) chunks = [(seeds[i:i + chunk_size], min_ns_hcp) for i in range(0, len(seeds), chunk_size)] with mp.Pool(n_workers) as pool: all_results = pool.map(worker_chunk, chunks) results = [r for chunk_results in all_results for r in chunk_results] elapsed = time.time() - t0 print(f"Klaar in {elapsed:.1f}s. {len(results)} deals voldoen aan filter " f"({100*len(results)/n_deals:.1f}% van totaal).") diffs = [t - s for (_, _, t, s) in results] troef_all = [t for (_, _, t, _) in results] sa_all = [s for (_, _, _, s) in results] hcps = [h for (_, h, _, _) in results] print("\n=== TOTAAL (alle filter-deals) ===") print(f"Aantal deals : {len(results)}") print(f"Gem. NS HCP : {statistics.mean(hcps):.1f}") print(f"Gem. tricks troef : {statistics.mean(troef_all):.3f}") print(f"Gem. tricks SA : {statistics.mean(sa_all):.3f}") print(f"Gem. verschil : {statistics.mean(diffs):.3f} slag") print(f"Mediaan verschil : {statistics.median(diffs):.3f}") print(f"Std.dev verschil : {statistics.stdev(diffs):.3f}") print(f"Min/Max verschil : {min(diffs):.1f} / {max(diffs):.1f}") sorted_diffs = sorted(diffs) n = len(sorted_diffs) print(f"P10/P25/P50/P75/P90: " f"{sorted_diffs[n//10]:.1f}/{sorted_diffs[n//4]:.1f}/" f"{sorted_diffs[n//2]:.1f}/{sorted_diffs[3*n//4]:.1f}/" f"{sorted_diffs[9*n//10]:.1f}") print("\n=== UITSPLITSING NAAR FITLENGTE ===") by_length = {} for (length, _, t, s) in results: bucket = length if length < 11 else 11 by_length.setdefault(bucket, []).append(t - s) for length in sorted(by_length): diffs_l = by_length[length] label = f"{length}+-kaart" if length == 11 else f"{length}-kaart" print(f" {label} fit ({len(diffs_l):>6}): " f"gem. verschil = {statistics.mean(diffs_l):+.3f}, " f"sd = {statistics.stdev(diffs_l):.3f}") print("\n=== UITSPLITSING NAAR NS HCP-BAND ===") by_hcp = {} for (_, hcp, t, s) in results: if hcp < 23: bucket = "20-22" elif hcp < 26: bucket = "23-25" elif hcp < 29: bucket = "26-28" elif hcp < 32: bucket = "29-31" else: bucket = "32+" by_hcp.setdefault(bucket, []).append(t - s) for bucket in ["20-22", "23-25", "26-28", "29-31", "32+"]: if bucket in by_hcp: diffs_h = by_hcp[bucket] print(f" NS {bucket} HCP ({len(diffs_h):>6}): " f"gem. verschil = {statistics.mean(diffs_h):+.3f}, " f"sd = {statistics.stdev(diffs_h):.3f}") print("\n=== HISTOGRAM VAN HET VERSCHIL (troef - SA) ===") rounded = Counter(round(d) for d in diffs) max_count = max(rounded.values()) for diff_val in sorted(rounded): count = rounded[diff_val] bar = '#' * int(60 * count / max_count) print(f" {diff_val:+3d}: {count:>6} {bar}") if __name__ == '__main__': N_DEALS = 10000 N_WORKERS = 8 MIN_NS_HCP = 20 if len(sys.argv) > 1: N_DEALS = int(sys.argv[1]) if len(sys.argv) > 2: N_WORKERS = int(sys.argv[2]) if len(sys.argv) > 3: MIN_NS_HCP = int(sys.argv[3]) main(n_deals=N_DEALS, n_workers=N_WORKERS, min_ns_hcp=MIN_NS_HCP)