Files
Grovewright/game/tests/test_session_link.gd
Steven Wroblewski be22e9f358 Abschlusskorrekturen M1c: Schläge der Gäste, Figuren und Fragebogen
- Der Host hält einen Schlag eines Gastes, der während der Abklingzeit
  ankommt, bis zum ersten Takt danach. Übers Internet ging sonst etwa
  jeder fünfte Schlag eines Gastes verloren.
- ENet drosselt die Figuren nie mehr; sonst konnten die Figuren eines
  von zwei Gästen dauerhaft stehen bleiben. Figuren gehen geordnet, und
  eine stille Verbindung gilt nach höchstens acht Sekunden als verloren.
- Positionen, die keine Zahlen sind, und Figuren von Spielern, die
  nicht online sind, werden verworfen.
- Ein Host mit angehaltener Simulation sagt das in der Statuszeile.
- Neue Tests: Schlag während der Abklingzeit, Zielauswahl von NetLink,
  verworfene Figuren, Neuabgleich über die Brücke.
- Der Fragebogen sagt, was jeder Rechner braucht, und die Freigabe in
  der Firewall gehört zum Host. CLAUDE.md beschreibt das Halten der
  Schläge, die Drosselung und die Zeitüberschreitung und ergänzt die
  Vertrauensregel um die Schlüssel.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-10-02 01:22:36 +02:00

159 lines
6.2 KiB
GDScript

extends TestCase
## A host and guests in one process, each with its own GwSession and
## NetLink, connected over the loopback interface.
var host_key := PackedByteArray([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
var guest_key := PackedByteArray([2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2])
var host: SessionLink
## Notices per SessionLink, collected while frames pass.
var notices: Dictionary[SessionLink, Array] = {}
func _start_host() -> void:
var session := GwSession.new()
session.start_new(7)
session.join(host_key, "Host")
session.step()
var link := NetLink.new()
check_eq(link.host(0, "127.0.0.1"), OK, "the host listens")
host = SessionLink.new(session, link)
notices[host] = []
func _start_guest(key: PackedByteArray, name: String) -> SessionLink:
var session := GwSession.new()
session.start_guest(key, name)
var link := NetLink.new()
link.join("127.0.0.1", host.link.local_port())
var guest := SessionLink.new(session, link)
notices[guest] = []
return guest
## Lets `frames` frames pass. The host runs a tick per frame if `ticking`;
## every guest runs what arrived.
func _play(guests: Array[SessionLink], frames: int, ticking := true) -> void:
for frame in frames:
notices[host].append_array(host.pump(1.0 / 60.0))
if ticking:
host.session.step()
host.flush()
for guest in guests:
notices[guest].append_array(guest.pump(1.0 / 60.0))
for i in guest.session.pending_ticks():
guest.session.step()
await tree.process_frame
func _types(link: SessionLink) -> Array:
return notices[link].map(func(notice: Dictionary) -> String: return notice["type"])
func _first(link: SessionLink, type: String) -> Dictionary:
for notice: Dictionary in notices[link]:
if notice["type"] == type:
return notice
return {}
func _close(links: Array[SessionLink]) -> void:
for link in links:
link.link.close()
func test_a_guest_joins_over_the_network_and_follows_the_host() -> void:
_start_host()
var guest := _start_guest(guest_key, "Grace")
await _play([guest], 40)
await _play([guest], 10, false)
check(_types(host).has("guest_arrived"), "the host sees the guest arrive")
check_eq(_first(guest, "world_received").get("resync"), false, "the guest got the host's world")
check_eq(guest.session.local_player(), 1, "the guest is player 1")
check_eq(guest.session.player_name(0), "Host", "the guest knows the host's name")
check_eq(guest.session.tick(), host.session.tick(), "the guest is at the host's tick")
check_eq(guest.session.state_hash_hex(), host.session.state_hash_hex(), "both worlds are the same")
check_eq(guest.session.save().size(), 0, "a guest does not save the host's world")
_close([host, guest])
func test_a_hit_by_the_guest_reaches_the_host() -> void:
_start_host()
var guest := _start_guest(guest_key, "Grace")
await _play([guest], 30)
var rock := host.session.spawn_point() + Vector2i(7, -2)
guest.session.dig(rock.x, rock.y)
await _play([guest], 10)
await _play([guest], 10, false)
var index := rock.y * host.session.map_width() + rock.x
check_eq(host.session.damage_codes()[index], 1, "the host's rock took the hit")
check_eq(guest.session.state_hash_hex(), host.session.state_hash_hex(), "both worlds are the same")
_close([host, guest])
func test_figures_travel_to_the_other_machines() -> void:
_start_host()
var guest := _start_guest(guest_key, "Grace")
var other := _start_guest(PackedByteArray([3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]), "Linus")
await _play([guest, other], 30)
host.send_figure(Vector2(10.0, 20.0))
guest.send_figure(Vector2(5.5, 6.5))
await _play([guest, other], 10)
check_eq(_first(host, "figure"), {"type": "figure", "player": 1, "position": Vector2(5.5, 6.5)}, "the host sees the guest")
var seen := notices[other].filter(func(notice: Dictionary) -> bool: return notice["type"] == "figure")
check(seen.has({"type": "figure", "player": 0, "position": Vector2(10.0, 20.0)}), "the other guest sees the host")
check(seen.has({"type": "figure", "player": 1, "position": Vector2(5.5, 6.5)}), "and the first guest, passed on by the host")
check(not notices[guest].any(func(notice: Dictionary) -> bool: return notice["type"] == "figure" and notice["player"] == 1), "nobody gets their own figure back")
_close([host, guest, other])
func test_figures_that_cannot_be_shown_are_dropped() -> void:
_start_host()
var guest := _start_guest(guest_key, "Grace")
var other := _start_guest(PackedByteArray([3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]), "Linus")
await _play([guest, other], 30)
# A position that is no number, as a broken or changed client may send.
guest.link.send_figure(GwSession.HOST_PEER, guest._position_bytes(Vector2(NAN, 1.0)))
# A position of a player who is not online, as it can arrive late.
host.link.send_figure(0, host._figure_bytes(5, Vector2(1.0, 2.0)))
await _play([guest, other], 10)
check(not _types(host).has("figure"), "the host drops the position that is no number")
check(not _types(other).has("figure"), "passes nothing on, and nobody shows the player who is not online")
check(not _types(guest).has("figure"), "not even the guest that sent it")
_close([host, guest, other])
func test_a_guest_with_the_key_of_a_player_in_the_world_is_turned_away() -> void:
_start_host()
var twin := _start_guest(host_key, "Host")
await _play([twin], 30)
check_eq(_first(twin, "refused").get("reason"), GwSession.REFUSED_ALREADY_PLAYING, "the reason is given")
check(_types(twin).has("host_lost"), "then the host closes the connection")
check(not twin.session.is_running(), "the guest never got a world")
_close([host, twin])
func test_a_guest_that_quits_leaves_the_world() -> void:
_start_host()
var guest := _start_guest(guest_key, "Grace")
await _play([guest], 30)
check_eq(host.session.online_players(), PackedInt64Array([0, 1]), "two players online")
guest.link.close()
await _play([], 10)
check(_types(host).has("guest_left"), "the host sees the guest leave")
check_eq(host.session.online_players(), PackedInt64Array([0]), "only the host is online")
_close([host])
func test_a_guest_learns_when_the_host_is_gone() -> void:
_start_host()
var guest := _start_guest(guest_key, "Grace")
await _play([guest], 30)
host.link.close()
await _play([guest], 10, false)
check(_types(guest).has("host_lost"), "the guest notices")
_close([guest])