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) 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) static func choose_action(values: Array, total_dice: int, bid: Dictionary, min_raise: int, must_bid: bool, rng: RandomNumberGenerator) -> Dictionary: var known: Array = [] for v in values: if v > 0: known.append(v) var unknown := total_dice - known.size() if bid.is_empty(): var op := _opening(known, unknown, rng) return {"type": "bid", "count": op.count, "face": op.face} var hold := chance(known, unknown, bid.count, bid.face) var raise_ = _best_raise(known, unknown, total_dice, bid, min_raise) if not must_bid: var doubt := 0.30 + 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(): if must_bid: # Notgebot: minimal erhöhen, auch wenn unwahrscheinlich var c: int = bid.count + maxi(1, min_raise) if c > total_dice: return {"type": "challenge"} if not must_bid else {"type": "bid", "count": total_dice, "face": bid.face} return {"type": "bid", "count": c, "face": bid.face} return {"type": "challenge"} return {"type": "bid", "count": raise_.count, "face": raise_.face} 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, min_raise: int) -> Dictionary: var best := {} for face in range(1, 7): for count in range(1, total + 1): if not _is_higher(count, face, bid, min_raise): continue var p := chance(known, unknown, count, face) if p < 0.42: 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, min_raise: int) -> bool: if count >= bid.count + min_raise: return true if min_raise > 1: return false return count == bid.count and face > bid.face static func choose_booster(offer: Array, values: Array, dice_left: int, rng: RandomNumberGenerator) -> Dictionary: var scores: Array = [] for id in offer: var b: Dictionary = BoosterDB.get_booster(id) var s := rng.randf() if b.kind == BoosterDB.Kind.BUFF: s += 0.35 if dice_left <= 2 and b.kind == BoosterDB.Kind.BUFF: s += 0.35 scores.append(s) var pick := 0 for i in scores.size(): if scores[i] > scores[pick]: pick = i var id: String = offer[pick] return {"id": id, "target": _target_for(id, values)} static func _target_for(id: String, values: Array) -> int: if values.is_empty(): return 0 if id == "twin": # Würfel mit dem häufigsten Wert var counts := {} for v in values: counts[v] = counts.get(v, 0) + 1 var best_v: int = values[0] for v: int in counts: if counts[v] > counts.get(best_v, 0): best_v = v return values.find(best_v) # sonst: schwächster Würfel var idx := 0 for i in values.size(): if values[i] < values[idx]: idx = i return idx