あかんわ

覚えたことをブログに書くようにすれば多少はやる気が出るかと思ったんです

恋するプログラムをSinatraでWebアプリにするPart.7[CHAPTER6 感情コントロールの魔術師]②

記事の概要

『恋するプログラム』の[CHAPTER6 感情コントロールの魔術師]を参考に、会話の中の単語によって、キャラクターの表情が変化するチャットボットを作成します。

目次

開発環境

参考記事
Rubyの開発環境構築は、こちらの記事を参考にさせていただきました。

ノビィをおだてる

会話によって変化したチャットボットの感情値を参照し、キャラクターのアニメーションの種類を変化させます。

アプリケーションディレクトリの構成
~/programinlove
    |- proto.rb                 // CUIチャットボットのメインファイル
    |- unmo.rb                  // チャットボットオブジェクトのモデル
    |- responder.rb             // 応答オブジェクトのモデル
    |- dictionary.rb            // 辞書を読み込み管理するクラス
    |- /dics                    // 辞書ファイルを格納するディレクトリ
    |     |- random.txt         // ランダムな応答を返すための辞書
    |     |- pattern.txt        // パターンに合った応答を返すための辞書
    |- noby.rb                  // Webアプリのメインファイル
    |- /views                   // ビューのテンプレートを配置するディレクトリ
    |     |- index.erb          // チャットのインターフェースを表示するビュー
    |- /public                  // 静的ファイルを配置するディレクトリ
          |- nobycanvas.js      // キャラクターの画像をアニメーションさせるJavascript
          |- styles.css         // チャットのインターフェースをレイアウトするcss
          |- /img               // 画像ファイルを格納するディレクトリ
               |- /normal       // 平常時の画像を格納するディレクトリ
               |- /blink        // 瞬きの画像を格納するディレクトリ
               |- /lookaround   // 周囲を見回す画像を格納するディレクトリ
               |- /talk         // 喋る画像を格納するディレクトリ
               ~ 以下に表情ごとの画像を格納するディレクトリを配置
ソースコード

noby.rbは、追加したヘルパーメソッドchange_looksによって、感情値noby.moodに沿った表情の情報をビューに送ります。
前回の記事までは、ヘルパーメソッド内でチャットボットオブジェクトnobyを生成していましたが、 ヘルパーメソッド内で生成したインスタンス変数はリクエストの度に生成されるらしく、感情値のような増減する値の情報を保持できないようなので、コンフィギュレーションでの生成に修正しました。

nobycanvas.jsでは、ビューのdata属性経由で取得した表情の情報をもとに、アニメーションを作成します。
画力の限界により、サンプルプログラムよりもアニメーションの種類は少なくなっています。

noby.rb
require 'sinatra'
require 'sinatra/reloader'

require_relative 'unmo'


# 会話ログを格納する配列
log_area = []

# 起動時にオプションを設定する
# 状態を保持するオブジェクトの生成に使用
configure do
  # ノビィ生成
  set :noby, Unmo.new('noby')
end

# ヘルパーメソッドを定義する
# ルーティングメソッドの中で使う
helpers do
  def noby
    # ノビィへアクセス
    noby = settings.noby
  end

  def prompt(resp_opt)
    # 応答を表示する際のプロンプトを作成
    resp_opt ? "#{noby.name}#{noby.responder_name}" : "#{noby.name}"
  end

  def change_looks
    # 感情値で表情を変化させる
    case noby.mood
    when -5..5 then 'talk'
    when -10..-5 then 'angry_talk'
    when -15..-10 then 'more_angry_talk'
    when 5..10 then 'happy_talk'
    when 10..15 then 'more_happy_talk'
    end
  end
end

# URL'/'にアクセス
get '/' do
  # 会話ログを初期化してnobyfomを表示
  log_area = []

  erb :index
end

# URL'/'にPOSTメソッドでアクセス
post '/' do
  # ユーザの入力を取得
  talk_text = params['inputarea']

  # Responderを表示するチェックボックスの状態を取得
  # チェックされてる場合は状態を維持
  resp_opt = params['respoption']
  @check = "checked" if resp_opt

  # ユーザの入力があれば応答して会話ログに表示
  unless talk_text.empty?
    @responder_resp = noby.dialogue(talk_text)
    log_area << "> #{talk_text}<br>"
    log_area << "#{prompt(resp_opt)}> #{@responder_resp}<br>"
    @noby_state = change_looks
  end

  @talk_log = log_area.join

  erb :index
end
public/nobycanvas.js
document.addEventListener('DOMContentLoaded', function(){
  /* 配列の要素番号 */
  var index = 0;
  /* キャラクターの情緒状態を確認 */
  var nobyState = document.getElementById("nobycanvas").dataset.nobystate;

  /* 喋る画像を情緒ごとに格納 */
  var talkPtn = {
    talk: [
      "img/normal/0000.png",
      "img/talk/0000.png",
      "img/talk/0001.png",
      "img/talk/0000.png",
      "img/talk/0001.png",
      "img/normal/0000.png"
    ],
    happy_talk: [
      "img/happy/0000.png",
      "img/happy_talk/0000.png",
      "img/happy_talk/0001.png",
      "img/happy_talk/0000.png",
      "img/happy_talk/0001.png",
      "img/happy/0000.png"
    ],
    more_happy_talk: [
      "img/more_happy/0000.png",
      "img/more_happy_talk/0000.png",
      "img/more_happy_talk/0001.png",
      "img/more_happy_talk/0002.png",
      "img/more_happy_talk/0000.png",
      "img/more_happy_talk/0001.png",
      "img/more_happy_talk/0002.png",
      "img/more_happy/0000.png"
    ],
    angry_talk: [
      "img/angry/0000.png",
      "img/angry_talk/0000.png",
      "img/angry_talk/0001.png",
      "img/angry_talk/0000.png",
      "img/angry_talk/0001.png",
      "img/angry/0000.png"
    ],
    more_angry_talk: [
      "img/more_angry/0000.png",
      "img/more_angry_talk/0000.png",
      "img/more_angry_talk/0001.png",
      "img/more_angry_talk/0000.png",
      "img/more_angry_talk/0001.png",
      "img/more_angry/0000.png"
    ]
  };

  /* 情緒ごとのアニメーションのパターンを作成 */
  /* 通常時のパターン */
  var normalPtn = [
    ["img/normal/0000.png"],
    [
      "img/normal/0000.png",
      "img/blink/0000.png",
      "img/blink/0001.png",
      "img/normal/0000.png"
    ],
    [
      "img/normal/0000.png",
      "img/lookaround/0000.png",
      "img/lookaround/0001.png",
      "img/lookaround/0002.png",
      "img/lookaround/0003.png",
      "img/lookaround/0004.png",
      "img/normal/0000.png"
    ]
  ];
  /* 機嫌がいい時のパターン */
  var happyPtn = [
    ["img/happy/0000.png"],
    [
      "img/happy/0000.png",
      "img/happy_blink/0000.png",
      "img/happy_blink/0001.png",
      "img/happy/0000.png"
    ]
  ];
  /* 上機嫌の時のパターン */
  var moreHappyPtn = [
    ["img/more_happy/0000.png"],
    [
      "img/more_happy/0000.png",
      "img/more_happy_blink/0000.png",
      "img/more_happy_blink/0001.png",
      "img/more_happy/0000.png"
    ]
  ];
  /* 機嫌が悪い時のパターン */
  var angryPtn = [
    ["img/angry/0000.png"],
  ];
  /* 怒っている時のパターン */
  var moreAngryPtn = [
    ["img/more_angry/0000.png"],
  ];

  /* 関数の中で画像を操作するための変数 */
  var animePtn = [];
  var imgAry = [];
  var timeoutId;

  /* 応答時のアニメーションを表示 */
  function respAnime(looks) {
    imgAry = talkPtn[looks];
    nobyState = '';
    flipAnime();
  }

  /* アニメーションのパターンからランダムに選択 */
  function selectAnime() {
    imgAry = animePtn[Math.floor(Math.random() * animePtn.length)];
  }

  /* 画像を順番に表示してアニメーションを作成 */
  function flipAnime(){
    timeoutId = setTimeout(flipAnime, 100);

    document.getElementById("nobycanvas").getElementsByTagName("img")[0].src = imgAry[index];
    index++;
    if (index >= imgAry.length){
      index = 0;
      clearTimeout(timeoutId);
    }
  }

  /* 情緒状態によって表情を変化させる */
  /* 応答時は喋るアニメーションを表示 */
  switch (nobyState) {
    case 'talk':
      respAnime('talk');
      animePtn = normalPtn;
      break;
    case 'happy_talk':
      respAnime('happy_talk');
      animePtn = happyPtn;
      break;
    case 'more_happy_talk':
      respAnime('more_happy_talk');
      animePtn = moreHappyPtn;
      break;
    case 'angry_talk':
      respAnime('angry_talk');
      animePtn = angryPtn;
      break;
    case 'more_angry_talk':
      respAnime('more_angry_talk');
      animePtn = moreAngryPtn;
      break;
    default:
      animePtn = normalPtn;
      break;
  }

  /* 待機時は適当なアニメーションを選んで表示する */
  setInterval(selectAnime, 5000);
  setInterval(flipAnime, 5000);
});

このコードのコミットには、[chapter6-6]のタグが付いてます。

実行結果

ターミナルでnoby.rbを動かし、ブラウザにhttp://localhost:4567を入力します。

~/programinlove
$ruby noby.rb

f:id:b0npu:20160107142020p:plain

おだてると、すごく笑顔になります。

f:id:b0npu:20160207175759p:plain

参考書籍

参考記事

Sinatraについては、こちらの記事を参考にさせていただきました。

関連記事

- 恋するプログラムをSinatraでWebアプリにするPart.0[はじめに]
- 恋するプログラムをSinatraでWebアプリにするPart.1[CHAPTER3 ほんとに無能]
- 恋するプログラムをSinatraでWebアプリにするPart.2[CHAPTER4 あこがれのGUI]①
- 恋するプログラムをSinatraでWebアプリにするPart.3[CHAPTER4 あこがれのGUI]②
- 恋するプログラムをSinatraでWebアプリにするPart.4[CHAPTER4 あこがれのGUI]③
- 恋するプログラムをSinatraでWebアプリにするPart.5[CHAPTER5 辞書を片手に]
- 恋するプログラムをSinatraでWebアプリにするPart.6[CHAPTER6 感情コントロールの魔術師]①
- 恋するプログラムをSinatraでWebアプリにするPart.8[CHAPTER7 学習のススメ]①
- 恋するプログラムをSinatraでWebアプリにするPart.9[CHAPTER7 学習のススメ]②
- 恋するプログラムをSinatraでWebアプリにするPart.10[CHAPTER7 学習のススメ]③
- 恋するプログラムをSinatraでWebアプリにするPart.11[CHAPTER8 文章を作り出す]
- 恋するプログラムをSinatraでWebアプリにするPart.12[CHAPTER9 ノビィ、ネットワークにつながる]
- 恋するプログラムをSinatraでWebアプリにするPart.13[おわりに]