LuneSession hält den autoritativen Matchzustand; Clients senden nur Absichten. Fremde Sitze, veraltete Befehle und Doppelkäufe sind geprüft, Disconnect führt zur Bot-Übernahme statt zum Kernverlust. Exporte und entpackbare Demos 0.16.0-m4 für Linux und Windows.
146 lines
7.3 KiB
GDScript
146 lines
7.3 KiB
GDScript
extends Node
|
|
## Network protocol tests for the authoritative LuneSession (no rendering).
|
|
## Covers foreign seats, stale commands, double purchase, bot takeover on disconnect
|
|
## and the rule that a disconnect is never a core destruction.
|
|
## Run: godot --headless --path . res://tests/luneraria/test_lune_net.tscn
|
|
var checks: int = 0
|
|
var failures: Array[String] = []
|
|
|
|
func check(ok: bool, label: String) -> void:
|
|
checks += 1
|
|
if not ok:
|
|
failures.append(label)
|
|
printerr("NET_FAIL: ", label)
|
|
|
|
func _ready() -> void:
|
|
call_deferred("run")
|
|
|
|
func new_host(seed: int = 5) -> LuneSession:
|
|
var session: LuneSession = LuneSession.new()
|
|
session.name = "LuneSession"
|
|
add_child(session)
|
|
session.host_offline(0, {"seed": seed, "human_slot": 0,
|
|
"bunds": ["glutpakt", "wurzelbund", "glutpakt", "wurzelbund", "glutpakt", "wurzelbund"]})
|
|
return session
|
|
|
|
func run() -> void:
|
|
_test_seat_ownership()
|
|
_test_stale_command()
|
|
_test_double_purchase()
|
|
_test_foreign_uid()
|
|
_test_disconnect_is_not_core_loss()
|
|
_test_six_seats_and_phases()
|
|
var report := {"checks": checks, "failures": failures}
|
|
var file := FileAccess.open("res://docs/luneraria/logs/net-tests.json", FileAccess.WRITE)
|
|
file.store_string(JSON.stringify(report, "\t"))
|
|
file.close()
|
|
print("LUNE_NET_%s: %d checks; %d failures" % ["PASS" if failures.is_empty() else "FAIL", checks, failures.size()])
|
|
get_tree().quit(0 if failures.is_empty() else 1)
|
|
|
|
func _test_seat_ownership() -> void:
|
|
var session: LuneSession = new_host()
|
|
session.seat_peer(1, 42, "Gast")
|
|
check(session.seat_of_peer(42) == 1, "peer is seated on slot 1")
|
|
# Peer 42 tries to act as slot 3.
|
|
var spoofed: Dictionary = session.host_command(42, 3, {"type": "buy", "type_id": "w01"})
|
|
check(not spoofed.ok, "command from another seat is rejected")
|
|
# Unseated peer.
|
|
var orphan: Dictionary = session.host_command(99, 0, {"type": "buy", "type_id": "w01"})
|
|
check(not orphan.ok, "command from an unseated peer is rejected")
|
|
# The owner may act during planning (bots are pre-ready, so do not ready up yet).
|
|
var own: Dictionary = session.host_command(1, 0, {"type": "buy", "type_id": session.match_state.base_ids("glutpakt")[0]})
|
|
check(own.ok, "the seat owner may buy")
|
|
session.close()
|
|
|
|
func _test_stale_command() -> void:
|
|
var session: LuneSession = new_host()
|
|
session.host_ready(0)
|
|
for slot: int in range(1, 6):
|
|
session.host_ready(slot)
|
|
check(session.match_state.phase == LuneMatch.Phase.COMBAT, "all ready starts combat")
|
|
var late: Dictionary = session.host_command(1, 0, {"type": "buy", "type_id": "w01"})
|
|
check(not late.ok and "spät" in str(late.message) or "vorbei" in str(late.message), "stale planning command after the phase closed is rejected")
|
|
session.close()
|
|
|
|
func _test_double_purchase() -> void:
|
|
var session: LuneSession = new_host()
|
|
var ids: Array[String] = session.match_state.base_ids("glutpakt")
|
|
var cost: int = int(DataCatalog.get_unit(ids[0]).cost)
|
|
session.match_state.players[0].draft.gold = 1000
|
|
var first: Dictionary = session.host_command(1, 0, {"type": "buy", "type_id": ids[0]})
|
|
var gold_after_first: int = int(session.match_state.players[0].draft.gold)
|
|
var roster_after_first: int = int(session.match_state.players[0].draft.roster.size())
|
|
check(first.ok and roster_after_first == 1 and gold_after_first == 1000 - cost, "one purchase spends the catalogue cost exactly once")
|
|
# A duplicated packet is another buy command: it may buy a second unit at a cost,
|
|
# but it must never hand out a free clone (ownership is the uid list plus gold).
|
|
session.host_command(1, 0, {"type": "buy", "type_id": ids[0]})
|
|
var roster_after: int = int(session.match_state.players[0].draft.roster.size())
|
|
var gold_after: int = int(session.match_state.players[0].draft.gold)
|
|
check(roster_after == roster_after_first + 1, "a repeated buy creates one more unit, not a free clone")
|
|
check(gold_after == gold_after_first - cost, "the repeated buy also pays exactly once")
|
|
var uids: Dictionary = {}
|
|
for entry: Dictionary in session.match_state.players[0].draft.roster:
|
|
uids[str(entry.uid)] = true
|
|
check(uids.size() == roster_after, "every owned unit has a unique id")
|
|
session.close()
|
|
|
|
func _test_foreign_uid() -> void:
|
|
var session: LuneSession = new_host()
|
|
session.seat_peer(1, 7, "Gast")
|
|
var ids: Array[String] = session.match_state.base_ids("glutpakt")
|
|
session.match_state.players[0].draft.gold = 500
|
|
session.host_command(1, 0, {"type": "buy", "type_id": ids[0]})
|
|
var uid: String = str(session.match_state.players[0].draft.roster[0].uid)
|
|
# Guest tries to upgrade the host's unit.
|
|
var steal: Dictionary = session.host_command(7, 1, {"type": "upgrade", "uid": uid})
|
|
check(not steal.ok, "a guest cannot upgrade a foreign unit id")
|
|
# Guest tries to act as the host seat.
|
|
var hijack: Dictionary = session.host_command(7, 0, {"type": "upgrade", "uid": uid})
|
|
check(not hijack.ok, "a guest cannot act on the host seat")
|
|
# Owner may upgrade their own unit.
|
|
var ids2: Array[String] = session.match_state.base_ids("glutpakt")
|
|
var upgrade_id: String = str(DataCatalog.get_unit(ids[0]).get("upgrade_id", ""))
|
|
if not upgrade_id.is_empty():
|
|
session.match_state.players[0].draft.gold = 500
|
|
var ok: Dictionary = session.host_command(1, 0, {"type": "upgrade", "uid": uid})
|
|
check(ok.ok, "the owner may upgrade their own unit")
|
|
session.close()
|
|
|
|
func _test_disconnect_is_not_core_loss() -> void:
|
|
var session: LuneSession = new_host()
|
|
session.seat_peer(1, 50, "Gast")
|
|
session.seat_peer(2, 51, "Gast2")
|
|
var core0: float = session.match_state.core_hp(0)
|
|
var core1: float = session.match_state.core_hp(1)
|
|
session._on_peer_disconnected(50)
|
|
check(session.match_state.core_hp(0) == core0 and session.match_state.core_hp(1) == core1, "disconnect does not damage either Ancient")
|
|
check(session.match_state.phase != LuneMatch.Phase.OVER, "disconnect does not end the match")
|
|
check(session.match_state.winner == LuneMatch.ONGOING, "disconnect is not a core victory")
|
|
check(not session.seat_peers.has(1), "the lost seat is no longer peer-held")
|
|
var bot_planned: bool = false
|
|
for entry: Dictionary in session.lobby:
|
|
if int(entry.slot) == 1:
|
|
bot_planned = bool(entry.bot) and bool(entry.ready)
|
|
check(bot_planned, "the disconnected seat is taken over by a bot")
|
|
session.close()
|
|
|
|
func _test_six_seats_and_phases() -> void:
|
|
var session: LuneSession = new_host()
|
|
check(session.match_state.players.size() == 6, "six seats exist")
|
|
var teams: Dictionary = {}
|
|
for player: Dictionary in session.match_state.players:
|
|
teams[player.team] = int(teams.get(player.team, 0)) + 1
|
|
check(teams.size() == 2 and teams[0] == 3 and teams[1] == 3, "two teams of three")
|
|
check(session.match_state.map.buildings.filter(func(d: Dictionary) -> bool: return d.tier != "core").size() == 16, "sixteen towers")
|
|
check(session.match_state.map.buildings.filter(func(d: Dictionary) -> bool: return d.tier == "core").size() == 2, "two Ancients")
|
|
session.host_ready(0)
|
|
for slot: int in range(1, 6):
|
|
session.host_ready(slot)
|
|
session.plan_bot_seats()
|
|
# Bots already planned via host_begin_combat; ensure a full round works offline-style.
|
|
if session.match_state.phase == LuneMatch.Phase.PLANNING:
|
|
session.host_begin_combat()
|
|
check(session.match_state.phase == LuneMatch.Phase.COMBAT, "combat starts when everyone is ready")
|
|
session.host_finish_combat()
|
|
check(session.match_state.phase == LuneMatch.Phase.PLANNING or session.match_state.phase == LuneMatch.Phase.OVER, "settlement returns to planning or ends")
|
|
session.close()
|