はじめに
『数学ガールの秘密ノート/ビットとバイナリー』ネタ第二段です。
第4章では「フリップ・トリップ」というゲームが話題となっています。どんなゲームかは説明するより見た方が早いので、
つまり、
- 石を一つずつ反転させる
- 同じパターンを二回出したら負け
- エラーにならずに全パターンを出したら勝ち
例によってTwitterで誰かが実装しているのを見かけたので、「今度こそWebpack使ってイマドキなJSを書こう」と
思ったものの試作品をin-browser Babel transformer使って書いてみたら100行ぐらいで書けたので、「これ別にガチでモジュール化する必要ねえな」と考え直し
Reactで書いたものをVueでも書いてみることにしました。コードは以下に置いてあります。
https://gist.github.com/junjis0203/4b26b3ef265879aa264f139f2ec13b5a
React実装
React版にはプロトタイプがありこちらになります。
https://gist.github.com/junjis0203/0910a106e7a8da2304f4fa648a6f9ed0
当然、「石」、「石をまとめたゲーム」というコンポーネントを作り実装したわけですが、とりあえず動かすのを目的に書いていたので「多分これReactの流儀に反してるな」と作りながら思っていました。具体的には
- Stoneが状態を持っていいのか
- Gameが持つ石全体の情報およびこれまでの履歴ってReact的な状態?とりあえず普通のインスタンス変数として持つか(あかん)
- 石全体情報(Gameのstones)って普通に書き換えちゃっていいのかな
そういえばReactのチュートリアルは○×ゲームだけどどうなってたっけと読み直してみたら疑問に思っていたことはすべて書いてありました。すなわち、
- Stoneにはstateは持たせない。親コンポーネントからpropsで渡されたものを使って自身を描画する。クリックされたことは親に上げて親で状態の更新を行うべし。さらに描画以外にメソッドも必要ないのでクラスではなく関数で書けばよい。
- 履歴も状態である。
- ミュータブルはよくない。イミュータブルにすべし。先にArray.slice()して複製を行え。
ということでチュートリアルに従って書き換えたら、
大体チュートリアルと同じプログラム構造になりました(笑)1
https://gist.github.com/junjis0203/4b26b3ef265879aa264f139f2ec13b5a#file-fliptrip-react-html
Vue.js実装
次にReact版をVueに移植。
というより、Vueを使ったことがなかったので簡単なプログラムを移植してみてReactとVueの違いを眺めるというのが目的です。
というわけでガイドを読みながらまったり移植しましたが2時間ぐらいでできました。
https://gist.github.com/junjis0203/4b26b3ef265879aa264f139f2ec13b5a#file-fliptrip-vue-html
Reactとここが違うなと思った個所は
- dataの書き換えが普通の代入文でできる(裏では黒魔術が使われているようですが)
- render関数ではなくtemplateなのであれこれできない。computedを使えばどうにかはできる。まあ処理が書けるとすぐに複雑化するのでtemplate式の方がいいと思いますが。と思ったらrenderも書けるのですね。
なおVue的にはapp.number = 4
とするとReactiveに石の数が増える(ゲーム状態はリセット)ようにすべきな気がしますがそこはできていません。
→Vueのドキュメント読み直してやる方法を把握・実装しました。後述。
状態を保持するべきコンポーネントとイベントディスパッチの話
で、また浮上したのが「Vueの場合、状態は石が持つの?ゲームが持つの?」ということで、ガイドをざっと眺めた感じでは特に記述はなく、
「コンポーネントにdataを定義していいのだから石が持っていいのだろう。でもゲームにクリックされたことを使えないといけないし」とガイドを探したところ、イベントディスパッチの方法を見つけました。これを使うと石から盤、さらにゲームへのクリック通知が以下のように書けます。
Vue.component('stone', {
methods: {
handleClick: function() {
this.toggle = !this.toggle;
this.$emit('stone-click', this.index);
}
},
template: `
<div class="stone-wrapper">
<div
class="stone"
v-bind:class="classObject"
v-on:click="handleClick"
/>
</div>
`,
Vue.component('board', {
methods: {
handleClick: function(i) {
this.$emit('stone-click', i);
}
},
template: `
<div className="board">
<stone
v-for="stone in stones"
v-bind:key="stone.key"
v-bind:index="stone.index"
v-on:stone-click="handleClick"
></stone>
</div>
`
Vue.component('game', {
methods: {
handleClick: function(i) {
// 反転、履歴への積み、勝ち負け判定
}
},
template: `
<div class="game">
<board
v-bind:number="number"
v-on:stone-click="handleClick"
/>
<div class="message">
{{ message }}
</div>
</div>
`
Reactの場合、イベントハンドラのプロパティが渡されてるか調べてから呼ばないといけませんが、この仕組みなら渡されなくてもイベントが無視されるだけなので安全ですね。
number再代入時に石の数が増えるようにする処理
単純移植したものだとapp.number = 4
としても石の数がReactiveに変わらなかったのですがドキュメントを読んでたらReactiveにする方法がわかりました。
元々の実装では以下のように渡されたnumber
からdataとしてstones
を作っていました。これだとnumberが変わってもstonesは変更されません。
Vue.component('board', {
props: ['number'],
data: function() {
const stones = [];
for (let i = 0; i < this.number; i++) {
stones.push({index: i});
}
return {
stones: stones
};
},
template: `
<div className="board">
<stone
v-for="stone in stones"
v-bind:key="stone.index"
v-bind:index="stone.index"
v-on:stone-click="handleClick"
></stone>
</div>
`
stonesを作っているのはv-forが要素繰り返しだけでインデックス的な繰り返しが書けないと思っていたためですが、実はできることがわかりました。
整数ループを使うと以下のように簡潔に書くことができます。
Vue.component('board', {
props: ['number'],
template: `
<div className="board">
<stone
v-for="i in number"
v-bind:key="i"
v-bind:index="i"
v-on:stone-click="handleClick"
></stone>
</div>
`
これで石の数は変わるわけですがゲーム状態をリセットしないといけません。これにはウォッチャを使用します。
Vue.component('game', {
props: ['number'],
watch: {
// reset game state when number is reassigned
number: function(newNumber, oldNumber) {
const stones = Array(newNumber).fill(false);
this.history = [stones.slice()];
this.fulltrip = this.error = false;
}
},
また石も全部白に戻さないといけません。$children
を使うことで実装が行えますが、$childrenは使用を控えてくださいとなっていますね2。より良い方法を見つけたら書き直したいと思います。
Vue.component('board', {
props: ['number'],
watch: {
// reset stone state when number is reassigned
number: function(newNumber, oldNumber) {
for (let c of this.$children) {
c.toggle = false;
}
}
},
おわりに
というわけで秘密ノート/ビットとバイナリーで紹介されているフリップトリップをReactとVueで実装してみました。Vueは初見でしたがなかなかおもしろいなと思いました。
さて、フリップトリップですが必勝法があります。それは・・・、自分で読んで確認してください(笑)
https://note11.hyuki.net/