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:
team3
2026-09-08 16:11:07 +02:00
parent 318f9f3159
commit 3f4532ad59
5 changed files with 219 additions and 240 deletions

View File

@@ -136,44 +136,52 @@ Klassisches Liar's Dice ohne Joker. Jeder startet mit 5 Würfeln und sieht nur
die eigenen. die eigenen.
1. **Würfeln** – alle Würfel fallen physikalisch in den eigenen Becher. 1. **Würfeln** – alle Würfel fallen physikalisch in den eigenen Becher.
2. **Boost wählen** – jeder bekommt 3 zufällige Booster angeboten und wählt 2. **Ansagen** – reihum wird ein Gebot "Anzahl x Augenzahl" über alle Würfel
einen; er wirkt nur für die laufende Runde.
3. **Ansagen** – reihum wird ein Gebot "Anzahl x Augenzahl" über alle Würfel
am Tisch gemacht. Jedes Gebot muss höher sein: mehr Würfel, oder gleich am Tisch gemacht. Jedes Gebot muss höher sein: mehr Würfel, oder gleich
viele bei höherer Augenzahl. viele bei höherer Augenzahl.
4. **Anfechten** – statt zu erhöhen kann man das Vorgebot anzweifeln. Alle 3. **Anfechten** – statt zu erhöhen kann man das Vorgebot anzweifeln. Alle
Becher werden gehoben und gezählt: Stimmt das Gebot, verliert der Becher werden gehoben und gezählt: Stimmt das Gebot, verliert der
Anfechter einen Würfel, sonst der Bieter. Anfechter einen Würfel, sonst der Bieter.
Für Boosterwahl und Ansage hat man jeweils **30 Sekunden**; die Restzeit steht Für die Ansage hat man **30 Sekunden**; die Restzeit steht neben „Deine Ansage"
im Booster-Fenster beziehungsweise neben „Deine Ansage" und wird unter zehn und wird unter zehn Sekunden rot. Läuft die Zeit ab, entscheidet der Host mit
Sekunden rot. Läuft die Zeit ab, entscheidet der Host automatisch – beim Booster der Bot-Logik. Ohne dieses Limit würde ein abwesender oder getrennter
zufällig, beim Gebot mit der Bot-Logik. Ohne dieses Limit würde ein abwesender Mitspieler den ganzen Tisch dauerhaft blockieren.
oder getrennter Mitspieler den ganzen Tisch dauerhaft blockieren.
Wer keine Würfel mehr hat, scheidet aus. Der Letzte gewinnt. Wer keine Würfel mehr hat, scheidet aus. Der Letzte gewinnt.
## Booster ## Karten
| Booster | Art | Wirkung | Karten ersetzen die frühere Booster-Phase. Die kostete jede Runde eine eigene
Wartezeit und riss den Ablauf auseinander.
* Wer einen Würfel abgibt, **zieht eine Karte**. Das ist der einzige Nachschub,
also bekommt der etwas zurück, der gerade hinten liegt.
* Gespielt wird **jederzeit während der Ansagen**, nicht nur am eigenen Zug.
* Mehr als **drei** Karten passen nicht auf die Hand.
* Die Hand liegt offen unten links; ein Klick spielt die Karte sofort. Nur
*Feinschliff* und *Glücksgriff* fragen noch, welcher Würfel gemeint ist.
| Karte | Art | Wirkung |
|---|---|---| |---|---|---|
| Neuwurf | Buff | Alle eigenen Würfel neu werfen | | Neuwurf | Buff | Alle eigenen Würfel neu werfen |
| Feinschliff | Buff | Einen gewählten eigenen Würfel neu werfen | | Feinschliff | Buff | Einen gewählten eigenen Würfel neu werfen |
| Glücksgriff | Buff | Ein gewählter Würfel wird zur 6 | | Glücksgriff | Buff | Ein gewählter Würfel wird zur 6 |
| Zwilling | Buff | Ein anderer eigener Würfel nimmt den Wert des gewählten an | | Nachschub | Buff | Du bekommst einen Würfel dazu |
| Widerruf | Buff | Hebt Nebel, Störfeuer und Maulkorb auf, die auf dir liegen |
| Spähen | Buff | Von jedem Gegner wird ein zufälliger Würfel aufgedeckt | | Spähen | Buff | Von jedem Gegner wird ein zufälliger Würfel aufgedeckt |
| Rütteln | Debuff | Jeder Gegner wirft einen zufälligen Würfel neu | | Erhöhen | Debuff | Die laufende Ansage steigt um 1 |
| Absenken | Debuff | Bei jedem Gegner sinkt ein zufälliger Würfel um 1 | | Absenken | Debuff | Bei jedem Gegner sinkt ein zufälliger Würfel um 1 |
| Nebel | Debuff | Jeder Gegner sieht einen eigenen Würfel nicht mehr | | Nebel | Debuff | Jeder Gegner sieht einen eigenen Würfel nicht mehr |
| Störfeuer | Debuff | Gegner müssen diese Runde um mindestens 2 erhöhen | | Störfeuer | Debuff | Gegner müssen diese Runde um mindestens 2 erhöhen |
| Maulkorb | Debuff | Der nächste Spieler muss bieten und darf nicht anfechten | | Maulkorb | Debuff | Wer gerade dran ist, muss bieten und darf nicht anfechten |
## Aufbau ## Aufbau
| Datei | Inhalt | | Datei | Inhalt |
|---|---| |---|---|
| `scripts/net.gd` | Autoload: WebSocket-Verbindung, Räume, Lobby, Spielstart | | `scripts/net.gd` | Autoload: WebSocket-Verbindung, Räume, Lobby, Spielstart |
| `scripts/boosters.gd` | Autoload: Booster-Katalog | | `scripts/boosters.gd` | Autoload: Kartenkatalog |
| `scripts/game.gd` | Spiellogik (host-autoritativ), RPCs, Zustandsabgleich | | `scripts/game.gd` | Spiellogik (host-autoritativ), RPCs, Zustandsabgleich |
| `scripts/table.gd` | Aufbau der 3D-Szene: Tisch, Würfelzonen, Becher, Kamera | | `scripts/table.gd` | Aufbau der 3D-Szene: Tisch, Würfelzonen, Becher, Kamera |
| `scripts/die.gd` | Physik-Würfel, Ablesen der Augenzahl aus der Ruhelage | | `scripts/die.gd` | Physik-Würfel, Ablesen der Augenzahl aus der Ruhelage |

View File

@@ -102,38 +102,12 @@ static func _is_higher(count: int, face: int, bid: Dictionary, min_raise: int) -
return count == bid.count and face > bid.face return count == bid.count and face > bid.face
static func choose_booster(offer: Array, values: Array, dice_left: int, rng: RandomNumberGenerator) -> Dictionary: ## Welcher eigene Würfel soll dran glauben? Für Karten mit Zielwahl.
var scores: Array = [] static func target_for(id: String, values: Array) -> int:
for id in offer:
var b: Dictionary = BoosterDB.get_booster(id)
var s := rng.randf()
if b.kind == BoosterDB.Kind.BUFF:
s += 0.35
if dice_left <= 2 and b.kind == BoosterDB.Kind.BUFF:
s += 0.35
scores.append(s)
var pick := 0
for i in scores.size():
if scores[i] > scores[pick]:
pick = i
var id: String = offer[pick]
return {"id": id, "target": _target_for(id, values)}
static func _target_for(id: String, values: Array) -> int:
if values.is_empty(): if values.is_empty():
return 0 return 0
if id == "twin": # Der schwächste Würfel: den neu zu werfen oder zur 6 zu machen bringt
# Würfel mit dem häufigsten Wert # am meisten.
var counts := {}
for v in values:
counts[v] = counts.get(v, 0) + 1
var best_v: int = values[0]
for v: int in counts:
if counts[v] > counts.get(best_v, 0):
best_v = v
return values.find(best_v)
# sonst: schwächster Würfel
var idx := 0 var idx := 0
for i in values.size(): for i in values.size():
if values[i] < values[idx]: if values[i] < values[idx]:

View File

@@ -1,9 +1,15 @@
extends Node extends Node
## Katalog aller Booster. Jede Runde bekommt jeder Spieler 3 davon ## Katalog aller Karten.
## zufällig angeboten und wählt genau einen aus. ##
## Karten gibt es nicht mehr rundenweise zur Auswahl. Wer einen Würfel
## verliert, zieht eine. Gespielt wird jederzeit während der Ansagen - deshalb
## wirkt jede Karte sofort und braucht keine eigene Phase mehr.
enum Kind { BUFF, DEBUFF } enum Kind { BUFF, DEBUFF }
## Mehr als das passt nicht auf die Hand; sonst hortet man bis zum Schluss.
const HANDLIMIT := 3
const ALL := [ const ALL := [
{ {
"id": "reroll_all", "kind": Kind.BUFF, "target": false, "id": "reroll_all", "kind": Kind.BUFF, "target": false,
@@ -21,9 +27,14 @@ const ALL := [
"desc": "Ein gewählter Würfel wird zur 6.", "desc": "Ein gewählter Würfel wird zur 6.",
}, },
{ {
"id": "twin", "kind": Kind.BUFF, "target": true, "id": "gain_die", "kind": Kind.BUFF, "target": false,
"name": "Zwilling", "name": "Nachschub",
"desc": "Ein anderer deiner Würfel nimmt den Wert des gewählten an.", "desc": "Du bekommst einen Würfel dazu.",
},
{
"id": "cancel", "kind": Kind.BUFF, "target": false,
"name": "Widerruf",
"desc": "Hebt Nebel, Störfeuer und Maulkorb auf, die auf dir liegen.",
}, },
{ {
"id": "peek", "kind": Kind.BUFF, "target": false, "id": "peek", "kind": Kind.BUFF, "target": false,
@@ -31,9 +42,9 @@ const ALL := [
"desc": "Von jedem Gegner wird dir ein zufälliger Würfel aufgedeckt.", "desc": "Von jedem Gegner wird dir ein zufälliger Würfel aufgedeckt.",
}, },
{ {
"id": "enemy_reroll", "kind": Kind.DEBUFF, "target": false, "id": "bid_up", "kind": Kind.DEBUFF, "target": false,
"name": "Rütteln", "name": "Erhöhen",
"desc": "Jeder Gegner muss einen zufälligen Würfel neu werfen.", "desc": "Die laufende Ansage steigt um 1.",
}, },
{ {
"id": "enemy_lower", "kind": Kind.DEBUFF, "target": false, "id": "enemy_lower", "kind": Kind.DEBUFF, "target": false,
@@ -53,7 +64,7 @@ const ALL := [
{ {
"id": "gag", "kind": Kind.DEBUFF, "target": false, "id": "gag", "kind": Kind.DEBUFF, "target": false,
"name": "Maulkorb", "name": "Maulkorb",
"desc": "Der nächste Spieler muss bieten und darf nicht anfechten.", "desc": "Wer gerade dran ist, muss bieten und darf nicht anfechten.",
}, },
] ]
@@ -66,17 +77,9 @@ func get_booster(id: String) -> Dictionary:
func needs_target(id: String) -> bool: func needs_target(id: String) -> bool:
var b := get_booster(id) return get_booster(id).get("target", false)
return b.get("target", false)
func draw_offer(rng: RandomNumberGenerator, amount: int = 3) -> Array: ## Eine zufällige Karte, für den, der gerade einen Würfel verloren hat.
var pool := ALL.duplicate() func zieh(rng: RandomNumberGenerator) -> String:
var out: Array = [] return String(ALL[rng.randi_range(0, ALL.size() - 1)].id)
for i in amount:
if pool.is_empty():
break
var k := rng.randi_range(0, pool.size() - 1)
out.append(pool[k].id)
pool.remove_at(k)
return out

View File

@@ -2,7 +2,6 @@ extends Node3D
## Host-autoritative Spiellogik + Client-Ansicht. ## Host-autoritative Spiellogik + Client-Ansicht.
const START_DICE := 5 const START_DICE := 5
const BOOST_TIMEOUT := 30.0
const BID_TIMEOUT := 30.0 const BID_TIMEOUT := 30.0
const REVEAL_PAUSE := 5.0 ## Anzeigedauer des Rundenergebnisses 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 peeked: Dictionary = {} ## viewer -> {target_seat: [idx, value]}
var min_raise: Dictionary = {} ## seat -> int var min_raise: Dictionary = {} ## seat -> int
var must_bid: Dictionary = {} ## seat -> bool var must_bid: Dictionary = {} ## seat -> bool
var chosen_boost: Dictionary = {} var hand: Dictionary = {} ## seat -> Array[String], Karten auf der Hand
var offers: Dictionary = {} ## seat -> die drei angebotenen Booster 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 last_bids: Dictionary = {} ## seat -> letzte Ansage dieser Runde
var log_lines: Array = [] var log_lines: Array = []
var phase := "lobby" var phase := "lobby"
@@ -78,6 +78,7 @@ func setup(cfg: Dictionary) -> void:
hud.pause_changed.connect(_on_pause_changed) hud.pause_changed.connect(_on_pause_changed)
for i in seats.size(): for i in seats.size():
hand[i] = []
table.set_dice_count(i, START_DICE) table.set_dice_count(i, START_DICE)
table.set_dice_visible(i, false) table.set_dice_visible(i, false)
table.set_cup_down(i, i != local_seat) table.set_cup_down(i, i != local_seat)
@@ -123,9 +124,6 @@ func _host_loop() -> void:
await _phase_roll() await _phase_roll()
# Zwischen den Phasen prüfen: wer während des Wurfs verlässt, darf nicht # 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. # noch in die Boosterphase laufen - dort ist das Netzwerk schon weg.
if not _alive():
return
await _phase_boost()
if not _alive(): if not _alive():
return return
var result: Dictionary = await _phase_bid() var result: Dictionary = await _phase_bid()
@@ -201,7 +199,7 @@ func _phase_roll() -> void:
peeked.clear() peeked.clear()
min_raise.clear() min_raise.clear()
must_bid.clear() must_bid.clear()
chosen_boost.clear() letzte_karte.clear()
banner = "" banner = ""
_log("--- Neue Runde ---") _log("--- Neue Runde ---")
@@ -256,137 +254,106 @@ func _await_settled(list: Array, timeout := 7.0) -> void:
return return
func _phase_boost() -> void: # ---------------------------------------------------------------- Karten
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()
await _wait_until(_all_boosts_in, BOOST_TIMEOUT) ## Wer einen Würfel abgibt, zieht eine Karte. Das ist der einzige Nachschub.
if not _alive(): 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 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): func _do_play(seat: int, id: String, target: int) -> void:
var fallback: String = offers[i][rng.randi_range(0, offers[i].size() - 1)] if seat < 0 or phase != "bid" or not seats[seat].alive:
chosen_boost[i] = {"id": fallback, "target": 0} return
await _apply_boosters() # 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(): if not _alive():
return return
_snapshot_all() _snapshot_all()
_push_state() _push_state()
func _all_boosts_in() -> bool: func _wirkung(seat: int, id: String, target: int) -> void:
for i in _alive_seats(): var vals: Array = host_values[seat]
if not _answered.has(i) and not seats[i].ai: var ziel: int = clampi(target, 0, maxi(0, vals.size() - 1))
return false match id:
return true "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: ## Würfel neu werfen und die Augenzahl danach übernehmen.
var peer: int = seats[seat].peer func _neu_werfen(seat: int, idxs: Array) -> void:
if peer == multiplayer.get_unique_id(): _stream = true
hud.show_booster_offer(ids, my_values) _throw_subset(seat, idxs)
else: await _await_settled([seat])
_offer.rpc_id(peer, ids) _stream = false
if not _alive():
return
func _apply_boosters() -> void: var dl: Array = table.dice_of(seat)
var rerolls := {} ## seat -> Array[int] for k: int in idxs:
var order := _alive_seats() if k < dl.size():
host_values[seat][k] = dl[k].top_value()
for seat in order: table.freeze_dice(seat)
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
func _set_die(seat: int, idx: int, value: int) -> void: func _set_die(seat: int, idx: int, value: int) -> void:
@@ -419,6 +386,10 @@ func _phase_bid() -> Dictionary:
_push_state() _push_state()
if seats[turn].ai: if seats[turn].ai:
await _sleep(1.1) 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, var a := DiceAI.choose_action(host_values[turn], _total_dice(), bid,
min_raise.get(turn, 1), must_bid.get(turn, false), rng) min_raise.get(turn, 1), must_bid.get(turn, false), rng)
var res := _apply_action(turn, a) var res := _apply_action(turn, a)
@@ -442,6 +413,22 @@ func _phase_bid() -> Dictionary:
return {} 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. ## Gibt {} zurück wenn das Spiel weitergeht, sonst das Anfechtungs-Ergebnis.
func _apply_action(seat: int, a: Dictionary) -> Dictionary: func _apply_action(seat: int, a: Dictionary) -> Dictionary:
if a.type == "bid" and not _valid_bid(seat, int(a.count), int(a.face)): 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 return
seats[loser].count -= 1 seats[loser].count -= 1
_zieh_karte(loser)
if seats[loser].count <= 0: if seats[loser].count <= 0:
seats[loser].alive = false seats[loser].alive = false
seats[loser].count = 0 seats[loser].count = 0
@@ -657,14 +645,14 @@ func _push_state() -> void:
_state.rpc_id(peer, _state_for(i)) _state.rpc_id(peer, _state_for(i))
## Erst nach der Wahl offenlegen, sonst verrät die Anzeige während der ## Wer hat diese Runde schon eine Karte gespielt? Beim Würfeln steht noch
## Boosterphase, wer schon gewählt hat. ## nichts an, danach ist es offen.
func _round_boosters() -> Array: func _round_boosters() -> Array:
var out: Array = [] var out: Array = []
if phase == "boost" or phase == "roll": if phase == "roll":
return out return out
for i in seats.size(): for i in seats.size():
out.append(String(chosen_boost.get(i, {}).get("id", ""))) out.append(String(letzte_karte.get(i, "")))
return out return out
@@ -700,7 +688,8 @@ func _state_for(seat: int) -> Dictionary:
"boosts": _round_boosters(), "boosts": _round_boosters(),
"actual": reveal_total if phase == "reveal" else 0, "actual": reveal_total if phase == "reveal" else 0,
"bids": _seat_bids(), "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 my_values = st.values
banner = st.banner banner = st.banner
if not multiplayer.is_server(): 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()): for i in mini(seats.size(), st.seats.size()):
seats[i].alive = st.seats[i].alive seats[i].alive = st.seats[i].alive
if seats[i].count != st.seats[i].count: 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: func _on_booster_chosen(id: String, target: int) -> void:
if multiplayer.is_server(): if multiplayer.is_server():
_do_boost(local_seat, id, target) _do_play(local_seat, id, target)
else: else:
_req_boost.rpc_id(1, id, target) _req_boost.rpc_id(1, id, target)
@@ -812,7 +804,7 @@ func _req_challenge() -> void:
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
func _req_boost(id: String, target: int) -> void: 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: func _seat_of(peer: int) -> int:
@@ -843,18 +835,3 @@ func _do_challenge(seat: int) -> void:
return return
_answered[seat] = _apply_action(seat, {"type": "challenge"}) _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)

View File

@@ -39,6 +39,8 @@ var bid_btn: Button
var challenge_btn: Button var challenge_btn: Button
var action_panel: PanelContainer var action_panel: PanelContainer
var hand_panel: PanelContainer
var hand_box: HBoxContainer
var boost_slot: CenterContainer var boost_slot: CenterContainer
var pause_slot: Control var pause_slot: Control
var pause_hint: Label var pause_hint: Label
@@ -278,6 +280,19 @@ func _build() -> void:
timer_bid.vertical_alignment = VERTICAL_ALIGNMENT_CENTER timer_bid.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
av.add_child(timer_bid) av.add_child(timer_bid)
# --- Handkarten unten links, über der Ansageleiste
hand_panel = _panel()
_place(hand_panel, 0, 1, 0, 1, 16, -190, 16, -124)
root.add_child(hand_panel)
var hv := VBoxContainer.new()
hv.add_theme_constant_override("separation", 4)
hand_panel.add_child(hv)
hv.add_child(_label("Karten", 13, DIM))
hand_box = HBoxContainer.new()
hand_box.add_theme_constant_override("separation", 6)
hv.add_child(hand_box)
hand_panel.visible = false
# --- Booster-Overlay # --- Booster-Overlay
boost_slot = _slot(0, 0, 1, 1, 0, 0, 0, 0) boost_slot = _slot(0, 0, 1, 1, 0, 0, 0, 0)
boost_slot.visible = false boost_slot.visible = false
@@ -482,6 +497,8 @@ func update_state(st: Dictionary, my_seat: int) -> void:
bg = ROT bg = ROT
bid_row.add_child(DieIcon.new(int(st.bid.face), px, bg)) bid_row.add_child(DieIcon.new(int(st.bid.face), px, bg))
_zeichne_hand(st)
_clear(boosts_box) _clear(boosts_box)
var boosts: Array = st.get("boosts", []) var boosts: Array = st.get("boosts", [])
var any := false var any := false
@@ -504,7 +521,7 @@ func update_state(st: Dictionary, my_seat: int) -> void:
boosts_box.add_child(_label("Maulkorb: du musst bieten", 14, ACCENT)) boosts_box.add_child(_label("Maulkorb: du musst bieten", 14, ACCENT))
any = true any = true
if not any: if not any:
boosts_box.add_child(_label("Keine Booster aktiv", 14, DIM)) boosts_box.add_child(_label("Keine Karte gespielt", 14, DIM))
banner_label.text = st.banner banner_label.text = st.banner
@@ -551,7 +568,9 @@ func _process(_delta: float) -> void:
var col := Color(0.95, 0.45, 0.4) if left <= 10.0 else DIM var col := Color(0.95, 0.45, 0.4) if left <= 10.0 else DIM
timer_bid.text = text if wants_bid else "" timer_bid.text = text if wants_bid else ""
timer_bid.add_theme_color_override("font_color", col) timer_bid.add_theme_color_override("font_color", col)
timer_boost.text = "Noch %s - danach wird zufällig gewählt" % text if wants_boost else "" # Läuft die Ansagezeit ab, während die Würfelauswahl offen steht, wird
# automatisch angesagt - die Karte bleibt dann ungespielt auf der Hand.
timer_boost.text = "Noch %s bis zur Ansage" % text if wants_boost else ""
timer_boost.add_theme_color_override("font_color", col) timer_boost.add_theme_color_override("font_color", col)
@@ -596,32 +615,29 @@ func set_banner(text: String) -> void:
# ---------------------------------------------------------------- Booster # ---------------------------------------------------------------- Booster
func show_booster_offer(ids: Array, values: Array) -> void: ## Die Hand liegt offen unten links. Ein Klick spielt die Karte sofort;
_my_values = values ## nur Karten mit eigenem Ziel fragen noch nach dem Würfel.
_pending_boost = "" func _zeichne_hand(st: Dictionary) -> void:
target_row.visible = false if hand_box == null:
boost_title.text = "Wähle einen Booster" return
_clear(boost_cards) var karten: Array = st.get("hand", [])
for id: String in ids: hand_panel.visible = not karten.is_empty()
_clear(hand_box)
var spielbar: bool = st.phase == "bid"
for id: String in karten:
var b: Dictionary = BoosterDB.get_booster(id) var b: Dictionary = BoosterDB.get_booster(id)
var card := _panel(Color(0.10, 0.12, 0.17, 0.98)) if b.is_empty():
card.custom_minimum_size = Vector2(210, 170) continue
var v := VBoxContainer.new()
v.add_theme_constant_override("separation", 6)
card.add_child(v)
var kind_col := Color(0.5, 0.9, 0.6) if b.kind == BoosterDB.Kind.BUFF else Color(0.95, 0.5, 0.5)
v.add_child(_label("BUFF" if b.kind == BoosterDB.Kind.BUFF else "DEBUFF", 12, kind_col))
v.add_child(_label(b.name, 20, ACCENT))
var d := _label(b.desc, 14, Color(0.8, 0.82, 0.88))
d.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
d.custom_minimum_size = Vector2(190, 60)
v.add_child(d)
var btn := Button.new() var btn := Button.new()
btn.text = "Wählen" btn.text = String(b.name)
btn.tooltip_text = String(b.desc)
btn.custom_minimum_size = Vector2(0, 40)
btn.disabled = not spielbar
var col := Color(0.5, 0.9, 0.6) if b.kind == BoosterDB.Kind.BUFF else Color(0.95, 0.5, 0.5)
btn.add_theme_color_override("font_color", col)
btn.add_theme_color_override("font_hover_color", Color.WHITE)
btn.pressed.connect(_pick.bind(id)) btn.pressed.connect(_pick.bind(id))
v.add_child(btn) hand_box.add_child(btn)
boost_cards.add_child(card)
boost_slot.visible = true
func _pick(id: String) -> void: func _pick(id: String) -> void:
@@ -633,6 +649,7 @@ func _pick(id: String) -> void:
boost_title.text = "%s - welchen Würfel?" % BoosterDB.get_booster(id).name boost_title.text = "%s - welchen Würfel?" % BoosterDB.get_booster(id).name
_clear(boost_cards) _clear(boost_cards)
_clear(target_row) _clear(target_row)
boost_slot.visible = true
for i in _my_values.size(): for i in _my_values.size():
var b := Button.new() var b := Button.new()
b.custom_minimum_size = Vector2(56, 56) b.custom_minimum_size = Vector2(56, 56)