Ники, общий рейтинг (топ-10 по картам), таблица Tab, сдача в гонке, таймер Parkour

- Ники: профиль user://profile.cfg (client_id + ник), панель при первом запуске и по N;
  Net.nick_of(id); ники над призраками, в HUD/итогах гонки и таблицах
- Рейтинг на сервере: лучшее время каждого игрока по картам race/parkour/metro, топ-10,
  user://leaderboard.json (атомарная запись, лимиты, проверка карты/комнаты/времени)
- Таблица лидеров по Tab, доски топ-3 над порталами в хабе, итоги гонки с общим рейтингом
- Гонка: сдача (G, удержание 1,5 с), RACE_GIVEUP; Parkour: таймер, финиш на вершине, R — перезапуск
- Новые RPC (submit_profile, recv_nick, submit_record, recv_record_result, request_leaderboard,
  recv_leaderboard): клиент и сервер выкладываются вместе
- Тесты: nicks_lb_test, race_giveup_test, parkour_run_test, nick_ui_test, ui_tour; README обновлён

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxZo3ekR5m1af437Qzqwc1
This commit is contained in:
2026-10-29 12:09:33 +05:00
co-authored by Claude Sonnet 5.5
parent d38152ea97
commit 666bebfb60
31 changed files with 2014 additions and 44 deletions
+111
View File
@@ -0,0 +1,111 @@
extends Node
# Клиент-сценарий для проверки ников и рейтинга (см. nicks_lb_test.py): выполняет команды из --cmds=a;b;c
# и печатает факты строками «[имя] ФАКТ …». Без игрока и карты, если не вызвана команда scene:hub.
# godot --headless --path <копия> res://tools/test/lb_client.tscn -- --client --name=A --profile=/tmp/a.cfg --cmds="nick:Алиса;map:race;sleep:2;show;quit"
# Команды: wait_connected, nick:<текст>, map:<id>, scene:hub, sleep:<с>, waitghosts:<n>, show, rec:<карта>:<время>,
# recn:<карта>:<сколько>:<время0> (подряд, время каждый раз на 1 с меньше), req:<карта>, reqn:<карта>:<сколько>,
# bad (заведомо неверные submit_record), last:<карта>, quit.
var label := "?"
var cmds: Array = []
var results := 0
var board_updates := 0
func _ready() -> void:
for a in OS.get_cmdline_user_args():
if a.begins_with("--name="): label = a.substr(7)
if a.begins_with("--cmds="): cmds = a.substr(7).split(";")
Net.record_result.connect(func(m, t, r, pb, mr): results += 1; say("ФАКТ РЕЗУЛЬТАТ %s время=%.3f место=%d личный=%s рекорд_карты=%s" % [m, t, r, pb, mr]))
Net.leaderboard_updated.connect(func(_m): board_updates += 1)
_run.call_deferred()
func say(text: String) -> void:
print("[%s] %s" % [label, text])
func _wait_until(cond: Callable, timeout := 8.0) -> bool:
var t := 0.0
while not cond.call():
await get_tree().create_timer(0.1).timeout
t += 0.1
if t > timeout:
return false
return true
func _board_text(m: String) -> String:
var b = Net.boards.get(m)
if b == null:
return "нет данных"
var parts := []
for e in b.top:
parts.append("%s:%.3f" % [e[0], e[1]])
return "n=%d место=%d моё=%.3f топ=%s" % [b.top.size(), b.my_rank, b.my_time, "|".join(parts)]
func _run() -> void:
for c in cmds:
var p: PackedStringArray = c.split(":", true, 3)
match p[0]:
"wait_connected":
say("ФАКТ ПОДКЛЮЧЁН %s" % await _wait_until(func(): return Net.connected))
"nick":
Net.set_nick(c.substr(5))
"map":
Net.enter_map(p[1])
say("ФАКТ КАРТА %s готова=%s" % [p[1], await _wait_until(func(): return Net.map_ready)])
"scene":
add_child((load("res://maps/hub/hub.tscn") as PackedScene).instantiate())
say("ФАКТ ХАБ готов=%s" % await _wait_until(func(): return Net.map_ready))
"sleep":
await get_tree().create_timer(float(p[1])).timeout
"waitghosts":
say("ФАКТ ПРИЗРАКИ %s" % await _wait_until(func(): return Net.ghosts.size() >= int(p[1]), 10.0))
await get_tree().create_timer(0.7).timeout
"show":
var gl := []
for id in Net.ghosts:
var g = Net.ghosts[id]
var l: Label3D = g.label
gl.append("%d=%s(%s)" % [id, l.text, Net.nick_of(id)])
var ps := []
for id in Net.nicks:
ps.append("%d=%s" % [id, Net.nicks[id]])
ps.sort()
gl.sort()
say("ФАКТ НИКИ свой=%s ники=%s призраки=%s" % [Net.nick_of(Net.own_id()), ",".join(ps), ",".join(gl)])
"rec":
Net.submit_my_record(p[1], float(p[2]))
await get_tree().create_timer(0.6).timeout
"recn":
for i in int(p[2]):
Net.submit_my_record(p[1], float(p[3]) - i)
await get_tree().create_timer(1.5).timeout
say("ФАКТ ОТВЕТОВ_НА_ЗАПИСИ %d" % results)
"req":
var before := board_updates
Net.request_board(p[1])
await _wait_until(func(): return board_updates > before, 3.0)
say("ФАКТ ТАБЛИЦА %s %s" % [p[1], _board_text(p[1])])
"reqn":
var before := board_updates
for i in int(p[2]):
Net.request_leaderboard.rpc_id(1, p[1])
await get_tree().create_timer(1.5).timeout
say("ФАКТ ОТВЕТОВ_НА_ЗАПРОСЫ %d" % (board_updates - before))
"bad":
var before := results
Net.submit_record.rpc_id(1, "zzz", 60.0) # нет такой карты
Net.submit_record.rpc_id(1, "race", 5.0) # быстрее допустимого
Net.submit_record.rpc_id(1, "race", 99999.0) # дольше допустимого
Net.submit_record.rpc_id(1, "race", NAN)
Net.submit_record.rpc_id(1, "race", -3.0)
Net.submit_record.rpc_id(1, "parkour", 90.0) # карта, на которой мы не находимся
Net.request_leaderboard.rpc_id(1, "zzz")
await get_tree().create_timer(1.5).timeout
say("ФАКТ НЕВЕРНЫЕ_ПРИНЯТЫ %d" % (results - before))
"last":
say("ФАКТ ПОСЛЕДНИЙ %s" % str(Net.last_record.get(p[1])))
"quit":
say("ГОТОВО")
get_tree().quit()
return
say("ГОТОВО")
get_tree().quit()