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:
38
README.md
38
README.md
@@ -136,44 +136,52 @@ Klassisches Liar's Dice ohne Joker. Jeder startet mit 5 Würfeln und sieht nur
|
||||
die eigenen.
|
||||
|
||||
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
|
||||
einen; er wirkt nur für die laufende Runde.
|
||||
3. **Ansagen** – reihum wird ein Gebot "Anzahl x Augenzahl" über alle Würfel
|
||||
2. **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
|
||||
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
|
||||
Anfechter einen Würfel, sonst der Bieter.
|
||||
|
||||
Für Boosterwahl und Ansage hat man jeweils **30 Sekunden**; die Restzeit steht
|
||||
im Booster-Fenster beziehungsweise neben „Deine Ansage" und wird unter zehn
|
||||
Sekunden rot. Läuft die Zeit ab, entscheidet der Host automatisch – beim Booster
|
||||
zufällig, beim Gebot mit der Bot-Logik. Ohne dieses Limit würde ein abwesender
|
||||
oder getrennter Mitspieler den ganzen Tisch dauerhaft blockieren.
|
||||
Für die Ansage hat man **30 Sekunden**; die Restzeit steht neben „Deine Ansage"
|
||||
und wird unter zehn Sekunden rot. Läuft die Zeit ab, entscheidet der Host mit
|
||||
der Bot-Logik. Ohne dieses Limit würde ein abwesender oder getrennter
|
||||
Mitspieler den ganzen Tisch dauerhaft blockieren.
|
||||
|
||||
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 |
|
||||
| Feinschliff | Buff | Einen gewählten eigenen Würfel neu werfen |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| Nebel | Debuff | Jeder Gegner sieht einen eigenen Würfel nicht mehr |
|
||||
| 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
|
||||
|
||||
| Datei | Inhalt |
|
||||
|---|---|
|
||||
| `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/table.gd` | Aufbau der 3D-Szene: Tisch, Würfelzonen, Becher, Kamera |
|
||||
| `scripts/die.gd` | Physik-Würfel, Ablesen der Augenzahl aus der Ruhelage |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
static func choose_booster(offer: Array, values: Array, dice_left: int, rng: RandomNumberGenerator) -> Dictionary:
|
||||
var scores: Array = []
|
||||
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:
|
||||
## 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
|
||||
if id == "twin":
|
||||
# Würfel mit dem häufigsten Wert
|
||||
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
|
||||
# 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]:
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
extends Node
|
||||
## Katalog aller Booster. Jede Runde bekommt jeder Spieler 3 davon
|
||||
## zufällig angeboten und wählt genau einen aus.
|
||||
## Katalog aller Karten.
|
||||
##
|
||||
## 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 }
|
||||
|
||||
## Mehr als das passt nicht auf die Hand; sonst hortet man bis zum Schluss.
|
||||
const HANDLIMIT := 3
|
||||
|
||||
const ALL := [
|
||||
{
|
||||
"id": "reroll_all", "kind": Kind.BUFF, "target": false,
|
||||
@@ -21,9 +27,14 @@ const ALL := [
|
||||
"desc": "Ein gewählter Würfel wird zur 6.",
|
||||
},
|
||||
{
|
||||
"id": "twin", "kind": Kind.BUFF, "target": true,
|
||||
"name": "Zwilling",
|
||||
"desc": "Ein anderer deiner Würfel nimmt den Wert des gewählten an.",
|
||||
"id": "gain_die", "kind": Kind.BUFF, "target": false,
|
||||
"name": "Nachschub",
|
||||
"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,
|
||||
@@ -31,9 +42,9 @@ const ALL := [
|
||||
"desc": "Von jedem Gegner wird dir ein zufälliger Würfel aufgedeckt.",
|
||||
},
|
||||
{
|
||||
"id": "enemy_reroll", "kind": Kind.DEBUFF, "target": false,
|
||||
"name": "Rütteln",
|
||||
"desc": "Jeder Gegner muss einen zufälligen Würfel neu werfen.",
|
||||
"id": "bid_up", "kind": Kind.DEBUFF, "target": false,
|
||||
"name": "Erhöhen",
|
||||
"desc": "Die laufende Ansage steigt um 1.",
|
||||
},
|
||||
{
|
||||
"id": "enemy_lower", "kind": Kind.DEBUFF, "target": false,
|
||||
@@ -53,7 +64,7 @@ const ALL := [
|
||||
{
|
||||
"id": "gag", "kind": Kind.DEBUFF, "target": false,
|
||||
"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:
|
||||
var b := get_booster(id)
|
||||
return b.get("target", false)
|
||||
return get_booster(id).get("target", false)
|
||||
|
||||
|
||||
func draw_offer(rng: RandomNumberGenerator, amount: int = 3) -> Array:
|
||||
var pool := ALL.duplicate()
|
||||
var out: Array = []
|
||||
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
|
||||
## Eine zufällige Karte, für den, der gerade einen Würfel verloren hat.
|
||||
func zieh(rng: RandomNumberGenerator) -> String:
|
||||
return String(ALL[rng.randi_range(0, ALL.size() - 1)].id)
|
||||
|
||||
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)
|
||||
|
||||
@@ -39,6 +39,8 @@ var bid_btn: Button
|
||||
var challenge_btn: Button
|
||||
var action_panel: PanelContainer
|
||||
|
||||
var hand_panel: PanelContainer
|
||||
var hand_box: HBoxContainer
|
||||
var boost_slot: CenterContainer
|
||||
var pause_slot: Control
|
||||
var pause_hint: Label
|
||||
@@ -278,6 +280,19 @@ func _build() -> void:
|
||||
timer_bid.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
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
|
||||
boost_slot = _slot(0, 0, 1, 1, 0, 0, 0, 0)
|
||||
boost_slot.visible = false
|
||||
@@ -482,6 +497,8 @@ func update_state(st: Dictionary, my_seat: int) -> void:
|
||||
bg = ROT
|
||||
bid_row.add_child(DieIcon.new(int(st.bid.face), px, bg))
|
||||
|
||||
_zeichne_hand(st)
|
||||
|
||||
_clear(boosts_box)
|
||||
var boosts: Array = st.get("boosts", [])
|
||||
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))
|
||||
any = true
|
||||
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
|
||||
|
||||
@@ -551,7 +568,9 @@ func _process(_delta: float) -> void:
|
||||
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.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)
|
||||
|
||||
|
||||
@@ -596,32 +615,29 @@ func set_banner(text: String) -> void:
|
||||
|
||||
# ---------------------------------------------------------------- Booster
|
||||
|
||||
func show_booster_offer(ids: Array, values: Array) -> void:
|
||||
_my_values = values
|
||||
_pending_boost = ""
|
||||
target_row.visible = false
|
||||
boost_title.text = "Wähle einen Booster"
|
||||
_clear(boost_cards)
|
||||
for id: String in ids:
|
||||
## Die Hand liegt offen unten links. Ein Klick spielt die Karte sofort;
|
||||
## nur Karten mit eigenem Ziel fragen noch nach dem Würfel.
|
||||
func _zeichne_hand(st: Dictionary) -> void:
|
||||
if hand_box == null:
|
||||
return
|
||||
var karten: Array = st.get("hand", [])
|
||||
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 card := _panel(Color(0.10, 0.12, 0.17, 0.98))
|
||||
card.custom_minimum_size = Vector2(210, 170)
|
||||
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)
|
||||
if b.is_empty():
|
||||
continue
|
||||
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))
|
||||
v.add_child(btn)
|
||||
boost_cards.add_child(card)
|
||||
boost_slot.visible = true
|
||||
hand_box.add_child(btn)
|
||||
|
||||
|
||||
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
|
||||
_clear(boost_cards)
|
||||
_clear(target_row)
|
||||
boost_slot.visible = true
|
||||
for i in _my_values.size():
|
||||
var b := Button.new()
|
||||
b.custom_minimum_size = Vector2(56, 56)
|
||||
|
||||
Reference in New Issue
Block a user