RustでDiscord botでゲームを作った話

公開:2020/12/06
更新:2020/12/07
4 min読了の目安(約4300字IDEAアイデア記事

この記事は 三重大学 計算研究会 Advent Calendar 2020 6日目の記事です.

背景・概要

サークル内向けにRustでDiscordのbotでゲームを作ったので,作るのに使ったcrateの話や感想の話をしたいと思います.

作ったきっかけは,サークルの人たちとリアルで会えないなぁの話をしていたときに ハイパーロボットbot の話が出たとかだった気がします.最初の版を勢いで作ったら朝になりました.成果としてはかなり(現時点で2718問,ストックは65万問ぐらい)遊んでくれたのでうれしい.何人かの試験勉強を破壊しかけた. 他の人もゲームbot作ったりして楽しかったです.

serenity (0.9.2)

docs.rs: https://docs.rs/serenity/0.9.2/serenity/
examples: https://github.com/serenity-rs/serenity/tree/current/examples

Discord bot周りの操作にはserenityというcrateを使いました.
以下のコードのようなイベントドリブンっぽい書き方ができます.
DBのpoolなどは,serenity::client::EventHandlerを実装する構造体に入れておきます(以下のコードではHandlerに入れる).

#[async_trait]
impl EventHandler for Handler {
    // メッセージが投稿されたときの処理
    async fn message(&self, ctx: Context, msg: Message) {
        // botに反応しないようにする
        if msg.author.bot {
            return;
        }
	// サーバーのID
        eprintln!("guild_id = {:?}", msg.guild_id);
	// チャンネル名
        let channel_name = msg.channel_id.name(&ctx.cache).await;
        eprintln!("channel_name = {:?}", channel_name);
	// メッセージの送信
	let content = "Hello, world";
	if let Err(why) = msg.channel_id.say(&ctx.http, content).await {
	    println!("Error sending message: {:?}", why);
	}
        eprintln!();
    }

    // botがonlineになったときの処理
    async fn ready(&self, _: Context, ready: Ready) {
        println!("{} is connected!", ready.user.name);
    }
    
    // ほかにもいろいろある,デフォルト実装があるので必要なものだけ実装すればいい
}

asyncなtraitは現時点では直接書けないので,serenity::async_trait(元はasync-trait crate)を使うところが少し注意です.また,tokioは0.2を使います.

このcrateを使うときはexamplesのコードが参考になります.

sqlx (0.4.1)

docs.rs: https://docs.rs/sqlx/0.4.1/sqlx/
repository(README.md): https://github.com/launchbadge/sqlx
examples: https://github.com/launchbadge/sqlx/tree/master/examples

SQLのクエリがコンパイル時に(実際にサーバにアクセスされて)チェックされます.すごい.
PostgreSQL以外の対応は発展途上のようなのでDBにはPostgreSQLを使うのがよさそうです.

このcrateを使うときはリポジトリのREADME.mdのQuickstartが参考になります.
featuresの設定もREADME.mdが参考になります.

一番多用した書き方は,query_as!を使う以下のような書き方です.fetch_one以外にもfetchfetch_allなどがあります.

struct NumProblems {
    max: Option<i32>,
}
let res = sqlx::query_as!(
    NumProblems,
    "SELECT max(id) FROM ricochet_robots_problems"
).fetch_one(pool).await?;
struct Problem {
   // ...
}
let res = sqlx::query_as!(
    Problem,
    "SELECT * FROM ricochet_robots_problems WHERE id = $1",
    id
).fetch_one(pool).await?;

sqlx-cli

README.md: https://github.com/launchbadge/sqlx/tree/master/sqlx-cli

sqlxを使うときにmigrationの管理などをするためのCLIが用意されています.
インストール方法や使い方はREADME.mdに書いてあります.

ron (0.6.2)

docs.rs: https://docs.rs/ron/0.6.2/ron/

serdeで使えるシリアライズ表現の1種です.Rustのような書き方をします.
DBに文字列で放り込んでおいてそのまま人間が読める,というのはうれしかったですが,人手で書かないのであればJSONなどで十分な気がします.人手で書く場合はコメントを書けるのがうれしそうです.

png (0.16.7)

docs.rs: https://docs.rs/png/0.16.7/png/index.html

png形式の画像の作成ができるcrateです.RGBAを指定して画像が作れます.

このcrateを使うときはdocs.rsのトップページのプログラムが参考になります.

gif (0.11.1)

docs.rs: https://docs.rs/gif/0.11.1/gif/

GIFの作成ができるcrateです.

注意点ですが,Frameを作る際に,ドキュメントに Note: This method is not optimized for speed と書かれている関数は 本当に 遅いです.使う色の数が少ない場合は,gif::Encoder::newpalletを渡して,gif::Frame::from_indexed_pixelspalletで与えた色を使うと高速にエンコードできます.

それ以外はdocs.rsのトップページのプログラムが参考になります.

ちなみにdiscordに投稿する場合はgifで投稿するとアンチエイリアスがかからなかった記憶があります.なのでmp4に変換したものを投稿するようにしています.

fnv (1.0.7)

docs.rs: https://docs.rs/fnv/1.0.7/fnv/

keyのサイズが小さいときにstdのHashMap/HashSetより早いMap/Setが使えます.注意点としてはcollision attacks対策がないです.攻撃者がkeyを制御できるところには使わないようにしましょう(たぶん).

ちなみにkeyにu64しか使わないなどの場合は,特に操作せずにそのままhash値にしてしまうような,std::hash::Hashertraitの実装を自作したほうが早かった記憶があります.今回の用途ではkey自体がhash値だったので,最終的にはfnvは使っていません.自分でHasherを実装する場合はfnvのソースコードを参考にするといいと思います.

感想など

  • Rustのasyncの使い方をちょっと理解した
  • Mutexをlockしたままawaitできないのが罠,というかこれをコンパイル時にちゃんと落とせるのすごい
  • src/bin以下のファイルが無限に増えていったんだけど,機能ごとにファイル分けとくのとコマンドライン引数で機能分けるのとどっちがいいんだろう
  • DBのデータをアップデート後もちゃんと使える状態で更新していくのが想像の100倍ぐらい大変だった
  • これがきっかけでデータベーススペシャリスト受けたけど玉砕した
  • 2718問,キリがいいですね
  • optimal(optimalでない)
  • 生活を破壊しないように夜0時~7時は動かないようにしたんですが,この時間帯にメンテするのが楽なので自分の生活が破壊(ry

おまけ

かなり難しかった問題をいくつか置いておきます.

たぶん最短18手

たぶん最短19手

たぶん最短33手