extends RefCounted class_name DiceAI ## Entscheidungslogik für KI-Spieler. static func _pmf(n: int, k: int, p: float) -> float: var c := 1.0 for i in k: c = c * float(n - i) / float(i + 1) return c * pow(p, k) * pow(1.0 - p, n - k) ## P(X >= k) für X ~ Bin(n, p) static func tail(n: int, k: int, p: float) -> float: if k <= 0: return 1.0 if k > n: return 0.0 var s := 0.0 for i in range(k, n + 1): s += _pmf(n, i, p) return clampf(s, 0.0, 1.0) static func _own(values: Array, face: int) -> int: var c := 0 for v in values: if v == face: c += 1 return c ## Wahrscheinlichkeit, dass ein Gebot (count, face) genau stimmt. static func exact_chance(values: Array, unknown: int, count: int, face: int) -> float: var k := count - _own(values, face) if k < 0 or k > unknown: return 0.0 return _pmf(unknown, k, 1.0 / 6.0) ## Wahrscheinlichkeit, dass ein Gebot (count, face) hält. static func chance(values: Array, unknown: int, count: int, face: int) -> float: return tail(unknown, count - _own(values, face), 1.0 / 6.0) ## 0 = leicht, 1 = normal, 2 = schwer. Das Menü setzt es vor dem Start. static var stufe := 1 ## Je Stufe vier Regler: ## bluff - wie oft er blufft, statt ein glaubwürdiges Gebot anzufechten. ## Ohne das wäre er ein reiner Rechner, und den liest man. ## genau - ab welcher Trefferchance er "Genau" ruft. ## zweifel - unter welcher Haltechance er anficht. Hoch heißt: voreilig. ## sicher - welche Chance ein eigenes Gebot mindestens haben muss. const STUFEN := [ {"bluff": 0.15, "genau": 0.35, "zweifel": 0.40, "sicher": 0.30}, {"bluff": 0.40, "genau": 0.22, "zweifel": 0.30, "sicher": 0.42}, # Gemessen: bei zweifel 0.24 / genau 0.18 dauerte eine Partie doppelt so # lang wie auf normal - viele Züge je Runde, viele nullsummige Genau-Treffer. {"bluff": 0.50, "genau": 0.26, "zweifel": 0.28, "sicher": 0.48}, ] ## Wie oft er ein sicheres Gebot einen Schritt höher setzt, als nötig wäre. const FRECH := 0.12 ## Wie oft er eine gute Genau-Gelegenheit auch wirklich nutzt. const GENAU_LUST := 0.35 static func _regler() -> Dictionary: return STUFEN[clampi(stufe, 0, STUFEN.size() - 1)] ## `wissen`: was der Bot über die anderen weiss - "bekannt" sind aufgedeckte ## Gegnerwürfel (Spähen), "fenster" ist je Augenzahl [von, bis] vom Zählen. ## Ohne das würfe er die Münzen und ignorierte, was sie ihm sagen. static func choose_action(values: Array, total_dice: int, bid: Dictionary, must_bid: bool, rng: RandomNumberGenerator, wissen: Dictionary = {}) -> Dictionary: var known: Array = [] for v in values: if v > 0: known.append(v) for v in wissen.get("bekannt", []): known.append(int(v)) var unknown := maxi(0, total_dice - known.size()) var fenster: Dictionary = wissen.get("fenster", {}) if bid.is_empty(): var op := _opening(known, unknown, rng) return {"type": "bid", "count": op.count, "face": op.face} var r := _regler() var hold := chance(known, unknown, bid.count, bid.face) var genau_p := exact_chance(known, unknown, bid.count, bid.face) if fenster.has(int(bid.face)): # Gezählt: das Fenster schlägt jede Schätzung. var f: Array = fenster[int(bid.face)] var von := int(f[0]) var bis := int(f[1]) if bis < int(bid.count): hold = 0.0 genau_p = 0.0 elif von >= int(bid.count): hold = 1.0 genau_p = 1.0 if von == bis and von == int(bid.count) else (0.34 if von == int(bid.count) else 0.0) else: hold = float(bis - int(bid.count) + 1) / float(bis - von + 1) genau_p = 1.0 / float(bis - von + 1) var raise_ = _best_raise(known, unknown, total_dice, bid, float(r.sicher)) var low := lowest_raise(bid, total_dice) if not must_bid: # Liegt die Ansage vermutlich haarscharf, lohnt "Genau" mehr als die # Anfechtung: der Ansager verliert, und man bekommt selbst einen dazu. # Aber nur ab und zu: Genau nimmt keinen Würfel vom Tisch. Ein Bot, # der es bei jeder Gelegenheit ruft, zieht die Partie ins Endlose. # Sicheres Wissen (Fenster exakt) nutzt er dagegen immer. if genau_p >= 0.999 or (genau_p > float(r.genau) and hold < 0.6 and rng.randf() < GENAU_LUST): return {"type": "exact"} var doubt := float(r.zweifel) + rng.randf_range(-0.07, 0.07) if hold < doubt and (raise_.is_empty() or raise_.p < 0.32): return {"type": "challenge"} if raise_.is_empty(): # Kein sicheres Gebot mehr. Anfechten wäre ehrlich - aber wer immer # ehrlich ist, wird gelesen. Also ab und zu bluffen, solange es noch # ein gültiges Gebot gibt. if not low.is_empty() and (must_bid or rng.randf() < float(r.bluff)): return {"type": "bid", "count": low.count, "face": low.face} return {"type": "challenge"} if rng.randf() < FRECH and int(raise_.count) + 1 <= total_dice: return {"type": "bid", "count": int(raise_.count) + 1, "face": raise_.face} return {"type": "bid", "count": raise_.count, "face": raise_.face} ## Das kleinste Gebot, das noch über dem liegenden steht ({} = keins mehr). static func lowest_raise(bid: Dictionary, total: int) -> Dictionary: if int(bid.face) < 6: return {"count": int(bid.count), "face": int(bid.face) + 1} if int(bid.count) + 1 <= total: return {"count": int(bid.count) + 1, "face": 1} return {} static func _opening(known: Array, unknown: int, rng: RandomNumberGenerator) -> Dictionary: var best_face := rng.randi_range(1, 6) var best := -1 for f in range(1, 7): var c := _own(known, f) if c > best: best = c best_face = f var count := maxi(1, best + int(round(unknown / 6.0))) return {"count": count, "face": best_face} static func _best_raise(known: Array, unknown: int, total: int, bid: Dictionary, sicher: float) -> Dictionary: var best := {} for face in range(1, 7): for count in range(1, total + 1): if not _is_higher(count, face, bid): continue var p := chance(known, unknown, count, face) if p < sicher: continue if best.is_empty() or count < best.count or (count == best.count and p > best.p): best = {"count": count, "face": face, "p": p} break return best static func _is_higher(count: int, face: int, bid: Dictionary) -> bool: if count > bid.count: return true return count == bid.count and face > bid.face ## Welcher eigene Würfel soll umgedreht werden? Einer, der die Augenzahl ## noch nicht zeigt - sonst wäre die Münze verschenkt. static func target_for(values: Array, face: int) -> int: for i in values.size(): if values[i] != face: return i return 0