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>
This commit is contained in:
275
scripts/game.gd
275
scripts/game.gd
@@ -2,7 +2,6 @@ extends Node3D
|
||||
## Host-autoritative Spiellogik + Client-Ansicht.
|
||||
|
||||
const START_DICE := 5
|
||||
const BOOST_TIMEOUT := 30.0
|
||||
const BID_TIMEOUT := 30.0
|
||||
const REVEAL_PAUSE := 5.0 ## Anzeigedauer des Rundenergebnisses
|
||||
|
||||
@@ -23,8 +22,9 @@ var fogged: Dictionary = {} ## seat -> Array[int] verdeckter Würfel
|
||||
var peeked: Dictionary = {} ## viewer -> {target_seat: [idx, value]}
|
||||
var min_raise: Dictionary = {} ## seat -> int
|
||||
var must_bid: Dictionary = {} ## seat -> bool
|
||||
var chosen_boost: Dictionary = {}
|
||||
var offers: Dictionary = {} ## seat -> die drei angebotenen Booster
|
||||
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 last_bids: Dictionary = {} ## seat -> letzte Ansage dieser Runde
|
||||
var log_lines: Array = []
|
||||
var phase := "lobby"
|
||||
@@ -78,6 +78,7 @@ func setup(cfg: Dictionary) -> void:
|
||||
hud.pause_changed.connect(_on_pause_changed)
|
||||
|
||||
for i in seats.size():
|
||||
hand[i] = []
|
||||
table.set_dice_count(i, START_DICE)
|
||||
table.set_dice_visible(i, false)
|
||||
table.set_cup_down(i, i != local_seat)
|
||||
@@ -123,9 +124,6 @@ func _host_loop() -> void:
|
||||
await _phase_roll()
|
||||
# Zwischen den Phasen prüfen: wer während des Wurfs verlässt, darf nicht
|
||||
# noch in die Boosterphase laufen - dort ist das Netzwerk schon weg.
|
||||
if not _alive():
|
||||
return
|
||||
await _phase_boost()
|
||||
if not _alive():
|
||||
return
|
||||
var result: Dictionary = await _phase_bid()
|
||||
@@ -201,7 +199,7 @@ func _phase_roll() -> void:
|
||||
peeked.clear()
|
||||
min_raise.clear()
|
||||
must_bid.clear()
|
||||
chosen_boost.clear()
|
||||
letzte_karte.clear()
|
||||
banner = ""
|
||||
_log("--- Neue Runde ---")
|
||||
|
||||
@@ -256,137 +254,106 @@ func _await_settled(list: Array, timeout := 7.0) -> void:
|
||||
return
|
||||
|
||||
|
||||
func _phase_boost() -> void:
|
||||
phase = "boost"
|
||||
_answered.clear()
|
||||
offers.clear()
|
||||
for i in _alive_seats():
|
||||
offers[i] = BoosterDB.draw_offer(rng)
|
||||
if seats[i].ai:
|
||||
var pick := DiceAI.choose_booster(offers[i], host_values[i], seats[i].count, rng)
|
||||
chosen_boost[i] = pick
|
||||
_answered[i] = true
|
||||
else:
|
||||
_send_offer(i, offers[i])
|
||||
deadline = _now() + BOOST_TIMEOUT
|
||||
_push_state()
|
||||
# ---------------------------------------------------------------- Karten
|
||||
|
||||
await _wait_until(_all_boosts_in, BOOST_TIMEOUT)
|
||||
if not _alive():
|
||||
## 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)
|
||||
return
|
||||
var id := BoosterDB.zieh(rng)
|
||||
karten.append(id)
|
||||
hand[seat] = karten
|
||||
_log("%s zieht %s." % [seats[seat].name, BoosterDB.get_booster(id).name])
|
||||
|
||||
for i in _alive_seats():
|
||||
if not chosen_boost.has(i):
|
||||
var fallback: String = offers[i][rng.randi_range(0, offers[i].size() - 1)]
|
||||
chosen_boost[i] = {"id": fallback, "target": 0}
|
||||
await _apply_boosters()
|
||||
|
||||
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:
|
||||
return
|
||||
var karten: Array = hand.get(seat, [])
|
||||
if not karten.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
|
||||
if not _alive():
|
||||
return
|
||||
_snapshot_all()
|
||||
_push_state()
|
||||
|
||||
|
||||
func _all_boosts_in() -> bool:
|
||||
for i in _alive_seats():
|
||||
if not _answered.has(i) and not seats[i].ai:
|
||||
return false
|
||||
return true
|
||||
func _wirkung(seat: int, id: String, target: int) -> 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()))
|
||||
"reroll_one":
|
||||
await _neu_werfen(seat, [ziel])
|
||||
"set_six":
|
||||
_set_die(seat, ziel, 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])
|
||||
"cancel":
|
||||
fogged.erase(seat)
|
||||
min_raise.erase(seat)
|
||||
must_bid.erase(seat)
|
||||
"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]]
|
||||
peeked[seat] = info
|
||||
"bid_up":
|
||||
if not bid.is_empty():
|
||||
bid.count = int(bid.count) + 1
|
||||
last_bids[int(bid.seat)] = {"count": bid.count, "face": bid.face}
|
||||
"enemy_lower":
|
||||
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))
|
||||
"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 not liste.has(k):
|
||||
liste.append(k)
|
||||
fogged[o] = liste
|
||||
"raise_min":
|
||||
for o in _alive_seats():
|
||||
if o != seat:
|
||||
min_raise[o] = 2
|
||||
"gag":
|
||||
must_bid[turn] = true
|
||||
|
||||
|
||||
func _send_offer(seat: int, ids: Array) -> void:
|
||||
var peer: int = seats[seat].peer
|
||||
if peer == multiplayer.get_unique_id():
|
||||
hud.show_booster_offer(ids, my_values)
|
||||
else:
|
||||
_offer.rpc_id(peer, ids)
|
||||
|
||||
|
||||
func _apply_boosters() -> void:
|
||||
var rerolls := {} ## seat -> Array[int]
|
||||
var order := _alive_seats()
|
||||
|
||||
for seat in order:
|
||||
var pick: Dictionary = chosen_boost[seat]
|
||||
var id: String = pick.id
|
||||
var target: int = clampi(int(pick.get("target", 0)), 0, maxi(0, host_values[seat].size() - 1))
|
||||
var b: Dictionary = BoosterDB.get_booster(id)
|
||||
_log("%s nutzt %s." % [seats[seat].name, b.name])
|
||||
|
||||
match id:
|
||||
"reroll_all":
|
||||
rerolls[seat] = range(host_values[seat].size())
|
||||
"reroll_one":
|
||||
rerolls[seat] = _merge(rerolls.get(seat, []), [target])
|
||||
"enemy_reroll":
|
||||
for o in order:
|
||||
if o != seat and host_values[o].size() > 0:
|
||||
var k := rng.randi_range(0, host_values[o].size() - 1)
|
||||
rerolls[o] = _merge(rerolls.get(o, []), [k])
|
||||
"gag":
|
||||
must_bid[_next_alive(seat)] = true
|
||||
"raise_min":
|
||||
for o in order:
|
||||
if o != seat:
|
||||
min_raise[o] = 2
|
||||
|
||||
if not rerolls.is_empty():
|
||||
_stream = true
|
||||
for seat: int in rerolls:
|
||||
_throw_subset(seat, rerolls[seat])
|
||||
await _await_settled(rerolls.keys())
|
||||
_stream = false
|
||||
if not _alive():
|
||||
return
|
||||
for seat: int in rerolls:
|
||||
var dl: Array = table.dice_of(seat)
|
||||
for k: int in rerolls[seat]:
|
||||
if k < dl.size():
|
||||
host_values[seat][k] = dl[k].top_value()
|
||||
table.freeze_dice(seat)
|
||||
|
||||
for seat in order:
|
||||
var pick: Dictionary = chosen_boost[seat]
|
||||
var id: String = pick.id
|
||||
var vals: Array = host_values[seat]
|
||||
var target: int = clampi(int(pick.get("target", 0)), 0, maxi(0, vals.size() - 1))
|
||||
match id:
|
||||
"set_six":
|
||||
_set_die(seat, target, 6)
|
||||
"twin":
|
||||
if vals.size() > 1:
|
||||
var others: Array = []
|
||||
for k in vals.size():
|
||||
if k != target:
|
||||
others.append(k)
|
||||
_set_die(seat, others[rng.randi_range(0, others.size() - 1)], vals[target])
|
||||
"enemy_lower":
|
||||
for o in order:
|
||||
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))
|
||||
"peek":
|
||||
var info := {}
|
||||
for o in order:
|
||||
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]]
|
||||
peeked[seat] = info
|
||||
"fog":
|
||||
for o in order:
|
||||
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 not liste.has(k):
|
||||
liste.append(k)
|
||||
fogged[o] = liste
|
||||
|
||||
|
||||
func _merge(a: Array, b: Array) -> Array:
|
||||
var out := a.duplicate()
|
||||
for v in b:
|
||||
if not out.has(v):
|
||||
out.append(v)
|
||||
return out
|
||||
## Würfel neu werfen und die Augenzahl danach übernehmen.
|
||||
func _neu_werfen(seat: int, idxs: Array) -> void:
|
||||
_stream = true
|
||||
_throw_subset(seat, idxs)
|
||||
await _await_settled([seat])
|
||||
_stream = false
|
||||
if not _alive():
|
||||
return
|
||||
var dl: Array = table.dice_of(seat)
|
||||
for k: int in idxs:
|
||||
if k < dl.size():
|
||||
host_values[seat][k] = dl[k].top_value()
|
||||
table.freeze_dice(seat)
|
||||
|
||||
|
||||
func _set_die(seat: int, idx: int, value: int) -> void:
|
||||
@@ -419,6 +386,10 @@ 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)
|
||||
if not _alive():
|
||||
return {}
|
||||
var a := DiceAI.choose_action(host_values[turn], _total_dice(), bid,
|
||||
min_raise.get(turn, 1), must_bid.get(turn, false), rng)
|
||||
var res := _apply_action(turn, a)
|
||||
@@ -442,6 +413,22 @@ 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():
|
||||
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
|
||||
if rng.randf() > lust:
|
||||
return
|
||||
var id: String = karten[rng.randi_range(0, karten.size() - 1)]
|
||||
if id == "bid_up" and bid.is_empty():
|
||||
return
|
||||
await _do_play(seat, id, DiceAI.target_for(id, host_values[seat]))
|
||||
await _sleep(0.6)
|
||||
|
||||
|
||||
## Gibt {} zurück wenn das Spiel weitergeht, sonst das Anfechtungs-Ergebnis.
|
||||
func _apply_action(seat: int, a: Dictionary) -> Dictionary:
|
||||
if a.type == "bid" and not _valid_bid(seat, int(a.count), int(a.face)):
|
||||
@@ -523,6 +510,7 @@ 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
|
||||
@@ -657,14 +645,14 @@ func _push_state() -> void:
|
||||
_state.rpc_id(peer, _state_for(i))
|
||||
|
||||
|
||||
## Erst nach der Wahl offenlegen, sonst verrät die Anzeige während der
|
||||
## Boosterphase, wer schon gewählt hat.
|
||||
## Wer hat diese Runde schon eine Karte gespielt? Beim Würfeln steht noch
|
||||
## nichts an, danach ist es offen.
|
||||
func _round_boosters() -> Array:
|
||||
var out: Array = []
|
||||
if phase == "boost" or phase == "roll":
|
||||
if phase == "roll":
|
||||
return out
|
||||
for i in seats.size():
|
||||
out.append(String(chosen_boost.get(i, {}).get("id", "")))
|
||||
out.append(String(letzte_karte.get(i, "")))
|
||||
return out
|
||||
|
||||
|
||||
@@ -700,7 +688,8 @@ func _state_for(seat: int) -> Dictionary:
|
||||
"boosts": _round_boosters(),
|
||||
"actual": reveal_total if phase == "reveal" else 0,
|
||||
"bids": _seat_bids(),
|
||||
"boost": chosen_boost.get(seat, {}).get("id", ""),
|
||||
"boost": String(letzte_karte.get(seat, "")),
|
||||
"hand": (hand.get(seat, []) as Array).duplicate(),
|
||||
}
|
||||
|
||||
|
||||
@@ -716,6 +705,9 @@ func _apply_state(st: Dictionary) -> void:
|
||||
my_values = st.values
|
||||
banner = st.banner
|
||||
if not multiplayer.is_server():
|
||||
# Die eigene Hand kommt vom Server; ohne sie wüsste der Client nur,
|
||||
# was das HUD gerade zeichnet.
|
||||
hand[local_seat] = st.get("hand", [])
|
||||
for i in mini(seats.size(), st.seats.size()):
|
||||
seats[i].alive = st.seats[i].alive
|
||||
if seats[i].count != st.seats[i].count:
|
||||
@@ -768,7 +760,7 @@ func _on_challenge_pressed() -> void:
|
||||
|
||||
func _on_booster_chosen(id: String, target: int) -> void:
|
||||
if multiplayer.is_server():
|
||||
_do_boost(local_seat, id, target)
|
||||
_do_play(local_seat, id, target)
|
||||
else:
|
||||
_req_boost.rpc_id(1, id, target)
|
||||
|
||||
@@ -812,7 +804,7 @@ func _req_challenge() -> void:
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _req_boost(id: String, target: int) -> void:
|
||||
_do_boost(_seat_of(multiplayer.get_remote_sender_id()), id, target)
|
||||
_do_play(_seat_of(multiplayer.get_remote_sender_id()), id, target)
|
||||
|
||||
|
||||
func _seat_of(peer: int) -> int:
|
||||
@@ -843,18 +835,3 @@ func _do_challenge(seat: int) -> void:
|
||||
return
|
||||
_answered[seat] = _apply_action(seat, {"type": "challenge"})
|
||||
|
||||
|
||||
func _do_boost(seat: int, id: String, target: int) -> void:
|
||||
if seat < 0 or phase != "boost" or chosen_boost.has(seat) or not seats[seat].alive:
|
||||
return
|
||||
# Nur aus dem eigenen Angebot: sonst könnte ein veränderter Client sich
|
||||
# jede Runde den stärksten Booster aus dem Katalog nehmen.
|
||||
if not (offers.get(seat, []) as Array).has(id):
|
||||
return
|
||||
chosen_boost[seat] = {"id": id, "target": target}
|
||||
_answered[seat] = true
|
||||
|
||||
|
||||
@rpc("authority", "reliable")
|
||||
func _offer(ids: Array) -> void:
|
||||
hud.show_booster_offer(ids, my_values)
|
||||
|
||||
Reference in New Issue
Block a user