Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Structure a pygame (pygame-ce) game in Python: the init/event/update/draw loop, delta-time movement, Surface/Rect blitting, keyboard/mouse input, and Sprite/Group management with collision. Use when building or debugging a pygame game — when the user mentions pygame, pygame-ce, the game loop, blit, Surface, Rect, sprite groups, or clock.tick. Targets pygame-ce.
.claude/skills/gamedev-skills-pygame-core/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 54% | 0% |
| case-22 | ✓→✓ | = Same ✓ | 23% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 560% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 63% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 77% | 0% |
Build the foundation of a pygame game in Python: the main loop, delta-time movement, drawing with Surface/Rect, input, and Sprite/Group management. Targets pygame-ce 2.5.7 (the actively maintained community fork; same import pygame).
input handling, blitting, or sprite/group collision.
import pygame and the project depends on pygame-ce(or pygame).
When not to use: Python language questions unrelated to pygame. 3D rendering (pygame is 2D). For cross-engine save/load use save-systems; for rebindable input architecture see input-systems.
pip install pygame-ce — it's themaintained fork and imports as pygame. Don't install both in one environment.
pygame.init(), screen =pygame.display.set_mode((w, h)), clock = pygame.time.Clock().
frame (for event in pygame.event.get()), update state, redraw, then pygame.display.flip().
dt = clock.tick(60) / 1000 (seconds)and scale all motion by dt. Keep positions as floats; blit at integer rects.
KEYDOWN/MOUSEBUTTONDOWN, for discreteactions) and polled (pygame.key.get_pressed(), for held movement).
Sprite + Group. Subclass pygame.sprite.Spritewith image/rect; group.update(dt) and group.draw(screen) handle the batch. Run it and watch the window before assuming it works.
pythonimport pygame pygame.init() screen = pygame.display.set_mode((800, 600)) pygame.display.set_caption("My Game") clock = pygame.time.Clock() running = True while running: dt = clock.tick(60) / 1000 # cap at 60 FPS; dt = seconds since last frame for event in pygame.event.get(): # MUST drain the queue or the OS thinks it hung if event.type == pygame.QUIT: running = False # update game state here, scaled by dt ... screen.fill((18, 18, 28)) # clear each frame # draw everything here ... pygame.display.flip() # present the frame pygame.quit()
pythonfrom pygame.math import Vector2 pos = Vector2(100, 100) # keep position as floats speed = 220 # PIXELS PER SECOND, not per frame # inside the loop, after computing dt: keys = pygame.key.get_pressed() direction = Vector2( keys[pygame.K_RIGHT] - keys[pygame.K_LEFT], keys[pygame.K_DOWN] - keys[pygame.K_UP], ) if direction.length_squared() > 0: direction = direction.normalize() # equal speed on diagonals pos += direction * speed * dt # RIGHT: dt-scaled screen.blit(player_img, (round(pos.x), round(pos.y))) # blit at integer pixels
pythonfor event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN: # discrete press: jump, menu, pause if event.key == pygame.K_SPACE: jump() elif event.key == pygame.K_ESCAPE: running = False elif event.type == pygame.MOUSEBUTTONDOWN: shoot_at(event.pos) # event.pos = (x, y) # Polled state (read once per frame) for continuous/held input: keys = pygame.key.get_pressed() if keys[pygame.K_a]: move_left(dt)
pythonclass Player(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() # convert() once at load makes blits much faster; _alpha keeps transparency. self.image = pygame.image.load("player.png").convert_alpha() self.rect = self.image.get_rect(center=(x, y)) self.pos = pygame.math.Vector2(self.rect.center) self.speed = 240 def update(self, dt): # Group.update(dt) calls this per sprite keys = pygame.key.get_pressed() self.pos.x += (keys[pygame.K_RIGHT] - keys[pygame.K_LEFT]) * self.speed * dt self.rect.center = (round(self.pos.x), round(self.pos.y)) all_sprites = pygame.sprite.Group() all_sprites.add(Player(400, 300)) # in the loop: all_sprites.update(dt) # calls each sprite's update(dt) all_sprites.draw(screen) # blits each sprite at its rect
python# Sprite vs group: e.g. player picking up coins (True = remove collided coins). collected = pygame.sprite.spritecollide(player, coins, dokill=True) score += len(collected) # Group vs group: bullets vs enemies (kill both on hit). hits = pygame.sprite.groupcollide(bullets, enemies, True, True) # Plain rect overlap (no sprites needed): if player.rect.colliderect(door_rect): open_door()
pygame.event.get() (or pygame.event.pump()) every frame.
Scale by dt = clock.tick(fps) / 1000 and use pixels-per-second values.
rect coordinates are integers; store thetrue position as a Vector2 of floats and assign rect.center = round(...) each frame.
.convert() (opaque) or.convert_alpha() (transparent) on loaded images once; un-converted surfaces blit far slower.
pygame.display.flip() (or update()), or youdrew before screen.fill(...) so it was cleared away.
Draw background first, sprites last.
pip install pygame got the old one → for the maintained fork usepip install pygame-ce; having both installed causes import conflicts.
speed.
Group variants (GroupSingle, LayeredUpdates for z-order), pixel-perfectcollision with mask, slicing a spritesheet, simple animation, sound/music, and text rendering, read references/sprites-and-collision.md.
love2d-core — the same loop concepts in LÖVE/Lua.bevy-ecs — a heavier ECS engine when a project outgrows pygame.input-systems / save-systems — engine-agnostic input and persistence.platformer / roguelike — genre templates that pair with pygame.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-22 | pass→pass | 22,919 | 18,043 | -21% | 1 | 1 | 0% | 4,788 | 5,868 | +23% | 0 | 0 | — |
case-01 | fail→pass | 12,404 | 10,249 | -17% | 1 | 1 | 0% | 2,825 | 4,355 | +54% | 0 | 0 | — |
case-02 | fail→fail | 16,058 | 15,931 | -1% | 1 | 1 | 0% | 3,640 | 5,461 | +50% | 0 | 0 | — |
case-03 | pass→pass | 2,477 | 1,703 | -31% | 1 | 1 | 0% | 347 | 2,291 | +560% | 0 | 0 | — |
case-04 | pass→pass | 10,096 | 6,932 | -31% | 1 | 1 | 0% | 1,998 | 3,255 | +63% | 0 | 0 | — |
case-05 | pass→pass | 10,741 | 8,350 | -22% | 1 | 1 | 0% | 1,954 | 3,460 | +77% | 0 | 0 | — |
case-06 | pass→pass | 4,687 | 3,523 | -25% | 1 | 1 | 0% | 778 | 2,592 | +233% | 0 | 0 | — |
case-07 | pass→pass | 7,661 | 4,066 | -47% | 1 | 1 | 0% | 1,411 | 2,695 | +91% | 0 | 0 | — |
case-08 | pass→pass | 10,017 | 5,220 | -48% | 1 | 1 | 0% | 1,923 | 2,953 | +54% | 0 | 0 | — |
case-09 | pass→pass | 9,446 | 6,359 | -33% | 1 | 1 | 0% | 1,918 | 3,225 | +68% | 0 | 0 | — |
case-10 | pass→pass | 7,773 | 4,240 | -45% | 1 | 1 | 0% | 772 | 2,644 | +242% | 0 | 0 | — |
case-11 | pass→pass | 2,650 | 2,653 | +0% | 1 | 1 | 0% | 407 | 2,454 | +503% | 0 | 0 | — |
case-16 | pass→pass | 3,009 | 2,181 | -28% | 1 | 1 | 0% | 567 | 2,303 | +306% | 0 | 0 | — |
case-12 | pass→pass | 7,979 | 4,699 | -41% | 1 | 1 | 0% | 1,574 | 2,778 | +76% | 0 | 0 | — |
case-13 | pass→pass | 3,440 | 2,537 | -26% | 1 | 1 | 0% | 590 | 2,395 | +306% | 0 | 0 | — |
case-14 | pass→pass | 6,869 | 4,062 | -41% | 1 | 1 | 0% | 1,204 | 2,626 | +118% | 0 | 0 | — |
case-15 | pass→pass | 11,220 | 5,349 | -52% | 1 | 1 | 0% | 1,927 | 2,897 | +50% | 0 | 0 | — |
case-21 | pass→pass | 19,781 | 15,668 | -21% | 1 | 1 | 0% | 4,032 | 5,252 | +30% | 0 | 0 | — |
case-17 | pass→pass | 3,861 | 3,602 | -7% | 1 | 1 | 0% | 687 | 2,583 | +276% | 0 | 0 | — |
case-18 | pass→pass | 10,345 | 5,371 | -48% | 1 | 1 | 0% | 1,939 | 2,976 | +53% | 0 | 0 | — |
case-19 | pass→pass | 3,499 | 3,680 | +5% | 1 | 1 | 0% | 660 | 2,575 | +290% | 0 | 0 | — |
case-20 | pass→pass | 26,210 | 22,429 | -14% | 1 | 1 | 0% | 5,293 | 6,693 | +26% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 8/2/2026 | +13% |
Other measured skills in the registry, with their headline benchmark lift.