├── config ├── __init__.py └── config_dist.py ├── lib ├── __init__.py ├── game.py ├── misc.py ├── bot.py └── irc.py ├── .gitignore ├── serve.py ├── timer.py ├── LICENSE └── README.md /config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | *.pyc 3 | config/config.py -------------------------------------------------------------------------------- /serve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from sys import exit 4 | from config.config import config 5 | import lib.bot as bot 6 | 7 | # Twitch Plays 8 | # Inpsired by http://twitch.tv/twitchplayspokemon 9 | # Written by Aidan Thomson - 10 | 11 | try: 12 | bot.Bot().run() 13 | except KeyboardInterrupt: 14 | exit() 15 | -------------------------------------------------------------------------------- /config/config_dist.py: -------------------------------------------------------------------------------- 1 | config = { 2 | 3 | 'irc': { 4 | 'server': 'irc.twitch.tv', 5 | 'port': 6667 6 | }, 7 | 8 | 'account': { 9 | 'username': 'username', 10 | 'password': 'oauth:' # http://twitchapps.com/tmi/ 11 | }, 12 | 13 | 'throttled_buttons': { 14 | 'start': 10 15 | }, 16 | 17 | 'misc': { 18 | 'chat_height': 13 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /timer.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | import os 4 | 5 | def clear(): 6 | os.system('cls' if os.name == 'nt' else 'clear') 7 | print('\n') 8 | 9 | days = 0 10 | hours = 0 11 | minutes = 0 12 | seconds = 0 13 | 14 | clear() 15 | 16 | while True: 17 | 18 | seconds += 1 19 | 20 | if seconds == 60: 21 | seconds = 0 22 | minutes += 1 23 | clear() 24 | 25 | if minutes == 60: 26 | minutes = 0 27 | hours += 1 28 | clear() 29 | 30 | if hours == 24: 31 | hours = 0 32 | days += 1 33 | clear() 34 | 35 | message = '\t{0:>2}d {1:>2}h {2:>2}m {3:>2}s\r'.format(days, hours, minutes, seconds) 36 | 37 | sys.stdout.write(message) 38 | sys.stdout.flush() 39 | 40 | time.sleep(1) -------------------------------------------------------------------------------- /lib/game.py: -------------------------------------------------------------------------------- 1 | import win32api 2 | import win32con 3 | import time 4 | 5 | class Game: 6 | 7 | keymap = { 8 | 'up': 0x30, 9 | 'down': 0x31, 10 | 'left': 0x32, 11 | 'right': 0x33, 12 | 'a': 0x34, 13 | 'b': 0x35, 14 | 'start': 0x36, 15 | 'select': 0x37 16 | } 17 | 18 | def get_valid_buttons(self): 19 | return [button for button in self.keymap.keys()] 20 | 21 | def is_valid_button(self, button): 22 | return button in self.keymap.keys() 23 | 24 | def button_to_key(self, button): 25 | return self.keymap[button] 26 | 27 | def push_button(self, button): 28 | win32api.keybd_event(self.button_to_key(button), 0, 0, 0) 29 | time.sleep(.15) 30 | win32api.keybd_event(self.button_to_key(button), 0, win32con.KEYEVENTF_KEYUP, 0) -------------------------------------------------------------------------------- /lib/misc.py: -------------------------------------------------------------------------------- 1 | import time 2 | from os import system 3 | 4 | def pp(message, mtype='INFO'): 5 | mtype = mtype.upper() 6 | print '[%s] [%s] %s' % (time.strftime('%H:%M:%S', time.gmtime()), mtype, message) 7 | 8 | def ppi(channel, message, username): 9 | print '[%s %s] <%s> %s' % (time.strftime('%H:%M:%S', time.gmtime()), channel, username.lower(), message) 10 | 11 | def pbot(message, channel=''): 12 | if channel: 13 | msg = '[%s %s] <%s> %s' % (time.strftime('%H:%M:%S', time.gmtime()), channel, 'BOT', message) 14 | else: 15 | msg = '[%s] <%s> %s' % (time.strftime('%H:%M:%S', time.gmtime()), 'BOT', message) 16 | 17 | print msg 18 | 19 | def pbutton(message_buffer): 20 | system('cls') 21 | print '\n\n' 22 | print '\n'.join([' {0:<12s} {1:>6s}'.format(message['username'][:12].title(), message['button'].lower()) for message in message_buffer]) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Aidan Thomson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Twitch Plays 2 | ============ 3 | 4 | Clone of [Twitch Plays Pokemon](http://twitch.tv/twitch_plays_pokemon). 5 | 6 | 7 | 8 | Installation 9 | ============ 10 | 11 | You're going to need to have [pywin32](http://sourceforge.net/projects/pywin32/) installed. If you run into any errors try running this with Python [2.7.x](http://www.python.org/download/releases/2.7/). 12 | 13 | Rename `config/config_dist.py` to `config/config.py`, and replace the username/password there with your Twitch username and [OAuth token](http://www.twitchapps.com/tmi/). Feel free to modify the start throttle here aswell. 14 | 15 | In your VBA/Emulator, set the controls to the following - 16 | 17 | ``` 18 | Up: 0 19 | Down: 1 20 | Left: 2 21 | Right: 3 22 | Button A: 4 23 | Button B: 5 24 | Start: 6 25 | Select: 7 26 | ``` 27 | 28 | After you've set that up, open up your terminal and type `python serve.py`. If your username/password is wrong you will be notified. 29 | 30 | Whilst the script is running make sure you have your emulator in focus as your primary window. If you click onto another window, the script won't work. If you're not able to stay focused on one window as you need to do other things with your computer, you could try running all of this from within a virtual machine. 31 | 32 | -- 33 | 34 | 35 | If you have any question or need help, feel free to [message me on Twitch](http://www.twitch.tv/message/compose?to=aidraj_) or send an email to `aidraj0 at gmail dot com`. 36 | 37 | You'll need to have VBA in focus for this to work, so your best bet would be to run all of this 38 | from within a VM. 39 | -------------------------------------------------------------------------------- /lib/bot.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from config.config import config 4 | from lib.irc import Irc 5 | from lib.game import Game 6 | from lib.misc import pbutton 7 | 8 | class Bot: 9 | 10 | def __init__(self): 11 | self.config = config 12 | self.irc = Irc(config) 13 | self.game = Game() 14 | self.message_buffer = [{'username': '', 'button': ''}] * self.config['misc']['chat_height'] 15 | 16 | def set_message_buffer(self, message): 17 | self.message_buffer.insert(self.config['misc']['chat_height'] - 1, message) 18 | self.message_buffer.pop(0) 19 | 20 | def run(self): 21 | throttle_timers = {button:0 for button in config['throttled_buttons'].keys()} 22 | 23 | while True: 24 | new_messages = self.irc.recv_messages(1024) 25 | 26 | if not new_messages: 27 | continue 28 | 29 | for message in new_messages: 30 | button = message['message'].lower() 31 | username = message['username'].lower() 32 | 33 | if not self.game.is_valid_button(button): 34 | continue 35 | 36 | if button in self.config['throttled_buttons']: 37 | if time.time() - throttle_timers[button] < self.config['throttled_buttons'][button]: 38 | continue 39 | 40 | throttle_timers[button] = time.time() 41 | 42 | self.set_message_buffer({'username': username, 'button': button}) 43 | pbutton(self.message_buffer) 44 | self.game.push_button(button) -------------------------------------------------------------------------------- /lib/irc.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import sys 3 | import re 4 | 5 | from lib.misc import pp, pbot 6 | 7 | class Irc: 8 | 9 | socket_retry_count = 0 10 | 11 | def __init__(self, config): 12 | self.config = config 13 | self.set_socket_object() 14 | 15 | def set_socket_object(self): 16 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 17 | self.sock = sock 18 | 19 | sock.settimeout(10) 20 | 21 | username = self.config['account']['username'].lower() 22 | password = self.config['account']['password'] 23 | 24 | server = self.config['irc']['server'] 25 | port = self.config['irc']['port'] 26 | 27 | try: 28 | sock.connect((server, port)) 29 | except: 30 | pp('Error connecting to IRC server. (%s:%i) (%i)' % (server, port, self.socket_retry_count + 1), 'error') 31 | 32 | if self.socket_retry_count < 2: 33 | self.socket_retry_count += 1 34 | return self.set_socket_object() 35 | else: 36 | sys.exit() 37 | 38 | sock.settimeout(None) 39 | 40 | sock.send('USER %s\r\n' % username) 41 | sock.send('PASS %s\r\n' % password) 42 | sock.send('NICK %s\r\n' % username) 43 | 44 | if not self.check_login_status(self.recv()): 45 | pp('Invalid login.', 'error') 46 | sys.exit() 47 | else: 48 | pp('Login successful!') 49 | 50 | sock.send('JOIN #%s\r\n' % username) 51 | pp('Joined #%s' % username) 52 | 53 | def ping(self, data): 54 | if data.startswith('PING'): 55 | self.sock.send(data.replace('PING', 'PONG')) 56 | 57 | def recv(self, amount=1024): 58 | return self.sock.recv(amount) 59 | 60 | def recv_messages(self, amount=1024): 61 | data = self.recv(amount) 62 | 63 | if not data: 64 | pbot('Lost connection, reconnecting.') 65 | return self.set_socket_object() 66 | 67 | self.ping(data) 68 | 69 | if self.check_has_message(data): 70 | return [self.parse_message(line) for line in filter(None, data.split('\r\n'))] 71 | 72 | def check_login_status(self, data): 73 | if not re.match(r'^:(testserver\.local|tmi\.twitch\.tv) NOTICE \* :Login unsuccessful\r\n$', data): return True 74 | 75 | def check_has_message(self, data): 76 | return re.match(r'^:[a-zA-Z0-9_]+\![a-zA-Z0-9_]+@[a-zA-Z0-9_]+(\.tmi\.twitch\.tv|\.testserver\.local) PRIVMSG #[a-zA-Z0-9_]+ :.+$', data) 77 | 78 | def parse_message(self, data): 79 | return { 80 | 'channel': re.findall(r'^:.+\![a-zA-Z0-9_]+@[a-zA-Z0-9_]+.+ PRIVMSG (.*?) :', data)[0], 81 | 'username': re.findall(r'^:([a-zA-Z0-9_]+)\!', data)[0], 82 | 'message': re.findall(r'PRIVMSG #[a-zA-Z0-9_]+ :(.+)', data)[0].decode('utf8') 83 | } --------------------------------------------------------------------------------