""" in_and_out.py — empirische toets van Jeff Rubens' "in-and-out valuation" (The Secrets of Winning Bridge, Dover 1980), zoals besproken in De Shell, Deel II. CLAIM (Rubens). Bij een schoppenfit met partner is een hand met de SECUNDAIRE honneurs IN de troefkleur en de PRIMAIRE honneurs BUITEN sterker dan andersom, bij GELIJKE HCP. Paar 1 (9 HCP): a) S:HV x H:A x x D:xxx C:xxxx b) S:AH x H:V x x D:xxx C:xxxx Paar 2 (7 HCP): c) S:H x x H:A x x D:xxx C:xxxx d) S:A x x H:H x x D:xxx C:xxxx (Nederlandse notatie H=heer, V=vrouw, B=boer; in de PBN-strings die DDS leest wordt dat de Engelse K, Q, J, met T voor de tien. S/H/D/C = de vier kleuren.) Enige verschil binnen een paar: de plaatsing van A en de lagere honneur tussen de troefkleur (schoppen) en een zijkleur (harten). De vorm (3-3-3-4) en de HCP zijn identiek. Dit is dezelfde 'zelfde punten, andere plaatsing'-opzet als Voorbeeld 1 (Hand 1a/1b), nu microscopisch en gecontroleerd. METHODE. Monte-Carlo. Responder (Zuid) krijgt de testhand met vaste honneurs en willekeurige lage spotcards; opener (Noord) krijgt een 1S-achtige opening (5+ schoppen, 11-19 HCP); OW krijgt de rest. Met double-dummy (Bo Haglunds DDS via endplay) berekenen we het aantal SCHOPPENSLAGEN voor NS = max(N, S als leider). We vergelijken de gemiddelden van de twee varianten binnen een paar. NB. De twee varianten worden ONAFHANKELIJK getrokken (independent samples). Een perfect gepaarde opzet is principieel onmogelijk: doordat A en V tussen de handen wisselen, belanden in variant a en b nu juist verschillende honneurs BUITEN responder, wat verschillende globale verdelingen afdwingt. Met grote N en de gerapporteerde standaardfout is dat geen probleem. """ import random from statistics import mean, median, pstdev from endplay.types import Deal, Denom, Player from endplay.dds import calc_dd_table SUITS = "SHDC" HONOUR_PTS = {"A": 4, "K": 3, "Q": 2, "J": 1} SPOTS = list("98765432") # 'x' = lage kaart, < 10, conform Rubens' notatie FULL_RANKS = list("AKQJT98765432") def hcp(cards): return sum(HONOUR_PTS.get(c[1], 0) for c in cards) def full_deck(): return [s + r for s in SUITS for r in FULL_RANKS] # Responder-templates: (vaste honneurs per kleur, # spots per kleur) # elke kleur: lijst honneurranks die vast bij responder horen TEMPLATES = { "a": {"S": ["K", "Q"], "H": ["A"], "D": [], "C": []}, # SHV / HA -> 9 HCP "b": {"S": ["A", "K"], "H": ["Q"], "D": [], "C": []}, # SAH / HV -> 9 HCP "c": {"S": ["K"], "H": ["A"], "D": [], "C": []}, # SH / HA -> 7 HCP "d": {"S": ["A"], "H": ["K"], "D": [], "C": []}, # SA / HH -> 7 HCP } SHAPE = {"S": 3, "H": 3, "D": 3, "C": 4} def build_responder(template, rng): """Bouw responders 13 kaarten: vaste honneurs + random lage spots, vorm 3-3-3-4.""" cards = [] used = set() for suit in SUITS: honours = template[suit] for r in honours: c = suit + r cards.append(c) used.add(c) n_spots = SHAPE[suit] - len(honours) pool = [suit + r for r in SPOTS if suit + r not in used] rng.shuffle(pool) for c in pool[:n_spots]: cards.append(c) used.add(c) return cards def deal_for(template, rng, opener_min_spades=5, opener_hcp=(11, 19), max_tries=2000): """Genereer een volledige deal (PBN) met responder=Zuid, opener=Noord (5+S).""" responder = build_responder(template, rng) rest = [c for c in full_deck() if c not in set(responder)] for _ in range(max_tries): rng.shuffle(rest) opener = rest[:13] n_spades = sum(1 for c in opener if c[0] == "S") if n_spades < opener_min_spades: continue h = hcp(opener) if not (opener_hcp[0] <= h <= opener_hcp[1]): continue west = rest[13:26] east = rest[26:39] return responder, opener, west, east return None # opgegeven (zeldzaam) def to_pbn(north, east, south, west): """Vier handen (lijsten kaarten) -> PBN-string 'N:.. .. .. ..'.""" order_rank = {r: i for i, r in enumerate(FULL_RANKS)} def fmt(hand): by_suit = {s: [] for s in SUITS} for c in hand: by_suit[c[0]].append(c[1]) parts = [] for s in SUITS: rr = sorted(by_suit[s], key=lambda r: order_rank[r]) parts.append("".join(rr)) return ".".join(parts) return "N:" + " ".join(fmt(h) for h in (north, east, south, west)) def ns_spade_tricks(pbn): d = Deal(pbn) t = calc_dd_table(d) return max(t[Denom.spades, Player.north], t[Denom.spades, Player.south]) def run(variant, n_deals, seed=0): rng = random.Random(seed) tricks = [] tmpl = TEMPLATES[variant] misses = 0 while len(tricks) < n_deals: res = deal_for(tmpl, rng) if res is None: misses += 1 continue responder, opener, west, east = res pbn = to_pbn(opener, east, responder, west) # N=opener, S=responder tricks.append(ns_spade_tricks(pbn)) return tricks if __name__ == "__main__": import sys N = int(sys.argv[1]) if len(sys.argv) > 1 else 300 print(f"In-and-out valuation — {N} deals per variant\n") # sanity: HCP-check for v in "abcd": r = build_responder(TEMPLATES[v], random.Random(1)) print(f" variant {v}: {sorted(r)} HCP={hcp(r)}") print() def pct_game(tricks): # 4S = 10 slagen return 100.0 * sum(1 for x in tricks if x >= 10) / len(tricks) def dist(tricks): from collections import Counter c = Counter(tricks) return " ".join(f"{k}:{c[k]}" for k in sorted(c)) pairs = [("a", "b", "Paar 1 (9 HCP)"), ("c", "d", "Paar 2 (7 HCP)")] for v1, v2, label in pairs: t1 = run(v1, N, seed=100) t2 = run(v2, N, seed=200) m1, m2 = mean(t1), mean(t2) se1, se2 = pstdev(t1) / len(t1) ** 0.5, pstdev(t2) / len(t2) ** 0.5 diff = m1 - m2 se_diff = (se1 ** 2 + se2 ** 2) ** 0.5 print(f"{label}") print(f" variant {v1} (in-and-out goed) : gem={m1:.3f} med={median(t1)} " f"SE={se1:.3f} 4S+={pct_game(t1):.1f}%") print(f" variant {v2} (in-and-out slecht): gem={m2:.3f} med={median(t2)} " f"SE={se2:.3f} 4S+={pct_game(t2):.1f}%") print(f" verschil ({v1}-{v2}) = {diff:+.3f} slag SE={se_diff:.3f} " f"z={diff/se_diff:+.2f}") print(f" verdeling {v1}: {dist(t1)}") print(f" verdeling {v2}: {dist(t2)}") print()