183 lines
6.3 KiB
GDScript
183 lines
6.3 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) 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
|