From 077c4b30d4ada4258642fe83f0c390ca2148dc1c Mon Sep 17 00:00:00 2001 From: dedeprogames-official Date: Sun, 1 Feb 2026 14:32:22 -0300 Subject: [PATCH 1/2] Add Made by ChatGPT --- RLBotPack/MadeByChatGPT/.gitattributes | 2 + RLBotPack/MadeByChatGPT/.gitignore | 3 + RLBotPack/MadeByChatGPT/appearance.cfg | 104 +++ RLBotPack/MadeByChatGPT/bot.cfg | 29 + RLBotPack/MadeByChatGPT/bot.py | 867 +++++++++++++++++++++++++ RLBotPack/MadeByChatGPT/objects.py | 421 ++++++++++++ RLBotPack/MadeByChatGPT/routines.py | 400 ++++++++++++ RLBotPack/MadeByChatGPT/tools.py | 83 +++ RLBotPack/MadeByChatGPT/utils.py | 136 ++++ 9 files changed, 2045 insertions(+) create mode 100644 RLBotPack/MadeByChatGPT/.gitattributes create mode 100644 RLBotPack/MadeByChatGPT/.gitignore create mode 100644 RLBotPack/MadeByChatGPT/appearance.cfg create mode 100644 RLBotPack/MadeByChatGPT/bot.cfg create mode 100644 RLBotPack/MadeByChatGPT/bot.py create mode 100644 RLBotPack/MadeByChatGPT/objects.py create mode 100644 RLBotPack/MadeByChatGPT/routines.py create mode 100644 RLBotPack/MadeByChatGPT/tools.py create mode 100644 RLBotPack/MadeByChatGPT/utils.py diff --git a/RLBotPack/MadeByChatGPT/.gitattributes b/RLBotPack/MadeByChatGPT/.gitattributes new file mode 100644 index 00000000..dfe07704 --- /dev/null +++ b/RLBotPack/MadeByChatGPT/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/RLBotPack/MadeByChatGPT/.gitignore b/RLBotPack/MadeByChatGPT/.gitignore new file mode 100644 index 00000000..22a982ce --- /dev/null +++ b/RLBotPack/MadeByChatGPT/.gitignore @@ -0,0 +1,3 @@ + +.idea/ +Experimental diff --git a/RLBotPack/MadeByChatGPT/appearance.cfg b/RLBotPack/MadeByChatGPT/appearance.cfg new file mode 100644 index 00000000..5b476bc8 --- /dev/null +++ b/RLBotPack/MadeByChatGPT/appearance.cfg @@ -0,0 +1,104 @@ +# You don't have to manually edit this file! +# RLBotGUI has an appearance editor with a nice colorpicker, database of items and more! +# To open it up, simply click the (i) icon next to your bot's name and then click Edit Appearance + +[Bot Loadout] +# Primary Color selection +team_color_id = 5 +# Secondary Color selection +custom_color_id = 12 +# Car type (Octane, Merc, etc) +car_id = 23 +# Type of decal +decal_id = 0 +# Wheel selection +wheels_id = 1729 +# Boost selection +boost_id = 3114 +# Antenna Selection +antenna_id = 0 +# Hat Selection +hat_id = 0 +# Paint Type (for first color) +paint_finish_id = 1681 +# Paint Type (for secondary color) +custom_finish_id = 1681 +# Engine Audio Selection +engine_audio_id = 0 +# Car trail Selection +trails_id = 0 +# Goal Explosion Selection +goal_explosion_id = 4179 +# Finds the closest primary color swatch based on the provided RGB value like [34, 255, 60] +primary_color_lookup = None +# Finds the closest secondary color swatch based on the provided RGB value like [34, 255, 60] +secondary_color_lookup = None + +[Bot Loadout Orange] +# Primary Color selection +team_color_id = 3 +# Secondary Color selection +custom_color_id = 0 +# Car type (Octane, Merc, etc) +car_id = 23 +# Type of decal +decal_id = 0 +# Wheel selection +wheels_id = 1729 +# Boost selection +boost_id = 3114 +# Antenna Selection +antenna_id = 0 +# Hat Selection +hat_id = 0 +# Paint Type (for first color) +paint_finish_id = 1681 +# Paint Type (for secondary color) +custom_finish_id = 1681 +# Engine Audio Selection +engine_audio_id = 0 +# Car trail Selection +trails_id = 0 +# Goal Explosion Selection +goal_explosion_id = 4179 +# Finds the closest primary color swatch based on the provided RGB value like [34, 255, 60] +primary_color_lookup = None +# Finds the closest secondary color swatch based on the provided RGB value like [34, 255, 60] +secondary_color_lookup = None + +[Bot Paint Blue] +# car_paint_id +car_paint_id = 0 +# decal_paint_id +decal_paint_id = 0 +# wheels_paint_id +wheels_paint_id = 7 +# boost_paint_id +boost_paint_id = 4 +# antenna_paint_id +antenna_paint_id = 0 +# hat_paint_id +hat_paint_id = 0 +# trails_paint_id +trails_paint_id = 2 +# goal_explosion_paint_id +goal_explosion_paint_id = 9 + +[Bot Paint Orange] +# car_paint_id +car_paint_id = 0 +# decal_paint_id +decal_paint_id = 0 +# wheels_paint_id +wheels_paint_id = 14 +# boost_paint_id +boost_paint_id = 10 +# antenna_paint_id +antenna_paint_id = 0 +# hat_paint_id +hat_paint_id = 0 +# trails_paint_id +trails_paint_id = 14 +# goal_explosion_paint_id +goal_explosion_paint_id = 9 + diff --git a/RLBotPack/MadeByChatGPT/bot.cfg b/RLBotPack/MadeByChatGPT/bot.cfg new file mode 100644 index 00000000..a37ddfd8 --- /dev/null +++ b/RLBotPack/MadeByChatGPT/bot.cfg @@ -0,0 +1,29 @@ +[Locations] +# Path to loadout config. Can use relative path from here. +looks_config = ./appearance.cfg + +# Path to python file. Can use relative path from here. +#!!!Make sure it matches whatever you rename ExampleBot.py to!!! +python_file = ./bot.py + +# Name of the bot in-game +name = Made by ChatGPT + +maximum_tick_rate_preference = 120 + +[Details] +# These values are optional but useful metadata for helper programs +# Name of the bot's creator/developer +developer = ChatGPT 5.2 + +# Short description of the bot +description = "A tactical 1v1-focused RLBot built on GoslingUtils that blends smart shot selection, timed aerial intercepts, and momentum-efficient movement. It evaluates threat, picks future intercept points it can realistically reach, and visualizes its plan with rich in-game debug overlays (intercept circles, shot arrows, and ball flight paths)." + +# Fun fact about the bot +fun_fact = "It “thinks ahead” in the air—choosing aerial intercept points that are late enough to be reachable, so it arrives on time instead of jumping early and missing." + +# Link to github repository +github = https://github.com/ddthj/GoslingUtils + +# Programming language +language = Python diff --git a/RLBotPack/MadeByChatGPT/bot.py b/RLBotPack/MadeByChatGPT/bot.py new file mode 100644 index 00000000..8b02d82b --- /dev/null +++ b/RLBotPack/MadeByChatGPT/bot.py @@ -0,0 +1,867 @@ +from tools import * +from objects import * +from routines import * + +import math + + +# ============================================================ +# Helpers de desenho (render) - compatível mesmo sem "circle" +# ============================================================ + +def _safe_line(agent, a, b, color=(255, 255, 255)): + try: + agent.line(a, b, color) + return True + except Exception: + # se sua versão usa renderer direto, você pode adaptar aqui + return False + + +def _draw_circle(agent, center, radius=250, color=(255, 255, 255), steps=24, z_override=None): + # desenha círculo aproximado com linhas + pts = [] + z = center.z if z_override is None else z_override + for i in range(steps + 1): + t = (i / steps) * (math.pi * 2) + pts.append(Vector3(center.x + math.cos(t) * radius, center.y + math.sin(t) * radius, z)) + + for i in range(len(pts) - 1): + _safe_line(agent, pts[i], pts[i + 1], color) + + +def _draw_arrow(agent, start, end, color=(255, 255, 255), head_len=180, head_angle_deg=28): + _safe_line(agent, start, end, color) + + # cabeça da seta no "end" + dir_vec = (end - start) + if dir_vec.magnitude() < 1: + return + d = dir_vec.normalize() + + # cria 2 vetores laterais no plano XY + ang = math.radians(head_angle_deg) + # rotaciona d no plano XY + left = Vector3( + d.x * math.cos(ang) - d.y * math.sin(ang), + d.x * math.sin(ang) + d.y * math.cos(ang), + 0 + ) + right = Vector3( + d.x * math.cos(-ang) - d.y * math.sin(-ang), + d.x * math.sin(-ang) + d.y * math.cos(-ang), + 0 + ) + + p1 = end - left.normalize() * head_len + p2 = end - right.normalize() * head_len + _safe_line(agent, end, p1, color) + _safe_line(agent, end, p2, color) + + +def _draw_text_3d(agent, location, text, color=(255, 255, 255)): + """ + GoslingUtils normalmente tem renderer interno, mas algumas versões expõem helpers. + Tentamos várias opções sem quebrar. + """ + # tenta métodos comuns + for attr in ["draw_string_3d", "string", "text", "draw_text_3d"]: + fn = getattr(agent, attr, None) + if callable(fn): + try: + fn(location, text, color) + return True + except Exception: + pass + + # tenta renderer + renderer = getattr(agent, "renderer", None) + if renderer is not None: + for attr in ["draw_string_3d", "draw_string_2d", "draw_string"]: + fn = getattr(renderer, attr, None) + if callable(fn): + try: + # muitas versões: draw_string_3d(x,y,z, scaleX, scaleY, text, color) + # outras: draw_string_3d(vec, scale, text, color) + # vamos tentar formatos + try: + fn(location, 1, 1, text, color) + return True + except Exception: + try: + fn(location.x, location.y, location.z, 1, 1, text, color) + return True + except Exception: + pass + except Exception: + pass + + return False + + +# ============================================================ +# Helpers de "previsão" simples da bola (para render e timing) +# ============================================================ + +GRAVITY_Z = -650.0 # Rocket League approx + +def _ballistic_ball_pos(ball_loc, ball_vel, dt): + # movimento simples sem bounce (serve p/ debug render) + return Vector3( + ball_loc.x + ball_vel.x * dt, + ball_loc.y + ball_vel.y * dt, + ball_loc.z + ball_vel.z * dt + 0.5 * GRAVITY_Z * dt * dt + ) + + +def _ballistic_ball_vel(ball_vel, dt): + return Vector3(ball_vel.x, ball_vel.y, ball_vel.z + GRAVITY_Z * dt) + + +def _sample_ball_path(ball_loc, ball_vel, t_end, step=0.10): + pts = [] + t = 0.0 + loc = ball_loc + vel = ball_vel + while t < t_end: + pts.append(loc) + # integra + loc = _ballistic_ball_pos(loc, vel, step) + vel = _ballistic_ball_vel(vel, step) + + # clamp chão (sem bounce real, mas evita "z negativa" no debug) + if loc.z < 0: + loc = Vector3(loc.x, loc.y, 0) + vel = Vector3(vel.x, vel.y, 0) + + t += step + pts.append(loc) + return pts + + +# ============================================================ +# Helpers de estratégia / matemática +# ============================================================ + +def _cap(x, lo, hi): + return lo if x < lo else hi if x > hi else x + + +def _eta_to_point(agent, car, point): + """ + ETA simples: distância / velocidade + penalidade por ângulo. + Bom o suficiente pra rotação/commit. + """ + to = point - car.location + dist = to.magnitude() + if dist < 1: + return 0.0 + + local = car.local(to) + angle = abs(math.atan2(local.x, local.y)) # 0 -> alinhado, pi -> de costas + + speed = max(300.0, car.velocity.magnitude()) + base = dist / speed + + # penaliza giro + turn_penalty = (angle / math.pi) * 1.2 # até +1.2s + # penaliza se estiver quase parado + slow_penalty = 0.3 if speed < 900 else 0.0 + + return base + turn_penalty + slow_penalty + + +def _role_and_ranks(agent): + """ + Retorna: + - my_role: "FIRST" / "SECOND" / "THIRD" + - i_am_last_man: bool (mais perto do nosso gol) + - etas: lista (player_index, eta) + - goal_dists: lista (player_index, dist_own_goal) + """ + friends = list(getattr(agent, "friends", [])) + players = [agent.me] + friends + ball_loc = agent.ball.location + own_goal = agent.friend_goal.location + + # 1v1: não existe rotação 1/2/3 man; evitar comportamento "chuta e volta pro gol" sempre + if len(players) == 1: + eta = _eta_to_point(agent, agent.me, ball_loc) + gd = (agent.me.location - own_goal).magnitude() + return "FIRST", False, [(agent.me.index, eta)], [(agent.me.index, gd)] + + etas = [] + goal_dists = [] + + for p in players: + etas.append((p.index, _eta_to_point(agent, p, ball_loc))) + goal_dists.append((p.index, (p.location - own_goal).magnitude())) + + etas_sorted = sorted(etas, key=lambda x: x[1]) + goal_sorted = sorted(goal_dists, key=lambda x: x[1]) + + # rank do meu index na lista de ETA + my_idx = agent.me.index + my_rank_eta = [i for i, (idx, _) in enumerate(etas_sorted) if idx == my_idx][0] + my_rank_goal = [i for i, (idx, _) in enumerate(goal_sorted) if idx == my_idx][0] + + # regra simples p/ role: + # - FIRST: menor ETA + # - THIRD: mais perto do próprio gol (last man) + # - SECOND: resto + i_am_last_man = (my_rank_goal == 0) + + if my_rank_eta == 0 and not i_am_last_man: + my_role = "FIRST" + elif i_am_last_man: + my_role = "THIRD" + else: + my_role = "SECOND" + + return my_role, i_am_last_man, etas_sorted, goal_sorted + + +def _threat_level(agent): + """ + Heurística de perigo: + - bola indo pro nosso gol + - bola no nosso lado + - oponente com ETA menor que o nosso + """ + s = side(agent.team) + + ball = agent.ball + me = agent.me + own_goal = agent.friend_goal.location + + ball_on_our_side = (ball.location.y * s) < 0 + ball_towards_our_goal = (ball.velocity.y * s) < -200 + ball_close_to_goal = (ball.location - own_goal).magnitude() < 2800 + + # melhor ETA do inimigo vs nosso + foe_best_eta = 999 + for f in getattr(agent, "foes", []): + foe_best_eta = min(foe_best_eta, _eta_to_point(agent, f, ball.location)) + my_eta = _eta_to_point(agent, me, ball.location) + + foe_beats_me = foe_best_eta + 0.10 < my_eta + + threat = 0.0 + if ball_on_our_side: threat += 0.35 + if ball_towards_our_goal: threat += 0.35 + if ball_close_to_goal: threat += 0.35 + if foe_beats_me: threat += 0.35 + + return _cap(threat, 0.0, 1.5) + + +def _desired_approach_speed(agent, dist_to_ball, intercept_z, ball_vz): + """ + Controle inteligente (4): + - Se intercepto é "baixo" → pode chegar mais rápido + - Se bola tá caindo ou vai quicar → reduz pra chegar na hora do toque + """ + # base por distância + if dist_to_ball > 2200: + base = 2300 + elif dist_to_ball > 1400: + base = 2000 + elif dist_to_ball > 800: + base = 1600 + else: + base = 1100 + + # bola no ar / caindo: desacelera mais pra timing + if intercept_z > 160: + base -= 400 + if ball_vz < -200 and intercept_z > 120: + base -= 300 + + return _cap(base, 600, 2300) + + +def _choose_shot_target(agent, shot_kind): + # pra render/seta: um ponto “pra onde” queremos chutar + if shot_kind == "goal": + return agent.foe_goal.location + if shot_kind == "clear": + # limpa pro lado oposto do centro (evita devolver pro meio) + s = side(agent.team) + x = 3800 if agent.ball.location.x < 0 else -3800 + y = 1800 * s # empurrando pra frente + return Vector3(x, y, 0) + return agent.foe_goal.location + + +def _shot_score(agent, shot, shot_kind="goal"): + """ + (3) Scoring melhor: + - velocidade média até o intercepto + - alinhamento car->bola e bola->alvo + - penaliza altura se não tiver boost + - penaliza se for overcommit (threat alto) + """ + me = agent.me + ball = agent.ball + now = agent.time + + # intercept_time / ball_location existem nos shots do GoslingUtils normalmente + intercept_time = getattr(shot, "intercept_time", None) + ball_loc = getattr(shot, "ball_location", None) + + if intercept_time is None or ball_loc is None: + return -999999 + + dt = max(0.01, intercept_time - now) + dist = (ball_loc - me.location).magnitude() + avg_speed = dist / dt + + # alinhamentos + to_ball = (ball_loc - me.location) + to_ball_n = to_ball.normalize() if to_ball.magnitude() > 1 else Vector3(0, 1, 0) + + target_point = _choose_shot_target(agent, shot_kind) + ball_to_target = (target_point - ball_loc) + ball_to_target_n = ball_to_target.normalize() if ball_to_target.magnitude() > 1 else Vector3(0, 1, 0) + + # quanto o carro está “apontado” pro intercepto (aprox por local.y) + local = me.local(to_ball) + facing = _cap(local.y / (to_ball.magnitude() + 1e-6), -1, 1) # ~cos + align = _cap(to_ball_n.dot(ball_to_target_n), -1, 1) + + # ratio do find_hits (quando existir) + ratio = getattr(shot, "ratio", 1.0) + + # boost / altura + height_penalty = 0.0 + if ball_loc.z > 220 and me.boost < 40: + height_penalty += 0.7 + if ball_loc.z > 380 and me.boost < 60: + height_penalty += 1.2 + + # risco (se jogo perigoso, evita commits ruins) + threat = _threat_level(agent) + risk_penalty = 0.0 + if threat > 0.9 and shot_kind == "goal": + risk_penalty += 0.6 + + # score final + score = (avg_speed * 0.55) + (ratio * 900) + (align * 500) + (facing * 300) + score -= (height_penalty * 900) + score -= (risk_penalty * 800) + + return score + + +def _pick_best_shot(agent, shots_dict): + best = None + best_kind = None + best_score = -999999 + + for kind in ["goal", "clear"]: + for shot in shots_dict.get(kind, []): + sc = _shot_score(agent, shot, kind) + if sc > best_score: + best = shot + best_score = sc + best_kind = kind + + return best, best_kind, best_score + + +def _shot_is_aerial(shot): + name = shot.__class__.__name__.lower() + return ("aerial" in name) or ("air" in name) + + +def _has_routine(name): + return name in globals() and callable(globals()[name]) + + +# ============================================================ +# (NOVO) AERIAL: escolher ponto futuro inteligente + tempo de chegar +# ============================================================ + +def _aerial_time_needed(agent, intercept_loc): + """ + Estimativa simples do tempo mínimo pro aerial acontecer com chance: + - inclui "setup" (alinhar + pular) e viagem horizontal/vertical + - escala por boost baixo (mais lento/menos controle) + """ + me = agent.me + + dx = intercept_loc.x - me.location.x + dy = intercept_loc.y - me.location.y + d_xy = math.sqrt(dx * dx + dy * dy) + + h = max(0.0, intercept_loc.z - me.location.z) + + setup = 0.55 + travel = d_xy / 1700.0 + climb = max(0.0, h - 140.0) / 900.0 + margin = 0.10 + + t = setup + travel + climb + margin + + # boost baixo => precisa de mais tempo (aéreo mais lento e menos correção) + if me.boost < 55: + t *= 1.10 + if me.boost < 35: + t *= 1.18 + if me.boost < 20: + t *= 1.28 + + return t + + +def _pick_best_aerial(agent, shots_dict): + """ + Escolhe um shot AÉREO que: + - esteja no ar (z alto) + - seja futuro o suficiente para dar tempo de chegar + - maximize o score existente + bônus por 'margem de tempo' + """ + now = agent.time + me = agent.me + + best = None + best_kind = None + best_score = -999999 + + for kind in ["goal", "clear"]: + for shot in shots_dict.get(kind, []): + it = getattr(shot, "intercept_time", None) + il = getattr(shot, "ball_location", None) + if it is None or il is None: + continue + + # precisa estar no ar pra valer como aerial + if il.z < 260: + continue + + dt = it - now + if dt <= 0.0: + continue + + need = _aerial_time_needed(agent, il) + + # precisa ser um ponto futuro que dá tempo de chegar + if dt < (need + 0.08): + continue + + # muito longe no futuro costuma gerar aerial ruim/aleatório + if dt > 4.0: + continue + + # score base do shot + bônus por ter folga de tempo (mas sem exagero) + base_sc = _shot_score(agent, shot, kind) + slack = _cap(dt - need, 0.0, 1.25) + sc = base_sc + slack * 220.0 + + # se boost muito baixo, exige ainda mais folga prática + if me.boost < 25 and slack < 0.35: + sc -= 600.0 + + if sc > best_score: + best = shot + best_score = sc + best_kind = kind + + return best, best_kind, best_score + + +# ============================================================ +# Rotinas custom (kickoff cheat) – sem depender de rotinas do lib +# ============================================================ + +class CheatKickoff: + """ + Vai “cheatar” no kickoff: avança até um ponto seguro e pronto pra pegar rebote. + """ + def __init__(self, spot): + self.spot = spot + + def run(self, agent): + relative = self.spot - agent.me.location + dist = relative.magnitude() + local = agent.me.local(relative) + + defaultPD(agent, local) + defaultThrottle(agent, _cap(dist * 2, 0, 2300)) + agent.controller.boost = (dist > 1400 and abs(local.x) < 250 and abs(local.y) > 800) + + # termina quando chega + if dist < 250: + return True # pop + return False + + +# ============================================================ +# (NOVO) Wavedash (wayyshot) + render +# ============================================================ + +class WaveDash: + """ + Wavedash simples por temporização + detecção de descida. + - 1) jump curto + - 2) espera cair + - 3) dodge pra frente quando perto do chão + """ + def __init__(self): + self.t0 = None + self.phase = 0 + self.dodge_t = None + + def run(self, agent): + me = agent.me + + # se já não está no chão ao iniciar, não força + if self.t0 is None: + self.t0 = agent.time + self.phase = 0 + self.dodge_t = None + if not bool(getattr(me, "on_ground", True)): + return True + + elapsed = agent.time - self.t0 + + # render + _draw_text_3d(agent, me.location + Vector3(0, 0, 140), "WAVEDASH", (255, 120, 255)) + + # defaults + agent.controller.throttle = 1.0 + agent.controller.boost = False + agent.controller.handbrake = False + agent.controller.steer = 0.0 + agent.controller.yaw = 0.0 + agent.controller.roll = 0.0 + + # fase 0: jump curto + if self.phase == 0: + agent.controller.jump = True + agent.controller.pitch = -1.0 + if elapsed > 0.08: + self.phase = 1 + return False + + # fase 1: solta jump e inclina pra frente + if self.phase == 1: + agent.controller.jump = False + agent.controller.pitch = -1.0 + if elapsed > 0.22: + self.phase = 2 + return False + + # fase 2: espera cair e tocar perto do chão + if self.phase == 2: + agent.controller.jump = False + agent.controller.pitch = -1.0 + + # quando estiver descendo e perto do chão, executa dodge + if me.location.z < 28 and me.velocity.z < -50: + self.phase = 3 + self.dodge_t = agent.time + # timeout (se algo der errado) + if elapsed > 1.20: + return True + return False + + # fase 3: dodge curto pra frente + if self.phase == 3: + if self.dodge_t is None: + self.dodge_t = agent.time + + dt = agent.time - self.dodge_t + agent.controller.pitch = -1.0 + agent.controller.jump = (dt < 0.06) + + if dt > 0.14: + return True + return False + + return True + + +# ============================================================ +# BOT +# ============================================================ + +class ExampleBot(GoslingAgent): + def run(agent): + # ====================== + # State inicial + # ====================== + if not hasattr(agent, "dbg"): + agent.dbg = { + "action": "INIT", + "role": "UNK", + "intercept_t": None, + "intercept_loc": None, + "shot_kind": None, + "shot_target": None + } + + # wavedash cooldown + if not hasattr(agent, "wd_last"): + agent.wd_last = -9999.0 + + me = agent.me + ball = agent.ball + s = side(agent.team) + + # ====================== + # Role / rotação + # ====================== + my_role, i_am_last_man, etas_sorted, goal_sorted = _role_and_ranks(agent) + threat = _threat_level(agent) + + # texto da ação no carro (vai atualizando) + agent.dbg["role"] = my_role + + # ====================== + # RENDER base: linhas de debug do seu exemplo + # ====================== + try: + left_test_a = Vector3(-4100 * s, ball.location.y, 0) + left_test_b = Vector3(4100 * s, ball.location.y, 0) + _safe_line(agent, me.location, left_test_a, (0, 255, 0)) + _safe_line(agent, me.location, left_test_b, (255, 0, 0)) + except Exception: + pass + + # ====================== + # Se já tem rotina rodando, só render e sai + # ====================== + if len(agent.stack) > 0: + # render texto + _draw_text_3d(agent, me.location + Vector3(0, 0, 120), + f"[{agent.dbg['role']}] {agent.dbg['action']}", (255, 255, 255)) + return + + # ====================== + # KICKOFF avançado (8) + # ====================== + if agent.kickoff_flag: + # quem é o taker? menor ETA da equipe + my_is_taker = (etas_sorted[0][0] == me.index) + + if my_is_taker: + agent.dbg["action"] = "KICKOFF: TAKING" + if _has_routine("kickoff"): + agent.push(kickoff()) + else: + # fallback: acelera reto pra bola + agent.controller.throttle = 1 + agent.controller.boost = True + else: + # cheat spot: ligeiramente à frente do meio, do nosso lado + cheat_spot = Vector3(0, -800 * s, 0) + agent.dbg["action"] = "KICKOFF: CHEAT" + agent.push(CheatKickoff(cheat_spot)) + + _draw_text_3d(agent, me.location + Vector3(0, 0, 120), + f"[{agent.dbg['role']}] {agent.dbg['action']}", (255, 255, 255)) + return + + # ====================== + # Targets e shots (3) + Aerials (6) + # ====================== + targets = { + "goal": (agent.foe_goal.left_post, agent.foe_goal.right_post), + "clear": (Vector3(-4100 * s, ball.location.y, 0), Vector3(4100 * s, ball.location.y, 0)), + } + + shots = find_hits(agent, targets) + + best_shot, best_kind, best_score = _pick_best_shot(agent, shots) + + # ====================== + # Intercept info p/ render (círculo / trajeto / seta) + # ====================== + intercept_time = None + intercept_loc = None + if best_shot is not None: + intercept_time = getattr(best_shot, "intercept_time", None) + intercept_loc = getattr(best_shot, "ball_location", None) + + agent.dbg["intercept_t"] = intercept_time + agent.dbg["intercept_loc"] = intercept_loc + agent.dbg["shot_kind"] = best_kind + agent.dbg["shot_target"] = _choose_shot_target(agent, best_kind) if best_kind else None + + # ====================== + # DECISÃO PRINCIPAL + # ====================== + + # Regra de commit por rotação: + # - THIRD (last man) só comita em clear/save ou se ameaça baixa + can_commit_attack = (my_role != "THIRD") or (threat < 0.55) + + # Se existe shot bom + if best_shot is not None: + is_aerial = _shot_is_aerial(best_shot) + z = intercept_loc.z if intercept_loc else 0 + + # (6) Aerial: agora escolhe ponto FUTURO inteligente que dá tempo de chegar + if is_aerial or z > 250: + aerial_shot, aerial_kind, aerial_sc = _pick_best_aerial(agent, shots) + + if aerial_shot is not None and me.boost >= 35 and can_commit_attack: + # atualiza debug/intercept para render apontar pro ponto certo do aerial + a_it = getattr(aerial_shot, "intercept_time", None) + a_il = getattr(aerial_shot, "ball_location", None) + + agent.dbg["intercept_t"] = a_it + agent.dbg["intercept_loc"] = a_il + agent.dbg["shot_kind"] = aerial_kind + agent.dbg["shot_target"] = _choose_shot_target(agent, aerial_kind) if aerial_kind else None + + agent.dbg["action"] = f"SHOT: AERIAL ({aerial_kind})" + agent.push(aerial_shot) + else: + agent.dbg["action"] = "HOLD: NO GOOD AERIAL POINT / SAFE ROTATION" + + else: + # (4) Speed control na aproximação + # Se o shot for cedo demais / perto demais, às vezes a gente chega “antes”. + # Então só comita se score ok e role permite. + if best_score > 500 and can_commit_attack: + agent.dbg["action"] = f"SHOT: GROUND ({best_kind})" + agent.push(best_shot) + else: + agent.dbg["action"] = "POSITION: WAIT / SUPPORT" + + # Sem shot: defesa / rotação / boost + if len(agent.stack) < 1: + # Perigo alto => defender + if threat > 0.85 or my_role == "THIRD": + # tenta usar save do GoslingUtils se existir e bola indo pro gol + ball_towards_our_goal = (ball.velocity.y * s) < -150 + ball_close_goal = (ball.location - agent.friend_goal.location).magnitude() < 3200 + + if ball_towards_our_goal and ball_close_goal and _has_routine("save"): + agent.dbg["action"] = "DEFENSE: SAVE ROUTINE" + agent.push(save(agent.friend_goal.location)) + else: + # fallback: ir pro far post com velocidade controlada + left_dist = (agent.friend_goal.left_post - me.location).magnitude() + right_dist = (agent.friend_goal.right_post - me.location).magnitude() + target = agent.friend_goal.left_post if left_dist < right_dist else agent.friend_goal.right_post + + # move um pouco pra fora do gol (far post + offset) + target = Vector3(target.x, target.y, 0) + + agent.dbg["action"] = "DEFENSE: FAR POST / SHADOW" + + relative = target - me.location + dist = relative.magnitude() + local = me.local(relative) + + # (NOVO) wavedash quando sem boost e longe, pra acelerar sem gastar boost + if (me.boost < 12 and dist > 2600 and me.velocity.magnitude() < 1050 and + abs(local.x) < 220 and (agent.time - agent.wd_last) > 2.2 and + bool(getattr(me, "on_ground", True))): + agent.wd_last = agent.time + agent.dbg["action"] = "MOVE: WAVEDASH" + agent.push(WaveDash()) + else: + defaultPD(agent, local) + + # velocidade segura (não torrar boost) + speed = _cap(dist * 1.8, 0, 2000) + defaultThrottle(agent, speed) + agent.controller.boost = (dist > 2600 and abs(local.x) < 200 and abs(local.y) > 900 and me.boost > 30) + + # Boost se estiver baixo e não for last man + elif me.boost < 30: + best_boost = None + best_val = -1.0 + for boost in agent.boosts: + if not boost.active: + continue + if not boost.large: + continue + + me_to_boost = (boost.location - me.location).normalize() + boost_to_goal = (agent.friend_goal.location - boost.location).normalize() + + val = boost_to_goal.dot(me_to_boost) + if val > best_val: + best_val = val + best_boost = boost + + if best_boost is not None and _has_routine("goto_boost"): + agent.dbg["action"] = "RESOURCE: GET BOOST (LARGE)" + agent.push(goto_boost(best_boost, agent.friend_goal.location)) + + # Support: posicionar mid/back post “inteligente” + else: + agent.dbg["action"] = "ROTATE: SUPPORT MID" + # spot de suporte: entre bola e nosso gol, um pouco pra trás + goal = agent.friend_goal.location + ball_to_goal = (goal - ball.location).normalize() + support = ball.location + ball_to_goal * 2200 # 2200 atrás da bola + support = Vector3(_cap(support.x, -3800, 3800), _cap(support.y, -5100, 5100), 0) + + relative = support - me.location + dist = relative.magnitude() + local = me.local(relative) + + # (NOVO) wavedash quando sem boost e longe, pra acelerar sem gastar boost + if (me.boost < 12 and dist > 2800 and me.velocity.magnitude() < 1050 and + abs(local.x) < 220 and (agent.time - agent.wd_last) > 2.2 and + bool(getattr(me, "on_ground", True))): + agent.wd_last = agent.time + agent.dbg["action"] = "MOVE: WAVEDASH" + agent.push(WaveDash()) + else: + defaultPD(agent, local) + + # (4) speed control: se estiver “chegando na bola” (perto), desacelera + desired_speed = _cap(dist * 2.0, 0, 2300) + + # se muito perto da bola, ajusta timing + dist_ball = (ball.location - me.location).magnitude() + desired_speed = min(desired_speed, _desired_approach_speed(agent, dist_ball, ball.location.z, ball.velocity.z)) + + defaultThrottle(agent, desired_speed) + agent.controller.boost = (dist > 2600 and abs(local.x) < 240 and abs(local.y) > 900 and me.boost > 20) + + # ============================================================ + # RENDER AVANÇADO (pedido do usuário) + # ============================================================ + + # texto no carro + _draw_text_3d(agent, me.location + Vector3(0, 0, 120), + f"[{agent.dbg['role']}] {agent.dbg['action']} | threat={threat:.2f}", (255, 255, 255)) + + # se temos intercepto, desenha: + it = agent.dbg["intercept_t"] + il = agent.dbg["intercept_loc"] + st = agent.dbg["shot_target"] + + if it is not None and il is not None: + # 1) círculo no chão do intercepto + ground = Vector3(il.x, il.y, 0) + _draw_circle(agent, ground, radius=260, color=(100, 220, 255), steps=28, z_override=5) + + # 2) seta indicando direção do chute (alvo) + if st is not None: + arrow_end = Vector3(st.x, st.y, 0) + # seta começa no círculo e aponta pro alvo (no chão) + _draw_arrow(agent, ground + Vector3(0, 0, 10), arrow_end + Vector3(0, 0, 10), color=(255, 180, 80)) + + # 3) path da bola até o intercepto (no ar) + dt = max(0.0, it - agent.time) + if dt > 0.05: + pts = _sample_ball_path(ball.location, ball.velocity, min(dt, 4.0), step=0.10) + + # desenha polyline + for i in range(len(pts) - 1): + _safe_line(agent, pts[i], pts[i + 1], (180, 255, 180)) + + # marca ponto exato do toque (il) + _draw_circle(agent, il, radius=120, color=(255, 255, 255), steps=18, z_override=il.z) + + # linha do carro até o intercepto (visual “vou pra lá”) + _safe_line(agent, me.location, il, (255, 255, 0)) diff --git a/RLBotPack/MadeByChatGPT/objects.py b/RLBotPack/MadeByChatGPT/objects.py new file mode 100644 index 00000000..c9b30d77 --- /dev/null +++ b/RLBotPack/MadeByChatGPT/objects.py @@ -0,0 +1,421 @@ +import math +import rlbot.utils.structures.game_data_struct as game_data_struct +from rlbot.agents.base_agent import BaseAgent, SimpleControllerState + + +# This file holds all of the objects used in gosling utils +# Includes custom vector and matrix objects + +class GoslingAgent(BaseAgent): + # This is the main object of Gosling Utils. It holds/updates information about the game and runs routines + # All utils rely on information being structured and accessed the same way as configured in this class + def initialize_agent(self): + # A list of cars for both teammates and opponents + self.friends = [] + self.foes = [] + # This holds the carobject for our agent + self.me = car_object(self.index) + + self.ball = ball_object() + self.game = game_object() + # A list of boosts + self.boosts = [] + # goals + self.friend_goal = goal_object(self.team) + self.foe_goal = goal_object(not self.team) + # A list that acts as the routines stack + self.stack = [] + # Game time + self.time = 0.0 + # Whether or not GoslingAgent has run its get_ready() function + self.ready = False + # the controller that is returned to the framework after every tick + self.controller = SimpleControllerState() + # a flag that tells us when kickoff is happening + self.kickoff_flag = False + + def get_ready(self, packet): + # Preps all of the objects that will be updated during play + field_info = self.get_field_info() + for i in range(field_info.num_boosts): + boost = field_info.boost_pads[i] + self.boosts.append(boost_object(i, boost.location, boost.is_full_boost)) + self.refresh_player_lists(packet) + self.ball.update(packet) + self.ready = True + + def refresh_player_lists(self, packet): + # makes new freind/foe lists + # Useful to keep separate from get_ready because humans can join/leave a match + self.friends = [car_object(i, packet) for i in range(packet.num_cars) if + packet.game_cars[i].team == self.team and i != self.index] + self.foes = [car_object(i, packet) for i in range(packet.num_cars) if packet.game_cars[i].team != self.team] + + def push(self, routine): + # Shorthand for adding a routine to the stack + self.stack.append(routine) + + def pop(self): + # Shorthand for removing a routine from the stack, returns the routine + return self.stack.pop() + + def line(self, start, end, color=None): + color = color if color != None else [255, 255, 255] + self.renderer.draw_line_3d(start.copy(), end.copy(), self.renderer.create_color(255, *color)) + + def debug_stack(self): + # Draws the stack on the screen + white = self.renderer.white() + for i in range(len(self.stack) - 1, -1, -1): + text = self.stack[i].__class__.__name__ + self.renderer.draw_string_2d(10, 50 + (50 * (len(self.stack) - i)), 3, 3, text, white) + + def clear(self): + # Shorthand for clearing the stack of all routines + self.stack = [] + + def preprocess(self, packet): + # Calling the update functions for all of the objects + if packet.num_cars != len(self.friends) + len(self.foes) + 1: self.refresh_player_lists(packet) + for car in self.friends: car.update(packet) + for car in self.foes: car.update(packet) + for pad in self.boosts: pad.update(packet) + self.ball.update(packet) + self.me.update(packet) + self.game.update(packet) + self.time = packet.game_info.seconds_elapsed + # When a new kickoff begins we empty the stack + if self.kickoff_flag == False and packet.game_info.is_round_active and packet.game_info.is_kickoff_pause: + self.stack = [] + # Tells us when to go for kickoff + self.kickoff_flag = packet.game_info.is_round_active and packet.game_info.is_kickoff_pause + + def get_output(self, packet): + # Reset controller + self.controller.__init__() + # Get ready, then preprocess + if not self.ready: + self.get_ready(packet) + self.preprocess(packet) + + self.renderer.begin_rendering() + # Run our strategy code + self.run() + # run the routine on the end of the stack + if len(self.stack) > 0: + self.stack[-1].run(self) + self.renderer.end_rendering() + # send our updated controller back to rlbot + return self.controller + + def run(self): + # override this with your strategy code + pass + + +class car_object: + # The carObject, and kin, convert the gametickpacket in something a little friendlier to use, + # and are updated by GoslingAgent as the game runs + def __init__(self, index, packet=None): + self.location = Vector3(0, 0, 0) + self.orientation = Matrix3(0, 0, 0) + self.velocity = Vector3(0, 0, 0) + self.angular_velocity = [0, 0, 0] + self.demolished = False + self.airborne = False + self.supersonic = False + self.jumped = False + self.doublejumped = False + self.boost = 0 + self.index = index + if packet is not None: + self.update(packet) + + def local(self, value): + # Shorthand for self.orientation.dot(value) + return self.orientation.dot(value) + + def update(self, packet): + car = packet.game_cars[self.index] + self.location.data = [car.physics.location.x, car.physics.location.y, car.physics.location.z] + self.velocity.data = [car.physics.velocity.x, car.physics.velocity.y, car.physics.velocity.z] + self.orientation = Matrix3(car.physics.rotation.pitch, car.physics.rotation.yaw, car.physics.rotation.roll) + self.angular_velocity = self.orientation.dot( + [car.physics.angular_velocity.x, car.physics.angular_velocity.y, car.physics.angular_velocity.z]).data + self.demolished = car.is_demolished + self.airborne = not car.has_wheel_contact + self.supersonic = car.is_super_sonic + self.jumped = car.jumped + self.doublejumped = car.double_jumped + self.boost = car.boost + + @property + def forward(self): + # A vector pointing forwards relative to the cars orientation. Its magnitude is 1 + return self.orientation.forward + + @property + def left(self): + # A vector pointing left relative to the cars orientation. Its magnitude is 1 + return self.orientation.left + + @property + def up(self): + # A vector pointing up relative to the cars orientation. Its magnitude is 1 + return self.orientation.up + + +class ball_object: + def __init__(self): + self.location = Vector3(0, 0, 0) + self.velocity = Vector3(0, 0, 0) + self.latest_touched_time = 0 + self.latest_touched_team = 0 + + def update(self, packet): + ball = packet.game_ball + self.location.data = [ball.physics.location.x, ball.physics.location.y, ball.physics.location.z] + self.velocity.data = [ball.physics.velocity.x, ball.physics.velocity.y, ball.physics.velocity.z] + self.latest_touched_time = ball.latest_touch.time_seconds + self.latest_touched_team = ball.latest_touch.team + + +class boost_object: + def __init__(self, index, location, large): + self.index = index + self.location = Vector3(location.x, location.y, location.z) + self.active = True + self.large = large + + def update(self, packet): + self.active = packet.game_boosts[self.index].is_active + + +class goal_object: + # This is a simple object that creates/holds goalpost locations for a given team (for soccer on standard maps only) + def __init__(self, team): + team = 1 if team == 1 else -1 + self.location = Vector3(0, team * 5100, 320) # center of goal line + # Posts are closer to x=750, but this allows the bot to be a little more accurate + self.left_post = Vector3(team * 850, team * 5100, 320) + self.right_post = Vector3(-team * 850, team * 5100, 320) + + +class game_object: + # This object holds information about the current match + def __init__(self): + self.time = 0 + self.time_remaining = 0 + self.overtime = False + self.round_active = False + self.kickoff = False + self.match_ended = False + + def update(self, packet): + game = packet.game_info + self.time = game.seconds_elapsed + self.time_remaining = game.game_time_remaining + self.overtime = game.is_overtime + self.round_active = game.is_round_active + self.kickoff = game.is_kickoff_pause + self.match_ended = game.is_match_ended + + +class Matrix3: + # The Matrix3's sole purpose is to convert roll, pitch, and yaw data into an orientation matrix + # An orientation matrix contains 3 Vector3's + # Matrix3[0] is the "forward" direction of a given car + # Matrix3[1] is the "left" direction of a given car + # Matrix3[2] is the "up" direction of a given car + # If you have a distance between the car and some object, ie ball.location - car.location, + # you can convert that to local coordinates by dotting it with this matrix + # ie: local_ball_location = Matrix3.dot(ball.location - car.location) + def __init__(self, pitch, yaw, roll): + cp = math.cos(pitch) + sp = math.sin(pitch) + cy = math.cos(yaw) + sy = math.sin(yaw) + cr = math.cos(roll) + sr = math.sin(roll) + self.data = ( + Vector3(cp * cy, cp * sy, sp), + Vector3(cy * sp * sr - cr * sy, sy * sp * sr + cr * cy, -cp * sr), + Vector3(-cr * cy * sp - sr * sy, -cr * sy * sp + sr * cy, cp * cr)) + self.forward, self.left, self.up = self.data + + def __getitem__(self, key): + return self.data[key] + + def dot(self, vector): + return Vector3(self.forward.dot(vector), self.left.dot(vector), self.up.dot(vector)) + + +class Vector3: + # This is the backbone of Gosling Utils. + # The Vector3 makes it easy to store positions, velocities, etc and perform vector math + # A Vector3 can be created with: + # - Anything that has a __getitem__ (lists, tuples, Vector3's, etc) + # - 3 numbers + # - A gametickpacket vector + def __init__(self, *args): + if hasattr(args[0], "__getitem__"): + self.data = list(args[0]) + elif isinstance(args[0], game_data_struct.Vector3): + self.data = [args[0].x, args[0].y, args[0].z] + elif isinstance(args[0], game_data_struct.Rotator): + self.data = [args[0].pitch, args[0].yaw, args[0].roll] + elif len(args) == 3: + self.data = list(args) + else: + raise TypeError("Vector3 unable to accept %s" % args) + + # Property functions allow you to use `Vector3.x` vs `Vector3[0]` + @property + def x(self): + return self.data[0] + + @x.setter + def x(self, value): + self.data[0] = value + + @property + def y(self): + return self.data[1] + + @y.setter + def y(self, value): + self.data[1] = value + + @property + def z(self): + return self.data[2] + + @z.setter + def z(self, value): + self.data[2] = value + + def __getitem__(self, key): + # To access a single value in a Vector3, treat it like a list + # ie: to get the first (x) value use: Vector3[0] + # The same works for setting values + return self.data[key] + + def __setitem__(self, key, value): + self.data[key] = value + + def __str__(self): + # Vector3's can be printed to console + return str(self.data) + + __repr__ = __str__ + + def __eq__(self, value): + # Vector3's can be compared with: + # - Another Vector3, in which case True will be returned if they have the same values + # - A single value, in which case True will be returned if the Vector's length matches the value + if hasattr(value, "__getitem__"): + return self.data == value.data + else: + return self.magnitude() == value + + # Vector3's support most operators (+-*/) + # If using an operator with another Vector3, each dimension will be independent + # ie x+x, y+y, z+z + # If using an operator with only a value, each dimension will be affected by that value + # ie x+v, y+v, z+v + def __add__(self, value): + if hasattr(value, "__getitem__"): + return Vector3(self[0] + value[0], self[1] + value[1], self[2] + value[2]) + return Vector3(self[0] + value, self[1] + value, self[2] + value) + + __radd__ = __add__ + + def __sub__(self, value): + if hasattr(value, "__getitem__"): + return Vector3(self[0] - value[0], self[1] - value[1], self[2] - value[2]) + return Vector3(self[0] - value, self[1] - value, self[2] - value) + + __rsub__ = __sub__ + + def __neg__(self): + return Vector3(-self[0], -self[1], -self[2]) + + def __mul__(self, value): + if hasattr(value, "__getitem__"): + return Vector3(self[0] * value[0], self[1] * value[1], self[2] * value[2]) + return Vector3(self[0] * value, self[1] * value, self[2] * value) + + __rmul__ = __mul__ + + def __truediv__(self, value): + if hasattr(value, "__getitem__"): + return Vector3(self[0] / value[0], self[1] / value[1], self[2] / value[2]) + return Vector3(self[0] / value, self[1] / value, self[2] / value) + + def __rtruediv__(self, value): + if hasattr(value, "__getitem__"): + return Vector3(value[0] / self[0], value[1] / self[1], value[2] / self[2]) + raise TypeError("unsupported rtruediv operands") + + def __abs__(self): + return Vector3(abs(self[0]), abs(self[1]), abs(self[2])) + + def magnitude(self): + # Magnitude() returns the length of the vector + return math.sqrt((self[0] * self[0]) + (self[1] * self[1]) + (self[2] * self[2])) + + def normalize(self, return_magnitude=False): + # Normalize() returns a Vector3 that shares the same direction but has a length of 1.0 + # Normalize(True) can also be used if you'd like the length of this Vector3 (used for optimization) + magnitude = self.magnitude() + if magnitude != 0: + if return_magnitude: + return Vector3(self[0] / magnitude, self[1] / magnitude, self[2] / magnitude), magnitude + return Vector3(self[0] / magnitude, self[1] / magnitude, self[2] / magnitude) + if return_magnitude: + return Vector3(0, 0, 0), 0 + return Vector3(0, 0, 0) + + # Linear algebra functions + def dot(self, value): + return self[0] * value[0] + self[1] * value[1] + self[2] * value[2] + + def cross(self, value): + # A .cross((0, 0, 1)) will rotate the vector counterclockwise by 90 degrees + return Vector3((self[1] * value[2]) - (self[2] * value[1]), (self[2] * value[0]) - (self[0] * value[2]), + (self[0] * value[1]) - (self[1] * value[0])) + + def flatten(self): + # Sets Z (Vector3[2]) to 0 + return Vector3(self[0], self[1], 0) + + def render(self): + # Returns a list with the x and y values, to be used with pygame + return [self[0], self[1]] + + def copy(self): + # Returns a copy of this Vector3 + return Vector3(self.data[:]) + + def angle(self, value): + # Returns the angle between this Vector3 and another Vector3 + return math.acos(round(self.flatten().normalize().dot(value.flatten().normalize()), 4)) + + def rotate(self, angle): + # Rotates this Vector3 by the given angle in radians + # Note that this is only 2D, in the x and y axis + return Vector3((math.cos(angle) * self[0]) - (math.sin(angle) * self[1]), + (math.sin(angle) * self[0]) + (math.cos(angle) * self[1]), self[2]) + + def clamp(self, start, end): + # Similar to integer clamping, Vector3's clamp() forces the Vector3's direction between a start and end Vector3 + # Such that Start < Vector3 < End in terms of clockwise rotation + # Note that this is only 2D, in the x and y axis + s = self.normalize() + right = s.dot(end.cross((0, 0, -1))) < 0 + left = s.dot(start.cross((0, 0, -1))) > 0 + if (right and left) if end.dot(start.cross((0, 0, -1))) > 0 else (right or left): + return self + if start.dot(s) < end.dot(s): + return end + return start diff --git a/RLBotPack/MadeByChatGPT/routines.py b/RLBotPack/MadeByChatGPT/routines.py new file mode 100644 index 00000000..f588f7f4 --- /dev/null +++ b/RLBotPack/MadeByChatGPT/routines.py @@ -0,0 +1,400 @@ +from utils import * + +#This file holds all of the mechanical tasks, called "routines", that the bot can do + +class atba(): + #An example routine that just drives towards the ball at max speed + def run(self, agent): + relative_target = agent.ball.location - agent.me.location + local_target = agent.me.local(relative_target) + defaultPD(agent, local_target) + defaultThrottle(agent, 2300) + +class aerial_shot(): + #Very similar to jump_shot(), but instead designed to hit targets above 300uu + #***This routine is a WIP*** It does not currently hit the ball very hard, nor does it like to be accurate above 600uu or so + def __init__(self, ball_location, intercept_time, shot_vector, ratio): + self.ball_location = ball_location + self.intercept_time = intercept_time + #The direction we intend to hit the ball in + self.shot_vector = shot_vector + #The point we hit the ball at + self.intercept = self.ball_location - (self.shot_vector * 125) + #dictates when (how late) we jump, much later than in jump_shot because we can take advantage of a double jump + self.jump_threshold = 600 + #what time we began our jump at + self.jump_time = 0 + #If we need a second jump we have to let go of the jump button for 3 frames, this counts how many frames we have let go for + self.counter = 0 + # used to keep track of how aligned the bot was with the shot vector when the routine starts + self.ratio = ratio + def run(self,agent): + raw_time_remaining = self.intercept_time - agent.time + #Capping raw_time_remaining above 0 to prevent division problems + time_remaining = cap(raw_time_remaining, 0.01, 10.0) + + car_to_ball = self.ball_location - agent.me.location + #whether we are to the left or right of the shot vector + side_of_shot = sign(self.shot_vector.cross((0,0,1)).dot(car_to_ball)) + + car_to_intercept = self.intercept - agent.me.location + car_to_intercept_perp = car_to_intercept.cross((0,0,side_of_shot)) #perpendicular + flat_distance_remaining = car_to_intercept.flatten().magnitude() + + speed_required = flat_distance_remaining / time_remaining + #When still on the ground we pretend gravity doesn't exist, for better or worse + acceleration_required = backsolve(self.intercept,agent.me,time_remaining, 325) + local_acceleration_required = agent.me.local(acceleration_required) + + #The adjustment causes the car to circle around the dodge point in an effort to line up with the shot vector + #The adjustment slowly decreases to 0 as the bot nears the time to jump + adjustment = car_to_intercept.angle(self.shot_vector) * flat_distance_remaining / 1.57 # size of adjustment + adjustment *= (cap(self.jump_threshold-(acceleration_required[2]),0.0,self.jump_threshold) / self.jump_threshold) # factoring in how close to jump we are + #we don't adjust the final target if we are already jumping + final_target = self.intercept + ((car_to_intercept_perp.normalize() * adjustment) if self.jump_time == 0 else 0) + + # Some extra adjustment to the final target to ensure it's inside the field and we don't try to drive through + # any goalposts to reach it + if abs(agent.me.location[1]) > 5150: final_target[0] = cap(final_target[0], -750, 750) + + local_final_target = agent.me.local(final_target - agent.me.location) + + #drawing debug lines to show the dodge point and final target (which differs due to the adjustment) + agent.line(agent.me.location,self.intercept) + agent.line(self.intercept-Vector3(0,0,100), self.intercept+Vector3(0,0,100),[255,0,0]) + agent.line(final_target-Vector3(0,0,100),final_target+Vector3(0,0,100),[0,255,0]) + + angles = defaultPD(agent,local_final_target) + + if self.jump_time == 0: + defaultThrottle(agent, speed_required) + agent.controller.boost = False if abs(angles[1]) > 0.3 or agent.me.airborne else agent.controller.boost + agent.controller.handbrake = True if abs(angles[1]) > 2.3 else agent.controller.handbrake + + velocity_required = car_to_intercept / time_remaining + good_slope = velocity_required[2] / cap(abs(velocity_required[0]) + abs(velocity_required[1]), 1, 10000) > 0.15 + if good_slope and (local_acceleration_required[2]) > self.jump_threshold and agent.me.velocity.flatten().normalize().dot(acceleration_required.flatten().normalize()) > 0.8: + # Switch into the jump when the upward acceleration required reaches our threshold. + # Hopefully we have aligned already... + self.jump_time = agent.time + else: + time_since_jump = agent.time - self.jump_time + + #While airborne we boost if we're within 30 degrees of our local acceleration requirement + if agent.me.airborne and local_acceleration_required.magnitude() * time_remaining > 90: + angles = defaultPD(agent, local_acceleration_required) + if abs(angles[0]) + abs(angles[1]) < 0.45: + agent.controller.boost = True + else: + final_target -= Vector3(0, 0, 45) + local_final_target = agent.me.local(final_target - agent.me.location) + angles = defaultPD(agent, local_final_target) + + if self.counter == 0 and (time_since_jump <= 0.2 and local_acceleration_required[2] > 0): + #hold the jump button up to 0.2 seconds to get the most acceleration from the first jump + agent.controller.jump = True + elif time_since_jump > 0.2 and self.counter < 3: + #Release the jump button for 3 ticks + agent.controller.jump = False + agent.controller.pitch = 0 + agent.controller.yaw = 0 + agent.controller.roll = 0 + self.counter += 1 + elif local_acceleration_required[2] > 300 and self.counter == 3: + #the acceleration from the second jump is instant, so we only do it for 1 frame + agent.controller.jump = True + agent.controller.pitch = 0 + agent.controller.yaw = 0 + agent.controller.roll = 0 + self.counter += 1 + + if raw_time_remaining < -0.25: + agent.pop() + agent.push(recovery()) + if not shot_valid(agent, self, 90): + agent.pop() + + +class flip(): + #Flip takes a vector in local coordinates and flips/dodges in that direction + #cancel causes the flip to cancel halfway through, which can be used to half-flip + def __init__(self,vector,cancel = False): + self.vector = vector.normalize() + self.pitch = abs(self.vector[0])* -sign(self.vector[0]) + self.yaw = abs(self.vector[1]) * sign(self.vector[1]) + self.cancel = cancel + #the time the jump began + self.time = -1 + #keeps track of the frames the jump button has been released + self.counter = 0 + def run(self,agent): + if self.time == -1: + elapsed = 0 + self.time = agent.time + else: + elapsed = agent.time - self.time + if elapsed < 0.15: + agent.controller.jump = True + elif elapsed >=0.15 and self.counter < 3: + agent.controller.jump = False + self.counter += 1 + elif elapsed < 0.3 or (not self.cancel and elapsed < 0.9): + agent.controller.jump = True + agent.controller.pitch = self.pitch + agent.controller.yaw = self.yaw + else: + agent.pop() + agent.push(recovery()) + +class goto(): + #Drives towards a designated (stationary) target + #Optional vector controls where the car should be pointing upon reaching the target + #TODO - slow down if target is inside our turn radius + def __init__(self, target, vector=None, direction = 1): + self.target = target + self.vector = vector + self.direction = direction + def run(self,agent): + car_to_target = self.target - agent.me.location + distance_remaining = car_to_target.flatten().magnitude() + + agent.line(self.target - Vector3(0,0,500),self.target + Vector3(0,0,500),[255,0,255]) + + if self.vector != None: + #See commands for adjustment in jump_shot or aerial for explanation + side_of_vector = sign(self.vector.cross((0,0,1)).dot(car_to_target)) + car_to_target_perp = car_to_target.cross((0,0,side_of_vector)).normalize() + adjustment = car_to_target.angle(self.vector) * distance_remaining / 3.14 + final_target = self.target + (car_to_target_perp * adjustment) + else: + final_target = self.target + + #Some adjustment to the final target to ensure it's inside the field and we don't try to dirve through any goalposts to reach it + if abs(agent.me.location[1]) > 5150: final_target[0] = cap(final_target[0],-750,750) + + local_target = agent.me.local(final_target - agent.me.location) + + angles = defaultPD(agent, local_target, self.direction) + defaultThrottle(agent, 2300, self.direction) + + agent.controller.boost = False + agent.controller.handbrake = True if abs(angles[1]) > 2.3 else agent.controller.handbrake + + velocity = 1+agent.me.velocity.magnitude() + if distance_remaining < 350: + agent.pop() + elif abs(angles[1]) < 0.05 and velocity > 600 and velocity < 2150 and distance_remaining / velocity > 2.0: + agent.push(flip(local_target)) + elif abs(angles[1]) > 2.8 and velocity < 200: + agent.push(flip(local_target,True)) + elif agent.me.airborne: + agent.push(recovery(self.target)) + +class goto_boost(): + #very similar to goto() but designed for grabbing boost + #if a target is provided the bot will try to be facing the target as it passes over the boost + def __init__(self,boost,target=None): + self.boost = boost + self.target = target + def run(self,agent): + car_to_boost = self.boost.location - agent.me.location + distance_remaining = car_to_boost.flatten().magnitude() + + agent.line(self.boost.location - Vector3(0,0,500),self.boost.location+ Vector3(0,0,500),[0,255,0]) + + if self.target != None: + vector = (self.target - self.boost.location).normalize() + side_of_vector = sign(vector.cross((0,0,1)).dot(car_to_boost)) + car_to_boost_perp = car_to_boost.cross((0,0,side_of_vector)).normalize() + adjustment = car_to_boost.angle(vector) * distance_remaining / 3.14 + final_target = self.boost.location + (car_to_boost_perp * adjustment) + car_to_target = (self.target - agent.me.location).magnitude() + else: + adjustment = 9999 + car_to_target = 0 + final_target = self.boost.location.copy() + + #Some adjustment to the final target to ensure it's inside the field and we don't try to dirve through any goalposts to reach it + if abs(agent.me.location[1]) > 5150: final_target[0] = cap(final_target[0],-750,750) + + local_target = agent.me.local(final_target - agent.me.location) + + angles = defaultPD(agent, local_target) + defaultThrottle(agent, 2300) + + agent.controller.boost = self.boost.large if abs(angles[1]) < 0.3 else False + agent.controller.handbrake = True if abs(angles[1]) > 2.3 else agent.controller.handbrake + + velocity = 1+agent.me.velocity.magnitude() + if self.boost.active == False or agent.me.boost >= 99.0 or distance_remaining < 350: + agent.pop() + elif agent.me.airborne: + agent.push(recovery(self.target)) + elif abs(angles[1]) < 0.05 and velocity > 600 and velocity < 2150 and (distance_remaining / velocity > 2.0 or (adjustment < 90 and car_to_target/velocity > 2.0)): + agent.push(flip(local_target)) + +class jump_shot(): + #Hits a target point at a target time towards a target direction + #Target must be no higher than 300uu unless you're feeling lucky + #TODO - speed + def __init__(self, ball_location, intercept_time, shot_vector, ratio, direction=1, speed=2300): + self.ball_location = ball_location + self.intercept_time = intercept_time + #The direction we intend to hit the ball in + self.shot_vector = shot_vector + #The point we dodge at + #173 is the 93uu ball radius + a bit more to account for the car's hitbox + self.dodge_point = self.ball_location - (self.shot_vector * 173) + #Ratio is how aligned the car is. Low ratios (<0.5) aren't likely to be hit properly + self.ratio = ratio + #whether the car should attempt this backwards + self.direction = direction + #Intercept speed not implemented + self.speed_desired = speed + #controls how soon car will jump based on acceleration required. max 584 + #bigger = later, which allows more time to align with shot vector + #smaller = sooner + self.jump_threshold = 400 + #Flags for what part of the routine we are in + self.jumping = False + self.dodging = False + self.counter = 0 + def run(self,agent): + raw_time_remaining = self.intercept_time - agent.time + #Capping raw_time_remaining above 0 to prevent division problems + time_remaining = cap(raw_time_remaining,0.001,10.0) + car_to_ball = self.ball_location - agent.me.location + #whether we are to the left or right of the shot vector + side_of_shot = sign(self.shot_vector.cross((0,0,1)).dot(car_to_ball)) + + car_to_dodge_point = self.dodge_point - agent.me.location + car_to_dodge_perp = car_to_dodge_point.cross((0,0,side_of_shot)) #perpendicular + distance_remaining = car_to_dodge_point.magnitude() + + speed_required = distance_remaining / time_remaining + acceleration_required = backsolve(self.dodge_point,agent.me,time_remaining,0 if not self.jumping else 650) + local_acceleration_required = agent.me.local(acceleration_required) + + #The adjustment causes the car to circle around the dodge point in an effort to line up with the shot vector + #The adjustment slowly decreases to 0 as the bot nears the time to jump + adjustment = car_to_dodge_point.angle(self.shot_vector) * distance_remaining / 2.0 #size of adjustment + adjustment *= (cap(self.jump_threshold-(acceleration_required[2]),0.0,self.jump_threshold) / self.jump_threshold) #factoring in how close to jump we are + #we don't adjust the final target if we are already jumping + final_target = self.dodge_point + ((car_to_dodge_perp.normalize() * adjustment) if not self.jumping else 0) + Vector3(0,0,50) + #Ensuring our target isn't too close to the sides of the field, where our car would get messed up by the radius of the curves + + #Some adjustment to the final target to ensure it's inside the field and we don't try to dirve through any goalposts to reach it + if abs(agent.me.location[1]) > 5150: final_target[0] = cap(final_target[0],-750,750) + + local_final_target = agent.me.local(final_target - agent.me.location) + + #drawing debug lines to show the dodge point and final target (which differs due to the adjustment) + agent.line(agent.me.location,self.dodge_point) + agent.line(self.dodge_point-Vector3(0,0,100), self.dodge_point+Vector3(0,0,100),[255,0,0]) + agent.line(final_target-Vector3(0,0,100),final_target+Vector3(0,0,100),[0,255,0]) + agent.line(agent.ball.location, agent.ball.location + (self.shot_vector * 300)) + + #Calling our drive utils to get us going towards the final target + angles = defaultPD(agent,local_final_target,self.direction) + defaultThrottle(agent, speed_required,self.direction) + + agent.line(agent.me.location, agent.me.location + (self.shot_vector*200), [255,255,255]) + + agent.controller.boost = False if abs(angles[1]) > 0.3 or agent.me.airborne else agent.controller.boost + agent.controller.handbrake = True if abs(angles[1]) > 2.3 and self.direction == 1 else agent.controller.handbrake + + if not self.jumping: + if raw_time_remaining <= 0.0 or (speed_required - 2300) * time_remaining > 60 or not shot_valid(agent,self): + #If we're out of time or not fast enough to be within 45 units of target at the intercept time, we pop + agent.pop() + if agent.me.airborne: + agent.push(recovery()) + elif local_acceleration_required[2] > self.jump_threshold and local_acceleration_required[2] > local_acceleration_required.flatten().magnitude(): + #Switch into the jump when the upward acceleration required reaches our threshold, and our lateral acceleration is negligible + self.jumping = True + else: + if (raw_time_remaining > 0.2 and not shot_valid(agent,self,150)) or raw_time_remaining <= -0.9 or (not agent.me.airborne and self.counter > 0): + agent.pop() + agent.push(recovery()) + elif self.counter == 0 and local_acceleration_required[2] > 0.0 and raw_time_remaining > 0.083: + #Initial jump to get airborne + we hold the jump button for extra power as required + agent.controller.jump = True + elif self.counter < 3: + #make sure we aren't jumping for at least 3 frames + agent.controller.jump = False + self.counter += 1 + elif raw_time_remaining <= 0.1 and raw_time_remaining > -0.9: + #dodge in the direction of the shot_vector + agent.controller.jump = True + if not self.dodging: + vector = agent.me.local(self.shot_vector) + self.p = abs(vector[0]) * -sign(vector[0]) + self.y = abs(vector[1]) * sign(vector[1]) * self.direction + self.dodging = True + #simulating a deadzone so that the dodge is more natural + agent.controller.pitch = self.p if abs(self.p) > 0.2 else 0 + agent.controller.yaw = self.y if abs(self.y) > 0.3 else 0 + +class kickoff(): + #A simple 1v1 kickoff that just drives up behind the ball and dodges + #misses the boost on the slight-offcenter kickoffs haha + def run(self,agent): + target = agent.ball.location + Vector3(0,200*side(agent.team),0) + local_target = agent.me.local(target - agent.me.location) + defaultPD(agent, local_target) + defaultThrottle(agent, 2300) + if local_target.magnitude() < 650: + agent.pop() + #flip towards opponent goal + agent.push(flip(agent.me.local(agent.foe_goal.location - agent.me.location))) + +class recovery(): + #Point towards our velocity vector and land upright, unless we aren't moving very fast + #A vector can be provided to control where the car points when it lands + def __init__(self,target=None): + self.target = target + def run(self, agent): + if self.target != None: + local_target = agent.me.local((self.target-agent.me.location).flatten()) + else: + local_target = agent.me.local(agent.me.velocity.flatten()) + + defaultPD(agent,local_target) + agent.controller.throttle = 1 + if not agent.me.airborne: + agent.pop() + +class short_shot(): + #This routine drives towards the ball and attempts to hit it towards a given target + #It does not require ball prediction and kinda guesses at where the ball will be on its own + def __init__(self,target): + self.target = target + def run(self,agent): + car_to_ball,distance = (agent.ball.location - agent.me.location).normalize(True) + ball_to_target = (self.target - agent.ball.location).normalize() + + relative_velocity = car_to_ball.dot(agent.me.velocity-agent.ball.velocity) + if relative_velocity != 0.0: + eta = cap(distance / cap(relative_velocity,400,2300),0.0, 1.5) + else: + eta = 1.5 + + #If we are approaching the ball from the wrong side the car will try to only hit the very edge of the ball + left_vector = car_to_ball.cross((0,0,1)) + right_vector = car_to_ball.cross((0,0,-1)) + target_vector = -ball_to_target.clamp(left_vector, right_vector) + final_target = agent.ball.location + (target_vector*(distance/2)) + + #Some adjustment to the final target to ensure we don't try to dirve through any goalposts to reach it + if abs(agent.me.location[1]) > 5150: final_target[0] = cap(final_target[0],-750,750) + + agent.line(final_target-Vector3(0,0,100),final_target+Vector3(0,0,100),[255,255,255]) + + angles = defaultPD(agent, agent.me.local(final_target-agent.me.location)) + defaultThrottle(agent, 2300 if distance > 1600 else 2300-cap(1600*abs(angles[1]),0,2050)) + agent.controller.boost = False if agent.me.airborne or abs(angles[1]) > 0.3 else agent.controller.boost + agent.controller.handbrake = True if abs(angles[1]) > 2.3 else agent.controller.handbrake + + if abs(angles[1]) < 0.05 and (eta < 0.45 or distance < 150): + agent.pop() + agent.push(flip(agent.me.local(car_to_ball))) diff --git a/RLBotPack/MadeByChatGPT/tools.py b/RLBotPack/MadeByChatGPT/tools.py new file mode 100644 index 00000000..4c455eaa --- /dev/null +++ b/RLBotPack/MadeByChatGPT/tools.py @@ -0,0 +1,83 @@ +from routines import * + +#This file is for strategic tools + +def find_hits(agent,targets): + #find_hits takes a dict of (left,right) target pairs and finds routines that could hit the ball between those target pairs + #find_hits is only meant for routines that require a defined intercept time/place in the future + #find_hits should not be called more than once in a given tick, as it has the potential to use an entire tick to calculate + + #Example Useage: + #targets = {"goal":(opponent_left_post,opponent_right_post), "anywhere_but_my_net":(my_right_post,my_left_post)} + #hits = find_hits(agent,targets) + #print(hits) + #>{"goal":[a ton of jump and aerial routines,in order from soonest to latest], "anywhere_but_my_net":[more routines and stuff]} + hits = {name:[] for name in targets} + struct = agent.get_ball_prediction_struct() + + #Begin looking at slices 0.25s into the future + #The number of slices + i = 15 + while i < struct.num_slices: + #Gather some data about the slice + intercept_time = struct.slices[i].game_seconds + time_remaining = intercept_time - agent.time + if time_remaining > 0: + ball_location = Vector3(struct.slices[i].physics.location) + ball_velocity = Vector3(struct.slices[i].physics.velocity).magnitude() + + if abs(ball_location[1]) > 5250: + break #abandon search if ball is scored at/after this point + + #determine the next slice we will look at, based on ball velocity (slower ball needs fewer slices) + i += 15 - cap(int(ball_velocity//150),0,13) + + car_to_ball = ball_location - agent.me.location + #Adding a True to a vector's normalize will have it also return the magnitude of the vector + direction, distance = car_to_ball.normalize(True) + + + #How far the car must turn in order to face the ball, for forward and reverse + forward_angle = direction.angle(agent.me.forward) + backward_angle = math.pi - forward_angle + + #Accounting for the average time it takes to turn and face the ball + #Backward is slightly longer as typically the car is moving forward and takes time to slow down + forward_time = time_remaining - (forward_angle * 0.318) + backward_time = time_remaining - (backward_angle * 0.418) + + #If the car only had to drive in a straight line, we ensure it has enough time to reach the ball (a few assumptions are made) + forward_flag = forward_time > 0.0 and (distance*1.025 / forward_time) < (2300 if agent.me.boost > 30 else max(1410, agent.me.velocity.flatten().magnitude())) + backward_flag = distance < 1500 and backward_time > 0.0 and (distance*1.05 / backward_time) < 1200 + + #Provided everything checks out, we begin to look at the target pairs + if forward_flag or backward_flag: + for pair in targets: + #First we correct the target coordinates to account for the ball's radius + #If fits == True, the ball can be scored between the target coordinates + left, right, fits = post_correction(ball_location, targets[pair][0], targets[pair][1]) + if fits: + #Now we find the easiest direction to hit the ball in order to land it between the target points + left_vector = (left - ball_location).normalize() + right_vector = (right - ball_location).normalize() + best_shot_vector = direction.clamp(left_vector, right_vector) + + #The slope represents how close the car is to the chosen vector, higher = better + slope = best_shot_vector.flatten().normalize().dot(car_to_ball.flatten().normalize()) * 0.5 + slope += distance * 0.0001 # the farther away we are, the easier it is to fix a bad slope + + if not in_field(ball_location - best_shot_vector * 200, 1): + # we don't want to try and hit the ball when it puts us against the wall + continue + + if forward_flag: + if ball_location[2] <= 300 and slope > 0.35: + hits[pair].append(jump_shot(ball_location, intercept_time, best_shot_vector, slope)) + elif slope > 0.7 and cap(ball_location[2]-400, 100, 2000) * 0.1 < agent.me.boost: + # if abs((car_to_ball / forward_time) - agent.me.velocity).magnitude() - 300 < 400 * forward_time: + hits[pair].append(aerial_shot(ball_location, intercept_time, best_shot_vector, slope)) + elif backward_flag and ball_location[2] < 300 and slope > 0.5: + hits[pair].append(jump_shot(ball_location, intercept_time, best_shot_vector, slope, -1)) + else: + i += 1 + return hits diff --git a/RLBotPack/MadeByChatGPT/utils.py b/RLBotPack/MadeByChatGPT/utils.py new file mode 100644 index 00000000..13971a81 --- /dev/null +++ b/RLBotPack/MadeByChatGPT/utils.py @@ -0,0 +1,136 @@ +import math +from objects import Vector3 + +#This file is for small utilities for math and movement + +def backsolve(target, car, time, gravity = 650): + #Finds the acceleration required for a car to reach a target in a specific amount of time + velocity_required = (target - car.location) / time + acceleration_required = velocity_required - car.velocity + acceleration_required[2] += (gravity * time) + return acceleration_required + +def cap(x, low, high): + #caps/clamps a number between a low and high value + if x < low: + return low + elif x > high: + return high + return x + +def defaultPD(agent, local_target, direction = 1.0): + #points the car towards a given local target. + #Direction can be changed to allow the car to steer towards a target while driving backwards + local_target *= direction + up = agent.me.local(Vector3(0,0,1)) #where "up" is in local coordinates + target_angles = [ + math.atan2(local_target[2],local_target[0]), #angle required to pitch towards target + math.atan2(local_target[1],local_target[0]), #angle required to yaw towards target + math.atan2(up[1],up[2])] #angle required to roll upright + #Once we have the angles we need to rotate, we feed them into PD loops to determing the controller inputs + agent.controller.steer = steerPD(target_angles[1],0) * direction + agent.controller.pitch = steerPD(target_angles[0],agent.me.angular_velocity[1]/4) + agent.controller.yaw = steerPD(target_angles[1],-agent.me.angular_velocity[2]/4) + agent.controller.roll = steerPD(target_angles[2],agent.me.angular_velocity[0]/2) + #Returns the angles, which can be useful for other purposes + return target_angles + +def defaultThrottle(agent, target_speed, direction = 1.0): + #accelerates the car to a desired speed using throttle and boost + car_speed = agent.me.local(agent.me.velocity)[0] + t = (target_speed * direction) - car_speed + agent.controller.throttle = cap((t**2) * sign(t)/1000, -1.0, 1.0) + agent.controller.boost = True if t > 150 and car_speed < 2275 and agent.controller.throttle == 1.0 else False + return car_speed + +def in_field(point,radius): + #determines if a point is inside the standard soccer field + point = Vector3(abs(point[0]),abs(point[1]),abs(point[2])) + if point[0] > 4080 - radius: + return False + elif point[1] > 5900 - radius: + return False + elif point[0] > 880 - radius and point[1] > 5105 - radius: + return False + elif point[0] > 2650 and point[1] > -point[0] + 8025 - radius: + return False + return True + +def post_correction(ball_location, left_target, right_target): + #this function returns target locations that are corrected to account for the ball's radius + # it also checks to make sure the ball can fit between the corrected locations + ball_radius = 110 # We purposely make this a bit larger so that our shots have a higher chance of success + goal_line_perp = (right_target - left_target).cross((0,0,1)) + left_adjusted = left_target + ((left_target - ball_location).normalize().cross((0,0,-1))*ball_radius) + right_adjusted = right_target + ((right_target - ball_location).normalize().cross((0,0,1))*ball_radius) + left_corrected = left_target if (left_adjusted-left_target).dot(goal_line_perp) > 0.0 else left_adjusted + right_corrected = right_target if (right_adjusted-right_target).dot(goal_line_perp) > 0.0 else right_adjusted + + new_goal_line, new_goal_width = (right_corrected - left_corrected).normalize(True) + new_goal_perp = (new_goal_line.cross((0, 0, 1))) + goal_center = left_corrected + (new_goal_line * new_goal_width * 0.5) + ball_to_goal = (goal_center - ball_location).normalize() + + ball_fits = new_goal_width * abs(new_goal_perp.dot(ball_to_goal)) > ball_radius * 2 + return left_corrected, right_corrected, ball_fits + +def quadratic(a,b,c): + #Returns the two roots of a quadratic + inside = math.sqrt((b*b) - (4*a*c)) + if a != 0: + return (-b + inside)/(2*a),(-b - inside)/(2*a) + else: + return -1,-1 + +def shot_valid(agent, shot, threshold = 45): + #Returns True if the ball is still where the shot anticipates it to be + #First finds the two closest slices in the ball prediction to shot's intercept_time + #threshold controls the tolerance we allow the ball to be off by + slices = agent.get_ball_prediction_struct().slices + soonest = 0 + latest = len(slices)-1 + while len(slices[soonest:latest+1]) > 2: + midpoint = (soonest+latest) // 2 + if slices[midpoint].game_seconds > shot.intercept_time: + latest = midpoint + else: + soonest = midpoint + #preparing to interpolate between the selected slices + dt = slices[latest].game_seconds - slices[soonest].game_seconds + time_from_soonest = shot.intercept_time - slices[soonest].game_seconds + slopes = (Vector3(slices[latest].physics.location) - Vector3(slices[soonest].physics.location)) * (1/dt) + #Determining exactly where the ball will be at the given shot's intercept_time + predicted_ball_location = Vector3(slices[soonest].physics.location) + (slopes * time_from_soonest) + #Comparing predicted location with where the shot expects the ball to be + return (shot.ball_location - predicted_ball_location).magnitude() < threshold + +def side(x): + #returns -1 for blue team and 1 for orange team + if x == 0: + return -1 + return 1 + +def sign(x): + #returns the sign of a number, -1, 0, +1 + if x < 0.0: + return -1 + elif x > 0.0: + return 1 + else: + return 0.0 + +def steerPD(angle, rate): + #A Proportional-Derivative control loop used for defaultPD + return cap(((35*(angle+rate))**3)/10, -1.0, 1.0) + +def lerp(a, b, t): + #Linearly interpolate from a to b using t + #For instance, when t == 0, a is returned, and when t == 1, b is returned + #Works for both numbers and Vector3s + return (b - a) * t + a + +def invlerp(a, b, v): + #Inverse linear interpolation from a to b with value v + #For instance, it returns 0 if v == a, and returns 1 if v == b, and returns 0.5 if v is exactly between a and b + #Works for both numbers and Vector3s + return (v - a)/(b - a) From d33fcd8296b243a55bcfe7e3655a90afdef53269 Mon Sep 17 00:00:00 2001 From: dedeprogames-official Date: Sun, 1 Feb 2026 15:55:05 -0300 Subject: [PATCH 2/2] Fix Madeby ChatGPT --- RLBotPack/MadeByChatGPT/appearance.cfg | 8 +- RLBotPack/MadeByChatGPT/bot.py | 651 ++++++++++++------------- 2 files changed, 308 insertions(+), 351 deletions(-) diff --git a/RLBotPack/MadeByChatGPT/appearance.cfg b/RLBotPack/MadeByChatGPT/appearance.cfg index 5b476bc8..30a54a5d 100644 --- a/RLBotPack/MadeByChatGPT/appearance.cfg +++ b/RLBotPack/MadeByChatGPT/appearance.cfg @@ -1,7 +1,3 @@ -# You don't have to manually edit this file! -# RLBotGUI has an appearance editor with a nice colorpicker, database of items and more! -# To open it up, simply click the (i) icon next to your bot's name and then click Edit Appearance - [Bot Loadout] # Primary Color selection team_color_id = 5 @@ -82,7 +78,7 @@ hat_paint_id = 0 # trails_paint_id trails_paint_id = 2 # goal_explosion_paint_id -goal_explosion_paint_id = 9 +goal_explosion_paint_id = 0 [Bot Paint Orange] # car_paint_id @@ -100,5 +96,5 @@ hat_paint_id = 0 # trails_paint_id trails_paint_id = 14 # goal_explosion_paint_id -goal_explosion_paint_id = 9 +goal_explosion_paint_id = 0 diff --git a/RLBotPack/MadeByChatGPT/bot.py b/RLBotPack/MadeByChatGPT/bot.py index 8b02d82b..f6d3ac1e 100644 --- a/RLBotPack/MadeByChatGPT/bot.py +++ b/RLBotPack/MadeByChatGPT/bot.py @@ -14,12 +14,10 @@ def _safe_line(agent, a, b, color=(255, 255, 255)): agent.line(a, b, color) return True except Exception: - # se sua versão usa renderer direto, você pode adaptar aqui return False def _draw_circle(agent, center, radius=250, color=(255, 255, 255), steps=24, z_override=None): - # desenha círculo aproximado com linhas pts = [] z = center.z if z_override is None else z_override for i in range(steps + 1): @@ -33,15 +31,12 @@ def _draw_circle(agent, center, radius=250, color=(255, 255, 255), steps=24, z_o def _draw_arrow(agent, start, end, color=(255, 255, 255), head_len=180, head_angle_deg=28): _safe_line(agent, start, end, color) - # cabeça da seta no "end" dir_vec = (end - start) if dir_vec.magnitude() < 1: return d = dir_vec.normalize() - # cria 2 vetores laterais no plano XY ang = math.radians(head_angle_deg) - # rotaciona d no plano XY left = Vector3( d.x * math.cos(ang) - d.y * math.sin(ang), d.x * math.sin(ang) + d.y * math.cos(ang), @@ -60,11 +55,6 @@ def _draw_arrow(agent, start, end, color=(255, 255, 255), head_len=180, head_ang def _draw_text_3d(agent, location, text, color=(255, 255, 255)): - """ - GoslingUtils normalmente tem renderer interno, mas algumas versões expõem helpers. - Tentamos várias opções sem quebrar. - """ - # tenta métodos comuns for attr in ["draw_string_3d", "string", "text", "draw_text_3d"]: fn = getattr(agent, attr, None) if callable(fn): @@ -74,16 +64,12 @@ def _draw_text_3d(agent, location, text, color=(255, 255, 255)): except Exception: pass - # tenta renderer renderer = getattr(agent, "renderer", None) if renderer is not None: for attr in ["draw_string_3d", "draw_string_2d", "draw_string"]: fn = getattr(renderer, attr, None) if callable(fn): try: - # muitas versões: draw_string_3d(x,y,z, scaleX, scaleY, text, color) - # outras: draw_string_3d(vec, scale, text, color) - # vamos tentar formatos try: fn(location, 1, 1, text, color) return True @@ -106,7 +92,6 @@ def _draw_text_3d(agent, location, text, color=(255, 255, 255)): GRAVITY_Z = -650.0 # Rocket League approx def _ballistic_ball_pos(ball_loc, ball_vel, dt): - # movimento simples sem bounce (serve p/ debug render) return Vector3( ball_loc.x + ball_vel.x * dt, ball_loc.y + ball_vel.y * dt, @@ -125,11 +110,9 @@ def _sample_ball_path(ball_loc, ball_vel, t_end, step=0.10): vel = ball_vel while t < t_end: pts.append(loc) - # integra loc = _ballistic_ball_pos(loc, vel, step) vel = _ballistic_ball_vel(vel, step) - # clamp chão (sem bounce real, mas evita "z negativa" no debug) if loc.z < 0: loc = Vector3(loc.x, loc.y, 0) vel = Vector3(vel.x, vel.y, 0) @@ -148,10 +131,6 @@ def _cap(x, lo, hi): def _eta_to_point(agent, car, point): - """ - ETA simples: distância / velocidade + penalidade por ângulo. - Bom o suficiente pra rotação/commit. - """ to = point - car.location dist = to.magnitude() if dist < 1: @@ -163,9 +142,7 @@ def _eta_to_point(agent, car, point): speed = max(300.0, car.velocity.magnitude()) base = dist / speed - # penaliza giro - turn_penalty = (angle / math.pi) * 1.2 # até +1.2s - # penaliza se estiver quase parado + turn_penalty = (angle / math.pi) * 1.2 slow_penalty = 0.3 if speed < 900 else 0.0 return base + turn_penalty + slow_penalty @@ -176,20 +153,19 @@ def _role_and_ranks(agent): Retorna: - my_role: "FIRST" / "SECOND" / "THIRD" - i_am_last_man: bool (mais perto do nosso gol) - - etas: lista (player_index, eta) - - goal_dists: lista (player_index, dist_own_goal) + - etas_sorted + - goal_sorted + - have_mates: bool (tem teammate além de mim) """ - friends = list(getattr(agent, "friends", [])) + # garante que friends não inclui "me" + raw_friends = list(getattr(agent, "friends", [])) + friends = [f for f in raw_friends if getattr(f, "index", None) != agent.me.index] + have_mates = len(friends) > 0 + players = [agent.me] + friends ball_loc = agent.ball.location own_goal = agent.friend_goal.location - # 1v1: não existe rotação 1/2/3 man; evitar comportamento "chuta e volta pro gol" sempre - if len(players) == 1: - eta = _eta_to_point(agent, agent.me, ball_loc) - gd = (agent.me.location - own_goal).magnitude() - return "FIRST", False, [(agent.me.index, eta)], [(agent.me.index, gd)] - etas = [] goal_dists = [] @@ -200,17 +176,18 @@ def _role_and_ranks(agent): etas_sorted = sorted(etas, key=lambda x: x[1]) goal_sorted = sorted(goal_dists, key=lambda x: x[1]) - # rank do meu index na lista de ETA my_idx = agent.me.index my_rank_eta = [i for i, (idx, _) in enumerate(etas_sorted) if idx == my_idx][0] my_rank_goal = [i for i, (idx, _) in enumerate(goal_sorted) if idx == my_idx][0] - # regra simples p/ role: - # - FIRST: menor ETA - # - THIRD: mais perto do próprio gol (last man) - # - SECOND: resto i_am_last_man = (my_rank_goal == 0) + # Ajuste específico (pra 1v1 e evitar "chuta -> recua gol -> chuta -> recua"): + # sem mates, você precisa ser FIRST (pressão) e só recuar quando threat alto. + if not have_mates: + my_role = "FIRST" + return my_role, i_am_last_man, etas_sorted, goal_sorted, have_mates + if my_rank_eta == 0 and not i_am_last_man: my_role = "FIRST" elif i_am_last_man: @@ -218,16 +195,10 @@ def _role_and_ranks(agent): else: my_role = "SECOND" - return my_role, i_am_last_man, etas_sorted, goal_sorted + return my_role, i_am_last_man, etas_sorted, goal_sorted, have_mates def _threat_level(agent): - """ - Heurística de perigo: - - bola indo pro nosso gol - - bola no nosso lado - - oponente com ETA menor que o nosso - """ s = side(agent.team) ball = agent.ball @@ -238,7 +209,6 @@ def _threat_level(agent): ball_towards_our_goal = (ball.velocity.y * s) < -200 ball_close_to_goal = (ball.location - own_goal).magnitude() < 2800 - # melhor ETA do inimigo vs nosso foe_best_eta = 999 for f in getattr(agent, "foes", []): foe_best_eta = min(foe_best_eta, _eta_to_point(agent, f, ball.location)) @@ -256,12 +226,6 @@ def _threat_level(agent): def _desired_approach_speed(agent, dist_to_ball, intercept_z, ball_vz): - """ - Controle inteligente (4): - - Se intercepto é "baixo" → pode chegar mais rápido - - Se bola tá caindo ou vai quicar → reduz pra chegar na hora do toque - """ - # base por distância if dist_to_ball > 2200: base = 2300 elif dist_to_ball > 1400: @@ -271,7 +235,6 @@ def _desired_approach_speed(agent, dist_to_ball, intercept_z, ball_vz): else: base = 1100 - # bola no ar / caindo: desacelera mais pra timing if intercept_z > 160: base -= 400 if ball_vz < -200 and intercept_z > 120: @@ -281,31 +244,22 @@ def _desired_approach_speed(agent, dist_to_ball, intercept_z, ball_vz): def _choose_shot_target(agent, shot_kind): - # pra render/seta: um ponto “pra onde” queremos chutar if shot_kind == "goal": return agent.foe_goal.location if shot_kind == "clear": - # limpa pro lado oposto do centro (evita devolver pro meio) s = side(agent.team) x = 3800 if agent.ball.location.x < 0 else -3800 - y = 1800 * s # empurrando pra frente + y = 1800 * s return Vector3(x, y, 0) + if shot_kind == "wallshot": + return agent.foe_goal.location return agent.foe_goal.location def _shot_score(agent, shot, shot_kind="goal"): - """ - (3) Scoring melhor: - - velocidade média até o intercepto - - alinhamento car->bola e bola->alvo - - penaliza altura se não tiver boost - - penaliza se for overcommit (threat alto) - """ me = agent.me - ball = agent.ball now = agent.time - # intercept_time / ball_location existem nos shots do GoslingUtils normalmente intercept_time = getattr(shot, "intercept_time", None) ball_loc = getattr(shot, "ball_location", None) @@ -316,7 +270,6 @@ def _shot_score(agent, shot, shot_kind="goal"): dist = (ball_loc - me.location).magnitude() avg_speed = dist / dt - # alinhamentos to_ball = (ball_loc - me.location) to_ball_n = to_ball.normalize() if to_ball.magnitude() > 1 else Vector3(0, 1, 0) @@ -324,28 +277,23 @@ def _shot_score(agent, shot, shot_kind="goal"): ball_to_target = (target_point - ball_loc) ball_to_target_n = ball_to_target.normalize() if ball_to_target.magnitude() > 1 else Vector3(0, 1, 0) - # quanto o carro está “apontado” pro intercepto (aprox por local.y) local = me.local(to_ball) - facing = _cap(local.y / (to_ball.magnitude() + 1e-6), -1, 1) # ~cos + facing = _cap(local.y / (to_ball.magnitude() + 1e-6), -1, 1) align = _cap(to_ball_n.dot(ball_to_target_n), -1, 1) - # ratio do find_hits (quando existir) ratio = getattr(shot, "ratio", 1.0) - # boost / altura height_penalty = 0.0 if ball_loc.z > 220 and me.boost < 40: height_penalty += 0.7 if ball_loc.z > 380 and me.boost < 60: height_penalty += 1.2 - # risco (se jogo perigoso, evita commits ruins) threat = _threat_level(agent) risk_penalty = 0.0 if threat > 0.9 and shot_kind == "goal": risk_penalty += 0.6 - # score final score = (avg_speed * 0.55) + (ratio * 900) + (align * 500) + (facing * 300) score -= (height_penalty * 900) score -= (risk_penalty * 800) @@ -374,212 +322,194 @@ def _shot_is_aerial(shot): return ("aerial" in name) or ("air" in name) +def _shot_is_wall(shot): + name = shot.__class__.__name__.lower() + return ("wall" in name) + + def _has_routine(name): return name in globals() and callable(globals()[name]) # ============================================================ -# (NOVO) AERIAL: escolher ponto futuro inteligente + tempo de chegar +# Aerial inteligente: escolher intercepto futuro com tempo de preparo # ============================================================ -def _aerial_time_needed(agent, intercept_loc): +def _aerial_required_time(agent, intercept_loc): """ - Estimativa simples do tempo mínimo pro aerial acontecer com chance: - - inclui "setup" (alinhar + pular) e viagem horizontal/vertical - - escala por boost baixo (mais lento/menos controle) + Estima um tempo mínimo pra conseguir chegar num intercepto aéreo. + (sem mudar sua base, só garantindo que o aerial não é "cedo demais") """ me = agent.me + to = intercept_loc - me.location + dist = to.magnitude() - dx = intercept_loc.x - me.location.x - dy = intercept_loc.y - me.location.y - d_xy = math.sqrt(dx * dx + dy * dy) - - h = max(0.0, intercept_loc.z - me.location.z) + # penaliza giro (se alvo muito de lado/atrás) + local = me.local(to) + angle = abs(math.atan2(local.x, max(1e-6, local.y))) # 0 alinhado + turn_pen = (angle / math.pi) * 0.55 - setup = 0.55 - travel = d_xy / 1700.0 - climb = max(0.0, h - 140.0) / 900.0 - margin = 0.10 + # penaliza altura + z = max(0.0, intercept_loc.z - me.location.z) + height_pen = _cap(z / 900.0, 0.0, 1.0) * 0.75 - t = setup + travel + climb + margin + # base por distância (aerial precisa de "tempo de spool") + base = dist / 1550.0 # ~ velocidade efetiva média no ar (aprox) + base = _cap(base, 0.55, 2.6) - # boost baixo => precisa de mais tempo (aéreo mais lento e menos correção) - if me.boost < 55: - t *= 1.10 - if me.boost < 35: - t *= 1.18 - if me.boost < 20: - t *= 1.28 + # se boost baixo, exige mais tempo + boost = me.boost + boost_pen = 0.35 if boost < 40 else 0.15 if boost < 60 else 0.0 - return t + return base + turn_pen + height_pen + boost_pen -def _pick_best_aerial(agent, shots_dict): +def _pick_best_aerial_shot(agent, shots_dict): """ - Escolhe um shot AÉREO que: - - esteja no ar (z alto) - - seja futuro o suficiente para dar tempo de chegar - - maximize o score existente + bônus por 'margem de tempo' + Escolhe um aerial que dê tempo real de chegar. + Procura tanto em "goal" quanto "clear". """ now = agent.time - me = agent.me - best = None best_kind = None best_score = -999999 for kind in ["goal", "clear"]: for shot in shots_dict.get(kind, []): - it = getattr(shot, "intercept_time", None) - il = getattr(shot, "ball_location", None) - if it is None or il is None: - continue - - # precisa estar no ar pra valer como aerial - if il.z < 260: + intercept_time = getattr(shot, "intercept_time", None) + intercept_loc = getattr(shot, "ball_location", None) + if intercept_time is None or intercept_loc is None: continue - dt = it - now - if dt <= 0.0: + dt = intercept_time - now + if dt < 0.1: continue - need = _aerial_time_needed(agent, il) - - # precisa ser um ponto futuro que dá tempo de chegar - if dt < (need + 0.08): + # só trata como aerial se for realmente alto ou class indicar + if not (_shot_is_aerial(shot) or intercept_loc.z > 250): continue - # muito longe no futuro costuma gerar aerial ruim/aleatório - if dt > 4.0: + req = _aerial_required_time(agent, intercept_loc) + if dt < req: continue - # score base do shot + bônus por ter folga de tempo (mas sem exagero) - base_sc = _shot_score(agent, shot, kind) - slack = _cap(dt - need, 0.0, 1.25) - sc = base_sc + slack * 220.0 - - # se boost muito baixo, exige ainda mais folga prática - if me.boost < 25 and slack < 0.35: - sc -= 600.0 - + sc = _shot_score(agent, shot, kind) + (dt * 40.0) # leve bias pra "mais tempo" quando tudo igual if sc > best_score: best = shot - best_score = sc best_kind = kind + best_score = sc return best, best_kind, best_score # ============================================================ -# Rotinas custom (kickoff cheat) – sem depender de rotinas do lib +# Wallshot: usar rotina do GoslingUtils se existir + render # ============================================================ -class CheatKickoff: +def _ball_near_wall(agent): + b = agent.ball.location + return (abs(b.x) > 3600) or (abs(b.y) > 4950) + + +def _try_push_wallshot(agent): """ - Vai “cheatar” no kickoff: avança até um ponto seguro e pronto pra pegar rebote. + Tenta acionar wallshot usando rotinas do GoslingUtils, sem quebrar se não existir. + Retorna True se conseguiu pushar algo. """ - def __init__(self, spot): - self.spot = spot + if not (_has_routine("wall_shot") or _has_routine("wallshot") or _has_routine("wallShot")): + return False - def run(self, agent): - relative = self.spot - agent.me.location - dist = relative.magnitude() - local = agent.me.local(relative) + target = agent.foe_goal.location - defaultPD(agent, local) - defaultThrottle(agent, _cap(dist * 2, 0, 2300)) - agent.controller.boost = (dist > 1400 and abs(local.x) < 250 and abs(local.y) > 800) + for nm in ["wall_shot", "wallshot", "wallShot"]: + fn = globals().get(nm, None) + if callable(fn): + try: + # tentativas de assinatura comuns + try: + agent.push(fn(target)) + return True + except Exception: + try: + agent.push(fn(agent.foe_goal.left_post, agent.foe_goal.right_post)) + return True + except Exception: + try: + agent.push(fn()) + return True + except Exception: + pass + except Exception: + pass - # termina quando chega - if dist < 250: - return True # pop - return False + return False # ============================================================ -# (NOVO) Wavedash (wayyshot) + render +# Kickoff: não termina ao tocar na bola; termina quando kickoff_flag acabar # ============================================================ -class WaveDash: +class KickoffWrapper: """ - Wavedash simples por temporização + detecção de descida. - - 1) jump curto - - 2) espera cair - - 3) dodge pra frente quando perto do chão + Mantém a lógica de kickoff rodando enquanto kickoff_flag estiver True. + Se a rotina kickoff() "acabar cedo" (ex: ao tocar a bola), a wrapper continua (recria) até o kickoff acabar. """ def __init__(self): - self.t0 = None - self.phase = 0 - self.dodge_t = None + self.inner = kickoff() if _has_routine("kickoff") else None def run(self, agent): - me = agent.me + if not agent.kickoff_flag: + return True # pop quando o kickoff acabar + + if self.inner is None: + # fallback simples: acelerar pra bola + to_ball = agent.ball.location - agent.me.location + local = agent.me.local(to_ball) + defaultPD(agent, local) + defaultThrottle(agent, 2300) + agent.controller.boost = True + return False - # se já não está no chão ao iniciar, não força - if self.t0 is None: - self.t0 = agent.time - self.phase = 0 - self.dodge_t = None - if not bool(getattr(me, "on_ground", True)): - return True + done = False + try: + done = bool(self.inner.run(agent)) + except Exception: + done = False - elapsed = agent.time - self.t0 - - # render - _draw_text_3d(agent, me.location + Vector3(0, 0, 140), "WAVEDASH", (255, 120, 255)) - - # defaults - agent.controller.throttle = 1.0 - agent.controller.boost = False - agent.controller.handbrake = False - agent.controller.steer = 0.0 - agent.controller.yaw = 0.0 - agent.controller.roll = 0.0 - - # fase 0: jump curto - if self.phase == 0: - agent.controller.jump = True - agent.controller.pitch = -1.0 - if elapsed > 0.08: - self.phase = 1 - return False + # Se o kickoff() encerrou cedo (ex: tocou na bola), recria e continua até kickoff_flag acabar + if done: + self.inner = kickoff() if _has_routine("kickoff") else None - # fase 1: solta jump e inclina pra frente - if self.phase == 1: - agent.controller.jump = False - agent.controller.pitch = -1.0 - if elapsed > 0.22: - self.phase = 2 - return False + return False # NUNCA pop enquanto kickoff_flag True - # fase 2: espera cair e tocar perto do chão - if self.phase == 2: - agent.controller.jump = False - agent.controller.pitch = -1.0 - - # quando estiver descendo e perto do chão, executa dodge - if me.location.z < 28 and me.velocity.z < -50: - self.phase = 3 - self.dodge_t = agent.time - # timeout (se algo der errado) - if elapsed > 1.20: - return True - return False - # fase 3: dodge curto pra frente - if self.phase == 3: - if self.dodge_t is None: - self.dodge_t = agent.time +class CheatKickoff: + """ + Cheat no kickoff: vai pro spot e espera. Só termina quando kickoff_flag acabar. + """ + def __init__(self, spot): + self.spot = spot - dt = agent.time - self.dodge_t - agent.controller.pitch = -1.0 - agent.controller.jump = (dt < 0.06) + def run(self, agent): + #if not agent.kickoff_flag: + # return True # termina só quando kickoff acaba - if dt > 0.14: - return True - return False + relative = self.spot - agent.me.location + dist = relative.magnitude() + local = agent.me.local(relative) - return True + defaultPD(agent, local) + + # chega rápido, mas ao chegar segura posição (não termina o kickoff) + #if dist > 250: + # defaultThrottle(agent, _cap(dist * 2, 0, 2300)) + # agent.controller.boost = (dist > 1400 and abs(local.x) < 250 and abs(local.y) > 800) + #else: + # defaultThrottle(agent, 0) + # agent.controller.boost = False + # agent.controller.handbrake = True # segura bem + + #return False # ============================================================ @@ -587,9 +517,15 @@ def run(self, agent): # ============================================================ class ExampleBot(GoslingAgent): + def initialize_agent(self): + super().initialize_agent() + self.allow_kickoff_reset = True # permite setar kickoff 1x por kickoff + self.kickoff_committed = False # já escolhemos papel nesse kickoff? + self.kickoff_role = None # "TAKER" ou "CHEAT" + def run(agent): # ====================== - # State inicial + # Memória (não mexe na base; só adiciona o mínimo necessário) # ====================== if not hasattr(agent, "dbg"): agent.dbg = { @@ -601,25 +537,24 @@ def run(agent): "shot_target": None } - # wavedash cooldown - if not hasattr(agent, "wd_last"): - agent.wd_last = -9999.0 + # usado pra evitar loop "chuta -> recua gol" especialmente em 1v1 + if not hasattr(agent, "last_commit_time"): + agent.last_commit_time = -999.0 + agent.last_commit_kind = None me = agent.me ball = agent.ball s = side(agent.team) # ====================== - # Role / rotação + # Role / rotação (1) - sem mudar o resto # ====================== - my_role, i_am_last_man, etas_sorted, goal_sorted = _role_and_ranks(agent) + my_role, i_am_last_man, etas_sorted, goal_sorted, have_mates = _role_and_ranks(agent) threat = _threat_level(agent) - - # texto da ação no carro (vai atualizando) agent.dbg["role"] = my_role # ====================== - # RENDER base: linhas de debug do seu exemplo + # RENDER base: linhas # ====================== try: left_test_a = Vector3(-4100 * s, ball.location.y, 0) @@ -633,30 +568,24 @@ def run(agent): # Se já tem rotina rodando, só render e sai # ====================== if len(agent.stack) > 0: - # render texto _draw_text_3d(agent, me.location + Vector3(0, 0, 120), f"[{agent.dbg['role']}] {agent.dbg['action']}", (255, 255, 255)) return # ====================== - # KICKOFF avançado (8) + # KICKOFF avançado (8) + FIX término (não ao toque) + suporta 1v1/2v2/3v3 # ====================== if agent.kickoff_flag: - # quem é o taker? menor ETA da equipe + # quem é o taker? menor ETA da equipe (entre friends + me) my_is_taker = (etas_sorted[0][0] == me.index) if my_is_taker: - agent.dbg["action"] = "KICKOFF: TAKING" - if _has_routine("kickoff"): - agent.push(kickoff()) - else: - # fallback: acelera reto pra bola - agent.controller.throttle = 1 - agent.controller.boost = True + agent.dbg["action"] = "KICKOFF: TAKING (WRAPPED)" + agent.push(KickoffWrapper()) else: # cheat spot: ligeiramente à frente do meio, do nosso lado cheat_spot = Vector3(0, -800 * s, 0) - agent.dbg["action"] = "KICKOFF: CHEAT" + agent.dbg["action"] = "KICKOFF: CHEAT (HOLD)" agent.push(CheatKickoff(cheat_spot)) _draw_text_3d(agent, me.location + Vector3(0, 0, 120), @@ -664,7 +593,7 @@ def run(agent): return # ====================== - # Targets e shots (3) + Aerials (6) + # Targets e shots (3) + Aerials inteligentes (6) + Wallshot # ====================== targets = { "goal": (agent.foe_goal.left_post, agent.foe_goal.right_post), @@ -675,14 +604,21 @@ def run(agent): best_shot, best_kind, best_score = _pick_best_shot(agent, shots) + # Aerial melhor: escolhe um ponto futuro que dá tempo (pedido) + best_aerial, best_aerial_kind, best_aerial_score = _pick_best_aerial_shot(agent, shots) + # ====================== # Intercept info p/ render (círculo / trajeto / seta) # ====================== intercept_time = None intercept_loc = None - if best_shot is not None: - intercept_time = getattr(best_shot, "intercept_time", None) - intercept_loc = getattr(best_shot, "ball_location", None) + + # se escolher aerial, render deve usar o intercept dele + chosen_for_render = best_aerial if best_aerial is not None else best_shot + + if chosen_for_render is not None: + intercept_time = getattr(chosen_for_render, "intercept_time", None) + intercept_loc = getattr(chosen_for_render, "ball_location", None) agent.dbg["intercept_t"] = intercept_time agent.dbg["intercept_loc"] = intercept_loc @@ -690,137 +626,157 @@ def run(agent): agent.dbg["shot_target"] = _choose_shot_target(agent, best_kind) if best_kind else None # ====================== - # DECISÃO PRINCIPAL + # DECISÃO PRINCIPAL (mantém sua base; só corrige o loop e melhora aerial/wallshot) # ====================== - # Regra de commit por rotação: - # - THIRD (last man) só comita em clear/save ou se ameaça baixa + # commit rule por rotação: can_commit_attack = (my_role != "THIRD") or (threat < 0.55) - # Se existe shot bom - if best_shot is not None: - is_aerial = _shot_is_aerial(best_shot) - z = intercept_loc.z if intercept_loc else 0 + # Ajuste importante: + # O bloco "defesa porque role THIRD" só faz sentido quando EXISTE teammate. + # Em 1v1, isso causava o loop "chuta -> recua gol". + third_role_matters = have_mates - # (6) Aerial: agora escolhe ponto FUTURO inteligente que dá tempo de chegar - if is_aerial or z > 250: - aerial_shot, aerial_kind, aerial_sc = _pick_best_aerial(agent, shots) + # (1v1) Evitar "chuta -> recua" logo após commit: pequena janela de pressão (sem mudar resto) + just_committed = (agent.time - agent.last_commit_time) < 1.20 - if aerial_shot is not None and me.boost >= 35 and can_commit_attack: - # atualiza debug/intercept para render apontar pro ponto certo do aerial - a_it = getattr(aerial_shot, "intercept_time", None) - a_il = getattr(aerial_shot, "ball_location", None) + # ====================== + # WALLSHOT (pedido) - tenta quando bola está na parede e não estamos em perigo alto + # ====================== + if len(agent.stack) < 1 and _ball_near_wall(agent) and threat < 0.95: + # se temos boost razoável e podemos comitar (ou é 1v1 e threat baixo), tenta wallshot + if me.boost >= 25 and (can_commit_attack or not have_mates): + if _try_push_wallshot(agent): + agent.dbg["action"] = "SHOT: WALLSHOT" + agent.last_commit_time = agent.time + agent.last_commit_kind = "wallshot" + # forçar render de alvo wallshot + agent.dbg["shot_kind"] = "wallshot" + agent.dbg["shot_target"] = _choose_shot_target(agent, "wallshot") - agent.dbg["intercept_t"] = a_it - agent.dbg["intercept_loc"] = a_il - agent.dbg["shot_kind"] = aerial_kind - agent.dbg["shot_target"] = _choose_shot_target(agent, aerial_kind) if aerial_kind else None + # ====================== + # AERIAL (pedido): só usa se o aerial escolhido dá tempo + # ====================== + if len(agent.stack) < 1 and best_aerial is not None: + # usa aerial apenas se faz sentido (boost + commit ok) e não estamos em perigo absurdo + if me.boost >= 45 and (can_commit_attack or threat < 0.70): + agent.dbg["action"] = f"SHOT: AERIAL (SMART) ({best_aerial_kind})" + agent.push(best_aerial) + agent.last_commit_time = agent.time + agent.last_commit_kind = "aerial" + agent.dbg["shot_kind"] = best_aerial_kind + agent.dbg["shot_target"] = _choose_shot_target(agent, best_aerial_kind) - agent.dbg["action"] = f"SHOT: AERIAL ({aerial_kind})" - agent.push(aerial_shot) - else: - agent.dbg["action"] = "HOLD: NO GOOD AERIAL POINT / SAFE ROTATION" + # ====================== + # GROUND SHOT / CLEAR (como antes) + # ====================== + if len(agent.stack) < 1: + if best_shot is not None: + z = getattr(best_shot, "ball_location", Vector3(0, 0, 0)).z - else: - # (4) Speed control na aproximação - # Se o shot for cedo demais / perto demais, às vezes a gente chega “antes”. - # Então só comita se score ok e role permite. - if best_score > 500 and can_commit_attack: - agent.dbg["action"] = f"SHOT: GROUND ({best_kind})" + # se é um shot do tipo wall já vindo do find_hits, também conta + if _shot_is_wall(best_shot) and threat < 0.95: + agent.dbg["action"] = f"SHOT: WALL (HIT) ({best_kind})" agent.push(best_shot) + agent.last_commit_time = agent.time + agent.last_commit_kind = "wall" else: - agent.dbg["action"] = "POSITION: WAIT / SUPPORT" + if best_score > 500 and (can_commit_attack or not have_mates): + agent.dbg["action"] = f"SHOT: GROUND ({best_kind})" + agent.push(best_shot) + agent.last_commit_time = agent.time + agent.last_commit_kind = best_kind + else: + agent.dbg["action"] = "POSITION: WAIT / SUPPORT" - # Sem shot: defesa / rotação / boost + # ====================== + # Sem rotina (ou após decidir não chutar): defesa / boost / support + # ====================== if len(agent.stack) < 1: - # Perigo alto => defender - if threat > 0.85 or my_role == "THIRD": - # tenta usar save do GoslingUtils se existir e bola indo pro gol - ball_towards_our_goal = (ball.velocity.y * s) < -150 - ball_close_goal = (ball.location - agent.friend_goal.location).magnitude() < 3200 - - if ball_towards_our_goal and ball_close_goal and _has_routine("save"): - agent.dbg["action"] = "DEFENSE: SAVE ROUTINE" - agent.push(save(agent.friend_goal.location)) - else: - # fallback: ir pro far post com velocidade controlada - left_dist = (agent.friend_goal.left_post - me.location).magnitude() - right_dist = (agent.friend_goal.right_post - me.location).magnitude() - target = agent.friend_goal.left_post if left_dist < right_dist else agent.friend_goal.right_post + # 1v1: se acabou de chutar e threat não é alto, não recuar pro gol — faz pressão/posição no meio + if (not have_mates) and just_committed and threat < 0.95: + agent.dbg["action"] = "PRESSURE: POST-SHOT MID" + # ponto de pressão: um pouco atrás da bola (pra não overcommit), mas não no gol + pressure = Vector3(ball.location.x, ball.location.y - (1200 * s), 0) + pressure = Vector3(_cap(pressure.x, -3800, 3800), _cap(pressure.y, -3500, 3500), 0) + + relative = pressure - me.location + dist = relative.magnitude() + local = me.local(relative) + defaultPD(agent, local) - # move um pouco pra fora do gol (far post + offset) - target = Vector3(target.x, target.y, 0) + # (4) speed control + não gastar boost à toa + desired_speed = _cap(dist * 2.0, 0, 2300) + dist_ball = (ball.location - me.location).magnitude() + desired_speed = min(desired_speed, _desired_approach_speed(agent, dist_ball, ball.location.z, ball.velocity.z)) + defaultThrottle(agent, desired_speed) + agent.controller.boost = (dist > 2400 and abs(local.x) < 240 and abs(local.y) > 900 and me.boost > 20) - agent.dbg["action"] = "DEFENSE: FAR POST / SHADOW" + else: + # DEFESA: antes era "threat > 0.85 or my_role == THIRD" + # Agora só considera THIRD como defesa obrigatória quando há teammate (2v2/3v3). + if threat > 0.85 or (my_role == "THIRD" and third_role_matters): + ball_towards_our_goal = (ball.velocity.y * s) < -150 + ball_close_goal = (ball.location - agent.friend_goal.location).magnitude() < 3200 + + if ball_towards_our_goal and ball_close_goal and _has_routine("save"): + agent.dbg["action"] = "DEFENSE: SAVE ROUTINE" + agent.push(save(agent.friend_goal.location)) + else: + left_dist = (agent.friend_goal.left_post - me.location).magnitude() + right_dist = (agent.friend_goal.right_post - me.location).magnitude() + target = agent.friend_goal.left_post if left_dist < right_dist else agent.friend_goal.right_post + target = Vector3(target.x, target.y, 0) - relative = target - me.location - dist = relative.magnitude() - local = me.local(relative) + agent.dbg["action"] = "DEFENSE: FAR POST / SHADOW" - # (NOVO) wavedash quando sem boost e longe, pra acelerar sem gastar boost - if (me.boost < 12 and dist > 2600 and me.velocity.magnitude() < 1050 and - abs(local.x) < 220 and (agent.time - agent.wd_last) > 2.2 and - bool(getattr(me, "on_ground", True))): - agent.wd_last = agent.time - agent.dbg["action"] = "MOVE: WAVEDASH" - agent.push(WaveDash()) - else: + relative = target - me.location + dist = relative.magnitude() + local = me.local(relative) defaultPD(agent, local) - # velocidade segura (não torrar boost) speed = _cap(dist * 1.8, 0, 2000) defaultThrottle(agent, speed) agent.controller.boost = (dist > 2600 and abs(local.x) < 200 and abs(local.y) > 900 and me.boost > 30) - # Boost se estiver baixo e não for last man - elif me.boost < 30: - best_boost = None - best_val = -1.0 - for boost in agent.boosts: - if not boost.active: - continue - if not boost.large: - continue - - me_to_boost = (boost.location - me.location).normalize() - boost_to_goal = (agent.friend_goal.location - boost.location).normalize() - - val = boost_to_goal.dot(me_to_boost) - if val > best_val: - best_val = val - best_boost = boost - - if best_boost is not None and _has_routine("goto_boost"): - agent.dbg["action"] = "RESOURCE: GET BOOST (LARGE)" - agent.push(goto_boost(best_boost, agent.friend_goal.location)) - - # Support: posicionar mid/back post “inteligente” - else: - agent.dbg["action"] = "ROTATE: SUPPORT MID" - # spot de suporte: entre bola e nosso gol, um pouco pra trás - goal = agent.friend_goal.location - ball_to_goal = (goal - ball.location).normalize() - support = ball.location + ball_to_goal * 2200 # 2200 atrás da bola - support = Vector3(_cap(support.x, -3800, 3800), _cap(support.y, -5100, 5100), 0) - - relative = support - me.location - dist = relative.magnitude() - local = me.local(relative) - - # (NOVO) wavedash quando sem boost e longe, pra acelerar sem gastar boost - if (me.boost < 12 and dist > 2800 and me.velocity.magnitude() < 1050 and - abs(local.x) < 220 and (agent.time - agent.wd_last) > 2.2 and - bool(getattr(me, "on_ground", True))): - agent.wd_last = agent.time - agent.dbg["action"] = "MOVE: WAVEDASH" - agent.push(WaveDash()) + # Boost se estiver baixo e não for last man (em 1v1 isso vale também) + elif me.boost < 30: + best_boost = None + best_val = -1.0 + for boost in agent.boosts: + if not boost.active: + continue + if not boost.large: + continue + + me_to_boost = (boost.location - me.location).normalize() + boost_to_goal = (agent.friend_goal.location - boost.location).normalize() + + val = boost_to_goal.dot(me_to_boost) + if val > best_val: + best_val = val + best_boost = boost + + if best_boost is not None and _has_routine("goto_boost"): + agent.dbg["action"] = "RESOURCE: GET BOOST (LARGE)" + agent.push(goto_boost(best_boost, agent.friend_goal.location)) + + # Support: mid/back post “inteligente” else: + agent.dbg["action"] = "ROTATE: SUPPORT MID" + goal = agent.friend_goal.location + ball_to_goal = (goal - ball.location).normalize() + support = ball.location + ball_to_goal * 2200 + support = Vector3(_cap(support.x, -3800, 3800), _cap(support.y, -5100, 5100), 0) + + relative = support - me.location + dist = relative.magnitude() + local = me.local(relative) defaultPD(agent, local) - # (4) speed control: se estiver “chegando na bola” (perto), desacelera desired_speed = _cap(dist * 2.0, 0, 2300) - # se muito perto da bola, ajusta timing + # (4) speed control dist_ball = (ball.location - me.location).magnitude() desired_speed = min(desired_speed, _desired_approach_speed(agent, dist_ball, ball.location.z, ball.velocity.z)) @@ -828,14 +784,12 @@ def run(agent): agent.controller.boost = (dist > 2600 and abs(local.x) < 240 and abs(local.y) > 900 and me.boost > 20) # ============================================================ - # RENDER AVANÇADO (pedido do usuário) + # RENDER AVANÇADO (com wallshot também) # ============================================================ - # texto no carro _draw_text_3d(agent, me.location + Vector3(0, 0, 120), f"[{agent.dbg['role']}] {agent.dbg['action']} | threat={threat:.2f}", (255, 255, 255)) - # se temos intercepto, desenha: it = agent.dbg["intercept_t"] il = agent.dbg["intercept_loc"] st = agent.dbg["shot_target"] @@ -848,7 +802,6 @@ def run(agent): # 2) seta indicando direção do chute (alvo) if st is not None: arrow_end = Vector3(st.x, st.y, 0) - # seta começa no círculo e aponta pro alvo (no chão) _draw_arrow(agent, ground + Vector3(0, 0, 10), arrow_end + Vector3(0, 0, 10), color=(255, 180, 80)) # 3) path da bola até o intercepto (no ar) @@ -856,12 +809,20 @@ def run(agent): if dt > 0.05: pts = _sample_ball_path(ball.location, ball.velocity, min(dt, 4.0), step=0.10) - # desenha polyline + # cor diferente se for wallshot + is_wall_context = ("WALL" in agent.dbg["action"].upper()) + path_color = (255, 200, 255) if is_wall_context else (180, 255, 180) + for i in range(len(pts) - 1): - _safe_line(agent, pts[i], pts[i + 1], (180, 255, 180)) + _safe_line(agent, pts[i], pts[i + 1], path_color) # marca ponto exato do toque (il) _draw_circle(agent, il, radius=120, color=(255, 255, 255), steps=18, z_override=il.z) - # linha do carro até o intercepto (visual “vou pra lá”) + # linha do carro até o intercepto _safe_line(agent, me.location, il, (255, 255, 0)) + + # render extra wallshot: linha vertical até o chão + círculo no ponto da parede + if is_wall_context and il.z > 80: + _safe_line(agent, Vector3(il.x, il.y, 0), il, (255, 200, 255)) + _draw_circle(agent, Vector3(il.x, il.y, 0), radius=140, color=(255, 200, 255), steps=18, z_override=10)