Tuesday, April 19, 2022

Python - Project - Pong Game (Day 22)

This is a 100 Days challenge to learn a new language (Python). 100 Days of Code - The Complete Python Pro Bootcamp 

I will post some notes to motivate myself to finish this challenge.


Project - Pong Game







Break down the problem:

    1. Create the screen
    2. Create and move a paddle
    3. Create another paddle
    4. Create the ball and make it move
    5. Detect collision with wall and bounce
    6. Detect collision with paddle
    7. Detect when paddle misses
    8. Keep Scores


Design



1. One class for the divider in the middle screen
2. One class for scoreboard for both left and right team
3. One class for paddle for both left and right team
4. One class for ball


Pong Game



divider.py
from turtle import Turtle

DOWN = 270


class Divider(Turtle): 
    def __init__(self, screen_y_range):
        """Constructor"""
        super().__init__()

        self.hideturtle()
        self.color("white")
        self.penup()
        self.setheading(DOWN)
        self.setpos(0, screen_y_range)

        keep_drawing = True
        while keep_drawing:
            self.pendown()
            self.forward(10)
            self.penup()
            self.forward(10)

            if self.ycor() <= screen_y_range * -1:
                keep_drawing = False

scoreboard.py
from turtle import Turtle


class Scoreboard(Turtle):

    def __init__(self, screen_y_range):
        """Constructor"""
        super().__init__()

        # Define attributes
        self.team_a_score = 0
        self.team_b_score = 0

        self.hideturtle()
        self.color("white")
        self.penup()
        # Move it down a bit
        self.setpos(0, screen_y_range - 100)
        self.refresh_scoreboard()

    def refresh_scoreboard(self):
        """refresh_scoreboard"""

        self.clear()
        self.write(
            f"{self.team_a_score}    {self.team_b_score}",
            False,
            align="center",
            font=("Arial", 60, "normal"),
        )

    def increase_score_by_team(self, team):
        """Increase team's score and refresh the scoreboard"""
        if team == "a":
            self.team_a_score += 1
        else:
            self.team_b_score += 1

        self.refresh_scoreboard()

    def game_over(self):
        """Show Game Over message"""
        # Move turtle to center first
        self.setpos(0, 0)

        self.write(
            "Game Over",
            False,
            align="center",
            font=("Arial", 60, "normal"),
        )


paddle.py
from turtle import Turtle

# Constants
MOVING_DISTANCE = 50
UP = 90
DOWN = 270


class Paddle(Turtle):

    def __init__(self, position):
        """Constructor"""
        super().__init__()

        self.shape("square")
        self.shapesize(1.0, 5, 1)
        self.color("white")
        self.penup()
        self.setheading(UP)
        self.setpos(position)

    def move_up(self):
        """Make Paddel mvoe UP"""
        self.setheading(UP)
        self.forward(MOVING_DISTANCE)

    def move_down(self):
        """Make Paddel mvoe DOWN"""
        self.setheading(DOWN)
        self.forward(MOVING_DISTANCE)


ball.py
import random
from turtle import Turtle

STARTING_POSITION = (0, 0)
UP = 90
LEFT = 180
DOWN = 270
RIGHT = 0


class Ball(Turtle):

    def __init__(self):
        """Constructor"""
        super().__init__()

        # Attributes
        self.ball_speed = 0.1

        self.penup()
        self.color("white")
        self.shape("circle")
        self.reset()

    def move(self, move_distance):
        """make ball move forward with current heading"""
        self.forward(move_distance)

    def change_direction(self, heading):
        """change ball's heading"""
        self.setheading(heading)

    def reset(self):
        """reset ball including speed, position and heading"""
        self.ball_speed = 0.1
        self.setpos(STARTING_POSITION)
        new_heading = self.get_random_heading()
        self.change_direction(new_heading)

    def get_random_heading(self):
        """generate an random heading"""
        new_heading = random.randint(0, 360)

        # Avoid weird heading
        while (
            abs(new_heading - UP) <= 10
            or abs(new_heading - LEFT) <= 10
            or abs(new_heading - DOWN) <= 10
            or abs(new_heading - RIGHT) <= 10
        ):
            new_heading = random.randint(0, 360)

        return new_heading

    def bounce(self):
        """bounce from top or bottom wall"""
        current_heading = self.heading()
        new_heading = 360 - current_heading

        self.change_direction(new_heading)

    def pong(self):
        """pong from paddle"""
        current_heading = self.heading()
        new_heading = 540 - current_heading

        self.change_direction(new_heading)

    def speed_up(self):
        """increase ball's speed"""
        self.ball_speed *= 0.8

main.py
import time
from turtle import Screen
from divider import Divider
from scoreboard import Scoreboard
from paddle import Paddle
from ball import Ball

# Constants
MOVE_DISTANCE = 20
OFFSET = 20
SCREEN_X_RANGE = 500
SCREEN_Y_RANGE = 300
POINTS_TO_WIN = 10
TEAM_A_PADDLE_X = SCREEN_X_RANGE * -1 + 100
TEAM_B_PADDLE_X = SCREEN_X_RANGE - 100

# Setup Screen
screen = Screen()
screen.setup(SCREEN_X_RANGE * 2, SCREEN_Y_RANGE * 2)
screen.bgcolor("black")
# Turn turtle animation off
screen.tracer(0)

divier = Divider(SCREEN_Y_RANGE)
scoreboard = Scoreboard(SCREEN_Y_RANGE)
ball = Ball()
paddle_a = Paddle((TEAM_A_PADDLE_X, 0))
paddle_b = Paddle((TEAM_B_PADDLE_X, 0))

# Listen key events on Screen
screen.onkey(paddle_a.move_up, "w")
screen.onkey(paddle_a.move_down, "s")
screen.onkey(paddle_b.move_up, "Up")
screen.onkey(paddle_b.move_down, "Down")
screen.listen()

# Refresh Screen
# Perform a TurtleScreen update. To be used when tracer is turned off.
screen.update()

is_game_on = True

while is_game_on:
    # Give some delay
    time.sleep(ball.ball_speed)

    # Make ball move
    ball.move(MOVE_DISTANCE)

    # Check if the ball touch the wall behind the paddle
    if (
        ball.xcor() + OFFSET >= SCREEN_X_RANGE
        or ball.xcor() - OFFSET <= SCREEN_X_RANGE * -1
    ):
        # Update Scoreboard
        if ball.xcor() + OFFSET >= SCREEN_X_RANGE:
            scoreboard.increase_score_by_team("a")
        else:
            scoreboard.increase_score_by_team("b")

        # Determine if it is game over or not
        if (
            scoreboard.team_a_score == POINTS_TO_WIN
            or scoreboard.team_b_score == POINTS_TO_WIN
        ):
            scoreboard.game_over()
            is_game_on = False
        else:
            # Reset ball
            ball.reset()

            # Add some delay for the new turn
            time.sleep(0.5)

    # Check if the ball touch the paddle
    elif (
        ball.xcor() + OFFSET >=
TEAM_B_PADDLE_X and paddle_b.distance(ball) <= 40
    ) or (ball.xcor() - OFFSET <=
TEAM_A_PADDLE_X and paddle_a.distance(ball) <= 40):
        ball.pong()
        ball.speed_up()

    # Check if the ball touch the top and bottom wall
    elif (
        ball.ycor() + OFFSET >= SCREEN_Y_RANGE
        or ball.ycor() - OFFSET <= SCREEN_Y_RANGE * -1
    ):
        ball.bounce()

    # Refresh Screen
    # Perform a TurtleScreen update.
# To be used when tracer is turned off.
    screen.update()

screen.exitonclick()


Highlight



1. Instead of adding multiple turtle and link together as a paddle, using 'shapesize' of api


No comments:

Post a Comment