Files
my-world/tools/test/race_giveup_test.py
T
aibrashandClaude Sonnet 5.5 666bebfb60 Ники, общий рейтинг (топ-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
2026-10-29 12:09:33 +05:00

98 lines
6.9 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Игровая проверка сдачи (клавиша G) и рейтинга в гонке: 2 клиента-бота на локальном сервере.
tools/test/race_giveup_test.py [рабочая_папка=/tmp/race_giveup]
Круг 1: A на воротах 1 держит G 1,0 с и отпускает (отмена), на воротах 2 сдаётся; B финиширует.
Ожидание: итоги одинаковы у обоих (B 1-й со временем, A «сдался»), в рейтинг попал только B.
Круг 2: A сдаётся на воротах 1, B на воротах 2 — все сдались, круг сразу заканчивается, в рейтинге по-прежнему только B.
Порт 9080 должен быть свободен."""
import os, re, shutil, signal, subprocess, sys, time, json
SRC = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
W = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else "/tmp/race_giveup")
XDG = os.path.join(W, "xdg")
ok = True
def check(cond, text):
global ok
print((" ДА " if cond else " НЕТ ") + text)
if not cond:
ok = False
shutil.rmtree(W, ignore_errors=True); os.makedirs(W)
subprocess.run([f"{SRC}/tools/test/make_test_server.sh", f"{W}/server"], check=True, stdout=subprocess.DEVNULL)
subprocess.run([f"{SRC}/tools/test/make_test_project.sh", f"{W}/project"], check=True, stdout=subprocess.DEVNULL)
pg = f"{W}/project/project.godot"
t = open(pg).read()
t = t.replace('Bot="*res://tools/test/bot.gd"', 'Bot="*res://tools/test/bot.gd"\nRaceBot="*res://tools/test/race_bot.gd"').replace('config/name="world2"', 'config/name="world2-racetest"')
open(pg, "w").write(t)
subprocess.run(["godot", "--headless", "--import", "--path", f"{W}/project"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
for n, nick, cid in (("A", "Aa", "a" * 32), ("B", "Bb", "b" * 32)):
open(f"{W}/prof_{n}.cfg", "w").write(f'[profile]\nclient_id="{cid}"\nnick="{nick}"\nasked=true\n')
slog = open(f"{W}/server.log", "w")
server = subprocess.Popen(["godot", "--headless", "--path", f"{W}/server"], stdout=slog, stderr=subprocess.STDOUT, env=dict(os.environ, XDG_DATA_HOME=XDG))
time.sleep(2.5)
def bot(n, *args):
return subprocess.Popen(["godot", "--headless", "--path", f"{W}/project", "res://maps/race/race.tscn", "--", "--client", "--race-log",
f"--race-best={W}/best_{n}.cfg", f"--profile={W}/prof_{n}.cfg", f"--name={n}", "--expect=2", "--rounds=2", *args],
stdout=open(f"{W}/{n}.log", "w"), stderr=subprocess.STDOUT)
pa = bot("A", "--gu1=cancel@1,giveup@2", "--gu2=giveup@1")
pb = bot("B", "--delay=2.0", "--gu2=giveup@2")
try:
pa.wait(timeout=420); pb.wait(timeout=60)
except subprocess.TimeoutExpired:
pa.kill(); pb.kill()
check(False, "боты не уложились во время")
time.sleep(1)
server.terminate(); server.wait(); slog.close()
L = {n: open(f"{W}/{n}.log", errors="replace").read().splitlines() for n in "AB"}
sv = open(f"{W}/server.log", errors="replace").read()
def tables(lines):
res, cur = [], []
for l in lines:
m = re.search(r"итог: место (\d+) игрок (-?\d+) время (-?[\d.]+) ворот (\d+) сдался=(\w+)", l)
if m:
if cur and int(m.group(1)) == 1:
res.append(cur); cur = []
cur.append((int(m.group(1)), int(m.group(2)), float(m.group(3)), int(m.group(4)), m.group(5)))
elif cur and "фаза -> WAIT" in l:
res.append(cur); cur = []
if cur: res.append(cur)
return res
ta, tb = tables(L["A"]), tables(L["B"])
print("таблицы A:", ta); print("таблицы B:", tb)
check(len(ta) == 2 and len(tb) == 2, "оба видели итоги двух кругов")
check(ta == tb, "итоги одинаковы у A и B в обоих кругах (включая признак «сдался»)")
if len(ta) == 2:
r1 = ta[0]
check(len(r1) == 2 and r1[0][2] > 0 and r1[0][4] == "false" and r1[1][4] == "true" and r1[1][2] < 0,
"круг 1: первым финишировавший B со временем, вторым сдавшийся A без времени")
check(len(set(x[1] for x in r1)) == 2 and r1[1][3] >= 2, "A сдался, пройдя %d ворот" % r1[1][3])
check(all(x[4] == "true" and x[2] < 0 for x in ta[1]) and len(ta[1]) == 2, "круг 2: оба сдались, времени ни у кого нет")
a = "\n".join(L["A"])
check("ОТМЕНА_УДЕРЖАНИЯ: отпускаю на" in a and re.search(r"шкала=(0\.\d+|1\.[0-4]\d*) сдался=false", a) is not None, "отмена: G отпущена через 1,0 с (< 1,5 с) — не сдался")
check(re.search(r"ПОСЛЕ_ОТМЕНЫ hold=0\.00 сдался=false", a) is not None, "после отмены шкала сброшена в 0")
sd = re.findall(r"СДАЛСЯ_БОТ unix=([\d.]+) на_старте=(\w+) заморожен=(\w+)", a + "\n" + "\n".join(L["B"]))
check(len(sd) >= 2 and all(x[1] == "true" and x[2] == "false" for x in sd), "после сдачи игрок на стартовой площадке и не заморожен (%d сдач; последний сдавшийся круга не успевает напечатать — круг кончается в тот же кадр)" % len(sd))
# круг 2: все сдались -> RESULTS в тот же кадр, а не через 45 с
race_unix = {n: [float(m.group(1)) for l in L[n] for m in [re.search(r"ФАЗА RACE, unix=([\d.]+)", l)] if m] for n in "AB"}
res_unix = {n: [float(m.group(1)) for l in L[n] for m in [re.search(r"ФАЗА RESULTS, unix=([\d.]+)", l)] if m] for n in "AB"}
print(" unix RACE:", race_unix, " RESULTS:", res_unix)
if len(res_unix["A"]) == 2 and len(race_unix["A"]) == 2:
d = res_unix["A"][1] - race_unix["A"][1]
check(d < 30.0, "круг 2 (все сдались) закончился через %.1f с после старта, не ждал 45 с" % d)
imm = False
for n in "AB":
for k, l in enumerate(L[n]):
if "СДАЛСЯ (время гонки" in l and any("фаза -> RESULTS" in x for x in L[n][k + 1:k + 4]):
imm = True
check(imm, "последняя сдача сразу привела к итогам (СДАЛСЯ -> фаза RESULTS подряд в логе)")
rec = re.findall(r"\[рекорд\] race (\S+) ([\d.]+)", sv)
print(" записи на сервере:", rec)
check(len(rec) == 1 and rec[0][0] == "Bb", "в рейтинг попала одна запись — B (сдавшийся A не записан)")
for n in "AB":
errs = [l for l in L[n] if "SCRIPT ERROR" in l or "Parse Error" in l]
check(not errs, "в логе %s нет ошибок скриптов %s" % (n, errs[:2]))
print("\nПРОВЕРКА ПРОЙДЕНА" if ok else "\nПРОВЕРКА ПРОВАЛЕНА")
sys.exit(0 if ok else 1)