class_name Net extends Node ## LAN-Mehrspieler: der Host führt das Spiel, Mitspieler senden Aktionen und erhalten ## Zustandsbilder aus ihrer Sicht (fremde Hände bleiben verborgen). signal lobby_changed signal started(seat: int) signal snapshot_received(snap: Dictionary) signal action_applied signal ended(reason: String) signal migrated # nach einem Host-Ausfall: dieser Rechner führt das Spiel jetzt signal status_changed const PORT := 24455 var peer: ENetMultiplayerPeer var is_host := false var connected := false var my_name := "Spieler" var my_pick := {"looks": {}} # eigene Kartenoptik (Seltenheit), die der Gegner sieht var names := {} # peer_id -> Name (nur im Host vollständig, sonst Kopie) var picks := {} # peer_id -> {"looks": {...}} (Host) var seats := {} # peer_id -> Spielerindex (Host) var my_seat := -1 var game: Game # Host var status := "" var waiting := {} # Name -> {"seat": int, "since": float}: Platz nach Verbindungsabbruch freihalten var rejoin_seconds := 60.0 var spectator := false # eigene Rolle: nur zuschauen var spectators := {} # Host: peer_id -> Name var successor_ip := "" # Mitspieler: Adresse des Nachfolge-Hosts var i_am_successor := false var backup := {} # Nachfolger: voller Spielstand nach jeder Aktion var backup_names := {} # Nachfolger: Platz -> Name aller Menschen var last_winner := "" # Mitspieler: Sieger laut letztem Zustandsbild var reconnecting := false func _ready() -> void: multiplayer.peer_connected.connect(_on_peer_connected) multiplayer.peer_disconnected.connect(_on_peer_disconnected) multiplayer.connected_to_server.connect(_on_connected) multiplayer.connection_failed.connect(func(): if not reconnecting: _fail("Verbindung fehlgeschlagen")) multiplayer.server_disconnected.connect(_on_server_lost) func host(port: int = PORT) -> Error: peer = ENetMultiplayerPeer.new() var err := peer.create_server(port, 8) # ein Mitspieler, der Rest Zuschauer if err != OK: status = "Kann nicht hosten (Port %d belegt?)" % port return err multiplayer.multiplayer_peer = peer is_host = true connected = true names = {1: my_name} picks = {1: my_pick} spectators = {} status = "Warte auf Mitspieler …" lobby_changed.emit() return OK func join(ip: String, port: int = PORT) -> Error: peer = ENetMultiplayerPeer.new() var err := peer.create_client(ip, port) if err != OK: status = "Adresse ungültig" return err multiplayer.multiplayer_peer = peer is_host = false status = "Verbinde mit %s …" % ip return OK func leave() -> void: if multiplayer.multiplayer_peer != null: multiplayer.multiplayer_peer.close() multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new() peer = null is_host = false connected = false names = {} picks = {} seats = {} waiting = {} my_seat = -1 game = null status = "" spectator = false spectators = {} successor_ip = "" i_am_successor = false backup = {} backup_names = {} last_winner = "" func local_addresses() -> Array: return Array(IP.get_local_addresses()).filter(func(a): return "." in a and not a.begins_with("127.")) func _fail(reason: String) -> void: status = reason ended.emit(reason) leave() func _on_connected() -> void: connected = true status = "Verbunden" register.rpc_id(1, my_name, my_pick, spectator) # --- Host-Wechsel --------------------------------------------------------------- # Der Host schickt dem Mitspieler mit der kleinsten Kennung nach jeder Aktion den vollen # Spielstand und allen die Adresse dieses Nachfolgers. Fällt der Host aus, eröffnet der # Nachfolger das Spiel neu, die anderen verbinden sich mit ihm und kehren auf ihre Plätze zurück. func _successor_id() -> int: var ids: Array = seats.keys().filter(func(id): return id != 1) ids.sort() return ids[0] if not ids.is_empty() else -1 func _update_successor() -> void: if not is_host or game == null: return var succ := _successor_id() var ip := "" if succ > 0 and peer != null and peer.get_peer(succ) != null: ip = peer.get_peer(succ).get_remote_address() for id in seats: if id != 1: set_successor.rpc_id(id, ip, id == succ) ## Platz -> Name aller Menschen (auch der gerade fehlenden), für die Rückkehr nach dem Wechsel. func _seat_names() -> Dictionary: var out := {} for id in seats: out[seats[id]] = names.get(id, "") for pname in waiting: out[waiting[pname].seat] = pname return out @rpc("authority", "reliable") func set_successor(ip: String, me: bool) -> void: successor_ip = ip i_am_successor = me @rpc("authority", "reliable") func receive_backup(snap: Dictionary, seat_names: Dictionary) -> void: backup = snap backup_names = seat_names func _on_server_lost() -> void: if is_host: return if last_winner != "": _fail("Host hat das Spiel beendet") elif i_am_successor and not backup.is_empty(): _become_host() elif successor_ip != "": status = "Host weg, verbinde mit dem Nachfolger …" status_changed.emit() _reconnect(successor_ip) else: _fail("Host hat das Spiel beendet") func _become_host() -> void: var snap := backup var seat_names := backup_names var seat := my_seat if multiplayer.multiplayer_peer != null: multiplayer.multiplayer_peer.close() var err := FAILED for attempt in 5: err = host() if err == OK: break await get_tree().create_timer(1.0).timeout if err != OK: _fail("Host weg, Übernahme fehlgeschlagen") return game = Game.from_snapshot(snap) seats = {1: seat} my_seat = seat waiting = {} for s in seat_names: if int(s) != seat: waiting[seat_names[s]] = {"seat": int(s), "since": Time.get_unix_time_from_system()} game.say("%s übernimmt als Host." % my_name) status = "Du bist jetzt Host, die anderen verbinden neu …" successor_ip = "" i_am_successor = false backup = {} migrated.emit() func _reconnect(ip: String) -> void: reconnecting = true await get_tree().create_timer(2.0).timeout for attempt in 5: if join(ip) == OK: var t := 0.0 while not connected and t < 4.0 and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTING: await get_tree().process_frame t += get_process_delta_time() if connected: reconnecting = false return await get_tree().create_timer(1.0).timeout reconnecting = false _fail("Nachfolge-Host nicht erreichbar") func _on_peer_connected(_id: int) -> void: pass func _on_peer_disconnected(id: int) -> void: if not is_host: return var pname: String = names.get(id, "") names.erase(id) picks.erase(id) spectators.erase(id) if game != null and seats.has(id): var p: Game.Player = game.players[seats[id]] if game.winner == "": waiting[pname] = {"seat": seats[id], "since": Time.get_unix_time_from_system()} game.say("%s hat die Verbindung verloren, der Platz bleibt %d s frei." % [p.name, int(rejoin_seconds)]) seats.erase(id) _update_successor() action_applied.emit() lobby_update.rpc(names, spectators) lobby_changed.emit() ## Host, je Bild: wartende Spieler überspringen; nach Ablauf der Frist scheiden sie aus. func tick() -> void: if not is_host or game == null or game.winner != "": return var now := Time.get_unix_time_from_system() for pname in waiting.keys(): var w: Dictionary = waiting[pname] var p: Game.Player = game.players[w.seat] if now - float(w.since) > rejoin_seconds: waiting.erase(pname) game.say("%s kehrt nicht zurück und scheidet aus." % p.name) game.damage(p, 999) game.cleanup() game.check_end() if game.winner == "" and game.current() == p: game.end_turn() # sonst bliebe das Spiel beim Ausgeschiedenen hängen action_applied.emit() elif game.current() == p: game.say("%s ist nicht da, der Zug wird übersprungen." % p.name) game.end_turn() action_applied.emit() @rpc("any_peer", "reliable") func register(name: String, pick: Dictionary, as_spectator: bool = false) -> void: if not is_host: return var id := multiplayer.get_remote_sender_id() if as_spectator: spectators[id] = name lobby_update.rpc(names, spectators) lobby_changed.emit() if game != null: start_game.rpc_id(id, -1) receive_snapshot.rpc_id(id, game.snapshot(-2)) return if game != null: # Laufendes Spiel: nur Rückkehr auf einen freigehaltenen Platz if waiting.has(name): seats[id] = waiting[name].seat names[id] = name waiting.erase(name) game.say("%s ist zurück." % name) start_game.rpc_id(id, seats[id]) receive_snapshot.rpc_id(id, game.snapshot(seats[id])) _update_successor() action_applied.emit() else: status = "%s wollte beitreten, das Spiel läuft schon" % name return names[id] = name picks[id] = pick lobby_update.rpc(names, spectators) lobby_changed.emit() @rpc("authority", "reliable") func lobby_update(n: Dictionary, sp: Dictionary = {}) -> void: names = n spectators = sp lobby_changed.emit() ## Host: Duell mit dem ersten Mitspieler starten; allein gegen die KI. func start_battle() -> void: var ids: Array = names.keys() ids.sort() ids = ids.slice(0, 2) var r := Game.make_rng() var players := [] seats = {} for id in ids: var pick: Dictionary = picks.get(id, {}) var p := Game.Player.new(names[id], true, Game.START_LIFE) for cid in pick.get("looks", {}): if Cards.has_id(cid): p.looks[cid] = int(pick.looks[cid]) players.append(p) if players.size() < 2: players.append(DeckGen.random_opponent()) game = Game.new(players[0], players[1], r) game.start() for id in ids: seats[id] = game.players.find(players[ids.find(id)]) my_seat = seats[1] for id in ids: if id != 1: start_game.rpc_id(id, seats[id]) for id in spectators: start_game.rpc_id(id, -1) _update_successor() started.emit(my_seat) @rpc("authority", "reliable") func start_game(seat: int) -> void: my_seat = seat started.emit(seat) ## Host: Zustand an alle senden (Mitspieler aus ihrer Sicht, Zuschauer ohne Verborgenes, ## der Nachfolger zusätzlich den vollen Spielstand). func broadcast() -> void: if not is_host or game == null: return for id in seats: if id != 1: receive_snapshot.rpc_id(id, game.snapshot(seats[id])) for id in spectators: receive_snapshot.rpc_id(id, game.snapshot(-2)) var succ := _successor_id() if succ > 0: receive_backup.rpc_id(succ, game.snapshot(-1), _seat_names()) @rpc("authority", "reliable") func receive_snapshot(snap: Dictionary) -> void: last_winner = str(snap.get("winner", "")) snapshot_received.emit(snap) ## Mitspieler: Aktion an den Host schicken. func send_action(action: Dictionary) -> void: if is_host: _apply(1, action) else: request_action.rpc_id(1, action) @rpc("any_peer", "reliable") func request_action(action: Dictionary) -> void: if not is_host: return _apply(multiplayer.get_remote_sender_id(), action) func _apply(id: int, action: Dictionary) -> void: if game == null or not seats.has(id): return var p: Game.Player = game.players[seats[id]] if game.winner != "": return if game.current() != p: return match str(action.get("type", "")): "play": var idx := int(action.get("hand", -1)) if idx < 0 or idx >= p.hand.size(): return game.play_card(p, p.hand[idx], bool(action.get("face_down", false))) "reveal": var cr := game.find_creature(int(action.get("uid", -1))) if cr != null: game.reveal(p, cr) "attack": var cr := game.find_creature(int(action.get("uid", -1))) var t = resolve_ref(action.get("target")) if cr != null and t != null: game.attack(cr, t) "draw": game.draw(p) "fuse": # zwei Wesen (uid, other) oder zwei Handkarten (hand, other_hand) var a = game.find_creature(int(action.get("uid", -1))) var b = game.find_creature(int(action.get("other", -1))) var i := int(action.get("hand", -1)) var j := int(action.get("other_hand", -1)) if a == null and i >= 0 and j >= 0 and i < p.hand.size() and j < p.hand.size(): a = p.hand[i] b = p.hand[j] if a != null and b != null: game.fuse(p, a, b) "end": game.end_turn() _: return action_applied.emit() func resolve_ref(ref): if ref == null or not (ref is Dictionary): return null if ref.get("kind") == "creature": return game.find_creature(int(ref.get("uid", -1))) var i := int(ref.get("index", -1)) if i >= 0 and i < game.players.size(): return game.players[i] return null static func ref_dict(target) -> Dictionary: if target is Game.Creature: return {"kind": "creature", "uid": target.uid} return {"kind": "player", "index": target.order}