【Twitter】ランパの台詞botを作成してみる その3

というわけで、今回は「ランパの台詞bot」制作その3です!
少しずつ作っていた台詞集ですが、ゲーム開始からエンディングまでの台詞を集め終わりました!

前回同様、改良点や苦労した点を載せていきます。
ちょっと長めの記事ですが、今回でほぼ完成までいけた・・・かと思います!

前回の記事は↓になります!

【Twitter】ランパの台詞botを作成してみる その2

ツイートした台詞を一定期間再ツイートしないようにする

かなりレパートリーは豊富になりましたが、それでも短期間に連続してツイートしてしまうことがあります。
以下は1月19日のツイートです。

種類が豊富なのに短期間で同じ台詞を言わせるのはいただけないです。
そんなわけで、1回ツイートした台詞は少し期間を空け、連続でツイートしないようにします。

これを実現するにはサーバ側でデータを取っておくか、過去のツイートを取得して判定するしかありみあせん。
幸い、WordPress 用に MySQL が入っているので、それを使ってサーバ側で判定してみます。
当初「台詞をデータベースで管理する」ことは想定していなかったので、台詞ごとにシーケンシャルなIDなんていいものはありません。
当然ながら一意に識別できるコードなんてものもありません。

そこで使用するのがハッシュ値を計算する md5() です!
これで台詞をハッシュ値に変換し、識別しやすい形に置き換えて保存します。
普通に台詞を格納するのも手ですが、最大130文字近くになる台詞がある上、マルチバイト文字をそのまま入れて比較・・・というのは避けたかったためです。

下準備として、データベースとテーブルを作成しておきます。
端末上から直接作ってしまいましょう。

CREATE DATABASE twitter_bot;

use twitter_bot;

CREATE TABLE rampa_tweet_history (
    id INT PRIMARY KEY AUTO_INCREMENT NOT NULL,
    message TEXT NOT NULL,
    create_date DATETIME NOT NULL
);

データベース名やテーブル名は適当です。
id は不要かもしれませんが、一応作っておきます。
create_date は一定期間以上経過した台詞を弾くために必要です。

前回作成したPHPファイルを修正していきます。
それなりに長くなりましたが、以下がソースになります!

<?php

require_once("twitteroauth-master/autoload.php");
require_once('twitteroauth-master/src/TwitterOAuth.php');

use Abraham\TwitterOAuth\TwitterOAuth;

/**
 * ランパの台詞集から台詞を抽出・ツイートするクラス
 */
class TwitterRampaBot {

    private $customer_key;
    private $customer_secret;
    private $access_token;
    private $access_token_secret;

    private $db_user = 'db_user';
    private $db_password = 'db_password';
    private $interval_spec = 'P3D';

    const file_path = 'rampa_serifu.txt';
    const max_tweet_len = 140;

    /**
     * コンストラクタ
     */
    public function __construct()
    {
        $this->customer_key = "customer_key";
        $this->customer_secret = "customer_secret";
        $this->access_token = "access_token";
        $this->access_token_secret = "access_token_secret";

        mb_internal_encoding('UTF-8');
    }

    /**
     * つぶやきを送信する
     */
    public function tweet()
    {
        $message = $this->getTweetWord();

        // 文字数チェック
        if (self::max_tweet_len < mb_strlen($message)) {
            trigger_error('ツイート可能な文字数を超えています。', E_USER_ERROR);
            exit;
        }

        // サーバにつぶやきを送信
        $connection = new TwitterOAuth($this->customer_key, $this->customer_secret, $this->access_token, $this->access_token_secret);
        $connection->post("statuses/update", array("status" => $message));
    }

    /**
     * ツイート用の文字列を取得・整形する
     *
     * @return string
     */
    private function getTweetWord()
    {
        // テキストファイルから文字列読み込み
        $input_lines = file(self::file_path);
        $words = array();

        // データベース接続
        try {
            $dsn = 'mysql:host=localhost;dbname=twitter_bot;charset=utf8';
            $pdo = new PDO($dsn, $this->db_user, $this->db_password);
        } catch (\Exception $e) {
            trigger_error('データベースへの接続に失敗しました。', E_USER_ERROR);
            exit;
        }

        // 先頭 # の行を取得するまでループ
        while (true) {
            $line_num = rand(0, count($input_lines));

            // 先頭が # で始まる行を抽出
            if (isset($input_lines[$line_num]) && preg_match('/^\# .*$/', $input_lines[$line_num])) {
                $words = array();

                // # 以降を場面として取得
                $scene = $this->excludeLineFeed($input_lines[$line_num]);
                $scene = str_replace('# ', '', $scene);

                // 複数行の台詞を全て取得するまでループ
                for ($i = $line_num + 1; ; $i++) {
                    // 文字列か空or改行コードのみの場合は終了
                    if (empty($input_lines[$i]) || preg_match('/^' . PHP_EOL . '$/', $input_lines[$i])) {
                        break;
                    }

                    $words[] = $this->excludeLineFeed($input_lines[$i]);
                }

                // 台詞と発言場面を設定
                $message = '';
                foreach ($words as $word) {
                    $message .= $word . PHP_EOL;
                }
                $message .= '【' . $scene . '】';

                // 台詞が一定期間内にツイートされているかチェック
                if (!$this->checkMessage($pdo, $message)) {
                    continue;
                }

                // ツイートする台詞を保存
                $this->insertMessage($pdo, $message);

                break;
            }
        }

        $pdo = null;

        return $message;
    }

    /**
     * 台詞がデータベース内に保存されているかチェックする
     *
     * @param PDO $pdo
     * @param string $message
     */
    private function checkMessage(PDO $pdo, $message)
    {
        $sql = 'SELECT count(id) FROM rampa_tweet_history WHERE message = :message AND create_date > :create_date';
        $date = new DateTime();
        $date->sub(new DateInterval($this->interval_spec));

        $pdo_stat = $pdo->prepare($sql);
        $pdo_stat->bindParam(':message', md5($message));
        $pdo_stat->bindParam(':create_date', $date->format('Y-m-d H:i:s'));
        $pdo_stat->execute();

        // ツイートされていた場合は台詞を再取得
        if (0 < $pdo_stat->fetchColumn()) {
            return false;
        }

        return true;
    }

    /**
     * 台詞をデータベースへ挿入する
     *
     * @param PDO $pdo
     * @param string $message
     */
    private function insertMessage(PDO $pdo, $message)
    {
        $sql = 'INSERT INTO rampa_tweet_history (message, create_date) VALUES (:message, :create_date)';
        $date = new DateTime();

        $pdo_stat = $pdo->prepare($sql);
        $pdo_stat->bindParam(':message', md5($message));
        $pdo_stat->bindParam(':create_date', $date->format('Y-m-d H:i:s'));
        $pdo_stat->execute();
    }

    /**
     * 指定した文字列から改行コードを除外する
     *
     * @param string $str
     * @return string
     */
    private function excludeLineFeed($str)
    {
        return str_replace(PHP_EOL, '', $str);
    }
}

$twitterRampaBot = new TwitterRampaBot();
$twitterRampaBot->tweet();

exit;

?>

※ツイートの取得や送信部分は前回の記事をご参照下さい。

MySQL への接続には PDO_MySQL を使用しました。
明確な理由はありませんが、ガチガチに書くわけでもないので、PDO が手っ取り早いかなーと。
仕事ではDB関連はフレームワークに任せてしまうので、別の意味で苦戦していたりもします。

ポイントは checkMessage() 内の処理です。
一定期間は interval_spec で定義し、P3D を設定しておきます。
これを DateTime::sub() で指定することで、「現在時間から3日前の日時」になります。
このあたりの指定方法は公式マニュアルに書いてあります。
http://php.net/manual/ja/dateinterval.construct.php

「データベース内に台詞が存在するか」だけ欲しいので、実行する SQL は「SELECT COUNT(id)」です。
execute() 実行後、fetchColumn() を実行することでレコード数を取得することができます。
あとは if で評価して真偽を返し、false であれば continue で再度台詞を取得します。true であれば台詞をハッシュ化してデータベースに保存します。

作りたての状態で実行すると、データベース接続が出来なかったり、日付型の比較でしくじったりするので、事前にテストを行うことをおすすめします。
端末上から直接PHPファイルを指定して実行すればOKです。
その場合、「サーバにつぶやきを送信」の部分をコメントアウトし、echo や var_dump() を使用しましょう。
テスト用として別ファイルで作ってしまうと安全です。

乱数の生成方法の変更

そもそもPHPの rand() はいろいろと問題があるようで、これを使用していること自体がまずいみたいです。
より良い乱数生成と数倍の速度を持つという mt_rand() で置き換えます。
使い方は変わらないようなので、本当に置き換えるだけ!

$line_num = mt_rand(0, count($input_lines));

「より良い乱数」になったかどうかはまだ分かりません。
ただ rand() では特定の台詞のみ偏って選出されたりしたので、そのあたりの改善が期待できる・・・かもしれません。

上で記載した「一定期間内にツイートした台詞は除外」機能と合わせれば、少なくとも同じ台詞が続いてしまうことは避けらます。
データベースやPHPのモジュールにもよりますが、気になっている方は試してみてはいかがでしょうか。

苦労したこと

前回に引き続き、bot製作に関して苦労したことを紹介します。

抜けが無く収集できているか心配になる

抜けがないように収集したつもりですが、やはり不安な部分もあります。
例えばランパの変身が可能なエリアで、且つ該当の変身をまだ取得していない場合に特殊な台詞があります。

今のボクのチカラじゃここではへんしんできないみたいだ・・・
ぼうけんしてフシギな石を見つけたら、またへんしんできるようになるかも・・・?
気になるところがあるかもしれないけれど、ひとまず今は先にすすんで、あとでここにもどってみようよ

今のボクのチカラじゃここではへんしんできないみたいだ・・・
ぼうけんしてフシギな石を見つけたら、またへんしんできるようになるかも・・・?
ひとまず今はちがうみちをすすんでみて・・・あとでここにもどってみようよ、スタフィー

この際、場所によって微妙に台詞が違ったりします。
自分は後半で気付きましたが、もしかしたら前半の隠しエリアなどにも設定されているかもしれません。
その場合、ストーリーを最初からやり直すことになりかねません。

手動で収集・入力することに限界を感じる瞬間ですが、メインとなるストーリー上のイベントはすべて搭載しているので、台詞botとしては妥協ラインかなーとも思います。
裏を返せばそれだけの台詞パターンがある位、ゲーム自体の作りが丁寧ということですね。
攻略本で変身するエリアだけを確認する等、漏れがないような施策を考えていきたいところです。

かなり長い台詞がある

例として、以下はステージ8-3後半の会話です。
途中キョロスケが1~2回話す以外、ほぼランパのみが喋り続けます。

す、スタフィー・・・なかまが・・・なかまが・・・

みんなっ、しっかりっ!!へんじをしてーーー!!
ダメだ・・・みんなチカラをダイールにすいとられてうごけないみたい・・・

ダイール・・・何てことを・・・
ゆるさない!ゆるさないぞーぉおお!!!
何があっても・・・みんなをもとの元気なすがたにもどすんだ!!!

ボクをダイールからまもるために、みんなはぎせいになった・・・
みんなは・・・なかまは・・・かぞくどうぜんなんだ
ボクには・・・パパもママもいないから・・・みんながかぞくなんだ!

・・・うん、ボクが生まれてすぐ・・・パパもママもしんじゃったんだって
すごく小さかったからよくわからなかったけど
さみしくないようにって、みんながやさしくしてくれた

ボク、こうやってスタフィーたちとであえてわかったんだ
スタフィーも王子、ボクも王子、おなじ王子なのに、スタフィーはいつもボクをたすけてくれた
このままじゃいけないんだ!スタフィーのように、強くやさしく・・・みんなをまもれる王子にならなきゃ!

ねぇ、スタフィー ボクやるよ!
ダイールをたおして・・・なかま・・・かぞくをとりもどすんだ!!!
ぜったいこのままでおわらせるわけにはいかない!さあ!行こう!

台詞自体はランパの過去や成長、決意が見て取れる大変よろしいものです。
ただし、bot的には「ツイート可能な文字数は140まで」という制限に引っ掛かります。
仕方ないので文としての意味合いが切れない程度に分けていますが、それでも140文字以内に収まるか怪しくなる台詞もあります。
上の例で言えば、「ボク、こうやってスタフィーたちと~」から3行で129文字です。

この例に限らず、稀に非常に長い台詞があります。(7-4など)
こればかりはどうしようもないので、やはり文としての意味合いを保ちつつ分けています。
一部強引な分け方をしていますが、「こんな台詞がある」というbotであり、所謂「名言集」ではないので・・・と言い訳しておきます。

余談ですが、その割にはエンディングは超あっさりで、ダイール撃破後にランパが発した明確な台詞は以下の2つのみです。

スタフィー!だいじょうぶ?

スタフィー!ほんとうにありがとう
またあそびにきてね

後者はスタッフロール後の一番最後の台詞で、「また遊んでね!」的な意味合いが含まれていると思われるため、実質1つのみです。
まあエンディングなのでナレーションベースということもありますが、ちょっと驚きました。

「ランパのあつめもの」の存在を忘れる

キョロスケ様のかばんメニューに「あつめもの」があります。
このメニュー、ストーリーの進行具合に応じてランパのコメントがあります。

DSC_0306

が、このメニュー自体の存在をステージ5まで忘れていました。
更新タイミングは(たぶん)ステージクリア時とエンディング後なので、ステージ5からメモっていけばやり直す範囲も抑えられますが、完全にノーマークだったので精神的ダメージが大きかったです。

ちなみに「今日のゲスト」の各台詞と、クリア後にあつめものを見た際の「ランパのにちじょう」シリーズはすべて回収しました。

DSC_0326

ただし、「ランパのにちじょう (海)」だけ異常に出現率が低く逃しそうになりました。
内容は上画像の通りですが、さらっとすごいことを言っているので意図的に低確率なのかもしれません。
隕石を弾き返すほどの回転をランパが習得できるとは思えませんが・・・。

「キョロスケ様のキャラ診断」なるフラッシュがある

公式サイトに「キョロスケ様のキャラ診断」なるものがあります。
公式自体見たことがなかったので、海外のフォーラムで初めて知ったりもします。

現在でも公式サイトにリンクがありますが、画面下部にひっそりと表示されているだけです。
気合でランパっぽくなるような選択肢を選びます。

starfy_sindan_rampa2

やはりというか、キャラクター毎のコメントがあります。
ゲーム外の台詞ですが、「ゲーム内の」と限定して作っているわけでもないので、とりあえず台詞集に追加しておきました。
ちなみにハマグリ曰く、「真面目で素直な優等生タイプ」らしいです。

まとめ

そんなわけで、ランパの台詞bot作成(ほぼ)完了です!
プレイしていると、キャラクターやストーリーが懐かしく感じて、意外とハマってしまったりします。

現在の台詞パターンは193種類です!
1アクションゲームとしてみると相当な量ではないでしょうか。
あとは上述した「あつめもの」をステージ1~4分入れれば完成かと思います!

PHPカンファレンス2016 (関東) に行ってきました!

というわけで、やや久々の行ってきましたシリーズです。
今日はプログラマっぽく、PHPカンファレンス2016に行ってきました!
こういった技術系のイベントは行ったことがありませんでしたが、今後ITな生活を送る上では重要かと思い、申し込んでみました!

山梨発で、朝10時までに大田区産業プラザに到着する必要があります。
いつも通り高速バスで行きますが、身延線だと間に合わないため、竜王駅から出ている便で向かいました。

cimg2100_r

京急蒲田駅から産業プラザまでは徒歩5分ほどです。
大きな建物なので分かりやすかったです。

cimg2102_r

1Fにセッション用スペースとスポンサーのブースがありました。
セッションの合間に軽く回ってみました。

cimg2103_r

PHP技術者認定試験のブースです。
最近受けて準上級になったので印象深いです。

cimg2104_r

↑のうまい棒です。
ペチゾーが欲しいところでしたが、くじはハズレでした。
そんな運命力はなかったようです。
合格体験記のものと同一らしいので、書いて送って獲得したほうが楽ですね。

cimg2106_r

レバテックのブースにあった問題です。
パッと見ではかなり厳しい…というかusort()がある時点で半分諦めていたりします。
その場で答えられた人はなかなかのPHPerかと思われます。

セッションやLTの内容は割愛しますが、「ほほう!」というものもあれば、「ふーん…」というものもありました。
概念すら知らなかった技術や言語があったので、そういう意味では有意義でした。
ただ一口にPHPといっても、フレームワークからツールまで幅広いので、全てを把握するのは困難ですね。

cimg2108_r

上は最も印象に残った Cygames のセッションの様子です。
ホールに入りきらず、自分も外側に立ってみていたような状態です。
運営側の工夫やサーバ構成などが聞けたので面白かったです。

そんなわけで、短めですがPHPカンファンレス2016にお邪魔してきました!
今後もこういった技術系のセミナーやイベントに顔を出していきたいところです。

【PHP】Symfony2のFormBuilderにデフォルト値を設定する方法

※投稿時の Symfony のバージョンは 2.7.9 、EC-CUBE のバージョンは3.0.10です。

というわけで、今回は PHP のフレームワーク Symfony2 に関するメモです。
仕事で使っている関係上いろいろと詰まることがあり、今日もちょっとしたことで詰まってしまいました。
主に「EC-CUBE3」で使っているので、それを元にして書いておきます。

shot2ss20160714205855672

やりたかったことは「FromBuilder の時にデフォルト値を設定する」です。
(プレースホルダとは別で、画面表示時からフォームに値が設定されている状態です)
普通なら getForm() した後に setData() すれば問題ないのですが、EC-CUBE のフックポイント的に FormBuilder の状態でいじれると便利だったりします。

テストとして、会員登録画面の「生年月日」欄の年をデフォルトで「2000」にしようと思います。
そこでこんなコードを書きましたが、フォームにデフォルト値として設定されません。

$Customer = $app['eccube.repository.customer']->newCustomer();

$builder = $app['form.factory']->createBuilder('entry', $Customer);
$builder->get('birth')->get('year')->setData(2000);

ぐぐったらスタックオーバーフローにそんな話題がありました。

http://stackoverflow.com/questions/18870866/symfony-2-3-form-getdata-doesnt-work-in-subforms-collections

setDataLocked() なる関数でロックしてあげないと getForm() 時に消える(?)らしいです。
ならこれでロックすればOKですね。

$Customer = $app['eccube.repository.customer']->newCustomer();

$builder = $app['form.factory']->createBuilder('entry', $Customer);
$builder->get('birth')->get('year')->setData(2000)->setDataLocked(true);

shot2ss20160714205839398

今度はデフォルト値としてしっかり入りました!
値を変えて確認画面に行ってもちゃんと変わったので、大丈夫かと思われます。

ちなみに getForm() 後であれば普通に $form->get(‘フィールド名’)->setData() で設定できます。
但し handleRequest() してしまうと変えられなくなります。
また、「プラグインからフックして設定する」などの理由がなければ Type クラスの $buider->add() のオプションで指定しまう方が確実です。

まとめ

そんなわけで、Symfony2 の setData() に関するお話でした。
Symfony2 は初見ではとっつきにくい割に日本語サイトが少ないので厳しいです。
EC-CUBE3 のフレームワークとしても使われているので、もっと日本語サイトも増えてくれるといいなーなんて思います。

ゲーム開発とは関係ありませんが、今後も何か詰まったことがあったらメモして忘れないようにしたいです。
特に PHP は一生付き合っていく言語になりそうなので、尚更ですね。
そもそもゲーム開発だけでは話題が続かなくなってきているので、いろいろ話題を織り交ぜていくブログに方向転換しようかと思います。

カテゴリー PHP