Classifica globale: API dei punteggi, schermata iniziale e nome memorizzato
- services/scores-api: microservizio (solo stdlib) con SQLite WAL, idempotenza per submission_id, limiti di frequenza e validazione; immagine Docker non-root. - nginx: reverse proxy su /api/zombie-fps/ (stessa origine del gioco, niente CORS). - scripts/leaderboard.gd: client HTTP con retry idempotente; su desktop la classifica resta disattivata senza --api=, cosi' i test non sporcano la classifica pubblica. - scripts/player_profile.gd: nome, id anonimo e miglior punteggio in user:// e in window.localStorage; il nome scelto non viene piu' richiesto; riparazione dei profili con la stringa "null". - scripts/start_screen.gd: menu con nome, GIOCA e classifica; la partita parte solo dal pulsante (via la rete di sicurezza sull'input, Invio nel campo non avvia). - scripts/main.gd, player.gd: avvio esplicito, segnale died, nota dell'invio. - tools: test_scores_api.py (42), test_leaderboard (14), test_player_profile (21), test_start_screen (10); suite esistenti adeguate all'avvio manuale. - docs/HIGHSCORES.md + docs/WEB-DEPLOY.md aggiornati.
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
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
|
||||
Reference in New Issue
Block a user