Files
pirate-dice/scripts/ai.gd
2026-09-07 04:29:23 +02:00

142 lines
4.0 KiB
GDScript

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 = int(bid.count) + maxi(1, min_raise)
if c <= total_dice:
return {"type": "bid", "count": c, "face": bid.face}
if min_raise <= 1 and int(bid.face) < 6:
return {"type": "bid", "count": int(bid.count), "face": int(bid.face) + 1}
# Es gibt kein gültiges Gebot mehr - dann bleibt nur die Anfechtung.
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