Skip to main content

Build interactive arcade games that run entirely inside Google Slides

Project description

slide-games

PyPI CI License: MIT Python 3.11+

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

  1. Open console.cloud.google.com.
  2. Click the project dropdown at the top-left → New Project.
  3. Give it any name → Create.

2. Enable the Google Slides API

  1. In the left sidebar go to APIs & ServicesLibrary.
  2. Search for Google Slides API → click it → Enable.

3. Configure the OAuth consent screen

  1. Left sidebar → APIs & ServicesOAuth consent screen.
  2. Select ExternalCreate.
  3. Fill in App name (anything, e.g. slide-games), User support email, and Developer contact email.
  4. Click Save and Continue through all remaining steps until you reach the dashboard.

4. Create OAuth credentials

  1. Left sidebar → APIs & ServicesCredentials.
  2. Click + Create CredentialsOAuth client ID.
  3. Application type: Desktop appCreate.
  4. In the confirmation dialog click Download JSON.
  5. Rename the downloaded file to credentials.json and 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.json or token.json to version control. Both are listed in .gitignore by 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

slide_games-0.1.0.tar.gz (57.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

slide_games-0.1.0-py3-none-any.whl (38.8 kB view details)

Uploaded Python 3

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

Hashes for slide_games-0.1.0.tar.gz
Algorithm Hash digest
SHA256 0edb34dc32b4888cf4e8a467f6a0e4b96b50803e6dbb15e4ecdeb0535db8fa93
MD5 06f111234825b1c07ab5757f4d28e0ba
BLAKE2b-256 a3ac525c90a98f7c6565927ebd31d262d845895de68534c758b27926bf66fc22

See more details on using hashes here.

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

Hashes for slide_games-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 178fe3cfa1ce2a29fad11a9b416780420e884ef3c89e67cc9c3bb2ebe8876f39
MD5 00e49bd6fce1c0f0ddd28c46ae99697e
BLAKE2b-256 813f9c7981f996ae9a176b5c32f3b4faa50ae2227491dafebb7edc533f877216

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page