diff --git a/src/bot.py b/src/bot.py index 2638d0c..088b637 100644 --- a/src/bot.py +++ b/src/bot.py @@ -11,6 +11,7 @@ from util.spikes import SpikeWatcher from util.vec import Vec3 +# Would you like to use numpy utilities? Check out the np_util folder! class MyBot(BaseAgent): @@ -97,7 +98,7 @@ def find_correction(current: Vec3, ideal: Vec3) -> float: def draw_debug(renderer, text_lines: List[str]): renderer.begin_rendering() - y = 200 + y = 250 for line in text_lines: renderer.draw_string_2d(50, y, 1, 1, line, renderer.yellow()) y += 20 diff --git a/src/np_util/data.py b/src/np_util/data.py new file mode 100644 index 0000000..6c23ec2 --- /dev/null +++ b/src/np_util/data.py @@ -0,0 +1,141 @@ +'''Rocket League data pre-processing.''' + +from .utils import Car, Ball, BoostPad, arr_from_list, arr_from_rot, arr_from_vec, orient_matrix + +import numpy as np + + +def setup(self, packet, field_info): + """Sets up the variables and classes for the agent. + + Arguments: + self {BaseAgent} -- The agent. + packet {GameTickPacket} -- Information about the game. + field_info {FieldInfoPacket} -- Information about the game field. + """ + + # Game info + self.game_time = packet.game_info.seconds_elapsed + self.dt = 1.0 / 120.0 + self.last_time = 0.0 + self.r_active = packet.game_info.is_round_active + self.ko_pause = packet.game_info.is_kickoff_pause + self.m_ended = packet.game_info.is_match_ended + self.gravity = packet.game_info.world_gravity_z + + # Creates Car objects for each car. + self.teammates = [] + self.opponents = [] + for index in range(packet.num_cars): + car = packet.game_cars[index] + if index == self.index: + self.player = Car(self.index, self.team, self.name) + elif car.team == self.team: + self.teammates.append(Car(index, car.team, car.name)) + else: + self.opponents.append(Car(index, car.team, car.name)) + + # Creates a Ball object. + self.ball = Ball() + + # Creates Boostpad objects. + self.l_pads = [] + self.s_pads = [] + for i in range(field_info.num_boosts): + pad = field_info.boost_pads[i] + pad_type = self.l_pads if pad.is_full_boost else self.s_pads + pad_obj = BoostPad(i, arr_from_vec(pad.location)) + pad_type.append(pad_obj) + + +def process(self, packet): + """Processes the gametick packet. + Arguments: + self {BaseAgent} -- The agent. + packet {GameTickPacket} -- The game packet being processed. + """ + + # Processing game info. + self.game_time = packet.game_info.seconds_elapsed + self.dt = self.game_time - self.last_time + self.last_time = self.game_time + self.r_active = packet.game_info.is_round_active + self.ko_pause = packet.game_info.is_kickoff_pause + self.m_ended = packet.game_info.is_match_ended + self.gravity = packet.game_info.world_gravity_z + + # Processing player data. + # From packet: + self.player.pos = arr_from_vec(packet.game_cars[self.player.index].physics.location) + self.player.rot = arr_from_rot(packet.game_cars[self.player.index].physics.rotation) + self.player.vel = arr_from_vec(packet.game_cars[self.player.index].physics.velocity) + self.player.ang_vel = arr_from_vec(packet.game_cars[self.player.index].physics.angular_velocity) + self.player.dead = packet.game_cars[self.player.index].is_demolished + self.player.wheel_c = packet.game_cars[self.player.index].has_wheel_contact + self.player.sonic = packet.game_cars[self.player.index].is_super_sonic + self.player.jumped = packet.game_cars[self.player.index].jumped + self.player.d_jumped = packet.game_cars[self.player.index].double_jumped + self.player.boost = packet.game_cars[self.player.index].boost + # Calculated: + self.player.orient_m = orient_matrix(self.player.rot, self.player.orient_m) + + # Processing teammates. + for teammate in self.teammates: + # From packet: + teammate.pos = arr_from_vec(packet.game_cars[teammate.index].physics.location) + teammate.rot = arr_from_rot(packet.game_cars[teammate.index].physics.rotation) + teammate.vel = arr_from_vec(packet.game_cars[teammate.index].physics.velocity) + teammate.ang_vel = arr_from_vec(packet.game_cars[teammate.index].physics.angular_velocity) + teammate.dead = packet.game_cars[teammate.index].is_demolished + teammate.wheel_c = packet.game_cars[teammate.index].has_wheel_contact + teammate.sonic = packet.game_cars[teammate.index].is_super_sonic + teammate.jumped = packet.game_cars[teammate.index].jumped + teammate.d_jumped = packet.game_cars[teammate.index].double_jumped + teammate.boost = packet.game_cars[teammate.index].boost + + # Processing opponents. + for opponent in self.opponents: + # From packet: + opponent.pos = arr_from_vec(packet.game_cars[opponent.index].physics.location) + opponent.rot = arr_from_rot(packet.game_cars[opponent.index].physics.rotation) + opponent.vel = arr_from_vec(packet.game_cars[opponent.index].physics.velocity) + opponent.ang_vel = arr_from_vec(packet.game_cars[opponent.index].physics.angular_velocity) + opponent.dead = packet.game_cars[opponent.index].is_demolished + opponent.wheel_c = packet.game_cars[opponent.index].has_wheel_contact + opponent.sonic = packet.game_cars[opponent.index].is_super_sonic + opponent.jumped = packet.game_cars[opponent.index].jumped + opponent.d_jumped = packet.game_cars[opponent.index].double_jumped + opponent.boost = packet.game_cars[opponent.index].boost + + # Processing Ball data. + self.ball.pos = arr_from_vec(packet.game_ball.physics.location) + self.ball.rot = arr_from_rot(packet.game_ball.physics.rotation) + self.ball.vel = arr_from_vec(packet.game_ball.physics.velocity) + self.ball.ang_vel = arr_from_vec(packet.game_ball.physics.angular_velocity) + self.ball.last_touch = packet.game_ball.latest_touch + + # Processing ball prediction. + ball_prediction_struct = self.get_ball_prediction_struct() + dtype = [ + ('physics', [ + ('pos', ' np.ndarray: + """Converts list to numpy array. + Arguments: + L {list} -- The list to convert containing 3 elements. + Returns: + np.array -- Numpy array with the same contents as the list. + """ + if len(L) != 3: + raise ValueError('Expected a list of length 3.') + return np.array(L[0], L[1], L[2]) + + +def arr_from_rot(R: Rotator) -> np.ndarray: + """Converts rotator to numpy array. + + Arguments: + R {Rotator} -- Rotator class containing pitch, yaw, and roll. + + Returns: + np.ndarray -- Numpy array with the same contents as the rotator. + """ + return np.array([R.pitch, R.yaw, R.roll]) + + +def arr_from_vec(V: Vector3) -> np.ndarray: + """Converts vector3 to numpy array. + + Arguments: + V {Vector3} -- Vector3 class containing x, y, and z. + + Returns: + np.ndarray -- Numpy array with the same contents as the vector3. + """ + return np.array([V.x, V.y, V.z]) + + +# ----------------------------------------------------------- + +# USEFUL UTILITY FUNCTIONS: + +def normalise(V: np.ndarray) -> np.ndarray: + """Normalises a vector. + + Arguments: + V {np.ndarray} -- Vector. + + Returns: + np.ndarray -- Normalised vector. + """ + magnitude = np.linalg.norm(V) + if magnitude != 0.0: + return V / magnitude + else: + return V + + +def angle_between_vectors(v1: np.ndarray, v2: np.ndarray) -> float: + """Returns the positive angle in radians between vectors v1 and v2. + + Arguments: + v1 {np.ndarray} -- First vector. + v2 {np.ndarray} -- Second vector + + Returns: + float -- Positive acute angle between the vectors in radians. + """ + v1_u = normalise(v1) + v2_u = normalise(v2) + return np.arccos(np.clip(np.dot(v1_u, v2_u), -1.0, 1.0)) + +# ----------------------------------------------------------- + +# FUNCTIONS FOR CONVERTING BETWEEN WORLD AND LOCAL COORDINATES: + +def orient_matrix(R: np.ndarray, A: np.ndarray = np.zeros((3, 3))) -> np.ndarray: + """Converts from Euler angles to an orientation matrix. + + Arguments: + R {np.ndarray} -- Pitch, yaw, and roll. + A {np.ndarray} -- Previous orientation matrix. (default: {np.zeros((3, 3))}) + + Returns: + np.ndarray -- Orientation matrix of shape (3, 3). + """ + # Credits to chip https://samuelpmish.github.io/notes/RocketLeague/aerial_control/ + pitch: float = R[0] + yaw: float = R[1] + roll: float = R[2] + + CR: float = np.cos(roll) + SR: float = np.sin(roll) + CP: float = np.cos(pitch) + SP: float = np.sin(pitch) + CY: float = np.cos(yaw) + SY: float = np.sin(yaw) + + A = np.zeros((3, 3)) + + # front direction + A[0, 0] = CP * CY + A[1, 0] = CP * SY + A[2, 0] = SP + + # right direction + A[0, 1] = CY * SP * SR - CR * SY + A[1, 1] = SY * SP * SR + CR * CY + A[2, 1] = -CP * SR + + # up direction + A[0, 2] = -CR * CY * SP - SR * SY + A[1, 2] = -CR * SY * SP + SR * CY + A[2, 2] = CP * CR + + return A + + +def local(A: np.ndarray, p0: np.ndarray, p1: np.ndarray) -> np.ndarray: + """Transforms world coordinates into local coordinates. + + Arguments: + A {np.ndarray} -- The local orientation matrix. + p0 {np.ndarray} -- World x, y, and z coordinates of the start point for the vector. + p1 {np.ndarray} -- World x, y, and z coordinates of the end point for the vector. + + Returns: + np.ndarray -- Local x, y, and z coordinates. + """ + return np.dot(A.T, p1 - p0) + + +def world(A: np.ndarray, p0: np.ndarray, p1: np.ndarray) -> np.ndarray: + """Transforms local into world coordinates. + + Arguments: + A {np.ndarray} -- The local orientation matrix. + p0 {np.ndarray} -- World x, y, and z coordinates of the start point for the vector. + p1 {np.ndarray} -- Local x, y, and z coordinates of the end point for the vector. + + Returns: + np.ndarray -- World x, y, and z coordinates. + """ + return p0 + A * p1 + +# ----------------------------------------------------------- + +# ROCKET LEAGUE SPECIFIC FUNCTIONS: + +def team_sign(team: int) -> int: + """Gives the sign for a calculation based on team. + + Arguments: + team {int} -- 0 if Blue, 1 if Orange. + + Returns: + int -- 1 if Blue, -1 if Orange + """ + # Other creative ways to do it: + + # return (1, -1)[team] + + # return 1 if team == 0 else -1 + + # for i in range(team): + # return 1 + # return -1 + + # return -range(-1, 2, -2)[team] + + return -2 * team + 1 + + +def turn_r(v: np.ndarray) -> float: + """Calculates the minimum turning radius for given velocity. + + Arguments: + v {np.ndarray} -- A velocity vector. + + Returns: + float -- The smallest radius possible for the given velocity. + """ + s = np.linalg.norm(v) + return -6.901E-11 * s**4 + 2.1815E-07 * s**3 - 5.4437E-06 * s**2 + 0.12496671 * s + 157 + +# ----------------------------------------------------------- + +# OTHER: + +def linear_predict(start_pos: np.ndarray, start_vel: np.ndarray, start_time: float, seconds: float) -> np.ndarray: + """Predicts motion of object in a straight line. + + Arguments: + start_pos {np.ndarray} -- Current position. + start_vel {np.ndarray} -- Current velocity. + start_time {float} -- Current time. + seconds {float} -- Time for which to predict. + + Returns: + np.ndarray -- linear prediction, 60 tps. + dtype = [ + ('pos', float, (ticks, 3)), + ('vel', float, (ticks, 3)), + ('time', float, (ticks, 1)) + ] + """ + ticks = int(60*seconds) + time = np.linspace(0, seconds, ticks)[:, np.newaxis] + pos = start_pos + time * start_vel + vel = np.ones_like(time) * start_vel + time += start_time + + dtype = [('pos', float, (ticks, 3)), ('vel', float, (ticks, 3)), ('time', float, (ticks, 1))] + prediction = np.array((pos, vel, time), dtype=dtype) + return prediction + + +def closest_to(pos: np.ndarray, others: np.ndarray) -> int: + """Finds the index of the closest point. + + Arguments: + pos {np.ndarray} -- coordinates of the position of interest. + others {np.ndarray} -- Array where each row is a position. + + Returns: + int -- index of the closest position to the position of interest. + """ + vectors = others - pos + distances = np.sqrt(np.einsum('ij,ij->i', vectors, vectors)) + return np.argmin(distances) # returns first if more than one is closest + + +def special_sauce(x: float, a: float) -> float: + """Modified sigmoid function. + + Arguments: + x {float} -- The input. + a {float} -- Constant; larger => faster rise. + + Returns: + float -- Output. + """ + # Graph showing how it can be used for steering: + # https://www.geogebra.org/m/udfp2zcy + return 2 / (1 + np.exp(a*x)) - 1