OK Desu Ka?

[Twitch][Python] チャット欄からゲーム(キーボード)を動かす

Twitch Plays Pokémonについて

Twitch Plays Pokemon」という視聴者がチャットでゲームを操作するチャンネルがある。
2014年から活動している有名チャンネルである。活動詳細についてはWikipediaを参照。

自分のTwitchチャンネルで遊んでみる

このシステムを気軽に体験出来るシンプルなpythonのコードがGithubにて公開されている。
これを少し改良して動作確認まで行ったヤツと導入手順を記す。

2秒ごとにチャット内容を集計し、最も投票の多かったキー操作を行う内容となっている。
Wikipediaにある「民主主義モード」のコードである。
改善点の多いコードだが、簡単に解説も入れる。

導入手順

  1. pythonインストール
  2. 下記リンクより「twitchchatplay.py」をダウンロード
  3. Download

  4. 「twitchchatplay.py」をテキストファイルとして開き、コードを編集
    • 17-28行目:押されるキーの設定。
      「up」「↑」などの上判定を指すコメントがあったら「w」キーが入力されるように設定している。
      「#」以降はプログラムに影響しないメモ欄。
    • 
      keys = [
      #    Key.up,                                 # 0:UP
      #    Key.down,                               # 1:DOWN
      #    Key.left,                               # 2:LEFT
      #    Key.right,                              # 3:RIGHT
      #    Key.enter,                              # 10:START
      #    Key.shift_r,                            # 11:SELECT
          pynput.keyboard.KeyCode.from_char('w'),  # u:0:up,↑
          pynput.keyboard.KeyCode.from_char('s'),  # d:1:down,↓
          pynput.keyboard.KeyCode.from_char('a'),  # l:2:left,←
          pynput.keyboard.KeyCode.from_char('d'),  # r:3:right,→
          pynput.keyboard.KeyCode.from_char('h'),  # a:4:A
          pynput.keyboard.KeyCode.from_char('g'),  # b:5:B
          pynput.keyboard.KeyCode.from_char('t'),  # x:6:X
          pynput.keyboard.KeyCode.from_char('f'),  # y:7:Y
          pynput.keyboard.KeyCode.from_char('r'),  # l1:8:L1
          pynput.keyboard.KeyCode.from_char('y'),  # r1:9:R1
          pynput.keyboard.KeyCode.from_char('n'),  # start:10:start
          pynput.keyboard.KeyCode.from_char('v'),  # select:11:select
      ]
      
    • 39行目:自分のTwitchユーザ名を記載する。
    • 40行目:Twitchの認証で使われる「OAuthトークン」というコードを記載する。
      このリンクへ飛んで「Connect」を選択すると生成される。
      「oauth:~」と書かれている全てをコピーして貼り付けること。
    • 41行目:自分のチャンネル名を記載する。「#」を頭に付けること。
    • 
          def __init__(self):
              self.server = 'irc.chat.twitch.tv'
              self.port = 6667
              self.nickname = 'ユーザ名'
              self.token = 'oauthトークン'
              self.channel = '#チャンネル名'
      
    • 53行目:「seconds=2」という部分が投票の集計を行う間隔。2秒毎で良いと思う。
      秒数を変更する場合は150-186行目の連射処理を調整する必要があるかも。
    • 
              self.sched.add_job(self.voteCount, 'interval', seconds=2)   #2秒おきに判定
      
    • 79-80行目:連射を行うための条件。
      例ではチャット内容のどこかに「-(ハイフン・マイナス)」があったら連射するようにしている。
    • 
                      #連射フラグ設定
                      global barrage
                      barrage = 0 #初期化
                      if msgContent.find("-") >=0 or msgContent.find("-") >=0:
                          barrage = 1
      
    • 82-105行目:投票の集計を行う条件。
      「チャット内容に"○○"が含まれていたら1票」という判定を行っている。
      ダブルクォーテーションで判定条件を決め打ちしているので「sTarT」みたいに大文字・小文字が混じっていると投票されない。
      これを解消したい場合はlowerメソッドなどを利用してチャット内容を変換させる必要があるだろう。
      スタート・セレクト・L・R・十字キー・A・B・X・Yの計12ボタン分書いてある。
      それぞれの判定をどのように行うかを考える必要がある。
    • 
                      if msgContent.find("start") >=0 or msgContent.find("START") >=0 or msgContent.find("Start") >=0 or msgContent.find("すたーと") >=0 or msgContent.find("スタート") >=0 or msgContent.find("スタート") >=0:
                          self.voteDict["start"] += 1
                      elif msgContent.find("select") >=0 or msgContent.find("SELECT") >=0 or msgContent.find("Select") >=0 or msgContent.find("せれくと") >=0 or msgContent.find("セレクト") >=0 or msgContent.find("セレクト") >=0:
                          self.voteDict["select"] += 1
                      elif msgContent.find("l1") >=0 or msgContent.find("L1") >=0 or msgContent.find("える") >=0 or msgContent.find("エル") >=0 or msgContent.find("エル") >=0:
                          self.voteDict["l1"] += 1
                      elif msgContent.find("r1") >=0 or msgContent.find("R1") >=0 or msgContent.find("あーる") >=0 or msgContent.find("アール") >=0 or msgContent.find("アール") >=0:
                          self.voteDict["r1"] += 1
                      elif msgContent.find("u") >=0 or msgContent.find("U") >=0 or msgContent.find("up") >=0 or msgContent.find("UP") >=0 or msgContent.find("Up") >=0 or msgContent.find("↑") >=0 or msgContent.find("上") >=0 or msgContent.find("うえ") >=0 or msgContent.find("ウエ") >=0 or msgContent.find("ウエ") >=0:
                          self.voteDict["u"] += 1
                      elif msgContent.find("d") >=0 or msgContent.find("D") >=0 or msgContent.find("down") >=0 or msgContent.find("DOWN") >=0 or msgContent.find("Down") >=0 or msgContent.find("↓") >=0 or msgContent.find("下") >=0 or msgContent.find("した") >=0 or msgContent.find("シタ") >=0 or msgContent.find("シタ") >=0:
                          self.voteDict["d"] += 1
                      elif msgContent.find("l") >=0 or msgContent.find("L") >=0 or msgContent.find("left") >=0 or msgContent.find("LEFT") >=0 or msgContent.find("Left") >=0 or msgContent.find("←") >=0 or msgContent.find("左") >=0 or msgContent.find("ひだり") >=0 or msgContent.find("ヒダリ") >=0 or msgContent.find("ヒダリ") >=0:
                          self.voteDict["l"] += 1
                      elif msgContent.find("r") >=0 or msgContent.find("R") >=0 or msgContent.find("right") >=0 or msgContent.find("RIGHT") >=0 or msgContent.find("Right") >=0 or msgContent.find("→") >=0 or msgContent.find("右") >=0 or msgContent.find("みぎ") >=0 or msgContent.find("ミギ") >=0 or msgContent.find("ミギ") >=0:
                          self.voteDict["r"] += 1
                      elif msgContent.find("a") >=0 or msgContent.find("A") >=0 or msgContent.find("えー") >=0 or msgContent.find("エー") >=0 or msgContent.find("エー") >=0:
                          self.voteDict["a"] += 1
                      elif msgContent.find("b") >=0 or msgContent.find("B") >=0 or msgContent.find("びー") >=0 or msgContent.find("ビー") >=0 or msgContent.find("ビー") >=0:
                          self.voteDict["b"] += 1
                      elif msgContent.find("x") >=0 or msgContent.find("X") >=0 or msgContent.find("えっくす") >=0 or msgContent.find("エックス") >=0 or msgContent.find("エックス") >=0:
                          self.voteDict["x"] += 1
                      elif msgContent.find("y") >=0 or msgContent.find("Y") >=0 or msgContent.find("わい") >=0 or msgContent.find("ワイ") >=0 or msgContent.find("ワイ") >=0:
                          self.voteDict["y"] += 1
      
    • 119-142行目:投票が最も多かったキーを押すための設定。
      17-28行目のモノと紐付いている。
    • 
              if nullCheck:
                  print('入力待ち')
      
              elif voteWinner=="u":
                  idx = 0
              elif voteWinner=="d":
                  idx = 1
              elif voteWinner=="l":
                  idx = 2
              elif voteWinner=="r":
                  idx = 3
              elif voteWinner=="a":
                  idx = 4
              elif voteWinner=="b":
                  idx = 5
              elif voteWinner=="x":
                  idx = 6
              elif voteWinner=="y":
                  idx = 7
              elif voteWinner=="l1":
                  idx = 8
              elif voteWinner=="r1":
                  idx = 9
              elif voteWinner=="start":
                  idx = 10
              elif voteWinner=="select":
                  idx = 11
      
    • 144-147行目:投票があった場合、キーを押して離す動作を行う。
      押したら必ず離す処理を入れる必要がある。
      time.sleepによる待ち時間処理を挟まないと一気に処理されてしまいバグる可能性がある。
      0.1ではなく0.05でも良い。
    • 
              if idx is not None:
                  keyboard.press(keys[idx])
                  time.sleep(0.1)
                  keyboard.release(keys[idx])
      
    • 150-186行目:連射処理。
      ループ処理を書くのが面倒だったので押して離す処理をコピペペペペしている。
      2秒の間に10連射を行うように調整している。
    • 
              #連射
              if idx is not None and barrage == 1:
                  keyboard.press(keys[idx])
                  time.sleep(0.1)
                  keyboard.release(keys[idx])
                  time.sleep(0.1)
                  keyboard.press(keys[idx])
                  time.sleep(0.1)
                  keyboard.release(keys[idx])
                  time.sleep(0.1)
                  keyboard.press(keys[idx])
                  time.sleep(0.1)
                  keyboard.release(keys[idx])
                  time.sleep(0.1)
                  keyboard.press(keys[idx])
                  time.sleep(0.1)
                  keyboard.release(keys[idx])
                  time.sleep(0.1)
                  keyboard.press(keys[idx])
                  time.sleep(0.1)
                  keyboard.release(keys[idx])
                  time.sleep(0.1)
                  keyboard.press(keys[idx])
                  time.sleep(0.1)
                  keyboard.release(keys[idx])
                  time.sleep(0.1)
                  keyboard.press(keys[idx])
                  time.sleep(0.1)
                  keyboard.release(keys[idx])
                  time.sleep(0.1)
                  keyboard.press(keys[idx])
                  time.sleep(0.1)
                  keyboard.release(keys[idx])
                  time.sleep(0.1)
                  keyboard.press(keys[idx])
                  time.sleep(0.1)
                  keyboard.release(keys[idx])
                  barrage = 0 #連射フラグ初期化
      
  5. スタートメニューよりコマンドプロンプトを起動
  6. pythonの外部パッケージ「pynput」をインストール
  7. pip install pynput
  8. pythonの外部パッケージ「emoji」をインストール
  9. pip install emoji
  10. pythonの外部パッケージ「apscheduler」をインストール
  11. pip install apscheduler
  12. 「twitchchatplay.py」を実行
  13. py "C:\Users\(ユーザ名)\Desktop\twitchchatplay.py"

  14. 「snes9x」などの動かしたいエミュレータを起動し、17-28行目と紐付いたキー入力の設定を行う
  15. エミュレータよりゲームを起動
  16. Twitchチャットで動作確認
    コマンドプロンプトを閉じると終了する

その他

本当はキーではなくゲームパッドの入力をさせたかったのだが…
大変そうだったので調べるのを止めてしまった。
これを動かしている間、キーボードに触れなくなるのがデメリット。

「aaaa」「a4」とチャットがあった場合はAボタンを4回入力させたい!
といった機能を追加したい場合は条件を新たに考えて組み込む必要がある。

興味があれば自分自身でカスタマイズして使ってみてほしい。
可能性は無限大である。