この記事で扱っていること
- 1回だけウソをついても正解できる数当てゲームの作り方
を紹介しています。
注意:すべてのエラーを確認しているわけではないので、記事の内容を実装する際には自己責任でお願いします。また、エラー配線は適当な部分があるので適宜修正してください。
この記事が出ているのは4月1日、エイプリルフールということで、ウソにまつわるプログラムを書こうと思いました。
そこで、一度だけウソをついても正解を導ける数当てゲームを作ります。
ただ、これだけではつまらないので、色々なデザインパターンで表現する方法も併せて紹介しています。
LabVIEWは基本的にはハードウェアを操作したり演算解析したりと「真面目な」プログラムを書くのに使用しますが、たまにはこのような息抜きのゲームを作ってみるのも楽しいし、かえってアルゴリズムが複雑だったりするので、普段組まないようなプログラムを組むことで、いいLabVIEWの練習になると思います。
どんな結果になるか
フロントパネルには数当てゲームを進めるための文字列表示器と、ブール制御器が3つあります。
ゲームの遊び方としては単純で、プログラム実行前に、1から15までの数で好きな数を一つ思い浮かべ、それを当てるというものです。
プログラムを実行すると、数字の羅列がランダムに表れます。
表示されている数の中に、思い浮かんだ数があれば「ある」を、なければ「ない」を押していきます。
ただし、1度だけなら、本来は表示されていないのに「ある」とウソをついて回答してもいいこととします。(ウソをつかなくてもいいですが、ウソを2回以上つくと正解は導けません)
プログラムの構造
これは数学パズルのようなものです。
ウソをついたかどうかを判定する仕組みがあり、ウソを見破った後、選ばれた数字の羅列の中から、共通する数字を探り当てます。
特定のアーキテクチャを用いない、シンプルなプログラムで実現できるので、まずはそのやり方を紹介します。
ブロックダイアグラムは以下の通りです。
最初のForループにて、プレイヤーに質問を投げかけるパートがあります。
このときに質問で出てくる選択肢(数の羅列)のパターンは実は決まっており、以下の図の一番左にあるクラスタ配列の定数のようにしています。
数の羅列の法則は、記事後半の「ウソを見破る仕組み」を見てください。
このクラスタには数字の配列と文字列が要素として含まれており、固定値としていますが、質問をする時点で一つのクラスタ要素にある数字の配列の要素の順番は変わってもいいので1D配列シャッフルの関数で適当に要素番号を入れ替えて質問を出しています。
プレイヤーが質問に答えたら、「ある」と回答をしたものだけForループから取り出します。
このとき、「ある」の数が7個以上であれば、プレイヤーが思い浮かべた数は「15」で決定です。
もし6個以下であれば、プレイヤーがウソをついたかを見破る処理を行い、その後にどの数字を思い浮かべたかを調べる処理をして、最終的な答えを導きます。
これらの一連の処理はそれぞれサブVIにまとめています。
ひとつめのサブVI、mark count.viの中身は以下の通りです。
今回、ウソがあるかを見破るためには印(マーク)を用いており、プレイヤーがあると答えたクラスタ要素に対応するマークを一つずつ調べて、4種のマーク「A」「B」「C」「D」が何個あったかを調べています。
もしウソが含まれていた場合、マークが1つ以上「奇数個」になるのですが、それぞれのマークの数を情報として出力しています。
それぞれのマークの数を受け取り、どのマークが奇数個だったかを調べて、奇数個のマークに対応するクラスタ要素を取り除くのが、次のcreate true array.viです。
前のmark count.viで数値配列として受け取った、各マークの出現回数の結果を2で割って、配列要素のAND処理を行うことで、一つでも奇数があったかを判定しています。
奇数があった場合には数値配列の各要素を2で割った結果をcreate mark string.viに渡し、奇数個判定を受けたマークだけの文字列を生成するようにしてから、それらの結果を「ある」と答えた配列と照らして、どの配列要素でウソをついていたかを判定、それを取り除いて配列出力としています。
create mark string.viは以下のような単純な構造をしています。
これでやっと、ウソの無い、「ある」に分類されるべき配列要素だけになったはずなので、あとはその中でどの配列にも含まれている数を絞り込むだけです。
ここでは、コレクションタイプのセットを使用し、共通集合を導くようにしています。
この処理を行うことで、どの配列要素にも共通する要素だけに絞り込むことができます。
一応これでプログラムの説明は終わりです。
余力のある方は次の項目も読み進めてもらえればと思います。
ステートマシンでの実装
LabVIEWのいいところは、データの流れを線でつなげばプログラムが書けることにあります。
上記のプログラムは、記事後半にある「ウソを見破る仕組み」を基にシーケンシャルにそのまま実装した形としています。
今回のような単発のプログラムでは他の実装を試す必要はないと思いますが、上記のプログラムを敢えてステートマシンで実装する場合の例も紹介します。
4つのステートを定義しています。
最初のinitializeステートでは、質問を投げかけるための配列要素を用意しています。
また、プレイヤーが「ある」と選んだ要素を格納しておくための配列もあらかじめ用意しておくようにしています。
questionステートでは、プレイヤーに質問を投げます。
ここでの実装としては、質問するための配列から要素を一つ削除し、その削除した要素が問題としています。
質問するための配列の要素数が0になったら次のjudgeステートに進みますが、それまではquestionステートを繰り返します。
「ある」に選ばれた要素は配列連結追加でどんどん追加していきます。
judgeステートでは、既に紹介した3つのサブVIを使用して数を当てるようにしています。
最後はendステートです。
もしプレイヤーが途中で終了ボタンを押したらゲームは途中で終了しましたと表示させますが、それ以外は特に何もすることなくWhileループを止めてプログラムを終了します。
やっていることはシーケンシャルな処理なのでステートマシン本来の強みである自由な状態遷移を行わせる必要はあまりないのですが、同じことを様々な組み方で考えることは、LabVIEWでできる事の幅を広げることにつながります。
キューメッセージハンドラでの実装
今回のプログラムに対してはオーバースペックですが、同じことをキューメッセージハンドラの考え方で実装することもできます。
利点があるとすれば、ローカル変数を使用しなくなる、ということになりますが、今回のプログラムでは本来ここまでやる必要はなく、あくまでキューメッセージハンドラの練習というような位置づけでいいかなと思います。
注意点としては、いつでも途中でプログラムを止められるように、「ユーザーの入力がないと終わらないWhileループ」を作らないことです。
今回のプログラムではユーザーの入力は「ある」か「ない」(終了ボタンはゲームの結果に関係ないので除きます)ですが、これらの入力を待ち、入力があったらそれに応答するという動作をいかに実現するかがポイントです。
まずは、プレイヤーの入力をイベントとして検出する、Event Handling Loopです。
今回は短いプログラムであるためどのようなタイミングで「ある」や「ない」が押されてもいいのですが、本来はプログラムの現在の状況を基に各ボタンが押された影響がプログラムにどう影響するかを考える必要があるため、敢えて「User Input」というステートでプレイヤーからの入力全てを受け止める方針にしています。
次にMain Control Loopです。
以下の図で特に明記していませんが、このループにシフトレジスタで回しているクラスタはdata listとchosenの二つの要素しか持っておらず、これらはステートマシンのプログラムの説明で出た「質問を投げかけるための配列要素」と「プレイヤーが「ある」と選んだ要素を格納しておくための配列」のことです。
なのでinitステートではこの二つの要素を初期化しています。ここはステートマシンのInitializeステートとやっていることは同じですね。
次にQuizステートに進むのですが、ここでQuizという列挙体をシフトレジスタで渡しています。
さらに、UI Loopに対してTake Quizというメッセージを送っています。
これにより、UI Loopで問題を投げかける文字を表示するのですが、その際に数字の配列が必要なので、data listのクラスタ配列から指標配列の関数で0番目の要素を取り出してmessage dataとしてUI Loopに渡しています。
プレイヤーが「ある」か「ない」を押した場合に、その結果はUser Input内で扱われますが、このステートの中身にはケースストラクチャを設けて、「今プログラムはどのような状態で、その状態に対してプレイヤーからの入力をどう扱うか」を決めています。
Quizステートにて、「Quiz」という列挙体を入れていたため、User Inputステートの中のケースストラクチャは「Quiz」が実行されます。
その結果、次にProcess Quiz Answerに移行することになり、プレイヤーが選択した「ある」や「ない」の結果はProcess Quiz Answerに引き継いでいます。
Process Quiz Answerステートでは、ステートマシンのプログラムで紹介したのと同じような処理をすることで、再び質問を投げかけるかあるいはJudgeステートに移るかを決めています。
Judgeステートに移ったら、例の3つのサブVIを使用して結果をUI Loopに渡します。
それ以外のCancelやEndステートは以下の図の通り実装してやることにします。
最後にUI Loopの紹介です。
基本的にMain Control Loopから必要なメッセージおよびメッセージデータを受け取ってフロントパネルの文字列表示器に文字を表示するためだけの役割を担います。
Main Control Loopの各ステートと照らし合わせながら確認してみてください。
キューメッセージハンドラを使用することで、文字列表示器のローカル変数がなくなりました。
今回のプログラムは小規模なのでローカル変数を使ってもたかが知れていますが、ある程度大きなプログラムであってもこのようにキューでそれぞれの役割ごとに処理を分けていくことでキレイに書くことができます。
ウソを見破る仕組み
今回のプログラムでウソを見抜く仕組みは「文字数チェック」にあります。
それぞれの数字の羅列には「A」や「BC」などといった文字が付与されています。
この情報を基に、ウソがあったかを見破ります。
全てのあるなしに答えた後、あると答えられたものに対してこれらの文字列を並べたときに、A、B、C、Dがそれぞれ偶数個含まれている場合にはウソをつかれていないと判断します。
もし一つ以上の文字、例えばBとCとDとが奇数個の場合、「BCD」という組のあるペアにひもづいた数字群には考えていた数字がなかったのにあるとウソをついていたと判断できます。
こういった「誤りチェック」、何かのデータの送受信の際にデータの改変があっても元のデータを復元するような場合に利用されたりもして様々な種類があると思いますが、一部の情報に誤りがあっても正しい答えを導けるのは便利ですね。
(読者の方で、今回の誤りチェックに使用している復号プロセスの名前を知っている方いましたらぜひコメント残してもらえるとうれしいです)
本記事では、1回だけウソをついても数が当てられるゲームの作り方について紹介しました。
アルゴリズムを考えるのも、それをプログラムで表現するのも、普段とは異なる趣向のものを紹介してみました。
同じことをやるにせよ、様々なデザインパターンでプログラムを書く練習としても参考になればうれしいです。
ここまで読んでいただきありがとうございました。
コメント