【追加機能編2】Misskey Playを作ってみよう#3

どうも、冷凍食品のじゃがいもです
近くのスーパーにいるかもしれません


今回すること

前回の追加機能編に続き、おみくじプリセットに新しい機能を追加していきます
前回は選択肢と確率を変化させました

今回は引き直しボタンを追加します(説明することが多いのでこれだけにします)

関数化

今回はおみくじの結果を引き直しできるようにするため、今まで一度だけ行っていたくじの作成を1つの処理としてまとめて使いまわせるようにします

このために、くじを作成する処理を前回説明した「関数」にします

関数は以下のように作ります

@関数名(引数名){
    処理1
    処理2
    ...(好きなだけ処理を書く)
    return 返り値
}

引数がない場合は引数名は省略します、返り値がない場合はreturn~の行を省略します(返り値を返すこの構文はreturn文といいます)

引数は、関数内では変数のように使えます

引数を2つ以上指定したい場合は
引数名1,引数名2
のようにコンマで区切って並べます(並んだ順に渡されます)

return文は何回でも書くことができます、これはif文で分岐した先で処理を終了したいときなどに使えます


Note
引数の渡し方として、値渡しとアドレス渡しという2つがあります
リストのように長いものは、それ自体ではなく「アドレス」という同じものを扱うためのデータを渡します
要するに、値渡しでは値をコピーして渡し、アドレス渡しではそのものを渡すと思ってください
何が違うかというと、例えば値渡しで渡された引数は、関数内の処理で引数の値が変わっても、参照元(関数を呼び出すときに引数に指定した変数)には影響しません
これは当たり前のように思えるかもしれませんが、アドレス渡しの場合、変数そのものを渡すことと変わらないので、引数の中身を変えると元の変数の中身も変わります
値渡しかアドレス渡しかは、データの形式によって変わります、例えば数値であれば値渡しになり、リストであればアドレス渡しになります
最初の内はそれをすべて覚える必要はないですが、「引数の中身を変えると、元の変数も変わってしまうことがある」ということは覚えておいてください(分かっていないとよくバグります)
ちなみにこの値渡しとアドレス渡しは、変数をコピーするときにも同じようなことが起こります


これに従って、乱数を生成する関数を渡すとくじの結果を返す関数を作ってみます

なので、乱数の生成後から結果の生成までの部分を取り出します

// ランダムに選択肢を選ぶ
let chosen = choices[random(0, (choices.len - 1))]

// timeScopeからランダムに選ぶ
var chosenScope=""
let randomNum=random(1 , 10)
if (randomNum==1) {
	chosenScope=timeScope[0]
} else {
	chosenScope=timeScope[1]
}

// 結果のテキスト
let result = `**{chosenScope}{chosen}** をお知らせします。`

これを上記の形式に従って囲みます、返り値はresultです、引数はそのままrandom、関数名はmakeOmikujiとします

@makeOmikuji(random){ //乱数からくじを生成する関数
    // ランダムに選択肢を選ぶ
    let chosen = choices[random(0, (choices.len - 1))]

    // timeScopeからランダムに選ぶ
    var chosenScope=""
    let randomNum=random(1 , 10)
    if (randomNum==1) {
        chosenScope=timeScope[0]
    } else {
        chosenScope=timeScope[1]
    }

    // 結果のテキスト
    let result = `**{chosenScope}{chosen}** をお知らせします。`
    return result
}

関数を作るときも、インデント(処理の先頭にタブか半角空白4字)を意識すると読みやすくなります


Note
変数にはスコープ(日本語で言えば、範囲)というものがあります
これは変数にアクセスできる範囲のことです
今回引数名をrandomとしましたが、これは関数の外にあるrandomとは別のものとして新しく宣言されたものです
このように関数内の引数や、chosenのように関数内で宣言された変数は「ローカル変数」といい、関数の外にあるものは「グローバル関数」といいます
引数や変数を宣言するとき、名前が被ってはいけませんが、スコープが異なるときはより狭い変数(グローバル変数より、ローカル変数の方が狭い)に上書きされます、これはグローバル変数側には影響を及ぼさず、関数の処理が終わるとローカル変数は登録を削除されます
なので、関数外から、関数内のローカル変数にアクセスすることはできません(そのような変数は登録されていないので)
逆に、関数内から、関数外のグローバル変数にアクセスすることはできます(同名のローカル変数を宣言した場合を除く)


引き直し前に最初のくじを表示しておく必要があるので、関数を書き終わった直後(関数外)に以下の処理で結果を出しておきます

let result = makeOmikuji(random)

これで、関数化前と同じ動作をしつつ、再利用に必要な処理を関数としてまとめることができました

引き直し処理

引き直す処理を行うreroll関数を作ります(引数、返り値はなしです)
ここでやりたいことは、画面に表示される結果を更新することです

画面はこの部分で作っています

// UIを表示
Ui:render([
	Ui:C:container({
		align: 'center'
		children: [
			Ui:C:mfm({ text: result })
			Ui:C:postFormButton({
				text: "投稿する"
				rounded: true
				primary: true
				form: {
					text: `{result}{Str:lf}{THIS_URL}`
				}
			})
		]
	})
])

結果(MFM)を表示してるのは

Ui:C:mfm({ text: result })

の部分です
これを後から書き換えられるようにしたいので、これに以下のようにしてresultMfmというタグ(名前みたいなもの)を付けます

Ui:C:mfm({ text: result },"resultMfm")

これはUi:C:mfmというMFMウィジェットを作る関数で、第一引数には表示の方法・内容の指定を書き、第二引数にはタグを文字列で書きます
なぜ第二引数を省略できるのか?というと、特別な書き方をすると一部の引数を省略できるようになります、これについては今は説明しませんが、普通の関数の引数を省略しないように気を付けてください

タグ名は被らないようにしてください

これをすることで、タグを指定してそのウィジェットの内容を書き換えることができます
以下のようにします

Ui:get("タグ").update(変更内容)

変更内容は、MFMウィジェットを作るときに書いてあった

{ text: result }

このようなものです
このデータ形式を「辞書」といいます
リストのように複数の要素を持つことができますが、順番ではなく「キー」(鍵)によって中身と紐づけます、

例えば

var jisho = {
    a: 1,
    b: 2
}

としたとき、1という値にaというラベルが付いていて、2にはb、というように考えて下さい

jisho["a"]

jisho.a

のように書くことで、配列のようにその値を取りだすことができます

今回は、textという鍵に文字列が紐づけられた辞書を渡すことで表示内容を決めることができます

また、UIを表示する部分で

Ui:C:postFormButton({
				text: "投稿する"
				rounded: true
				primary: true
				form: {
					text: `{result}{Str:lf}{THIS_URL}`
				}
			})

の部分は投稿ボタンを表示するものなので、同じようにタグを付けます

            Ui:C:postFormButton({
				text: "投稿する"
				rounded: true
				primary: true
				form: {
					text: `{result}{Str:lf}{THIS_URL}`
				}
			},"postButton")

変更のときは、変更する部分だけのデータがあれば大丈夫です、なので

{
    form: {
        text: "変更後の投稿文"
    }
}

を渡すことで投稿文が変化します

辞書についての注意ですが、コロン(:)の後は必ず半角スペースを置いてください(しないとエラーになるので)

まとめると、以下のような関数になります

@reroll(){ // 引き直して表示
    let result = makeOmikuji(random)
    Ui:get("resultMfm").update({text: `{result}`})
    Ui:get("postButton").update({form: {text: `{result}{Str:lf}{THIS_URL}`}})
}

(投稿文のStr:lfは改行を表し、THIS_URLはMisskeyPlayが用意したPlayのURLを表す定数です)
この関数はUIを表示する処理の上ぐらいに書いておいてください

乱数の設定

乱数は

// シードが「PlayID+ユーザーID+今日の日付」である乱数生成器を用意
let random = Math:gen_rng(`{THIS_ID}{USER_ID}{Date:year()}{Date:month()}{Date:day()}`)

の部分でrandom関数を作っていました
このMath:gen_rngというのは関数を生成する関数です(AiScriptでは、関数も値として扱うことができます)
このとき、引数に渡しているのは乱数の「シード」と呼ばれる文字列です

シードとは乱数を作るための元になるデータです、コンピュータは完全にランダムな値を作ることはできないので、実際にはランダムっぽい値を計算して返します
その計算に使うデータがシードで、シードが同じなら同じ乱数を生成します
つまり、日付のデータをシードとして設定すれば、その日は何度起動しても同じくじになります、シードを設定し直さずに続けて使う場合、結果は変化しますが、その変化の仕方はシードによって決まります

テンプレートのときはPlayID+ユーザーID+日付になっていますが、引き直しできることも考えて時間ごとに変わるようにします

時、分、秒は以下のように取得できます

Date:hour()
Date:minute()
Date:second()

これもシードに追加して、以下のように書き換えます

// シードが「PlayID+ユーザーID+今日の日付+時刻」である乱数生成器を用意
let random = Math:gen_rng(`{THIS_ID}{USER_ID}{Date:year()}{Date:month()}{Date:day()}{Date:hour()}{Date:minute()}{Date:second()}`)

引き直しボタンの追加

引き直しができるようになったので、今度はそれを実行するためのボタンを追加します
UIを表示しているのは以下の部分でした

// UIを表示
Ui:render([
	Ui:C:container({
		align: 'center'
		children: [
			Ui:C:mfm({ text: result })
			Ui:C:postFormButton({
				text: "投稿する"
				rounded: true
				primary: true
				form: {
					text: `{result}{Str:lf}{THIS_URL}`
				}
			})
		]
	})
])

Ui:renderはUiを表示する関数で、引数としてウィジェットのリストを渡します
Ui:C:containterは文字通りコンテナウィジェットで、この中に含むウィジェットは引数の辞書のchildrenキーにリストとして指定します
今はMFMテキストと投稿ボタンを表示しているので、この間に引き直しボタンを追加しましょう

ボタンは以下のように作ります

Ui:C:button({
  text: "タイトル"
  onClick: 押したときに実行する関数
})

あといくつか指定できるものがありますが、それは公式ドキュメントの方を見てください

https://misskey-hub.net/ja/docs/for-developers/plugin/plugin-api-reference/#uicbutton

今回は以下のようになります

Ui:C:button({
  text: "引き直し"
  onClick: reroll
})

まとめ

今回のコードと実行結果はこんな感じです

// 選択肢
let choices = [
	"1時"
	"2時"
	"3時"
	"4時"
	"5時"
	"6時"
	"7時"
	"8時"
	"9時"
	"10時"
	"11時"
	"12時"
]

// 選択肢
let timeScope = [
	"午前"
	"午後"
] 

// シードが「PlayID+ユーザーID+今日の日付」である乱数生成器を用意
let random = Math:gen_rng(`{THIS_ID}{USER_ID}{Date:year()}{Date:month()}{Date:day()}`)

@makeOmikuji(random){ //乱数からくじを生成する関数
    // ランダムに選択肢を選ぶ
    let chosen = choices[random(0, (choices.len - 1))]

    // timeScopeからランダムに選ぶ
    var chosenScope=""
    let randomNum=random(1 , 10)
    if (randomNum==1) {
        chosenScope=timeScope[0]
    } else {
        chosenScope=timeScope[1]
    }

    // 結果のテキスト
    let result = `**{chosenScope}{chosen}** をお知らせします。`
    return result
}

let result = makeOmikuji(random)

@reroll(){ // 引き直して表示
    let result = makeOmikuji(random)
    Ui:get("resultMfm").update({text: `{result}`})
    Ui:get("postButton").update({form: {text: `{result}{Str:lf}{THIS_URL}`}})
}

// UIを表示
Ui:render([
	Ui:C:container({
		align: 'center'
		children: [
			Ui:C:mfm({ text: result },"resultMfm")
			Ui:C:button({
  			text: "引き直し"
  			onClick: reroll
			})
			Ui:C:postFormButton({
				text: "投稿する"
				rounded: true
				primary: true
				form: {
					text: `{result}{Str:lf}{THIS_URL}`
				}
			},"postButton")
		]
	})
])

次回は別のテンプレートか、もしくは一からPlayを作るかのどちらかをします

それでは、また今度

いいなと思ったら応援しよう!