Search code examples
pythonpygamegame-physics

Cannot get ball to bounce in pygame


I have a function to map the path of my ball (gravity(t) returns 4.9t^2):

def path(x, y, p, a, t, bounce=False):
        vx, vy = p * math.cos(a), p * math.sin(a)  #Velocities
        if bounce: vx, vy = -vx, -vy
        dx, dy = vx * t, -(vy * t - gravity(t))  #Distances Traveled

        print(f'    x-pos: {dx + x:.0f}px')
        print(f'    y-pos: {abs(dy - y):.0f}px')

        return round(dx + x), round(dy + y)

I call it here:

else:
            time += .3 * speed_multiplier
            print('\n   time: %ss' % round(time, 2))
            if ball.y <= START_Y:
                if BARRIER < ball.x < SCREEN_WIDTH:
                    po = ball.path(x, y, power, ang, time)
                    ball.x, ball.y = po[0], po[1]
                else:
                    print('Out of Bounds!')
                    penalty = True
                    p_ticks = pg.time.get_ticks()
                    strokes += 1
                    shoot = False
                    if BARRIER < xb < SCREEN_WIDTH:
                        ball.x = xb
                    else:
                        ball.x = START_X
                    ball.y = yb
            else:
                shoot = False
                ball.y = START_Y

This works perfectly. Now, wanting to add bounce, I tried this:

else:
            time += .3 * speed_multiplier
            print('\n   time: %ss' % round(time, 2))
            if ball.y <= START_Y:
                if BARRIER < ball.x < SCREEN_WIDTH:
                    po = ball.path(x, y, power, ang, time)
                    ball.x, ball.y = po[0], po[1]
                else:
                    print('Out of Bounds!')
                    penalty = True
                    p_ticks = pg.time.get_ticks()
                    strokes += 1
                    shoot = False
                    if BARRIER < xb < SCREEN_WIDTH:
                        ball.x = xb
                    else:
                        ball.x = START_X
                    ball.y = yb
            else:
                po = ball.path(x, y, power, ang, time, True)
                ball.x, ball.y = po[0], po[1]

Bounce doesn't work, and the ball goes a bit down after each stroke after falling off the screen.

Any suggestions?

Edit – Here's the full code for those who'd like to run it:

import math
import pygame as pg

SCREEN_WIDTH = 1500
SCREEN_HEIGHT = 800
WINDOW_COLOR = (100, 100, 100)
LINE_COLOR = (0, 0, 255)
ALINE_COLOR = (0, 0, 0)
BARRIER = 1
START_X = int(.5 * SCREEN_WIDTH)
START_Y = int(.99 * SCREEN_HEIGHT)


pg.font.init()
strokeFont = pg.font.SysFont("monospace", 50)
STROKECOLOR = (255, 255, 0)

powerFont = pg.font.SysFont("arial", 15, bold=True)
POWERCOLOR = (0, 255, 0)

angleFont = pg.font.SysFont("arial", 15, bold=True)
ANGLECOLOR = (0, 255, 0)

penaltyFont = pg.font.SysFont("georgia", 40, bold=True)
PENALTYCOLOR = (255, 0, 0)

speedMultiplierFont = pg.font.SysFont("courier new", 13)
SPEEDMULTIPLIERCOLOR = (255, 0, 0)

powerMultiplierFont = pg.font.SysFont("courier new", 13)
POWERMULTIPLIERCOLOR = (255, 0, 0)


class Ball(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.radius = 10
        self.color = (255, 255, 255)
        self.outlinecolor = (255, 0, 0)

    def show(self, window):
        pg.draw.circle(window, self.outlinecolor, (self.x, self.y), self.radius)
        pg.draw.circle(window, self.color, (self.x, self.y), self.radius - int(.4 * self.radius))

    @staticmethod
    def path(x, y, p, a, t, bounce=False):
        vx, vy = p * math.cos(a), p * math.sin(a)  #Velocities
        if bounce: vx, vy = -vx, -vy
        dx, dy = vx * t, -(vy * t - gravity(t))  #Distances Traveled

        if bounce: pass

        print(f'    x-pos: {dx + x:.0f}px')
        print(f'    y-pos: {abs(dy - y):.0f}px')

        return round(dx + x), round(dy + y)

    @staticmethod
    def quadrant(x, y, xm, ym):
        if ym < y and xm > x:
            return 1
        elif ym < y and xm < x:
            return 2
        elif ym > y and xm < x:
            return 3
        elif ym > y and xm > x:
            return 4
        else:
            return False


def draw_window():
    window.fill(WINDOW_COLOR)
    ball.show(window)
    if not shoot:
        pg.draw.arrow(window, ALINE_COLOR, ALINE_COLOR, aline[0], aline[1], 5)
        pg.draw.arrow(window, LINE_COLOR, LINE_COLOR, line[0], line[1], 5)

    stroke_text = 'Strokes: %s' % strokes
    stroke_label = strokeFont.render(stroke_text, 1, STROKECOLOR)
    if not strokes:
        window.blit(stroke_label, (SCREEN_WIDTH - .21 * SCREEN_WIDTH, SCREEN_HEIGHT - .985 * SCREEN_HEIGHT))
    else:
        window.blit(stroke_label, (SCREEN_WIDTH - (.21+.02*math.floor(math.log10(strokes))) * SCREEN_WIDTH, SCREEN_HEIGHT - .985 * SCREEN_HEIGHT))

    power_text = 'Shot Strength: %sN' % power_display
    power_label = powerFont.render(power_text, 1, POWERCOLOR)
    if not shoot: window.blit(power_label, (cursor_pos[0] + .008 * SCREEN_WIDTH, cursor_pos[1]))

    angle_text = 'Angle: %s°' % angle_display
    angle_label = angleFont.render(angle_text, 1, ANGLECOLOR)
    if not shoot: window.blit(angle_label, (ball.x - .06 * SCREEN_WIDTH, ball.y - .01 * SCREEN_HEIGHT))

    if penalty:
        penalty_text = 'Out of Bounds! +1 Stroke'
        penalty_label = penaltyFont.render(penalty_text, 1, PENALTYCOLOR)
        penalty_rect = penalty_label.get_rect(center=(SCREEN_WIDTH/2, .225*SCREEN_HEIGHT))
        window.blit(penalty_label, penalty_rect)

    speed_multiplier_text = 'Speed: {:2.2f} m/s'.format(speed_multiplier)
    speed_multiplier_label = speedMultiplierFont.render(speed_multiplier_text, 1, SPEEDMULTIPLIERCOLOR)
    window.blit(speed_multiplier_label, (.91*SCREEN_WIDTH,.98*SCREEN_HEIGHT))

    power_multiplier_text = f'Strength: {int(power_multiplier*100)}%'
    power_multiplier_label = powerMultiplierFont.render(power_multiplier_text, 1, POWERMULTIPLIERCOLOR)
    window.blit(power_multiplier_label, (.01*SCREEN_WIDTH,.98*SCREEN_HEIGHT))

    #strength

    pg.display.flip()


def angle(cursor_pos):
    x, y, xm, ym = ball.x, ball.y, cursor_pos[0], cursor_pos[1]
    if x-xm:
        angle = math.atan((y - ym) / (x - xm))
    elif y > ym:
        angle = math.pi/2
    else:
        angle = 3*math.pi/2

    q = ball.quadrant(x,y,xm,ym)
    if q: angle = math.pi*math.floor(q/2) - angle

    if round(angle*deg) == 360:
        angle = 0

    if x > xm and not round(angle*deg):
        angle = math.pi

    return angle

def gravity(t):
    return 4.9*t**2


def arrow(screen, lcolor, tricolor, start, end, trirad):
    pg.draw.line(screen, lcolor, start, end, 2)
    rotation = (math.atan2(start[1] - end[1], end[0] - start[0])) + math.pi/2
    pg.draw.polygon(screen, tricolor, ((end[0] + trirad * math.sin(rotation),
                                        end[1] + trirad * math.cos(rotation)),
                                       (end[0] + trirad * math.sin(rotation - 120*rad),
                                        end[1] + trirad * math.cos(rotation - 120*rad)),
                                       (end[0] + trirad * math.sin(rotation + 120*rad),
                                        end[1] + trirad * math.cos(rotation + 120*rad))))
setattr(pg.draw, 'arrow', arrow)


def distance(x, y):
    return math.sqrt(x**2 + y**2)

def initialize():
    pg.init()
    pg.display.set_caption('Golf')
    window = pg.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
    pg.event.set_grab(True)
    pg.mouse.set_cursor((8, 8), (0, 0), (0, 0, 0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0, 0, 0))

    return window

rad, deg = math.pi/180, 180/math.pi
x, y, time, power, ang, strokes = 0, 0, 0, 0, 0, 0
xb, yb = None, None
shoot, penalty = False, False
p_ticks = 0

ball = Ball(START_X, START_Y)
quit = False

strength_dict = {0: .01, 1: .02, 2: .04, 3: .08, 4: .16, 5: .25, 6: .50, 7: .75, 8: 1}; stkey = 6
speed_dict = {0: .25, 1: .5, 2: 1, 3: 1.5, 4: 2, 5: 2.5, 6: 3, 7: 3.5, 8: 4, 9: 5, 10: 7.5, 11: 10}; spkey = 4

window = initialize()
try:
    while not quit:
        power_multiplier = strength_dict[stkey]
        speed_multiplier = speed_dict[spkey]

        seconds = (pg.time.get_ticks()-p_ticks)/1000
        if seconds > 1.2: penalty = False

        cursor_pos = pg.mouse.get_pos()
        line = [(ball.x, ball.y), cursor_pos]
        line_ball_x, line_ball_y = cursor_pos[0] - ball.x, cursor_pos[1] - ball.y

        aline = [(ball.x, ball.y), (ball.x + .015 * SCREEN_WIDTH, ball.y)]

        if not shoot:
            power_display = round(
                distance(line_ball_x, line_ball_y) * power_multiplier/5)

            angle_display = round(angle(cursor_pos) * deg)

        else:
            time += .3 * speed_multiplier
            print('\n   time: %ss' % round(time, 2))
            if ball.y <= START_Y:
                if BARRIER < ball.x < SCREEN_WIDTH:
                    po = ball.path(x, y, power, ang, time)
                    ball.x, ball.y = po[0], po[1]
                else:
                    print('Out of Bounds!')
                    penalty = True
                    p_ticks = pg.time.get_ticks()
                    strokes += 1
                    shoot = False
                    if BARRIER < xb < SCREEN_WIDTH:
                        ball.x = xb
                    else:
                        ball.x = START_X
                    ball.y = yb
            else:
                po = ball.path(x, y, power, ang, time, True)
                ball.x, ball.y = po[0], po[1]

        for event in pg.event.get():
            if event.type == pg.QUIT:
                quit = True

            if event.type == pg.KEYDOWN:
                if event.key == pg.K_ESCAPE:
                    quit = True

                if event.key == pg.K_RIGHT:
                    if spkey != max(speed_dict):
                        spkey += 1

                if event.key == pg.K_LEFT:
                    if spkey != min(speed_dict):
                        spkey -= 1

                if event.key == pg.K_UP:
                    if stkey != max(strength_dict):
                        stkey += 1

                if event.key == pg.K_DOWN:
                    if stkey != min(strength_dict):
                        stkey -= 1

            if event.type == pg.MOUSEBUTTONDOWN:
                if not shoot:
                    shoot = True
                    x, y = ball.x, ball.y
                    xb, yb = ball.x, ball.y
                    time, power = 0, (
                        distance(line_ball_x, line_ball_y)) * power_multiplier/6
                    print('\n\nBall Hit!')
                    print('\npower: %sN' % round(power, 2))
                    ang = angle(cursor_pos)
                    print('angle: %s°' % round(ang * deg, 2))
                    print('cos(a): %s' % round(math.cos(ang), 2)), print('sin(a): %s' % round(math.sin(ang), 2))
                    strokes += 1

        draw_window()

    print("\nShutting down...")
    pg.quit()

except Exception as error:
    print(f'A fatal error ({error}) has occurred. The program is shutting down.')
    pg.quit()

Edit 2 – For those who'd like to see the final solution, here's my current code:

import math
import pygame as pg


SCREEN_WIDTH = 1500
SCREEN_HEIGHT = 800
WINDOW_COLOR = (100, 100, 100)

LINE_COLOR = (0, 0, 255)
ALINE_COLOR = (0, 0, 0)

BARRIER = 1
BOUNCE_FUZZ = 0

START_X = int(.5 * SCREEN_WIDTH)
START_Y = int(.99 * SCREEN_HEIGHT)


pg.font.init()
strokeFont = pg.font.SysFont("monospace", 50)
STROKECOLOR = (255, 255, 0)

powerFont = pg.font.SysFont("arial", 15, bold=True)
POWERCOLOR = (0, 255, 0)

angleFont = pg.font.SysFont("arial", 15, bold=True)
ANGLECOLOR = (0, 255, 0)

penaltyFont = pg.font.SysFont("georgia", 40, bold=True)
PENALTYCOLOR = (255, 0, 0)

speedMultiplierFont = pg.font.SysFont("courier new", 13)
SPEEDMULTIPLIERCOLOR = (255, 0, 0)

powerMultiplierFont = pg.font.SysFont("courier new", 13)
POWERMULTIPLIERCOLOR = (255, 0, 0)


class Ball(object):
    def __init__(self, x, y, dx = 0, dy = 0, brate = .8):
        self.x = x
        self.y = y
        self.dx = dx
        self.dy = dy
        self.brate = brate
        self.radius = 10
        self.color = (255, 255, 255)
        self.outlinecolor = (255, 0, 0)

    def show(self, window):
        pg.draw.circle(window, self.outlinecolor, (int(self.x), int(self.y)), self.radius)
        pg.draw.circle(window, self.color, (int(self.x), int(self.y)), self.radius - int(.4 * self.radius))

    def update(self, update_frame):
        update_frame += 1
        ax = 0
        ay = 9.81

        dt = 0.2 * speed_multiplier
        self.vx += ax * dt
        self.vy += ay * dt

        self.x += self.vx * dt
        self.y += self.vy * dt

        bounced = False
        if self.y + self.radius > SCREEN_HEIGHT:
            self.y = SCREEN_HEIGHT - self.radius
            self.vy = -self.vy
            bounced = True

        # if (self.x - self.radius < BARRIER):
        #     self.x = BARRIER + self.radius
        #     self.vx = -self.vx
        #     bounced = True

        # if (self.x + self.radius > SCREEN_WIDTH - BARRIER):
        #     self.x = SCREEN_WIDTH - BARRIER - self.radius
        #     self.vx = -self.vx
        #     bounced = True

        if bounced:
            self.vx *= self.brate
            self.vy *= self.brate

        print(f'\n    Update Frame: {update_frame}\n'
              '    x-pos: %spx' % round(self.x),
              '    y-pos: %spx' % round(self.y),
              '    x-vel: %spx/u' % round(self.vx),
              '    y-vel: %spx/u' % round(self.vy),
              sep='\n')

        return update_frame

    @staticmethod
    def quadrant(x, y, xm, ym):
        if ym < y and xm > x:
            return 1
        elif ym < y and xm < x:
            return 2
        elif ym > y and xm < x:
            return 3
        elif ym > y and xm > x:
            return 4
        else:
            return False


def draw_window():
    window.fill(WINDOW_COLOR)
    ball.show(window)
    if not shoot:
        pg.draw.arrow(window, ALINE_COLOR, ALINE_COLOR, aline[0], aline[1], 5)
        pg.draw.arrow(window, LINE_COLOR, LINE_COLOR, line[0], line[1], 5)

    stroke_text = 'Strokes: %s' % strokes
    stroke_label = strokeFont.render(stroke_text, 1, STROKECOLOR)
    if not strokes:
        window.blit(stroke_label, (SCREEN_WIDTH - .21 * SCREEN_WIDTH, SCREEN_HEIGHT - .985 * SCREEN_HEIGHT))
    else:
        window.blit(stroke_label, (SCREEN_WIDTH - (.21+.02*math.floor(math.log10(strokes))) * SCREEN_WIDTH, SCREEN_HEIGHT - .985 * SCREEN_HEIGHT))

    power_text = 'Shot Strength: %sN' % power_display
    power_label = powerFont.render(power_text, 1, POWERCOLOR)
    if not shoot: window.blit(power_label, (cursor_pos[0] + .008 * SCREEN_WIDTH, cursor_pos[1]))

    angle_text = 'Angle: %s°' % angle_display
    angle_label = angleFont.render(angle_text, 1, ANGLECOLOR)
    if not shoot: window.blit(angle_label, (ball.x - .06 * SCREEN_WIDTH, ball.y - .01 * SCREEN_HEIGHT))

    if penalty:
        penalty_text = 'Out of Bounds! +1 Stroke'
        penalty_label = penaltyFont.render(penalty_text, 1, PENALTYCOLOR)
        penalty_rect = penalty_label.get_rect(center=(SCREEN_WIDTH/2, .225*SCREEN_HEIGHT))
        window.blit(penalty_label, penalty_rect)

    speed_multiplier_text = 'Speed: {:2.2f} m/s'.format(speed_multiplier)
    speed_multiplier_label = speedMultiplierFont.render(speed_multiplier_text, 1, SPEEDMULTIPLIERCOLOR)
    window.blit(speed_multiplier_label, (.91*SCREEN_WIDTH,.98*SCREEN_HEIGHT))

    power_multiplier_text = f'Strength: {int(power_multiplier*100)}%'
    power_multiplier_label = powerMultiplierFont.render(power_multiplier_text, 1, POWERMULTIPLIERCOLOR)
    window.blit(power_multiplier_label, (.01*SCREEN_WIDTH,.98*SCREEN_HEIGHT))

    #strength

    pg.display.flip()


def angle(cursor_pos):
    x, y, xm, ym = ball.x, ball.y, cursor_pos[0], cursor_pos[1]
    if x-xm:
        angle = math.atan((y - ym) / (x - xm))
    elif y > ym:
        angle = math.pi/2
    else:
        angle = 3*math.pi/2

    q = ball.quadrant(x,y,xm,ym)
    if q: angle = math.pi*math.floor(q/2) - angle

    if round(angle*deg) == 360:
        angle = 0

    if x > xm and not round(angle*deg):
        angle = math.pi

    return angle

def gravity(t):
    return 4.9*t**2


def arrow(screen, lcolor, tricolor, start, end, trirad):
    pg.draw.line(screen, lcolor, start, end, 2)
    rotation = (math.atan2(start[1] - end[1], end[0] - start[0])) + math.pi/2
    pg.draw.polygon(screen, tricolor, ((end[0] + trirad * math.sin(rotation),
                                        end[1] + trirad * math.cos(rotation)),
                                       (end[0] + trirad * math.sin(rotation - 120*rad),
                                        end[1] + trirad * math.cos(rotation - 120*rad)),
                                       (end[0] + trirad * math.sin(rotation + 120*rad),
                                        end[1] + trirad * math.cos(rotation + 120*rad))))
setattr(pg.draw, 'arrow', arrow)


def distance(x, y):
    return math.sqrt(x**2 + y**2)

def initialize():
    pg.init()
    pg.display.set_caption('Golf')
    window = pg.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
    pg.event.set_grab(True)
    pg.mouse.set_cursor((8, 8), (0, 0), (0, 0, 0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0, 0, 0))

    return window

rad, deg = math.pi/180, 180/math.pi
x, y, power, ang, strokes = [0]*5
xb, yb = None, None
shoot, penalty = False, False
p_ticks, update_frame = 0, 0

ball = Ball(START_X, START_Y)
quit = False

strength_dict = {0: .01, 1: .02, 2: .04, 3: .08, 4: .16, 5: .25, 6: .50, 7: .75, 8: 1}; stkey = 6
speed_dict = {0: .25, 1: .5, 2: 1, 3: 1.5, 4: 2, 5: 2.5, 6: 3, 7: 3.5, 8: 4, 9: 5, 10: 7.5, 11: 10}; spkey = 4

window = initialize()
while not quit:
    power_multiplier = strength_dict[stkey]
    speed_multiplier = speed_dict[spkey]

    seconds = (pg.time.get_ticks()-p_ticks)/1000
    if seconds > 1.2: penalty = False

    cursor_pos = pg.mouse.get_pos()
    line = [(ball.x, ball.y), cursor_pos]
    line_ball_x, line_ball_y = cursor_pos[0] - ball.x, cursor_pos[1] - ball.y

    aline = [(ball.x, ball.y), (ball.x + .015 * SCREEN_WIDTH, ball.y)]

    if not shoot:
        power_display = round(
            distance(line_ball_x, line_ball_y) * power_multiplier/5)

        angle_display = round(angle(cursor_pos) * deg)

    else:
        if abs(ball.vy) < 5 and abs(ball.vx) < 1 and abs(ball.y - (START_Y - 2*BARRIER)) <= BOUNCE_FUZZ:
            shoot = False
            ball.y = START_Y
            print('\nThe ball has come to a rest!')
            update_frame = 0
        else:
            update_frame = ball.update(update_frame)

        if not BARRIER < ball.x < SCREEN_WIDTH:
            shoot = False
            print('\nOut of Bounds!')
            penalty = True
            p_ticks = pg.time.get_ticks()
            strokes += 1

            if BARRIER < xb < SCREEN_WIDTH:
                ball.x = xb
            else:
                ball.x = START_X
            ball.y = yb

    for event in pg.event.get():
        if event.type == pg.QUIT:
            quit = True

        if event.type == pg.KEYDOWN:
            if event.key == pg.K_ESCAPE:
                quit = True

            if event.key == pg.K_RIGHT:
                if spkey != max(speed_dict):
                    spkey += 1

            if event.key == pg.K_LEFT:
                if spkey != min(speed_dict):
                    spkey -= 1

            if event.key == pg.K_UP:
                if stkey != max(strength_dict):
                    stkey += 1

            if event.key == pg.K_DOWN:
                if stkey != min(strength_dict):
                    stkey -= 1

        if event.type == pg.MOUSEBUTTONDOWN:
            if not shoot:
                shoot = True
                x, y = ball.x, ball.y
                xb, yb = ball.x, ball.y
                power = (distance(line_ball_x, line_ball_y)) / 10
                print('\n\nBall Hit!')
                print('\npower: %sN' % round(power, 2))
                ang = angle(cursor_pos)
                print('angle: %s°' % round(ang * deg, 2))
                print('cos(a): %s' % round(math.cos(ang), 2)), print('sin(a): %s' % round(math.sin(ang), 2))

                ball.vx, ball.vy = power * math.cos(ang), -power * math.sin(ang)

                strokes += 1

    draw_window()

print("\nShutting down...")
pg.quit()

Solution

  • I'm really not sure how you want to manage your physics update but here is how I would do it:

    When the user clicks, an initial velocity for the ball is computed. Using the angle and the distance as you did is perfectly fine.

    Please note I added vx and vy members to the Ball class.

    if event.type == pg.MOUSEBUTTONDOWN:
        if not shoot:
            shoot = True
            power = distance(line_ball_x, line_ball_y)) / 10                    
            ang = angle(cursor_pos)
            ball.vx, ball.vy = power * math.cos(ang), -power * math.sin(ang)
    
            strokes += 1
    

    Now, I replaced all the logic you had to manage the position of the ball by an update() member function.

    The idea of this method, is to update the velocity and the position of the ball using euler integration. At every timestep, you calculate the new speed given the acceleration of gravity (9.81 m.s⁻² is the acceleration of gravity on earth at see level, but you can take whatever value your want here), and you compute the new position given that updated speed*.

    Here it is in practice:

    def update(self, dt):
        ax = 0 # Acceleration along x axis
        ay = 9.81 # Acceleration along y axis. The value is positive here since y=0 is on the top of the window and you want the ball to go down
    
        # New velocity is the old one with the acceleration multiplied by the time elapsed since last call
        self.vx += ax * dt
        self.vy += ay * dt
    
        # New position is the old one with the velocity multiplied by the time elapsed since last call
        self.x += self.vx * dt
        self.y += self.vy * dt
    

    So now you have a nicely moving ball when you click (which is the state you were in if I understood correctly). Now, to answer your question, here is how you can handle the bounces using this method.

    The idea is to check, after updating the velocity, if the ball is leaving the window, and if it is, reverse the velocity for the concerned axis (and only this one):

        # Check if the ball is falling through the floor
        # You need to take the radius into account if you do not want it to leave the screen at all.
        if self.y + self.radius > SCREEN_HEIGHT:
            # If it's outside, we move it along the floor
            self.y = SCREEN_HEIGHT - self.radius
            # And we invert the y velocity
            self.vy = -self.vy
    
        # You can do the same for the edges:
        if self.x - self.radius < 0:
            self.x = self.radius
            self.vx = -self.vx
    
        if self.x + self.radius > SCREEN_WIDTH:
            self.x = SCREEN_WIDTH  - self.radius
            self.vx = -self.vx
    

    Here you have a nicely bouncing ball. The problem is that it never stops bouncing etc. What you can do is to take into account the energy lost when hitting a wall or the floor by multiplying the velocity by a factor when a hit occurs:

        bounced = False
        # Make this variable true when hitting the ground or a wall
    
        # And update the velocities if needed
        if bounced: 
            self.vx *= 0.9 # Or any other value < 1, at your will.
            self.vy *= 0.9
    

    So now you have a bouncing ball that stops after some time. Here is the complete code (from your example)

    import math
    import pygame as pg
    
    SCREEN_WIDTH = 1500
    SCREEN_HEIGHT = 800
    WINDOW_COLOR = (100, 100, 100)
    LINE_COLOR = (0, 0, 255)
    ALINE_COLOR = (0, 0, 0)
    BARRIER = 1
    START_X = int(.5 * SCREEN_WIDTH)
    START_Y = int(.99 * SCREEN_HEIGHT)
    
    
    pg.font.init()
    strokeFont = pg.font.SysFont("monospace", 50)
    STROKECOLOR = (255, 255, 0)
    
    powerFont = pg.font.SysFont("arial", 15, bold=True)
    POWERCOLOR = (0, 255, 0)
    
    angleFont = pg.font.SysFont("arial", 15, bold=True)
    ANGLECOLOR = (0, 255, 0)
    
    penaltyFont = pg.font.SysFont("georgia", 40, bold=True)
    PENALTYCOLOR = (255, 0, 0)
    
    speedMultiplierFont = pg.font.SysFont("courier new", 13)
    SPEEDMULTIPLIERCOLOR = (255, 0, 0)
    
    powerMultiplierFont = pg.font.SysFont("courier new", 13)
    POWERMULTIPLIERCOLOR = (255, 0, 0)
    
    
    class Ball(object):
        def __init__(self, x, y, dx = 0, dy = 0):
            self.x = x
            self.y = y
            self.dx = dx
            self.dy = dy
            self.radius = 10
            self.color = (255, 255, 255)
            self.outlinecolor = (255, 0, 0)
    
        def show(self, window):
            pg.draw.circle(window, self.outlinecolor, (int(self.x), int(self.y)), self.radius)
            pg.draw.circle(window, self.color, (int(self.x), int(self.y)), self.radius - int(.4 * self.radius))
    
        def update(self):
            ax = 0
            ay = 9.81
    
            dt = 0.1
            self.vx += ax * dt
            self.vy += ay * dt
    
            self.x += self.vx * dt
            self.y += self.vy * dt
    
            bounced = False
            if (self.y + self.radius > SCREEN_HEIGHT):
                self.y = SCREEN_HEIGHT - self.radius
                self.vy = -self.vy
                bounced = True
    
            if (self.x - self.radius < BARRIER):
                self.x = BARRIER + self.radius
                self.vx = -self.vx
                bounced = True
    
            if (self.x + self.radius > SCREEN_WIDTH - BARRIER):
                self.x = SCREEN_WIDTH - BARRIER - self.radius
                self.vx = -self.vx
                bounced = True
    
            if bounced:
                self.vx *= 0.9
                self.vy *= 0.9
    
            print(self.x, self.y, self.vx, self.vy)
    
        @staticmethod
        def quadrant(x, y, xm, ym):
            if ym < y and xm > x:
                return 1
            elif ym < y and xm < x:
                return 2
            elif ym > y and xm < x:
                return 3
            elif ym > y and xm > x:
                return 4
            else:
                return False
    
    
    def draw_window():
        window.fill(WINDOW_COLOR)
        ball.show(window)
        if not shoot:
            pg.draw.arrow(window, ALINE_COLOR, ALINE_COLOR, aline[0], aline[1], 5)
            pg.draw.arrow(window, LINE_COLOR, LINE_COLOR, line[0], line[1], 5)
    
        stroke_text = 'Strokes: %s' % strokes
        stroke_label = strokeFont.render(stroke_text, 1, STROKECOLOR)
        if not strokes:
            window.blit(stroke_label, (SCREEN_WIDTH - .21 * SCREEN_WIDTH, SCREEN_HEIGHT - .985 * SCREEN_HEIGHT))
        else:
            window.blit(stroke_label, (SCREEN_WIDTH - (.21+.02*math.floor(math.log10(strokes))) * SCREEN_WIDTH, SCREEN_HEIGHT - .985 * SCREEN_HEIGHT))
    
        power_text = 'Shot Strength: %sN' % power_display
        power_label = powerFont.render(power_text, 1, POWERCOLOR)
        if not shoot: window.blit(power_label, (cursor_pos[0] + .008 * SCREEN_WIDTH, cursor_pos[1]))
    
        angle_text = 'Angle: %s°' % angle_display
        angle_label = angleFont.render(angle_text, 1, ANGLECOLOR)
        if not shoot: window.blit(angle_label, (ball.x - .06 * SCREEN_WIDTH, ball.y - .01 * SCREEN_HEIGHT))
    
        if penalty:
            penalty_text = 'Out of Bounds! +1 Stroke'
            penalty_label = penaltyFont.render(penalty_text, 1, PENALTYCOLOR)
            penalty_rect = penalty_label.get_rect(center=(SCREEN_WIDTH/2, .225*SCREEN_HEIGHT))
            window.blit(penalty_label, penalty_rect)
    
        speed_multiplier_text = 'Speed: {:2.2f} m/s'.format(speed_multiplier)
        speed_multiplier_label = speedMultiplierFont.render(speed_multiplier_text, 1, SPEEDMULTIPLIERCOLOR)
        window.blit(speed_multiplier_label, (.91*SCREEN_WIDTH,.98*SCREEN_HEIGHT))
    
        power_multiplier_text = f'Strength: {int(power_multiplier*100)}%'
        power_multiplier_label = powerMultiplierFont.render(power_multiplier_text, 1, POWERMULTIPLIERCOLOR)
        window.blit(power_multiplier_label, (.01*SCREEN_WIDTH,.98*SCREEN_HEIGHT))
    
        #strength
    
        pg.display.flip()
    
    
    def angle(cursor_pos):
        x, y, xm, ym = ball.x, ball.y, cursor_pos[0], cursor_pos[1]
        if x-xm:
            angle = math.atan((y - ym) / (x - xm))
        elif y > ym:
            angle = math.pi/2
        else:
            angle = 3*math.pi/2
    
        q = ball.quadrant(x,y,xm,ym)
        if q: angle = math.pi*math.floor(q/2) - angle
    
        if round(angle*deg) == 360:
            angle = 0
    
        if x > xm and not round(angle*deg):
            angle = math.pi
    
        return angle
    
    def gravity(t):
        return 4.9*t**2
    
    
    def arrow(screen, lcolor, tricolor, start, end, trirad):
        pg.draw.line(screen, lcolor, start, end, 2)
        rotation = (math.atan2(start[1] - end[1], end[0] - start[0])) + math.pi/2
        pg.draw.polygon(screen, tricolor, ((end[0] + trirad * math.sin(rotation),
                                            end[1] + trirad * math.cos(rotation)),
                                           (end[0] + trirad * math.sin(rotation - 120*rad),
                                            end[1] + trirad * math.cos(rotation - 120*rad)),
                                           (end[0] + trirad * math.sin(rotation + 120*rad),
                                            end[1] + trirad * math.cos(rotation + 120*rad))))
    setattr(pg.draw, 'arrow', arrow)
    
    
    def distance(x, y):
        return math.sqrt(x**2 + y**2)
    
    def initialize():
        pg.init()
        pg.display.set_caption('Golf')
        window = pg.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
        pg.event.set_grab(True)
        pg.mouse.set_cursor((8, 8), (0, 0), (0, 0, 0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0, 0, 0))
    
        return window
    
    rad, deg = math.pi/180, 180/math.pi
    x, y, time, power, ang, strokes = 0, 0, 0, 0, 0, 0
    xb, yb = None, None
    shoot, penalty = False, False
    p_ticks = 0
    
    ball = Ball(START_X, START_Y)
    quit = False
    
    strength_dict = {0: .01, 1: .02, 2: .04, 3: .08, 4: .16, 5: .25, 6: .50, 7: .75, 8: 1}; stkey = 6
    speed_dict = {0: .25, 1: .5, 2: 1, 3: 1.5, 4: 2, 5: 2.5, 6: 3, 7: 3.5, 8: 4, 9: 5, 10: 7.5, 11: 10}; spkey = 4
    
    window = initialize()
    try:
        while not quit:
            power_multiplier = strength_dict[stkey]
            speed_multiplier = speed_dict[spkey]
    
            seconds = (pg.time.get_ticks()-p_ticks)/1000
            if seconds > 1.2: penalty = False
    
            cursor_pos = pg.mouse.get_pos()
            line = [(ball.x, ball.y), cursor_pos]
            line_ball_x, line_ball_y = cursor_pos[0] - ball.x, cursor_pos[1] - ball.y
    
            aline = [(ball.x, ball.y), (ball.x + .015 * SCREEN_WIDTH, ball.y)]
    
            if not shoot:
                power_display = round(
                    distance(line_ball_x, line_ball_y) * power_multiplier/5)
    
                angle_display = round(angle(cursor_pos) * deg)
    
            else:
                ball.update()
    
                # time += .3 * speed_multiplier
                # print('\n   time: %ss' % round(time, 2))
                # if ball.y <= START_Y:
                #     if BARRIER < ball.x and ball.x < SCREEN_WIDTH:
                #         po = ball.path(x, y, power, ang, time)
                #         ball.x, ball.y = po[0], po[1]
                #     else:
                #         print('Out of Bounds!')
                #         penalty = True
                #         p_ticks = pg.time.get_ticks()
                #         strokes += 1
                #         shoot = False
                #         if BARRIER < xb < SCREEN_WIDTH:
                #             ball.x = xb
                #         else:
                #             ball.x = START_X
                #         ball.y = yb
                # else:
                #     po = ball.path(x, y, power, ang, time, True)
                #     ball.x, ball.y = po[0], po[1]
    
            for event in pg.event.get():
                if event.type == pg.QUIT:
                    quit = True
    
                if event.type == pg.KEYDOWN:
                    if event.key == pg.K_ESCAPE:
                        quit = True
    
                    if event.key == pg.K_RIGHT:
                        if spkey != max(speed_dict):
                            spkey += 1
    
                    if event.key == pg.K_LEFT:
                        if spkey != min(speed_dict):
                            spkey -= 1
    
                    if event.key == pg.K_UP:
                        if stkey != max(strength_dict):
                            stkey += 1
    
                    if event.key == pg.K_DOWN:
                        if stkey != min(strength_dict):
                            stkey -= 1
    
                if event.type == pg.MOUSEBUTTONDOWN:
                    if not shoot:
                        shoot = True
                        x, y = ball.x, ball.y
                        xb, yb = ball.x, ball.y
                        time, power = 0, (
                            distance(line_ball_x, line_ball_y)) / 10
                        print('\n\nBall Hit!')
                        print('\npower: %sN' % round(power, 2))
                        ang = angle(cursor_pos)
                        print('angle: %s°' % round(ang * deg, 2))
                        print('cos(a): %s' % round(math.cos(ang), 2)), print('sin(a): %s' % round(math.sin(ang), 2))
    
                        ball.vx, ball.vy = power * math.cos(ang), -power * math.sin(ang)
    
                        strokes += 1
    
            draw_window()
    
        print("\nShutting down...")
        pg.quit()
    
    except Exception as error:
        print(f'A fatal error ({error}) has occurred. The program is shutting down.')
        pg.quit()
    

    Again, please keep in mind that this is A way to handle the physics, this might not be the way you want to handle it.

    (* Actually this is not "classic" euler method, this is the semi-implicit one, which is more stable)