この記事で扱っていること
- フラッシュ暗算ゲームを作る方法
を紹介しています。
注意:すべてのエラーを確認しているわけではないので、記事の内容を実装する際には自己責任でお願いします。また、エラー配線は適当な部分があるので適宜修正してください。
フラッシュ暗算とは、短い間だけ表示される四則演算を素早く次々に行っていくもので、算盤ができる人が得意な印象があります。
ただそこまで極めないにせよ、簡単な四則演算を次々に行って、既定の問題数を解き終わるまでのタイムを競うのは誰でも参加でき盛り上がれると思います。
この記事では、なるべく一つのループで、ステートマシンをベースにLabVIEWで簡単に遊べるフラッシュ暗算を作る方法を紹介しています。
どんな結果になるか
フロントパネルにはピクチャ表示器と回答欄があります。

プログラムを実行すると最初に難易度選択、次に問題数の選択画面が出ます。
これらの画面ではキーボードの上下ボタンで選択肢を選びReturnキーで決定ができます。

その後、指定した難易度、問題数の問題が出るので、制限時間内に解けるか、何秒時間を余らせられるかを競うことができます。
ここでいう「難易度」とは、「かんたん」の場合には足し算と引き算だけ、「ふつう」は掛け算が加わり、「むずかしい」では割り算も追加、といった区分けとしています。
見た目の配置に特別気を使っているわけではないので、以下の図のように、問題の式が「真ん中」ではない位置に表示されますが、ここはパラメータを変えれば適宜調整できる部分になります。

プログラムの構造
プログラムはステートマシンをベースにしています。
イベントストラクチャを使用したプログラムにすればユーザーの入力を受け付けるまで待機することができますが、今回はイベントストラクチャを使用していません。
イベントストラクチャは便利なのですが、LabVIEWを使い始めの方にとっては触りづらい場合もあるので、入力待ちについては同じステートを繰り返すことで対応しています。
また、ピクチャ表示器の表示を何度も更新することになるのですがピクチャ更新は多少時間がかかる処理なので必要な時にだけ表示を更新するようにします。
そのために、ステートマシンでありながら、「複数先のステートまで指定する」仕組みをつけるために、ステートを指定する列挙体を配列で扱っています(キューを使える方はキューで実装しても問題ないと思います)。

まずはinitステートです。
いわゆる状態の初期化として、ステートマシンで扱っているクラスタの一部のパラメータに対し初期値を指定しています。
クラスタはタイプ定義していて、以下の図を参考にしてみてください。
クラスタの中に「クラスタの配列」が入っています。
また、ユーザーの入力を受け付けるために、キーボードを初期化関数を使用してこの関数のデバイスID出力をシフトレジスタに配線するようにしておきます。

使用しているサブVI、create message.viは、文字通り表示するメッセージを生成するのに使用しています。
このサブVIに渡したitem nameの文字列とoptionの文字列配列とで、ユーザーに表示するメッセージを構成し、selected itemの数値で、どの選択肢を選択しているかの矢印を表現することができるようにしています。

次にupdate pictureステートです。
こちらではピクチャ表示器の表示状態を更新するだけで、その内容はクラスタの2Dピクチャの情報をそのまま使っています。
2Dピクチャ表示器の表示状態はこのステートでしか変更しないので、後々のステートで画面表示の内容を変える場合には必ずこのステートにいきつくこととなります。

次はselect levelステートです。
難易度の選択をユーザーに行わせ、ユーザーが決定(Returnキー)を押すまで次のステートには進まず待機します。
Returnキー以外には、上下矢印キーが押されると、画面上で現在選択している選択肢を指し示す矢印が上下に移動するようにします。

中にあるケースストラクチャではそれぞれ以下のケースを実装しておきます。
initででてきたcreate message.viをSelectedとChange modeで使用しています。

ここで新しく出ているサブVIを2つ紹介します。
manage pressed key.viでは、ユーザーがキーボード上で押したボタンに応じて4つのパターンを識別しています。
4つとは、上にも書いた、「Returnキーを押す」、「上(下)矢印キーを押す」の3つと「これら以外のボタンを押すor何も押さない」の4つです。
押されたかどうかは、入力データを取得して、その出力の列挙体のいずれかが1D配列検索で見つかるかどうかで判定させます。
同時に複数のキーが押されることを想定していないですが、難易度選択の時に例えば上下キーを同時に押すというのは意味のない操作となるので特に対処していません。
どのキーが押されたかをブール値でそれぞれ判定しこれらブール値を配列としてさらに数値に変換することで、4パターンを数値として区別できるようになります。

もう一つ使用しているselect item.viでは、キーボードの上矢印が押されたらitemを1増やし、下矢印が押されたらitemを1減らしています。
このitemというのはcreate message.viのselected itemに配線するものであり、難易度は3段階しかないので、必ず0か1か2である必要があるため、範囲内と強制の関数を使用して値を制限しています。

メインのステートマシンに戻って、次はselect number of problemのステートです。
こちらは先ほど難易度を選択させたのと同じような感じで問題数を選択させる部分となります。

構造も、難易度を選択させるときと似ていて、中にあるケースストラクチャの各ケースは以下のようにします。
Selectedケースにおいて、number of problemsが、これまでは問題数を表す指標番号の意味合いだったのに対し(例えば「0」なら問題数が10、「2」なら問題数が30だった)、このステート以降は問題数そのもの(10とか30とか)を表すパラメータとして使うことにしています。
(本来プログラムの途中でパラメータの意味合いを変えるのは混乱を招きますし、ステートマシンのような「前のステートに戻る」可能性があるプログラムの場合扱いが複雑になってしまうので避けるべきで、代わりに別のパラメータを設ける方がいいのですが、今回のステートマシンはこの先で難易度や問題数を選択させる状態に戻ることがないため横着しています)

次にready countステートです。
難易度および問題数を選択させたら、3、2、1とカウントしてから問題が始まるように、カウントダウンするような表示を行わせるためのステートです。
時間が経過したかどうかは経過時間Express VIを使用して判断させています。
厳密に言うとこのケースはupdate pictureステートと繰り返しながら実行され、正確に3秒待つわけではないですが、あくまで問題数選択の後に間髪いれずに問題がスタートする状態を避けるためだけの目的なので、そこまでの厳密さは必要ないと思いこのような作りとしています。(そもそもWindowsのシステムでそもそも厳密に3秒という指定をミリ秒単位で3.000秒として指定することは無理ですし)

次にcreate problemsステートです。
難易度に合わせた問題を作成していきます。
このステートは上のready countステートよりも前に実行されても構いませんが、順番を変える場合には次にどのステートに移動するかについては慎重に選ぶようにしてください。

使用しているサブVIの一つ、create problem.viの中身は以下の通りです。
problem rangeという入力は、select item.viの結果であるlevelに2を加えた値が入ることになりますが、これに乱数をかけて切り上げ整数化することで、1~4の値がランダムで得られることになります。
この1~4にそれぞれ足し算、引き算、掛け算、割り算を対応させれば、ランダムでそれぞれの計算式を出題できるようになります。
各四則演算で、計算の対象になる値a、bと、答えになるanswerをそれぞれこちらも乱数で用意します。
以下の図で各演算をケースストラクチャで分けて何か複雑なことをしているように見えるかもしれません。
割り算以外は単に乱数を二つ生成して足したり引いたり掛けた値をそのままanswerにすればいいのですが、割り算は今回のプログラムでは必ず割り切れる組み合わせを出題する必要があり、この形式としました。
ここはもう少し工夫できるポイントだと思います。

create problemsステートでもう一つ使われているdraw problem.viは以下のようにしています。
式をピクチャに表示するときに、+や-、×、÷といった記号も線や丸で表すためにこれらの記号表示を生成しています。

各記号表示の一例は以下の図を参考にしてみてください。
足し算、引き算は単に線を引くだけなので比較的シンプルです。

掛け算も線だけでできていますが、斜め線になっているので注意が必要です。
また、割り算は丸も書くので横棒とのバランスも調整してみてください。

次にaccept user inputステートです。
ユーザーの入力を受け付けるのがこのステートであり、OKボタンが押された時点で入力欄の文字列と答えであるanswerを比較し、同じなら正解ということで次のステートであるjudge continueに進みます。
また、このステートが実行されている間に経過時間を測っています。
このステートが実行されるたびに一度だけティックカウント値を読み取り、最初に読み取った値との引き算(を1000で割った値)を経過時間(秒)としています。

OKボタンが押された場合に入力欄の数値とanswerが不一致の場合にはaccept user inputが繰り返されるようにしています。
ただしこのときにはキーフォーカスが入力欄に自動的にならないので、OKボタンを押してから再び入力欄文字列にキーフォーカスを当てる必要がある分のタイムロスが発生するように仕向けます。
また、文字列入力の値も消えないので、間違った答えを消す分のタイムロスもあわせて発生します。
OKボタンが押されていない場合でもひたすらaccept user inputステートを繰り返すようにしておきます。

次のjudge continueではプログラムを続けるかどうかを判定します。
決めた出題数と今何問目かという情報を照らして、これらが一致したらfinishステートに移行しますが、一致しない場合には次の問題を表示するように再びdraw problem.viを使用して2Dピクチャの値を更新、update pictureを経由してaccept user inputに進むようにします。

最後、finishステートでプログラムは終了です。

間違えた場合に時間的ペナルティを課す場合
上記プログラムでは、もし間違った答えを書いた場合、キーフォーカスが自動的に移らなくなったり間違った答えを消す分の時間的ロスは発生するにせよ、そこまで大きなペナルティはありませんでした。
ですが敢えて明確にペナルティを課す、例えば間違えるたびに経過時間が5秒余計に足されるといった作りにする場合には以下のようにプログラムを修正します。
アプローチとしては、間違えた場合にだけTrueになるようなブール値を新たに設け、これがTrueとなる回数を、間違えた回数としてカウントし、これにペナルティ時間(例えば5)を掛け算して、これまでにあった経過時間を測る機能の最後に足してあげます。

本記事では、フラッシュ暗算ゲームを作る方法を紹介しました。
扱う数を変えれば、2桁以上の計算にも対応できるかと思うので、簡単すぎる場合には調整してください。
プログラムの作りとしても配列で列挙体を扱うステートマシンの参考になればうれしいです。
ここまで読んでいただきありがとうございました。
コメント