142 lines
4.0 KiB
GDScript
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
|