Files
my-world/tools/test/race_bot.gd
T
aibrashandClaude Sonnet 5.5 9130202a51 Карта Race: гонка на ногах по киберпанк-антиутопии, 8 игроков
- maps/race: трасса 633 м из Blender (три развилки: срез по контейнерам, каньон со стенами
  против нижней дороги, труба с лазерами против коридора), запечённый свет, небо, дождь, гул;
  фоновая песня (Ogg Vorbis, ~0,6 МБ) включается на «СТАРТ!»
- Правила (race_rules.gd): R — готов; когда готовы все — отсчёт 6 с, заморозка в слотах,
  ворота строго по порядку, сплиты, место, итоги, личный рекорд (user://race_best.cfg)
- Сеть: Net.submit_race / recv_race / race_event (RACE_READY, START, CP, FINISH, STATE).
  ВНИМАНИЕ: набор RPC изменён — клиент и сервер выкладываются вместе, старый клиент
  с новым сервером не работает (rpc node checksum failed)
- player.gd: input_frozen и place() для режимов карт; project.godot: действие ready (R)
- Портал RACE в хабе, пресет экспорта Race (include_filter для ресурсов, грузимых строкой)
- tools/test: двухклиентный тест гонки на локальном сервере, бот, облёт, проверка пакета
- README: правила гонки, события, проверки, заметки

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxZo3ekR5m1af437Qzqwc1
2026-10-28 17:28:05 +05:00

225 lines
9.1 KiB
GDScript
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.
extends Node
# Бот для проверки режима «Гонка» (автозагрузка RaceBot во ВРЕМЕННОЙ копии проекта, см. tools/test/race_game_test.sh).
# Сам «жмёт R» (действие ready через Input.parse_input_event), на отсчёте пытается идти (проверка заморозки),
# в гонке бежит безопасный маршрут race_route.json (Blender-координаты; идея — бот-бегун из Blender models/race_01/tests/runner.gd).
# Аргументы (после --): --name=A --delay=<с задержки старта бега> --expect=<сколько игроков ждать перед «R»>
# --rounds=<сколько кругов пройти> --quit-race-time=<выйти из игры на этой секунде гонки>
# --die-at-gate=<n>: после ворот n умереть (проверка возрождения лицом по трассе)
# --fall-at-gate=<n>: после ворот n упасть в пустоту (проверка срыва ниже ворот)
# Факты печатаются строками «[имя] …»; сверяет их tools/test/race_game_test.sh.
const Data = preload("res://maps/race/race_data.gd")
var label := "?"
var delay := 0.0
var expect := 2
var rounds := 1
var quit_race_time := -1.0
var die_gate := -1
var fall_gate := -1
var route: Array = []
var wi := 1
var jumped_at := -100
var fr := 0
var rules
var p
var last_phase := -1
var rounds_done := 0
var last_press := -10.0
var t := 0.0
var freeze_start := Vector3.ZERO
var special_done := false
var special_wait := 0
var special_expect := Vector3.ZERO
var stuck_fr := 0
var spectator_logged := false
var music_logged := false
var music_cd_logged := false
var freeze_logged := false
func _ready() -> void:
for a in OS.get_cmdline_user_args():
if a.begins_with("--name="): label = a.substr(7)
if a.begins_with("--delay="): delay = float(a.substr(8))
if a.begins_with("--expect="): expect = int(a.substr(9))
if a.begins_with("--rounds="): rounds = int(a.substr(9))
if a.begins_with("--quit-race-time="): quit_race_time = float(a.substr(17))
if a.begins_with("--die-at-gate="): die_gate = int(a.substr(14))
if a.begins_with("--fall-at-gate="): fall_gate = int(a.substr(15))
route = JSON.parse_string(FileAccess.get_file_as_string("res://tools/test/race_route.json")).safe
func gp(w: Array) -> Vector3:
return Vector3(w[0], w[2], -w[1])
func key(a: String, down: bool) -> void:
if down: Input.action_press(a)
else: Input.action_release(a)
func ray(from: Vector3, to: Vector3) -> Dictionary:
var q := PhysicsRayQueryParameters3D.create(from, to)
q.exclude = [p.get_rid()]
return p.get_world_3d().direct_space_state.intersect_ray(q)
func say(text: String) -> void:
print("[%s] %s" % [label, text])
func _physics_process(_delta: float) -> void:
var sc := get_tree().current_scene
if sc == null or sc.name != "Race":
return
rules = sc.get_node_or_null("Rules")
p = get_tree().get_first_node_in_group("local_player")
if rules == null or p == null:
return
fr += 1
t += _delta
var ph: int = rules.phase
if ph != last_phase:
say("ФАЗА %s, unix=%.3f, место_в_слоте=(%.1f, %.1f, %.1f)" % [rules.Phase.keys()[ph], Time.get_unix_time_from_system(), p.global_position.x, p.global_position.y, p.global_position.z])
if ph == rules.Phase.COUNTDOWN:
freeze_start = p.global_position
if ph == rules.Phase.WAIT and last_phase == rules.Phase.RESULTS:
rounds_done += 1
say("КРУГОВ_ЗАВЕРШЕНО %d" % rounds_done)
if rounds_done >= rounds:
say("ГОТОВО, выхожу")
get_tree().quit()
if ph == rules.Phase.RACE:
wi = 1
special_done = false
music_logged = false
music_cd_logged = false
freeze_logged = false
say("УЧАСТНИК=%s" % rules.participants.has(rules.my_id))
last_phase = ph
match ph:
rules.Phase.WAIT:
_wait()
rules.Phase.COUNTDOWN:
# пытаемся бежать: заморозка не должна пускать
key("move_forward", true)
key("sprint", true)
key("jump", true)
if (Time.get_ticks_msec() + 3000) >= rules.start_tick and not music_cd_logged:
music_cd_logged = true
say("МУЗЫКА на отсчёте: %s" % _music_playing())
if (Time.get_ticks_msec() + 300) >= rules.start_tick and not freeze_logged:
freeze_logged = true
say("ЗАМОРОЗКА: сдвиг за отсчёт %.3f м (по горизонтали)" % Vector2(p.global_position.x - freeze_start.x, p.global_position.z - freeze_start.z).length())
rules.Phase.RACE:
if not music_logged and rules.race_time() > 1.0:
music_logged = true
say("МУЗЫКА после старта: играет=%s" % _music_playing())
_race()
_:
for k in ["move_forward", "sprint", "jump"]:
key(k, false)
func _wait() -> void:
for k in ["move_forward", "sprint", "jump"]:
key(k, false)
if rules.waiting_for_race():
if not spectator_logged:
spectator_logged = true
say("ГОНКА_ИДЁТ: жду следующий круг (участник=%s)" % rules.participants.has(rules.my_id))
return
if rules.flags_ready.get(rules.my_id, false):
return
if Net.ghosts.size() < expect - 1 or t - last_press < 1.5:
return
last_press = t
spectator_logged = false
var ev := InputEventAction.new()
ev.action = "ready"
ev.pressed = true
Input.parse_input_event(ev)
say("жму R, видно игроков: %d" % (Net.ghosts.size() + 1))
func _race() -> void:
if not rules.participants.has(rules.my_id):
return # бегаем свободно без зачёта — бот просто стоит
if quit_race_time >= 0.0 and rules.race_time() >= quit_race_time:
say("ВЫХОЖУ из игры в разгар гонки (время %.1f)" % rules.race_time())
get_tree().quit()
return
if rules.finish.has(rules.my_id):
for k in ["move_forward", "sprint", "jump"]:
key(k, false)
return
if rules.race_time() < delay:
return
# --- проверки возрождения ---
var g: int = rules.passed.get(rules.my_id, 0)
if not special_done and die_gate >= 0 and g == die_gate and not p.is_dead:
special_done = true
special_wait = 1
say("УМИРАЮ после ворот %d" % g)
p.die()
elif not special_done and fall_gate >= 0 and g == fall_gate and not p.is_dead:
special_done = true
special_wait = 1
say("ПАДАЮ в пустоту после ворот %d" % g)
p.global_position.y -= 45.0
p.velocity = Vector3.ZERO
if special_wait > 0:
if p.is_dead:
special_wait = 2
elif special_wait == 2:
special_wait = 0
var f: Vector3 = -p.global_transform.basis.z
var ex: float = rules.map.gate_yaw(g - 1)
var want := Vector3(-sin(ex), 0, -cos(ex))
say("ВОЗРОЖДЕНИЕ у ворот %d: взгляд (%.2f, %.2f), ожидался (%.2f, %.2f), позиция (%.1f, %.1f, %.1f), ворота (%.1f, %.1f, %.1f) -> %s" % [
g, f.x, f.z, want.x, want.z, p.global_position.x, p.global_position.y, p.global_position.z,
rules.map.gate_pos(g - 1).x, rules.map.gate_pos(g - 1).y, rules.map.gate_pos(g - 1).z,
"ВЕРНО" if Vector2(f.x, f.z).distance_to(Vector2(want.x, want.z)) < 0.05 and p.global_position.distance_to(rules.map.gate_pos(g - 1)) < 1.0 else "ОШИБКА"])
return
_drive()
func _drive() -> void:
var pos: Vector3 = p.global_position
if fr % 120 == 0 and OS.get_cmdline_user_args().has("--bot-log"):
say("бег: wi=%d поз=(%.1f, %.1f, %.1f) v=(%.1f, %.1f) пол=%s" % [wi, pos.x, pos.y, pos.z, p.velocity.x, p.velocity.z, p.is_on_floor()])
var tgt := gp(route[wi])
var to := Vector2(tgt.x - pos.x, tgt.z - pos.z)
var seg_a := gp(route[wi - 1])
var seg := Vector2(tgt.x - seg_a.x, tgt.z - seg_a.z)
var reached := to.length() < 2.5 or (to.dot(seg) < 0.0 and to.length() < 8.0)
if _at_gate(tgt): # точка маршрута на воротах: считается взятой, только когда плоскость ворот уже пересечена
reached = to.dot(seg) < 0.0 and to.length() < 12.0
if reached and wi < route.size() - 1:
wi += 1
tgt = gp(route[wi])
to = Vector2(tgt.x - pos.x, tgt.z - pos.z)
if p.is_dead:
return
key("move_forward", true)
key("sprint", true)
var dir := to.normalized()
p.rotation.y = atan2(-dir.x, -dir.y)
# прыжок: край впереди или препятствие
var jump := false
if p.is_on_floor() and fr - jumped_at > 12:
var ahead := Vector3(dir.x, 0, dir.y) * (0.5 + Vector2(p.velocity.x, p.velocity.z).length() * 0.045)
var probe := pos + ahead
if ray(probe, probe + Vector3(0, -2.6, 0)).is_empty():
jump = true
if not ray(pos + Vector3(0, -0.5, 0), pos + Vector3(0, -0.5, 0) + Vector3(dir.x, 0, dir.y) * 1.4).is_empty():
jump = true
if jump:
jumped_at = fr
key("jump", jump or fr - jumped_at < 22)
func _music_playing() -> bool:
for n in get_tree().current_scene.get_children():
if n is AudioStreamPlayer and n.stream is AudioStreamOggVorbis and n.playing:
return true
return false
func _at_gate(w: Vector3) -> bool:
for g in Data.CHECKPOINTS:
if Vector2(g[0].x - w.x, g[0].z - w.z).length() < 3.0:
return true
return false