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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- pygame-games Installs to ~/.agents/skills/pygame-games/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install pygame-games The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project pygame-games Installs to .agents/skills/pygame-games/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/pygame-games.zipunzip -o pygame-games.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) pygame-games PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- You need the generic fixed-timestep loop design: use game-loop-architecture
- The game targets the browser first and Python is not required: use phaser-games
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.
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
- Read
AGENTS.md,pyproject.toml/requirements.txt. Determine which package is used:pygame-ce(community edition, actively developed, recommended for new projects) orpygame. Both import aspygame; never install both in one environment, since they overwrite each other’s files. Check withpython -c "import pygame; print(pygame.version.ver, getattr(pygame, 'IS_CE', False))". - Use the project’s environment tool (uv, venv, poetry). Installing needs network; ask if the sandbox blocks it.
- 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.SCALEDrenders 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=1requiresSCALEDor OpenGL and may be ignored by some drivers;clock.tick(FPS)still caps the rate.Rectstores integers; moving byvelocity * dtwith small values truncates to zero. UseFRect(pygame-ce) or keep aVector2position and copy it into the rect.
Structure decisions
| Need | Approach |
|---|---|
| Multiple screens | Scene classes with handle_event, update(dt), draw(surface); a manager swaps or stacks them |
| Many sprites, layered drawing | pygame.sprite.LayeredUpdates or sort by rect.bottom for top-down depth |
| Collisions between groups | pygame.sprite.groupcollide, spritecollide with collide_mask only where pixel-precision matters |
| Tile maps | Pre-render static layers to one Surface at load; draw it with one blit per frame |
| Tiled maps | pytmx (load_pygame) if already a dependency |
| Camera | Offset vector subtracted when blitting; do not move every sprite’s world position |
| Fixed-step physics | Accumulator 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.renderper frame for static labels is wasteful. - Draw only what is on screen; for big worlds, cull by camera rect.
- Use
pygame.transform.scale_by/rotozoomonce and cache rotations (e.g. 36 frames) rather than rotating every frame. - Profile with
python -m cProfile -s cumtime main.pybefore optimising.
Audio
- Call
pygame.mixer.pre_init(44100, -16, 2, 512)beforepygame.init()to reduce latency. - Sound effects:
pygame.mixer.Soundloaded once. Music:pygame.mixer.music.load/playstreams. - 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
| Target | Tool | Notes |
|---|---|---|
| Windows/macOS/Linux executable | PyInstaller (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 |
| Browser | pygbag | Main loop must be async def main() with await asyncio.sleep(0) each frame; assets in the game folder; OGG audio |
| Python users | pyproject.toml with an entry point | Simplest 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;KEYDOWNevents for presses. Mixing them up drops quick taps.clock.tick()returns milliseconds, not seconds.Surface.set_alphaon 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;
SCALEDhandles 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.yaml
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.