extends Node3D ## Host-autoritative Spiellogik + Client-Ansicht. const START_DICE := 5 const BID_TIMEOUT := 30.0 ## Aufdecken in drei Etappen: Becher hoch, zählen, Urteil. So baut sich die ## Spannung auf, statt dass alles auf einmal dasteht. const ZEIT_BECHER := 1.3 const ZEIT_ZAEHLEN := 2.2 ## reicht, um bis zu 15 passende Würfel aufleuchten zu lassen const ZEIT_URTEIL := 2.8 const WURF_ZEIT := 1.1 ## wie lange die geworfene Münze zu sehen ist, ## bevor ihre Wirkung einsetzt var table: GameTable var hud: CanvasLayer ## Alle Wartezeiten des Ablaufs (Bot-Pausen, Aufdecken, Münzwurf) laufen ## durch `_sleep` und lassen sich hiermit stauchen. Im Spiel 1.0; die Tests ## setzen 0, sonst dauert eine Bot-Partie Minuten statt Sekunden. Kein ## class_name dafür: den Klassencache baut nur der Editor, headless fehlt er. var tempo := 1.0 ## Würfel je Platz zu Beginn. Bonuswürfel (Nachschub, Genau) füllen nur bis ## hierhin auf - so wird der Tisch über die Partie sicher leerer. var dice_start := START_DICE ## Simulation: kein Würfelwurf, keine Physik - Werte werden direkt gezogen. ## Nur für `make sim`; fünfzig Partien in Sekunden statt Minuten. var simulation := false var runde := 0 ## laufende Rundennummer, für Anzeige und Messung var ausgeschieden: Array = [] ## Plätze in der Reihenfolge ihres Ausscheidens var cfg_start: Dictionary = {} ## Startaufstellung, fürs Nochmal var rng := RandomNumberGenerator.new() var seats: Array = [] ## [{peer, name, ai, count, alive}] var local_seat := 0 ## -1 auf dem Spielserver: der sitzt nicht mit var host_values: Array = [] ## nur Host: seat -> Array[int] var my_values: Array = [] ## Client-Sicht (-1 = vernebelt) # Rundenzustand (nur Host) var bid: Dictionary = {} var turn := 0 var starter := 0 var peeked: Dictionary = {} ## viewer -> {target_seat: Array[[idx, value]]} var gezaehlt: Dictionary = {} ## seat -> {face: [von, bis]}, durch Zählen var verlauf: Array = [] ## alle Ansagen dieser Runde, {seat, count, face, erhoeht} var must_bid: Dictionary = {} ## seat -> bool var hand: Dictionary = {} ## seat -> Array[String], Münzen beim Spieler var topf: Array = [] ## geworfene Münzen, {seat, id, stark, nr}; bleibt liegen, bis ihn einer nimmt var _muenze_laeuft := false ## eine Münze wird gerade aufgelöst var _wurf_nr := 0 ## zählt die Würfe übers Spiel, damit das HUD neue erkennt var _klang_phase := "" ## letzte Phase, für die ein Klang lief var _gezaehlt_gezeigt := false ## Hervorhebung beim Aufdecken lief schon var _topf_ziel := -1 ## wer den Topf gerade genommen hat, für den Flug der Münzen var last_bids: Dictionary = {} ## seat -> letzte Ansage dieser Runde var log_lines: Array = [] var phase := "lobby" var banner := "" var _answered: Dictionary = {} var _stream := false 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: rng.randomize() func setup(cfg: Dictionary) -> void: cfg_start = cfg.duplicate(true) DiceAI.stufe = int(cfg.get("stufe", 1)) seats.clear() if cfg.has("seats"): # Rückkehrer: die laufende Aufstellung, Platz für Platz, in der # Reihenfolge des Servers - daran hängen alle Indizes im Zustand. dice_start = int(cfg.get("dice", START_DICE)) for s in cfg.seats: seats.append({"peer": int(s.peer), "name": String(s.name), "ai": bool(s.ai), "count": int(s.count), "alive": bool(s.alive), "verwaist": false}) else: var dice := int(cfg.get("dice", START_DICE)) # Tests spielen mit weniger dice_start = dice var peers: Array = cfg.peers for p in peers: seats.append({"peer": int(p), "name": String(cfg.names[p]), "ai": false, "count": dice, "alive": true, "verwaist": false}) var bots: Array = cfg.get("bots", []) for i in int(cfg.ai): var b: Dictionary = bots[i] if i < bots.size() else {} seats.append({"peer": 0, "name": String(b.get("name", "Bot %d" % (i + 1))), "ai": true, "stufe": int(b.get("stufe", DiceAI.stufe)), "count": dice, "alive": true, "verwaist": false}) # Der Spielserver spielt nicht mit und bekommt deshalb keinen Platz. var me := multiplayer.get_unique_id() local_seat = -1 for i in seats.size(): if seats[i].peer == me: local_seat = i table = GameTable.new() add_child(table) # Der Server hat keinen Platz, legt den Tisch aber fest so, als säße Platz 0 # unten. Diese Ausrichtung ist der Bezug für alle Würfelkoordinaten. table.build(seats.size(), maxi(local_seat, 0)) # Ohne Platz am Tisch gibt es nichts anzuzeigen und nichts zu bedienen. if local_seat >= 0: hud = preload("res://scripts/hud.gd").new() add_child(hud) hud.bid_pressed.connect(_on_bid_pressed) hud.challenge_pressed.connect(_on_challenge_pressed) hud.exact_pressed.connect(_on_exact_pressed) hud.rematch_pressed.connect(_on_rematch) hud.karte_pressed.connect(_on_karte) hud.booster_chosen.connect(_on_booster_chosen) hud.quit_pressed.connect(_on_quit) hud.pause_changed.connect(_on_pause_changed) for i in seats.size(): # Die Startmünze zieht nur der Host. Beim Client kommt sie mit dem # ersten Zustand, sonst läge dort kurz eine andere. hand[i] = [BoosterDB.zieh(rng)] if multiplayer.is_server() else [] table.set_dice_count(i, seats[i].count) table.set_dice_visible(i, false) table.set_cup_down(i, i != local_seat) host_values.append([]) _refresh_labels() if multiplayer.is_server(): multiplayer.peer_disconnected.connect(_on_peer_lost) _host_loop() func _on_peer_lost(peer: int) -> void: var seat := _seat_of(peer) if seat < 0 or seats[seat].ai: return seats[seat].ai = true seats[seat].peer = 0 seats[seat].verwaist = true _log("%s hat das Spiel verlassen - übernimmt ein Bot." % seats[seat].name) _push_state() ## Nur Server: ein Rückkehrer mit diesem Namen bekommt seinen Platz zurück. ## Gibt die Aufstellung für seinen Client zurück, oder {} wenn es keinen ## verwaisten Platz mit dem Namen gibt. func wiedereinstieg(peer: int, pname: String) -> Dictionary: for i in seats.size(): var s = seats[i] if not s.verwaist or String(s.name).to_lower() != pname.to_lower(): continue s.peer = peer s.ai = false s.verwaist = false _log("%s ist zurück und übernimmt wieder." % s.name) var aufstellung: Array = [] for t in seats: aufstellung.append({"peer": t.peer, "name": t.name, "ai": t.ai, "count": t.count, "alive": t.alive}) _nachliefern(i) return {"seats": aufstellung, "stufe": DiceAI.stufe, "dice": dice_start} return {} ## Dem Rückkehrer Zustand und Würfellage schicken - kurz verzögert, damit ## sein Spielknoten steht, bevor das erste Paket dort ankommt. func _nachliefern(seat: int) -> void: await _sleep(0.6) if not _alive(): return _refresh_labels() _push_state() if _peer_live(seats[seat].peer): _dice_snapshot.rpc_id(seats[seat].peer, _pack(seat)) func _refresh_labels() -> void: for i in seats.size(): var s = seats[i] # Die Würfelzahl steht im Panel oben rechts, hier reicht der Name. # Lange Namen werden gekürzt, damit sie auf den Becher passen. var short: String = s.name if s.name.length() <= 9 else s.name.substr(0, 8) + "…" var txt: String = short if s.alive else "%s · raus" % short var col := Color(1, 1, 1) if not s.alive: col = Color(0.5, 0.5, 0.55) elif phase == "bid" and i == turn: col = Color(1.0, 0.85, 0.3) table.set_label(i, txt, col) table.set_active_seat(turn if phase == "bid" else -1) # ---------------------------------------------------------------- Host-Ablauf func _host_loop() -> void: _log("Spiel gestartet - %d Spieler, je %d Würfel." % [seats.size(), START_DICE]) starter = 0 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 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 _log(banner) Kampagne.ergebnis(seats, ausgeschieden) _push_state() func _alive_count() -> int: var n := 0 for s in seats: if s.alive: n += 1 return n func _alive_seats() -> Array: var out: Array = [] for i in seats.size(): if seats[i].alive: out.append(i) return out func _next_alive(from: int) -> int: var i := from for _n in seats.size(): i = (i + 1) % seats.size() if seats[i].alive: return i return from func _total_dice() -> int: var n := 0 for s in seats: if s.alive: n += s.count 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 func _phase_roll() -> void: phase = "roll" runde += 1 deadline = 0.0 reveal_total = 0 bid = {} last_bids.clear() peeked.clear() gezaehlt.clear() verlauf.clear() must_bid.clear() _topf_ziel = -1 banner = "" _log("--- Neue Runde ---") for i in _alive_seats(): table.set_dice_count(i, seats[i].count) table.set_dice_visible(i, i == local_seat) table.set_cup_down(i, i != local_seat) _push_state() if simulation: for i in _alive_seats(): host_values[i] = [] for _k in seats[i].count: host_values[i].append(rng.randi_range(1, 6)) table.arrange_and_show(i, host_values[i], rng) _push_state() return _stream = true for i in _alive_seats(): table.throw_dice(i, rng) await _await_settled(_alive_seats()) _stream = false if not _alive(): return for i in _alive_seats(): host_values[i] = [] for d in table.dice_of(i): host_values[i].append(d.top_value()) table.freeze_dice(i) _snapshot_all() _push_state() 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 var ok := true for s in list: # Verirrte Würfel zurückholen, sonst wartet man hier vergebens. if table.fange_verirrte(s): ok = false continue for d in table.dice_of(s): if d.freeze: continue if not d.is_settled(): ok = false elif d.flatness() < 0.86: d.nudge(rng) ok = false if ok: return # ---------------------------------------------------------------- Münzen und Aufdecken # Ausgelagert nach muenzen.gd und aufdecken.gd. Die Hüllen hier bleiben, weil # Tests und Aufrufer den Weg über das Spiel kennen. var muenzen := preload("res://scripts/muenzen.gd").new(self) var aufdecken := preload("res://scripts/aufdecken.gd").new(self) func _zieh_muenze(seat: int) -> void: muenzen._zieh_muenze(seat) func _do_play(seat: int, id: String, target: int) -> void: await muenzen._do_play(seat, id, target) func _wirkung(seat: int, id: String, target: int, stark: bool) -> void: await muenzen._wirkung(seat, id, target, stark) func _count_face(face: int) -> int: return muenzen._count_face(face) func _bot_muenze(seat: int, a: Dictionary) -> bool: return await muenzen._bot_muenze(seat, a) func _topf_an(winner: int) -> void: muenzen._topf_an(winner) func _phase_reveal(result: Dictionary) -> void: await aufdecken._phase_reveal(result) func _all_values() -> Array: return aufdecken._all_values() ## Würfel neu werfen und die Augenzahl danach übernehmen. func _neu_werfen(seat: int, idxs: Array) -> void: if simulation: for k: int in idxs: if k < host_values[seat].size(): host_values[seat][k] = rng.randi_range(1, 6) return _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: if idx < 0 or idx >= host_values[seat].size(): return host_values[seat][idx] = value var dl: Array = table.dice_of(seat) if idx < dl.size() and dl[idx].is_inside_tree(): dl[idx].snap_to_value(value, rng) func _throw_subset(seat: int, idxs: Array) -> void: var dl: Array = table.dice_of(seat) var zone: Node3D = table.zones[seat] for k: int in idxs: if k >= dl.size(): continue 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 _alive(): _answered.clear() deadline = 0.0 if seats[turn].ai else _now() + BID_TIMEOUT _push_state() if seats[turn].ai: await _sleep(1.1) # Erst planen, dann passend dazu eine Münze werfen. Die kann die # Lage ändern - also danach noch einmal entscheiden. DiceAI.stufe = int(seats[turn].get("stufe", DiceAI.stufe)) var a := DiceAI.choose_action(host_values[turn], _total_dice(), bid, must_bid.get(turn, false), rng, _bot_wissen(turn)) if await _bot_muenze(turn, a): a = DiceAI.choose_action(host_values[turn], _total_dice(), bid, must_bid.get(turn, false), rng, _bot_wissen(turn)) if not _alive(): return {} var res := _apply_action(turn, a) if not res.is_empty(): return res else: await _wait_until(func() -> bool: return _answered.has(turn) or seats[turn].ai, BID_TIMEOUT) if _answered.has(turn): var res: Dictionary = _answered[turn] if not res.is_empty(): return res else: # Zeit abgelaufen oder getrennt: automatisch entscheiden var a := DiceAI.choose_action(host_values[turn], _total_dice(), bid, must_bid.get(turn, false), rng, _bot_wissen(turn)) _log("%s wird automatisch gespielt." % seats[turn].name) var res := _apply_action(turn, a) if not res.is_empty(): return res turn = _next_alive(turn) return {} ## Nachschub gibt es nur, wer wirklich hinten liegt: höchstens die halbe ## Startzahl. Gemessen: mit jedem Rückweg dehnt sich die Partie - einzelne ## liefen über 50 Runden. func _hinten(seat: int) -> bool: return seats[seat].count * 2 <= dice_start ## Was ein Platz über die anderen weiss: aufgedeckte Würfel und Zählfenster. func _bot_wissen(seat: int) -> Dictionary: var bekannt: Array = [] for o in peeked.get(seat, {}): for paar in peeked[seat][o]: bekannt.append(int(paar[1])) return {"bekannt": bekannt, "fenster": gezaehlt.get(seat, {})} ## Gibt {} zurück wenn das Spiel weitergeht, sonst das Anfechtungs-Ergebnis. func _apply_action(seat: int, a: Dictionary) -> Dictionary: if a.type == "exact": _log("%s ruft: Genau!" % seats[seat].name) return {"challenger": seat, "bid": bid.duplicate(), "exact": true} if a.type == "bid" and not _valid_bid(seat, int(a.count), int(a.face)): # 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} verlauf.append({"seat": seat, "count": bid.count, "face": bid.face, "erhoeht": false}) 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} return DiceAI.lowest_raise(bid, _total_dice()) 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 if bid.is_empty(): return true if count > bid.count: return true return count == bid.count and face > bid.face ## process_always = false: eine offene Pause hält den Timer wirklich an. func _sleep(sec: float) -> void: if not _alive(): return await get_tree().create_timer(sec * tempo, 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 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: log_lines.append(line) if log_lines.size() > 8: log_lines.pop_front() # ---------------------------------------------------------------- Sync func _physics_process(delta: float) -> void: if not _alive() or not _stream or not multiplayer.is_server(): return _stream_acc += delta if _stream_acc < 1.0 / 30.0: return _stream_acc = 0.0 for i in seats.size(): var peer: int = seats[i].peer if not _peer_live(peer): continue _dice_stream.rpc_id(peer, _pack(i)) func _snapshot_all() -> void: for i in seats.size(): var peer: int = seats[i].peer if _peer_live(peer): _dice_snapshot.rpc_id(peer, _pack(i)) func _pack(seat: int) -> PackedFloat32Array: var out := PackedFloat32Array() for d in table.dice_of(seat): var t: Transform3D = d.global_transform var q := t.basis.get_rotation_quaternion() out.append_array([t.origin.x, t.origin.y, t.origin.z, q.x, q.y, q.z, q.w]) return out func _unpack(data: PackedFloat32Array) -> void: var dl: Array = table.dice_of(local_seat) var n := mini(dl.size(), int(data.size() / 7)) # Jeder Client dreht seinen eigenen Platz nach unten, der Server nicht. # Ohne diese Drehung landen die eigenen Würfel in der Zone des Nachbarn. var dreh := Transform3D(Basis(Vector3.UP, TAU * local_seat / float(seats.size())), Vector3.ZERO) for i in n: var o := i * 7 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].place(dreh * t) @rpc("authority", "unreliable_ordered") func _dice_stream(data: PackedFloat32Array) -> void: _unpack(data) @rpc("authority", "reliable") func _dice_snapshot(data: PackedFloat32Array) -> void: _unpack(data) @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 < 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) banner = text hud.set_banner(text) func _push_state() -> void: if not _alive(): return _refresh_labels() for i in seats.size(): var peer: int = seats[i].peer if peer == multiplayer.get_unique_id(): _apply_state(_state_for(i)) elif _peer_live(peer): _state.rpc_id(peer, _state_for(i)) ## Was liegt im Topf? Der bleibt auch übers Würfeln hinweg liegen. func _round_boosters() -> Array: return topf.duplicate(true) ## 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: var vis: Array = [] if phase == "roll": for _k in seats[seat].count: # während des Wurfs steht der Wert noch nicht fest vis.append(-1) else: vis = host_values[seat].duplicate() 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, "banner": banner, "peek": peeked.get(seat, {}), "zaehlung": gezaehlt.get(seat, {}), "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(), "verlauf": verlauf.duplicate(true), "topf_an": _topf_ziel, "voll": not _hinten(seat), "log": log_lines.duplicate(), "hand": (hand.get(seat, []) as Array).duplicate(), } @rpc("authority", "reliable") func _state(st: Dictionary) -> void: _apply_state(st) func _apply_state(st: Dictionary) -> void: phase = st.phase turn = st.turn bid = st.bid 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: seats[i].count = st.seats[i].count table.set_dice_count(i, seats[i].count) if phase != _last_phase: _last_phase = phase if phase == "roll": for i in seats.size(): if i != local_seat: table.set_dice_visible(i, false) table.set_cup_down(i, seats[i].alive) table.set_dice_visible(local_seat, true) _refresh_labels() table.set_topf(st.get("boosts", []), int(st.get("topf_an", -1))) if phase == "reveal" and int(st.get("actual", -1)) >= 0 and not _gezaehlt_gezeigt: _gezaehlt_gezeigt = true if not bid.is_empty(): table.zaehlen_zeigen(int(bid.face), st.get("open", [])) elif phase != "reveal" and _gezaehlt_gezeigt: _gezaehlt_gezeigt = false table.hervorhebung_weg() if phase != _klang_phase: _klang_phase = phase match phase: "roll": Klang.wuerfel() "reveal": Klang.becher() "over": Klang.chime() 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) # ---------------------------------------------------------------- Aktionen func _on_bid_pressed(count: int, face: int) -> void: if multiplayer.is_server(): _do_bid(local_seat, count, face) else: _req_bid.rpc_id(1, count, face) func _on_challenge_pressed() -> void: if multiplayer.is_server(): _do_challenge(local_seat) else: _req_challenge.rpc_id(1) func _on_exact_pressed() -> void: if multiplayer.is_server(): _do_exact(local_seat) else: _req_exact.rpc_id(1) func _on_booster_chosen(id: String, target: int) -> void: if multiplayer.is_server(): _do_play(local_seat, id, target) else: _req_boost.rpc_id(1, id, target) ## Im Einzelspieler hält das Pausenmenü das Spiel wirklich an. Sobald ein ## zweiter Mensch am Tisch sitzt, läuft die Runde weiter - sonst würde ein ## geöffnetes Menü alle anderen blockieren. func _on_pause_changed(on: bool) -> void: if not multiplayer.get_peers().is_empty(): return if on: _pause_left = 0.0 if deadline <= 0.0 else maxf(0.0, deadline - _now()) get_tree().paused = on if not on and _pause_left > 0.0: deadline = _now() + _pause_left _pause_left = 0.0 _push_state() ## Noch eine Partie mit denselben Bots. Solo wandert der Wunsch über Net ins ## neu geladene Menü, das dann sofort startet. Im Netz bittet der Raumbesitzer ## den Server, den Raum neu aufzusetzen. func _on_rematch() -> void: if _quitting: return if Net.room_code != Net.SOLO: Net.request_rematch() return var bots := 0 for s in seats: if s.ai: bots += 1 var kap := "" if Kampagne.kampagnenpartie and cfg_start.get("bots", []).size() > 0: kap = String(cfg_start.bots[0].name) Net.rematch = {"name": Net.player_name, "ai": bots, "stufe": int(cfg_start.get("stufe", 1)), "bots": cfg_start.get("bots", []), "kampagne": kap} _on_quit() ## Zurück auf die Seekarte: das frisch geladene Menü zeigt sie sofort. func _on_karte() -> void: if _quitting: return Kampagne.zur_karte = true _on_quit() 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") @rpc("any_peer", "reliable") func _req_bid(count: int, face: int) -> void: _do_bid(_seat_of(multiplayer.get_remote_sender_id()), count, face) @rpc("any_peer", "reliable") func _req_challenge() -> void: _do_challenge(_seat_of(multiplayer.get_remote_sender_id())) @rpc("any_peer", "reliable") func _req_exact() -> void: _do_exact(_seat_of(multiplayer.get_remote_sender_id())) @rpc("any_peer", "reliable") func _req_boost(id: String, target: int) -> void: _do_play(_seat_of(multiplayer.get_remote_sender_id()), id, target) 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 return -1 func _do_bid(seat: int, count: int, face: int) -> void: if seat < 0 or phase != "bid" or seat != turn or _answered.has(seat): return if not _valid_bid(seat, count, face): return _answered[seat] = _apply_action(seat, {"type": "bid", "count": count, "face": face}) func _do_challenge(seat: int) -> void: if seat < 0 or phase != "bid" or seat != turn or _answered.has(seat): return 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"}) ## "Genau" unterliegt denselben Regeln wie die Anfechtung: nur am Zug, nur ## mit Ansage, und der Maulkorb sperrt es genauso. func _do_exact(seat: int) -> void: if seat < 0 or phase != "bid" or seat != turn or _answered.has(seat): return if bid.is_empty(): return if must_bid.get(seat, false) and not _lowest_legal_bid(seat).is_empty(): return _answered[seat] = _apply_action(seat, {"type": "exact"})