Pygame Games skill for Codex

Python 2D games that run the same on every PC. Codex checks whether the project uses pygame-ce or classic pygame, then writes a clean events-update-draw loop with delta time, converts every image once, manages sprites in groups and resolves wall collisions per axis. Logic is tested headless with SDL dummy drivers, and the game is packaged with PyInstaller or pygbag so it runs without a Python install.

  • Game Development
  • $pygame-games
  • 200 lines
  • 2 files
  • ~2,100 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- pygame-games

Installs to ~/.agents/skills/pygame-games/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $pygame-games, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds 2D games in Python with pygame-ce (or classic pygame) - main loop with clock and delta time, SCALED pixel-art windows, sprites and groups, Rect and FRect collision, surfaces with convert_alpha, tilemaps, sound mixer, scene management, headless tests with the dummy video driver, and packaging with PyInstaller or pygbag for the browser. Use when writing or fixing a Pygame project, a game runs at different speeds on different machines, sprites flicker or blit slowly, both pygame and pygame-ce are installed, or the game must ship as an executable or web build.

Use it when

  • Writing or refactoring a 2D game in Python with Pygame
  • Movement speed or physics depends on frame rate
  • Blitting is slow or pixel art looks blurry when scaled
  • Shipping a Pygame game as an executable or to the web

Use something else when

How to check the result

pytest passes with SDL dummy drivers, movement distance per second matches at 30 and 144 FPS, and the packaged build loads assets when launched from another directory.

The mistake it prevents

Installing pygame and pygame-ce side by side, which overwrite each other's files under the same import name and cause baffling version-specific errors.

~/.agents/skills/pygame-games/SKILL.md

--- name: pygame-games description: Builds 2D games in Python with pygame-ce (or classic pygame) - main loop with clock and delta time, SCALED pixel-art windows, sprites and groups,… license: MIT ---

Done well means: one clear loop (events, update with dt, draw, flip), all images converted once at load, sprites managed in groups, movement independent of frame rate, scenes separated, the game logic testable without a window, and a build that runs on a machine without Python.

Before writing code

  1. Read AGENTS.md, pyproject.toml / requirements.txt. Determine which package is used: pygame-ce (community edition, actively developed, recommended for new projects) or pygame. Both import as pygame; never install both in one environment, since they overwrite each other’s files. Check with python -c "import pygame; print(pygame.version.ver, getattr(pygame, 'IS_CE', False))".
  2. Use the project’s environment tool (uv, venv, poetry). Installing needs network; ask if the sandbox blocks it.
  3. Find the asset folder and how paths are built. Use paths relative to the source file, not the working directory.

Skeleton

from pathlib import Path
import pygame

ASSETS = Path(__file__).resolve().parent / "assets"
WIDTH, HEIGHT = 320, 180            # logical resolution for pixel art
FPS = 60


class Player(pygame.sprite.Sprite):
    SPEED = 90.0                    # pixels per second

    def __init__(self, pos, image: pygame.Surface, *groups):
        super().__init__(*groups)
        self.image = image
        self.rect = self.image.get_frect(center=pos)   # FRect keeps sub-pixel position (pygame-ce)
        self.velocity = pygame.Vector2()

    def update(self, dt: float, keys) -> None:
        self.velocity.x = (keys[pygame.K_RIGHT] - keys[pygame.K_LEFT]) * self.SPEED
        self.velocity.y = (keys[pygame.K_DOWN] - keys[pygame.K_UP]) * self.SPEED
        if self.velocity.length_squared() > 0:
            self.velocity.scale_to_length(self.SPEED)  # no faster diagonals
        self.rect.center += self.velocity * dt


def main() -> None:
    pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.SCALED | pygame.RESIZABLE, vsync=1)
    pygame.display.set_caption("Game")
    clock = pygame.time.Clock()

    hero_img = pygame.image.load(ASSETS / "hero.png").convert_alpha()   # convert after set_mode
    all_sprites = pygame.sprite.Group()
    player = Player((WIDTH / 2, HEIGHT / 2), hero_img, all_sprites)

    running = True
    while running:
        dt = min(clock.tick(FPS) / 1000.0, 0.05)       # seconds; clamp after hitches
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
                running = False

        all_sprites.update(dt, pygame.key.get_pressed())

        screen.fill((18, 18, 24))
        all_sprites.draw(screen)
        pygame.display.flip()

    pygame.quit()


if __name__ == "__main__":
    main()
  • pygame.SCALED renders at the logical size and scales to the window with integer-friendly scaling, which keeps pixel art sharp and handles fullscreen toggling (pygame.display.toggle_fullscreen()).
  • vsync=1 requires SCALED or OpenGL and may be ignored by some drivers; clock.tick(FPS) still caps the rate.
  • Rect stores integers; moving by velocity * dt with small values truncates to zero. Use FRect (pygame-ce) or keep a Vector2 position and copy it into the rect.

Structure decisions

NeedApproach
Multiple screensScene classes with handle_event, update(dt), draw(surface); a manager swaps or stacks them
Many sprites, layered drawingpygame.sprite.LayeredUpdates or sort by rect.bottom for top-down depth
Collisions between groupspygame.sprite.groupcollide, spritecollide with collide_mask only where pixel-precision matters
Tile mapsPre-render static layers to one Surface at load; draw it with one blit per frame
Tiled mapspytmx (load_pygame) if already a dependency
CameraOffset vector subtracted when blitting; do not move every sprite’s world position
Fixed-step physicsAccumulator loop (see game-loop-architecture) when collisions must be stable

Axis-separated collision with walls:

def move_and_collide(sprite, walls, dt):
    sprite.rect.x += sprite.velocity.x * dt
    for wall in pygame.sprite.spritecollide(sprite, walls, False):
        if sprite.velocity.x > 0: sprite.rect.right = wall.rect.left
        elif sprite.velocity.x < 0: sprite.rect.left = wall.rect.right
    sprite.rect.y += sprite.velocity.y * dt
    for wall in pygame.sprite.spritecollide(sprite, walls, False):
        if sprite.velocity.y > 0: sprite.rect.bottom = wall.rect.top
        elif sprite.velocity.y < 0: sprite.rect.top = wall.rect.bottom

Performance

  • convert() / convert_alpha() every loaded image once; unconverted surfaces blit many times slower. They require a display mode to exist first.
  • Never load images, fonts or sounds inside the loop. Cache rendered text; font.render per frame for static labels is wasteful.
  • Draw only what is on screen; for big worlds, cull by camera rect.
  • Use pygame.transform.scale_by / rotozoom once and cache rotations (e.g. 36 frames) rather than rotating every frame.
  • Profile with python -m cProfile -s cumtime main.py before optimising.

Audio

  • Call pygame.mixer.pre_init(44100, -16, 2, 512) before pygame.init() to reduce latency.
  • Sound effects: pygame.mixer.Sound loaded once. Music: pygame.mixer.music.load/play streams.
  • Prefer OGG for music and WAV or OGG for short effects; MP3 support depends on build.

Testing without a window

# tests/conftest.py
import os
os.environ.setdefault("SDL_VIDEODRIVER", "dummy")
os.environ.setdefault("SDL_AUDIODRIVER", "dummy")
def test_player_speed_is_frame_rate_independent():
    import pygame
    pygame.init()
    pygame.display.set_mode((1, 1))
    img = pygame.Surface((8, 8))
    keys = {pygame.K_RIGHT: 1, pygame.K_LEFT: 0, pygame.K_UP: 0, pygame.K_DOWN: 0}
    a, b = Player((0, 0), img), Player((0, 0), img)
    for _ in range(60): a.update(1 / 60, keys)
    for _ in range(30): b.update(1 / 30, keys)
    assert abs(a.rect.centerx - b.rect.centerx) < 0.01

Keep rules (scoring, inventory, level parsing) in plain Python modules that do not import pygame, so most tests need no display at all.

Packaging

TargetToolNotes
Windows/macOS/Linux executablePyInstaller (pyinstaller --onedir --windowed main.py --add-data "assets:assets")Build on each target OS; use --onedir for faster start; on Windows the --add-data separator is ; in older versions
BrowserpygbagMain loop must be async def main() with await asyncio.sleep(0) each frame; assets in the game folder; OGG audio
Python userspyproject.toml with an entry pointSimplest for jams and tools

In frozen builds, resolve assets from sys._MEIPASS when present (PyInstaller one-file) or from the executable’s folder.

Gotchas

  • Event loop must run every frame, or the OS marks the window as not responding.
  • pygame.key.get_pressed() for held keys; KEYDOWN events for presses. Mixing them up drops quick taps.
  • clock.tick() returns milliseconds, not seconds.
  • Surface.set_alpha on a per-pixel alpha surface behaves differently between versions; test fades on the target version.
  • Mac Retina and Windows DPI scaling can make the window tiny or blurry; SCALED handles most cases.

Verification

SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy python -m pytest -q
python -m cProfile -s cumtime main.py | head -30     # when performance is in question
  • Run the game at 30 and 144 FPS caps: movement covers the same distance per second.
  • Build the executable and launch it from a different working directory: assets still load.
  • If no display is available here, say which checks need the user to play the build.

Hand-off summary

**Stack:** pygame-ce 2.5, Python 3.12, uv
**Structure:** SceneManager (Title, Play, Pause), sprites in LayeredUpdates, camera offset
**Fixed:** speed tied to FPS (now dt-based with FRect), images converted once at load
**Tests:** 14 pytest tests with dummy SDL drivers
**Build:** PyInstaller onedir for Windows (built on Windows); pygbag web build needs async main (done)

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Pygame Games"
  short_description: "Python 2D games that run the same on every PC"
  default_prompt: "$pygame-games Make a top-down Pygame prototype with a scrolling camera and wall collisions"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem