Build interactive arcade games that run entirely inside Google Slides
Project description
slide-games
A Python framework for building interactive arcade games that run entirely inside Google Slides.
Each reachable game state becomes one slide. Directional buttons are hyperlinks that jump between slides. Players open the presentation in Presentation mode and navigate with the on-screen arrows.
How it works
define game logic → BFS discovers all states → one slide per state
↓ ↓
BaseGame subclass builder.py Google Slides API
nav buttons wired with hyperlinks → play in Presentation mode (Ctrl+Shift+F5)
State spaces grow quickly when collectibles are involved (Pac-Man with n pellets has positions × 2ⁿ states). The package enforces a max_states cap (default 1 000) to catch runaway games before they hit the API.
Installation
Install via pip install slide-games.
Prerequisites — Google Cloud credentials
Follow these steps once per Google account.
1. Create a project
- Open console.cloud.google.com.
- Click the project dropdown at the top-left → New Project.
- Give it any name → Create.
2. Enable the Google Slides API
- In the left sidebar go to APIs & Services → Library.
- Search for Google Slides API → click it → Enable.
3. Configure the OAuth consent screen
- Left sidebar → APIs & Services → OAuth consent screen.
- Select External → Create.
- Fill in App name (anything, e.g.
slide-games), User support email, and Developer contact email. - Click Save and Continue through all remaining steps until you reach the dashboard.
4. Create OAuth credentials
- Left sidebar → APIs & Services → Credentials.
- Click + Create Credentials → OAuth client ID.
- Application type: Desktop app → Create.
- In the confirmation dialog click Download JSON.
- Rename the downloaded file to
credentials.jsonand place it in the directory where you run your script.
5. First run
The first time you call build_presentation() a browser tab opens asking you to sign in and grant access. After you approve, a token.json is saved alongside credentials.json so you won't be prompted again.
Never commit
credentials.jsonortoken.jsonto version control. Both are listed in.gitignoreby default.
Quick start
Single-level maze
from slide_games import build_presentation, MazeGame, generate_maze, DARK
maze = generate_maze(20, 16, algorithm="backtracker", seed=7)
url = build_presentation(MazeGame(maze), title="Maze", theme=DARK, max_states=700)
print(url)
Multi-level campaign
build_campaign links the win slide of each level to the starting position of the next, creating a seamless campaign.
from slide_games import build_campaign, MazeGame, generate_maze, DARK
levels = [
MazeGame(generate_maze(20, 16, algorithm="backtracker", seed=7)), # Easy
MazeGame(generate_maze(25, 20, algorithm="prim", seed=42)), # Medium
MazeGame(generate_maze(30, 24, algorithm="kruskal", seed=99)), # Hard
]
url = build_campaign(levels, title="Maze Quest", theme=DARK, max_states=1500)
print(url)
Building a custom game
Subclass BaseGame and implement three methods. The package handles state discovery, slide creation, rendering, and linking.
from slide_games import BaseGame, build_presentation
class MyGame(BaseGame):
def get_initial_state(self):
"""Return the starting state (any hashable object)."""
...
def get_transitions(self, state):
"""Return {direction: next_state_or_None} for all four directions.
Use None for blocked directions — the button is rendered but grayed out.
"""
return {
"up": ...,
"down": ...,
"left": ...,
"right": ...,
}
def is_terminal(self, state) -> bool:
"""Return True when the game is won (or lost)."""
...
url = build_presentation(MyGame(), title="My Game")
Custom rendering
Override render() to draw fully custom visuals using a pygame-like API. The D-pad buttons and win banner are still added automatically — keep content above y = NAV_RESERVED_Y (~820 px) to avoid overlap.
from slide_games import BaseGame, build_presentation, SCREEN_W, NAV_RESERVED_Y
from slide_games.gfx import Color, Rect, draw
class MyGame(BaseGame):
...
def render(self, surface, state) -> None:
# 1920 × 1080 virtual coordinate space
surface.fill(Color(10, 10, 40))
draw.rect(surface, Color(0, 200, 100),
Rect(state.x * 80, state.y * 80, 70, 70),
border_radius=10)
draw.text(surface, f"Score: {state.score}",
Rect(20, 20, 400, 50),
color=Color(255, 255, 255), font_size=28, bold=True)
Available drawing functions:
| Function | Description |
|---|---|
surface.fill(color) |
Fill background |
draw.rect(surface, color, Rect(x,y,w,h), border_radius=0, width=0) |
Rectangle (filled or outline) |
draw.circle(surface, color, (cx,cy), radius, width=0) |
Circle (filled or outline) |
draw.line(surface, color, (x1,y1), (x2,y2), width=1) |
Line segment |
draw.lines(surface, color, points, width=1, closed=False) |
Multi-segment polyline |
draw.triangle(surface, color, rect, direction="up", width=0) |
Triangle — direction: "up" "down" "left" "right" |
draw.shape(surface, color, shape_type, rect, width=0) |
Any Google Slides built-in shape ("DIAMOND", "STAR_5", "HEXAGON", …) |
draw.progress_bar(surface, rect, value, max_value, fg_color, bg_color, border_radius=0) |
Filled progress bar |
draw.text(surface, text, rect, color, font_size=24, bold=False, italic=False, align="LEFT", vertical_align="MIDDLE") |
Text label |
Color supports arithmetic and conversion helpers: lerp(other, t), darken(f), lighten(f), with_alpha(a), grayscale(), complementary().
Rect helpers: scale_by(fx, fy), padded(px, py), clip(other), union(other), fit(other), clamp(other), contains(other), plus midpoint properties (midleft, midright, midtop, midbottom).
Vector2 provides a full 2D vector type: arithmetic operators, normalize(), dot(), cross(), distance_to(), lerp(), rotate(degrees), reflect(), from_polar().
See examples/snake_demo.py, examples/pacman_demo.py, and examples/sokoban_demo.py for complete rendering examples.
Lightweight rendering hooks
For games that use the default grid renderer, override these instead of render():
def get_cell_color(self, state, ch: str, is_player: bool) -> dict | None:
"""Return an RGB dict to override a cell's fill, or None for the theme default."""
if is_player:
return {"red": 1.0, "green": 0.0, "blue": 0.5}
return None
def get_cell_image_url(self, state, ch: str, is_player: bool) -> str | None:
"""Return a public image URL to overlay on a cell, or None."""
if is_player:
return "https://example.com/hero.png"
return None
def get_extra_shapes(self, state) -> list[dict]:
"""Return extra shapes to draw on top of the grid."""
return [
{"type": "ellipse", "x": 2.1, "y": 1.1, "w": 0.8, "h": 0.8,
"color": {"red": 1.0, "green": 0.0, "blue": 0.0}},
]
def show_win_banner(self, state) -> bool:
"""Return False to suppress the win banner (e.g. for loss states)."""
return self.pellets <= state.eaten and state.player != state.ghost
API reference
build_presentation
build_presentation(
game, # BaseGame instance
title="Slide Game", # presentation title
theme=DARK, # Theme object
credentials_file="credentials.json",
verbose=True, # print progress to stdout
max_states=1_000, # hard cap — raises ValueError if exceeded
progress_callback=None, # fn(done, total, phase) — phase is "building" or "sending"
retry_callback=None, # fn(remaining_seconds) — called during rate-limit waits
) -> str # URL of the created presentation
build_campaign
build_campaign(
games, # list[BaseGame] — one per level, in order
title="Slide Game",
theme=DARK,
credentials_file="credentials.json",
verbose=True,
max_states=1_000, # per-level cap
progress_callback=None,
retry_callback=None,
) -> str # URL of the created presentation
The win slide of each level includes a Next Level ► button that jumps to the next level's starting position. The final level's win slide shows only "YOU WIN!" with no next-level button.
Progress callbacks
def my_progress(done: int, total: int, phase: str) -> None:
# phase == "building" → done/total are state counts
# phase == "sending" → done/total are API request counts
print(f"{phase}: {done}/{total}")
def my_retry(remaining_seconds: int) -> None:
print(f"Rate limited — retrying in {remaining_seconds}s")
url = build_campaign(levels, progress_callback=my_progress, retry_callback=my_retry)
Named colours
The package exports 40+ named Color constants for convenience:
from slide_games import (
BLACK, WHITE, RED, GREEN, BLUE, YELLOW, CYAN, MAGENTA,
ORANGE, PURPLE, GRAY, LIGHT_GRAY, DARK_GRAY,
GOLD, SILVER, BROWN, PINK, HOT_PINK, NAVY, TEAL,
CORAL, SALMON, TOMATO, VIOLET, INDIGO, CRIMSON,
SKY_BLUE, SLATE_GRAY, MINT, TURQUOISE, # … and more
)
Themes
| Name | Style |
|---|---|
DARK |
Dark navy/blue (default) |
PACMAN |
Classic black + blue walls |
RETRO |
Dark with orange walls |
Create a custom theme:
from slide_games import Theme
from slide_games.themes import rgb
MY_THEME = Theme(
name="my_theme",
background=rgb(10, 10, 30),
wall=rgb(80, 40, 120),
floor=rgb(20, 20, 50),
player=rgb(255, 100, 0),
goal=rgb(0, 220, 120),
pellet=rgb(200, 200, 200),
btn_active=rgb(80, 40, 120),
btn_inactive=rgb(40, 40, 60),
btn_text=rgb(255, 255, 255),
title_text=rgb(255, 200, 50),
win_text=rgb(255, 200, 50),
)
State space guide
| Game type | States | Notes |
|---|---|---|
| Maze, 16×16 | ~511 | passable cells only |
| Maze, 20×16 | ~639 | |
| Maze, 25×20 | ~999 | |
| Snake, 3×3 grid | ~517 | all self-avoiding walk prefixes |
| Snake, 4×4 grid | ~5 000 | grows quickly with grid size |
| Pac-Man, 9×7, 2 pellets | ~491 | player × ghost × pellet states |
| Pac-Man, 15×11, 2 pellets | ~2 700 | positions × 2² pellet states |
| Sokoban, 7×7, 1 box | ~500 | player × box positions |
| Sokoban, 7×7, 2 boxes | ~5 000 | player × box² configurations |
Pass a higher max_states when you know the count is safe. BFS state discovery runs locally and is fast; the slow part is uploading content to the Google Slides API.
Performance
The sending phase uploads slide content via batchUpdate calls to the Google Slides API (quota: 60 write calls/minute per user). The builder uses a global token-bucket rate limiter (≤50 calls/minute, shared across all concurrent build_campaign calls) and sends up to 5 batches concurrently (500 requests each) per demo. Requests retry automatically on rate-limit (429) and transient connection errors with exponential backoff.
For typical presentations (~500 states):
- Building phase (pure Python, BFS + rendering): a few seconds
- Sending phase (network-bound): roughly 1–3 minutes depending on API latency
Running the demos
python examples/run_all_demos.py
Builds all four demos in parallel (~500 states each), with a live progress table that updates in real time. Each demo's URL is printed the moment it finishes.
| Demo | Grid / Level | ~States |
|---|---|---|
| Maze Quest | 16×16 maze | 511 |
| Snake | 3×3 grid | 517 |
| Pac-Man | 9×7 figure-8 maze, 2 pellets | 491 |
| Sokoban | 7×7, 1 box | 600 |
Individual demos:
python examples/maze_demo.py
python examples/snake_demo.py
python examples/pacman_demo.py
python examples/sokoban_demo.py
Running tests
pytest # run all tests
pytest --cov=slide_games # with coverage report
No Google API credentials are required — all API calls are mocked.
Contributing
Contributions are welcome. See CONTRIBUTING.md for dev setup, code style, and PR guidelines. To report a security vulnerability, see SECURITY.md.
Optional pre-commit hooks (ruff + mypy on every commit) — setup instructions in CONTRIBUTING.md.
Changelog
See CHANGELOG.md for version history.
Publishing to PyPI
python -m build
twine upload dist/*
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file slide_games-0.1.0.tar.gz.
File metadata
- Download URL: slide_games-0.1.0.tar.gz
- Upload date:
- Size: 57.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0edb34dc32b4888cf4e8a467f6a0e4b96b50803e6dbb15e4ecdeb0535db8fa93
|
|
| MD5 |
06f111234825b1c07ab5757f4d28e0ba
|
|
| BLAKE2b-256 |
a3ac525c90a98f7c6565927ebd31d262d845895de68534c758b27926bf66fc22
|
File details
Details for the file slide_games-0.1.0-py3-none-any.whl.
File metadata
- Download URL: slide_games-0.1.0-py3-none-any.whl
- Upload date:
- Size: 38.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
178fe3cfa1ce2a29fad11a9b416780420e884ef3c89e67cc9c3bb2ebe8876f39
|
|
| MD5 |
00e49bd6fce1c0f0ddd28c46ae99697e
|
|
| BLAKE2b-256 |
813f9c7981f996ae9a176b5c32f3b4faa50ae2227491dafebb7edc533f877216
|