Ruby on RailsとAlexaで、めちゃんこ操作しにくいゲーム作ってみた。

　この記事では、Ruby on Railsを使用して、Alexaによる音声認識で操作することができる３×３のスライドパズル作成についてかきました。
　Ruby on Railsや、今回サーバーの実装として利用したngrokの導入方法についての説明は省いておりますのでご了承ください。
　今回、技術的説明は極力省き、実装面のみの記事としているため、主な技術要件の参考記事は以下を参照ください。

1. Alexaコンソールについて
　Alexaコンソール（Alexaスキル）では、どのような情報をサーバー(Ruby on Rails側)にPOSTするのか書き込みます。
[クリスマスプレゼントに、Amazon Echoはいかが？〜Alexaスキルを自作してみよう〜 - Qiita](https://qiita.com/tk07Sky/items/d0ce4eb99ae8fc07feee)

2. ActionCableについて
　ActionCableは、Railsに比較的新しく備え付けられた双方向通信を実装するための方法です。
[最短で作るActionCableチャットアプリ - Qiita](https://qiita.com/port-development/items/120b2bee0cef505a4005)

3. ngrokについて
　こんなの説明いらんやろ!と言われそうですが一応。。
[【3分で出来る】ngrokでデプロイをしてみよう！ - Qiita](https://qiita.com/derorian3/items/5ab961c1d5cff5f50141)

#ngrokを利用するための下準備

```#config/environments/development.rb
config.hosts << '.ngrok.io'
```
上記を追加し、ngrokによるホスティングを許可してあげればOK
#ActionCableを利用するための下準備
これは簡単です。

```bash:bash
$ rails g channel talks
```
このコマンドで、WebSocketに関するファイルが生成されます。
今回はコントローラー名も一括してtalksにしています。

```#app/channels/talks_channel.rb
class TalksChannel < ApplicationCable::Channel
  def subscribed
    stream_from "some_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak
  end
end

```
some_channelというケーブル名にしましょう。

#Railsのルーティング設定
以下を記載します。

```ruby:#config/routes.rb
Rails.application.routes.draw do
  root 'talks#index'
  resources :talks, only: [:create, :index]
end
```
　カレントディレクトリをプロジェクト内にし、以下のコマンドでルーティングを確認できます。

```bash:bash
$ rails routes
```
```bash
POST   /talks(.:format)
```
　Alexaに話しかけた内容は、エンドポイントで指定されたURLにPOSTで送られます。/talksをAlexaコンソール上でエンドポイントに設定します。

　続いて、コントローラーです。

```#app/controllers/tasks_controller.rb
class TalksController < ApplicationController
  skip_before_action :verify_authenticity_token

  # POST /talks
  def create
    request = AlexaRubykit::build_request(params)
    #paramsはAlexaから送られるJSONデータ
    request_word = request.slots[:message][:value]

    response = AlexaRubykit::Response.new
    response.add_speech("#{request.slots[:message][:value]}に移動します.")

    message = @@board.slide(request_word)
    ActionCable.server.broadcast("some_channel",message)

    render json: response.build_response
  end

  def index
    @@board = Board.new
  end
  
end

#0は空白
class Board
  def initialize
    @@panels = [
      [1,3,6],
      [0,7,2],
      [5,8,4]
    ]
  end
  #入れ替える要素の番号を引数として配列で受け取る index → [j,i]
  def swap(index1,index2)
    tmp = @@panels[index1[0]][index1[1]]
    @@panels[index1[0]][index1[1]] = @@panels[index2[0]][index2[1]]
    @@panels[index2[0]][index2[1]] = tmp
  end
  def slide(dir)
    brank_index = Array.new(2)
    @@panels.each_with_index do |panel,j|
      i = panel.find_index { |value| value == 0}
      if i != nil
        brank_index = [j,i]
        break
      end
    end
    case dir
    when '上', 'ウェイ'
      if brank_index[0] != 2
        self.swap(brank_index,[brank_index[0]+1,brank_index[1]])
      end
    when '下'
      if brank_index[0] != 0
        self.swap(brank_index,[brank_index[0]-1,brank_index[1]])
      end
    when '左'
      if brank_index[1] != 2
        self.swap(brank_index,[brank_index[0],brank_index[1]+1])
      end
    when '右'
      if brank_index[1] != 0
        self.swap(brank_index,[brank_index[0],brank_index[1]-1])
      end
    end
    @@panels
  end
end
```
```Gemfile:#Gemfileに以下を追加
gem 'alexa_rubykit', '1.3.1'
```
　一気に長いコードが出てきたので少し説明します。
　まず、Boardクラスは、初期化やAlexaからのメッセージによって盤面を移動させたりする関数(slide())から構成されています。

```ruby
request = AlexaRubykit::build_request(params)
request_word = request.slots[:message][:value]
```
　この部分では、Alexaから送られたJSONがparamsに入っており、AlexaRubyKitで受け取り、目的とする単語(今回の場合はスライドパズルなので「上」や「下」)をrequest_wordに渡します。

```ruby
response = AlexaRubykit::Response.new
    response = AlexaRubykit::Response.new
    response.add_speech("#{request.slots[:message][:value]}に移動します.")
~~
    render json: response.build_response
```

　この部分では、Alexaに応答させる言葉をadd_speechに持たせて、renderでAlexaに送っています。

```ruby
message = @@board.slide(request_word)
ActionCable.server.broadcast("some_channel",message)
```

　Alexaから受け取った言葉をslide関数に渡して、盤面を更新していきます。そして更新された盤面をsome_channelというActionCableを使ってクライアント側にブチ飛ばしていますね。

#クライアント側での処理
　先ほどブチ飛ばした内容は、talks_channel.jsで受け取ります。

```#javascript/channels/talks_channel.js
import consumer from "./consumer"

consumer.subscriptions.create("TalksChannel", {
  connected() {
    // Called when the subscription is ready for use on the server
  },

  disconnected() {
    // Called when the subscription has been terminated by the server
  },

  received(data) {
    // Called when there's incoming data on the websocket for this channel
    let txt = '';
    data.forEach((value) => {
      txt += '<div class="line">';
      value.forEach((e) => {
        if (e == 0){
          txt += '<div class="empty-box" ></div>';
        }else{
          txt += '<div class="box" ><p>' + e + '</p></div>';
        };
      });
      txt += '</div>'
    });
    document.querySelector("#display").innerHTML = txt;
    console.log(txt);
  },

  speak: function() {
    return this.perform('speak');
  }
});
```
　こんな感じで、クライアント側のjsをいじると、画面を更新することなくスライドパズルが動くというわけです。
　あとはHTMLとCSSを適当にいじって完成です。
<img width="593" alt="スクリーンショット 2020-03-06 16.13.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541141/1644f8aa-2698-5293-b3a0-df48f1e34553.png">
　なんとも春らしい見た目。
#最後に
　AlexaRubyKitってなかなか日本語のドキュメントもないしかなり苦戦したので、これからAlexaを使ってアプリケーションを作ろうとする方の手助けになればいいなと思います。
　ActionCableは筆者的に今熱いのでどんどんプロダクトに使って行きたいですね。
　これからも励んでいきますので、よろしくお願いします。:pig_nose: