This commit is contained in:
team3
2026-09-16 01:37:30 +02:00
parent ae75ed8fa3
commit 9b3cd55ba7
6 changed files with 549 additions and 160 deletions

View File

@@ -4,6 +4,8 @@ extends Node3D
const START_DICE := 5
const BID_TIMEOUT := 30.0
const REVEAL_PAUSE := 5.0 ## Anzeigedauer des Rundenergebnisses
const WURF_ZEIT := 1.1 ## wie lange die geworfene Münze zu sehen ist,
## bevor ihre Wirkung einsetzt
var table: GameTable
var hud: CanvasLayer
@@ -19,12 +21,13 @@ var bid: Dictionary = {}
var turn := 0
var starter := 0
var fogged: Dictionary = {} ## seat -> Array[int] verdeckter Würfel
var peeked: Dictionary = {} ## viewer -> {target_seat: [idx, value]}
var peeked: Dictionary = {} ## viewer -> {target_seat: Array[[idx, value]]}
var min_raise: Dictionary = {} ## seat -> int
var must_bid: Dictionary = {} ## seat -> bool
var hand: Dictionary = {} ## seat -> Array[String], Karten auf der Hand
var letzte_karte: Dictionary = {} ## seat -> zuletzt gespielte Karte, nur Anzeige
var _karte_laeuft := false ## eine Karte wird gerade aufgelöst
var hand: Dictionary = {} ## seat -> Array[String], Münzen beim Spieler
var mitte: Dictionary = {} ## seat -> zuletzt in die Mitte gelegte Münze
var _muenze_laeuft := false ## eine Münze wird gerade aufgelöst
var _wurf_nr := 0 ## zählt die Würfe, damit das HUD neue erkennt
var last_bids: Dictionary = {} ## seat -> letzte Ansage dieser Runde
var log_lines: Array = []
var phase := "lobby"
@@ -78,7 +81,9 @@ func setup(cfg: Dictionary) -> void:
hud.pause_changed.connect(_on_pause_changed)
for i in seats.size():
hand[i] = []
# Die Startmünze zieht nur der Host. Beim Client kommt sie mit dem
# ersten Zustand, sonst läge dort kurz eine andere.
hand[i] = [BoosterDB.zieh(rng)] if multiplayer.is_server() else []
table.set_dice_count(i, START_DICE)
table.set_dice_visible(i, false)
table.set_cup_down(i, i != local_seat)
@@ -113,6 +118,7 @@ func _refresh_labels() -> void:
elif phase == "bid" and i == turn:
col = Color(1.0, 0.85, 0.3)
table.set_label(i, txt, col)
table.set_active_seat(turn if phase == "bid" else -1)
# ---------------------------------------------------------------- Host-Ablauf
@@ -199,7 +205,8 @@ func _phase_roll() -> void:
peeked.clear()
min_raise.clear()
must_bid.clear()
letzte_karte.clear()
mitte.clear()
_wurf_nr = 0
banner = ""
_log("--- Neue Runde ---")
@@ -254,57 +261,81 @@ func _await_settled(list: Array, timeout := 7.0) -> void:
return
# ---------------------------------------------------------------- Karten
# ---------------------------------------------------------------- Münzen
## Wer einen Würfel abgibt, zieht eine Karte. Das ist der einzige Nachschub.
func _zieh_karte(seat: int) -> void:
var karten: Array = hand.get(seat, [])
if karten.size() >= BoosterDB.HANDLIMIT:
_log("%s hat die Hand voll und zieht nicht nach." % seats[seat].name)
## Wer einen Würfel abgibt, bekommt eine Münze. Das ist der einzige Nachschub.
func _zieh_muenze(seat: int) -> void:
var muenzen: Array = hand.get(seat, [])
if muenzen.size() >= BoosterDB.HANDLIMIT:
_log("%s hat schon drei Münzen und bekommt keine." % seats[seat].name)
return
var id := BoosterDB.zieh(rng)
karten.append(id)
hand[seat] = karten
_log("%s zieht %s." % [seats[seat].name, BoosterDB.get_booster(id).name])
muenzen.append(id)
hand[seat] = muenzen
_log("%s bekommt %s." % [seats[seat].name, BoosterDB.get_booster(id).name])
## Ausspielen heißt: Münze in die Mitte legen und werfen. Wappen verstärkt,
## Zahl lässt es bei der Grundwirkung. Beides hilft - verschenkt ist nie ein Zug.
func _do_play(seat: int, id: String, target: int) -> void:
if seat < 0 or phase != "bid" or not seats[seat].alive:
return
# Zwei Karten gleichzeitig würden sich beim Würfeln in die Quere kommen.
if _karte_laeuft:
# Zwei Münzen gleichzeitig würden sich beim Würfeln in die Quere kommen.
if _muenze_laeuft:
return
var karten: Array = hand.get(seat, [])
if not karten.has(id):
var muenzen: Array = hand.get(seat, [])
if not muenzen.has(id):
return
karten.erase(id)
hand[seat] = karten
letzte_karte[seat] = id
_log("%s spielt %s." % [seats[seat].name, BoosterDB.get_booster(id).name])
_karte_laeuft = true
await _wirkung(seat, id, target)
_karte_laeuft = false
muenzen.erase(id)
hand[seat] = muenzen
_muenze_laeuft = true
var stark := rng.randf() < BoosterDB.WAPPEN_CHANCE
_wurf_nr += 1
mitte[seat] = {"id": id, "stark": stark, "nr": _wurf_nr}
_log("%s wirft %s - %s." % [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.
_push_state()
await _sleep(WURF_ZEIT)
if _alive():
await _wirkung(seat, id, target, stark)
_muenze_laeuft = false
if not _alive():
return
# Der Widerruf hat keine eigene Verstärkung; bei Wappen bleibt er liegen.
if stark and BoosterDB.keeps_on_wappen(id):
var zurueck: Array = hand.get(seat, [])
if zurueck.size() < BoosterDB.HANDLIMIT:
zurueck.append(id)
hand[seat] = zurueck
_snapshot_all()
_push_state()
func _wirkung(seat: int, id: String, target: int) -> void:
func _wirkung(seat: int, id: String, target: int, stark: bool) -> void:
var vals: Array = host_values[seat]
var ziel: int = clampi(target, 0, maxi(0, vals.size() - 1))
match id:
"reroll_all":
await _neu_werfen(seat, range(vals.size()))
if stark and not host_values[seat].is_empty():
_set_die(seat, rng.randi_range(0, host_values[seat].size() - 1), 6)
"reroll_one":
await _neu_werfen(seat, [ziel])
await _neu_werfen(seat, _ziel_und_zufall(seat, ziel, stark))
"set_six":
_set_die(seat, ziel, 6)
for k: int in _ziel_und_zufall(seat, ziel, stark):
_set_die(seat, k, 6)
"gain_die":
seats[seat].count += 1
table.set_dice_count(seat, seats[seat].count)
host_values[seat].append(1)
await _neu_werfen(seat, [host_values[seat].size() - 1])
var neu: int = host_values[seat].size() - 1
await _neu_werfen(seat, [neu])
if stark:
_set_die(seat, neu, 6)
"cancel":
fogged.erase(seat)
min_raise.erase(seat)
@@ -312,33 +343,69 @@ func _wirkung(seat: int, id: String, target: int) -> void:
"peek":
var info := {}
for o in _alive_seats():
if o != seat and host_values[o].size() > 0:
var k := rng.randi_range(0, host_values[o].size() - 1)
info[o] = [k, host_values[o][k]]
if o != seat:
info[o] = _zufalls_paare(o, 2 if stark else 1)
peeked[seat] = info
"bid_up":
if not bid.is_empty():
bid.count = int(bid.count) + 1
bid.count = int(bid.count) + (2 if stark else 1)
last_bids[int(bid.seat)] = {"count": bid.count, "face": bid.face}
"enemy_lower":
var minus := 2 if stark else 1
for o in _alive_seats():
if o != seat and host_values[o].size() > 0:
var k := rng.randi_range(0, host_values[o].size() - 1)
_set_die(o, k, maxi(1, host_values[o][k] - 1))
if o != seat:
for k: int in _zufalls_idx(o, 1):
_set_die(o, k, maxi(1, int(host_values[o][k]) - minus))
"fog":
for o in _alive_seats():
if o != seat and host_values[o].size() > 0:
var k := rng.randi_range(0, host_values[o].size() - 1)
var liste: Array = fogged.get(o, [])
if o == seat:
continue
var liste: Array = fogged.get(o, [])
for k: int in _zufalls_idx(o, 2 if stark else 1):
if not liste.has(k):
liste.append(k)
fogged[o] = liste
fogged[o] = liste
"raise_min":
for o in _alive_seats():
if o != seat:
min_raise[o] = 2
min_raise[o] = 3 if stark else 2
"gag":
must_bid[turn] = true
if stark:
for o in _alive_seats():
if o != seat:
must_bid[o] = true
else:
must_bid[turn] = true
## Bis zu `n` verschiedene Würfelplätze eines Sitzes, zufällig gezogen.
func _zufalls_idx(seat: int, n: int) -> Array:
var frei: Array = range(host_values[seat].size())
var out: Array = []
while out.size() < n and not frei.is_empty():
out.append(frei.pop_at(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, 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 host_values[seat].size():
if k != ziel:
frei.append(k)
if not frei.is_empty():
out.append(frei[rng.randi_range(0, frei.size() - 1)])
return out
## Würfel neu werfen und die Augenzahl danach übernehmen.
@@ -386,8 +453,8 @@ func _phase_bid() -> Dictionary:
_push_state()
if seats[turn].ai:
await _sleep(1.1)
# Bots spielen ihre Karten, bevor sie ansagen.
await _bot_karte(turn)
# Bots werfen ihre Münzen, bevor sie ansagen.
await _bot_muenze(turn)
if not _alive():
return {}
var a := DiceAI.choose_action(host_values[turn], _total_dice(), bid,
@@ -413,16 +480,16 @@ func _phase_bid() -> Dictionary:
return {}
## Ein Bot legt manchmal eine Karte, statt sie bis zum Ausscheiden zu horten.
func _bot_karte(seat: int) -> void:
var karten: Array = hand.get(seat, [])
if karten.is_empty():
## Ein Bot wirft manchmal eine Münze, statt sie bis zum Ausscheiden zu horten.
func _bot_muenze(seat: int) -> void:
var muenzen: Array = hand.get(seat, [])
if muenzen.is_empty():
return
# Volle Hand: raus damit. Sonst nur ab und zu, sonst ist sofort alles weg.
var lust := 1.0 if karten.size() >= BoosterDB.HANDLIMIT else 0.35
# Voller Vorrat: raus damit. Sonst nur ab und zu, sonst ist alles sofort weg.
var lust := 1.0 if muenzen.size() >= BoosterDB.HANDLIMIT else 0.35
if rng.randf() > lust:
return
var id: String = karten[rng.randi_range(0, karten.size() - 1)]
var id: String = muenzen[rng.randi_range(0, muenzen.size() - 1)]
if id == "bid_up" and bid.is_empty():
return
await _do_play(seat, id, DiceAI.target_for(id, host_values[seat]))
@@ -510,17 +577,20 @@ func _phase_reveal(result: Dictionary) -> void:
return
seats[loser].count -= 1
_zieh_karte(loser)
if seats[loser].count <= 0:
seats[loser].alive = false
seats[loser].count = 0
host_values[loser] = []
table.set_dice_count(loser, 0)
_log("%s scheidet aus." % seats[loser].name)
else:
_zieh_muenze(loser)
starter = loser if seats[loser].alive else _next_alive(loser)
# Nur wer noch spielt, bekommt den Becher wieder. Auf einer leeren Zone
# sähe er aus, als lägen dort noch Würfel.
for i in seats.size():
table.set_dice_visible(i, false)
table.set_cup_down(i, true)
table.set_cup_down(i, seats[i].alive)
banner = ""
_push_state()
@@ -645,14 +715,14 @@ func _push_state() -> void:
_state.rpc_id(peer, _state_for(i))
## Wer hat diese Runde schon eine Karte gespielt? Beim Würfeln steht noch
## nichts an, danach ist es offen.
## Welche Münze liegt von wem in der Mitte? Beim Würfeln steht noch nichts
## an, danach ist es offen.
func _round_boosters() -> Array:
var out: Array = []
if phase == "roll":
return out
for i in seats.size():
out.append(String(letzte_karte.get(i, "")))
out.append(mitte.get(i, {}))
return out
@@ -688,7 +758,6 @@ func _state_for(seat: int) -> Dictionary:
"boosts": _round_boosters(),
"actual": reveal_total if phase == "reveal" else 0,
"bids": _seat_bids(),
"boost": String(letzte_karte.get(seat, "")),
"hand": (hand.get(seat, []) as Array).duplicate(),
}
@@ -719,7 +788,7 @@ func _apply_state(st: Dictionary) -> void:
for i in seats.size():
if i != local_seat:
table.set_dice_visible(i, false)
table.set_cup_down(i, true)
table.set_cup_down(i, seats[i].alive)
table.set_dice_visible(local_seat, true)
_refresh_labels()
_apply_fog_to_table()