diff --git a/examples/pygame-ce/README.md b/examples/pygame-ce/README.md
new file mode 100644
index 0000000..1a112cc
--- /dev/null
+++ b/examples/pygame-ce/README.md
@@ -0,0 +1,18 @@
+# pygame-ce Examples
+
+Each sub-directory contains a self-contained example. The order in
+which the examples are to appear is specified in `order.json` (an
+array of directory names in the expected order).
+
+In each example directory you'll find:
+
+* `config.toml` - must conform to the specification outlined here:
+ https://docs.pyscript.net/latest/user-guide/configuration/ This is
+ parsed and ultimately turned into a JSON representation as part of
+ the package's API object.
+* `setup.py` - Python code for contextual and environmental setup,
+ NOT SEEN BY THE END USER, but is run before the `code.py` code is
+ evaluated. Allows us to create useful (IPython) shims, avoid
+ repeating boilerplate and whatnot.
+* `code.py` - the actual code added to the editor which forms the
+ practical example of using the package.
diff --git a/examples/pygame-ce/order.json b/examples/pygame-ce/order.json
new file mode 100644
index 0000000..70c49c9
--- /dev/null
+++ b/examples/pygame-ce/order.json
@@ -0,0 +1,4 @@
+[
+ "surfaces_and_shapes",
+ "rects_and_blitting"
+]
diff --git a/examples/pygame-ce/rects_and_blitting/code.py b/examples/pygame-ce/rects_and_blitting/code.py
new file mode 100644
index 0000000..63b9e7c
--- /dev/null
+++ b/examples/pygame-ce/rects_and_blitting/code.py
@@ -0,0 +1,92 @@
+# ---------------------------------------------------------------------
+# Rects, sprites, and blitting one Surface onto another.
+# ---------------------------------------------------------------------
+
+import pygame
+pygame.init()
+
+
+def show_surface(surface, caption=""):
+ buffer = io.BytesIO()
+ pygame.image.save(surface, buffer, "PNG")
+ encoded = base64.b64encode(buffer.getvalue()).decode("ascii")
+ label = f"
{caption}
" if caption else ""
+ display(
+ HTML(
+ f'{label}
'
+ ),
+ append=True,
+ )
+
+
+heading("Rects: the workhorse of pygame")
+note(
+ "A pygame.Rect describes a rectangular area: position and size. "
+ "Almost every game uses Rects for positioning sprites, "
+ "detecting collisions, and clipping draw operations."
+)
+
+# Build a small "sprite" Surface with per-pixel alpha so its
+# transparent areas don't show when we blit it later.
+def make_token(color, label):
+ """Make a 64x64 round token with a letter on it."""
+ token = pygame.Surface((64, 64), pygame.SRCALPHA)
+ pygame.draw.circle(token, color, (32, 32), 30)
+ pygame.draw.circle(token, "white", (32, 32), 30, width=3)
+ font = pygame.font.SysFont(None, 40)
+ text = font.render(label, True, "white")
+ token.blit(text, text.get_rect(center=(32, 32)))
+ return token
+
+red_token = make_token("crimson", "R")
+blue_token = make_token("steelblue", "B")
+
+# A board to blit them onto.
+board = pygame.Surface((480, 320))
+board.fill((20, 80, 40)) # felt-table green
+
+# Draw a grid of guide lines using Rect for the playfield.
+play_area = pygame.Rect(20, 20, 440, 280)
+pygame.draw.rect(board, (10, 50, 25), play_area)
+pygame.draw.rect(board, "white", play_area, width=2)
+
+# Place the tokens. Surface.get_rect() gives us a Rect we can move
+# around with helpers like .center, .move(), and .colliderect().
+red_rect = red_token.get_rect(center=(140, 160))
+blue_rect = blue_token.get_rect(center=(180, 170))
+
+# blit() copies one Surface onto another at the rect's top-left.
+board.blit(red_token, red_rect)
+board.blit(blue_token, blue_rect)
+
+# Rects can detect overlap, useful for collision checks.
+overlapping = red_rect.colliderect(blue_rect)
+note(f"Do the two tokens overlap? {overlapping}")
+
+show_surface(board, caption="Two tokens blitted onto a board")
+
+heading("Animating a Rect across frames")
+note(
+ "A game loop usually updates positions then redraws. Here we "
+ "render a few frames of a token sliding across the board and "
+ "show them as a strip so you can see the motion."
+)
+
+frames = []
+slider = make_token("gold", "G")
+slider_rect = slider.get_rect(center=(60, 160))
+
+for step in range(5):
+ frame = board.copy()
+ # Move the rect 90 pixels to the right each frame.
+ slider_rect = slider_rect.move(90, 0)
+ frame.blit(slider, slider_rect)
+ frames.append(frame)
+
+# Compose all frames into one tall image.
+strip = pygame.Surface((480, 320 * len(frames)))
+for index, frame in enumerate(frames):
+ strip.blit(frame, (0, index * 320))
+
+show_surface(strip, caption="Five frames of a sliding token")
diff --git a/examples/pygame-ce/rects_and_blitting/config.toml b/examples/pygame-ce/rects_and_blitting/config.toml
new file mode 100644
index 0000000..98e0600
--- /dev/null
+++ b/examples/pygame-ce/rects_and_blitting/config.toml
@@ -0,0 +1 @@
+packages = ["pygame-ce"]
diff --git a/examples/pygame-ce/rects_and_blitting/setup.py b/examples/pygame-ce/rects_and_blitting/setup.py
new file mode 100644
index 0000000..a0819f3
--- /dev/null
+++ b/examples/pygame-ce/rects_and_blitting/setup.py
@@ -0,0 +1,22 @@
+"""Setup for the rects-and-blitting example. No IPython shim here."""
+import os
+os.environ.setdefault("SDL_VIDEODRIVER", "dummy")
+
+import io
+import base64
+import js
+from pyscript import window, HTML, display as _display
+
+js.alert = window.alert
+
+
+def display(*args, **kwargs):
+ return _display(*args, **kwargs, target=__pyscript_display_target__)
+
+
+def heading(text, level=2):
+ display(HTML(f"{text}"), append=True)
+
+
+def note(text):
+ display(HTML(f"{text}
"), append=True)
diff --git a/examples/pygame-ce/surfaces_and_shapes/code.py b/examples/pygame-ce/surfaces_and_shapes/code.py
new file mode 100644
index 0000000..7e5ff48
--- /dev/null
+++ b/examples/pygame-ce/surfaces_and_shapes/code.py
@@ -0,0 +1,74 @@
+"""
+A first look at pygame-ce: drawing on a Surface.
+
+Pygame is a game library, and at its heart is the Surface, a 2D
+pixel canvas you draw onto. In a normal pygame program you would
+call pygame.display.set_mode(...) to get a window-backed Surface
+and run a game loop. Here we work with offscreen Surfaces directly,
+which is the same API minus the window, and show the result inline.
+
+See https://pyga.me/docs/ for the full reference.
+"""
+from IPython.core.display import display, HTML
+
+# pygame-ce needs a dummy video driver when running headless inside a
+# web worker. We must set this BEFORE importing pygame.
+import os
+os.environ.setdefault("SDL_VIDEODRIVER", "dummy")
+
+import io
+import base64
+import pygame
+
+
+def show_surface(surface, caption=""):
+ """Render a pygame Surface as an inline PNG in the page."""
+ # pygame.image.save can write to any file-like object. We grab the
+ # PNG bytes, base64-encode them, and embed them in an
tag.
+ buffer = io.BytesIO()
+ pygame.image.save(surface, buffer, "PNG")
+ encoded = base64.b64encode(buffer.getvalue()).decode("ascii")
+ label = f"{caption}
" if caption else ""
+ display(
+ HTML(
+ f'{label}
'
+ ),
+ append=True,
+ )
+
+
+# pygame must be initialized before most subsystems will work.
+pygame.init()
+
+heading("Drawing shapes on a Surface")
+note(
+ "We create a 480x320 Surface, fill the background, and draw some "
+ "shapes with pygame.draw. Colors can be given as RGB tuples or "
+ "named CSS-style strings."
+)
+
+canvas = pygame.Surface((480, 320))
+canvas.fill((30, 30, 60)) # deep navy background
+
+# A row of filled circles, like balloons rising.
+for index, color in enumerate(["tomato", "gold", "mediumseagreen", "skyblue"]):
+ center = (80 + index * 110, 200)
+ pygame.draw.circle(canvas, color, center, 36)
+ pygame.draw.circle(canvas, "white", center, 36, width=3)
+
+# A polygon (a simple house outline).
+house = [(360, 260), (360, 180), (410, 140), (460, 180), (460, 260)]
+pygame.draw.polygon(canvas, "khaki", house)
+pygame.draw.polygon(canvas, "saddlebrown", house, width=4)
+
+# Anti-aliased line across the sky.
+pygame.draw.aaline(canvas, "white", (10, 40), (470, 80))
+
+show_surface(canvas, caption="Hand-drawn scene on a 480x320 Surface")
+
+note(
+ "The Surface is just an in-memory image. The same drawing calls "
+ "would render to the screen Surface returned by "
+ "pygame.display.set_mode in a regular game."
+)
diff --git a/examples/pygame-ce/surfaces_and_shapes/config.toml b/examples/pygame-ce/surfaces_and_shapes/config.toml
new file mode 100644
index 0000000..98e0600
--- /dev/null
+++ b/examples/pygame-ce/surfaces_and_shapes/config.toml
@@ -0,0 +1 @@
+packages = ["pygame-ce"]
diff --git a/examples/pygame-ce/surfaces_and_shapes/setup.py b/examples/pygame-ce/surfaces_and_shapes/setup.py
new file mode 100644
index 0000000..bb018e1
--- /dev/null
+++ b/examples/pygame-ce/surfaces_and_shapes/setup.py
@@ -0,0 +1,42 @@
+"""
+Shim IPython's display API onto PyScript so example code written in a
+Jupyter/IPython idiom runs unmodified in the browser.
+"""
+
+import sys
+import types
+import js
+from pyscript import window, HTML, display as _display
+
+js.alert = window.alert
+
+
+def display(*args, **kwargs):
+ """Wrap pyscript.display so output lands in the example target."""
+ return _display(
+ *args, **kwargs, target=__pyscript_display_target__,
+ )
+
+
+ipython = types.ModuleType("IPython")
+core = types.ModuleType("IPython.core")
+core_display = types.ModuleType("IPython.core.display")
+core_display.display = display
+core_display.HTML = HTML
+ipython.core = core
+core.display = core_display
+ipython.version_info = (9, 0, 2, '')
+ipython.get_ipython = lambda: None
+ipython.display = core_display
+sys.modules["IPython"] = ipython
+sys.modules["IPython.core"] = core
+sys.modules["IPython.core.display"] = core_display
+sys.modules["IPython.display"] = core_display
+
+
+def heading(text, level=2):
+ display(HTML(f"{text}"), append=True)
+
+
+def note(text):
+ display(HTML(f"{text}
"), append=True)