Mini-project description - Pong#

# Implementation of classic arcade game Pong

try:
    import simplegui
except ModuleNotFoundError:
    import simplequi as simplegui

import random

# initialize globals - pos and vel encode vertical info for paddles
WIDTH = 600
HEIGHT = 400
BALL_RADIUS = 20
PAD_WIDTH = 8
PAD_HEIGHT = 80
HALF_PAD_WIDTH = PAD_WIDTH / 2
HALF_PAD_HEIGHT = PAD_HEIGHT / 2
LEFT = False
RIGHT = True
FRAME_RATE = 60
PADDLE_VEL = 100
BALL_VEL_INCR_FACTOR = 1.1
SCORE_FONT_SIZE = 50
SCORE_OFFSET_X_FROM_MIDDLE = 30
SCORE_OFFSET_Y = 60
BALL_COLOR_GREEN_INITIAL = 255
BALL_COLOR_GREEN_INCR = -30
# this value is added to the green channel after every paddle strike

score1 = score2 = 0
ball_pos = []
ball_vel = []  # unit: pixels per frame update

paddle1_pos = paddle2_pos = 0
paddle1_vel = paddle2_vel = 0
# paddles have only vertical position and velocity
# their horizontal pos/vel is implicit
ball_color_green = 0


# initialize ball_pos and ball_vel for new bal in middle of table
# if direction is RIGHT, the ball's velocity is upper right, else upper left
def spawn_ball(direction):
    global ball_pos, ball_vel  # these are vectors stored as lists
    global ball_color_green

    ball_pos = [WIDTH/2, HEIGHT/2]
    ball_color_green = BALL_COLOR_GREEN_INITIAL

    if direction == RIGHT:
        x_vel_sign = 1
    else:
        x_vel_sign = -1
    ball_vel = [
        x_vel_sign * random.randrange(120, 140),
        - random.randrange(60, 180)
        ]


# define event handlers
def new_game():
    global paddle1_pos, paddle2_pos, paddle1_vel, paddle2_vel
    # these are numbers
    global score1, score2  # these are ints
    global ball_color_green

    paddle1_pos = paddle2_pos = HEIGHT/2
    paddle1_vel = paddle2_vel = 0
    score1 = score2 = 0
    ball_color_green = BALL_COLOR_GREEN_INITIAL
    spawn_ball(RIGHT)


def draw(canvas: simplegui.Canvas):
    global score1, score2, paddle1_pos, paddle2_pos, ball_pos, ball_vel

    # draw mid line and gutters
    canvas.draw_line([WIDTH / 2, 0], [WIDTH / 2, HEIGHT], 1, "White")
    canvas.draw_line([PAD_WIDTH, 0], [PAD_WIDTH, HEIGHT], 1, "White")
    canvas.draw_line(
        [WIDTH-PAD_WIDTH, 0], [WIDTH-PAD_WIDTH, HEIGHT], 1, "White")

    # update ball
    # vertical collision check
    if ball_pos[1] <= 0+BALL_RADIUS or ball_pos[1] >= HEIGHT-BALL_RADIUS:
        ball_vel[1] *= -1

    # gutter and paddle collision check
    def paddle_collision_check(player_side: bool):
        global score1, score2, ball_color_green
        if player_side == LEFT:
            touched_paddle = ball_pos[1] <= paddle1_pos + HALF_PAD_HEIGHT and \
                ball_pos[1] >= paddle1_pos - HALF_PAD_HEIGHT
        else:
            touched_paddle = ball_pos[1] <= paddle2_pos + HALF_PAD_HEIGHT and \
                ball_pos[1] >= paddle2_pos - HALF_PAD_HEIGHT

        if touched_paddle:
            # reflect the ball and increase velocity
            ball_vel[0] *= -1 * BALL_VEL_INCR_FACTOR
            ball_vel[1] *= BALL_VEL_INCR_FACTOR
            # NOTE
            # the velocity and its x, y components build up a triangle.
            # The velocity is the hypotenuse.  If we scale the hypotenuse of
            # the triangle by the factor f, then x and y will be scaled by the
            # same factor

            # adjust ball color
            ball_color_green += BALL_COLOR_GREEN_INCR
            if ball_color_green >= 255:
                ball_color_green = 255
            elif ball_color_green <= 0:
                ball_color_green = 0
        else:
            if player_side == LEFT:
                spawn_ball(RIGHT)
                score2 += 1
            else:
                spawn_ball(RIGHT)
                score1 += 1

    if ball_pos[0] <= PAD_WIDTH+BALL_RADIUS:  # left gutter
        paddle_collision_check(LEFT)
    elif ball_pos[0] >= WIDTH-PAD_WIDTH-BALL_RADIUS:  # right gutter
        paddle_collision_check(RIGHT)

    ball_pos[0] += ball_vel[0] / FRAME_RATE
    ball_pos[1] += ball_vel[1] / FRAME_RATE

    # draw ball
    # green channel is dynamic
    ball_color = f'#ff{ball_color_green:x}00'
    canvas.draw_circle(
        ball_pos,
        BALL_RADIUS,
        2,
        ball_color,
        ball_color)

    # update paddle's vertical position, keep paddle on the screen
    paddle1_pos += paddle1_vel / FRAME_RATE
    paddle2_pos += paddle2_vel / FRAME_RATE

    def update_and_draw_paddle(canvas: simplegui.Canvas, player_side: bool):
        paddle_pos = paddle1_pos if player_side == LEFT else paddle2_pos

        # update paddle
        if paddle_pos <= HALF_PAD_HEIGHT:
            paddle_pos = HALF_PAD_HEIGHT
        elif paddle_pos >= HEIGHT-HALF_PAD_HEIGHT:
            paddle_pos = HEIGHT-HALF_PAD_HEIGHT

        # draw paddle
        x = HALF_PAD_WIDTH if player_side == LEFT else WIDTH-HALF_PAD_WIDTH
        canvas.draw_line(
            [x, paddle_pos-HALF_PAD_HEIGHT],
            [x, paddle_pos+HALF_PAD_HEIGHT],
            PAD_WIDTH,
            'yellow',
        )
    update_and_draw_paddle(canvas, LEFT)
    update_and_draw_paddle(canvas, RIGHT)

    # draw scores
    text = f'{score1}   {score2}'
    textwidth = frame.get_canvas_textwidth(text, SCORE_FONT_SIZE, 'monospace')
    canvas.draw_text(
        text,
        [WIDTH/2-textwidth/2, SCORE_OFFSET_Y],
        SCORE_FONT_SIZE,
        'gray',
        'monospace')


def keydown(key):
    global paddle1_vel, paddle2_vel
    if key == simplegui.KEY_MAP['w']:
        paddle1_vel = -PADDLE_VEL
    elif key == simplegui.KEY_MAP['s']:
        paddle1_vel = PADDLE_VEL
    elif key == simplegui.KEY_MAP['up']:
        paddle2_vel = -PADDLE_VEL
    elif key == simplegui.KEY_MAP['down']:
        paddle2_vel = PADDLE_VEL


def keyup(key):
    global paddle1_vel, paddle2_vel
    paddle1_vel = paddle2_vel = 0


# create frame
frame = simplegui.create_frame("Pong", WIDTH, HEIGHT)
frame.set_draw_handler(draw)
frame.set_keydown_handler(keydown)
frame.set_keyup_handler(keyup)
frame.add_button('Reset', new_game)

# start frame
new_game()
frame.start()