Die Boosterwahl kostete jede Runde eine eigene Phase mit 30 Sekunden Frist. Zwischen Wurf und Ansage lag damit eine Wartezeit, die den Ablauf zerriss. Jetzt gibt es Karten statt einer Phase: * Wer einen Wuerfel abgibt, zieht eine Karte. Das ist der einzige Nachschub, also bekommt der etwas zurueck, der gerade hinten liegt. * Gespielt wird jederzeit waehrend der Ansagen, nicht nur am eigenen Zug. * Hoechstens drei Karten auf der Hand. * Die Hand liegt offen unten links, ein Klick spielt. Nur Feinschliff und Gluecksgriff fragen noch nach dem Wuerfel. Der Katalog ist angepasst, weil jede Karte jetzt sofort wirken muss: neu sind Erhoehen (Ansage +1), Widerruf (hebt Nebel, Stoerfeuer und Maulkorb auf dir auf) und Nachschub (ein Wuerfel dazu). Zwilling und Ruetteln sind raus - beide brauchten den festen Zeitpunkt vor dem Wurf. Bots spielen ihre Karten am eigenen Zug, bei voller Hand immer. Zwei Karten gleichzeitig wuerden sich beim Wuerfeln in die Quere kommen, deshalb wird immer nur eine aufgeloest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
116 lines
3.3 KiB
GDScript
116 lines
3.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) 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
|
|
|
|
|
|
## Welcher eigene Würfel soll dran glauben? Für Karten mit Zielwahl.
|
|
static func target_for(id: String, values: Array) -> int:
|
|
if values.is_empty():
|
|
return 0
|
|
# Der schwächste Würfel: den neu zu werfen oder zur 6 zu machen bringt
|
|
# am meisten.
|
|
var idx := 0
|
|
for i in values.size():
|
|
if values[i] < values[idx]:
|
|
idx = i
|
|
return idx
|