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 fromTwitchIO
’scommands.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 seeLogged 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. 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. 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. 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!