pygame · diff

v2.0.0 to v2.0.0

342 added, 306 removed. Audit A to A.

---
name: pygame
description: Use when building 2D games in Python with pygame - game loop and delta time, sprites and groups, collision detection, drawing and surfaces, events and input, sound and fonts, camera, or performance optimization
metadata:
author: mte90
version: 2.0.0
tags:
- python
- game-development
- 2d-games
- pygame
- graphics
- game-engine
---
# Pygame
Python game development library.
- ## Overview
-
- Pygame is a set of Python modules designed for writing video games. It provides functionality for creating graphics, handling input, playing sounds, and more.
-
- **Key Features:**
- - 2D graphics and sprites
- - Event handling
- - Sound and music playback
- - Font rendering
- - Collision detection
- - Game loops
- - Hardware acceleration
-
- ### Installation
+ ## Quick Start
```bash
- # Install pygame-ce (Community Edition) - recommended
- pip install pygame-ce
-
- # Legacy pygame (no longer maintained)
- pip install pygame
-
- # With additional features
- pip install pygame-ce[fonts]
- ```
-
- ### pygame-ce (Community Edition)
-
- pygame-ce is the maintained fork of pygame. Key differences:
-
- - 20-30% faster performance in many benchmarks
- - `IS_CE` flag to detect pygame-ce
- - Better Python 3.10+ support
- - Active development and bug fixes
-
- ```python
- import pygame
- print(pygame.ver) # '2.x.x' for pygame-ce, '1.x.x' for legacy
-
- # Check if using pygame-ce
- if hasattr(pygame, 'IS_CE'):
- print("Using pygame-ce!")
+ pip install pygame-ce # Community Edition - actively maintained
```
- ## Getting Started
-
- ### Basic Window
-
```python
import pygame
- import sys
- # Initialize pygame
pygame.init()
-
- # Create window
screen = pygame.display.set_mode((800, 600))
- pygame.display.set_caption("My Game")
+ clock = pygame.time.Clock()
- # Game loop
running = True
while running:
- # Event handling
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
- if event.type == pygame.KEYDOWN:
- if event.key == pygame.K_ESCAPE:
- running = False
- # Drawing
- screen.fill((0, 0, 0)) # Black background
- pygame.draw.rect(screen, (255, 0, 0), (100, 100, 50, 50))
-
- # Update display
+ screen.fill((0, 0, 0))
+ # Draw game objects
pygame.display.flip()
-
- # Quit pygame
- pygame.quit()
- sys.exit()
- ```
-
- ## Events
-
- ### Event Types
-
- ```python
- for event in pygame.event.get():
- # Quit
- if event.type == pygame.QUIT:
- running = False
- # Key pressed
- if event.type == pygame.KEYDOWN:
- if event.key == pygame.K_SPACE:
- print("Space pressed")
- if event.key == pygame.K_ESCAPE:
- running = False
-
- # Key released
- if event.type == pygame.KEYUP:
- if event.key == pygame.K_SPACE:
- print("Space released")
-
- # Mouse clicked
- if event.type == pygame.MOUSEBUTTONDOWN:
- x, y = event.pos
- button = event.button
- if button == 1: # Left click
- print(f"Left click at {x}, {y}")
-
- # Mouse motion
- if event.type == pygame.MOUSEMOTION:
- x, y = event.pos
- rel_x, rel_y = event.rel
-
- # Joystick events
- if event.type == pygame.JOYBUTTONDOWN:
- if event.button == 0: # A button
- print("Joystick A pressed")
-
- # Window events
- if event.type == pygame.WINDOWFOCUSLOST:
- print("Window lost focus")
- if event.type == pygame.WINDOWRESIZED:
- print(f"Window resized to {event.x}x{event.y}")
- ```
-
- ### Input States
-
- ```python
- # Keyboard state
- keys = pygame.key.get_pressed()
- if keys[pygame.K_SPACE]:
- print("Space is held down")
-
- # Mouse state
- mouse_pos = pygame.mouse.get_pos()
- mouse_buttons = pygame.mouse.get_pressed()
- if mouse_buttons[0]: # Left button
- print("Left mouse button held")
-
- # Joystick state
- joystick = pygame.joystick.Joystick(0)
- axes = joystick.get_numaxes()
- for i in range(axes):
- value = joystick.get_axis(i)
- ```
-
- ## Collision Detection
-
- ```python
- # Rectangle collision
- if pygame.sprite.collide_rect(sprite1, sprite2):
- print("Collision!")
-
- # Group collision
- hits = pygame.sprite.spritecollide(player, enemies, True) # Kill enemies
- for enemy in hits:
- score += 10
-
- # Group vs group
- pygame.sprite.groupcollide(bullets, enemies, True, True) # Kill both
-
- # Circle collision (requires radius attribute)
- pygame.sprite.collide_circle(sprite1, sprite2)
-
- # Group circle collision
- pygame.sprite.spritecollide(player, enemies, True, pygame.sprite.collide_circle)
-
- # Mask collision (pixel-perfect)
- mask1 = pygame.mask.from_surface(sprite1.image)
- mask2 = pygame.mask.from_surface(sprite2.image)
- if sprite1.rect.colliderect(sprite2.rect): # First check bounding
- offset = (sprite2.rect.x - sprite1.rect.x, sprite2.rect.y - sprite1.rect.y)
- if mask1.overlap(mask2, offset):
- print("Pixel-perfect collision!")
- ```
-
- ## Sound
-
- ```python
- # Initialize mixer
- pygame.mixer.init()
-
- # Load sound
- shoot_sound = pygame.mixer.Sound("shoot.wav")
- explosion_sound = pygame.mixer.Sound("explosion.wav")
-
- # Set volume
- shoot_sound.set_volume(0.5)
- explosion_sound.set_volume(0.8)
-
- # Play sound
- shoot_sound.play()
- shoot_sound.play(maxtime=500) # Stop after 500ms
-
- # Load music (streaming)
- pygame.mixer.music.load("bgm.mp3")
- pygame.mixer.music.play(-1) # Loop forever
- pygame.mixer.music.pause()
- pygame.mixer.music.unpause()
- pygame.mixer.music.stop()
+ clock.tick(60) # Limit to 60 FPS
- # Music volume
- pygame.mixer.music.set_volume(0.5)
+ pygame.quit()
```
- ## Fonts
-
- ```python
- # Initialize font system
- pygame.font.init()
-
- # Get default font
- font = pygame.font.Font(None, 36) # Default system font, size 36
-
- # Load custom font
- font = pygame.font.Font("custom.ttf", 36)
-
- # Render text
- text_surface = font.render("Hello, World!", True, WHITE) # Antialiased
- text_surface = font.render("Hello", False, RED) # Not antialiased
-
- # Get text size
- width, height = font.size("Hello")
-
- # Draw text on screen
- screen.blit(text_surface, (x, y))
- ```
+ For full API reference, see https://www.pygame.org/docs/ref/
- ## Time and Delta Time
+ ## Game Loop Architecture
- ```python
- import pygame
- import time
+ ### The Non-Negotiable Discipline
- # Clock
- clock = pygame.time.Clock()
+ A game loop has three phases: **handle events** → **update simulation** → **render**. Never derive motion from raw frame counts—always use delta time (dt).
- # Set framerate
- clock.tick(60) # 60 FPS
- fps = clock.get_fps()
+ **Critical pattern:** `dt = clock.tick(FPS) / 1000` without clamping lets a stalled frame (e.g., system suspend, GC pause) teleport entities through walls. A 5-second stall at 60 FPS yields `dt = 5.0`, moving a 100 px/s entity 500 pixels in one update.
- # Delta time (for consistent movement)
- last_time = time.time()
- while True:
- dt = time.time() - last_time
- last_time = time.time()
-
- # Move at consistent speed regardless of framerate
- player.x += player.speed * dt
- ```
+ **Rule:** Always clamp `max_dt` to prevent tunneling.
- ### Fixed Timestep Game Loop
+ ### Fixed Timestep with Render Interpolation
- The basic `clock.tick(fps)` approach varies the timestep when framerate drops, causing inconsistent physics. Fixed timestep updates physics at regular intervals while allowing interpolated rendering:
+ The robust pattern: fixed physics steps, interpolated rendering for smoothness.
```python
class Game:
def __init__(self):
pygame.init()
self.screen = pygame.display.set_mode((800, 600))
self.clock = pygame.time.Clock()
self.running = True
- # Fixed timestep configuration
- self.fixed_dt = 1/60 # 60 Hz physics
+ # Fixed timestep: physics always runs at 60 Hz
+ self.fixed_dt = 1/60
self.accumulator = 0.0
-
+ self.max_dt = 1/10 # Clamp: ignore frames > 100ms (prevents tunneling)
+
def run(self):
while self.running:
- # Calculate delta time (in seconds)
+ # Calculate delta time (seconds)
dt = self.clock.tick(60) / 1000.0
+ # Clamp to prevent spiral of death on stalled frames
+ if dt > self.max_dt:
+ dt = self.max_dt
+
# Handle events
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
# Fixed timestep update loop
self.accumulator += dt
while self.accumulator >= self.fixed_dt:
- self.fixed_update(self.fixed_dt)
+ self.update(self.fixed_dt)
self.accumulator -= self.fixed_dt
# Render with interpolation (smooths visual updates)
- interpolation = self.accumulator / self.fixed_dt
- self.render(interpolation)
+ interp = self.accumulator / self.fixed_dt
+ self.render(interp)
pygame.quit()
- def fixed_update(self, dt):
- """Physics/update at fixed 60 Hz"""
- # All game logic here - movement, collision, AI
- player.update_physics(dt)
+ def update(self, dt):
+ """Physics and game logic at fixed 60 Hz"""
+ # Movement: position += velocity * dt
+ # Collision detection
+ # AI decisions
+ pass
def render(self, interp):
"""Render with interpolation factor (0.0 to 1.0)"""
self.screen.fill((0, 0, 0))
- # Interpolate positions for smooth rendering
- for sprite in all_sprites:
- render_x = sprite.x + (sprite.vx * interp)
- render_y = sprite.y + (sprite.vy * interp)
+ # Interpolate positions for smooth rendering between physics steps
+ for sprite in self.all_sprites:
+ render_x = sprite.prev_x + (sprite.x - sprite.prev_x) * interp
+ render_y = sprite.prev_y + (sprite.y - sprite.prev_y) * interp
self.screen.blit(sprite.image, (render_x, render_y))
pygame.display.flip()
```
- **Why fixed timestep matters:**
- - Consistent physics regardless of framerate
- - Deterministic network games (same simulation everywhere)
+ **Why this matters:**
+ - Consistent physics regardless of framerate drops
+ - Deterministic simulation (network games, replays)
- No "spiral of death" when frame time exceeds update time
- - Interpolation makes rendering smooth even when physics runs at lower rate
+ - Interpolation makes rendering smooth even when physics runs slower
- ## Deep Dives
+ **Variable delta alternative** (simpler, less precise):
- For detailed implementations, load these reference files on demand:
+ ```python
+ # Simpler: variable timestep (acceptable for non-critical physics)
+ dt = self.clock.tick(60) / 1000.0
+ if dt > 0.1: # Clamp to 100ms max
+ dt = 0.1
+ self.update(dt)
+ ```
- - **Drawing & Surfaces**: `references/drawing-surfaces.md` — Colors, shapes, surface operations, transforms
- - **Performance Optimization**: `references/performance.md` — Image conversion, RLEACCEL, batched blits, dirty rects, pre-rendered surfaces
- - **Sprites, Camera & UI**: `references/sprites-camera-ui.md` — Sprite classes, groups, camera implementation, button UI elements
+ ### Update/Draw/Collision Placement
- ## Best Practices
+ ```python
+ def update(self, dt):
+ # 1. Handle input (keyboard/mouse state)
+ # 2. Update positions: pos += vel * dt
+ # 3. Update animations (frame += dt * fps)
+ # 4. Collision detection (after positions change)
+ # 5. Game logic (score, state transitions)
- ### 1. Use Sprite Groups
+ def render(self, interp):
+ # 1. Clear screen (fill or restore background)
+ # 2. Draw static background (once, cached)
+ # 3. Draw sprites/groups (sorted by z-order if needed)
+ # 4. Draw UI overlay (HUD, score, health)
+ # 5. Flip display
+ ```
+ ## Sprite and Group Cost
+
+ ### Why Groups Are the Bottleneck
+
+ `Group.update()` calls `.update()` on **every sprite** each frame. For 1000 sprites, that's 1000 function calls. The cost is not the group—it's the work each sprite does.
+
+ **Profile before optimizing:**
+
```python
- # Good: Use groups for efficient updates
- all_sprites = pygame.sprite.Group()
- all_sprites.update() # Updates all sprites
- all_sprites.draw(screen) # Draws all sprites
+ import time
+
+ # Measure update cost
+ start = time.perf_counter()
+ all_sprites.update()
+ update_ms = (time.perf_counter() - start) * 1000
+
+ # Measure draw cost
+ start = time.perf_counter()
+ all_sprites.draw(screen)
+ draw_ms = (time.perf_counter() - start) * 1000
+
+ print(f"Update: {update_ms:.2f}ms, Draw: {draw_ms:.2f}ms")
+ # Budget: < 8ms update, < 8ms draw for 60 FPS headroom
```
- ### 2. Delta Time
+ ### Group Types and Their Costs
+ | Group Type | Cost | Use When |
+ |------------|------|----------|
+ | `Group` | O(n) update, O(n) draw | Most cases, sprites with `.update()` |
+ | `GroupSingle` | O(1) access | Single entity (player, boss) |
+ | `LayeredUpdates` | O(n) + layer sorting | Z-order rendering, depth sorting |
+ | `Sprite` (manual) | O(1) | Single sprite, no group overhead |
+
```python
- # Good: Frame-rate independent movement
- player.x += speed * dt
+ # GroupSingle: single entity, faster access
+ player = pygame.sprite.GroupSingle()
+ player.add(PlayerSprite())
+ player.update() # Updates only the single sprite
+ player.draw(screen)
+
+ # LayeredUpdates: control draw order by layer
+ layers = pygame.sprite.LayeredUpdates()
+ layers.add(background, layers=[0]) # Draw first
+ layers.add(player, layers=[1])
+ layers.add(enemies, layers=[2])
+ layers.add(particles, layers=[3]) # Draw last (on top)
+ layers.change_layer(player, 5) # Move player to top
```
- ### 3. Pre-load Resources
+ ### When to Draw Manually
+ Skip groups when:
+ - Sprites don't need `.update()` (static background tiles)
+ - You need custom draw order per frame
+ - You're using dirty rect optimization
+
```python
- # Good: Load images/sounds once at startup
- def load_game():
- global player_image, enemy_image, shoot_sound
- player_image = pygame.image.load("player.png").convert_alpha()
- enemy_image = pygame.image.load("enemy.png").convert_alpha()
- shoot_sound = pygame.mixer.Sound("shoot.wav")
+ # Manual draw: skip group overhead for static tiles
+ for tile in background_tiles:
+ screen.blit(tile.image, tile.rect) # No update() call
+
+ # Custom z-order: sort before draw
+ sprites_to_draw = sorted(all_sprites, key=lambda s: s.z_index)
+ for sprite in sprites_to_draw:
+ screen.blit(sprite.image, sprite.rect)
```
- ### 4. Clean Exit
+ ### GroupCollide vs Manual Collision
+ `GroupCollide` returns a dict keyed by sprite—expensive if you only need "did anything hit?"
+
```python
- # Good: Proper cleanup
- try:
- game_loop()
- finally:
- pygame.quit()
- sys.exit()
+ # Expensive: groupcollide builds full dict
+ hits = pygame.sprite.groupcollide(bullets, enemies, False, False)
+ # hits = {bullet1: [enemy1, enemy2], bullet2: [enemy3], ...}
+
+ # Cheaper: spritecollide, short-circuit on first hit
+ for bullet in bullets:
+ if pygame.sprite.spritecollideany(bullet, enemies):
+ bullet.kill()
+ break # Stop checking
```
- ## Game Loop Template
+ ## Collision Detection Decision Guide
+ ### Collision Strategies by Cost
+
+ | Strategy | Cost | Use When |
+ |----------|------|----------|
+ | AABB (`rect.colliderect`) | O(1), fast | Most games, initial filter |
+ | `spritecollide` (rect) | O(n) per sprite | Small sprite counts (< 100) |
+ | `groupcollide` (dict) | O(n×m) | Need full collision map |
+ | Mask (`collide_mask`) | O(pixels) | Pixel-perfect needed |
+ | Circle (`collide_circle`) | O(1), approximate | Round sprites, medium precision |
+
+ ### The Failure Modes
+
+ **Tunneling at high velocity:** A bullet moving 200 px/frame skips a 32-pixel enemy entirely.
+
+ **Fix:** Use continuous collision (swept AABB) or subdivide high-velocity updates.
+
```python
- import pygame
- import sys
+ # Subdivide to prevent tunneling
+ def update_high_velocity(bullet, dt, subdivisions=4):
+ sub_dt = dt / subdivisions
+ for _ in range(subdivisions):
+ bullet.x += bullet.vx * sub_dt
+ bullet.y += bullet.vy * sub_dt
+ if pygame.sprite.spritecollideany(bullet, enemies):
+ return True # Collision detected
+ return False
+ ```
- class Game:
+ **O(n²) broad-phase:** 500 bullets × 500 enemies = 250,000 checks per frame.
+
+ **Fix:** AABB pre-filter with spatial partitioning.
+
+ ```python
+ # AABB pre-filter: reject distant sprites first
+ def collide_with_filter(bullet, enemies):
+ # Fast bounding box check
+ for enemy in enemies:
+ if not bullet.rect.colliderect(enemy.rect):
+ continue # Skip expensive mask check
+
+ # Only now do the expensive check
+ if bullet.mask and enemy.mask:
+ offset = (enemy.rect.x - bullet.rect.x,
+ enemy.rect.y - bullet.rect.y)
+ if bullet.mask.overlap(enemy.mask, offset):
+ return True
+ return False
+
+ # For large counts: use a quadtree or grid
+ # (See references/performance.md for spatial partitioning)
+ ```
+
+ ### Mask vs Colorkey vs Rect
+
+ ```python
+ # Rect collision (fastest, least precise)
+ if player.rect.colliderect(enemy.rect):
+ handle_collision()
+
+ # Colorkey: simple transparency, no pixel-perfect
+ image = pygame.image.load("sprite.png").convert()
+ image.set_colorkey((0, 0, 0)) # Black is transparent
+
+ # Mask: pixel-perfect (slow, use as second filter)
+ mask = pygame.mask.from_surface(image) # Expensive, do once at load
+ # Store mask on sprite
+ sprite.mask = mask
+
+ # Collision: rect first (AABB), then mask
+ if sprite1.rect.colliderect(sprite2.rect):
+ offset = (sprite2.rect.x - sprite1.rect.x,
+ sprite2.rect.y - sprite1.rect.y)
+ if sprite1.mask.overlap(sprite2.mask, offset):
+ handle_pixel_perfect_collision()
+ ```
+
+ **Rule:** Always use AABB as first filter. Mask collision is 10-100× slower than rect.
+
+ ## Surface and Blit Pitfalls
+
+ ### Per-Frame Allocation
+
+ **Never** allocate Surfaces inside the game loop.
+
+ ```python
+ # Bad: Allocates new surface every frame (triggers GC, causes stutter)
+ while running:
+ text = font.render(score_text, True, WHITE)
+ screen.blit(text, (10, 10))
+
+ # Good: Cache the surface, update only when score changes
+ class HUD:
def __init__(self):
- pygame.init()
- self.screen = pygame.display.set_mode((800, 600))
- self.clock = pygame.time.Clock()
- self.running = True
-
- def handle_events(self):
- for event in pygame.event.get():
- if event.type == pygame.QUIT:
- self.running = False
-
- def update(self):
- pass
+ self.score_font = pygame.font.Font(None, 36)
+ self.score_surface = None
+ self.score = 0
- def draw(self):
- self.screen.fill((0, 0, 0))
- pygame.display.flip()
+ def set_score(self, value):
+ if value != self.score:
+ self.score = value
+ self.score_surface = self.score_font.render(
+ str(value), True, WHITE
+ )
- def run(self):
- while self.running:
- dt = self.clock.tick(60) / 1000.0 # Delta time in seconds
- self.handle_events()
- self.update()
- self.draw()
- pygame.quit()
- sys.exit()
+ def draw(self, screen):
+ if self.score_surface:
+ screen.blit(self.score_surface, (10, 10))
+ ```
- if __name__ == "__main__":
- game = Game()
- game.run()
+ ### convert() and convert_alpha() at Load Time
+
+ ```python
+ # Bad: Converts on every load (or worse, every frame)
+ image = pygame.image.load("sprite.png").convert()
+
+ # Good: Load once at startup, convert immediately
+ class AssetManager:
+ def __init__(self):
+ self.player = pygame.image.load("player.png").convert_alpha()
+ self.tile = pygame.image.load("tile.png").convert()
+ # Opaque → convert(), transparent → convert_alpha()
```
+ **Rule of thumb:**
+ - `convert()`: Opaque images, no transparency (20-30% faster blit)
+ - `convert_alpha()`: Images with alpha channels or colorkeys
+ - Never call `convert()` inside the game loop
+
+ ### set_alpha vs Alpha Channel
+
+ ```python
+ # Bad: set_alpha on many sprites (slow, recomputes every blit)
+ for sprite in hundreds_of_sprites:
+ sprite.image.set_alpha(128)
+
+ # Good: Use alpha channel in the image itself (precomputed)
+ # Load with transparency already baked in
+ sprite = pygame.image.load("fading.png").convert_alpha()
+ ```
+
+ **Rule:** `set_alpha()` on a Surface is slower than having the alpha channel in the image data. Pre-render semi-transparent surfaces once.
+
+ ### blit vs blits Batching
+
+ ```python
+ # Bad: Multiple individual blit calls
+ for sprite in sprites:
+ screen.blit(sprite.image, sprite.rect)
+
+ # Good: Batched blits (single call, faster)
+ screen.blits([(s.image, s.rect) for s in sprites])
+
+ # Even better: pre-compute the list
+ blit_list = [(s.image, s.rect) for s in sprites]
+ screen.blits(blit_list)
+ ```
+
+ **Rule:** `blits()` is 10-30% faster than individual `blit()` calls for large sprite counts.
+
+ ## When Not to Use Pygame
+
+ ### Browser-Targeted Games
+
+ **Problem:** Pygbag (pygame-to-WebAssembly) has limitations:
+ - Large binary size (~10MB+ initial load)
+ - Input latency in browser
+ - No access to native APIs (file system, notifications)
+ - Mobile browser performance issues
+
+ **Alternative:** Native JS/TypeScript game engines (Phaser, Babylon.js) or Unity/Godot export to WebGL.
+
+ ### Hardware-Accelerated 3D or Heavy 2D Sprite Counts
+
+ **Problem:** Pygame uses software rendering (SDL2 software renderer by default). Even with `HWSURFACE`, it's not a full 3D pipeline.
+
+ **Symptoms:**
+ - > 1000 moving sprites at 60 FPS
+ - Need for 3D transformations (rotation in 3D space, perspective)
+ - Shader effects (bloom, depth of field)
+
+ **Alternative:**
+ - 2D: Godot (GDScript, built-in sprite batching)
+ - 3D: Panda3D, Ursina, or OpenGL/DirectX directly
+ - High-performance 2D: Pyglet (OpenGL-backed)
+
+ ### Projects Needing a Retained-Mode Scene Graph
+
+ **Problem:** Pygame is immediate-mode. You redraw everything every frame. No scene graph, no object retention.
+
+ **Symptoms:**
+ - Complex UI with nested widgets
+ - Need for editor-time scene composition
+ - Undo/redo history of scene changes
+
+ **Alternative:** Godot (built-in scene system), Unity, or a retained-mode UI library (Dear PyGui for tools).
+
+ ### Games That Must Ship on Mobile
+
+ **Problem:** Pygame has no official mobile support. Pygbag targets web, not native mobile.
+
+ **Symptoms:**
+ - Need iOS/Android app store distribution
+ - Need native mobile features (push notifications, in-app purchases)
+ - Need touch-optimized input handling
+
+ **Alternative:** Godot, Unity, or Flutter with game plugins.
+
+ ## Deep Dives
+
+ Load these reference files on demand for detailed implementations:
+
+ - **Drawing & Surfaces**: `references/drawing-surfaces.md` — Colors, shapes, surface operations, transforms
+ - **Performance Optimization**: `references/performance.md` — Image conversion, RLEACCEL, batched blits, dirty rects, pre-rendered surfaces
+ - **Sprites, Camera & UI**: `references/sprites-camera-ui.md` — Sprite classes, groups, camera implementation, button UI elements
+
## References
- **Official Documentation**: https://www.pygame.org/docs/
- **Pygame Wiki**: https://www.pygame.org/wiki/
- **KidsCanCode Pygame Tutorials**: https://kidscancode.org/pygame_tutorials/
+