Micropython Pong

Written by Satej Gandre

A vintage pong gaming console

History

Pong is known to be one of the earliest revolutionary games to be released on consoles. The key feature about the game was that it allowed two players to compete against each other in an engaging, digitized format. Pong was also the first successful game to be released, as it increased Atari's (The company that created pong) sales and revenue. This inspired various other video game companies to mimic pong's unique pixelated look and publish more games similar to it. In essence, pong sparked the video game industry as a whole.

Mechanics

The original game of pong consisted of a large black playing field with two luminescent paddles and a single ball. Players could control their own paddle in a vertical direction in order to prevent the ball from spiraling out of the screen. The player who had the most points before a certain number of outs would be declared as the winner. This game can be modified whilst also retaining most of its unique characteristics if a person were to play in single player mode. Firstly, the second paddle could simply be removed as there wouldn't be a player who would control it. Instead of a paddle, the ball could simply bounce off of three walls but not the one behind the first player's paddle. The paddle could also be moved to the horizontal side of the screen to increase difficulty as the paddle would have to cover more distance. A point would be awarded to the player every time the ball bounced off of the paddle. The game would be over when the player misses the ball five times. With all of these modifications, the game of pong could be converted into a single player game.

Microcontrollers

A microcontroller is an integrated circuit that involves a microprocessor, random access memory, and General Purpose Input Output (GPIO) controllers. The microprocessor would handle the computing and heavy load of the script that the microcontroller is running. The memory would store data and functions that would be referenced in the script, and the GPIO controllers would provide and monitor electrical signals to various components that could be attached to the microcontroller. The Raspberry Pi Pico is a very affordable yet powerful microcontroller. Many devices that can be incorporated in a breadboard circuit with a microcontroller would support the I2C protocol. I2C stands for Inter-Integrated Circuit and involves two data lines: SDA and SCL. The SDA line is used for the transfer of bi-directional data between the device and the microcontroller. The SCL line carries the clock signal generated by the microcontroller to synchronize data transfers on the SDA line. This regulates when data can be sent and read, allowing for a more clear transfer. An SSD1306 OLED Display is a common I2C device that is paired with the Raspberry Pi Pico.

Setup

Before programming any game, it is mandatory to set up the build and environment. For me, I will use a:

To assemble the setup, start by placing the Raspberry Pi Pico on the left side of the breadboard. Next, position the SSD1306 OLED display in the center. Connect the VCC pin of the OLED display to the 3.3V power rail using a red wire, and the GND pin to the ground rail with a black wire. Attach the SCL pin of the display to GPIO 17 on the Pico with a yellow wire and the SDA pin to GPIO 16 with a blue wire. For the buttons, place them on the right side of the breadboard. Connect one terminal of the first button to GPIO 0 on the Pico with a green wire, and link its other terminal to the ground rail using a black wire. Similarly, connect one terminal of the second button to GPIO 1 with a red wire and the other terminal to the ground rail with another black wire. Finally, ensure the 3.3V pin of the Pico is connected to the power rail and a GND pin is connected to the ground rail to complete the circuit. This arrangement allows the Pico to control the OLED display via I2C and detect input from the two buttons.

A vintage pong gaming console

Initialization

The Pong game code begins by importing necessary modules: machine, ssd1306, utime, and urandom. The machine module allows us to interact with the hardware components of the Raspberry Pi Pico, including GPIO pins and the I2C interface. The ssd1306 module is specifically for controlling the SSD1306 OLED display. The utime module provides functions for time delays, which are essential for controlling the game's timing and animations. Finally, the urandom module is used to introduce randomness into the ball's movement, making the game more unpredictable and challenging. Next, we initialize the I2C interface for the display with the line i2c = machine.I2C(0, scl=machine.Pin(17), sda=machine.Pin(16), freq=400000). This sets up the I2C communication on bus 0 with GPIO 17 as the clock (SCL) and GPIO 16 as the data (SDA) lines, and a frequency of 400 kHz. The oled variable is then initialized as an instance of the ssd1306.SSD1306_I2C class, setting the display resolution to 128x64 pixels and linking it to the I2C interface we just configured. Two buttons are initialized using machine.Pin to assign GPIO pins 0 and 1 to button_a and button_b, respectively. These pins are set as inputs with pull-up resistors, meaning they will read a high value when not pressed and a low value when pressed. Several variables are defined to set up the initial conditions of the game: paddle_width and paddle_height define the size of the paddle, while paddle_x and paddle_y set its initial position on the screen. The ball's initial position is set with ball_x and ball_y, and its movement direction is determined by ball_dx and ball_dy. The score is tracked with the score variable, and misses keeps count of how many times the player has missed the ball, with max_misses setting the limit at which the game ends.

# A singleplayer pong game created by Satej Gandre
import machine
import ssd1306
import utime
import urandom

# Initialize I2C for the display
i2c = machine.I2C(0,scl = machine.Pin(17), sda = machine.Pin(16), freq=400000)
oled = ssd1306.SSD1306_I2C(128, 64, i2c)

# Initialize buttons
button_a = machine.Pin(0, machine.Pin.IN, machine.Pin.PULL_UP)
button_b = machine.Pin(1, machine.Pin.IN, machine.Pin.PULL_UP)

# Paddle and ball parameters
paddle_width = 20
paddle_height = 4
paddle_x = 86  # Start paddle in the middle
paddle_y = 60
ball_x = 64
ball_y = 32
ball_dx = 2
ball_dy = 2
score = 0
misses = 0
max_misses = 5
            

Functions

The draw function handles all the display updates. It clears the screen with oled.fill(0), draws a horizontal line at y=10 using oled.hline, and renders the paddle and ball using oled.fill_rect. It also displays the current score in the top-left corner and a health bar in the top-right corner, which diminishes as the player misses the ball. The display is updated with oled.show(). The update_paddle function reads the state of the buttons to move the paddle left or right. If button_a is pressed and the paddle is not at the left edge of the screen, the paddle moves left. Similarly, if button_b is pressed and the paddle is not at the right edge, the paddle moves right. The update_ball function moves the ball according to its current direction. It handles collisions with the walls, the paddle, and detects when the ball goes out of bounds. If the ball hits the left or right wall, its horizontal direction (ball_dx) is reversed. If it hits the top of the screen, its vertical direction (ball_dy) is reversed. When the ball collides with the paddle, the ball's vertical direction is reversed, and randomness is added to its movement to make the game more dynamic. The ball's speed is also capped to ensure it remains manageable. If the ball goes below the screen, a miss is recorded, and the game is reset.

def draw():
    oled.fill(0)
    oled.hline(0, 10, 128, 1)
    oled.fill_rect(paddle_x, paddle_y, paddle_width, paddle_height, 1)
    oled.fill_rect(int(ball_x), int(ball_y), 2, 2, 1)
    oled.rect(113, 0, 15, 7, 1)
    oled.fill_rect(113, 0, int(20*((max_misses-misses-1)/max_misses)), 7, 1)
    oled.text(f'Score: {score}', 0, 0)
    oled.show()

def update_paddle():
    global paddle_x
    if not button_a.value() and paddle_x > 0:
        paddle_x -= 4
    if not button_b.value() and paddle_x < (128 - paddle_width):
        paddle_x += 4

def update_ball():
    global ball_x, ball_y, ball_dx, ball_dy, score, misses
    ball_x += ball_dx
    ball_y += ball_dy

    # Ball collision with walls
    if ball_x <= 0 or ball_x >= 126:
        ball_dx = -ball_dx
    if ball_y <= 11:
        ball_dy = -ball_dy
    # Ball collision with paddle
    if (paddle_y <= ball_y + 2 <= paddle_y + paddle_height and
        paddle_x <= ball_x <= paddle_x + paddle_width):
        ball_y = paddle_y - 2  # Ensure ball is positioned above the paddle
        ball_dy = -ball_dy
        # Add randomness to the ball's direction
        ball_dx += urandom.uniform(-1, 1)
        ball_dy += urandom.uniform(-1, 1)
        # Ensure the ball's speed is within reasonable bounds
        if ball_dx > 3:
            ball_dx = 3
        if ball_dx < -3:
            ball_dx = -3
        if ball_dy > 3:
            ball_dy = 3
        if ball_dy < -3:
            ball_dy = -3
        if (-0.6 < ball_dy) and (ball_dy < 0.6):
            ball_dy = [-0.6,0.6][urandom.randint(0,1)]
        if (-0.6 < ball_dx) and (ball_dx < 0.6):
            ball_dx = [-0.6,0.6][urandom.randint(0,1)]
        score += 1
        print(ball_dx)
        print(ball_dy)
    # Ball out of bounds
    if ball_y > 64:
        misses += 1
        reset_game() # This function will be defined later!
            

Text animations

The reset_game function resets the ball's position and direction. If the player has missed the ball more than the maximum allowed misses (max_misses), the game over animation is triggered by calling the game_over_animation function. The game_over_animation function creates an animation sequence that displays "Game Over" and then credits the game's creator. The animation moves the text upwards, mimicking a scrolling effect. After the animation, it waits for the player to press a button to restart the game. The mainloop function is the heart of the game, containing the main game loop. It starts with a splash screen that displays the game's title and the creator's name, scrolling the text upward. After the splash screen, the main loop runs continuously, updating the paddle and ball positions, drawing the game elements, and managing the game logic. The loop includes a small delay (utime.sleep(0.01)) to control the speed of the game.

def reset_game():
    global ball_x, ball_y, ball_dx, ball_dy, score, misses
    ball_x, ball_y = 64, 32
    ball_dx, ball_dy = 2, 2
    if misses >= max_misses:
        game_over_animation()
        # Reset variables
        misses = 0
        score = 0

def game_over_animation():
    for i in range(75):
        oled.fill(0)
        oled.text("Game Over", 25, i-10)
        oled.show()
        utime.sleep(0.05)
    for i in range(91):
        oled.fill(0)
        oled.text("Created by:", 25, i-26)
        oled.text("Satej Gandre", 18, i-10)
        oled.show()
        utime.sleep(0.05)
    for i in range(42):
        oled.fill(0)
        oled.text("Restart?", 32, i-10)
        oled.show()
        utime.sleep(0.05)
    while 1:
        if (not button_a.value()) or (not button_b.value()):
            break
    for i in range(33):
        oled.fill(0)
        oled.text("Restart?", 32, i+32)
        oled.show()
        utime.sleep(0.05)
    utime.sleep(1)

def mainloop():
    # Splashscreen
    for i in range(42):
        oled.fill(0)
        oled.text("PyAreSquare 2024",0,i-41)
        oled.text("Pong", 50, i-10)
        oled.show()
        utime.sleep(0.05)
    while 1:
        if (not button_a.value()) or (not button_b.value()):
            break
    for i in range(51):
        oled.fill(0)
        oled.text("PyAreSquare 2024",0,int(1.5*i))
        oled.text("Pong", 50, i+32)
        oled.show()
        utime.sleep(0.05)
    utime.sleep(1)
    while True: # Main gameloop
        update_paddle()
        update_ball()
        draw()
        utime.sleep(0.01)
        
mainloop() # Start the game
        

Easter Eggs

The aimbot function in the Pong game code is a hidden feature designed to assist players by automatically aligning the paddle with the ball. When both buttons are pressed simultaneously, the function calculates the position of the ball relative to the paddle and adjusts the paddle's position accordingly to ensure it is always in the optimal position to hit the ball. This provides the player with an almost guaranteed way to keep the ball in play, making the game significantly easier. The aimbot function is designed as an easter egg because it is not a part of the standard game mechanics and is hidden from the player. It is triggered only when an unusual condition is met: both control buttons being pressed at the same time. This is not a typical game action and may not be discovered immediately by the player.

def aimbot():
    global paddle_x, ball_x
    if (not button_a.value()) and (not button_b.value()):
        if (paddle_x) < ball_x and (paddle_x+20) < ball_x and paddle_x < (128 - paddle_width):
            paddle_x += 4
        elif (paddle_x) > ball_x and (paddle_x+20) > ball_x and paddle_x > 0:
            paddle_x -= 4
        

Make sure to include the aimbot function in the mainloop of the code if you want to activate it.

Final remarks

The Pong game developed for the Raspberry Pi Pico using MicroPython showcases a blend of basic hardware interaction and programming ingenuity. Through detailed setup instructions and a well-structured codebase, we explored how to harness the power of the Pico's GPIO pins and I2C interface to create a simple yet engaging game. The inclusion of hardware components like the SSD1306 OLED display and push buttons illustrated how physical elements could be integrated into software development to enrich user interaction. Each segment of the code, from initializing hardware interfaces to handling game logic and rendering graphics, was designed to deliver a cohesive gaming experience. Key features such as the aimbot function added layers of complexity and enjoyment. This hidden functionality, activated by pressing both control buttons simultaneously, serves as an easter egg, providing automatic paddle alignment with the ball. This not only makes the game accessible for beginners but also adds an element of surprise for advanced users who discover this hidden feature.

The design and coding principles applied here extend beyond this simple Pong game, offering insights into more sophisticated projects involving microcontrollers and interactive displays. The detailed explanation of the game's imports, variables, and functions provides a thorough understanding of how each component contributes to the overall functionality. The aimbot function, in particular, highlights how creative coding can introduce hidden features that enhance user engagement. Moreover, the inclusion of a game over animation demonstrates how to create smooth transitions and visually appealing effects, enriching the user experience. The careful management of game state, score tracking, and player feedback through display updates rounds off a well-thought-out project that balances simplicity with interactivity. By assembling the hardware and understanding the code, developers can appreciate the synergy between physical components and software logic. This project serves as a stepping stone for more complex endeavors, emphasizing the importance of clear, modular code and the potential for creative enhancements through features like easter eggs. Ultimately, this Pong game is a testament to the educational and practical value of microcontroller projects, inspiring further exploration and innovation in the realm of embedded systems.