extends SceneTree ## Verifica il client della classifica contro un'istanza locale dell'API. ## Avviare con: tools/test_leaderboard.sh (che avvia il servizio e passa --api=...) ## ## NOTA GDScript: le lambda catturano le variabili LOCALI per valore, quindi gli ## assegnamenti fatti dentro una lambda non si vedono fuori. Qui i valori modificati ## dai segnali sono variabili di CLASSE, non locali. const Profile = preload("res://scripts/player_profile.gd") const Leaderboard = preload("res://scripts/leaderboard.gd") var world: Node3D var api := "" var failures := 0 var rows: Array = [] var received := false var info: Dictionary = {} var submitted := false var info2: Dictionary = {} var submitted2 := false var submit_events := 0 var fetch_failures := 0 var offline_reason := "" func _initialize() -> void: call_deferred("run_test") func check(label: String, condition: bool, detail: String = "") -> void: if condition: print("PASS: " + label) else: failures += 1 print("FAIL: %s %s" % [label, detail]) func run_test() -> void: for argument in OS.get_cmdline_user_args(): if argument.begins_with("--api="): api = argument.substr(6) world = load("res://scenes/main.tscn").instantiate() root.add_child(world) var client = world.leaderboard # --- indirizzo e struttura del client --- check("il client risolve l'url passato da --api", api != "" and client.base_url == api, "base=%s api=%s" % [client.base_url, api]) check("lettura e invio usano nodi HTTPRequest distinti", client._get_request != client._post_request) check("il profilo ha un player_id di 32 caratteri e un nome", str(world.profile["player_id"]).length() == 32 and str(world.profile["name"]) != "") var submission := Profile.new_submission_id() check("il submission_id ha formato uuid v4", submission.length() == 36 and submission.substr(14, 1) == "4" and submission.count("-") == 4) # --- classifica vuota (la risposta puo' essere gia' arrivata prima dei segnali) --- client.leaderboard_ready.connect(_on_ready) client.leaderboard_failed.connect(_on_failed) client.fetch() await _wait_for(func(): return received, 60) check("la classifica vuota viene letta senza errori", received and rows.is_empty()) # --- due letture ravvicinate non devono fallire --- fetch_failures = 0 client.fetch() client.fetch() await _wait_for(func(): return fetched_twice_done(), 40) check("due letture ravvicinate non generano errori di richiesta in corso", fetch_failures == 0, "errori=%d" % fetch_failures) # --- invio e posizione --- client.score_submitted.connect(_on_submitted) client.submit("Tester", 1500) await _wait_for(func(): return submitted, 60) check("l'invio restituisce posizione, id della riga e totale", submitted and int(info.get("rank", 0)) == 1 and int(info.get("total", 0)) == 1 and str(info.get("id", "")).length() == 36, str(info)) # --- secondo giocatore: la classifica si ordina --- var second = Leaderboard.new() second.player_id = "player-second-0001" root.add_child(second) second.score_submitted.connect(_on_submitted2) second.submit("Secondo", 3000) await _wait_for(func(): return submitted2, 60) check("il secondo invio finisce in testa alla classifica", int(info2.get("rank", 0)) == 1) received = false client.fetch() await _wait_for(func(): return received, 60) check("la classifica contiene due partite ordinate", rows.size() == 2 and int(rows[0]["score"]) == 3000 and int(rows[1]["score"]) == 1500, str(rows)) # --- la schermata iniziale mostra righe e riassunto personale --- var screen = world.start_screen # Il campo del nome c'e' solo se il nome non e' gia' stato scelto (profilo locale). check("la schermata iniziale mostra il nome del giocatore", screen != null and (screen.name_field != null or screen.name_locked == true), "campo=%s bloccato=%s" % [str(screen.name_field if screen else null), str(screen.name_locked if screen else null)]) screen.show_leaderboard(rows, 2, {"best": 1500, "rank": 2, "games": 1}) check("le righe finiscono nella lista della schermata", screen.rows_box.get_child_count() >= 2, "righe=%d" % screen.rows_box.get_child_count()) check("il riassunto personale e' compilato", "1500" in screen.me_label.text) # --- l'invio parte una sola volta alla morte (percorso reale: il giocatore muore) --- client.score_submitted.connect(func(_data): submit_events += 1) world.player.set_physics_process(false) world.player.take_damage(100000) # morte vera: costruisce l'overlay e invia world.player.take_damage(100000) # gia' morto: ignorato world._on_player_died() # rete di sicurezza: non deve inviare di nuovo await create_timer(1.5).timeout check("alla morte si invia una sola volta", world.score_submitted and submit_events == 1, "eventi=%d" % submit_events) check("la schermata di morte mostra l'esito dell'invio", is_instance_valid(world.player.death_note) and world.player.death_note.text != "", world.player.death_note.text) # --- servizio irraggiungibile: il gioco resta vivo --- var broken = Leaderboard.new() broken.player_id = "player-offline-0001" root.add_child(broken) # prima nella scena: _ready() risolve l'url broken.base_url = "http://127.0.0.1:1" + Leaderboard.API_PATH # poi lo si forza broken.leaderboard_failed.connect(func(reason): offline_reason = reason) broken.fetch() await _wait_for(func(): return offline_reason != "", 60) check("servizio irraggiungibile segnalato senza bloccare il gioco", offline_reason != "", offline_reason) print("%d controlli superati, %d falliti" % [14 - failures, failures]) world.queue_free() await process_frame quit(1 if failures > 0 else 0) func fetched_twice_done() -> bool: return fetch_failures == 0 and received func _wait_for(condition: Callable, steps: int) -> void: for i in steps: if condition.call(): return await create_timer(0.1).timeout func _on_ready(list: Array, _total: int, _me: Dictionary) -> void: rows = list received = true func _on_failed(_reason: String) -> void: fetch_failures += 1 func _on_submitted(data: Dictionary) -> void: info = data submitted = true func _on_submitted2(data: Dictionary) -> void: info2 = data submitted2 = true