Skip to main content

Marketing Basics

22 minute read

Getting Your Game Noticed

Master indie game marketing! Build communities, create compelling content, reach influencers, optimize store pages, and launch successfully! 📢🎮🚀

Marketing Timeline

📅 When to Start Marketing

The answer: As soon as possible!

gantt title Marketing Timeline dateFormat YYYY-MM-DD section Development Concept Art :2024-01-01, 30d Prototype :2024-01-15, 45d Alpha :2024-03-01, 60d Beta :2024-05-01, 30d section Marketing Social Media Start :2024-01-01, 180d Dev Blog :2024-01-15, 165d Press Kit :2024-03-01, 90d Trailer :2024-04-01, 30d Influencer Outreach :2024-04-15, 45d Launch Campaign :2024-05-15, 15d

Building Your Brand

🎨 Game Identity

Essential Brand Elements

Creating Your Elevator Pitch


# pitch_generator.py - Help create compelling game pitches
class PitchGenerator:
    def __init__(self):
        self.templates = [
            "{game_name} is a {genre} game where you {core_mechanic} to {goal}.",
            "Imagine {reference1} meets {reference2} - that's {game_name}.",
            "In {game_name}, you play as {protagonist} who must {challenge} using {unique_feature}.",
            "{game_name}: {adjective1}, {adjective2} {genre} with {unique_selling_point}."
        ]
    
    def generate_pitch(self, game_info):
        """Generate different pitch variations"""
        pitches = []
        
        # Format: Game X meets Game Y
        if 'references' in game_info:
            ref_pitch = f"{game_info['references'][0]} meets {game_info['references'][1]}"
            pitches.append(ref_pitch)
        
        # Format: Genre + USP
        genre_pitch = f"A {game_info['genre']} game with {game_info['usp']}"
        pitches.append(genre_pitch)
        
        # Format: Problem + Solution
        problem_pitch = f"Ever wanted to {game_info['fantasy']}? Now you can in {game_info['name']}!"
        pitches.append(problem_pitch)
        
        return pitches
    
    def create_steam_description(self, game_info):
        """Create formatted Steam store description"""
        description = f"""
[h1]About {game_info['name']}[/h1]
{game_info['elevator_pitch']}

[h2]Key Features[/h2]
[list]
{"".join(f"[*]{feature}\n" for feature in game_info['features'])}
[/list]

[h2]Gameplay[/h2]
{game_info['gameplay_description']}

[h2]Story[/h2]
{game_info['story']}
"""
        return description

# Example usage
game = {
    'name': 'Pixel Dungeons',
    'genre': 'roguelike platformer',
    'usp': 'time manipulation mechanics',
    'fantasy': 'explore endless dungeons without dying',
    'references': ['Spelunky', 'Braid'],
    'elevator_pitch': 'A challenging roguelike where every death teaches you something new.',
    'features': [
        'Procedurally generated levels',
        'Time rewind mechanic',
        '100+ unique items',
        'Local co-op mode',
        'Daily challenges'
    ],
    'gameplay_description': 'Navigate through procedurally generated dungeons...',
    'story': 'You are the last Time Keeper...'
}

pitch_gen = PitchGenerator()
pitches = pitch_gen.generate_pitch(game)
        

Social Media Strategy

📱 Platform-Specific Strategies

Platform Best For Content Type Frequency
Twitter/X Quick updates, GIFs #gamedev, #screenshotsaturday Daily
TikTok Viral potential Behind-scenes, funny bugs 3-4/week
YouTube Devlogs, trailers Long-form content Weekly/Monthly
Reddit Community feedback Progress posts Weekly
Discord Community building Direct engagement Always on
Instagram Visual showcase Art, screenshots 2-3/week

Content Calendar Tool


import datetime
import json
from typing import List, Dict

class ContentCalendar:
    def __init__(self):
        self.calendar = {}
        self.hashtags = {
            'monday': ['#MadeWithUnity', '#IndieDevMonday'],
            'tuesday': ['#TuesdayThoughts', '#GameDev'],
            'wednesday': ['#WIPWednesday', '#IndieGame'],
            'thursday': ['#ThrowbackThursday', '#GameArt'],
            'friday': ['#FollowFriday', '#IndieDevs'],
            'saturday': ['#ScreenshotSaturday', '#SaturdayVibes'],
            'sunday': ['#SilentSunday', '#GameDesign']
        }
    
    def generate_week_content(self, week_start: datetime.date):
        """Generate content ideas for a week"""
        week_content = {}
        
        for i in range(7):
            date = week_start + datetime.timedelta(days=i)
            day_name = date.strftime('%A').lower()
            
            week_content[str(date)] = {
                'primary_hashtags': self.hashtags[day_name],
                'content_ideas': self.get_content_ideas(day_name),
                'platforms': self.get_best_platforms(day_name)
            }
        
        return week_content
    
    def get_content_ideas(self, day_name: str) -> List[str]:
        """Get content ideas based on day"""
        ideas = {
            'monday': [
                'Share development goals for the week',
                'Post about tools/tech you\'re using',
                'Tutorial or tip for other devs'
            ],
            'tuesday': [
                'Game design philosophy post',
                'Character spotlight',
                'Lore snippet'
            ],
            'wednesday': [
                'Work in progress GIF',
                'Before/after comparison',
                'Bug turned feature'
            ],
            'thursday': [
                'Throwback to early prototype',
                'Evolution of game art',
                'Old vs new mechanics'
            ],
            'friday': [
                'Shoutout to community members',
                'Recommend other indie games',
                'Team member spotlight'
            ],
            'saturday': [
                'Polish screenshot/GIF',
                'New feature showcase',
                'Visual effects demonstration'
            ],
            'sunday': [
                'Reflection on development',
                'Community question',
                'Upcoming week preview'
            ]
        }
        return ideas.get(day_name, ['General game update'])
    
    def get_best_platforms(self, day_name: str) -> List[str]:
        """Suggest best platforms for each day"""
        platform_schedule = {
            'monday': ['Twitter', 'LinkedIn'],
            'tuesday': ['Reddit', 'Twitter'],
            'wednesday': ['Twitter', 'Instagram'],
            'thursday': ['TikTok', 'Twitter'],
            'friday': ['Twitter', 'Discord'],
            'saturday': ['Twitter', 'Instagram', 'Reddit'],
            'sunday': ['YouTube', 'Discord']
        }
        return platform_schedule.get(day_name, ['Twitter'])
    
    def create_post_template(self, post_type: str) -> str:
        """Create template for different post types"""
        templates = {
            'screenshot': """
🎮 {game_name} - {feature_name}

{description}

{call_to_action}

{hashtags}
""",
            'devlog': """
📝 Dev Update #{number}

This week's progress:
✅ {achievement1}
✅ {achievement2}
🔄 {in_progress}
📅 {next_week}

{link}
{hashtags}
""",
            'question': """
🤔 Quick question for the community:

{question}

Let me know in the comments! 💭

{hashtags}
""",
            'milestone': """
🎉 HUGE MILESTONE! 

{achievement}

Thank you all for the support! ❤️

{stats}

{hashtags}
"""
        }
        return templates.get(post_type, '{content}\n\n{hashtags}')

# Automated posting
class SocialMediaAutomation:
    def __init__(self, api_keys: Dict[str, str]):
        self.api_keys = api_keys
        self.platforms = {}
        
    def schedule_post(self, platform: str, content: str, 
                     media_path: str = None, schedule_time: datetime = None):
        """Schedule a post across platforms"""
        if platform == 'twitter':
            return self.post_to_twitter(content, media_path, schedule_time)
        elif platform == 'reddit':
            return self.post_to_reddit(content, media_path, schedule_time)
        # Add more platforms...
    
    def cross_post(self, content: Dict[str, str], media_path: str = None):
        """Post to multiple platforms with platform-specific content"""
        results = {}
        for platform, platform_content in content.items():
            try:
                results[platform] = self.schedule_post(
                    platform, platform_content, media_path
                )
            except Exception as e:
                results[platform] = f"Error: {e}"
        return results
        

Press Kit Creation

📰 Professional Press Kit

Press Kit Structure


<!-- presskit.html - Basic press kit template -->
<!DOCTYPE html>
<html>
<head>
    <title>Game Name - Press Kit</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 1200px; margin: 0 auto; }
        .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px; }
        .section { padding: 20px; margin: 20px 0; }
        .gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
        .download-btn { background: #4CAF50; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; }
    </style>
</head>
<body>
    <div class="header">
        <h1>Game Name</h1>
        <p>Tagline goes here</p>
    </div>
    
    <div class="section">
        <h2>Fact Sheet</h2>
        <ul>
            <li><strong>Developer:</strong> Your Studio</li>
            <li><strong>Release Date:</strong> Q2 2024</li>
            <li><strong>Platforms:</strong> PC, Mac, Linux</li>
            <li><strong>Price:</strong> $14.99 USD</li>
            <li><strong>Genre:</strong> Action Platformer</li>
            <li><strong>Players:</strong> 1-4 (local co-op)</li>
        </ul>
    </div>
    
    <div class="section">
        <h2>Description</h2>
        <p>Elevator pitch here.</p>
        <p>Detailed description...</p>
    </div>
    
    <div class="section">
        <h2>Features</h2>
        <ul>
            <li>Feature 1</li>
            <li>Feature 2</li>
            <li>Feature 3</li>
        </ul>
    </div>
    
    <div class="section">
        <h2>Media</h2>
        <h3>Trailer</h3>
        <iframe width="560" height="315" src="https://youtube.com/embed/VIDEO_ID"></iframe>
        
        <h3>Screenshots</h3>
        <div class="gallery">
            <img src="screenshot1.jpg" alt="Screenshot 1">
            <img src="screenshot2.jpg" alt="Screenshot 2">
            <img src="screenshot3.jpg" alt="Screenshot 3">
        </div>
        
        <h3>Logos & Icons</h3>
        <a href="assets.zip" class="download-btn">Download Press Assets</a>
    </div>
    
    <div class="section">
        <h2>About the Developer</h2>
        <p>Studio history and background...</p>
    </div>
    
    <div class="section">
        <h2>Contact</h2>
        <ul>
            <li><strong>Email:</strong> press@yourgame.com</li>
            <li><strong>Twitter:</strong> @yourgame</li>
            <li><strong>Website:</strong> www.yourgame.com</li>
            <li><strong>Discord:</strong> discord.gg/yourgame</li>
        </ul>
    </div>
</body>
</html>
        

Press Kit Checklist

Influencer Outreach

🎥 Reaching Content Creators

Influencer Database Tool


import csv
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from datetime import datetime

class InfluencerOutreach:
    def __init__(self):
        self.influencers = []
        self.email_templates = {}
        
    def load_influencer_database(self, csv_path):
        """Load influencer information from CSV"""
        with open(csv_path, 'r') as file:
            reader = csv.DictReader(file)
            self.influencers = list(reader)
    
    def filter_influencers(self, min_subscribers=1000, 
                          genres=None, platforms=None):
        """Filter influencers based on criteria"""
        filtered = self.influencers
        
        if min_subscribers:
            filtered = [i for i in filtered 
                       if int(i.get('subscribers', 0)) >= min_subscribers]
        
        if genres:
            filtered = [i for i in filtered 
                       if any(g in i.get('genres', '') for g in genres)]
        
        if platforms:
            filtered = [i for i in filtered 
                       if i.get('platform') in platforms]
        
        return filtered
    
    def create_email_template(self, influencer_tier='standard'):
        """Create personalized email templates"""
        templates = {
            'micro': """
Hi {name}!

I've been following your channel and love your content on {genre} games!

I'm developing {game_name}, a {game_genre} game that I think your audience would enjoy.
{game_pitch}

Would you be interested in checking it out? I can provide a Steam key right away.

Best,
{your_name}
""",
            'standard': """
Hi {name},

I hope this email finds you well! I'm {your_name}, developer of {game_name}.

I've been watching your videos, especially your recent one on {recent_video}, 
and I think {game_name} would be a great fit for your channel.

{game_name} is a {game_genre} game where {game_pitch}

Key features your audience might enjoy:
{features}

I'd love to send you a key if you're interested. The game launches on {launch_date}, 
but you're welcome to cover it whenever works best for your schedule.

Press kit: {press_kit_url}
Trailer: {trailer_url}

Thanks for your time!
{your_name}
""",
            'large': """
Dear {name},

I'm {your_name} from {studio_name}. We're launching {game_name} on {launch_date} 
and would be honored to have you cover it.

About {game_name}:
{game_pitch}

We can offer:
- Early access key
- Developer interview opportunity
- Exclusive content/footage
- Review embargo date: {embargo_date}

Our press kit with all assets: {press_kit_url}

We're also happy to provide any additional materials or answer any questions.

Best regards,
{your_name}
{studio_name}
"""
        }
        return templates.get(influencer_tier, templates['standard'])
    
    def track_outreach(self, influencer_id, status='sent'):
        """Track outreach status"""
        # Status: sent, opened, responded, covered, declined
        outreach_log = {
            'influencer_id': influencer_id,
            'date': datetime.now().isoformat(),
            'status': status
        }
        
        # Save to database or CSV
        with open('outreach_log.csv', 'a', newline='') as file:
            writer = csv.DictWriter(file, fieldnames=outreach_log.keys())
            writer.writerow(outreach_log)
    
    def calculate_influencer_value(self, influencer):
        """Estimate influencer marketing value"""
        subscribers = int(influencer.get('subscribers', 0))
        avg_views = int(influencer.get('avg_views', subscribers * 0.1))
        engagement_rate = float(influencer.get('engagement_rate', 0.02))
        
        # Simple value calculation
        estimated_reach = avg_views
        estimated_clicks = estimated_reach * engagement_rate
        estimated_conversions = estimated_clicks * 0.02  # 2% conversion
        
        return {
            'reach': estimated_reach,
            'clicks': estimated_clicks,
            'conversions': estimated_conversions,
            'value_score': (subscribers * 0.3 + avg_views * 0.5 + 
                           engagement_rate * 100000 * 0.2)
        }

# Key distribution system
class KeyDistribution:
    def __init__(self, steam_keys_file):
        self.available_keys = []
        self.distributed_keys = {}
        self.load_keys(steam_keys_file)
    
    def load_keys(self, filepath):
        """Load Steam keys from file"""
        with open(filepath, 'r') as file:
            self.available_keys = [line.strip() for line in file]
    
    def distribute_key(self, recipient_email, recipient_name, platform='Steam'):
        """Distribute a key and track it"""
        if not self.available_keys:
            return None
        
        key = self.available_keys.pop(0)
        self.distributed_keys[key] = {
            'recipient_email': recipient_email,
            'recipient_name': recipient_name,
            'platform': platform,
            'date': datetime.now().isoformat(),
            'activated': False
        }
        
        # Log distribution
        with open('key_distribution.json', 'w') as file:
            json.dump(self.distributed_keys, file, indent=2)
        
        return key
        

Community Building

👥 Building Your Community

graph TD A["Community Building"] --> B["Discord Server"] A --> C["Steam Community"] A --> D["Reddit Presence"] A --> E["Newsletter"] B --> F["Welcome Channel"] B --> G["Dev Updates"] B --> H["Bug Reports"] B --> I["Fan Art"] C --> J["Forums"] C --> K["Announcements"] C --> L["Guides"] D --> M["r/gamedev"] D --> N["r/indiegames"] D --> O["Genre Subreddits"] E --> P["Monthly Updates"] E --> Q["Exclusive Content"] E --> R["Beta Access"]

Discord Bot for Community


# discord_bot.py - Community management bot
import discord
from discord.ext import commands, tasks
import json
import random

class GameCommunityBot(commands.Bot):
    def __init__(self):
        intents = discord.Intents.default()
        intents.message_content = True
        super().__init__(command_prefix='!', intents=intents)
        
        self.dev_updates = []
        self.bug_reports = {}
        self.feature_requests = {}
        
    async def on_ready(self):
        print(f'{self.user} is online!')
        self.status_update.start()
    
    @tasks.loop(hours=1)
    async def status_update(self):
        """Rotate bot status"""
        statuses = [
            "In Development",
            "!help for commands",
            f"{len(self.guilds)} servers",
            "Launching Soon!"
        ]
        await self.change_presence(
            activity=discord.Game(random.choice(statuses))
        )
    
    @commands.command()
    async def roadmap(self, ctx):
        """Show development roadmap"""
        embed = discord.Embed(
            title="Development Roadmap",
            color=0x00ff00
        )
        embed.add_field(
            name="✅ Completed",
            value="- Core gameplay\n- Level 1-5",
            inline=False
        )
        embed.add_field(
            name="🔄 In Progress",
            value="- Multiplayer\n- Level 6-10",
            inline=False
        )
        embed.add_field(
            name="📅 Planned",
            value="- Level editor\n- Steam Workshop",
            inline=False
        )
        await ctx.send(embed=embed)
    
    @commands.command()
    async def bug(self, ctx, *, description):
        """Report a bug"""
        bug_id = len(self.bug_reports) + 1
        self.bug_reports[bug_id] = {
            'user': str(ctx.author),
            'description': description,
            'status': 'open'
        }
        
        embed = discord.Embed(
            title=f"Bug Report #{bug_id}",
            description=description,
            color=0xff0000
        )
        embed.set_footer(text=f"Reported by {ctx.author}")
        
        # Send to bug reports channel
        bug_channel = ctx.guild.get_channel(BUG_CHANNEL_ID)
        await bug_channel.send(embed=embed)
        await ctx.send(f"Thanks! Bug #{bug_id} has been reported.")
    
    @commands.command()
    async def beta(self, ctx):
        """Sign up for beta testing"""
        beta_role = discord.utils.get(ctx.guild.roles, name="Beta Tester")
        if beta_role in ctx.author.roles:
            await ctx.send("You're already a beta tester!")
        else:
            await ctx.author.add_roles(beta_role)
            await ctx.send("Welcome to the beta! Check #beta-access for your key.")
    
    @commands.command()
    @commands.has_role("Developer")
    async def announce(self, ctx, *, message):
        """Make an announcement (dev only)"""
        announcement_channel = ctx.guild.get_channel(ANNOUNCE_CHANNEL_ID)
        
        embed = discord.Embed(
            title="📢 Developer Update",
            description=message,
            color=0x00ff00,
            timestamp=datetime.utcnow()
        )
        embed.set_footer(text="From the dev team")
        
        await announcement_channel.send("@everyone", embed=embed)

# Run bot
bot = GameCommunityBot()
bot.run('YOUR_BOT_TOKEN')
        

Launch Strategy

🚀 Launch Day Checklist

Pre-Launch (1 Week Before)

Launch Day

Post-Launch (First Week)

Marketing Metrics

📊 Tracking Success


# analytics.py - Marketing analytics tracker
import pandas as pd
import matplotlib.pyplot as plt
from datetime import datetime, timedelta

class MarketingAnalytics:
    def __init__(self):
        self.metrics = {
            'steam_wishlists': [],
            'twitter_followers': [],
            'discord_members': [],
            'youtube_views': [],
            'website_visits': [],
            'newsletter_subs': []
        }
    
    def track_metric(self, metric_name, value, date=None):
        """Track a marketing metric"""
        if date is None:
            date = datetime.now()
        
        self.metrics[metric_name].append({
            'date': date,
            'value': value
        })
    
    def calculate_growth_rate(self, metric_name, days=7):
        """Calculate growth rate over period"""
        if len(self.metrics[metric_name]) < 2:
            return 0
        
        current = self.metrics[metric_name][-1]['value']
        past = self.metrics[metric_name][-days]['value'] if len(self.metrics[metric_name]) > days else self.metrics[metric_name][0]['value']
        
        if past == 0:
            return 100
        
        return ((current - past) / past) * 100
    
    def get_conversion_funnel(self):
        """Calculate marketing funnel conversion"""
        funnel = {
            'Awareness': self.get_latest('website_visits'),
            'Interest': self.get_latest('steam_wishlists'),
            'Consideration': self.get_latest('discord_members'),
            'Purchase': 0,  # Add sales data
            'Retention': 0   # Add retention data
        }
        
        conversions = {}
        stages = list(funnel.keys())
        
        for i in range(len(stages) - 1):
            if funnel[stages[i]] > 0:
                conversions[f"{stages[i]} → {stages[i+1]}"] = \
                    (funnel[stages[i+1]] / funnel[stages[i]]) * 100
        
        return funnel, conversions
    
    def generate_report(self):
        """Generate marketing report"""
        report = {
            'total_reach': sum([
                self.get_latest('twitter_followers'),
                self.get_latest('discord_members'),
                self.get_latest('newsletter_subs')
            ]),
            'wishlist_conversion': self.get_latest('steam_wishlists') / 
                                  max(self.get_latest('website_visits'), 1) * 100,
            'community_growth': {
                metric: self.calculate_growth_rate(metric)
                for metric in self.metrics.keys()
            },
            'top_channel': max(self.metrics.items(), 
                             key=lambda x: self.calculate_growth_rate(x[0]))[0]
        }
        
        return report
    
    def plot_metrics(self):
        """Visualize marketing metrics"""
        fig, axes = plt.subplots(2, 3, figsize=(15, 10))
        
        for idx, (metric_name, data) in enumerate(self.metrics.items()):
            if idx >= 6:
                break
            
            ax = axes[idx // 3, idx % 3]
            
            if data:
                dates = [d['date'] for d in data]
                values = [d['value'] for d in data]
                
                ax.plot(dates, values, marker='o')
                ax.set_title(metric_name.replace('_', ' ').title())
                ax.set_xlabel('Date')
                ax.set_ylabel('Count')
                ax.grid(True)
        
        plt.tight_layout()
        plt.savefig('marketing_metrics.png')
        plt.show()
    
    def get_latest(self, metric_name):
        """Get latest value for metric"""
        if self.metrics[metric_name]:
            return self.metrics[metric_name][-1]['value']
        return 0

# Usage
analytics = MarketingAnalytics()
analytics.track_metric('steam_wishlists', 1500)
analytics.track_metric('twitter_followers', 850)
analytics.track_metric('discord_members', 320)

report = analytics.generate_report()
print(f"Total Reach: {report['total_reach']}")
print(f"Top Growing Channel: {report['top_channel']}")
        

Best Practices

✨ Marketing Best Practices

Key Takeaways

🏋️‍♂️ Practice Exercise

🏋️‍♂️ Exercise 1: Three Pipelines, One Launch — Pitch Templates as Data + 5-Stage Conversion Funnel + Multi-Track Marketing Timeline in One Pygame Window

Objective: Build a runnable pygame window in roughly 95 lines that shows three orthogonal marketing-domain disciplines visible per frame on a 1088×540 window: a top timeline panel showing six parallel marketing tracks on different cadences, a bottom-left pitch panel rendering one of four templates from a list, and a bottom-right conversion-funnel panel showing five stages with stage-by-stage drop-off rates. (a) Marketing-copy as data externalization at MARKETING-COPY scope: four pitch templates live in a module-level PITCH_TEMPLATES list as format strings ('{name} is a {genre} where you {mech} to {goal}.', etc.); a GAME dict holds the values that fill those templates; seven days of hashtags live in a HASHTAGS dict keyed by day-of-week ('Mon': '#IndieDevMonday', ..., 'Sun': '#SilentSunday'); pressing 1/2/3/4 swaps which template renders by changing a single integer index, and the rendered pitch updates live without touching any control flow — the data IS the marketing copy, externalized from the runtime. Same data-driven externalization shape as architecture_save_load schema, pathfinding terrain-cost dict, behavior-trees child-list-order, decision-making personality dict, procedural generation seed, and publishing_executables .spec file (chat-70 at BUILD-CONFIG scope), now applied at MARKETING-COPY scope. (b) Conversion-funnel-as-multi-stage-signal at MARKETING-METRICS scope: a five-stage funnel ['Awareness', 'Interest', 'Consideration', 'Purchase', 'Retention'] is rendered as five horizontal bars with widths proportional to count; between adjacent bars, the conversion rate (counts[i+1] / counts[i]) * 100 is rendered as its own per-edge label — four edge metrics for five nodes, four independent optimization targets. Pressing A/I/C/P/E bumps the corresponding stage by a small amount, and the affected upstream/downstream conversion rates update live, demonstrating that doubling Awareness without improving Awareness→Interest produces zero downstream gain (the funnel is a pipeline of filters, not a single aggregate metric). Same proper-composition-vs-flat-aggregation shape as graphics_ui_hud's UIElement-protocol-vs-isinstance-branches and graphics_procedural's height-vs-moisture-independence, applied at MARKETING-METRICS scope. (c) Marketing-pipeline vs build-pipeline temporal-decoupling at PUBLISHING-PIPELINE scope: six marketing tracks ('Social Media' 180d, 'Dev Blog' 165d, 'Press Kit' 90d, 'Trailer' 30d, 'Influencer' 45d, 'Launch' 15d) live in a TRACKS list of (label, start_day, duration, color) tuples; each renders as its own horizontal Gantt bar at its declared start and length, and a red day-cursor advances at 30 days/sec across the timeline (SPACE pauses, R resets). The launch day at day 150 is marked with a yellow vertical line where all tracks converge — the marketing pipeline runs on calendar cadences spanning 15–180 days, while chat-70's build pipeline runs on-demand whenever the developer presses build; the two pipelines share the launch date as their convergence anchor but neither blocks the other. Same independent-data-composes-via-shared-anchor pattern as graphics_postprocessing's ping-pong-effects-dict, graphics_ui_hud's UIElement-protocol, and graphics_procedural's height-vs-moisture-independence, applied at PUBLISHING-PIPELINE scope. HUD shows the current day, day-of-week, today's hashtag (looked up from the dict, not branched on), and key bindings — three orthogonal marketing-domain disciplines visible per frame as concrete strings, bar geometries, and conversion percentages. ADVANCES the publishing module 1/5 → 2/5 partial; module-completeness stays 11/13 since publishing doesn't close in one chat (3 publishing lessons remain after this one: performance / platforms / updates).

Instructions:

  1. Define the three marketing-copy data structures at module level: PITCH_TEMPLATES = [...] as a list of four format-string templates; GAME = {'name': 'Pixel Dungeons', ...} as the dict that fills them; HASHTAGS = {'Mon': '#IndieDevMonday', ...} as the day-of-week→hashtag dict; DAYS = ['Mon', 'Tue', ..., 'Sun'] as the index lookup for the day cursor.
  2. Define the timeline data: TRACKS = [('Social Media', 0, 180, color), ('Dev Blog', 14, 165, color), ('Press Kit', 60, 90, color), ('Trailer', 90, 30, color), ('Influencer', 105, 45, color), ('Launch', 135, 15, color)]; TIMELINE_DAYS = 180; LAUNCH_DAY = 150. Define the funnel data: FUNNEL = ['Awareness', 'Interest', 'Consideration', 'Purchase', 'Retention']; counts = [10000, 1500, 600, 200, 120].
  3. Initialize pygame at 1088×540. Use Consolas/Courier monospace at sizes 12, 14, and 18-bold for the three font tiers. Define helper render_pitch(idx) returning PITCH_TEMPLATES[idx].format(**GAME), and conv_rate(prev, curr) returning 0.0 if prev == 0 else (curr / prev) * 100.0.
  4. Maintain three pieces of state: template_idx = 0 (selected pitch template), day_cursor = 0.0 (current day, advances DAY_RATE = 30.0 days per second), paused = False. Handle keys: 1–4 set template_idx; SPACE toggles paused; R resets day_cursor to 0; A/I/C/P/E bump counts[0..4] by 500/50/25/10/5 respectively (only one each per key press, which is the discrete-keydown semantics).
  5. Per-frame timeline render (top panel): for each track, compute x = bar_x0 + int((start_day / TIMELINE_DAYS) * timeline_width) and w = int((duration / TIMELINE_DAYS) * timeline_width), draw a colored rect at (x, track_y, w, 16) with the label rendered to the left margin. Draw the day cursor as a 2-pixel red vertical line at cursor_x = bar_x0 + int((day_cursor / TIMELINE_DAYS) * timeline_width), and the launch-day marker as a 1-pixel yellow vertical line at the launch-day x.
  6. Per-frame pitch panel (bottom-left): show four [1] template 1 labels with the active one highlighted in yellow; render render_pitch(template_idx) word-wrapped at ~38 chars across up to 5 lines so the rendered pitch is visible. Per-frame funnel panel (bottom-right): for each of the five stages, render a horizontal bar at width (count / max(counts)) * (panel_width - 140) with the stage label and count overlaid; below each bar (except the last), render ' -> {cr:.1f}% to next stage' using conv_rate(counts[i], counts[i+1]).
  7. Per-frame HUD: show 'Day {n} ({dow})' with the day-of-week looked up via DAYS[int(day_cursor) % 7], today's hashtag via HASHTAGS[dow], and the key bindings string. Verify the three orthogonal axes are visible per frame: (a) pressing 1–4 swaps the rendered pitch text without changing any control flow — the templates ARE data; (b) pressing A/I/C/P/E bumps a single stage and the displayed conversion rates change for the affected edges only — five stages, four edges, four independent signals; (c) the timeline panel shows six parallel bars converging at the yellow launch-day line while the red cursor sweeps left-to-right — independent cadences, shared launch anchor.
💡 Hint

The whole point of the demo is that the data IS the marketing pipeline. PITCH_TEMPLATES is a list, HASHTAGS is a dict, TRACKS is a list of tuples, FUNNEL is a list, counts is a list — every marketing artifact in the demo is rendered from a data structure that the program treats as content. Pressing 1–4 changes which template renders by changing an integer index; the control flow doesn't branch on which template is active. Pressing A/I/C/P/E bumps a single funnel stage and the conversion rates re-derive in one place — the funnel is a pipeline of filters, not a single aggregate metric, and that's why each edge needs its own conversion-rate label. The marketing tracks render at their declared start_day and duration against a fixed TIMELINE_DAYS = 180 scale; adding a seventh track is a one-line append to TRACKS, not a new branch in the render loop. The temporal-decoupling axis is the visual fact that the six tracks have different lengths, start at different days, and overlap freely — they don't block each other; they share the launch date as their convergence point but otherwise run independently.

✅ Example Solution
import pygame, sys

W, H = 1088, 540
FPS = 60

PITCH_TEMPLATES = [
    "{name} is a {genre} where you {mech} to {goal}.",
    "Imagine {ref1} meets {ref2} - that's {name}.",
    "In {name}, you play as {hero} who must {challenge}.",
    "{name}: {adj1}, {adj2} {genre} with {usp}.",
]
GAME = {'name': 'Pixel Dungeons', 'genre': 'roguelike', 'mech': 'rewind time',
        'goal': 'escape the loop', 'ref1': 'Spelunky', 'ref2': 'Braid',
        'hero': 'a Time Keeper', 'challenge': 'rewrite history',
        'adj1': 'snappy', 'adj2': 'rewarding', 'usp': 'time-rewind mechanics'}
HASHTAGS = {'Mon': '#IndieDevMonday', 'Tue': '#TuesdayTip', 'Wed': '#WIPWednesday',
            'Thu': '#ThrowbackThursday', 'Fri': '#FollowFriday',
            'Sat': '#ScreenshotSaturday', 'Sun': '#SilentSunday'}
DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']

TRACKS = [
    ('Social Media', 0, 180, (88, 166, 255)),
    ('Dev Blog',     14, 165, (140, 200, 100)),
    ('Press Kit',    60,  90, (240, 180, 80)),
    ('Trailer',      90,  30, (200, 120, 200)),
    ('Influencer',  105,  45, (240, 100, 100)),
    ('Launch',      135,  15, (255, 220, 60)),
]
TIMELINE_DAYS = 180
LAUNCH_DAY = 150

FUNNEL = ['Awareness', 'Interest', 'Consideration', 'Purchase', 'Retention']
counts = [10000, 1500, 600, 200, 120]

template_idx = 0
day_cursor = 0.0
paused = False
DAY_RATE = 30.0

pygame.init()
screen = pygame.display.set_mode((W, H))
clock = pygame.time.Clock()
small = pygame.font.SysFont('Consolas', 12)
font = pygame.font.SysFont('Consolas', 14)
big = pygame.font.SysFont('Consolas', 18, bold=True)

def render_pitch(idx):
    return PITCH_TEMPLATES[idx].format(**GAME)

def conv_rate(prev, curr):
    return 0.0 if prev == 0 else (curr / prev) * 100.0

running = True
while running:
    dt = clock.tick(FPS) / 1000.0
    for ev in pygame.event.get():
        if ev.type == pygame.QUIT: running = False
        elif ev.type == pygame.KEYDOWN:
            if ev.key in (pygame.K_1, pygame.K_2, pygame.K_3, pygame.K_4):
                template_idx = ev.key - pygame.K_1
            elif ev.key == pygame.K_SPACE: paused = not paused
            elif ev.key == pygame.K_r: day_cursor = 0.0
            elif ev.key == pygame.K_a: counts[0] += 500
            elif ev.key == pygame.K_i: counts[1] += 50
            elif ev.key == pygame.K_c: counts[2] += 25
            elif ev.key == pygame.K_p: counts[3] += 10
            elif ev.key == pygame.K_e: counts[4] += 5

    if not paused:
        day_cursor = min(TIMELINE_DAYS, day_cursor + DAY_RATE * dt)

    screen.fill((20, 20, 28))
    pygame.draw.rect(screen, (40, 40, 50), (10, 10, W-20, 180))
    screen.blit(big.render('Marketing Timeline (3 pipelines, 1 launch)', True, (220, 220, 240)), (20, 14))
    bar_x0, ty, bh, bp = 140, 38, 16, 6
    tw = W - 30 - bar_x0
    for label, start, dur, color in TRACKS:
        x = bar_x0 + int((start / TIMELINE_DAYS) * tw)
        bw = int((dur / TIMELINE_DAYS) * tw)
        pygame.draw.rect(screen, color, (x, ty, bw, bh))
        screen.blit(small.render(label, True, (200, 200, 200)), (20, ty + 2))
        ty += bh + bp
    cursor_x = bar_x0 + int((day_cursor / TIMELINE_DAYS) * tw)
    pygame.draw.line(screen, (255, 80, 80), (cursor_x, 36), (cursor_x, 180), 2)
    launch_x = bar_x0 + int((LAUNCH_DAY / TIMELINE_DAYS) * tw)
    pygame.draw.line(screen, (255, 220, 60), (launch_x, 36), (launch_x, 180), 1)
    dow = DAYS[int(day_cursor) % 7]
    info = f'Day {int(day_cursor)} ({dow}) ' + ('paused' if paused else '')
    screen.blit(font.render(info, True, (255, 255, 255)), (20, 162))
    screen.blit(small.render(f'today hashtag: {HASHTAGS[dow]}', True, (180, 220, 255)), (240, 164))

    pygame.draw.rect(screen, (40, 40, 50), (10, 200, 540, 160))
    screen.blit(big.render('Pitch (template-as-data)', True, (220, 220, 240)), (20, 204))
    for i in range(4):
        c = (255, 220, 80) if i == template_idx else (160, 160, 160)
        screen.blit(small.render(f'[{i+1}] template {i+1}', True, c), (20, 230 + i*16))
    pitch = render_pitch(template_idx)
    words, lines, cur = pitch.split(), [], ''
    for word in words:
        if len(cur) + len(word) + 1 > 38:
            lines.append(cur); cur = word
        else:
            cur = (cur + ' ' + word).strip()
    if cur: lines.append(cur)
    for li, line in enumerate(lines[:5]):
        screen.blit(font.render(line, True, (255, 255, 255)), (170, 232 + li*18))

    fx, fy, fw = 560, 200, W - 10 - 560
    pygame.draw.rect(screen, (40, 40, 50), (fx, fy, fw, 320))
    screen.blit(big.render('Conversion Funnel (5 stages, 4 signals)', True, (220, 220, 240)), (fx+10, fy+4))
    mx = max(counts) or 1
    for i, (stage, count) in enumerate(zip(FUNNEL, counts)):
        bar_w = int((count / mx) * (fw - 140))
        by = fy + 32 + i*52
        pygame.draw.rect(screen, (88, 166, 255), (fx+10, by, bar_w, 24))
        screen.blit(font.render(f'{stage}: {count}', True, (255, 255, 255)), (fx+14, by+4))
        if i < len(FUNNEL) - 1:
            cr = conv_rate(counts[i], counts[i+1])
            screen.blit(small.render(f'  -> {cr:.1f}% to next stage', True, (255, 200, 100)), (fx+10, by+28))

    hud = '1-4=pitch | A/I/C/P/E=bump funnel | SPACE=pause | R=reset day'
    screen.blit(small.render(hud, True, (160, 160, 180)), (10, 524))
    pygame.display.flip()

pygame.quit()

🎯 Quick Quiz

Question 1: The lesson's PitchGenerator.templates = [...] list and ContentCalendar.hashtags = {'monday': [...], ..., 'sunday': [...]} dict and InfluencerOutreach.email_templates = {'micro': ..., 'standard': ..., 'large': ...} tier dict are all examples of the same architectural pattern. What is the central design insight they share?

Question 2: The lesson's MarketingAnalytics.get_conversion_funnel() method returns a five-stage funnel ['Awareness', 'Interest', 'Consideration', 'Purchase', 'Retention'] and computes (funnel[stages[i+1]] / funnel[stages[i]]) * 100 as the conversion rate between each pair of adjacent stages. Why does the funnel surface five stages with four edge-conversion signals rather than collapsing to a single “total reach” or “total conversion” number?

Question 3: The lesson's Marketing Timeline Gantt chart shows six tracks (Social Media 180d, Dev Blog 165d, Press Kit 90d, Trailer 30d, Influencer Outreach 45d, Launch Campaign 15d) all starting at different days and converging at the launch date. How does this marketing pipeline relate to chat-70's build pipeline (the PyInstaller .spec→executable pipeline) at the same publishing-domain stage?

What's Next?

Your game is launched and marketed! Now let's learn how to maintain and update it post-launch.