218 lines
7.4 KiB
GDScript
218 lines
7.4 KiB
GDScript
extends RefCounted
|
|
## Münzen: ziehen, werfen, Wirkung, Bot-Absicht, Topf. Arbeitet auf dem Spiel
|
|
## `g` - Zustand und Ablauf bleiben dort, hier steht nur, was Münzen tun.
|
|
## Kein class_name: den Klassencache baut nur der Editor, headless fehlt er.
|
|
|
|
var g: Node
|
|
|
|
|
|
func _init(spiel: Node) -> void:
|
|
g = spiel
|
|
|
|
## Wer einen Würfel abgibt, bekommt eine Münze. Das ist der einzige Nachschub.
|
|
func _zieh_muenze(seat: int) -> void:
|
|
var muenzen: Array = g.hand.get(seat, [])
|
|
if muenzen.size() >= BoosterDB.HANDLIMIT:
|
|
g._log("%s hat schon drei Münzen und bekommt keine." % g.seats[seat].name)
|
|
return
|
|
var id: String = BoosterDB.zieh(g.rng)
|
|
muenzen.append(id)
|
|
g.hand[seat] = muenzen
|
|
g._log("%s bekommt %s." % [g.seats[seat].name, BoosterDB.get_booster(id).name])
|
|
|
|
|
|
## Ausspielen heißt: Münze in den Topf werfen. Geht nur am eigenen Zug, bevor
|
|
## man ansagt - so ist der Zug eine Entscheidung und nichts kommt sich in die
|
|
## Quere. Wappen verstärkt, Zahl lässt es bei der Grundwirkung.
|
|
func _do_play(seat: int, id: String, target: int) -> void:
|
|
if seat < 0 or g.phase != "bid" or seat != g.turn or g._answered.has(seat):
|
|
return
|
|
if g._muenze_laeuft:
|
|
return
|
|
if BoosterDB.needs_bid(id) and g.bid.is_empty():
|
|
return
|
|
if BoosterDB.needs_room(id) and not g._hinten(seat):
|
|
return
|
|
var muenzen: Array = g.hand.get(seat, [])
|
|
if not muenzen.has(id):
|
|
return
|
|
muenzen.erase(id)
|
|
g.hand[seat] = muenzen
|
|
g._muenze_laeuft = true
|
|
|
|
var stark: bool = g.rng.randf() < BoosterDB.WAPPEN_CHANCE
|
|
g._wurf_nr += 1
|
|
g.topf.append({"seat": seat, "id": id, "stark": stark, "nr": g._wurf_nr})
|
|
while g.topf.size() > BoosterDB.TOPF_MAX:
|
|
var weg: Dictionary = g.topf.pop_front()
|
|
g._log("%s fällt vom Tisch - der Topf ist voll." % BoosterDB.get_booster(String(weg.id)).name)
|
|
g._log("%s wirft %s - %s." % [g.seats[seat].name,
|
|
BoosterDB.get_booster(id).name, "Wappen" if stark else "Zahl"])
|
|
|
|
# Erst die Münze zeigen, dann wirken lassen. Andersherum sähe man die
|
|
# Folge vor der Ursache.
|
|
g._push_state()
|
|
await g._sleep(g.WURF_ZEIT)
|
|
if g._alive():
|
|
await _wirkung(seat, id, target, stark)
|
|
g._muenze_laeuft = false
|
|
if not g._alive():
|
|
return
|
|
g._snapshot_all()
|
|
g._push_state()
|
|
|
|
|
|
func _wirkung(seat: int, id: String, target: int, stark: bool) -> void:
|
|
var vals: Array = g.host_values[seat]
|
|
var ziel: int = clampi(target, 0, maxi(0, vals.size() - 1))
|
|
match id:
|
|
"peek":
|
|
var info := {}
|
|
for o in g._alive_seats():
|
|
if o != seat:
|
|
info[o] = _zufalls_paare(o, 2 if stark else 1)
|
|
g.peeked[seat] = info
|
|
"count":
|
|
# Zahl: ein Fenster von drei, in dem die Wahrheit irgendwo liegt.
|
|
# Genau wüsste man sonst, ob die Ansage hält - kein Bluff mehr.
|
|
var n: int = _count_face(int(g.bid.face))
|
|
var von: int = n if stark else maxi(0, n - g.rng.randi_range(0, 2))
|
|
g.gezaehlt[seat] = {int(g.bid.face): [von, von if stark else von + 2]}
|
|
"set_face":
|
|
for k: int in _ziel_und_zufall(seat, ziel, stark):
|
|
g._set_die(seat, k, int(g.bid.face))
|
|
"gain_die":
|
|
g.seats[seat].count += 1
|
|
g.table.set_dice_count(seat, g.seats[seat].count)
|
|
g.host_values[seat].append(1)
|
|
var neu: int = g.host_values[seat].size() - 1
|
|
await g._neu_werfen(seat, [neu])
|
|
if stark:
|
|
g._set_die(seat, neu, 6)
|
|
"bid_up":
|
|
g.bid.count = int(g.bid.count) + (2 if stark else 1)
|
|
g.last_bids[int(g.bid.seat)] = {"count": g.bid.count, "face": g.bid.face}
|
|
g.verlauf.append({"seat": int(g.bid.seat), "count": g.bid.count, "face": g.bid.face, "erhoeht": true})
|
|
"gag":
|
|
if stark:
|
|
for o in g._alive_seats():
|
|
if o != seat:
|
|
g.must_bid[o] = true
|
|
else:
|
|
g.must_bid[g._next_alive(seat)] = true
|
|
|
|
|
|
func _count_face(face: int) -> int:
|
|
var n := 0
|
|
for i in g._alive_seats():
|
|
for v in g.host_values[i]:
|
|
if v == face:
|
|
n += 1
|
|
return n
|
|
|
|
|
|
## Bis zu `n` verschiedene Würfelplätze eines Sitzes, zufällig gezogen.
|
|
func _zufalls_idx(seat: int, n: int) -> Array:
|
|
var frei: Array = range(g.host_values[seat].size())
|
|
var out: Array = []
|
|
while out.size() < n and not frei.is_empty():
|
|
out.append(frei.pop_at(g.rng.randi_range(0, frei.size() - 1)))
|
|
return out
|
|
|
|
|
|
func _zufalls_paare(seat: int, n: int) -> Array:
|
|
var out: Array = []
|
|
for k: int in _zufalls_idx(seat, n):
|
|
out.append([k, g.host_values[seat][k]])
|
|
return out
|
|
|
|
|
|
## Der selbst gewählte Würfel, bei Wappen zusätzlich ein anderer zufälliger.
|
|
func _ziel_und_zufall(seat: int, ziel: int, stark: bool) -> Array:
|
|
var out: Array = [ziel]
|
|
if not stark:
|
|
return out
|
|
var frei: Array = []
|
|
for k in g.host_values[seat].size():
|
|
if k != ziel:
|
|
frei.append(k)
|
|
if not frei.is_empty():
|
|
out.append(frei[g.rng.randi_range(0, frei.size() - 1)])
|
|
return out
|
|
|
|
|
|
## Ein Bot wirft eine Münze, wenn sie zu seinem Vorhaben passt. `a` ist die
|
|
## geplante Handlung. Gibt zurück, ob geworfen wurde.
|
|
func _bot_muenze(seat: int, a: Dictionary) -> bool:
|
|
var muenzen: Array = g.hand.get(seat, [])
|
|
if muenzen.is_empty():
|
|
return false
|
|
var vals: Array = g.host_values[seat]
|
|
var unknown: int = g._total_dice() - vals.size()
|
|
var will_bieten: bool = a.type == "bid"
|
|
# Wie glaubwürdig ist das, was der Bot vorhat bzw. was auf dem Tisch liegt?
|
|
var eigenes: float = DiceAI.chance(vals, unknown, int(a.count), int(a.face)) if will_bieten else 0.0
|
|
var liegt: float = DiceAI.chance(vals, unknown, int(g.bid.count), int(g.bid.face)) if not g.bid.is_empty() else 0.0
|
|
|
|
var wahl := ""
|
|
var ziel := 0
|
|
# Wirklich hinten liegen: Nachschub ist der Weg zurück. Bei Gleichstand
|
|
# nicht - sonst wirft ihn zu Beginn jeder, und der Tisch wird nie leerer.
|
|
if muenzen.has("gain_die") and g._hinten(seat):
|
|
wahl = "gain_die"
|
|
# Vor dem Anfechten lieber noch nachsehen, wenn es knapp ist.
|
|
elif not will_bieten and liegt > 0.2 and liegt < 0.55 and muenzen.has("count"):
|
|
wahl = "count"
|
|
elif not will_bieten and liegt > 0.2 and liegt < 0.55 and muenzen.has("peek"):
|
|
wahl = "peek"
|
|
# Zudrücken: die Ansage des Gegners höher schrauben, dann anfechten.
|
|
elif not will_bieten and muenzen.has("bid_up"):
|
|
wahl = "bid_up"
|
|
# Ein Bluff steht an: dem Nächsten den Mund verbieten.
|
|
elif will_bieten and eigenes < 0.35 and muenzen.has("gag"):
|
|
wahl = "gag"
|
|
# Auf derselben Augenzahl erhöhen: einen Würfel dazu drehen.
|
|
elif will_bieten and not g.bid.is_empty() and int(a.face) == int(g.bid.face) \
|
|
and muenzen.has("set_face") and vals.count(int(g.bid.face)) < vals.size():
|
|
wahl = "set_face"
|
|
ziel = DiceAI.target_for(vals, int(g.bid.face))
|
|
# Voller Vorrat: irgendwas Spielbares raus, sonst wird nie geworfen.
|
|
elif muenzen.size() >= BoosterDB.HANDLIMIT:
|
|
for id: String in muenzen:
|
|
if BoosterDB.needs_bid(id) and g.bid.is_empty():
|
|
continue
|
|
if BoosterDB.needs_room(id) and not g._hinten(seat):
|
|
continue
|
|
wahl = id
|
|
break
|
|
if wahl == "":
|
|
return false
|
|
await _do_play(seat, wahl, ziel)
|
|
await g._sleep(0.6)
|
|
return true
|
|
|
|
|
|
## Die grösste Würfelzahl am Tisch - wer weniger hat, liegt hinten.
|
|
func _groesste_hand() -> int:
|
|
var k := 0
|
|
for i in g._alive_seats():
|
|
k = maxi(k, g.seats[i].count)
|
|
return k
|
|
|
|
|
|
## Wer das Aufdecken gewinnt, nimmt aus dem Topf, so viel in den Vorrat
|
|
## passt. Der Rest bleibt liegen und wächst in die nächste Runde - so ist
|
|
## ein voller Vorrat kein Grund, den Topf zu verschenken.
|
|
func _topf_an(winner: int) -> void:
|
|
if g.topf.is_empty():
|
|
return
|
|
var h: Array = g.hand.get(winner, [])
|
|
var genommen := 0
|
|
g._topf_ziel = winner
|
|
while not g.topf.is_empty() and h.size() < BoosterDB.HANDLIMIT:
|
|
h.append(String(g.topf.pop_front().id))
|
|
genommen += 1
|
|
g.hand[winner] = h
|
|
g._log("%s nimmt %d Münze(n) aus dem Topf, %d bleiben." % [
|
|
g.seats[winner].name, genommen, g.topf.size()])
|