From 2cada61810d2e9e42f364dfb522fbbae2e486a58 Mon Sep 17 00:00:00 2001 From: rivques <38469076+rivques@users.noreply.github.com> Date: Tue, 18 May 2021 14:49:38 -0400 Subject: [PATCH 1/2] Update ElkBot --- RLBotPack/ElkBot/.gitignore | 5 +- RLBotPack/ElkBot/ExampleBot.cfg | 2 - RLBotPack/ElkBot/ExampleBot.py | 51 +-- RLBotPack/ElkBot/custom_routines.py | 6 + RLBotPack/ElkBot/requirements.txt | 4 +- RLBotPack/ElkBot/util/agent.py | 213 ++++++---- RLBotPack/ElkBot/util/routines.py | 576 +++++++++++++++++----------- RLBotPack/ElkBot/util/tools.py | 12 +- RLBotPack/ElkBot/util/utils.py | 141 +++++-- 9 files changed, 637 insertions(+), 373 deletions(-) diff --git a/RLBotPack/ElkBot/.gitignore b/RLBotPack/ElkBot/.gitignore index f7c807f7..41641bf8 100644 --- a/RLBotPack/ElkBot/.gitignore +++ b/RLBotPack/ElkBot/.gitignore @@ -129,4 +129,7 @@ dmypy.json .pyre/ # VS Code -.vscode/ \ No newline at end of file +.vscode/ + +# Bot errors +.errors/ \ No newline at end of file diff --git a/RLBotPack/ElkBot/ExampleBot.cfg b/RLBotPack/ElkBot/ExampleBot.cfg index 0ff7a37c..0235f029 100644 --- a/RLBotPack/ElkBot/ExampleBot.cfg +++ b/RLBotPack/ElkBot/ExampleBot.cfg @@ -28,5 +28,3 @@ github = https://github.com/rivques/ElkBot # Programming language language = Python - -tags = 1v1, teamplay diff --git a/RLBotPack/ElkBot/ExampleBot.py b/RLBotPack/ElkBot/ExampleBot.py index 2449bd12..230609ee 100644 --- a/RLBotPack/ElkBot/ExampleBot.py +++ b/RLBotPack/ElkBot/ExampleBot.py @@ -16,6 +16,8 @@ def initialize_agent(self): super().initialize_agent() self.state = None self.do_debug = True + self.RTG_flip_time = 0 + def run(self): my_goal_to_ball, my_ball_distance = (self.ball.location-self.friend_goal.location).normalize(True) goal_to_me = self.me.location-self.friend_goal.location @@ -152,8 +154,17 @@ def run(self): self.controller.boost = False if abs(angles[1]) > 0.5 or self.me.airborne else self.controller.boost self.controller.handbrake = True if abs(angles[1]) > 2.8 else False else: - if self.is_clear(): - self.push(retreat()) + if not (side(self.team) * self.me.location.y > 5120) and not (self.me.location-Vector(-893*sign(self.ball.location.x), 5000*side(self.team))).magnitude() < 200: + relative_target = Vector(-893*sign(self.ball.location.x), 5000*side(self.team)) - self.me.location + angles, vel = defaultDrive(self, 2300, self.me.local(relative_target)) + if angles[1] < 0.75 and relative_target.magnitude() > 750 and 500 < self.me.velocity.magnitude() < 1000 and self.time - self.RTG_flip_time > 2: + self.push(flip(self.me.local(relative_target))) + self.RTG_flip_time = self.time + else: + self.controller.boost = False if abs(angles[1]) > 0.5 or self.me.airborne else self.controller.boost + self.controller.handbrake = True if abs(angles[1]) > 2.8 else False + else: + self.state += ' (In goal)' if self.do_debug: #this overdoes the rendering, needs fixing @@ -213,42 +224,6 @@ def find_hits(self, targets, test_for_slow_ground_shots=True, test_for_slow_jump output[name] = shot return output - def get_tmcp_action(self): - if self.is_clear(): - if 'RTG' in self.state: - return { - "type": "DEFEND" - } - return { - "type": "WAIT", - "ready": -1 - } - - stack_routine_name = self.stack[0].__class__.__name__ - - if stack_routine_name in {'Aerial', 'jump_shot', 'ground_shot', 'double_jump', 'short_shot'}: - return { - "type": "BALL", - "time": -1 if stack_routine_name == 'short_shot' else self.stack[0].intercept_time - } - if stack_routine_name == "goto_boost": - return { - "type": "BOOST", - "target": self.stack[0].boost.index - } - - if stack_routine_name == 'retreat': - return { - "type": "WAIT", - "ready": -1 - } - - # by default, VirxERLU can't demo bots - return { - "type": "WAIT", - "ready": self.get_minimum_game_time_to_ball() - } - def get_minimum_game_time_to_ball(self): shot = find_any_shot(self) return -1 if shot is None else shot.intercept_time \ No newline at end of file diff --git a/RLBotPack/ElkBot/custom_routines.py b/RLBotPack/ElkBot/custom_routines.py index c2835cd1..390028b2 100644 --- a/RLBotPack/ElkBot/custom_routines.py +++ b/RLBotPack/ElkBot/custom_routines.py @@ -46,26 +46,32 @@ class speed_flip_kickoff: def __init__(self): self.old_boost = 34 self.stage = 0 + print("Speedflipping!") def run(self, agent: VirxERLU): + print("running speedflip routine") if self.stage == 0: agent.controller.boost = True if agent.me.boost > self.old_boost: self.stage = 1 + agent.print(f"Next stage: {self.stage}") else: self.old_boost = agent.me.boost elif self.stage == 1: angles = defaultPD(agent, agent.me.local_location(Vector(110*sign(agent.me.location.x)))) if abs(angles[1]) < 0.1: self.stage = 2 + agent.print(f"Next stage: {self.stage}") elif self.stage == 2: agent.push(speed_flip()) self.stage = 3 + agent.print(f"Next stage: {self.stage}") elif self.stage == 3: # TODO do a second flip is the opponent is speedflipping as well if False: agent.push(flip(agent.me.local_location(Vector(120*sign(agent.me.location.x))))) self.stage = 4 + agent.print(f"Next stage: {self.stage}") elif self.stage == 4: agent.pop() \ No newline at end of file diff --git a/RLBotPack/ElkBot/requirements.txt b/RLBotPack/ElkBot/requirements.txt index e939aa89..026864b0 100644 --- a/RLBotPack/ElkBot/requirements.txt +++ b/RLBotPack/ElkBot/requirements.txt @@ -1,3 +1,3 @@ numpy -rlbot -VirxERLU-CLib>=1.15.2,<2.0.0 \ No newline at end of file +rlbot==1.* +VirxERLU-CLib==2.* \ No newline at end of file diff --git a/RLBotPack/ElkBot/util/agent.py b/RLBotPack/ElkBot/util/agent.py index 17344503..6bbfb843 100644 --- a/RLBotPack/ElkBot/util/agent.py +++ b/RLBotPack/ElkBot/util/agent.py @@ -12,26 +12,27 @@ import numpy as np from rlbot.agents.base_agent import SimpleControllerState from rlbot.agents.standalone.standalone_bot import StandaloneBot, run_bot +from rlbot.messages.flat import MatchSettings +from rlbot.utils.structures.game_data_struct import GameTickPacket # If you're putting your bot in the botpack, or submitting to a tournament, make this True! -TOURNAMENT_MODE = True +TOURNAMENT_MODE = False # Make False to enable hot reloading, at the cost of the GUI EXTRA_DEBUGGING = False if not TOURNAMENT_MODE and EXTRA_DEBUGGING: from gui import Gui - from match_comms import MatchComms class VirxERLU(StandaloneBot): # Massive thanks to ddthj/GoslingAgent (GitHub repo) for the basis of VirxERLU # VirxERLU on VirxEC Showcase -> https://virxerlu.virxcase.dev/ # Wiki -> https://github.com/VirxEC/VirxERLU/wiki - def initialize_agent(self): + def __init__(self, name, team, index): + super().__init__(name, team, index) self.tournament = TOURNAMENT_MODE self.extra_debugging = EXTRA_DEBUGGING - self.startup_time = time_ns() self.true_name = re.split(r' \(\d+\)$', self.name)[0] self.debug = [[], []] @@ -39,17 +40,25 @@ def initialize_agent(self): self.debug_lines = True self.debug_3d_bool = True self.debug_stack_bool = True - self.debug_2d_bool = self.true_name == self.name + self.debug_2d_bool = self.name == self.true_name self.show_coords = False self.debug_ball_path = False self.debug_ball_path_precision = 10 self.disable_driving = False + def initialize_agent(self): + self.startup_time = time_ns() + T = datetime.now() T = T.strftime("%Y-%m-%d %H;%M") + error_folder = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "errors") + + if not os.path.isdir(error_folder): + os.mkdir(error_folder) + self.traceback_file = ( - os.getcwd(), + os.path.join(error_folder), f"-traceback ({T}).txt" ) @@ -58,11 +67,6 @@ def initialize_agent(self): self.print("Starting the GUI...") self.gui.start() - if self.matchcomms_root is not None: - self.match_comms = MatchComms(self) - self.print("Starting the match communication handler...") - self.match_comms.start() - self.print("Building game information") match_settings = self.get_match_settings() @@ -101,18 +105,11 @@ def initialize_agent(self): "heatseeker" ) - ball_size = ( - 92.75, - 69.25, - 139.5, - 239.75 - ) - self.gravity = gravity[mutators.GravityOption()] self.boost_accel = boost_accel[mutators.BoostStrengthOption()] self.boost_amount = boost_amount[mutators.BoostOption()] self.game_mode = game_mode[match_settings.GameMode()] - self.ball_radius = ball_size[mutators.BallSizeOption()] + self.ball_radius = 92.75 self.friends = () self.foes = () @@ -140,6 +137,7 @@ def initialize_agent(self): self.odd_tick = -1 self.delta_time = 1 / 120 self.last_sent_tmcp_packet = None + # self.sent_tmcp_packet_times = {} self.future_ball_location_slice = 180 self.balL_prediction_struct = None @@ -149,9 +147,6 @@ def retire(self): if not self.tournament and self.extra_debugging: self.gui.stop() - if self.matchcomms_root is not None: - self.match_comms.stop() - def is_hot_reload_enabled(self): # The tkinter GUI isn't compatible with hot reloading # Use the Continue and Spawn option in the RLBotGUI instead @@ -159,7 +154,12 @@ def is_hot_reload_enabled(self): def get_ready(self, packet): field_info = self.get_field_info() - self.boosts = tuple(boost_object(i, boost.location, boost.is_full_boost) for i, boost in enumerate(field_info.boost_pads)) + self.boosts = tuple(boost_object(i, field_info.boost_pads[i].location, field_info.boost_pads[i].is_full_boost) for i in range(field_info.num_boosts)) + if len(self.boosts) != 34: + print(f"There are {len(self.boosts)} boost pads! @Tarehart REEEEE!") + for i, boost in enumerate(self.boosts): + print(f"{boost.location} ({i})") + self.refresh_player_lists(packet) self.ball.update(packet) @@ -171,11 +171,18 @@ def get_ready(self, packet): self.ready = True def refresh_player_lists(self, packet): + match_settings = self.get_match_settings() # Useful to keep separate from get_ready because humans can join/leave a match self.friends = tuple(car_object(i, packet) for i in range(packet.num_cars) if packet.game_cars[i].team is self.team and i != self.index) self.foes = tuple(car_object(i, packet) for i in range(packet.num_cars) if packet.game_cars[i].team != self.team) self.me = car_object(self.index, packet) + try: + true_name = match_settings.PlayerConfigurations(self.index).Name() + self.true_name = true_name + except Exception: + print(f"{self.name}: I appear to have been forcefully pushed into a match! How rude.") + def push(self, routine): self.stack.append(routine) @@ -230,7 +237,7 @@ def clear(self): def is_clear(self): return len(self.stack) < 1 - def preprocess(self, packet): + def preprocess(self, packet: GameTickPacket): if packet.num_cars != len(self.friends)+len(self.foes)+1: self.refresh_player_lists(packet) @@ -246,6 +253,9 @@ def preprocess(self, packet): self.time = self.game.time self.gravity = self.game.gravity + self.ball_radius = self.ball.shape.hitbox.diameter if self.ball.shape.type in {1, 2} else (sum(self.ball.shape.hitbox.length, self.ball.shape.hitbox.width, self.ball.shape.hitbox.height) / 3) + self.ball_radius /= 2 + # When a new kickoff begins we empty the stack if not self.kickoff_flag and self.game.round_active and self.game.kickoff: self.kickoff_done = False @@ -263,7 +273,7 @@ def preprocess(self, packet): self.ball_prediction_struct = self.get_ball_prediction_struct() - if self.tournament and self.matchcomms_root is not None: + if self.matchcomms_root is not None: while 1: try: msg = self.matchcomms.incoming_broadcast.get_nowait() @@ -279,6 +289,10 @@ def preprocess(self, packet): except Exception: print_exc() + def is_shooting(self): + stack_routine_name = '' if self.is_clear() else self.stack[0].__class__.__name__ + return stack_routine_name in {'Aerial', 'jump_shot', 'double_jump', 'ground_shot', 'short_shot'} + def get_output(self, packet): try: # Reset controller @@ -294,18 +308,14 @@ def get_output(self, packet): if not self.is_clear(): self.clear() elif self.game.round_active: - stack_routine_name = '' if self.is_clear() else self.stack[0].__class__.__name__ - if stack_routine_name in {'Aerial', 'jump_shot', 'double_jump', 'ground_shot', 'short_shot'}: - self.shooting = True - else: - self.shooting = False + self.shooting = self.is_shooting() try: self.run() # Run strategy code; This is a very expensive function to run except Exception: t_file = os.path.join(self.traceback_file[0], self.name+self.traceback_file[1]) print(f"ERROR in {self.name}; see '{t_file}'") - print_exc() + print_exc(file=open(t_file, "a")) try: tmcp_packet = self.create_tmcp_packet() @@ -315,10 +325,16 @@ def get_output(self, packet): if self.last_sent_tmcp_packet is None or self.tmcp_packet_is_different(tmcp_packet): self.matchcomms.outgoing_broadcast.put_nowait(tmcp_packet) self.last_sent_tmcp_packet = tmcp_packet + + # t = math.floor(self.time) + # if self.sent_tmcp_packet_times.get(t) is None: + # self.sent_tmcp_packet_times[t] = 1 + # else: + # self.sent_tmcp_packet_times[t] += 1 except Exception: t_file = os.path.join(self.traceback_file[0], self.name+"-TMCP"+self.traceback_file[1]) print(f"ERROR in {self.name} with sending TMCP packet; see '{t_file}'") - print_exc() + print_exc(file=open(t_file, "a")) # run the routine on the end of the stack if not self.is_clear(): @@ -328,7 +344,7 @@ def get_output(self, packet): except Exception: t_file = os.path.join(self.traceback_file[0], r_name+self.traceback_file[1]) print(f"ERROR in {self.name}'s {r_name} routine; see '{t_file}'") - print_exc() + print_exc(file=open(t_file, "a")) if self.debugging: if self.debug_3d_bool: @@ -366,6 +382,11 @@ def get_output(self, packet): self.line(top_back_right, top_front_right, hitbox_color) if self.debug_2d_bool: + # if len(self.sent_tmcp_packet_times) > 0: + # avg_tmcp_packets = sum(self.sent_tmcp_packet_times.values()) / len(self.sent_tmcp_packet_times) + + # self.debug[1].insert(0, f"Avg. TMCP packets / sec: {avg_tmcp_packets}") + if self.delta_time != 0: self.debug[1].insert(0, f"TPS: {round(1 / self.delta_time)}") @@ -384,7 +405,7 @@ def get_output(self, packet): except Exception: t_file = os.path.join(self.traceback_file[0], "VirxERLU"+self.traceback_file[1]) print(f"ERROR with VirxERLU in {self.name}; see '{t_file}' and please report the bug at 'https://github.com/VirxEC/VirxERLU/issues'") - print_exc() + print_exc(file=open(t_file, "a")) return SimpleControllerState() def get_minimum_game_time_to_ball(self): @@ -392,6 +413,8 @@ def get_minimum_game_time_to_ball(self): return -1 def tmcp_packet_is_different(self, tmcp_packet): + # If you're looking to overwrite this, you might want to do a version check + # If the packets are the same if self.last_sent_tmcp_packet == tmcp_packet: return False @@ -403,6 +426,11 @@ def tmcp_packet_is_different(self, tmcp_packet): return True if action_type == "BALL": + dir1 = Vector(*self.last_sent_tmcp_packet["action"]["direction"]) + dir2 = Vector(*tmcp_packet["action"]["direction"]) + return abs(self.last_sent_tmcp_packet["action"]["time"] - tmcp_packet["action"]["time"]) >= 0.1 or dir1.magnitude() != dir2.magnitude() or dir1.angle(dir2) > 0.5 + + if action_type == "READY": return abs(self.last_sent_tmcp_packet["action"]["time"] - tmcp_packet["action"]["time"]) >= 0.1 if action_type == "BOOST": @@ -411,35 +439,45 @@ def tmcp_packet_is_different(self, tmcp_packet): if action_type == "DEMO": return abs(self.last_sent_tmcp_packet["action"]["time"] - tmcp_packet["action"]["time"]) >= 0.1 or self.last_sent_tmcp_packet["action"]["target"] != tmcp_packet["action"]["target"] - if action_type == "WAIT": - return abs(self.last_sent_tmcp_packet["action"]["ready"] - tmcp_packet["action"]["ready"]) >= 0.1 - # Right now, this is only for DEFEND return False def create_tmcp_packet(self): # https://github.com/RLBot/RLBot/wiki/Team-Match-Communication-Protocol # don't worry about duplicate packets - this is handled automatically + tmcp_version = [0, 9] return { - "tmcp_version": [0,7], + "tmcp_version": tmcp_version, "index": self.index, "team": self.team, - "action": self.get_tmcp_action() + "action": self.get_tmcp_action(tmcp_version) } - def get_tmcp_action(self): + def get_tmcp_action(self, tmcp_version): + # If you're looking to overwrite this, you might want to do a version check + if self.is_clear(): return { - "type": "WAIT", - "ready": -1 + "type": "READY", + "time": -1 } stack_routine_name = self.stack[0].__class__.__name__ - if stack_routine_name in {'Aerial', 'jump_shot', 'ground_shot', 'double_jump', 'short_shot'}: + if stack_routine_name == 'short_shot': + return { + "type": "BALL", + "time": -1, + "direction" : [0, 0, 0] + } + if stack_routine_name in {'Aerial', 'jump_shot', 'ground_shot', 'double_jump'}: + if self.stack[0].shot_vector is None: + self.stack[0].preprocess(self) + return { "type": "BALL", - "time": -1 if stack_routine_name == 'short_shot' else self.stack[0].intercept_time + "time": self.stack[0].intercept_time, + "direction": list(self.stack[0].shot_vector) } if stack_routine_name == "goto_boost": return { @@ -449,8 +487,8 @@ def get_tmcp_action(self): # by default, VirxERLU can't demo bots return { - "type": "WAIT", - "ready": self.get_minimum_game_time_to_ball() + "type": "READY", + "time": self.get_minimum_game_time_to_ball() } def handle_tmcp_packet(self, packet): @@ -474,13 +512,14 @@ def init(self): class car_object: - # The carObject, and kin, convert the gametickpacket in something a little friendlier to use, - # and are updated by VirxERLU as the game runs - def __init__(self, index, packet=None): + # objects convert the gametickpacket in something a little friendlier to use + # and are automatically updated by VirxERLU as the game runs + def __init__(self, index, packet=None, match_settings: MatchSettings=None): self._vec = Vector # ignore this property self.location = self._vec() self.orientation = Matrix3() self.velocity = self._vec() + self._local_velocity = self._vec() self.angular_velocity = self._vec() self.demolished = False self.airborne = False @@ -490,16 +529,23 @@ def __init__(self, index, packet=None): self.boost = 0 self.index = index self.tmcp_action = None + self.true_name = None + self.land_time = 0 + + if match_settings is not None: + try: + self.true_name = match_settings.PlayerConfigurations(index).Name() + except Exception: + pass if packet is not None: car = packet.game_cars[self.index] self.name = car.name - self.true_name = re.split(r' \(\d+\)$', self.name)[0] + if self.true_name is None: self.true_name = re.split(r' \(\d+\)$', self.name)[0] # e.x. 'ABot (12)' will instead be just 'ABot' self.team = car.team self.hitbox = hitbox_object(car.hitbox.length, car.hitbox.width, car.hitbox.height, Vector(car.hitbox_offset.x, car.hitbox_offset.y, car.hitbox_offset.z)) - self.offset = self.hitbox.offset # please use self.hitbox.offset and not self.offset - + self.update(packet) return @@ -508,7 +554,6 @@ def __init__(self, index, packet=None): self.true_name = None self.team = -1 self.hitbox = hitbox_object() - self.offset = self.hitbox.offset def local(self, value): # Generic localization @@ -520,7 +565,7 @@ def local_velocity(self, velocity=None): # y is the velocity to the left (+) or right (-) # z if the velocity upwards (+) or downwards (-) if velocity is None: - velocity = self.velocity + return self._local_velocity return self.local(velocity) @@ -548,11 +593,12 @@ def get_raw(self, agent, force_on_ground=False): tuple(self.hitbox.offset) ) - def update(self, packet): + def update(self, packet: GameTickPacket): car = packet.game_cars[self.index] car_phy = car.physics self.location = self._vec.from_vector(car_phy.location) self.velocity = self._vec.from_vector(car_phy.velocity) + self._local_velocity = self.local(self.velocity) self.orientation = Matrix3.from_rotator(car_phy.rotation) self.angular_velocity = self.orientation.dot((car_phy.angular_velocity.x, car_phy.angular_velocity.y, car_phy.angular_velocity.z)) self.demolished = car.is_demolished @@ -562,6 +608,9 @@ def update(self, packet): self.doublejumped = car.double_jumped self.boost = car.boost + if self.airborne and car.has_wheel_contact: + self.land_time = packet.game_info.seconds_elapsed + @property def forward(self): # A vector pointing forwards relative to the cars orientation. Its magnitude == 1 @@ -592,6 +641,17 @@ def __getitem__(self, index): return (self.length, self.width, self.height)[index] +class hitbox_sphere: + def __init__(self, diameter=92.75): + self.diameter = diameter + + +class hitbox_cylinder: + def __init__(self, diameter=92.75, height=92.75): + self.diameter = diameter + self.height = height + + class last_touch: def __init__(self): self.location = Vector() @@ -607,13 +667,30 @@ def update(self, packet): self.car = car_object(touch.player_index, packet) +class ball_shape: + def __init__(self): + self.type = -1 + self.hitbox = None + + def update(self, packet: GameTickPacket): + shape = packet.game_ball.collision_shape + self.type = shape.type + + if self.type == 0: + self.hitbox = hitbox_object(shape.box.length, shape.box.width, shape.box.height) + elif self.type == 1: + self.hitbox = hitbox_sphere(shape.sphere.diameter) + elif self.type == 2: + self.hitbox = hitbox_cylinder(shape.cylinder.diameter, shape.cylinder.height) + + class ball_object: def __init__(self): self._vec = Vector # ignore this property self.location = self._vec() self.velocity = self._vec() - self.latest_touched_time = 0 - self.latest_touched_team = 0 + self.last_touch = last_touch() + self.shape = ball_shape() def get_raw(self): return ( @@ -621,12 +698,12 @@ def get_raw(self): tuple(self.velocity) ) - def update(self, packet): + def update(self, packet: GameTickPacket): ball = packet.game_ball self.location = self._vec.from_vector(ball.physics.location) self.velocity = self._vec.from_vector(ball.physics.velocity) - self.latest_touched_time = ball.latest_touch.time_seconds - self.latest_touched_team = ball.latest_touch.team + self.last_touch.update(packet) + self.shape.update(packet) class boost_object: @@ -663,7 +740,7 @@ def __init__(self): self.foe_score = 0 self.gravity = Vector() - def update(self, team, packet): + def update(self, team, packet: GameTickPacket): game = packet.game_info self.time = game.seconds_elapsed self.time_remaining = game.game_time_remaining @@ -866,7 +943,7 @@ def angle(self, value: Vector) -> float: # Returns the angle between this Vector and another Vector in radians return math.acos(max(min(np.dot(self.normalize()._np, value.normalize()._np).item(), 1), -1)) - def rotate(self, angle: float) -> Vector: + def rotate2D(self, angle: float) -> Vector: # Rotates this Vector by the given angle in radians # Note that this is only 2D, in the x and y axis return Vector((math.cos(angle)*self.x) - (math.sin(angle)*self.y), (math.sin(angle)*self.x) + (math.cos(angle)*self.y), self.z) @@ -887,13 +964,13 @@ def clamp2D(self, start: Vector, end: Vector) -> Vector: def clamp(self, start: Vector, end: Vector) -> Vector: # This extends clamp2D so it also clamps the vector's z s = self.clamp2D(start, end) - start_z = min(start.z, end.z) - end_z = max(start.z, end.z) - if s.z < start_z: - s.z = start_z - elif s.z > end_z: - s.z = end_z + if s.z < start.z: + s = s.flatten().scale(1 - start.z) + s.z = start.z + elif s.z > end.z: + s = s.flatten().scale(1 - end.z) + s.z = end.z return s diff --git a/RLBotPack/ElkBot/util/routines.py b/RLBotPack/ElkBot/util/routines.py index 51e1bb97..fc0477df 100644 --- a/RLBotPack/ElkBot/util/routines.py +++ b/RLBotPack/ElkBot/util/routines.py @@ -1,9 +1,7 @@ import virxrlcu from util.agent import VirxERLU -from util.utils import (Vector, almost_equals, cap, cap_in_field, defaultDrive, - defaultPD, dodge_impulse, lerp, math, peek_generator, - side, sign) +from util.utils import * max_speed = 2300 throttle_accel = 66 + (2/3) @@ -12,6 +10,9 @@ jump_max_duration = 0.2 jump_speed = 291 + (2/3) jump_acc = 1458 + (1/3) +no_adjust_radians = 0.05 +min_adjust_radians = 0.5 +dodge_offset = 0.12 class wave_dash: @@ -95,9 +96,28 @@ def __init__(self, intercept_time, targets=None): self.needed_jump_time = -1 self.counter = 0 + self.upgrade_intercept_time = None + self.upgrade_targets = None + self.upgrade = False + def update(self, shot): - self.intercept_time = shot.intercept_time - self.targets = shot.targets + self.upgrade_intercept_time = shot.intercept_time + self.upgrade_targets = shot.targets + self.upgrade = True + + def preprocess(self, agent: VirxERLU): + T = self.intercept_time - agent.time + slice_n = round(T * 60) - 1 + ball = agent.ball_prediction_struct.slices[slice_n].physics.location + ball_location = Vector(ball.x, ball.y, ball.z) + car_to_ball_norm = (ball_location - agent.me.location).normalize() + shot_vector = car_to_ball_norm if self.targets is None else car_to_ball_norm.clamp((self.targets[0] - ball_location).normalize(), (self.targets[1] - ball_location).normalize()) + + if self.shot_vector is None or self.ball_location.dist(ball_location) > 5: + self.ball_location = ball_location + self.shot_vector = shot_vector + self.offset_target = self.ball_location - (self.shot_vector * agent.ball_radius) + self.needed_jump_time = round(virxrlcu.get_double_jump_time(round(self.offset_target.z - agent.me.location.z), round(agent.me.velocity.z), agent.gravity.z), 3) def run(self, agent: VirxERLU): # This routine is the same as jump_shot, but it's designed to hit the ball above 300uus and below 450uus without requiring boost @@ -105,38 +125,52 @@ def run(self, agent: VirxERLU): agent.shooting = True T = self.intercept_time - agent.time - # Capping T above 0 to prevent division problems - time_remaining = cap(T, 0.000001, 6) - if (not self.jumping and T > 0.1 and agent.odd_tick % 2 == 0) or self.ball_location is None: - slice_n = round(T * 60) - 1 - ball = agent.ball_prediction_struct.slices[slice_n].physics.location + if (not self.jumping and T > 1.5 and agent.odd_tick % 2 == 0) or self.ball_location is None: + if self.upgrade: + self.intercept_time = self.upgrade_intercept_time + self.targets = self.upgrade_targets + self.upgrade = False - self.ball_location = Vector(ball.x, ball.y, ball.z) - self.needed_jump_time = virxrlcu.get_double_jump_time(ball.z - agent.me.location.z, agent.me.velocity.z, agent.gravity.z) + T = self.intercept_time - agent.time + + self.preprocess(agent) - direction = (self.ball_location - agent.me.location).normalize() - self.shot_vector = direction if self.targets is None else direction.clamp((self.targets[0] - self.ball_location).normalize(), (self.targets[1] - self.ball_location).normalize()) - self.offset_target = self.ball_location - (self.shot_vector * agent.ball_radius) agent.sphere(self.ball_location, agent.ball_radius) - + agent.dbg_2d(f"Needed jump time: {self.needed_jump_time}") + car_to_ball = self.ball_location - agent.me.location - final_target = self.offset_target.copy().flatten() - Tj = T - self.needed_jump_time * 1.075 - agent.dbg_2d(f"Needed jump time: {round(self.needed_jump_time, 2)}") + final_target = self.offset_target.flatten() + Tj = T - (self.needed_jump_time + dodge_offset) + distance_remaining = None if Tj > 0 and self.targets is not None: - # whether we are to the left or right of the shot vector - side_of_shot = sign(self.shot_vector.cross(Vector(z=1)).dot(car_to_ball)) - car_to_offset_target = final_target - agent.me.location - car_to_dodge_perp = car_to_offset_target.cross(Vector(z=side_of_shot)) # perpendicular - - # 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_offset_target.angle2D(self.shot_vector) * min(Tj, 3) * 750 # size of adjustment - final_target += car_to_dodge_perp.normalize() * adjustment - - distance_remaining = self.offset_target.flat_dist(agent.me.location) + angle_to_shot_vector = abs(car_to_ball.angle2D(self.shot_vector)) + if angle_to_shot_vector > no_adjust_radians: + # whether we are to the left or right of the shot vector + side_of_shot = sign(self.shot_vector.cross(Vector(z=1)).dot(car_to_ball)) + car_to_offset_target = final_target - agent.me.location + car_to_offset_perp = car_to_offset_target.cross(Vector(z=side_of_shot)).normalize() # perpendicular ray + final_target += (-(self.shot_vector * (2560 - agent.ball_radius))) if angle_to_shot_vector > min_adjust_radians else (car_to_offset_perp * agent.me.hitbox.width * 0.6) + + if angle_to_shot_vector > min_adjust_radians: + ray_direction = (-self.shot_vector).rotate2D(side_of_shot * -min_adjust_radians) + distance_from_turn = ray_intersects_with_line(self.ball_location, ray_direction, agent.me.location, final_target) + true_final_target = self.offset_target + (car_to_offset_perp * agent.me.hitbox.width * 0.6) + + turn_rad = turn_radius(abs(agent.me.local_velocity().x)) + if ray_intersects_with_sphere(self.ball_location, ray_direction, agent.me.location + Vector(y=turn_rad) * agent.me.right, turn_rad) or ray_intersects_with_sphere(self.ball_location, ray_direction, agent.me.location + Vector(y=-turn_rad) * agent.me.right, turn_rad): + final_target = true_final_target + + if distance_from_turn is not None: + car_turn_point = self.ball_location + ray_direction * distance_from_turn + part_dist = agent.me.location.flat_dist(car_turn_point) + distance_remaining = part_dist + car_turn_point.flat_dist(true_final_target) + part_dist -= turn_rad + + if distance_remaining is None: + distance_remaining = final_target.flat_dist(agent.me.location) + part_dist = distance_remaining # Some adjustment to the final target to ensure it's inside the field and we don't try to drive through any goalposts or walls to reach it final_target = cap_in_field(agent, final_target) @@ -144,46 +178,47 @@ def run(self, agent: VirxERLU): # whether we should go forwards or backwards angle_to_target = abs(Vector(x=1).angle2D(local_final_target)) - direction = 1 if angle_to_target < 1.6 or agent.me.local_velocity().x > 1000 else -1 # drawing debug lines to show the dodge point and final target (which differs due to the adjustment) agent.line(agent.me.location, self.offset_target, agent.renderer.white()) - agent.line(self.offset_target-Vector(z=100), self.offset_target+Vector(z=100), agent.renderer.green()) - agent.line(final_target-Vector(z=100), final_target+Vector(z=100), agent.renderer.purple()) + agent.line(self.offset_target-Vector(z=agent.ball_radius), self.offset_target+Vector(z=agent.ball_radius), agent.renderer.green()) + agent.line(final_target-Vector(z=agent.ball_radius), final_target+Vector(z=agent.ball_radius), agent.renderer.purple()) vf = agent.me.velocity + agent.gravity * T distance_remaining = agent.me.local_location(self.offset_target).x if agent.me.airborne else distance_remaining distance_remaining -= agent.me.hitbox.length * 0.45 distance_remaining = max(distance_remaining, 0) - speed_required = distance_remaining / time_remaining + speed_required = distance_remaining / max(T, agent.delta_time) + direction = 1 if angle_to_target < 1.6 or speed_required > 1410 else -1 agent.dbg_2d(f"Speed required: {round(speed_required, 2)}") if not self.jumping: velocity = defaultDrive(agent, speed_required * direction, local_final_target)[1] if velocity == 0: velocity = 1 - local_offset_target = agent.me.local_location(self.offset_target.flatten()) + local_offset_target = agent.me.local_location(self.offset_target).flatten() true_angle_to_target = abs(Vector(x=1).angle2D(local_offset_target)) - local_vf = agent.me.local(vf.flatten()) + local_vf = agent.me.local(agent.me.velocity * T).flatten() true_distance_remaining = self.offset_target.flat_dist(agent.me.location) - time = true_distance_remaining / (abs(velocity) + dodge_impulse(agent)) + dodge_time = true_distance_remaining / (abs(velocity) + dodge_impulse(agent)) - (self.needed_jump_time + dodge_offset) - if ((abs(velocity) < 100 and true_distance_remaining < agent.me.hitbox.length / 2) or (abs(local_offset_target.y) < agent.ball_radius and direction * local_vf.x >= direction * (local_offset_target.x - agent.me.hitbox.length * 0.45) and direction * local_offset_target.x > 0)) and T <= self.needed_jump_time * 1.025: - self.jumping = True + if (abs(velocity) < 100 and true_distance_remaining < agent.me.hitbox.length / 2 + agent.ball_radius) or (abs(local_offset_target.y) < agent.me.hitbox.width / 2 and direction * local_vf.x >= (direction * local_offset_target.x) - agent.me.hitbox.length * 0.6 and direction * local_offset_target.x > 0): + self.jumping = T <= self.needed_jump_time + dodge_offset + 0.05 elif agent.me.airborne: - agent.push(recovery(final_target if Tj > 0 else None)) - elif T <= self.needed_jump_time or (Tj > 0 and true_distance_remaining > agent.me.hitbox.length / 2 and (not virxrlcu.double_jump_shot_is_viable(T, agent.boost_accel, tuple(agent.gravity), agent.me.get_raw(agent), self.offset_target.z, tuple((final_target - agent.me.location).normalize()), distance_remaining))): + agent.push(recovery(local_final_target if Tj > 0 else None)) + elif Tj < -agent.delta_time * 6 or (Tj > 0.05 and true_distance_remaining > agent.me.hitbox.length * 0.6 and not virxrlcu.double_jump_shot_is_viable(T, agent.boost_accel, tuple(agent.gravity), agent.me.get_raw(agent), self.offset_target.z, tuple((final_target - agent.me.location).normalize()), distance_remaining)): # If we're out of time or the ball was hit away or we just can't get enough speed, pop agent.pop() agent.shooting = False + agent.shot_weight = -1 + agent.shot_time = -1 if agent.me.airborne: agent.push(ball_recovery()) - elif agent.boost_amount != 'unlimited' and self.needed_jump_time * 1.075 > time: - time -= self.needed_jump_time * 1.075 - if agent.me.boost < 48 and angle_to_target < 0.03 and (true_angle_to_target < 0.1 or distance_remaining > 4480) and velocity > 600 and time >= 1: + elif dodge_time >= 1.2 and agent.time - agent.me.land_time > 0.5: + if agent.me.boost < 48 and angle_to_target < 0.03 and (true_angle_to_target < 0.1 or distance_remaining > 4480) and velocity > 600 and velocity < speed_required - 50: agent.push(flip(agent.me.local_location(self.offset_target))) - elif direction == -1 and velocity < 200 and time >= 1.5: + elif direction == -1 and velocity < 200: agent.push(flip(agent.me.local_location(self.offset_target), True)) else: # Mark the time we started jumping so we know when to dodge @@ -193,29 +228,32 @@ def run(self, agent: VirxERLU): jump_elapsed = agent.time - self.jump_time tau = jump_max_duration - jump_elapsed - xf = agent.me.location + agent.me.velocity * T + 0.5 * agent.gravity * T * T + Tj2 = max(T - dodge_offset, agent.delta_time) + + xf = agent.me.location + agent.me.velocity * Tj2 + 0.5 * agent.gravity * Tj2 * Tj2 if jump_elapsed == 0: vf += agent.me.up * jump_speed - xf += agent.me.up * jump_speed * T + xf += agent.me.up * jump_speed * Tj2 hf = vf vf += agent.me.up * jump_acc * tau - xf += agent.me.up * jump_acc * tau * (T - 0.5 * tau) + xf += agent.me.up * jump_acc * tau * (Tj2 - 0.5 * tau) hf += agent.me.up * jump_speed vf += agent.me.up * jump_speed - xf += agent.me.up * jump_speed * (T - tau) + xf += agent.me.up * jump_speed * (Tj2 - tau) delta_x = self.offset_target - xf d_direction = delta_x.normalize() - if direction == 1 and abs(agent.me.forward.dot(d_direction)) > 0.5: + if T > 0 and direction == 1 and abs(agent.me.forward.dot(d_direction)) > 0.75: delta_v = delta_x.dot(agent.me.forward) / T if agent.me.boost > 0 and delta_v >= agent.boost_accel * 0.1: agent.controller.boost = True - else: - agent.controller.throttle = cap(delta_v / (throttle_accel * 0.1), -1, 1) + agent.controller.throttle = 1 + elif abs(delta_v) >= throttle_accel * agent.delta_time: + agent.controller.throttle = cap(delta_v / (throttle_accel * agent.delta_time), -1, 1) if T <= -0.4 or (not agent.me.airborne and self.counter == 4): agent.pop() @@ -237,7 +275,7 @@ def run(self, agent: VirxERLU): defaultPD(agent, agent.me.local_location(self.offset_target.flatten()) * direction) l_vf = vf + agent.me.location - agent.line(l_vf-Vector(z=100), l_vf+Vector(z=100), agent.renderer.red()) + agent.line(l_vf-Vector(z=agent.ball_radius), l_vf+Vector(z=agent.ball_radius), agent.renderer.red()) class Aerial: @@ -246,9 +284,8 @@ def __init__(self, intercept_time, targets=None, fast_aerial=True): self.fast_aerial = fast_aerial self.targets = targets self.shot_vector = None - self.target = None - self.ball = None - + self.offset_target = None + self.ball_location = None self.jump_type_fast = None self.jumping = False self.dodging = False @@ -256,38 +293,70 @@ def __init__(self, intercept_time, targets=None, fast_aerial=True): self.jump_time = -1 self.counter = 0 + self.upgrade_intercept_time = None + self.upgrade_fast_aerial = None + self.upgrade_targets = None + self.upgrade = False + def update(self, shot): - self.intercept_time = shot.intercept_time - self.fast_aerial = shot.fast_aerial - self.targets = shot.targets + self.upgrade_intercept_time = shot.intercept_time + self.upgrade_fast_aerial = shot.fast_aerial + self.upgrade_targets = shot.targets + self.upgrade = True + + def preprocess(self, agent: VirxERLU): + T = self.intercept_time - agent.time + slice_n = math.ceil(T * 60) - 1 + ball = agent.ball_prediction_struct.slices[slice_n].physics.location + ball_location = Vector(ball.x, ball.y, ball.z) + car_to_ball_norm = (ball_location - agent.me.location).normalize() + shot_vector = car_to_ball_norm if self.targets is None else car_to_ball_norm.clamp((self.targets[0] - ball_location).normalize(), (self.targets[1] - ball_location).normalize()) + + if self.shot_vector is None or self.ball_location.dist(ball_location) > 5: + self.ball_location = ball_location + self.shot_vector = shot_vector + self.offset_target = self.ball_location - (self.shot_vector * agent.ball_radius) + + self.ceiling = agent.me.location.z > 2044 - agent.me.hitbox.height * 2 and not agent.me.jumped + if self.ceiling: + self.offset_target -= Vector(z=agent.ball_radius) def run(self, agent: VirxERLU): if not agent.shooting: agent.shooting = True T = self.intercept_time - agent.time - xf = agent.me.location + agent.me.velocity * T + 0.5 * agent.gravity * T * T - vf = agent.me.velocity + agent.gravity * T - - slice_n = math.ceil(T * 60) - 1 - - if (T > 0.1 and agent.odd_tick % 2 == 0) or self.ball is None: - ball = agent.ball_prediction_struct.slices[slice_n].physics.location - self.ball = Vector(ball.x, ball.y, ball.z) - self.ceiling = agent.me.location.z > 2044 - agent.me.hitbox.height * 2 and not agent.me.jumped - direction = (agent.ball.location - agent.me.location).normalize() - self.shot_vector = direction if self.targets is None else direction.clamp((self.targets[0] - self.ball).normalize(), (self.targets[1] - self.ball).normalize()) - self.target = self.ball - (self.shot_vector * agent.ball_radius) - agent.sphere(self.ball, agent.ball_radius) + if (T > 0.3 and agent.odd_tick % 2 == 0) or self.ball_location is None: + if self.upgrade: + self.intercept_time = self.upgrade_intercept_time + self.fast_aerial = self.upgrade_fast_aerial + self.targets = self.upgrade_targets + self.upgrade = False + + T = self.intercept_time - agent.time + + self.preprocess(agent) + + final_target = self.offset_target + if T > 0 and self.targets is not None: + car_to_ball = self.ball_location - agent.me.location + angle_to_shot_vector = abs(car_to_ball.angle2D(self.shot_vector)) + if angle_to_shot_vector > no_adjust_radians: + # whether we are to the left or right of the shot vector + side_of_shot = sign(self.shot_vector.cross(Vector(z=1)).dot(car_to_ball)) + car_to_offset_target = final_target - agent.me.location + car_to_offset_perp = car_to_offset_target.cross(Vector(z=side_of_shot)).normalize() # perpendicular ray + final_target += car_to_offset_perp * agent.me.hitbox.width * 0.6 - if self.ceiling: - self.target -= Vector(z=agent.ball_radius) + agent.sphere(self.ball_location, agent.ball_radius) + xf = agent.me.location + agent.me.velocity * T + 0.5 * agent.gravity * T * T + vf = agent.me.velocity + agent.gravity * T - if self.jumping or self.jump_time == -1: + if self.jumping or (self.jump_time == -1 and not agent.me.airborne): agent.dbg_2d("Jumping") - if not self.jumping or self.jump_time == -1: + if self.jump_time == -1: self.jump_type_fast = self.fast_aerial self.jumping = True self.jump_time = agent.time @@ -314,7 +383,7 @@ def run(self, agent: VirxERLU): if jump_elapsed <= jump_max_duration: agent.controller.jump = True - elif self.counter < 4: + else: self.counter += 1 if self.counter == 3: @@ -330,45 +399,46 @@ def run(self, agent: VirxERLU): if self.ceiling: agent.dbg_2d(f"Ceiling shot") - delta_x = self.target - xf + delta_x = final_target - xf direction = delta_x.normalize() if not self.jumping else delta_x.flatten().normalize() agent.line(agent.me.location, agent.me.location + (direction * 250), agent.renderer.black()) c_vf = vf + agent.me.location - agent.line(c_vf - Vector(z=100), c_vf + Vector(z=100), agent.renderer.blue()) - agent.line(xf - Vector(z=100), xf + Vector(z=100), agent.renderer.red()) - agent.line(self.target - Vector(z=100), self.target + Vector(z=100), agent.renderer.green()) + agent.line(c_vf - Vector(z=agent.ball_radius), c_vf + Vector(z=agent.ball_radius), agent.renderer.blue()) + agent.line(xf - Vector(z=agent.ball_radius), xf + Vector(z=agent.ball_radius), agent.renderer.red()) + agent.line(self.offset_target - Vector(z=agent.ball_radius), self.offset_target + Vector(z=agent.ball_radius), agent.renderer.green()) + + delta_v = delta_x.dot(agent.me.forward) / T - if not self.dodging: - target = delta_x if delta_x.magnitude() >= agent.boost_accel * agent.delta_time * 0.1 else self.shot_vector - target = agent.me.local(target) + if self.counter in {0, 4}: + target = agent.me.local(delta_x) if (delta_v >= agent.boost_accel * 0.1 + throttle_accel * agent.delta_time) or (T > 1 and delta_v >= throttle_accel * agent.delta_time * 0.1) else agent.me.local_location(self.offset_target) - if agent.controller.jump: - defaultPD(agent, target.flatten(), up=agent.me.up) + if self.jumping and self.jump_type_fast: + defaultPD(agent, target) elif virxrlcu.find_landing_plane(tuple(agent.me.location), tuple(agent.me.velocity), agent.gravity.z) == 4: defaultPD(agent, target, upside_down=True) else: - defaultPD(agent, target, upside_down=self.shot_vector.z < 0) + defaultPD(agent, target, upside_down=agent.me.location.z > self.offset_target.z) # only boost/throttle if we're facing the right direction - if abs(agent.me.forward.dot(direction)) > 0.75 and T > 0: - if T > 1 and not self.jumping: agent.controller.roll = 1 if self.shot_vector.z < 0 else -1 + if T > 0 and abs(agent.me.forward.angle(direction)) < 0.25 and not self.jumping: + if T > 0.3: agent.controller.roll = 1 if self.shot_vector.z < 0 else -1 # the change in velocity the bot needs to put it on an intercept course with the target - delta_v = delta_x.dot(agent.me.forward) / T - if not self.jumping and agent.me.boost > 0 and delta_v >= agent.boost_accel * agent.delta_time * 0.1: + if agent.me.airborne and agent.me.boost > 0 and delta_v >= agent.boost_accel * 0.1 + throttle_accel * agent.delta_time: agent.controller.boost = True - delta_v -= agent.boost_accel * agent.delta_time * 0.1 - - if abs(delta_v) >= throttle_accel * agent.delta_time: + agent.controller.throttle = 1 + elif abs(delta_v) >= throttle_accel * agent.delta_time * 0.1: agent.controller.throttle = cap(delta_v / (throttle_accel * agent.delta_time), -1, 1) - if T <= -0.2 or (not self.jumping and not agent.me.airborne) or (not self.jumping and T > 1.5 and not virxrlcu.aerial_shot_is_viable(T, agent.boost_accel, tuple(agent.gravity), agent.me.get_raw(agent), tuple(self.target))): + if T <= -0.2 or (not self.jumping and not agent.me.airborne) or (not self.jumping and ((delta_v >= agent.boost_accel * 0.1 + throttle_accel * agent.delta_time) or (T > 1 and delta_v >= throttle_accel * agent.delta_time * 0.1)) and not virxrlcu.aerial_shot_is_viable(T, agent.boost_accel, tuple(agent.gravity), agent.me.get_raw(agent), tuple(self.offset_target))): agent.pop() agent.shooting = False + agent.shot_weight = -1 + agent.shot_time = -1 agent.push(ball_recovery()) - elif (self.ceiling and self.target.dist(agent.me.location) < agent.ball_radius + agent.me.hitbox.length and not agent.me.doublejumped and agent.me.location.z < agent.ball.location.z + agent.ball_radius and self.target.y * side(agent.team) > -4240) or (not self.ceiling and not agent.me.doublejumped and T < 0.1): + elif (self.ceiling and self.offset_target.dist(agent.me.location) < agent.ball_radius + agent.me.hitbox.length and not agent.me.doublejumped and agent.me.location.z < agent.ball.location.z + agent.ball_radius and self.offset_target.y * side(agent.team) > -4240) or (not self.ceiling and not agent.me.doublejumped and T < 0.1): agent.dbg_2d("Flipping") - vector = agent.me.local_location(self.target).flatten().normalize() + vector = agent.me.local_location(self.offset_target).flatten().normalize() scale = 1 / max(abs(vector.x), abs(vector.y)) self.p = cap(-vector.x * scale, -1, 1) self.y = cap(vector.y * scale, -1, 1) @@ -468,14 +538,12 @@ def run(self, agent: VirxERLU, manual=False): final_target = cap_in_field(agent, final_target) # Some 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 local_target = agent.me.local_location(final_target) angle_to_target = abs(Vector(x=1).angle2D(local_target)) + true_angle_to_target = abs(Vector(x=1).angle2D(agent.me.local_location(self.target))) direction = 1 if angle_to_target < 1.6 or agent.me.local_velocity().x > 1000 else -1 agent.dbg_2d(f"Angle to target: {round(angle_to_target, 1)}") velocity = defaultDrive(agent, (2300 if distance_remaining > 1280 or not self.slow else cap(distance_remaining * 2, 1200, 2300)) * direction, local_target)[1] - if distance_remaining < 1280: agent.controller.boost = False - if velocity == 0: velocity = 1 - - time = distance_remaining / (abs(velocity) + dodge_impulse(agent)) + if distance_remaining < 2560: agent.controller.boost = False # this is to break rule 1's with TM8'S ONLY # 251 is the distance between center of the 2 longest cars in the game, with a bit extra @@ -488,12 +556,15 @@ def run(self, agent: VirxERLU, manual=False): elif self.rule1_timer != -1: self.rule1_timer = -1 + dodge_time = distance_remaining / (abs(velocity) + dodge_impulse(agent)) + if agent.me.airborne: agent.push(recovery(self.target)) - elif agent.boost_amount != 'unlimited' and agent.me.boost < 60 and angle_to_target < 0.03 and velocity > 500 and time > 1.5: - agent.push(flip(agent.me.local_location(self.target))) - elif agent.boost_amount != 'unlimited' and direction == -1 and velocity < 200 and time > 1.5: - agent.push(flip(agent.me.local_location(self.target), True)) + elif dodge_time >= 1.2 and agent.time - agent.me.land_time > 0.5: + if agent.me.boost < 48 and angle_to_target < 0.03 and (true_angle_to_target < 0.1 or distance_remaining > 4480) and velocity > 600: + agent.push(flip(agent.me.local_location(self.target))) + elif direction == -1 and velocity < 200: + agent.push(flip(agent.me.local_location(self.target), True)) class shadow: @@ -719,47 +790,80 @@ def __init__(self, intercept_time, targets=None): self.jump_time = -1 self.needed_jump_time = -1 + self.upgrade_intercept_time = None + self.upgrade_targets = None + self.upgrade = False + def update(self, shot): - self.intercept_time = shot.intercept_time - self.targets = shot.targets + self.upgrade_intercept_time = shot.intercept_time + self.upgrade_targets = shot.targets + self.upgrade = True + + def preprocess(self, agent: VirxERLU): + T = self.intercept_time - agent.time + slice_n = round(T * 60) - 1 + ball = agent.ball_prediction_struct.slices[slice_n].physics.location + ball_location = Vector(ball.x, ball.y, ball.z) + car_to_ball_norm = (ball_location - agent.me.location).normalize() + shot_vector = car_to_ball_norm if self.targets is None else car_to_ball_norm.clamp((self.targets[0] - ball_location).normalize(), (self.targets[1] - ball_location).normalize()) + + if self.shot_vector is None or self.ball_location.dist(ball_location) > 5: + self.ball_location = ball_location + self.shot_vector = shot_vector + self.offset_target = self.ball_location - (self.shot_vector * agent.ball_radius) + self.needed_jump_time = round(virxrlcu.get_jump_time(round(self.offset_target.z - agent.me.location.z), round(agent.me.velocity.z), agent.gravity.z), 3) def run(self, agent: VirxERLU): if not agent.shooting: agent.shooting = True T = self.intercept_time - agent.time - # Capping T above 0 to prevent division problems - time_remaining = cap(T, 0.000001, 6) - if (not self.jumping and T > 0.1 and agent.odd_tick % 2 == 0) or self.ball_location is None: - slice_n = round(T * 60) - 1 - ball = agent.ball_prediction_struct.slices[slice_n].physics.location + if (not self.jumping and T > 1 and agent.odd_tick % 2 == 0) or self.ball_location is None: + if self.upgrade: + self.targets = self.upgrade_targets + self.intercept_time = self.upgrade_intercept_time + self.upgrade = False - self.ball_location = Vector(ball.x, ball.y, ball.z) - self.needed_jump_time = virxrlcu.get_jump_time(ball.z - agent.me.location.z, agent.me.velocity.z, agent.gravity.z) + T = self.intercept_time - agent.time + + self.preprocess(agent) - direction = (self.ball_location - agent.me.location).normalize() - self.shot_vector = direction if self.targets is None else direction.clamp2D((self.targets[0] - self.ball_location).normalize(), (self.targets[1] - self.ball_location).normalize()) - self.offset_target = self.ball_location - (self.shot_vector * agent.ball_radius) agent.sphere(self.ball_location, agent.ball_radius) + agent.dbg_2d(f"Needed jump time: {self.needed_jump_time}") car_to_ball = self.ball_location - agent.me.location - final_target = self.offset_target.copy().flatten() - Tj = T - self.needed_jump_time * 1.075 - agent.dbg_2d(f"Needed jump time: {round(self.needed_jump_time, 2)}") + final_target = self.offset_target.flatten() + distance_remaining = None + Tj = T - (self.needed_jump_time + dodge_offset) if Tj > 0 and self.targets is not None: - # whether we are to the left or right of the shot vector - side_of_shot = sign(self.shot_vector.cross(Vector(z=1)).dot(car_to_ball)) - car_to_offset_target = final_target - agent.me.location - car_to_offset_perp = car_to_offset_target.cross(Vector(z=side_of_shot)) # perpendicular - - # 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_offset_target.angle2D(self.shot_vector) * cap(Tj, 0.5, 3) * 750 # size of adjustment - final_target += car_to_offset_perp.normalize() * adjustment - - distance_remaining = self.offset_target.flat_dist(agent.me.location) + angle_to_shot_vector = abs(car_to_ball.angle2D(self.shot_vector)) + if angle_to_shot_vector > no_adjust_radians: + # whether we are to the left or right of the shot vector + side_of_shot = sign(self.shot_vector.cross(Vector(z=1)).dot(car_to_ball)) + car_to_offset_target = final_target - agent.me.location + car_to_offset_perp = car_to_offset_target.cross(Vector(z=side_of_shot)).normalize() # perpendicular ray + final_target += (-(self.shot_vector * (2560 - agent.ball_radius))) if angle_to_shot_vector > min_adjust_radians else (car_to_offset_perp * agent.me.hitbox.width * 0.6) + + if angle_to_shot_vector > min_adjust_radians: + ray_direction = (-self.shot_vector).rotate2D(side_of_shot * -min_adjust_radians) + distance_from_turn = ray_intersects_with_line(self.ball_location, ray_direction, agent.me.location, final_target) + true_final_target = self.offset_target + (car_to_offset_perp * agent.me.hitbox.width * 0.6) + + turn_rad = turn_radius(abs(agent.me.local_velocity().x)) + if ray_intersects_with_sphere(self.ball_location, ray_direction, agent.me.location + Vector(y=turn_rad) * agent.me.right, turn_rad) or ray_intersects_with_sphere(self.ball_location, ray_direction, agent.me.location + Vector(y=-turn_rad) * agent.me.right, turn_rad): + final_target = true_final_target + + if distance_from_turn is not None: + car_turn_point = self.ball_location + ray_direction * distance_from_turn + part_dist = agent.me.location.flat_dist(car_turn_point) + distance_remaining = part_dist + car_turn_point.flat_dist(true_final_target) + part_dist -= turn_rad + + if distance_remaining is None: + distance_remaining = final_target.flat_dist(agent.me.location) + part_dist = distance_remaining # Some adjustment to the final target to ensure it's inside the field and we don't try to drive through any goalposts or walls to reach it (again) final_target = cap_in_field(agent, final_target) @@ -767,7 +871,6 @@ def run(self, agent: VirxERLU): # whether we should go forwards or backwards angle_to_target = abs(Vector(x=1).angle2D(agent.me.local_location(agent.ball.location) if self.jumping else local_final_target)) - direction = 1 if angle_to_target < 1.6 or agent.me.local_velocity().x > 1000 else -1 # drawing debug lines to show the dodge point and final target (which differs due to the adjustment) agent.line(agent.me.location, self.offset_target, agent.renderer.white()) @@ -776,31 +879,24 @@ def run(self, agent: VirxERLU): vf = agent.me.velocity + agent.gravity * T - distance_remaining = max((agent.me.local_location(self.offset_target).x if self.jumping else distance_remaining) - agent.me.hitbox.length * 0.45, 0) - speed_required = distance_remaining / time_remaining + distance_remaining -= agent.me.hitbox.length * 0.45 + distance_remaining = max(distance_remaining, 0) + speed_required = distance_remaining / max(T, agent.delta_time) + direction = 1 if angle_to_target < 1.6 or speed_required > 1410 else -1 agent.dbg_2d(f"Speed required: {round(speed_required, 2)}") - if speed_required < 1900 and agent.me.boost > 50 and T > 2 and distance_remaining > 2560: - agent.dbg_2d("Building speed") - speed_required *= 0.75 - if not self.jumping: - agent.dbg_2d(f"jump time: {self.needed_jump_time}") - velocity = defaultDrive(agent, speed_required * direction, local_final_target)[1] if velocity == 0: velocity = 1 - local_offset_target = agent.me.local_location(self.offset_target.flatten()) - true_angle_to_target = abs(Vector(x=1).angle2D(local_offset_target)) - local_vf = agent.me.local(vf.flatten()) - true_distance_remaining = self.offset_target.flat_dist(agent.me.location) - dodge_time = true_distance_remaining / (abs(velocity) + dodge_impulse(agent)) + local_vf = agent.me.local(agent.me.velocity * T).flatten() + dodge_time = part_dist / (abs(velocity) + dodge_impulse(agent)) - (self.needed_jump_time + dodge_offset) - if ((abs(velocity) < 100 and true_distance_remaining < agent.me.hitbox.length / 2) or (abs(local_offset_target.y) < agent.ball_radius and direction * local_vf.x >= direction * (local_offset_target.x - agent.me.hitbox.length * 0.45) and direction * local_offset_target.x > 0)) and T <= self.needed_jump_time * 1.025: - self.jumping = True + if (abs(velocity) < 100 and distance_remaining < agent.me.hitbox.length and Tj > -agent.delta_time * 6) or (abs(local_final_target.y) < agent.me.hitbox.width and direction * local_vf.x >= (direction * local_final_target.x) - agent.me.hitbox.length * 0.6 and direction * local_final_target.x > 0): + self.jumping = T <= self.needed_jump_time + dodge_offset + 0.05 elif agent.me.airborne: agent.push(recovery(final_target if Tj > 0 else None)) - elif T <= self.needed_jump_time or (Tj > 0 and true_distance_remaining > agent.me.hitbox.length / 2 and (not virxrlcu.jump_shot_is_viable(T, agent.boost_accel, tuple(agent.gravity), agent.me.get_raw(agent), self.offset_target.z, tuple((final_target - agent.me.location).normalize()), distance_remaining))): + elif Tj < -agent.delta_time * 6 or (Tj > 0.05 and distance_remaining > agent.me.hitbox.length / 2 and not virxrlcu.jump_shot_is_viable(T, agent.boost_accel, tuple(agent.gravity), agent.me.get_raw(agent), self.offset_target.z, tuple((final_target - agent.me.location).normalize()), distance_remaining)): # 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() agent.shooting = False @@ -808,11 +904,10 @@ def run(self, agent: VirxERLU): agent.shot_time = -1 if agent.me.airborne: agent.push(recovery()) - elif agent.boost_amount != 'unlimited' and self.needed_jump_time * 1.075 > dodge_time: - dodge_time -= self.needed_jump_time * 1.075 - if agent.me.boost < 48 and angle_to_target < 0.03 and (true_angle_to_target < 0.1 or distance_remaining > 4480) and velocity > 600 and dodge_time >= 1: + elif dodge_time >= 1.2 and agent.time - agent.me.land_time > 0.5: + if agent.me.boost < 48 and angle_to_target < 0.03 and velocity < speed_required - 50 and velocity - speed_required < dodge_impulse(agent) * 3: agent.push(flip(agent.me.local_location(self.offset_target))) - elif direction == -1 and velocity < 200 and dodge_time >= 1.5: + elif direction == -1 and velocity < 200: agent.push(flip(agent.me.local_location(self.offset_target), True)) else: if self.jump_time == -1: @@ -821,26 +916,27 @@ def run(self, agent: VirxERLU): jump_elapsed = agent.time - self.jump_time tau = jump_max_duration - jump_elapsed - xf = agent.me.location + agent.me.velocity * T + 0.5 * agent.gravity * T * T + Tj2 = max(T - dodge_offset, agent.delta_time) + + xf = agent.me.location + agent.me.velocity * Tj2 + 0.5 * agent.gravity * Tj2 * Tj2 if jump_elapsed == 0: vf += agent.me.up * jump_speed - xf += agent.me.up * jump_speed * T + xf += agent.me.up * jump_speed * Tj2 hf = vf.z vf += agent.me.up * jump_acc * tau - xf += agent.me.up * jump_acc * tau * (T - 0.5 * tau) + xf += agent.me.up * jump_acc * tau * (Tj2 - 0.5 * tau) delta_x = self.offset_target - xf d_direction = delta_x.normalize() - if T > 0 and direction == 1 and abs(agent.me.forward.dot(d_direction)) > 0.75: + if T > 0 and abs(agent.me.forward.angle(d_direction)) < 0.5: delta_v = delta_x.dot(agent.me.forward) / T - if agent.me.boost > 0 and delta_v >= agent.boost_accel * 0.1: + if agent.me.airborne and agent.me.boost > 0 and delta_v >= agent.boost_accel * 0.1 + throttle_accel * agent.delta_time: agent.controller.boost = True - delta_v -= agent.boost_accel * 0.1 - - if abs(delta_v) >= throttle_accel * agent.delta_time: + agent.controller.throttle = 1 + elif abs(delta_v) >= throttle_accel * agent.delta_time * 0.1: agent.controller.throttle = cap(delta_v / (throttle_accel * agent.delta_time), -1, 1) if T <= -0.8 or (not agent.me.airborne and self.counter >= 3): @@ -851,26 +947,33 @@ def run(self, agent: VirxERLU): agent.push(recovery()) return else: - local_flip_target = agent.ball.location - (self.shot_vector * agent.ball_radius) - if self.counter == 3 and agent.me.location.dist(local_flip_target) < (agent.ball_radius + agent.me.hitbox.length) * 1.02 and T <= 0.05: + if self.counter == 3 and T < dodge_offset: # Get the required pitch and yaw to flip correctly - vector = Vector(agent.me.local_location(agent.ball.location).x, agent.me.local_location(local_flip_target).y).normalize() + local_flip_target = agent.me.local_location(agent.ball.location - (self.shot_vector * agent.ball_radius)).flatten().normalize() + + if agent.me.location.flat_dist(agent.ball.location) < agent.me.hitbox.length + agent.ball_radius: + local_dir_target = agent.me.local(self.shot_vector.flatten()) + vector = Vector((1 if local_dir_target.x >= 0 else -1) * abs(local_flip_target.x), (1 if local_dir_target.y >= 0 else -1) * abs(local_flip_target.y)) + else: + vector = local_flip_target + scale = 1 / max(abs(vector.x), abs(vector.y)) self.p = cap(-vector.x * scale, -1, 1) self.y = cap(vector.y * scale, -1, 1) agent.controller.pitch = self.p agent.controller.yaw = self.y - # Wait 1 more frame before dodging + self.counter += 1 - elif self.counter == 4: + elif self.counter > 3: # Dodge - agent.controller.jump = True + agent.controller.jump = self.counter > 5 agent.controller.pitch = self.p agent.controller.yaw = self.y + self.counter += 1 else: # Face the target as much as possible - defaultPD(agent, agent.me.local_location(self.offset_target) * direction) + defaultPD(agent, agent.me.local_location(final_target + Vector(z=self.offset_target.z)) * direction) if jump_elapsed <= jump_max_duration and hf <= self.offset_target.z: # Initial jump to get airborne + we hold the jump button for extra power as required @@ -880,7 +983,7 @@ def run(self, agent: VirxERLU): self.counter += 1 l_vf = vf + agent.me.location - agent.line(l_vf-Vector(z=100), l_vf+Vector(z=100), agent.renderer.red()) + agent.line(l_vf-Vector(z=agent.ball_radius), l_vf+Vector(z=agent.ball_radius), agent.renderer.red()) class ground_shot: @@ -892,56 +995,85 @@ def __init__(self, intercept_time, targets=None): self.intercept_time = intercept_time self.targets = targets + self.upgrade_intercept_time = None + self.upgrade_targets = None + self.upgrade = False + def update(self, shot): - self.intercept_time = shot.intercept_time - self.targets = shot.targets + self.upgrade_intercept_time = shot.intercept_time + self.upgrade_targets = shot.targets + self.upgrade = True + + def preprocess(self, agent: VirxERLU): + T = self.intercept_time - agent.time + slice_n = round(T * 60) - 1 + ball = agent.ball_prediction_struct.slices[slice_n].physics.location + ball_location = Vector(ball.x, ball.y, ball.z) + car_to_ball_norm = (ball_location - agent.me.location).normalize() + shot_vector = car_to_ball_norm if self.targets is None else car_to_ball_norm.clamp((self.targets[0] - ball_location).normalize(), (self.targets[1] - ball_location).normalize()) + + if self.shot_vector is None or self.ball_location.dist(ball_location) > 5: + self.ball_location = ball_location + self.shot_vector = shot_vector + self.offset_target = self.ball_location - (self.shot_vector * agent.ball_radius) def run(self, agent: VirxERLU): if not agent.shooting: agent.shooting = True T = self.intercept_time - agent.time - # Capping T above 0 to prevent division problems - time_remaining = cap(T, 0.000001, 6) - if(T > 0.1 and agent.odd_tick % 2 == 0) or self.ball_location is None: - slice_n = round(T * 60) - 1 - agent.dbg_2d(f"Shot slice #: {slice_n}") + if (T > 0.2 and agent.odd_tick % 2 == 0) or self.ball_location is None: + if self.upgrade: + self.intercept_time = self.upgrade_intercept_time + self.targets = self.upgrade_targets + self.upgrade = False - ball = agent.ball_prediction_struct.slices[slice_n].physics.location - self.ball_location = Vector(ball.x, ball.y, ball.z) + T = self.intercept_time - agent.time - direction = (self.ball_location - agent.me.location).normalize() - self.shot_vector = direction if self.targets is None else direction.clamp2D((self.targets[0] - self.ball_location).normalize(), (self.targets[1] - self.ball_location).normalize()) - self.offset_target = self.ball_location - (self.shot_vector * agent.ball_radius) + self.preprocess(agent) + agent.sphere(self.ball_location, agent.ball_radius) + # Capping T above 0 to prevent division problems + time_remaining = cap(T, 0.000001, 6) - l_ball = agent.me.local(agent.ball.location) car_to_ball = agent.ball.location - agent.me.location - if abs(l_ball.y) < agent.ball_radius + agent.me.hitbox.width / 2 and abs(l_ball.z) < agent.ball_radius + agent.me.hitbox.height / 2: - final_target = agent.ball.location - (self.shot_vector * agent.ball_radius) - distance_remaining = max(agent.me.local_location(final_target).x - agent.me.hitbox.length * 0.45, 0) - speed_required = 2300 - agent.dbg_2d(f"Max speed") - else: - # Some adjustment to the final target to ensure it's inside the field and we don't try to drive through any goalposts or walls to reach it - final_target = self.offset_target.copy().flatten() + final_target = self.offset_target.flatten() + distance_remaining = None - if self.targets is not None: + if self.targets is not None: + angle_to_shot_vector = abs(car_to_ball.angle2D(self.shot_vector)) + if angle_to_shot_vector > no_adjust_radians: # whether we are to the left or right of the shot vector side_of_shot = sign(self.shot_vector.cross(Vector(z=1)).dot(car_to_ball)) car_to_offset_target = final_target - agent.me.location - car_to_offset_perp = car_to_offset_target.cross(Vector(z=side_of_shot)) # perpendicular + car_to_offset_perp = car_to_offset_target.cross(Vector(z=side_of_shot)).normalize() # perpendicular ray + final_target += (-(self.shot_vector * (2560 - agent.ball_radius))) if angle_to_shot_vector > min_adjust_radians else (car_to_offset_perp * agent.me.hitbox.width * 0.6) + + if angle_to_shot_vector > min_adjust_radians: + ray_direction = (-self.shot_vector).rotate2D(side_of_shot * -min_adjust_radians) + distance_from_turn = ray_intersects_with_line(self.ball_location, ray_direction, agent.me.location, final_target) + true_final_target = self.offset_target + (car_to_offset_perp * agent.me.hitbox.width * 0.6) + + turn_rad = turn_radius(abs(agent.me.local_velocity().x)) + if ray_intersects_with_sphere(self.ball_location, ray_direction, agent.me.location + Vector(y=turn_rad) * agent.me.right, turn_rad) or ray_intersects_with_sphere(self.ball_location, ray_direction, agent.me.location + Vector(y=-turn_rad) * agent.me.right, turn_rad): + final_target = true_final_target + + if distance_from_turn is not None: + car_turn_point = self.ball_location + ray_direction * distance_from_turn + part_dist = agent.me.location.flat_dist(car_turn_point) + distance_remaining = part_dist + car_turn_point.flat_dist(true_final_target) + part_dist -= turn_rad + + if distance_remaining is None: + distance_remaining = final_target.flat_dist(agent.me.location) + part_dist = distance_remaining - # 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_offset_target.angle2D(self.shot_vector) * cap(T, 0.5, 3) * 750 # size of adjustment - # we don't adjust the final target if we are already jumping - final_target += car_to_offset_perp.normalize() * adjustment + distance_remaining -= agent.me.hitbox.length * 0.45 + distance_remaining = max(distance_remaining, 0) - distance_remaining = max(self.offset_target.flat_dist(agent.me.location) - agent.me.hitbox.length * 0.45, 0) - speed_required = distance_remaining / time_remaining - agent.dbg_2d(f"Speed required: {speed_required}") + speed_required = distance_remaining / time_remaining + agent.dbg_2d(f"Speed required: {round(speed_required)}") # Some adjustment to the final target to ensure it's inside the field and we don't try to drive through any goalposts or walls to reach it (again) final_target = cap_in_field(agent, final_target) @@ -950,31 +1082,30 @@ def run(self, agent: VirxERLU): # the angle to the final target, in radians angle_to_target = abs(Vector(x=1).angle2D(local_final_target)) # whether we should go forwards or backwards - direction = 1 if angle_to_target < 1.6 or agent.me.local_velocity().x > 1000 else -1 + direction = 1 if angle_to_target < 1.6 or speed_required > 1410 or (abs(speed_required) < 100 and angle_to_target < 1.7) else -1 # drawing debug lines to show the dodge point and final target (which differs due to the adjustment) agent.line(agent.me.location, self.offset_target, agent.renderer.white()) - agent.line(self.offset_target-Vector(z=100), self.offset_target+Vector(z=100), agent.renderer.green()) - agent.line(final_target-Vector(z=100), final_target+Vector(z=100), agent.renderer.purple()) + agent.line(self.offset_target-Vector(z=agent.ball_radius), self.offset_target+Vector(z=agent.ball_radius), agent.renderer.green()) + agent.line(final_target-Vector(z=agent.ball_radius), final_target+Vector(z=agent.ball_radius), agent.renderer.purple()) velocity = defaultDrive(agent, speed_required * direction, local_final_target)[1] if velocity == 0: velocity = 1 - local_offset_target = agent.me.local_location(self.offset_target.flatten()) - true_angle_to_target = abs(Vector(x=1).angle2D(local_offset_target)) - true_distance_remaining = self.offset_target.flat_dist(agent.me.location) - time = true_distance_remaining / (abs(velocity) + dodge_impulse(agent)) + dodge_time = part_dist / (abs(velocity) + dodge_impulse(agent)) - 0.3 vf = agent.me.velocity + agent.gravity * T local_vf = agent.me.local(vf.flatten()) - if T > 0.25 and T < 0.35 and (direction == -1 or abs(velocity) + dodge_impulse(agent) <= (abs(speed_required) if agent.me.boost > 24 else 1900) or self.shot_vector.angle2D((final_target - agent.me.location)) > 0.05) and ((abs(velocity) < 100 and true_distance_remaining < agent.me.hitbox.length / 2) or (abs(local_offset_target.y) < agent.ball_radius and direction * local_vf.x >= direction * (local_offset_target.x - agent.me.hitbox.length / 2) and direction * local_offset_target.x > 0)): + if 0.25 < T and T < 0.35 and (direction == -1 or agent.me.local_velocity().x < 900): agent.pop() - local_flip_target = agent.me.local_location(agent.ball.location - (self.shot_vector * agent.ball_radius)) + Vector(agent.ball_radius) - agent.push(flip(local_flip_target, cancel=abs(Vector(x=1).angle2D(local_flip_target)) > 1.6)) + local_flip_target = agent.me.local_location(agent.ball.location - (self.shot_vector * agent.ball_radius)) + local_dir_target = agent.me.local(self.shot_vector) + vector = Vector((1 if local_dir_target.x >= 0 else -1) * abs(agent.me.local_location(agent.ball.location).x), (1 if local_dir_target.y else -1) * abs(local_flip_target.y)) + agent.push(flip(vector, cancel=abs(Vector(x=1).angle2D(local_flip_target)) > 1.6)) elif agent.me.airborne: agent.push(recovery(final_target if T > 0.5 else None)) - elif T <= 0 or (T > 0.75 and true_distance_remaining > agent.me.hitbox.length / 2 and not virxrlcu.ground_shot_is_viable(T, agent.boost_accel, agent.me.get_raw(agent), self.offset_target.z, tuple((final_target - agent.me.location).normalize()), distance_remaining)): + elif T <= -agent.delta_time * 6 or (T > 0.75 and distance_remaining > agent.me.hitbox.length / 2 + agent.ball_radius and not virxrlcu.ground_shot_is_viable(T, agent.boost_accel, agent.me.get_raw(agent), self.offset_target.z, tuple((final_target - agent.me.location).normalize()), distance_remaining)): # If we're out of time or not fast enough, we pop agent.pop() agent.shooting = False @@ -982,10 +1113,10 @@ def run(self, agent: VirxERLU): agent.shot_time = -1 if agent.me.airborne: agent.push(recovery()) - elif agent.boost_amount != 'unlimited' and T + 0.1 > time: - if agent.me.boost < 48 and angle_to_target < 0.03 and (true_angle_to_target < 0.1 or distance_remaining > 4480) and velocity > 500 and time > 1: + elif dodge_time >= 1.2 and agent.time - agent.me.land_time > 0.5: + if agent.me.boost < 48 and angle_to_target < 0.03 and velocity < speed_required - 50 and velocity - speed_required < dodge_impulse(agent) * 3: agent.push(flip(agent.me.local_location(self.offset_target))) - elif direction == -1 and velocity < 200 and time >= 1.5: + elif direction == -1 and velocity < 200: agent.push(flip(agent.me.local_location(self.offset_target), True)) @@ -1100,30 +1231,31 @@ def run(self, agent: VirxERLU): target_vector = -ball_to_target.clamp2D(left_vector, right_vector) final_target = agent.ball.location + (target_vector*(distance/2)) angle_to_target = abs(Vector(x=1).angle2D(agent.me.local_location(final_target))) - distance_remaining = agent.me.location.dist(final_target) - agent.me.hitbox.length * 0.45 + distance_remaining = (agent.me.location.flat_dist(final_target) - agent.me.hitbox.length * 0.45) + (agent.me.location.z - agent.me.hitbox.height / 2) # Some adjustment to the final target to ensure we don't try to drive through any goalposts to reach it final_target = cap_in_field(agent, final_target) local_final_target = agent.me.local_location(final_target) - agent.line(final_target-Vector(z=100), final_target + Vector(z=100), (255, 255, 255)) - direction = -1 if abs(Vector(x=1).angle2D(local_final_target)) > 2 and agent.me.local_velocity().x < 1000 else 1 + agent.line(final_target-Vector(z=agent.ball_radius), final_target + Vector(z=agent.ball_radius), (255, 255, 255)) + direction = 1 if angle_to_target < 1.6 else -1 angles, velocity = defaultDrive(agent, 1410 * direction, local_final_target) if velocity == 0: velocity = 1 - time = distance_remaining / (abs(velocity) + dodge_impulse(agent)) + dodge_time = distance_remaining / (abs(velocity) + dodge_impulse(agent)) - 0.3 if abs(angles[1]) < 0.05 and (eta < 0.45 or distance < 150): agent.pop() agent.shooting = False agent.shot_weight = -1 agent.shot_time = -1 agent.push(flip(agent.me.local(car_to_ball))) - elif agent.boost_amount != 'unlimited' and agent.me.location.z < 50 and agent.me.boost < 48 and angle_to_target < 0.03 and velocity > 500 and time >= 1.5: - speed_gain_routine = wave_dash if agent.gravity.z < -450 and time < 3 and time > 1 else flip - agent.push(speed_gain_routine(agent.me.local_location(agent.ball.location))) - elif agent.boost_amount != 'unlimited' and agent.me.location.z < 50 and direction == -1 and velocity < 200 and time >= 1.5: - agent.push(flip(agent.me.local_location(agent.ball.location), True)) + elif dodge_time >= 1.2 and agent.time - agent.me.land_time > 0.2: + if agent.me.boost < 48 and angle_to_target < 0.03 and (angle_to_target < 0.1 or distance_remaining > 4480) and velocity > 600 and velocity < 1360: + agent.push(flip(agent.me.local_location(self.offset_target))) + elif direction == -1 and velocity < 200: + agent.push(flip(agent.me.local_location(self.offset_target), True)) + class boost_down: def __init__(self): diff --git a/RLBotPack/ElkBot/util/tools.py b/RLBotPack/ElkBot/util/tools.py index a9250383..25039447 100644 --- a/RLBotPack/ElkBot/util/tools.py +++ b/RLBotPack/ElkBot/util/tools.py @@ -76,8 +76,10 @@ def find_shot(agent, target, cap_=6, can_aerial=True, can_double_jump=True, can_ can_ground = is_on_ground and can_ground can_jump = is_on_ground and can_jump can_double_jump = is_on_ground and can_double_jump + can_aerial = (not is_on_ground or agent.time - agent.me.land_time > 0.5) and can_aerial + any_ground = can_ground or can_jump or can_double_jump - if not can_ground and not can_jump and not can_double_jump and not can_aerial: + if not any_ground and not can_aerial: return # Here we get the slices that need to be searched - by defining a cap, we can reduce the number of slices and improve search times @@ -135,8 +137,10 @@ def find_any_shot(agent, cap_=6, can_aerial=True, can_double_jump=True, can_jump can_ground = is_on_ground and can_ground can_jump = is_on_ground and can_jump can_double_jump = is_on_ground and can_double_jump + can_aerial = (not is_on_ground or agent.time - agent.me.land_time > 0.5) and can_aerial + any_ground = can_ground or can_jump or can_double_jump - if not can_ground and not can_jump and not can_double_jump and not can_aerial: + if not any_ground and not can_aerial: return # Here we get the slices that need to be searched - by defining a cap, we can reduce the number of slices and improve search times @@ -188,13 +192,13 @@ def get_slices(agent, cap_): if agent.shooting and agent.stack[0].__class__.__name__ != "short_shot": # Get the time remaining time_remaining = agent.stack[0].intercept_time - agent.time - if time_remaining < 0.5 and time_remaining >= 0: + if 0 < time_remaining < 0.5: return # if the shot is done but it's working on it's 'follow through', then ignore this stuff if time_remaining > 0: # Convert the time remaining into number of slices, and take off the minimum gain accepted from the time - min_gain = 0.05 + min_gain = 0.2 end_slice = round(min(time_remaining - min_gain, cap_) * 60) if end_slices is None: diff --git a/RLBotPack/ElkBot/util/utils.py b/RLBotPack/ElkBot/util/utils.py index 50a8d2a2..34a464e5 100644 --- a/RLBotPack/ElkBot/util/utils.py +++ b/RLBotPack/ElkBot/util/utils.py @@ -1,18 +1,23 @@ from queue import Full -from util.agent import Vector, math +from util.agent import Vector, VirxERLU, math COAST_ACC = 525.0 -BREAK_ACC = 3500 +BRAKE_ACC = 3500 MIN_BOOST_TIME = 0.1 +REACTION_TIME = 0.04 + +BRAKE_COAST_TRANSITION = -(0.45 * BRAKE_ACC + 0.55 * COAST_ACC) +COASTING_THROTTLE_TRANSITION = -0.5 * COAST_ACC +MIN_WALL_SPEED = -0.5 * BRAKE_ACC def cap(x, low, high): # caps/clamps a number between a low and high value - return max(min(x, high), low) + return low if x < low else (high if x > high else x) -def cap_in_field(agent, target): +def cap_in_field(agent: VirxERLU, target): if abs(target.x) > 893 - agent.me.hitbox.length: target.y = cap(target.y, -5120 + agent.me.hitbox.length, 5120 - agent.me.hitbox.length) target.x = cap(target.x, -893 + agent.me.hitbox.length, 893 - agent.me.hitbox.length) if abs(agent.me.location.y) > 5120 - (agent.me.hitbox.length / 2) else cap(target.x, -4093 + agent.me.hitbox.length, 4093 - agent.me.hitbox.length) @@ -20,7 +25,7 @@ def cap_in_field(agent, target): return target -def defaultPD(agent, local_target, upside_down=False, up=None): +def defaultPD(agent: VirxERLU, local_target, upside_down=False, up=None): # points the car towards a given local target. # Direction can be changed to allow the car to steer towards a target while driving backwards @@ -40,41 +45,69 @@ def defaultPD(agent, local_target, upside_down=False, up=None): return target_angles -def defaultThrottle(agent, target_speed, target_angles=None, local_target=None): +def defaultThrottle(agent: VirxERLU, target_speed, target_angles=None, local_target=None): # accelerates the car to a desired speed using throttle and boost - car_speed = agent.me.local_velocity().x - - if not agent.me.airborne: - if target_angles is not None and local_target is not None: - turn_rad = turn_radius(abs(car_speed)) - agent.controller.handbrake = not agent.me.airborne and agent.me.velocity.magnitude() > 250 and (is_inside_turn_radius(turn_rad, local_target, sign(agent.controller.steer)) if abs(local_target.y) < turn_rad else abs(local_target.x) < turn_rad) - - angle_to_target = abs(target_angles[1]) - if target_speed < 0: - angle_to_target = math.pi - angle_to_target - if agent.controller.handbrake: - if angle_to_target > 2.6: - agent.controller.steer = sign(agent.controller.steer) - agent.controller.handbrake = False - else: - agent.controller.steer = agent.controller.yaw - - t = target_speed - car_speed - ta = throttle_acceleration(abs(car_speed)) * agent.delta_time - if ta != 0: - agent.controller.throttle = cap(t / ta, -1, 1) - elif sign(target_speed) * t > -COAST_ACC * agent.delta_time: - agent.controller.throttle = sign(target_speed) - elif sign(target_speed) * t <= -COAST_ACC * agent.delta_time: - agent.controller.throttle = sign(t) - - if not agent.controller.handbrake: - agent.controller.boost = t - ta >= agent.boost_accel * MIN_BOOST_TIME + car_speed = agent.me.forward.dot(agent.me.velocity) + + if agent.me.airborne: + return car_speed + + if target_angles is not None and local_target is not None: + turn_rad = turn_radius(abs(car_speed)) + agent.controller.handbrake = not agent.me.airborne and agent.me.velocity.magnitude() > 600 and (is_inside_turn_radius(turn_rad, local_target, sign(agent.controller.steer)) if abs(local_target.y) < turn_rad or car_speed > 1410 else abs(local_target.x) < turn_rad) + + angle_to_target = abs(target_angles[1]) + + if target_speed < 0: + angle_to_target = math.pi - angle_to_target + + if agent.controller.handbrake: + if angle_to_target > 2.6: + agent.controller.steer = sign(agent.controller.steer) + agent.controller.handbrake = False + else: + agent.controller.steer = agent.controller.yaw + + # Thanks to Chip's RLU speed controller for this + # https://github.com/samuelpmish/RLUtilities/blob/develop/src/mechanics/drive.cc#L182 + # I had to make a few changes because it didn't play very nice with driving backwards + + t = target_speed - car_speed + acceleration = t / REACTION_TIME + if car_speed < 0: acceleration *= -1 # if we're going backwards, flip it so it thinks we're driving forwards + + brake_coast_transition = BRAKE_COAST_TRANSITION + coasting_throttle_transition = COASTING_THROTTLE_TRANSITION + throttle_accel = throttle_acceleration(car_speed) + throttle_boost_transition = 1 * throttle_accel + 0.5 * agent.boost_accel + + if agent.me.up.z < 0.7: + brake_coast_transition = coasting_throttle_transition = MIN_WALL_SPEED + + # apply brakes when the desired acceleration is negative and large enough + if acceleration <= brake_coast_transition: + agent.controller.throttle = -1 + + # let the car coast when the acceleration is negative and small + elif brake_coast_transition < acceleration and acceleration < coasting_throttle_transition: + pass + + # for small positive accelerations, use throttle only + elif coasting_throttle_transition <= acceleration and acceleration <= throttle_boost_transition: + agent.controller.throttle = 1 if throttle_accel == 0 else cap(acceleration / throttle_accel, 0.02, 1) + + # if the desired acceleration is big enough, use boost + elif throttle_boost_transition < acceleration: + agent.controller.throttle = 1 + if t > 0 and not agent.controller.handbrake and angle_to_target < 1: agent.controller.boost = True # don't boost when we need to lose speed, we we're using handbrake, or when we aren't facing the target + + if car_speed < 0: + agent.controller.throttle *= -1 # earlier we flipped the sign of the acceleration, so we have to flip the sign of the throttle for it to be correct return car_speed -def defaultDrive(agent, target_speed, local_target): +def defaultDrive(agent: VirxERLU, target_speed, local_target): target_angles = defaultPD(agent, local_target) velocity = defaultThrottle(agent, target_speed, target_angles, local_target) @@ -205,7 +238,7 @@ def invlerp(a, b, v): return (v - a) / (b - a) -def send_comm(agent, msg): +def send_comm(agent: VirxERLU, msg): message = { "index": agent.index, "team": agent.team @@ -256,3 +289,39 @@ def dodge_impulse(agent): if dif > 0: impulse -= dif return impulse + + +def ray_intersects_with_line(origin, direction, point1, point2): + v1 = origin - point1 + v2 = point2 - point1 + v3 = Vector(-direction.y, direction.x) + v_dot = v2.dot(v3) + + t1 = v2.cross(v1).magnitude() / v_dot + + if t1 < 0: + return + + t2 = v1.dot(v3) / v_dot + + if 0 <= t1 and t2 <= 1: + return t1 + + +def ray_intersects_with_sphere(origin, direction, center, radius): + L = center - origin + tca = L.dot(direction) + + if tca < 0: + return False + + d2 = L.dot(L) - tca * tca + + if d2 > radius: + return False + + thc = math.sqrt(radius * radius - d2) + t0 = tca - thc + t1 = tca + thc + + return t0 > 0 or t1 > 0 From 62eb594d22b7b0ac8b409f5e14e0f190fc69996a Mon Sep 17 00:00:00 2001 From: rivques <38469076+rivques@users.noreply.github.com> Date: Tue, 18 May 2021 14:58:10 -0400 Subject: [PATCH 2/2] Add tags to ElkBot --- RLBotPack/ElkBot/ExampleBot.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RLBotPack/ElkBot/ExampleBot.cfg b/RLBotPack/ElkBot/ExampleBot.cfg index 0235f029..0ff7a37c 100644 --- a/RLBotPack/ElkBot/ExampleBot.cfg +++ b/RLBotPack/ElkBot/ExampleBot.cfg @@ -28,3 +28,5 @@ github = https://github.com/rivques/ElkBot # Programming language language = Python + +tags = 1v1, teamplay