I Made a Simple Twitch Plays Script in Python

 

What is Twitch Plays?

In February 2014, something magical happened on Twitch. A channel went live featuring Pokémon Red, but instead of the streamer playing the game, it was the viewers who controlled the action. By typing commands like “up,” “down,” or “a” in the chat, hundreds of thousands of viewers participated in playing the game together. The chaotic yet fascinating phenomenon was dubbed "Twitch Plays Pokemon". It became so popular that it spawned a dedicated category on Twitch, and countless “Twitch Plays” streams have followed ever since.

Inspired by this, I decided to create my own Twitch Plays script. Using Python (my favorite programming language), I took on the challenge to see if I could make my own Twitch Plays experience.

Here’s how I did it.

 

Prerequisites

Before we dive in, this tutorial assumes you have:

  • - A basic understanding of Python.
  • - Python installed, along with an IDE (such as VSCode or PyCharm).
  • - Familiarity with setting up Python environments.

 

Getting Started: Install Dependencies

The first step is to install the necessary dependencies for our Twitch Plays script:

1. pywin32 for simulating key presses on your computer. I initially tried using pyautogui, but it didn’t cooperate with my game, so I switched to pywin32:

pip install pywin32

2. TwitchIO to handle communication between the Twitch chat and the Python script:

pip install twitchio

3. dotenv to securely store sensitive information like your Twitch OAuth token and channel name in an .env file:

pip install python-dotenv

 

Getting Your Twitch OAuth Token

To make this work, you’ll need a Twitch OAuth Token for the channel you want to control. This token allows your script to interact with your Twitch account.

Note: OAuth tokens are sensitive, so be sure to keep yours safe. A quick Google search can show you how to generate one. I also recommend rotating your token periodically for security reasons.

 

Writing the Script

Now that we have our dependencies in place, let’s start coding. At its core, our script will listen for chat messages, process commands, and simulate key presses in response.

Step 1: Setting Up the Bot

Here’s a basic Twitch Plays script setup. We’ll use the TwitchIO library to connect to Twitch and listen for chat commands.

script.py

import os
from dotenv import load_dotenv
from twitchio.ext import commands
import win32api
import win32con

load_dotenv()
token = os.getenv('TWITCH_OAUTH_TOKEN')
channel = os.getenv('CHANNEL_NAME')

class Bot(commands.Bot):

    def __init__(self):
        super().__init__(token=token, prefix='!',
                         initial_channels=[channel],
                         case_insensitive=True)

    async def event_ready(self):
        print(f'Logged in as {self.nick}')

bot = Bot()
bot.run()

 

.env

TWITCH_OAUTH_TOKEN="<oauth_token>"
CHANNEL_NAME="<channel_name>"

 

Let’s break this down:

  • - We load our environment variables (OAuth token and channel name) using dotenv.
  • - We create a class Bot that inherits from TwitchIO’s commands.Bot and initialize it with the token, a command prefix (!), and the channel name.
  • - The event_ready function triggers once the bot successfully connects to Twitch. If everything is set up correctly, you should see Logged in as <your_channel_name> in the terminal.

 

Step 2: Simulating Key Presses

Now let’s add functionality to press keys based on chat commands. For this, we’ll use win32api to simulate the key presses.

script.py

import time
import os
from dotenv import load_dotenv
from twitchio.ext import commands
import win32api
import win32con

load_dotenv()
token = os.getenv('TWITCH_OAUTH_TOKEN')
channel = os.getenv('CHANNEL_NAME')

class Bot(commands.Bot):

    def __init__(self):
        super().__init__(token=token, prefix='!',
                         initial_channels=[channel],
                         case_insensitive=True)

    async def event_ready(self):
        print(f'Logged in as {self.nick}')

    async def event_message(self, message):
        # Check for command with or without "!"
        if message.content.startswith('!'):
            await self.handle_commands(message)
        else:
            # If command does not have "!" prefix, trigger anyway
            message.content = f'!{message.content}'
            await self.handle_commands(message)

    def press_key(self, key):
        key_map = {
            'l': 0x4C,      # L
            'r': 0x52,      # R
        }
        
        if key in key_map:
            vk_key = key_map[key]
            win32api.keybd_event(vk_key, 0, 0, 0)  # Key down
            time.sleep(0.1)  # Key press duration
            win32api.keybd_event(vk_key, 0, win32con.KEYEVENTF_KEYUP, 0)  # Key up
        else:
            raise ValueError(f"Invalid key: {key}")
        
    @commands.command(name='r')
    async def r_command(self, ctx):
        self.press_key('r')

    @commands.command(name='l')
    async def l_command(self, ctx):
        self.press_key('l')

bot = Bot()
bot.run()

How It Works:

  1. 1. event_message: This method is called whenever someone sends a message in chat. If the message doesn’t start with the prefix !, we add it for convenience.
  2. 2. press_key: This function maps certain keys (e.g., L, R) to their respective virtual key codes using win32api. It simulates pressing and releasing keys to interact with the game.
  3. 3. Command functions: The @commands.command decorator links the chat commands (like !l and !r) to the key press functions.

 

Step 3: Expanding Commands for Movement

Now, let's map commands for controlling the character, such as up, down, left, and right:

script.py

import time
import os
from dotenv import load_dotenv
from twitchio.ext import commands
import win32api
import win32con

load_dotenv()
token = os.getenv('TWITCH_OAUTH_TOKEN')
channel = os.getenv('CHANNEL_NAME')

class Bot(commands.Bot):

    def __init__(self):
        super().__init__(token=token, prefix='!',
                         initial_channels=[channel],
                         case_insensitive=True)

    async def event_ready(self):
        print(f'Logged in as {self.nick}')

    async def event_message(self, message):
        # Check for command with or without "!"
        if message.content.startswith('!'):
            await self.handle_commands(message)
        else:
            # If command does not have "!" prefix, trigger anyway
            message.content = f'!{message.content}'
            await self.handle_commands(message)

    def press_key(self, key):
        key_map = {
            'up': 0x57,     # W
            'down': 0x53,   # S
            'left': 0x41,   # A
            'right': 0x44,  # D
            'a': 0x58,      # X
            'b': 0x43,      # C
            'l': 0x4C,      # L
            'r': 0x52,      # R
            'select': 0xBC, # ','
            'start': 0xBE   # '.'
        }
        
        if key in key_map:
            vk_key = key_map[key]
            win32api.keybd_event(vk_key, 0, 0, 0)  # Key down
            time.sleep(0.1)  # Key press duration
            win32api.keybd_event(vk_key, 0, win32con.KEYEVENTF_KEYUP, 0)  # Key up
        else:
            raise ValueError(f"Invalid key: {key}")
        
    @commands.command(name='up')
    async def up_command(self, ctx):
        self.press_key('up')

    @commands.command(name='down')
    async def down_command(self, ctx):
        self.press_key('down')

    @commands.command(name='left')
    async def left_command(self, ctx):
        self.press_key('left')

    @commands.command(name='right')
    async def right_command(self, ctx):
        self.press_key('right')

    @commands.command(name='a')
    async def a_command(self, ctx):
        self.press_key('a')

    @commands.command(name='b')
    async def b_command(self, ctx):
        self.press_key('b')

    @commands.command(name='l')
    async def l_command(self, ctx):
        self.press_key('l')

    @commands.command(name='r')
    async def r_command(self, ctx):
        self.press_key('r')

    @commands.command(name='select')
    async def select_command(self, ctx):
        self.press_key('select')

    @commands.command(name='start')
    async def start_command(self, ctx):
        self.press_key('start')
        
    @commands.command(name='help')
    async def help_command(self, ctx):
        commands_list = """
        Available commands:  up[1-4], down[1-4], left[1-4], right[1-4], a, b, l, r, select, start   [optional]
        """
        await ctx.send(commands_list)

bot = Bot()
bot.run()

This is a fully working script. Allowing your players to play a game using the mapped keys defined by our key map. It's important to remap your keys in your game to reflect what the script expects or vice versa. 

 

Step4: Allowing Multiple Steps at Once

Moving one step at a time can be tedious, so let’s enhance the script to allow commands like up3 or left2 to move multiple steps in one command.

script.py

import time
import os
from dotenv import load_dotenv
from twitchio.ext import commands
import win32api
import win32con

load_dotenv()
token = os.getenv('TWITCH_OAUTH_TOKEN')
channel = os.getenv('CHANNEL_NAME')

class Bot(commands.Bot):

    def __init__(self):
        super().__init__(token=token, prefix='!',
                         initial_channels=[channel],
                         case_insensitive=True)
        self.create_directional_commands()

    async def event_ready(self):
        print(f'Logged in as {self.nick}')

    async def event_message(self, message):
        # Check for command with or without "!"
        if message.content.startswith('!'):
            await self.handle_commands(message)
        else:
            # If command does not have "!" prefix, trigger anyway
            message.content = f'!{message.content}'
            await self.handle_commands(message)

    def press_key(self, key):
        key_map = {
            'up': 0x57,     # W
            'down': 0x53,   # S
            'left': 0x41,   # A
            'right': 0x44,  # D
            'a': 0x58,      # X
            'b': 0x43,      # C
            'l': 0x4C,      # L
            'r': 0x52,      # R
            'select': 0xBC, # ','
            'start': 0xBE   # '.'
        }
        
        if key in key_map:
            vk_key = key_map[key]
            win32api.keybd_event(vk_key, 0, 0, 0)  # Key down
            time.sleep(0.1)  # Key press duration
            win32api.keybd_event(vk_key, 0, win32con.KEYEVENTF_KEYUP, 0)  # Key up
        else:
            raise ValueError(f"Invalid key: {key}")
        
    def repeat_key_press(self, key, times):
        try:
            for _ in range(times):
                self.press_key(key)
                time.sleep(0.1)
        except ValueError as e:
            print(e)

    def create_directional_commands(self):
        directions = ['up', 'down', 'left', 'right']
        for direction in directions:
            command_name = f'{direction}'
            self.create_command(command_name, direction, 1)
            # Allow commands of <direction>[number] from 1-4
            for i in range(1, 5):
                command_name = f'{direction}{i}'
                self.create_command(command_name, direction, i)

    def create_command(self, command_name, direction, repeat_count):
        async def command_func(ctx):
            self.repeat_key_press(direction, repeat_count)

        command_func.__name__ = f'{command_name}_command'
        self.add_command(commands.Command(name=command_name, func=command_func))
   
    # Non-directional commands
    @commands.command(name='a')
    async def a_command(self, ctx):
        self.press_key('a')

    @commands.command(name='b')
    async def b_command(self, ctx):
        self.press_key('b')

    @commands.command(name='l')
    async def l_command(self, ctx):
        self.press_key('l')

    @commands.command(name='r')
    async def r_command(self, ctx):
        self.press_key('r')

    @commands.command(name='select')
    async def select_command(self, ctx):
        self.press_key('select')

    @commands.command(name='start')
    async def start_command(self, ctx):
        self.press_key('start')
        
    @commands.command(name='help')
    async def help_command(self, ctx):
        commands_list = """
        Available commands:  up[1-4], down[1-4], left[1-4], right[1-4], a, b, l, r, select, start   [optional]
        """
        await ctx.send(commands_list)

bot = Bot()
bot.run()

This script dynamically creates commands like up2 or right3 without needing to define them manually. It’s efficient and scales well with more complex input scenarios.

 

Run Your Script

You can test your script by simply running it, tabbing in to your game window, and typing chat commands (even when you are not streaming) to your Twitch channel. Pretty cool right?

 

Closing Thoughts

Creating my own Twitch Plays script was an enjoyable and much more straightforward experience than I thought it would be. TwitchIO abstracts out a lot of the functionality involved in the creation of the original script such as actually connecting your Twitch chat to commands being input into the script itself. 

There are a few obvious limitations to this script. First, if you want to give players full control over the game, they can only input very simple commands and it makes the game extremely slow to play. Second, if you are not modded on the Twitch channel, you cannot send the same command twice in a row. Twitch simply doesn't allow duplicate messages sent back to back. Third, having to have the script running and tabbing in to the game means that this script does not allow you to run it in the background. I hope to solve this issue at some point by searching for the window and simulating the key press without interrupting any other tasks I am performing. 

Feel free to use this script for yourself or play around and use a variation of it. If you end up using it for livestreaming, I would appreciate a shout out.

Happy programming!