update
This commit is contained in:
200
scripts/game.gd
200
scripts/game.gd
@@ -4,6 +4,7 @@ extends Node3D
|
||||
const START_DICE := 5
|
||||
const BOOST_TIMEOUT := 30.0
|
||||
const BID_TIMEOUT := 30.0
|
||||
const REVEAL_PAUSE := 5.0 ## Anzeigedauer des Rundenergebnisses
|
||||
|
||||
var table: GameTable
|
||||
var hud: CanvasLayer
|
||||
@@ -18,11 +19,13 @@ var my_values: Array = [] ## Client-Sicht (-1 = vernebelt)
|
||||
var bid: Dictionary = {}
|
||||
var turn := 0
|
||||
var starter := 0
|
||||
var fogged: Dictionary = {} ## seat -> int index
|
||||
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 last_bids: Dictionary = {} ## seat -> letzte Ansage dieser Runde
|
||||
var log_lines: Array = []
|
||||
var phase := "lobby"
|
||||
var banner := ""
|
||||
@@ -33,6 +36,8 @@ var _stream_acc := 0.0
|
||||
var _last_phase := ""
|
||||
var deadline := 0.0 ## Sekunden-Zeitstempel, 0 = keine Frist
|
||||
var _pause_left := 0.0
|
||||
var _quitting := false ## beim Verlassen: laufende Spielschleife sauber abbrechen
|
||||
var reveal_total := 0 ## beim Aufdecken: wie viele der angesagten Augenzahl wirklich da waren
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -108,17 +113,25 @@ func _refresh_labels() -> void:
|
||||
func _host_loop() -> void:
|
||||
_log("Spiel gestartet - %d Spieler, je %d Würfel." % [seats.size(), START_DICE])
|
||||
starter = 0
|
||||
while _alive_count() > 1:
|
||||
while _alive() and _alive_count() > 1:
|
||||
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()
|
||||
if not _alive():
|
||||
return
|
||||
await _phase_reveal(result)
|
||||
phase = "over"
|
||||
var winner := ""
|
||||
for s in seats:
|
||||
if s.alive:
|
||||
winner = s.name
|
||||
banner = "%s gewinnt!" % winner
|
||||
banner = "%s gewinnt" % winner
|
||||
_log(banner)
|
||||
_push_state()
|
||||
|
||||
@@ -156,6 +169,18 @@ func _total_dice() -> int:
|
||||
return n
|
||||
|
||||
|
||||
## Solange falsch, darf die Spielschleife nicht weiterlaufen - beim Verlassen
|
||||
## ist der Knoten sonst schon aus dem Baum und jeder Zugriff schlägt fehl.
|
||||
func _alive() -> bool:
|
||||
return not _quitting and is_inside_tree()
|
||||
|
||||
|
||||
## Ein gerade getrennter Peer steht noch in `seats`, bis das Signal ankommt -
|
||||
## ein RPC dorthin wäre ein Fehler in der Konsole.
|
||||
func _peer_live(peer: int) -> bool:
|
||||
return peer > 1 and multiplayer.get_peers().has(peer)
|
||||
|
||||
|
||||
func _now() -> float:
|
||||
return Time.get_ticks_msec() / 1000.0
|
||||
|
||||
@@ -163,8 +188,10 @@ func _now() -> float:
|
||||
func _phase_roll() -> void:
|
||||
phase = "roll"
|
||||
deadline = 0.0
|
||||
reveal_total = 0
|
||||
bid = {}
|
||||
fogged.clear()
|
||||
last_bids.clear()
|
||||
peeked.clear()
|
||||
min_raise.clear()
|
||||
must_bid.clear()
|
||||
@@ -183,6 +210,8 @@ func _phase_roll() -> void:
|
||||
table.throw_dice(i, rng)
|
||||
await _await_settled(_alive_seats())
|
||||
_stream = false
|
||||
if not _alive():
|
||||
return
|
||||
|
||||
for i in _alive_seats():
|
||||
host_values[i] = []
|
||||
@@ -197,7 +226,13 @@ func _await_settled(list: Array, timeout := 7.0) -> void:
|
||||
var step := 1.0 / float(Engine.physics_ticks_per_second)
|
||||
var t := 0.0
|
||||
while t < timeout:
|
||||
if not _alive():
|
||||
return
|
||||
await get_tree().physics_frame
|
||||
if not _alive():
|
||||
return
|
||||
if get_tree().paused:
|
||||
continue
|
||||
t += step
|
||||
if t < 0.7:
|
||||
continue
|
||||
@@ -218,7 +253,7 @@ func _await_settled(list: Array, timeout := 7.0) -> void:
|
||||
func _phase_boost() -> void:
|
||||
phase = "boost"
|
||||
_answered.clear()
|
||||
var offers := {}
|
||||
offers.clear()
|
||||
for i in _alive_seats():
|
||||
offers[i] = BoosterDB.draw_offer(rng)
|
||||
if seats[i].ai:
|
||||
@@ -231,12 +266,16 @@ func _phase_boost() -> void:
|
||||
_push_state()
|
||||
|
||||
await _wait_until(_all_boosts_in, BOOST_TIMEOUT)
|
||||
if not _alive():
|
||||
return
|
||||
|
||||
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()
|
||||
if not _alive():
|
||||
return
|
||||
_snapshot_all()
|
||||
_push_state()
|
||||
|
||||
@@ -290,6 +329,8 @@ func _apply_boosters() -> void:
|
||||
_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]:
|
||||
@@ -327,7 +368,11 @@ func _apply_boosters() -> void:
|
||||
"fog":
|
||||
for o in order:
|
||||
if o != seat and host_values[o].size() > 0:
|
||||
fogged[o] = rng.randi_range(0, host_values[o].size() - 1)
|
||||
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:
|
||||
@@ -343,7 +388,7 @@ func _set_die(seat: int, idx: int, value: int) -> void:
|
||||
return
|
||||
host_values[seat][idx] = value
|
||||
var dl: Array = table.dice_of(seat)
|
||||
if idx < dl.size():
|
||||
if idx < dl.size() and dl[idx].is_inside_tree():
|
||||
dl[idx].snap_to_value(value, rng)
|
||||
|
||||
|
||||
@@ -353,14 +398,16 @@ func _throw_subset(seat: int, idxs: Array) -> void:
|
||||
for k: int in idxs:
|
||||
if k >= dl.size():
|
||||
continue
|
||||
var off := Vector3(rng.randf_range(-0.1, 0.1), 0.42, rng.randf_range(-0.22, 0.22))
|
||||
var ang := rng.randf() * TAU
|
||||
var rad := GameTable.ZONE_RAD * rng.randf_range(0.25, 0.45)
|
||||
var off := Vector3(cos(ang) * rad, 0.42, sin(ang) * rad)
|
||||
dl[k].throw_from(zone.global_transform * off, rng)
|
||||
|
||||
|
||||
func _phase_bid() -> Dictionary:
|
||||
phase = "bid"
|
||||
turn = starter if seats[starter].alive else _next_alive(starter)
|
||||
while true:
|
||||
while _alive():
|
||||
_answered.clear()
|
||||
deadline = 0.0 if seats[turn].ai else _now() + BID_TIMEOUT
|
||||
_push_state()
|
||||
@@ -392,16 +439,38 @@ func _phase_bid() -> Dictionary:
|
||||
## 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)):
|
||||
a = {"type": "challenge"} if not bid.is_empty() else {"type": "bid", "count": 1, "face": 1}
|
||||
# Wer einen Maulkorb hat, darf sich nicht über ein ungültiges Gebot in
|
||||
# die Anfechtung retten - dann wird auf das kleinste gültige korrigiert.
|
||||
var low := _lowest_legal_bid(seat)
|
||||
if not low.is_empty() and (bid.is_empty() or must_bid.get(seat, false)):
|
||||
a = {"type": "bid", "count": low.count, "face": low.face}
|
||||
elif not bid.is_empty():
|
||||
a = {"type": "challenge"}
|
||||
else:
|
||||
a = {"type": "bid", "count": 1, "face": 1}
|
||||
if a.type == "challenge":
|
||||
_log("%s fechtet an!" % seats[seat].name)
|
||||
return {"challenger": seat, "bid": bid.duplicate()}
|
||||
bid = {"seat": seat, "count": int(a.count), "face": int(a.face)}
|
||||
last_bids[seat] = {"count": bid.count, "face": bid.face}
|
||||
must_bid.erase(seat)
|
||||
_log("%s sagt %d x %d an." % [seats[seat].name, bid.count, bid.face])
|
||||
return {}
|
||||
|
||||
|
||||
## Kleinstes Gebot, das dieser Platz noch ansagen darf ({} = gar keins mehr).
|
||||
func _lowest_legal_bid(seat: int) -> Dictionary:
|
||||
if bid.is_empty():
|
||||
return {"count": 1, "face": 1}
|
||||
var mr: int = min_raise.get(seat, 1)
|
||||
if mr == 1 and int(bid.face) < 6:
|
||||
return {"count": int(bid.count), "face": int(bid.face) + 1}
|
||||
var c: int = int(bid.count) + mr
|
||||
if c <= _total_dice():
|
||||
return {"count": c, "face": 1}
|
||||
return {}
|
||||
|
||||
|
||||
func _valid_bid(seat: int, count: int, face: int) -> bool:
|
||||
if face < 1 or face > 6 or count < 1 or count > _total_dice():
|
||||
return false
|
||||
@@ -426,18 +495,26 @@ func _phase_reveal(result: Dictionary) -> void:
|
||||
if v == b.face:
|
||||
total += 1
|
||||
|
||||
reveal_total = total
|
||||
var loser := challenger if total >= b.count else int(b.seat)
|
||||
banner = "%d x %d angesagt - tatsächlich %d. %s verliert einen Würfel." % [
|
||||
b.count, b.face, total, seats[loser].name]
|
||||
_log(banner)
|
||||
_log("%d x %d angesagt - tatsächlich %d. %s verliert einen Würfel." % [
|
||||
b.count, b.face, total, seats[loser].name])
|
||||
banner = "%s verliert" % seats[loser].name
|
||||
|
||||
for i in _alive_seats():
|
||||
table.arrange_and_show(i, host_values[i], rng)
|
||||
table.set_cup_down(i, false)
|
||||
if not multiplayer.get_peers().is_empty():
|
||||
_reveal.rpc(_all_values(), banner)
|
||||
# Gezielt an die Mitspieler am Tisch: ein Peer, der nur in der Lobby sitzt,
|
||||
# hat keinen Spielknoten und würde das Paket ins Leere laufen lassen.
|
||||
var values := _all_values()
|
||||
for i in seats.size():
|
||||
var peer: int = seats[i].peer
|
||||
if _peer_live(peer):
|
||||
_reveal.rpc_id(peer, values, banner)
|
||||
_push_state()
|
||||
await _sleep(4.5)
|
||||
await _sleep(REVEAL_PAUSE)
|
||||
if not _alive():
|
||||
return
|
||||
|
||||
seats[loser].count -= 1
|
||||
if seats[loser].count <= 0:
|
||||
@@ -461,18 +538,28 @@ func _all_values() -> Array:
|
||||
return out
|
||||
|
||||
|
||||
## process_always = false: eine offene Pause hält den Timer wirklich an.
|
||||
func _sleep(sec: float) -> void:
|
||||
await get_tree().create_timer(sec).timeout
|
||||
if not _alive():
|
||||
return
|
||||
await get_tree().create_timer(sec, false).timeout
|
||||
|
||||
|
||||
func _wait_until(cond: Callable, timeout: float) -> bool:
|
||||
var t := 0.0
|
||||
while t < timeout:
|
||||
if not _alive():
|
||||
return false
|
||||
if cond.call():
|
||||
return true
|
||||
await get_tree().process_frame
|
||||
t += get_process_delta_time()
|
||||
return cond.call()
|
||||
if not _alive():
|
||||
return false
|
||||
# Im Einzelspieler hält das Pausenmenü den Baum an - dann darf auch die
|
||||
# Frist nicht weiterlaufen, sonst ist der Zug nach dem Fortsetzen weg.
|
||||
if not get_tree().paused:
|
||||
t += get_process_delta_time()
|
||||
return _alive() and cond.call()
|
||||
|
||||
|
||||
func _log(line: String) -> void:
|
||||
@@ -484,7 +571,7 @@ func _log(line: String) -> void:
|
||||
# ---------------------------------------------------------------- Sync
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not multiplayer.is_server() or not _stream:
|
||||
if not _alive() or not _stream or not multiplayer.is_server():
|
||||
return
|
||||
_stream_acc += delta
|
||||
if _stream_acc < 1.0 / 30.0:
|
||||
@@ -492,7 +579,7 @@ func _physics_process(delta: float) -> void:
|
||||
_stream_acc = 0.0
|
||||
for i in seats.size():
|
||||
var peer: int = seats[i].peer
|
||||
if peer <= 1:
|
||||
if not _peer_live(peer):
|
||||
continue
|
||||
_dice_stream.rpc_id(peer, _pack(i))
|
||||
|
||||
@@ -500,7 +587,7 @@ func _physics_process(delta: float) -> void:
|
||||
func _snapshot_all() -> void:
|
||||
for i in seats.size():
|
||||
var peer: int = seats[i].peer
|
||||
if peer > 1:
|
||||
if _peer_live(peer):
|
||||
_dice_snapshot.rpc_id(peer, _pack(i))
|
||||
|
||||
|
||||
@@ -521,7 +608,7 @@ func _unpack(data: PackedFloat32Array) -> void:
|
||||
var t := Transform3D(Basis(Quaternion(data[o + 3], data[o + 4], data[o + 5], data[o + 6])),
|
||||
Vector3(data[o], data[o + 1], data[o + 2]))
|
||||
dl[i].set_kinematic(true)
|
||||
dl[i].global_transform = t
|
||||
dl[i].place(t)
|
||||
|
||||
|
||||
@rpc("authority", "unreliable_ordered")
|
||||
@@ -536,9 +623,11 @@ func _dice_snapshot(data: PackedFloat32Array) -> void:
|
||||
|
||||
@rpc("authority", "reliable")
|
||||
func _reveal(all_values: Array, text: String) -> void:
|
||||
# Auch die eigenen Würfel in den Bogen legen - sonst liegen sie als einzige
|
||||
# noch verstreut da, wo die Physik sie fallen liess. Dafür die Werte des
|
||||
# Hosts nehmen: my_values ist hier noch vernebelt, ein vernebelter Würfel
|
||||
# würde sonst mit beliebiger Seite nach oben liegen bleiben.
|
||||
for i in seats.size():
|
||||
if i == local_seat:
|
||||
continue
|
||||
if i < all_values.size() and not all_values[i].is_empty():
|
||||
table.arrange_and_show(i, all_values[i], rng)
|
||||
table.set_cup_down(i, false)
|
||||
@@ -547,14 +636,34 @@ func _reveal(all_values: Array, text: String) -> void:
|
||||
|
||||
|
||||
func _push_state() -> void:
|
||||
if not _alive():
|
||||
return
|
||||
_refresh_labels()
|
||||
for i in seats.size():
|
||||
var peer: int = seats[i].peer
|
||||
var st := _state_for(i)
|
||||
if peer == multiplayer.get_unique_id():
|
||||
_apply_state(st)
|
||||
elif peer > 1:
|
||||
_state.rpc_id(peer, st)
|
||||
_apply_state(_state_for(i))
|
||||
elif _peer_live(peer):
|
||||
_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.
|
||||
func _round_boosters() -> Array:
|
||||
var out: Array = []
|
||||
if phase == "boost" or phase == "roll":
|
||||
return out
|
||||
for i in seats.size():
|
||||
out.append(String(chosen_boost.get(i, {}).get("id", "")))
|
||||
return out
|
||||
|
||||
|
||||
## Was jeder Platz in dieser Runde zuletzt angesagt hat.
|
||||
func _seat_bids() -> Array:
|
||||
var out: Array = []
|
||||
for i in seats.size():
|
||||
out.append(last_bids.get(i, {}))
|
||||
return out
|
||||
|
||||
|
||||
func _state_for(seat: int) -> Dictionary:
|
||||
@@ -565,18 +674,22 @@ func _state_for(seat: int) -> Dictionary:
|
||||
elif phase == "reveal":
|
||||
vis = host_values[seat].duplicate() # aufgedeckt wird ohne Nebel
|
||||
else:
|
||||
var fg: Array = fogged.get(seat, [])
|
||||
for k in host_values[seat].size():
|
||||
vis.append(-1 if fogged.get(seat, -1) == k else host_values[seat][k])
|
||||
vis.append(-1 if fg.has(k) else host_values[seat][k])
|
||||
var pub: Array = []
|
||||
for s in seats:
|
||||
pub.append({"name": s.name, "count": s.count, "alive": s.alive, "ai": s.ai})
|
||||
return {
|
||||
"phase": phase, "turn": turn, "bid": bid, "seats": pub, "values": vis,
|
||||
"log": log_lines.duplicate(), "banner": banner,
|
||||
"banner": banner,
|
||||
"peek": peeked.get(seat, {}), "min_raise": min_raise.get(seat, 1),
|
||||
"must_bid": must_bid.get(seat, false), "total": _total_dice(),
|
||||
"time_left": 0.0 if deadline <= 0.0 else maxf(0.0, deadline - _now()),
|
||||
"open": _all_values() if phase == "reveal" else [],
|
||||
"boosts": _round_boosters(),
|
||||
"actual": reveal_total if phase == "reveal" else 0,
|
||||
"bids": _seat_bids(),
|
||||
"boost": chosen_boost.get(seat, {}).get("id", ""),
|
||||
}
|
||||
|
||||
@@ -608,6 +721,13 @@ func _apply_state(st: Dictionary) -> void:
|
||||
table.set_dice_visible(local_seat, true)
|
||||
_refresh_labels()
|
||||
_apply_fog_to_table()
|
||||
var bids: Array = st.get("bids", [])
|
||||
for i in mini(seats.size(), bids.size()):
|
||||
var b: Dictionary = bids[i]
|
||||
if b.is_empty():
|
||||
table.set_bid(i, 0, 0)
|
||||
else:
|
||||
table.set_bid(i, int(b.count), int(b.face))
|
||||
hud.update_state(st, local_seat)
|
||||
|
||||
|
||||
@@ -659,7 +779,13 @@ func _on_pause_changed(on: bool) -> void:
|
||||
|
||||
|
||||
func _on_quit() -> void:
|
||||
if _quitting:
|
||||
return
|
||||
_quitting = true
|
||||
get_tree().paused = false
|
||||
# Sofort ausblenden, damit das Verlassen unmittelbar wirkt.
|
||||
table.visible = false
|
||||
hud.visible = false
|
||||
Net.leave()
|
||||
get_tree().change_scene_to_file("res://scenes/main.tscn")
|
||||
|
||||
@@ -680,6 +806,8 @@ func _req_boost(id: String, target: int) -> void:
|
||||
|
||||
|
||||
func _seat_of(peer: int) -> int:
|
||||
if peer <= 0: # 0 steht für einen KI-Platz, das ist nie ein Absender
|
||||
return -1
|
||||
for i in seats.size():
|
||||
if seats[i].peer == peer:
|
||||
return i
|
||||
@@ -697,15 +825,21 @@ func _do_bid(seat: int, count: int, face: int) -> void:
|
||||
func _do_challenge(seat: int) -> void:
|
||||
if seat < 0 or phase != "bid" or seat != turn or _answered.has(seat):
|
||||
return
|
||||
if bid.is_empty() or must_bid.get(seat, false):
|
||||
if bid.is_empty():
|
||||
return
|
||||
# Der Maulkorb sperrt die Anfechtung nur, solange es überhaupt noch ein
|
||||
# gültiges Gebot gibt - sonst bliebe der Platz ohne jede Handlung.
|
||||
if must_bid.get(seat, false) and not _lowest_legal_bid(seat).is_empty():
|
||||
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):
|
||||
if seat < 0 or phase != "boost" or chosen_boost.has(seat) or not seats[seat].alive:
|
||||
return
|
||||
if BoosterDB.get_booster(id).is_empty():
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user