Skip to main content

Distribution Platforms

20 minute read

Getting Your Game to Players

Navigate the world of game distribution! Learn about Steam, itch.io, Epic Games Store, mobile app stores, and choose the right platform for your game! 🎮🚀🌍

Platform Overview

🎯 Major Distribution Platforms

Platform Revenue Split Entry Cost Audience
Steam 70/30 $100 132M+ active
itch.io Pay what you want Free Indie focused
Epic Games Store 88/12 Curated 68M+ active
GOG 70/30 Curated DRM-free focus
Google Play 70/30 (85/15 after $1M) $25 one-time 2.5B+ Android
Apple App Store 70/30 (85/15 for small) $99/year 1.5B+ iOS
graph TD A["Your Game"] --> B{Platform Choice} B --> C["PC/Desktop"] B --> D["Mobile"] B --> E["Web"] B --> F["Console"] C --> G["Steam"] C --> H["itch.io"] C --> I["Epic Games"] C --> J["GOG"] D --> K["Google Play"] D --> L["App Store"] D --> M["Amazon"] E --> N["Web Hosting"] E --> O["Game Portals"] E --> P["Facebook"] F --> Q["PlayStation"] F --> R["Xbox"] F --> S["Nintendo"]

Steam - The Giant

🎮 Publishing on Steam

Steamworks Integration


# steamworks.py - Steam API integration example
import sys
import os

# Steamworks API wrapper (using steamworks-py or similar)
try:
    import steamworks
except ImportError:
    print("Steamworks API not available")
    steamworks = None

class SteamIntegration:
    def __init__(self, app_id):
        self.app_id = app_id
        self.initialized = False
        self.client = None
        
    def initialize(self):
        """Initialize Steam API"""
        if not steamworks:
            print("Running without Steam")
            return False
        
        try:
            # Set Steam App ID
            os.environ['SteamAppId'] = str(self.app_id)
            
            # Initialize client
            self.client = steamworks.STEAMWORKS()
            self.client.initialize()
            self.initialized = True
            
            print(f"Steam initialized for App ID: {self.app_id}")
            return True
        except Exception as e:
            print(f"Failed to initialize Steam: {e}")
            return False
    
    def get_user_info(self):
        """Get current user information"""
        if not self.initialized:
            return None
        
        user = self.client.Users.GetLocalPlayer()
        return {
            'id': user.steam_id,
            'name': user.name,
            'level': user.level
        }
    
    def unlock_achievement(self, achievement_name):
        """Unlock a Steam achievement"""
        if not self.initialized:
            return False
        
        try:
            self.client.UserStats.SetAchievement(achievement_name)
            self.client.UserStats.StoreStats()
            print(f"Achievement unlocked: {achievement_name}")
            return True
        except Exception as e:
            print(f"Failed to unlock achievement: {e}")
            return False
    
    def set_rich_presence(self, key, value):
        """Set Steam Rich Presence"""
        if not self.initialized:
            return False
        
        try:
            self.client.Friends.SetRichPresence(key, value)
            return True
        except:
            return False
    
    def get_leaderboard(self, leaderboard_name, num_entries=10):
        """Get leaderboard entries"""
        if not self.initialized:
            return []
        
        try:
            leaderboard = self.client.UserStats.FindLeaderboard(leaderboard_name)
            entries = leaderboard.GetEntries(
                steamworks.LeaderboardDataRequest.Global,
                1, num_entries
            )
            
            return [{
                'rank': entry.rank,
                'score': entry.score,
                'user': entry.user_name
            } for entry in entries]
        except:
            return []
    
    def submit_score(self, leaderboard_name, score):
        """Submit score to leaderboard"""
        if not self.initialized:
            return False
        
        try:
            leaderboard = self.client.UserStats.FindLeaderboard(leaderboard_name)
            leaderboard.UploadScore(
                steamworks.LeaderboardUploadScoreMethod.KeepBest,
                score
            )
            return True
        except:
            return False
    
    def shutdown(self):
        """Shutdown Steam API"""
        if self.initialized and self.client:
            self.client.shutdown()
            self.initialized = False

# Steam App ID configuration
STEAM_APP_ID = 480  # Use 480 for testing (Spacewar)

# Usage in game
steam = SteamIntegration(STEAM_APP_ID)

if steam.initialize():
    # Get user info
    user = steam.get_user_info()
    if user:
        print(f"Welcome, {user['name']}!")
    
    # Set rich presence
    steam.set_rich_presence("status", "In Main Menu")
    
    # Unlock achievement
    steam.unlock_achievement("FIRST_LAUNCH")
    
    # Submit score
    steam.submit_score("HIGH_SCORES", 1000)

# Steamworks configuration file - steam_appid.txt
# Place in same directory as executable
# Contains only the App ID number: 480
        

Steam Store Page Requirements

itch.io - Indie Paradise

🎨 Publishing on itch.io

Butler - Command Line Tool


# Install Butler
# Download from: https://itch.io/docs/butler/

# Login to itch.io
butler login

# Push your game build
butler push mygame-windows.zip username/game-name:windows

# Push with version number
butler push mygame-windows.zip username/game-name:windows --userversion 1.0.0

# Different platforms
butler push mygame-linux.tar.gz username/game-name:linux
butler push mygame-mac.zip username/game-name:mac

# Web build (HTML5)
butler push webgame/ username/game-name:html5

# Check status
butler status username/game-name
        

itch.io API Integration


import requests
import json

class ItchIOAPI:
    def __init__(self, api_key):
        self.api_key = api_key
        self.base_url = "https://itch.io/api/1"
        self.headers = {
            "Authorization": f"Bearer {api_key}"
        }
    
    def get_my_games(self):
        """Get list of your games"""
        response = requests.get(
            f"{self.base_url}/{self.api_key}/my-games",
            headers=self.headers
        )
        return response.json()
    
    def get_game_stats(self, game_id):
        """Get game statistics"""
        response = requests.get(
            f"{self.base_url}/game/{game_id}/stats",
            headers=self.headers
        )
        return response.json()
    
    def post_devlog(self, game_id, title, body, status="published"):
        """Post a devlog update"""
        data = {
            "title": title,
            "body": body,
            "status": status
        }
        response = requests.post(
            f"{self.base_url}/game/{game_id}/devlog",
            headers=self.headers,
            json=data
        )
        return response.json()

# Web embed for itch.io
HTML_TEMPLATE = '''

'''
        

Mobile Distribution

📱 Google Play & App Store

Preparing for Mobile


# mobile_build.py - Mobile-specific configurations
import json
import os

class MobileBuildConfig:
    def __init__(self):
        self.config = {
            "android": {
                "package_name": "com.yourcompany.gamename",
                "version_code": 1,
                "version_name": "1.0.0",
                "min_sdk": 21,
                "target_sdk": 33,
                "permissions": [
                    "INTERNET",
                    "ACCESS_NETWORK_STATE"
                ],
                "features": [],
                "signing": {
                    "keystore": "release.keystore",
                    "key_alias": "release_key",
                    "store_password": os.environ.get("KEYSTORE_PASSWORD"),
                    "key_password": os.environ.get("KEY_PASSWORD")
                }
            },
            "ios": {
                "bundle_id": "com.yourcompany.gamename",
                "version": "1.0.0",
                "build": "1",
                "min_ios": "12.0",
                "device_capabilities": ["armv7"],
                "orientations": ["portrait", "landscape"],
                "info_plist": {
                    "CFBundleDisplayName": "Game Name",
                    "ITSAppUsesNonExemptEncryption": False,
                    "NSUserTrackingUsageDescription": "This app needs tracking for analytics"
                }
            }
        }
    
    def generate_android_manifest(self):
        """Generate AndroidManifest.xml content"""
        android = self.config["android"]
        
        manifest = f'''

    
    
    '''
        
        for permission in android['permissions']:
            manifest += f'\n    '
        
        manifest += '''
    
    
        
        
            
                
                
            
        
    
'''
        
        return manifest
    
    def generate_info_plist(self):
        """Generate Info.plist for iOS"""
        import plistlib
        
        ios = self.config["ios"]
        plist = {
            "CFBundleIdentifier": ios["bundle_id"],
            "CFBundleVersion": ios["build"],
            "CFBundleShortVersionString": ios["version"],
            "MinimumOSVersion": ios["min_ios"],
            "UIRequiredDeviceCapabilities": ios["device_capabilities"],
            "UISupportedInterfaceOrientations": ios["orientations"],
            **ios["info_plist"]
        }
        
        return plistlib.dumps(plist)

# App Store Connect API
class AppStoreConnect:
    def __init__(self, key_id, issuer_id, private_key_path):
        self.key_id = key_id
        self.issuer_id = issuer_id
        self.private_key = self.load_private_key(private_key_path)
        
    def create_jwt_token(self):
        """Create JWT for App Store Connect API"""
        import jwt
        import time
        
        header = {
            "alg": "ES256",
            "kid": self.key_id,
            "typ": "JWT"
        }
        
        payload = {
            "iss": self.issuer_id,
            "exp": int(time.time()) + 1200,  # 20 minutes
            "aud": "appstoreconnect-v1"
        }
        
        token = jwt.encode(payload, self.private_key, 
                          algorithm="ES256", headers=header)
        return token
    
    def upload_build(self, ipa_path):
        """Upload IPA to App Store Connect"""
        # Use altool or Transporter
        import subprocess
        
        cmd = [
            "xcrun", "altool",
            "--upload-app",
            "--type", "ios",
            "--file", ipa_path,
            "--apiKey", self.key_id,
            "--apiIssuer", self.issuer_id
        ]
        
        result = subprocess.run(cmd, capture_output=True, text=True)
        return result.returncode == 0
        

Web Distribution

🌐 Web-Based Games

Hosting Options

Web Monetization


// Web Monetization API
if (document.monetization) {
    document.monetization.addEventListener('monetizationstart', () => {
        console.log('Payment started');
        // Unlock premium features
        unlockPremiumContent();
    });
}

// Ad Integration (example with AdSense)
function showAd() {
    const adContainer = document.getElementById('ad-container');
    adContainer.innerHTML = `
        
        
        
        
    `;
}

// In-App Purchases with Stripe
async function purchaseItem(itemId, price) {
    const response = await fetch('/api/create-payment-intent', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ itemId, price })
    });
    
    const { clientSecret } = await response.json();
    
    // Use Stripe.js to complete payment
    const result = await stripe.confirmCardPayment(clientSecret);
    
    if (result.error) {
        console.error(result.error);
    } else {
        // Payment successful
        unlockItem(itemId);
    }
}
        

Platform Comparison

📊 Choosing the Right Platform

Criteria Steam itch.io Epic Mobile
Best For Commercial PC games Indie/experimental AAA/exclusive deals Casual/F2P
Discovery Algorithm-driven Community-driven Curated ASO/Featured
Competition Very high Moderate Lower Extreme
Features Workshop, Cloud, etc. Flexible pricing Free games program IAP, Ads
Marketing Wishlist system Direct to fans Epic support ASO critical

Multi-Platform Strategy

🎯 Release Strategy Timeline

gantt title Game Release Timeline dateFormat YYYY-MM-DD section Pre-Launch Beta Testing :2024-01-01, 30d Press Kit :2024-01-15, 14d Store Pages :2024-01-20, 7d section Launch itch.io Early Access :2024-02-01, 1d Steam Release :2024-03-01, 1d Epic Games :2024-03-01, 1d section Post-Launch Mobile Port :2024-04-01, 30d Console Port :2024-06-01, 60d DLC/Updates :2024-04-01, 180d

Platform Release Order

  1. itch.io Alpha/Beta: Test with early adopters
  2. Steam Early Access: Gather feedback, build wishlist
  3. Full Steam/Epic Release: Main launch
  4. Mobile Ports: Expand audience
  5. Console Ports: If successful on PC

Platform-Specific Features

🔧 Integration Checklist

Steam Features

Mobile Features

Best Practices

✨ Distribution Best Practices

Key Takeaways

🏋️‍♂️ Practice Exercise

🏋️‍♂️ Exercise 1: Four Adapters, One Game — Polymorphic Dispatch + Runtime Capability Probe + Build-Target Dict in One Pygame Window

Objective: Build a runnable pygame program (~95 lines) that distills the lesson's SteamIntegration / ItchIOAPI / AppStoreConnect per-platform wrappers into one cohesive demonstration of three orthogonal publishing-domain disciplines visible per frame on a 1088×540 window split into three vertical panels (left = Build Targets data dict, middle = Polymorphic Dispatch Log, right = Runtime Capability Probe). Three orthogonal disciplines visible per frame: (a) Platform-abstraction-layer as polymorphic-dispatch via shared protocol at PLATFORM-API scope — a `PlatformAdapter` ABC declares `unlock_achievement(name)` / `submit_score(score)` / `set_presence(key, val)` / `post_devlog(title)` as the uniform interface; SteamAdapter / ItchAdapter / MobileAdapter / WebAdapter subclasses implement each method backed by their respective backend libraries (Steamworks SetAchievement / itch REST devlog / GameCenter.report / localStorage.setItem); the game calls `ADAPTERS[current].unlock_achievement('FIRST_LAUNCH')` once and Python's MRO routes the call to the right backend with no isinstance branches anywhere in the game's update loop. Same UIManager-polymorphic-dispatch pattern from chat-67 graphics_ui_hud (UIElement protocol with HealthBar / Minimap / DialogBox implementations dispatching update/render polymorphically; no isinstance branches in UIManager) applied at PLATFORM-API scope where each adapter provides the same interface backed by different platform-native implementations; adding a new platform = one new adapter class + zero edits to caller code (Open/Closed Principle satisfied). (b) Platform-capability-detection at runtime via try-import / sys.platform / hasattr at PLATFORM-CAPABILITY scope — a `detect_capabilities()` function loops over the TARGETS dict and probes each platform's library availability via the canonical `try: import lib except ImportError: caps[key] = (False, ...)` pattern (mirroring the lesson's `try: import steamworks except ImportError: steamworks = None` plus `if not steamworks: return False` in SteamIntegration.initialize); the right panel displays each platform's probe result as OK or MISSING with the exception message visible. F-key toggles the simulated availability of the current platform and re-runs the probe, demonstrating that capability is a runtime fact discovered by attempting the import, not a build-time fact baked into the binary; the left panel's per-platform OK/MISSING badges update live as availability changes. Same design-time-validation-vs-runtime-discovery pattern as chat-47 platformer_level_design's validate() vs is_solid() (validate at design-time catches structural errors before shipping; is_solid at runtime catches gameplay edge cases) and chat-70 publishing_executables's hidden_imports static-AST-vs-runtime-discovery (static AST parsing misses dynamic imports; the hiddenimports list is the explicit declaration that bridges what static analysis missed) and chat-72 publishing_performance's profile-bars vs intuition (profiler output IS the runtime discovery instrument that replaces design-time intuition about hot paths) applied at PLATFORM-CAPABILITY scope where the design-time hypothesis is 'this feature is available on this platform' and the runtime discovery is the actual import / API call succeeding or raising = FOURTH lesson reinforcing the design-time-validation-vs-runtime-discovery pattern across Phase 8, FIRST applied at PLATFORM-CAPABILITY scope. (c) Build-target-as-data-externalization at BUILD-TARGET scope — a module-level TARGETS dict maps platform-key → {name, split, entry, color} carrying every per-platform fact the rendering and dispatch loops need; adding a new platform like 'amazon_fire' or 'epic' or 'gog' is a single TARGETS dict-entry edit with zero changes to the rendering loop, the capability-detection loop, or the per-platform key bindings (which iterate over the dict). The same shape mirrors the lesson's `MobileBuildConfig.config = {"android": {package_name, version_code, min_sdk, target_sdk, permissions, signing}, "ios": {bundle_id, version, build, min_ios, device_capabilities, orientations, info_plist}}` dict-of-platform-configs and the Platform Overview comparison table (Steam 70/30 $100 + itch.io PWYW Free + Epic 88/12 Curated + GOG 70/30 Curated + Google Play 70/30→85/15 $25 + Apple 70/30→85/15 $99/year). Same data-driven externalization pattern as architecture_save_load schema (chat-58 at SAVE-FORMAT scope), pathfinding terrain-cost dict (chat-60 at PATHFINDING-COST scope), behavior-trees child-list-order (chat-61 at PRIORITY scope), decision-making personality dict (chat-62 at AGENT-BEHAVIOR scope), procedural generation seed (chat-68 at WORLD-IDENTITY scope), publishing_executables .spec file (chat-70 at BUILD-CONFIG scope), and publishing_marketing pitch templates + hashtag day-of-week dict + email tier templates (chat-71 at MARKETING-COPY scope), applied at BUILD-TARGET scope where the platform list IS data the build pipeline iterates over = FIFTEENTH lesson reinforcing data-driven externalization across Phase 8, FIRST applied at BUILD-TARGET scope where adding a new platform target is a single dict-entry edit rather than scattered control-flow changes. Cross-references chat-70 publishing_executables (.spec file IS data the build pipeline reads at BUILD-CONFIG scope; chat-73's TARGETS dict IS data the build pipeline iterates over at BUILD-TARGET scope; chat-70 declares HOW to build, chat-73 declares WHERE to build to as adjacent publishing-domain stages).

Instructions:

  1. Open a 1088×540 pygame window split into three vertical panels: left (0–400) for the Build Targets data dict, middle (400–800) for the Polymorphic Dispatch Log, right (800–1088) for the Runtime Capability Probe.
  2. Define a module-level TARGETS dict mapping each platform key ('steam', 'itch', 'mobile', 'web') to a config dict {'name', 'split', 'entry', 'color'} — this is the single source of truth for every per-platform fact in the demo (rendering uses it, capability probe iterates it, key bindings select from it).
  3. Define a `PlatformAdapter` ABC with abstract methods `unlock_achievement(name)`, `submit_score(score)`, `set_presence(key, val)`, `post_devlog(title)`; implement four concrete subclasses (SteamAdapter / ItchAdapter / MobileAdapter / WebAdapter) where each method returns a string showing what the platform-native API call would look like (e.g., SteamAdapter.unlock_achievement returns f"Steamworks.SetAchievement('{n}')"; ItchAdapter.post_devlog returns the REST endpoint; MobileAdapter.submit_score returns the GameCenter / PlayGames call).
  4. Build a `detect_capabilities()` function that loops over TARGETS and for each platform attempts a simulated `try: import lib except ImportError` probe (use a SIM_AVAILABLE dict to stub the result so the demo runs without real platform libraries installed); return a dict mapping each key → (is_available, message) tuple.
  5. Wire keys 1/2/3/4 to switch the active platform among 'steam', 'itch', 'mobile', 'web'; wire keys A/S/P/D to call `unlock_achievement` / `submit_score` / `set_presence` / `post_devlog` on the active adapter via polymorphic dispatch (no isinstance branches — just `ADAPTERS[current].unlock_achievement(...)`); push every result onto a rolling 8-line log; wire F to toggle the current platform's simulated availability and re-run the probe; wire R to reset everything.
  6. In the left panel, render each TARGETS entry as a row showing name + revenue split + entry cost + an OK / MISSING badge driven by the capability probe; highlight the currently-selected row with a brighter background.
  7. In the middle panel, render the active adapter name + key-binding hints + the rolling polymorphic-dispatch log; in the right panel, render each platform's runtime probe result with green for OK and red for the ImportError message; HUD F-key and R-key hints in the bottom corner.
💡 Hint

The central insight is that all three axes are independent levers solving different parts of the multi-platform problem and none is reducible to the others. Polymorphic dispatch (axis a) gives you a uniform call site at the cost of one ABC and one adapter class per platform — without it, every game-logic site that wants to unlock an achievement has to know which platform is active, which scatters platform-check branches across the entire codebase. Runtime capability detection (axis b) handles the fact that you cannot statically know whether a given user's machine has the steamworks library, the GameCenter framework, or the localStorage browser API — the `try: import / except ImportError` IS the discovery instrument, and folding the conditional into the import machinery lets every method that uses the library guard with a single `if not self.initialized: return False` check rather than wrapping every call in its own try/except. Build-target externalization (axis c) means the platform list is data, not control flow — when you add a new storefront like Epic Games Store, you add one TARGETS dict entry and the rendering loop, capability probe, and key-binding routing all pick it up automatically because they iterate the dict instead of branching on hard-coded platform names. The three axes are layered: the TARGETS dict (c) drives WHICH platforms exist; the capability probe (b) drives WHICH of those are available right now; the polymorphic dispatch (a) drives HOW to call into them once selected. Removing any one axis collapses the design: lose (a) and you scatter platform-check branches across game logic; lose (b) and your game crashes on first launch when the user doesn't have Steam installed; lose (c) and adding a new platform requires editing every loop that touches platforms.

✅ Example Solution
import pygame, sys
from abc import ABC, abstractmethod

# === (c) Build-target-as-data: TARGETS dict externalized ===
TARGETS = {
    'steam':  {'name': 'Steam',     'split': '70/30', 'entry': '$100',    'color': (102, 192, 244)},
    'itch':   {'name': 'itch.io',   'split': 'PWYW',  'entry': 'Free',    'color': (250,  92,  92)},
    'mobile': {'name': 'Mobile',    'split': '70/30', 'entry': '$25-99',  'color': (164, 198,  57)},
    'web':    {'name': 'Web/HTML5', 'split': '0/100', 'entry': 'Hosting', 'color': (255, 173,  51)},
}

# === (b) Platform-capability-detection at runtime ===
SIM_AVAILABLE = {'steam': True, 'itch': True, 'mobile': True, 'web': True}

def detect_capabilities():
    caps = {}
    for key in TARGETS:
        try:
            # Real code: try: import steamworks  except ImportError: ...
            if not SIM_AVAILABLE.get(key, True):
                raise ImportError(f"{key} library not installed")
            caps[key] = (True, 'imported OK')
        except ImportError as e:
            caps[key] = (False, str(e))
    return caps

# === (a) Polymorphic dispatch via shared protocol ===
class PlatformAdapter(ABC):
    @abstractmethod
    def unlock_achievement(self, name): ...
    @abstractmethod
    def submit_score(self, score): ...
    @abstractmethod
    def set_presence(self, key, val): ...
    @abstractmethod
    def post_devlog(self, title): ...

class SteamAdapter(PlatformAdapter):
    def unlock_achievement(self, n): return f"Steamworks.SetAchievement('{n}')"
    def submit_score(self, s):       return f"Steamworks.UploadScore({s})"
    def set_presence(self, k, v):    return f"Friends.SetRichPresence('{k}', '{v}')"
    def post_devlog(self, t):        return f"[Steam: announcement post '{t}']"

class ItchAdapter(PlatformAdapter):
    def unlock_achievement(self, n): return f"[itch: client-side flag '{n}']"
    def submit_score(self, s):       return f"[itch: external leaderboard {s}]"
    def set_presence(self, k, v):    return f"[itch: not supported]"
    def post_devlog(self, t):        return f"POST /api/1/game/X/devlog '{t}'"

class MobileAdapter(PlatformAdapter):
    def unlock_achievement(self, n): return f"GameCenter.report('{n}')"
    def submit_score(self, s):       return f"PlayGames.submit({s})"
    def set_presence(self, k, v):    return f"[mobile: not applicable]"
    def post_devlog(self, t):        return f"[mobile: store update notes]"

class WebAdapter(PlatformAdapter):
    def unlock_achievement(self, n): return f"localStorage.setItem('ach_{n}', 1)"
    def submit_score(self, s):       return f"fetch('/api/score', body={s})"
    def set_presence(self, k, v):    return f"document.title = '{v}'"
    def post_devlog(self, t):        return f"[web: blog RSS '{t}']"

ADAPTERS = {'steam': SteamAdapter(), 'itch': ItchAdapter(),
            'mobile': MobileAdapter(), 'web': WebAdapter()}

pygame.init()
W, H = 1088, 540
screen = pygame.display.set_mode((W, H))
pygame.display.set_caption('Four Adapters, One Game')
font = pygame.font.SysFont('consolas', 14)
big  = pygame.font.SysFont('consolas', 18, bold=True)
clock = pygame.time.Clock()

current, log = 'steam', []
caps = detect_capabilities()

def call(action):
    a = ADAPTERS[current]                        # polymorphic dispatch
    if   action == 'A': r = a.unlock_achievement('FIRST_LAUNCH')
    elif action == 'S': r = a.submit_score(1000)
    elif action == 'P': r = a.set_presence('status', 'Playing')
    elif action == 'D': r = a.post_devlog('Patch 1.1 released')
    log.insert(0, f"{TARGETS[current]['name']}: {r}")
    del log[8:]

running = True
while running:
    for e in pygame.event.get():
        if e.type == pygame.QUIT: running = False
        elif e.type == pygame.KEYDOWN:
            sw = {pygame.K_1:'steam', pygame.K_2:'itch', pygame.K_3:'mobile', pygame.K_4:'web'}
            if e.key in sw: current = sw[e.key]
            elif e.key in (pygame.K_a, pygame.K_s, pygame.K_p, pygame.K_d):
                call(chr(e.key).upper())
            elif e.key == pygame.K_f:
                SIM_AVAILABLE[current] = not SIM_AVAILABLE.get(current, True)
                caps = detect_capabilities()
            elif e.key == pygame.K_r:
                log.clear()
                for k in SIM_AVAILABLE: SIM_AVAILABLE[k] = True
                caps = detect_capabilities()

    screen.fill((20, 20, 28))
    # LEFT panel: TARGETS data dict (axis c)
    screen.blit(big.render('Build Targets (data dict)', True, (200, 220, 255)), (16, 12))
    y = 44
    for key, t in TARGETS.items():
        ok, _ = caps[key]
        sel = (current == key)
        pygame.draw.rect(screen, (50, 50, 70) if sel else (30, 30, 40), (12, y, 372, 56))
        pygame.draw.rect(screen, t['color'], (12, y, 6, 56))
        screen.blit(big.render(t['name'], True, t['color']), (28, y + 4))
        screen.blit(font.render(f"split {t['split']}  entry {t['entry']}", True, (200, 200, 220)), (28, y + 30))
        screen.blit(font.render('OK' if ok else 'MISSING', True, (60, 200, 90) if ok else (220, 90, 90)), (320, y + 20))
        y += 64
    # MIDDLE panel: polymorphic dispatch log (axis a)
    mx = 400
    screen.blit(big.render('Polymorphic Dispatch Log', True, (200, 220, 255)), (mx + 12, 12))
    screen.blit(font.render(f"active: {TARGETS[current]['name']}  (1/2/3/4 switch)", True, (255, 220, 100)), (mx + 12, 40))
    screen.blit(font.render('A=achievement  S=score  P=presence  D=devlog', True, (160, 180, 200)), (mx + 12, 60))
    for i, line in enumerate(log):
        screen.blit(font.render(line[:54], True, (220, 220, 220)), (mx + 12, 92 + i * 22))
    # RIGHT panel: runtime capability probe (axis b)
    rx = 800
    screen.blit(big.render('Runtime Capability Probe', True, (200, 220, 255)), (rx + 12, 12))
    screen.blit(font.render('try: import lib except ImportError', True, (160, 180, 200)), (rx + 12, 40))
    y = 70
    for key, t in TARGETS.items():
        ok, msg = caps[key]
        screen.blit(font.render(f"{t['name']}:", True, (200, 200, 220)), (rx + 12, y))
        screen.blit(font.render(msg, True, (90, 220, 120) if ok else (240, 110, 110)), (rx + 12, y + 16))
        y += 44
    screen.blit(font.render('F = toggle current avail', True, (255, 220, 100)), (rx + 12, H - 50))
    screen.blit(font.render('R = reset all', True, (255, 220, 100)), (rx + 12, H - 30))

    pygame.display.flip()
    clock.tick(60)

pygame.quit()
sys.exit()

🎯 Quick Quiz

Question 1: The demo's PlatformAdapter ABC declares unlock_achievement / submit_score / set_presence / post_devlog as abstract methods, with SteamAdapter / ItchAdapter / MobileAdapter / WebAdapter subclasses each implementing every method backed by their respective platform-native API (Steamworks SetAchievement / itch REST devlog endpoint / GameCenter.report / localStorage.setItem). The game's input handler does ADAPTERS[current].unlock_achievement('FIRST_LAUNCH') with no isinstance branches and no platform-name string checks. Why is this polymorphic-dispatch-via-shared-protocol shape preferred over an if-elif-else chain on the active platform name scattered across every game-logic site that needs to call into a publishing platform?

Question 2: The demo's detect_capabilities() function loops over TARGETS and probes each platform via try: ... raise ImportError(...) except ImportError as e: caps[key] = (False, str(e)) — mirroring the lesson's try: import steamworks except ImportError: steamworks = None followed by if not steamworks: return False in SteamIntegration.initialize(). The F-key toggles the simulated availability of the current platform and re-runs the probe; the right panel updates live to show OK or MISSING with the exception message. Why is this runtime try-import probe the right way to determine which platforms a given user's installed copy of the game can actually reach, rather than baking platform support into the binary at build time or asking the user to declare it during install?

Question 3: The demo's TARGETS dict externalizes every per-platform fact (name, revenue split, entry cost, color) into one module-level data structure that the rendering loop, capability-detection loop, key-binding routing, and adapter lookup all iterate or key into. Adding Epic Games Store requires one new entry like 'epic': {'name': 'Epic', 'split': '88/12', 'entry': 'Curated', 'color': (40, 40, 40)} plus one new EpicAdapter — and the rendering loop, capability probe, and key binding 5 all pick it up automatically because they iterate the dict. Why is this build-target-as-data-externalization shape preferred over scattering per-platform facts across if-elif branches in the rendering loop, the capability probe, and the key handler?

What's Next?

Now that you know where to distribute, let's learn how to market your game effectively!