"""
Created on Sep 29, 2022

@author: Robert Duvall

This module represents the game of Pong, the first popular arcode game!

This version shows how to use classes to organize your code.
"""
import random
import turtle

# choose a name for your game to appear in the title bar of the game window
gameName = 'Pong with Classes'
# choose the size of your game window
screenWidth = 800
screenHeight = 600
# choose how fast paddles move
paddleSpeed = 20
# choose where paddles are positioned
paddleOffset = 50
# choose where scores are positioned
scoreXOffset = 50
scoreYOffset = 80


class GameObject(turtle.Turtle):
    """
    This class represents a "smart" turtle that also has some methods specific to our game.
    """
    def __init__(self, color, x, y):
        super().__init__()
        self.colorStr = color
        self.x = x
        self.y = y

    def draw(self):
        """
        Draw the object in its color and at its position without leaving a trail or animating.
        """
        self.speed(0)
        self.penup()
        self.color(self.colorStr)
        self.goto(self.x, self.y)


class Paddle(GameObject):
    """
    This class represents a user controlled paddle that blocks the ball from going off the screen.
    """
    def __init__(self, color, x, y):
        super().__init__(color, x, y)

    def draw(self):
        """
        Draw the paddle as a vertical bar.
        """
        super().draw()
        self.shape('square')
        self.shapesize(5, 1)

    def move(self, dy):
        """
        Move the paddle only up or down.
        """
        self.sety(self.ycor() + dy)

    def contains(self, x, y):
        """
        Returns True if given XY-coordinate is within the paddle based on its current position and size.
        """
        paddleX = self.xcor()
        paddleY = self.ycor()
        return (paddleX - 15 < x < paddleX + 15) and (paddleY - 60 < y < paddleY + 60)


class Ball(GameObject):
    """
    This class represents a ball that moves around the screen bouncing off things.
    """
    def __init__(self, color, dx, dy):
        super().__init__(color, 0, 0)
        self.dx = dx
        self.dy = dy

    def draw(self):
        """
        Draw the ball as a simple circle and set it up to move without leaving a trail.
        """
        super().draw()
        self.shape('circle')

    def move(self):
        """
        Move the ball based on its current velocity (dx, dy).
        """
        self.goto(self.xcor() + self.dx, self.ycor() + self.dy)

    def reset(self):
        """
        Reset the ball back to the middle of the screen and randomize its velocity.
        """
        self.goto(0, 0)
        self.dx = random.randint(1, 4)
        self.dy = random.randint(1, 4)
        if random.randint(1, 2) == 1:
            self.dx = -self.dx
        if random.randint(1, 2) == 1:
            self.dy = -self.dy

    def bounce_x(self):
        self.dx = -self.dx

    def bounce_y(self):
        self.dy = -self.dy


class Score(GameObject):
    """
    This class represents a score displayed to the players and updated when a player misses the ball.
    """
    def __init__(self, color, x, y):
        super().__init__(color, x, y)
        self.score = 0

    def draw(self):
        """
        Draw the score's value on the screen.
        """
        super().draw()
        self.hideturtle()
        self.clear()
        self.write(str(self.score), align='center', font=('Courier', 64, 'bold'))

    def increment(self):
        """
        Update the score's value as well as the text displayed.
        """
        self.score = self.score + 1
        self.draw()


# Move the players' paddles up or down based on the speed and update the screen to see the results
def paddle1_up():
    thePaddle1.move(paddleSpeed)
    theScreen.update()

def paddle1_down():
    thePaddle1.move(-paddleSpeed)
    theScreen.update()

def paddle2_up():
    thePaddle2.move(paddleSpeed)
    theScreen.update()

def paddle2_down():
    thePaddle2.move(-paddleSpeed)
    theScreen.update()

def reset_on_click(x, y):
    theBall.reset()
    theScreen.update()


def draw_scene(screen):
    """
    Draw the game's setting.

    Whatever turtles you create here or drawing that you do here will serve as the game's background
    to set the game's theme, but will not be part of the game's action.
    """
    screen.bgcolor('black')


def game_step():
    """
    Handle game "rules" for every step (i.e., frame or "moment"):
     - movement: move the ball each step?
     - collisions: check if the ball collided with the sides or paddles and then bounce or reset it
    Note, to make the ball appear to bounce, all that is needed is to reverse the appropriate "d" value:
     - if bouncing off the top or bottom, negate theBall.dy
     - if bouncing off either paddle, negate theBall.dx
    """
    # move ball based on its speed
    theBall.move()

    ballX = theBall.xcor()
    ballY = theBall.ycor()
    # check if ball hits top or bottom side, bounce it by reversing theBall's dy variable
    if ballY > (screenHeight // 2 - 10) or ballY < -(screenHeight // 2 - 10):
        theBall.bounce_y()

    # check if the ball got past player 1's paddle (hit the right edge), then reset ball and update player 2's score
    if ballX > (screenWidth // 2 - 10):
        theScore2Text.increment()
        theBall.reset()

    # check if the ball got past player 2's paddle (hit the left edge), then reset ball and update player 1's score
    if ballX < -(screenWidth // 2 - 10):
        theScore1Text.increment()
        theBall.reset()

    # check if the ball hits thePaddle1, bounce it by reversing theBall's dx variable
    if thePaddle1.contains(ballX, ballY):
        theBall.bounce_x()

    # check if the ball hits thePaddle2, bounce it by reversing theBall's dx variable
    if thePaddle2.contains(ballX, ballY):
        theBall.bounce_x()

    # DO NOT CHANGE - required to see the changes made and keep the game running
    theScreen.update()
    theScreen.ontimer(game_step, 10)


def setup():
    """
    Sets up the initial game scene
    """
    # make the game interactive
    theScreen.tracer(False)
    # listen for key presses
    theScreen.listen()
    theScreen.onkeypress(paddle1_up, 'Up')
    theScreen.onkeypress(paddle1_down, 'Down')
    theScreen.onkeypress(paddle2_up, 'w')
    theScreen.onkeypress(paddle2_down, 's')
    # for debugging purposes, allow mouse click to reset the ball
    theScreen.onclick(reset_on_click)
    # draw the game based on student's code
    draw_scene(theScreen)
    # draw the game objects by looping through the list and calling their draw method
    for obj in theGameObjects:
        obj.draw()
    theBall.reset()
    # show the results
    theScreen.update()
    # "loop" the game by calling this function again after a small delay
    theScreen.ontimer(game_step, 10)


# Set up the game variables
theScreen = turtle.Screen()
theScreen.title(gameName)
theScreen.setup(screenWidth, screenHeight)
theScreen.tracer(False)

# Create instances of game classes
theScore1Text = Score('yellow', scoreXOffset, screenHeight // 2 - scoreYOffset)
theScore2Text = Score('orange', -scoreXOffset, screenHeight // 2 - scoreYOffset)
thePaddle1 = Paddle('yellow', screenWidth // 2 - paddleOffset, 0)
thePaddle2 = Paddle('orange', -screenWidth // 2 + paddleOffset, 0)
theBall = Ball('cyan', 2, 2)
# make a list of all the game turtles to make it easier to use in some cases
theGameObjects = [
    theScore1Text,
    theScore2Text,
    thePaddle1,
    thePaddle2,
    theBall
]

# draw the game's setting and all the game objects
setup()

# play the game forever
theScreen.mainloop()
