Games in Python

Published May 09, 2024 · 1 min read

I had some fun creating simple games in Python using the pygame library. Here are some of the projects I worked on:

{
  description = "Nix environment with Python and pygame for games";

  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
  inputs.flake-utils.url = "github:numtide/flake-utils";

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
    let
        pkgs = nixpkgs.legacyPackages.${system};
    in {
        devShells.default = pkgs.mkShell {
            name = "pygame-shell";
            buildInputs = with pkgs; [
                python312
                python312Packages.pygame
            ];
        };
    });
}

Project 1: Pong

Pong Screenshot

#!/usr/bin/env python

import random
import pygame
import math

from pygame.locals import *

pygame.init()

ITEM_SIZE = 20
FRAME_RATE = 60
SCREEN_SIZE = (1080, 720)

screen = pygame.display.set_mode(SCREEN_SIZE)
pygame.display.set_caption("Python Pong")

font = pygame.font.SysFont(None, 24)
clock = pygame.time.Clock()
game_is_running = True

class Colors:
    white = (200, 200, 200)
    red = (200, 100, 20)
    green = (20, 200, 100)
    blue = (100, 20, 200)

    foreground = white
    background = (20, 20, 20)

class Player:
    def __init__(self, name):
        self.name = name

        self.size = 5
        self.score = 0
        self.direction = 0
        self.rect = Rect(0, 0, ITEM_SIZE, ITEM_SIZE * self.size)

        if self.name == "Left Player":
            self.rect.x = ITEM_SIZE * 3
            self.rect.y = SCREEN_SIZE[1] / 2 - (ITEM_SIZE * self.size / 2)
        elif self.name == "Right Player":
            self.rect.x = SCREEN_SIZE[0] - ITEM_SIZE * 3 - ITEM_SIZE
            self.rect.y = SCREEN_SIZE[1] / 2 - (ITEM_SIZE * self.size / 2)
        else:
            raise Exception('Unknown Player type: ' + name)

    def scored(self):
        self.score += 1

    def is_in_bounds(self):
        future_lowest_point = self.rect.y + self.rect.h + self.direction
        future_highest_point = self.rect.y + self.direction

        if future_lowest_point < SCREEN_SIZE[1] and self.direction > 0:
            return True

        if future_highest_point > 0 and self.direction < 0:
            return True

        return False

    def movement(self):
        if self.is_in_bounds():
            self.rect.y += self.direction * ITEM_SIZE / 4

        self.direction = 0

    def update(self):
        keys = pygame.key.get_pressed()

        if self.name == "Left Player":
            if keys[pygame.K_w]:
                self.direction = -1
            if keys[pygame.K_s]:
                self.direction = 1

        elif self.name == "Right Player":
            if keys[pygame.K_i]:
                self.direction = -1
            if keys[pygame.K_k]:
                self.direction = 1

        self.movement()

    def draw(self):
        color = Colors.red if self.name == "Right Player" else Colors.blue

        pygame.draw.rect(screen, color, self.rect)

class Ball:
    speed = 2
    radius = ITEM_SIZE / 2
    variance = 0.05

    def spawn(self):
        rand_n = random.randint(0, 10)
        rand_m = random.randint(0, 10)
        rand_direction_y = random.randint(25, 100) / 100 * (-1) ** rand_n

        self.x = SCREEN_SIZE[0] / 2
        self.y = SCREEN_SIZE[1] / 2
        self.dx = (-1) ** rand_m
        self.dy = 0.9 # rand_direction_y

    def paddle_acuracy(self, rect, future_y):
        min = rect.y
        max = rect.y + rect.h
        future_y_scaled = future_y - min / max - min

        if future_y_scaled >= 0 and future_y_scaled <= 100:
            return round(math.sin(future_y_scaled / 100 * math.pi), 2)
        else:
            return 0

    # FIXME: sometimes ball bounces off of goal
    def should_bounce(self):
        future_y = self.y + self.dy * self.speed
        left_height = self.paddle_acuracy(player_left.rect, future_y)
        right_height = self.paddle_acuracy(player_right.rect, future_y)

        future_x = self.x + self.dx * self.speed
        left_width = self.x - self.radius > player_left.rect.x + player_left.rect.w and future_x < player_left.rect.x
        right_width = self.x + self.radius < player_right.rect.x and future_x > player_right.rect.x + player_right.rect.x

        if future_y - self.radius < 0:
            return (1 + self.variance, -1 + self.variance)
        if future_y + self.radius > SCREEN_SIZE[1]:
            return (1 + self.variance, -1 + self.variance)

        if left_height > 0 and left_width:
            return (-1 + self.variance, 1 + self.variance)
        if right_height > 0 and right_width:
            return (-1 + self.variance, 1 + self.variance)

        return (1, 1)

    def check_goal(self):
        future_x = self.x + self.dx * self.speed
        if future_x < 0:
            player_right.scored()
            self.spawn()

        if future_x > SCREEN_SIZE[0]:
            player_left.scored()
            self.spawn()

    def update(self):
        self.check_goal()

        (dx, dy) = self.should_bounce()

        self.dx = self.dx * dx
        self.dy = self.dy * dy

        if dx != 1 or dy != 1:
            self.speed = self.speed + self.variance

        self.x += self.dx * self.speed
        self.y += self.dy * self.speed

    def draw(self):
        pygame.draw.circle(screen, Colors.white, (self.x, self.y), self.radius)

def update_logic():
    ball.update()
    player_left.update()
    player_right.update()

def draw_screen():
    screen.fill((20, 20, 20))

    ball.draw()
    player_left.draw()
    player_right.draw()

    draw_score()
    draw_fps()

def draw_score():
    img = font.render("Score: " + str(player_left.score) + " | " + str(player_right.score), True, (20, 200, 20))
    screen.blit(img, (20, 20))

def draw_fps():
    img = font.render("FPS: " + str(round(clock.get_fps())), True, (20, 200, 20))
    screen.blit(img, (20, 40))

def log(message):
    print("\033[91m" + message + "\033[90m")

def init():
    global player_left, player_right, ball

    player_left = Player("Left Player")
    player_right = Player("Right Player")

    ball = Ball()
    ball.spawn()

    print("\033[90m")

def game_loop():
    global game_is_running

    while game_is_running:
        for event in pygame.event.get():
            print(event)
            if event.type == pygame.QUIT:
                  game_is_running = False

        update_logic()
        draw_screen()

        pygame.display.update()
        clock.tick(FRAME_RATE)

if __name__ == "__main__":
    init()
    game_loop()
    pygame.quit()

Project 2: Snake

#!/usr/bin/env python

import random
import pygame
from pygame.locals import *

pygame.init()

ITEM_SIZE = 20
FRAME_RATE = 60
SCREEN_SIZE = (1080, 720)

screen = pygame.display.set_mode(SCREEN_SIZE)
pygame.display.set_caption("Python Snake")

font = pygame.font.SysFont(None, 24)
clock = pygame.time.Clock()
game_is_running = True

class Snake:
    rect = Rect(0, 0, ITEM_SIZE, ITEM_SIZE)
    direction = (0, 1)
    cooldown = 0
    tail = []
    x = 0
    y = 0

    def check_death(self):
        global game_is_running

        if self.x < 0 or self.x + ITEM_SIZE > SCREEN_SIZE[0]:
            log("DEATH because you ran into the vertical border")
            game_is_running = False
        if self.y < 0 or self.y + ITEM_SIZE > SCREEN_SIZE[1]:
            log("DEATH because you ran into the horizontal border")
            game_is_running = False

        for part in self.tail:
            if part[0] == self.x and part[1] == self.y:
                log("DEATH because you ate yourself..")
                game_is_running = False

    def movement(self):
        if self.cooldown > 0:
            self.cooldown -= 1
            return

        for i in range(len(self.tail)):
            if i + 1 == len(self.tail):
                self.tail[i] = (self.x, self.y)
                break

            self.tail[i] = self.tail[i + 1]

        self.x += ITEM_SIZE * self.direction[0]
        self.y += ITEM_SIZE * self.direction[1]

        self.check_death()

        self.rect = Rect(self.x, self.y, ITEM_SIZE, ITEM_SIZE)
        self.cooldown = FRAME_RATE / 12

    def update(self):
        self.movement()

        if self.x == apple.x and self.y == apple.y:
            self.tail.append((self.x, self.y))
            apple.spawn_apple()

    def draw(self):
        for part in self.tail:
            rect = Rect(part[0], part[1], ITEM_SIZE, ITEM_SIZE)
            pygame.draw.rect(screen, (100, 100, 100), rect)

        pygame.draw.rect(screen, (200, 200, 200), self.rect)

class Apple:
    rect = Rect(0, 0, ITEM_SIZE, ITEM_SIZE)
    x = 0
    y = 0

    def spawn_apple(self):
        rand_x = random.randint(0, int(SCREEN_SIZE[0] / ITEM_SIZE - 1))
        rand_y = random.randint(0, int(SCREEN_SIZE[1] / ITEM_SIZE - 1))

        self.x = rand_x * ITEM_SIZE
        self.y = rand_y * ITEM_SIZE

        log("New apple at: " + str(self.x) + "|" + str(self.y))

        self.rect = Rect(self.x, self.y, ITEM_SIZE, ITEM_SIZE)

    def draw(self):
        pygame.draw.rect(screen, (200, 20, 20), self.rect)

def update_logic():
    snake.update()

def draw_screen():
    screen.fill((20, 20, 20))

    apple.draw()
    snake.draw()

    draw_score()
    draw_fps()

def draw_score():
    img = font.render("Score: " + str(len(snake.tail)), True, (20, 200, 20))
    screen.blit(img, (20, 20))

def draw_fps():
    img = font.render("FPS: " + str(round(clock.get_fps())), True, (20, 200, 20))
    screen.blit(img, (20, 40))

def check_keyboard(event):
    if event.key == K_UP:
        snake.direction = (0, -1)
    if event.key == K_DOWN:
        snake.direction = (0, 1)
    if event.key == K_LEFT:
        snake.direction = (-1, 0)
    if event.key == K_RIGHT:
        snake.direction = (1, 0)

def log(message):
    print("\033[91m" + message + "\033[90m")

def init():
    global snake, apple

    snake = Snake()
    apple = Apple()

    apple.spawn_apple()

    print("\033[90m")

def game_loop():
    global game_is_running

    while game_is_running:
        for event in pygame.event.get():
            print(event)
            if event.type == pygame.KEYDOWN:
                check_keyboard(event)
            if event.type == pygame.QUIT:
                  game_is_running = False

        update_logic()
        draw_screen()

        pygame.display.update()
        clock.tick(FRAME_RATE)

    log("\nYou died with " + str(len(snake.tail)) + " point(s).\n")

if __name__ == "__main__":
    init()
    game_loop()
    pygame.quit()