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 умереть (проверка возрождения лицом по трассе) # --fall-at-gate=: после ворот n упасть в пустоту (проверка срыва ниже ворот) # --gu1=<план>, --gu2=<план>: сдача в круге 1 / 2. План: «cancel@1,giveup@2» — когда пройдено столько ворот, # держать G 1,0 с и отпустить (отмена) / держать до сдачи (проверка race_giveup_test.sh) # --map=race2: карта «Гонка 2». Бежать по её маршруту без физики бегуна нельзя (вентиляторы, лазеры, стены), поэтому бот # «проводит» игрока через ворота телепортами по ~0,35 м за кадр (меньше TELEPORT_JUMP: правила считают это бегом); проверяются правила, а не проходимость # (проходимость карты — боты в Blender models/race_02/tests/). # Факты печатаются строками «[имя] …»; сверяет их tools/test/race_game_test.sh. var Data = null # данные карты: race_data.gd или (--map=race2) race2_data.gd var map_id := "race" 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 var gu_plan := {1: [], 2: []} var gu_hold := -1.0 var gu_mode := "" var gu_done := {} var gu_after_cancel := false func _ready() -> void: for a in OS.get_cmdline_user_args(): if a.begins_with("--map="): map_id = a.substr(6) 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)) if a.begins_with("--gu1=") or a.begins_with("--gu2="): for e in a.substr(6).split(","): var q := e.split("@") gu_plan[int(a.substr(4, 1))].append({"mode": q[0], "gate": int(q[1])}) if map_id == "race2": Data = load("res://maps/race2/race2_data.gd") else: Data = load("res://maps/race/race_data.gd") 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 != ("Race2" if map_id == "race2" else "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", "giveup"]: 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.gave_up.has(rules.my_id): for k in ["move_forward", "sprint", "jump", "giveup"]: key(k, false) if gu_hold >= 0.0: # только что сдались: фиксируем, где мы и заморожены ли gu_hold = -1.0 var d := Vector2(p.global_position.x - Data.START_SLOTS[0].x, p.global_position.z - Data.START_SLOTS[0].z).length() say("СДАЛСЯ_БОТ unix=%.3f на_старте=%s заморожен=%s" % [Time.get_unix_time_from_system(), d < 2.0, p.input_frozen]) 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 cur_round := rounds_done + 1 if gu_after_cancel: gu_after_cancel = false say("ПОСЛЕ_ОТМЕНЫ hold=%.2f сдался=%s" % [rules._giveup_hold, rules.gave_up.has(rules.my_id)]) if gu_hold < 0.0 and gu_plan.has(cur_round): for e in gu_plan[cur_round]: var gk := "%d%s%d" % [cur_round, e.mode, e.gate] if not gu_done.has(gk) and rules.passed.get(rules.my_id, 0) == e.gate and not p.is_dead: gu_done[gk] = true gu_hold = 0.0 gu_mode = e.mode say("УДЕРЖИВАЮ G (%s) после ворот %d" % [e.mode, e.gate]) if gu_hold >= 0.0: for k in ["move_forward", "sprint", "jump"]: key(k, false) key("giveup", true) gu_hold += get_physics_process_delta_time() if gu_mode == "cancel" and gu_hold >= 1.0: say("ОТМЕНА_УДЕРЖАНИЯ: отпускаю на %.2f с, шкала=%.2f сдался=%s" % [gu_hold, rules._giveup_hold, rules.gave_up.has(rules.my_id)]) key("giveup", false) gu_hold = -1.0 gu_after_cancel = true 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_tele() -> void: var g: int = rules.passed.get(rules.my_id, 0) if g >= Data.CHECKPOINTS.size() or p.is_dead: return var gate: Array = Data.CHECKPOINTS[g] var yaw := deg_to_rad(gate[2]) var fwd := Vector3(-sin(yaw), 0, -cos(yaw)) var tgt: Vector3 = gate[0] + fwd * 1.5 var step := 0.35 # м за кадр (~21 м/с): круг ~60 с, как у настоящей гонки — иначе зритель C не успеет зайти в разгар круга # лазерный мост: идём медленно (12 м/с) и на ближних подступах к воротам выбираем полосу, где лазер выключен в момент пролёта var pos: Vector3 = p.global_position var lane := -1.0 if pos.x > 105.0 and pos.x < 115.0 and pos.z < -535.0 and pos.z > -615.0: step = 0.2 for l in Data.LASERS: if l[0].x < 110.0: # левая половина; правая — в паре var dz: float = pos.z - l[0].z # > 0 — ворота ещё впереди (идём в -Z) if dz > -1.0 and dz < 8.0: var t := Time.get_unix_time_from_system() + maxf(dz, 0.0) / 12.0 var left_on: bool = fposmod(t + l[5], l[3]) < l[4] or fposmod(t + 0.4 + l[5], l[3]) < l[4] or fposmod(t - 0.4 + l[5], l[3]) < l[4] var right_on: bool = fposmod(t + 1.5 + l[5], l[3]) < l[4] or fposmod(t + 1.9 + l[5], l[3]) < l[4] or fposmod(t + 1.1 + l[5], l[3]) < l[4] lane = 112.0 if left_on else 108.0 if left_on and right_on: lane = -2.0 # обе полосы заняты — стоим if lane == -2.0: return if lane > 0.0: p.global_position.x = lane pos.x = lane tgt.x = lane var d: Vector3 = tgt - pos if tgt.y - pos.y > 2.0: # ворота выше (крыша башни): сначала поднимаемся вертикально — иначе упрёмся в стену d = Vector3(0, minf(step, tgt.y - pos.y + 1.5), 0) elif d.length() > step: d = d.normalized() * step p.global_position = pos + d p.velocity = Vector3.ZERO p.rotation.y = yaw func _drive() -> void: if map_id == "race2": _drive_tele() return 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