Files
pirate-dice/scripts/ai.gd
team3 3f4532ad59 Booster-Phase raus, Karten auf die Hand
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>
2026-09-08 16:11:07 +02:00

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