もちっとメモ

もちっとメモ

もぐりのエンジニアが日々の中で試してみたことを気が向いたときに書き連ねていきます

Herokuでpython+mecab+ffmpegを使う

はじめに

最近、ちまちまとdiscord上で動くchatbotを作っているのですが、やっぱり公開サーバーで動かさないと日常的に使えないのでHerokuにデプロイすることにしました。 ただ、無料枠のHerokuで動かそうとするといろいろと解決しないといけない課題が多くてひたすら逃げていたのですが、ようやく重い腰を上げてデプロイしたので、手順をメモがてら残してみたいと思います。

必要な要件

を使っているのでこれらが動かせる環境が必要。

Herokuでpython+mecabの環境づくりは下記の記事を参考にしました。 herokuでpython+django+scikit-learn+mecab(1) ffmpegの環境づくりはこちらを参考にしました。 DiscordBotでyoutubeの音声をボイスチャットに流す

いざ、構築

condaとherokuの複数のビルドパックを使うのでheroku-buildpack-multiでアプリを作成する必要があります。気になるのはこのリポジトリがメンテナンスを終了していることでしょうか。

This buildpack is no longer actively maintained. The associated functionality exists natively on the Heroku platform. Please refer to https://devcenter.heroku.com/articles/buildpacks and https://devcenter.heroku.com/articles/using-multiple-buildpacks-for-an-app for documentation.

まず、ローカルでリポジトリを作ります。

$ git init
$ heroku create --buildpack https://github.com/heroku/heroku-buildpack-multi

使用するビルドパックは以下の通りです。

https://github.com/Sashimimochi/conda-buildpack.git
https://github.com/sunny4381/heroku-buildpack-linuxbrew.git
https://github.com/jonathanong/heroku-buildpack-ffmpeg-latest.git
https://github.com/Crazycatz00/heroku-buildpack-libopus.git

scikit-learnなどのCコンパイラを必要とするライブラリも使いたいのでcondaのビルドパックがいるのですが、pythonのバージョンを固定したいので以下のリポジトリをforkして自前で用意することにしました。 https://elements.heroku.com/buildpacks/teamupstart/conda-buildpack minicondaのバージョン一覧はこちらから見れます。 https://repo.continuum.io/miniconda/

ちなみに、ものによっては、以下のような最近のpipでは廃止されたオプションが付いていてビルドエラーを起こすので注意。

pip install -r requirements.txt  --exists-action=w --allow-all-external | indent

上記の.buildpacksheroku-buildpack-linuxbrewを入れたのでbrewが使えるようになっています。brewで入れたいパッケージを.cellarで指定します。

mecab
mecab-ipadic
open-jtalk

condaおよびpipでインストールするライブラリはconda-requirements.txtrequirements.txtに書いておきます。

Herokuにmecab-pythonをインストールしようとすると案の定、エラーが出ました。 herokuでpython+django+scikit-learn+mecab(1)では手動でインストールしていましたが、今回は、python3系を使うわけですし、mecab-python3ならはいったので、代わりにこちらを入れます。 Heroku mecabインストール時にエラー

あとはパスを指定してデプロイします。

$ heroku config:add LD_LIBRARY_PATH=/app/.linuxbrew/lib
$ heroku config:set MECAB_PATH=/app/.linuxbrew/lib/libmecab.so
$ git add .
$ git commit -m 'initial'
$ git push heroku master

滞りなく、一連のデプロイが進んで無事うまくいったように思えたのですが、最後のメッセージであえなく撃沈。

-----> Compressing...
 !     Compiled slug size: 3009.8M is too large (max is 500M).
 !     See: http://devcenter.heroku.com/articles/slug-size
 !     Push failed

https://devcenter.heroku.com/articles/slug-compiler#slug-size 上記のメッセージにもある通り、Herokuの無料枠のストレージサイズが500MBなのでデプロイ時にこの範囲内に収める必要があります。

容量を食っているのが、機械学習モデルだったので、Herokuでこれを動かすにはモデルのサイズを圧縮しておく必要がありました。 中でも一番のファイルサイズが大きいのがWord2Vecだったので、こちらのライブラリを使ってモデルサイズを圧縮しました。 50,000語程度にまで絞れば約60MBまで圧縮できます。 https://github.com/yagays/minify_w2v 重要語は以下から選択しました。 日本語を読むための語彙データベース Ver. 1.1 その他にもcacheを削除するなどいろいろ工夫の余地はありそうです。

これでリポジトリの容量をかなり圧縮できました。

大量のライブラリを使ってしまったせいで、これでもデプロイ時にpip installまでしてしまうと上限の500MBを超えてしまったので、起動時にpip installが走るようにしておきました。

bot: bash run.sh
pip install -r requirements.txt
python app.py

以上で、なんとか無事に動きました。

トラブルシューティング

Herokuリポジトリにログイン

Herokuにログインしてマニュアル操作するときは以下のようにすればログインしてbashコマンドが使えます。

heroku run bash

ログの確認

デプロイ後にアプリケーションのログが見たければ以下のコマンドで確認できます。

$ heroku logs

Herokuアプリケーションのログについて

やっぱりはてなブログを自分のホームページに埋め込みたい

※2021/1/18に複数のサイトからRSSを取得したい場合のやり方を末尾に追記しました。

経緯

長年お世話になっていたYQLが残念ながら2019年1月に無期限のサービス停止を発表されました。Yahooさん、今までありがとうございました。

したがって、再び新しい方法を考える必要が出てきました。
私情ではありますが、最近ようやくReactを勉強し始めましたので、今回はReactを使ったRSSフィードの取得・表示方法を考えてみました。
着手当初はどうしようと戦々恐々だったのですが、比較的簡単に行けそうだったのでご紹介してみます。
やり方さえ分かれば、30分もかからずにできてしまうと思います。(私は調べる過程を含めると半日くらいかかりました。)
※Reactのチュートリアルすらまともに理解しているか怪しい初心者がトライ&エラーで考えた方法なので、ベテランの方からすれば滅茶苦茶な実装になっているかもしれません。ご容赦ください。

とりあえずRSS情報を取得して表示する

何はともあれ、まずは完成形をお見せします。無事、ブログのタイトルとリンクを取得して飛べるようになりました。
見てくれ部分は私のUIセンスの問題なので、これなんかよりいくらでも格好よくできると思うので、ぜひ各々アレンジしてみてください。
(というか、格好いいデザインができたら教えてください)

f:id:T-N-Clark:20190530004403g:plain
完成図

環境は

$ create-react-app -V
2.1.1

を使っています。 で、肝心の実装ですが、あれこれ調べてみたところ、手っ取り早く実現できそうなものにRSSParserというライブラリがありました。
ということで今回は、このRSSParserを使います。 www.npmjs.com

ライブラリのインストールは

npm i rss-parser

とかで出来ます。

ひとまず、RSS情報を取ってきてコンソールに表示させてみます。
取り急ぎ動作確認したいならcreate-react-appでプロジェクト?を作ってApp.jsに書き込めば動作確認は出来ます。

import React from 'react';
//中略
const RssParser = require('rss-parser'); //importではなくrequireで呼びます
const url = 'http://t-n-clark.hatenadiary.jp/feed'
const rssParser = new RssParser();
rssParser.parseURL(url)
      .then((feed) => {
        console.log(feed);
      });
      .catch((error) => {
        console.error('Get Failed', error);
      });
class App extends Component {
  render(props) {
    const classes = this.props.classes;
//...後略

Chromeデベロッパーツールで確認すると、ちゃんとコンソールに取得したRSS情報が出ています。

f:id:T-N-Clark:20190530004713j:plainf:id:T-N-Clark:20190530004716j:plainf:id:T-N-Clark:20190530004723j:plain

ざっと見たところ、記事のタイトルとかの情報はこのitemsに入っているみたいです。

ちなみに、はてなブログRSS情報は

各ブログのトップURL/rss

もしくは

各ブログのトップ/feed

で取得出来ます。 例えば、このブログのRSS
https://t-n-clark.hatenadiary.jp/feed
で取れます。 ※HTTPでも取れそうな気がするのですが、HTTPだとスマホではちゃんと取れなかった表示されなかったので、HTTPSをおすすめします。 (端末ではなく、ブラウザの問題かもしれませんが... これが関係するのでしょうか?) bookmark.hatenastaff.com

Qiitaであれば、

https://qiita.com/アカウント名/feed.atom

で取得出来ます。 例えば、こんな感じ
https://qiita.com/Sashimimochi/feed.atom
ただ、HTTPSの場合はクロスドメイン制約があるので、URLの前にCORS(cross-origin resource sharing)PROXYをつけてあげる必要があります。
上記のRSSParserのページを読んだところ、こんな感じで行けそうです。

const CORS_PROXY = "https://cors-anywhere.herokuapp.com/"
//はてなブログならこっち
const url = CORS_PROXY + 'https://t-n-clark.hatenadiary.jp/feed'
//Qiitaならこっち
const url = CORS_PROXY + 'https://qiita.com/Sashimimochi/feed.atom'

あとは取得した情報をstateに格納してJSX内で呼び出してみます。 先ほどデバッグ用に出していたConsoleへの出力はもういらないので消しても大丈夫です。

import React from 'react';
import { Component } from 'react';
import PropTypes from 'prop-types';
import './App.css';

/*RSSParserのインスタンス生成*/
const RssParser = require('rss-parser');
const url = "http://t-n-clark.hatenadiary.jp/feed"
const rssParser = new RssParser();

class App extends Component {
  /*コンストラクターの定義*/
  constructor(props) {
    super(props);
    this.state = {
      contents: [],
    };
    this.componentDidMount = this.componentDidMount.bind(this)
  }
  /*stateにrssParserの結果をバインド*/
  componentDidMount() {
    rssParser.parseURL(url)
      .then((feed) => {
        const data = feed.items;
        console.log(data);
        this.setState({
          contents: [...data]
        });
      })
      .catch((error) => {
        console.error('Get Failed', error);
      })
  }
  render(props) {
    const classes = this.props.classes;
    /*表示するコンテンツの形に合わせてstateの中身を書きだす(mapで拡張forループみたいに使う)*/
    const contents = this.state.contents.map(content => {
      return <div>
        {content.title}
        /*{content.pubDate}  公開日(Qiitaなら{content.published})*/
       /*{content.link}  ページリンク*/
      </div>
    });
    return (
      <div className="App">
        <header className="App-header">
          {contents} /*ここに出力*/
        </header>
      </div>
    );
  }
}

App.propTypes = {
  classes: PropTypes.object.isRequired,
};

export default App;

これで無事RSS情報を取得して表示できるようになりました。

f:id:T-N-Clark:20190530004740j:plain

あとは、これを自分なりに装飾していくことになります。

見てくれを整える

参考になるかはわかりませんが、冒頭にあった私なりの装飾を紹介してみます。 主にMaterial-Ul(のCard)とreact-moment(※momentではありません)を使っています。 material-ui.com www.npmjs.com

×こっちではありません。 www.npmjs.com

それぞれ

# Material-UI (@4.0.1)
$ npm install @materail-ui/core
# react-moment (@0.9.2)
$ npm i react-moment

でインストールできます。 まずは、ほぼほぼMaterial-Ulの公式ドキュメントにあるCardのサンプルに先程のRSSデータを流し込みます。

import React from 'react';
import { Component } from 'react';
import PropTypes from 'prop-types';
import Card from '@material-ui/core/Card';
import Button from '@material-ui/core/Button';
import Avatar from '@material-ui/core/Avatar';
import red from '@material-ui/core/colors/red';
import { withStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
import './App.css';
import { CardHeader, CardContent } from '@material-ui/core';

const RssParser = require('rss-parser');
const url = "http://t-n-clark.hatenadiary.jp/feed"
const rssParser = new RssParser();

const styles = theme => ({
  blog: {
    margin: 20,
    padding: 20,
    marginTop: 10,
  },
  blogcontent: {
    overflowY: 'scroll',
    height: '200px',
  },
  avatar: {
    backgroundColor: red[500],
  },
});

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      contents: [],
    };
    this.componentDidMount = this.componentDidMount.bind(this)
  }
  componentDidMount() {
    rssParser.parseURL(url)
      .then((feed) => {
        const data = feed.items;
        console.log(data);
        this.setState({
          contents: [...data]
        });
      })
      .catch((error) => {
        console.error('Get Failed', error);
      })
  }
  render(props) {
    const classes = this.props.classes;
    const contents = this.state.contents.map(content => {
      return <Card>
        <CardHeader
          avatar={<Avatar arial-label="Blog" className={classes.avatar}>も</Avatar>}
          subheader={content.pubDate}
          title={content.title}
        />
        <CardContent key={content.id}>
          <Button size="small" color="primary" href={content.link}>Read More</Button>
        </CardContent>
      </Card>
    });
    return (
      <div className="App">
        <header className="App-header">
          <Grid container>
            <Grid item xs={12} sm={7}>
              <div className={classes.blog}>
                <h3>はてなブログ更新情報</h3>
                <div className={classes.blogcontent}>{contents}</div>
              </div>
            </Grid>
          </Grid>
        </header>
      </div>
    );
  }
}

App.propTypes = {
  classes: PropTypes.object.isRequired,
};

export default withStyles(styles)(App);

今回はタイトル(title)と日付(pubDate)とリンク(link)のみを使っていますが、
スペースとの相談次第では、サマリー(contentSnippet)とか入れてもいいかもしれません。
(RSSParserってサムネイル画像も取れているのでしょうか?もし取れているならサムネイル画像を使うのもありだと思います。)

はてブのfeed側の仕様なのか、RSSParser側の仕様なのか確かめてませんが、デフォルトだと最新30件?分のRSS情報が取れているみたいです。
なので、それをそのまま表示すると中々悲惨なことになります。
Material-UIにもbootstrapのようなグリッドシステム的なものがありますので、Gridで幅調整をすることが出来ます。
Gridを使えば多少のレスポンシブデザインに対応出来ます。
あとはoverflowY: 'scroll'で表示枠のサイズを指定したりしてみました。

f:id:T-N-Clark:20190530004747j:plainf:id:T-N-Clark:20190530004757j:plain
左:Gridなし,右:Gridあり

これで個人的には、おおよその見た目は及第点くらいにはなったのですが、時刻表示が世界標準のままで見にくいです。
そこで、react-momentというライブラリを使ってこれを整形します。
RSSParserで取ってきた日付をフォーマットを指定してMomentタグで挟んであげればいいみたいです。
例えば、YYYY/MM/DDにしたければ、

import Moment from 'react-moment'
<Moment format="YYYY/MM/DD">{content.pubDate}</Moment>

とやれば、

# 変換前
2019-03-01T02:30:00.000Z
# 変換後
2019/03/01

に出来ました。

f:id:T-N-Clark:20190530004802j:plain
こんな感じ

他にもいろいろあるみたいなので、公式のサンプルを参考にお好みのフォーマットにしてみてください。

ちなみにですが、Twitterの埋め込みなら、専用のライブラリ(私はreact-twitter-widgetsを使っています)を使えばもっとお手軽にできてしまいます。 www.npmjs.com

f:id:T-N-Clark:20190530004811j:plain

あとがき

以上で、React限定&初心者によるメチャメチャ実装ではありますが、ひとまず再びブログの更新情報を埋め込めるようになりました。
この方法はあと何年使えるんだろう…?

先日、Googleから発表されたiframeに代わるPortalsタグを使えば、現在稼働中のクラシカルなページでもSPA並みにサクサク遷移できるようになるみたいです。
まあ、まだearly stageみたいなので私は試していませんが… docs.google.com blog.uskay.io

私事ですが、今回RSSフィードの実現方法を変える必要があったので、この機にいっそのことホームページの方も一新しようかなと思っています。
今見返すと一昔前のような見た目ですし。無事、一新したら紹介するかもしれません。 暫定版→http://clarkphys.html.xdomain.jp/index.htmlclarkphys.html.xdomain.jp

それではまた。

2021/1/18追記

効率が良いかは自信がありませんが、複数のサイトからRSS情報を取得する場合は以下のようにurlcomponentDidMount()を書き換えればできます。 ポイントは次の2つです。

  1. setStateで情報を更新するときに、既存のstateと新規取得情報を結合しておく
  2. サイト内では順番通りになっているが結合したときに日付がバラバラなので、sortしておく
const RssParser = require('rss-parser');
const CORS_PROXY = "https://cors-anywhere.herokuapp.com/"
// 取得したいURLをListで定義しておく
const urls = [
    CORS_PROXY + 'https://t-n-clark.hatenadiary.jp/feed',
    CORS_PROXY + 'https://qiita.com/Sashimimochi/feed.atom'
]
const rssParser = new RssParser();

class Profile extends Component {
    constructor(props) {
        super(props);
        this.state = {
            contents: [],
        };
        this.componentDidMount = this.componentDidMount.bind(this)
    }
    componentDidMount() {
        urls.map((url) => {
            rssParser.parseURL(url)
                .then((feed) => {
                    // 新たに取得したfeedを取り出す
                    const data = feed.items;
                    // 既存のstateを取り出しておく
                    var feeds = this.state.contents
                    // 既存のstateに新規で取得したfeedを1つずつ追加する
                    data.map((d) => {
                        feeds.push(d)
                    })
                    // 日付でsortする
                    feeds.sort(function (a, b) {
                        if (a.pubDate < b.pubDate) {
                            return 1;
                        }
                        if (a.pubDate > b.pubDate) {
                            return -1;
                        }
                        return 0
                    });
                    // stateをupdateする
                    this.setState({
                        contents: feeds
                    });
                })
                .catch((error) => {
                    console.error('Get Failed', error);
                })
        })
    }

次回

t-n-clark.hatenadiary.jp

PythonでDiscordのTRPG用のダイスボットを自作してみた

始まり

始めに断っておきますが、私はTRPGもDiscordもにわか勢です。

最近、身内でTRPG(主にCoC)をぼちぼちやるのですが、毎回ダイス振って判定するのってメンドくさいよねって話になりました。 セッション環境はdiscordを使い始めたので、discord上で動くbotにしようということになりました。正直なところ、既に先人のお方が作られた素晴らしいものがあるので「discord-bcdicebot使えばよいのでは?」というのが賢い選択だと思います。 ですが、ポンコツの私には使い方がわからなかったので、勉強も兼ねて友人に手伝ってもらいながら作ってみました。

Python + Googleスプレッドシート (+ Heroku)で実現しています。 ソースコード全体はGitHubに晒しておきます。

PythonからGoogleスプレッドシートにアクセスする

ダイスを振るだけなら不要なのですが、技能の成否判定も自動でしたいのでキャラクターシートGoogleスプレッドシートに作って判定することにしました。 Googleスプレッドシートの操作方法はこちらを参考にしました。

こちらを参考にOAuth用のクライアントIDを作成してjsonファイルをダウンロードするところまで進めます。 セキュリティを度外視するならGoogle Apps ScriptでHTTPリクエストを受け付けるという方法もあるかとは思います。

スプレッドシートの仕様は適当でこんな感じです。

技能 合計値 純減 初期値 職業P 興味P
HP 9 0 9 0 0
MP 7 0 7 0 0
SAN 60 0 60 0 0
db 1d4 0 1d4 0 0
こぶし 50 0 50 0 0
図書館 50 0 25 25 0

初期値の決め方は下記を参照しました。

この中で使うのは技能合計値のカラムです。これを以下のような感じでpythonから取得します。

def get_gs():
    scopes = ['https://www.googleapis.com/auth/spreadsheets']
    json_file = './hoge.json'#OAuth用クライアントIDの作成でダウンロードしたjsonファイル
    credentials = ServiceAccountCredentials.from_json_keyfile_name(json_file, scopes=scopes)
    http_auth = credentials.authorize(Http())

    # スプレッドシート用クライアントの準備
    doc_id = 'doc_id'#これはスプレッドシートのURLのうちhttps://docs.google.com/spreadsheets/d/以下の部分です
    gs = gspread.authorize(credentials)
    gfile   = gs.open_by_key(doc_id) #読み書きするgoogle spreadsheet
    return gfile

ユーザーごとにでシートを切り替えられるようにします。シート名はなんでも良いのですが、私はdiscordから自動でシートを切り替えられるようにdiscordのIDをシート名にしています。

def get_charactor(sheet_name):

    gfile = get_gs()

    worksheet = gfile.worksheet(sheet_name)

    charactor = {}
    #技能名カラム
    cell_keys = worksheet.col_values(1)
    #合計値カラム
    cell_values = worksheet.col_values(2)
    for k,v in zip(cell_keys, cell_values):
        charactor[k] = v
    return charactor

ここまででスプレッドシートの設定は終了です。

Discord側のBOTを作る

次にDiscordの設定をしていきます。

Bot作成

DiscordのBOTの作り方はこちらを参考にさせていただきました

メッセージを取る

discordのメッセージ欄に特定の入力があったらダイスを振るようにします。入力形式は以下の仕様にします。ダイスを振るトリガーはdiceにします。

dice 1d100 技能名

これで受け取ってメッセージを返す待機botの処理を実装します。

client = discord.Client()
client_id = conf['client_id']

@client.event
async def on_ready():
    print('Logged in')
    print('-----')

@client.event
async def on_message(message):
    # 開始ワード
    if message.content.startswith('dice'):
        # 送り主がBotではないか
        if client.user != message.author:
            info = parse('dice {}d{} {}', message.content)
            if info:
                if info[1].isdecimal() and info[0].isdecimal():
                    dice_num = int(info[0])
                    dice_size = int(info[1])
                    key = info[2]
                    # メッセージを書きます
                    m = message.author.name + ' '
                    if key == '一時的狂気':
                        m = temp_madness()
                    elif key == '不定の狂気':
                        m = ind_madness()
                    elif key == 'dice':
                        m = simple_dice(dice_size, dice_num)
                    else:
                        chara = get_charactor(str(message.author))
                        msg, result = judge(chara, key, dice_size, dice_num)
                        m += msg
                        if result:
                            d = damage(chara, key)
                        else:
                            d = None
                        if d is not None:
                            m += '\nダメージ: ' + str(np.sum(d)) + ' = ' + str(d)
                    # メッセージが送られてきたチャンネルへメッセージを送ります
                    await client.send_message(message.channel, m)

client.run(client_id)

ダイス

ダイス振る部分を実装します。単純に入力したダイスを振るものと成否判定をするものの2種類用意します。 まず、1〜dice_sizeまでの一様整数乱数を1つ生成します。

def dice(dice_size):
    num = np.random.randint(1, int(dice_size))
    return num

単純にダイスを振る場合は次のようにしています。上記のダイスをdice_num回分振ります。2d6なら1d6のダイスを2回振っています。あとでメッセージ表示の際に個別のダイス結果も見たいのでダイス結果はnumpy.arrayにしています。合計値はnp.sumで計算します。msgはdiscordに返すメッセージです。

def simple_dice(dice_size, dice_num):
    dice_val = np.array([], dtype=np.int64)
    for i in range(dice_num):
        dice_val = np.append(dice_val, dice(dice_size))
    msg = 'dice: ' + str(np.sum(dice_val)) + ' = ' + str(dice_val)
    return msg

discord上ではこんな感じになります。

daa0ec9db009ee3a418450bc5e781ead.jpg

成否判定をするときもsimple_diceをベースに実装します。

成否判定

スプレッドシートから引っ張ってきた情報を参照して成否判定をさせます。 取得したスプレッドシートの情報は辞書型にして持たせておきます。

charactor = {
    'HP': 9,
    'MP': 7,
    'SAN': 60,
    'こぶし': 50,
    '図書館': 50
}

と言っても、インスタンス生成とかしているわけではないので、判定のたびにスプレッドシートからデータを参照しているので、たぶん非効率的です。

処理の流れは

  1. 入力メッセージからダイスのサイズと数を取得する
  2. ダイスを振ってダイス値を取得する
  3. 技能値をダイス値を比較する

といった感じです。ちなみに卓ルールで5以下でクリティカル、96以上でファンブルにしています。returnのbool値はダメージ判定時に使用します。

def judge(charactor, key, dice_size, dice_num):
    dice_val = np.array([], dtype=np.int64)
    for i in range(dice_num):
        dice_val = np.append(dice_val, dice(dice_size))
    if int(charactor[key]) >= np.sum(dice_val):
        msg = key + ' ' + str(charactor[key]) + ' >= ' + str(np.sum(dice_val)) + ' = ' + str(dice_val)
        if np.sum(dice_val) <= 5:
            msg += ' 【クリティカル】'
        msg += ' Success'
        return msg, True
    else:
        msg = key + ' ' + str(charactor[key]) + ' < ' + str(np.sum(dice_val)) + ' = ' + str(dice_val)
        if np.sum(dice_val) >= 96:
            msg += ' 【ファンブル】'
        msg += ' Fail'
        return msg, False

discord上ではこのように見えます。

0c4638e5b209ab36f55615788c200e2c.jpg

ダメージ判定

特定の技能名で技能判定が成功した際に自動でダメージロールを振るようにしました。トリガーとなる技能名はあらかじめダメージがわかっているこぶし,頭突き,キックだけに絞って実装します。マーシャルアーツは考慮していません。 ダメージボーナスがあればマイナスも含めて追加しています。

def damage(charactor, key):
    d = np.array([], dtype=np.int64)
    if key == 'こぶし':
        d = np.append(d, dice(3))
    elif key == '頭突き':
        d = np.append(d, dice(4))
    elif key == 'キック':
        d = np.append(d, dice(6))
    else:
        return None

    if 'd' in charactor['db']:
        result = parse('{}d{}', charactor['db'])
        dice_size = int(result[1])
        dice_num = int(result[0])
        for i in range(np.abs(dice_num)):
            if dice_num < 0:
                d = np.append(d, -dice(dice_size))
            else:
                d = np.append(d, dice(dice_size))
    return d
  • 成功
    0f66094ef3ff2cfbb2c6899fa6dbc468.jpg
  • 失敗
    f3519de16b5129f665654e4e0334d1b7.jpg
  • ダメージボーナスあり
    5c9890fa428b2d87386954e0c80a6fa5.jpg
  • ダメージボーナスあり(マイナス)
    720e61ae3edcb3ae1cb08a82a5eec24a.jpg

狂気表

discord上で次のように入力した場合に狂気票を振るようにします。

dice 1d10 狂気の種類

狂気の種類一時的発狂不定の狂気の2種類が振れます。1d10はフォーマットの統一の為につけていますが、実質使っていません。 一瞬、狂気表もスプレッドシートに書こうかと思いましたが、処理速度をあげる為にハードコーディングしてます(正直、面倒臭かったので)

def temp_madness():
    roll = {}
    roll[1] = '鸚鵡返し(誰かの動作・発言を真似することしか出来なくなる)'
    #(中略)
    roll[20] = '過信(自分を全能と信じて、どんなことでもしてしまう)'
    msg = roll[dice(20)]
    msg += '\n一時的狂気(' + str(dice(10)+4) + 'ラウンドまたは' + str(dice(6)*10+30) + '分)'
    return msg

def ind_madness():
    roll = {}
    roll[1] = '失語症(言葉を使う技能が使えなくなる)'
    #(中略)
    roll[10] = '殺人癖(誰彼構わず殺そうとする) '
    msg = roll[dice(10)]
    msg += '\n不定の狂気(' + str(dice(10)*10) + '時間)'
    return msg
  • 一時的狂気
    1c237209fa36504fbeda0ac9d5a32475.jpg
  • 不定の狂気
    eebbceb517ec43a602ee1a6e9de05c8c.jpg

Herokuにデプロイする

そのままではローカルでbotを起動している時しか使えないので不便です。そこでサーバーを立てて常時使えるようにします。今回はHerokuを使います。

Procfile

まずは、Heroku上で動かす実行スクリプトを作成します。今回はtrpg_bot.pyを実行するだけなので

woker: python trpg_bot.py

とします。

requirement.txt

requirement.txtpythonファイル内で使っているライブラリを記入します。今回は以下のライブラリを使用しています。

oauth2client
httplib2
gspread
discord
parse
numpy

デプロイ

基本的にはHerokuのアカウントを作ってCreate New AppしてHeroku Gitにしたがって進めれば良いです。

# Herokuにログインする
$ heroku login
# リポジトリをクローンする
$ heroku git:clone -a trpg_dice_bot
$ cd trpg_dice_bot

cloneしたリポジトリに作成したアプリケーションなどを格納します。

  • trpg_bot.py #メインアプリケーション
  • config.json #設定ファイル
  • requirement.txt # さっき作ったやつ
  • Procfile #さっき作ったやつ
  • oauth.json #Googleスプレッドシートを作った時にダウンロードしたoauthのjsonファイルです
$ git add .
$ git commit -am "make it better"
$ git push heroku master

無事デプロイに成功すると

remote: Verifying deploy... done.

のようなメッセージが表示されます。 デプロイした時点ではまだdiscord上ではbotはオフラインのはずです。

コメント 2019-02-27 220947.jpg

有効にするには、Resourcesからアプリを起動すればdiscord上でbotがオンラインになるはずです。 ちなみにHerokuの無料枠は550時間です。

Heroku

無事、オンラインになりました。

コメント 2019-02-27 220928.jpg

このあたりを参考にさせていただいています。

これで快適なTRPGライフが送れるはず。 少しでも誰かの参考になれば幸いです。

その他のリファレンス

UnityEditorのバージョン管理をしたい

本記事は関東ゲーム制作部 Advent Calendar 2018 16日目の記事です。 adventar.org

経緯

昨日12/14にUnity 2018.3がリリースされましたね。prtimes.jp個人的には長らく切望していたNested Prefab機能が実装されたと聞いて早速使ってみたいと思いました。ですが、Unity使いの皆様ならよくあると思うのですが、「Unityのバージョンアップすると過去のが動かなくなるから上げたくないんだよなぁ...」ということがあるかと思います。デフォルトではUnityは1環境に1バージョンしか存在できないっぽい?ので、特に共同開発なんかしていると周りと足並みそろえて変更する必要があると思います。Pythonであれば、pyenvとかで複数バージョンを共存させられるけど、Unityでもそういったことできないのかな~と思っていたら2通りのやり方を見つけたので、誰かの役に立てばと思い、メモしておきます。

UnityHubを使う

コメント 2018-12-16 002612.jpg
私はこの記事を書くにあたって知りましたが、こちらを使っている人は割といるのではないでしょうか?UnityHubの特徴はこちらの記事にわかりやすくまとめられています。 qiita.com 要点だけ抜粋すると

  • 複数のバージョンを管理できる
  • Componentを追加インストールできる
  • ver.2017以降であればUnityHub経由でインストールできる。それ以前もローカルにあれば、追加できる
  • 同一バージョンのUnityEditorを同時に起動できる

あたりでしょうか。これを使えば、プロジェクトでは古いバージョンを使っているけど、自分の環境では新しいバージョンを試したいなんてときに便利だと思います。手っ取り早く使いたいならこっちを使うのがおすすめです。 コメント 2018-12-16 011026.jpg

Unity on Docker

ここからが本命かつマイナーなお話になります。最近、バージョン管理や環境を汚さないようにDocker上でいろいろやろうという話を聞きます。かくいう私もPythonの環境とかはDocker上で作ったりしています。そこで、最近流行りのDockerを使ってUnityを動かせないかと思いました。結論から言うとおすすめしません。それでも、供養の意味も込めて検討記録ということで長くはなりますが、それでもいいよという心優しい方はお付き合いください。

Docker自体のインストールも書いているといよいよ長くなりすぎるので、ここでは割愛します。私はWindows10なので、Docker for Windowsを使うためにこの辺りを参考にしました。 qiita.com ついでにGUIでコンテナ管理したいのでKitematicも入れています。

Dockerのインストールには成功しました。いよいよ、Unityのコンテナを用意します。といっても私はdocker-compose up -d程度しか使えないので、誰かいい感じのDockerfileなりdocker-compose.ymlなど提供してくれてないかなぁと検索したところ、ありました。 github.com 探せばいるもんですね。先人は偉大です。しかもReadMeを読んだところなにも考えずにdocker-compose up -dをと唱えれば良いと書かれているもんだからありがたい限りです。ということで、早速コマンドプロンプトを起動して、リポジトリをcloneしてきて、Dokerfileがあるディレクトリまで移動して、実行します。 ※ここからは回線状況にもよりますが、作業に2~3時間程度要します。また、ディスク容量に4GB程度の空きを作っておく必要があります。

$docker-compose up -d

と叩くと、Dockerfileに従ってインストールが始まります。時間がかかりそうだったので、放置していたら、タイムアウトの関係でいくつかのインストールに失敗していたのと、share directoryにのアクセスに失敗しました的なメッセージが出て止まっていました。同時にポップアップでshare directoryのアクセスを許可しますか?のようなものが出ていたので、OKを押して再インストール。うまくいかない場合は、一度インストールしたイメージ類をすべて消してから再度実行するとうまくいくと思います。

$docker image list #ダウンロードしたイメージ一覧を表示
$docker rmi *** #***で指定したイメージを削除

当該のイメージが見つかりませんでしたのようなメッセージが出た場合はタグをつけて削除するとうまくいくと思います。 www.memotansu.jp

これでコンテナのダウンロードまでは成功したようなのですが、コンテナの起動がうまくいかない。メッセージにリビルドしてねと出ていたので

$docker-compose up --build

を実行。概ね上手くいったぽいけど、すると今度は、

standard_init_linux.go:190: exec user process caused "no such file or directory"

Windows10を使っているのですが、どうやらリポジトリからcloneしてきたときに、改行コードがCRLFに変換されて落としてきています。ですが、コンテナ自体はUbuntuだからかLFでないと動かないようです。 qiita.com なので、テキストエディタで改行コードをLFに変更して再度

$docker-compose up --build

を実行するとようやくUnityのコンテナが起動!!ゴールは近いです。提供ページに書かれている通り、VNC ViewerでVM内に入ってくださいとのとこなので、お好きなVNC Viewerを使ってコンテナ内に入ります。私は今回はUltra VNCを使いました。forest.watch.impress.co.jp www.atmarkit.co.jp辺りを参考にしながら、インストールの設定は基本デフォルトで、環境変数に登録とデスクトップにショートカットアイコンを作る的なものだけチェックを入れてインストール。さーて、IPアドレスを打っていざConnectと思ったのですが、またコケました。今度は何だと思ってKitematicのログを見ると、

Creating unity-service ... done
Attaching to unity-service
unity-service | stored passwd in file: /home/adminuser/.vnc/passwd
unity-service | 16/12/2018 07:00:26 -usepw: found /home/adminuser/.vnc/passwd
unity-service | 16/12/2018 07:00:26 x11vnc version: 0.9.13 lastmod: 2011-08-10  pid: 8
unity-service | 16/12/2018 07:00:26
unity-service | 16/12/2018 07:00:26 wait_for_client: WAIT:cmd=FINDCREATEDISPLAY-Xvfb
unity-service | 16/12/2018 07:00:26
unity-service | 16/12/2018 07:00:26 initialize_screen: fb_depth/fb_bpp/fb_Bpl 24/32/2560
unity-service | 16/12/2018 07:00:26
unity-service | 16/12/2018 07:00:26 Autoprobing TCP port
unity-service | 16/12/2018 07:00:26 Autoprobing selected TCP port 5900
unity-service | 16/12/2018 07:00:26 Autoprobing TCP6 port
unity-service | 16/12/2018 07:00:26 Autoprobing selected TCP6 port 5900
unity-service | 16/12/2018 07:00:26 listen6: bind: Address already in use
unity-service | 16/12/2018 07:00:26 Not listening on IPv6 interface.
unity-service | 16/12/2018 07:00:26
unity-service |
unity-service | The VNC desktop is:      767f1c3f2d95:0
unity-service | PORT=5900
unity-service | 16/12/2018 07:05:11 Got connection from client 172.16.250.1
unity-service | 16/12/2018 07:05:11   other clients:
unity-service | 16/12/2018 07:05:11 webSocketsHandshake: unknown connection error
unity-service | 16/12/2018 07:05:11 Client 172.16.250.1 gone
unity-service | 16/12/2018 07:05:11 Statistics             events    Transmit/ RawEquiv ( saved)
unity-service | 16/12/2018 07:05:11  TOTALS              :      0 |         0/        0 (  0.0%)
unity-service | 16/12/2018 07:05:11 Statistics             events    Received/ RawEquiv ( saved)
unity-service | 16/12/2018 07:05:11  TOTALS              :      0 |         0/        0 (  0.0%)
unity-service exited with code 137

的なメッセージが出ていました。どうやらVNCアクセスに失敗していたのかよくわかりませんが、VNC Viewerを立ち上げる前にUnityのコンテナを立ち上げたのがいけなかったぽいので、一度コンテナをdownさせてからVNC Viewerを立ち上げた後に、再度

$docker-compose up --build

を実行すると、

Creating unity-service ... done
Attaching to unity-service
unity-service | stored passwd in file: /home/adminuser/.vnc/passwd
unity-service | 16/12/2018 07:24:51 -usepw: found /home/adminuser/.vnc/passwd
unity-service | 16/12/2018 07:24:51 x11vnc version: 0.9.13 lastmod: 2011-08-10  pid: 8
unity-service | 16/12/2018 07:24:51
unity-service | 16/12/2018 07:24:51 wait_for_client: WAIT:cmd=FINDCREATEDISPLAY-Xvfb
unity-service | 16/12/2018 07:24:51
unity-service | 16/12/2018 07:24:51 initialize_screen: fb_depth/fb_bpp/fb_Bpl 24/32/2560
unity-service | 16/12/2018 07:24:51
unity-service | 16/12/2018 07:24:51 Autoprobing TCP port
unity-service | 16/12/2018 07:24:51 Autoprobing selected TCP port 5900
unity-service | 16/12/2018 07:24:51 Autoprobing TCP6 port
unity-service | 16/12/2018 07:24:51 Autoprobing selected TCP6 port 5900
unity-service | 16/12/2018 07:24:51 listen6: bind: Address already in use
unity-service | 16/12/2018 07:24:51 Not listening on IPv6 interface.
unity-service | 16/12/2018 07:24:51
unity-service |
unity-service | The VNC desktop is:      6afa1c440a08:0
unity-service | PORT=5900

となり、Connection Errorは解消されたっぽい。ということで、ローカルホストにVNCServer: 127.0.0.1でアクセスすると、パスワードを聞かれました。提供元のReadMeに書かれていた通り、パスワードpass123を入力すると、無事、コンテナ内に入れました。こんな感じのコンソール画面だけが表示された画面が見えるはずです。で、肝心のUnityはどこにあるのかというと、/opt/Unity/Editor/Unityにあるとのことですが、デフォルトのディレクトリが/opt/Unity/Editorなので、その場でUnityを実行すれば起動できました。

adminuser@6afa1c440a08:~$cd Unity

あとは画面に従って通常のUnityのインストールと同様に進めていきます。 image2.png image4.png ログインもできて、さーて開発を始める準備が整ったことでしょう。新規に作るもよし、既存のプロジェクトを使うもよし。自分で作っていたプロジェクトを使いたい場合はCloudに上げておくか、ローカルであれば、cloneしてきたリポジトリ直下にあるcontextフォルダにおくように書かれていたのでお好みの箇所に置きましょう。コンテナ内からアクセスするときは、File System->context->任意のフォルダに入っています。

ところが、これで動くはず...あれ?一瞬エディター画面が表示されてAbortedとか言われるぞ...???なんかいろいろ調べるとバージョンによってはバグで動かないっぽいことを見かけたので、Unityのバージョンをアップすることにしました。 Installing Unity 2017.1.0f2 In Docker Container 使用したバージョンはDockerfileに書かれていた当時のlatestであるver.2017.2.0f3にしました。バージョンを変更したい場合は、お好みのバージョンのURLをコピーしてDockerfileの71行目あたりにある.debファイルを指定しているURLを変更すれば行けました。それ以外のバージョンを使いたい場合は、たぶんこちらのスレッドから対応したUnityのイメージを落としてくればいいんだと思います。スレッドのページを下に繰っていくと、古いのだと5.1から最新で2017.2.0b11まであるみたいです。どれが行けてどれが行けないまでは検証していないのでわかりません(^-^; バージョンアップして再度dockder-compose up -dからやり直すと無事NewProject作成で画面が表示された...? image7.png メモリが足りないのかこれ以上まともに描画されませんでした。こんな調子なので当然、プロジェクトが重すぎたのか、2dだからダメなのか自前のプロジェクトはうまく描画できませんでした。本当はこんな感じの画面を出したかったんですが... image6.png

画像は私の所属するサークルNOELで制作したゲームです。現在は第2作目のリリースに向けて鋭意制作中です。最新情報は随時Twitterで流していきますので、よろしければフォローしてやってください。@CircleNoelGame(←単なる宣伝です。)

これでめでたくDocker上でUnityが表示できるようになりました(まともに動くとは言っていない)。ものすっごく動作がもっさりしますが、とりあえず動いたということで、今回はここらへんで力尽きてしまいました。あとは割り当てメモリとか増やせばいいんですかね?でも、私の持つ低スペックPCでは3GB割り当てるのが精いっぱいなんですよね。年内に進展があれば、時間を見つけて続きを書きたいと思います。

ちなみにUnrealEngineのdockerイメージも存在はするようです。頑張ればこっちも行けるんでしょうかね(;^_^A github.com

少しでも誰かの参考になれば幸いです。よければサークルNOELもよろしくお願いします。 Twitterはこちら@CircleNoelGame

その他のリファレンス

2018年私的読みたいアドベントカレンダー(随時更新)

qiita.com

ゲーム開発

その3まである。やっぱり最近ホットなのでしょうか?その2でAgent-MLのことを書いてくださる方がいるようなので楽しみです。 qiita.com qiita.com qiita.com adventar.org ↑私も投稿しました t-n-clark.hatenadiary.jp

お勉強

最近、まじめにお仕事でも使う必要が出てきたのでこの機に勉強でもし直そうかと思ってます。GAN関連の論文紹介もちらほら見かけるのでそのあたりを中心に見ておこうと思ってます。 qiita.com qiita.com qiita.com