この記事で扱っていること
- オセロを作る方法
を紹介しています。
注意:すべてのエラーを確認しているわけではないので、記事の内容を実装する際には自己責任でお願いします。また、エラー配線は適当な部分があるので適宜修正してください。
LabVIEWでオセロを作ってみました。
ルールは今さら言う必要もないと思うので説明は割愛します。
2人対戦で行うのが基本だと思いますが、今回は疑似AIとして、CPUとの対戦モードの実装もしています。
どんな結果になるか
フロントパネルにはオセロの盤面を表示するためのピクチャと、白黒どちらの手番かを示す文字列表示器を配置しています。

プログラムを実行すると、ゲーム盤面が表示され、白と黒交互に置ける場所にコマを配置しどちらかのコマが無くなったり盤面が全て埋まるとゲームが終わります。
後でプログラムを見ればわかりますが、盤面やコマの色はもちろん好きなように変えられます。

プログラムの構造
全体の構造は、最初に盤面を作ってからはイベントストラクチャが入ったWhileループがあるだけのシンプルな構造としています。
イベントも二つしかなく、そのうちの一つは停止ボタンが押された時にプログラムを停止させるだけなので実質イベントは一つしかありません。

プログラムの最初ではまずプログラム全体で使用するクラスタの初期化を行っていきます。
クラスタの中身は以下の図を参考にしてください。
クラスタの中にある、board statusという列挙体は、「empty」「black」「white」の三つの値を持つのに対し、piece(=コマ)という列挙体は「black」と「white」の二つの値しか持ちません。
その後、盤面を作成するためのサブVIであるcreate board.viを実行し、盤面上の特定の場所に特定のコマを配置するためのdraw piece.viを実行します。

create board.viはピクチャ系の関数を使って正方形をひたすら書いていく作業になります。盤面は8×8の大きさとしてそれぞれのマス目(プログラム中ではcellと表記)の大きさもcell sizeとして固定しています。
これらの情報を使ってピクチャ関数で盤面を作れます。

また、draw piece.viでは盤面上の指定した座標に丸を書いていく動作となります。
オセロの場合、一度あるマスに黒か白のコマが置かれると、それらが反転することはあってもマスからコマが消えることはないため、このサブVIが実行された時にはそのマスに描かれるものは黒コマか白コマかの二択となるため選択関数を使用しています。

ではメインviの方の大きなWhileループに入っていきますが、イベントストラクチャで定義したイベントで重要なのはboard pictureのマウスダウンイベントだけです。
つまりユーザーがオセロの盤面のどこかをクリックした際に発生するイベントで、プログラム側としては
- ユーザーが押したマスにはコマが置けるか
- コマが置ける場合に盤面はどのように変化するか
を処理すればいいだけです。
コマが置けるかどうかで場合分けがあるため、イベントストラクチャの中にケースストラクチャがあり、まずはコマが置けるかどうかの判定をsearch around.viとcheck empty.viで行っていきます。

search around.viではユーザーがクリックしたマスを中心として、0、45、90、135、180、235、270、315度の全8方向に対して「どのような変化が起きるか」を調べるための関数です。
中身としては、それぞれの方位を調べるための細かいサブVIに分かれています。

一見複雑そうですが、それぞれの方位を調べるうえでやることは一緒であり、各サブVIの中身も角度ごとに処理を変える必要がある部分と、共通している部分に分かれます。
どの方位に対しても処理を行って、それぞれで「coordinates」という座標を表すクラスタと、resultという列挙体配列の二つの要素を持ったクラスタ、coordinate and conditionを取得するとともに、その方位に対して反転するコマの数を示すnumber of reversed pieceを取得することを目指します。

角度ごとに処理を変える必要がある部分でやっていることは単純で、例えば0度に対しての処理については、ユーザーがクリックした盤面上のマスから見て0度方向に、このマス自身を除いて、右方向にひとつずつ、盤面の端に来るまでの座標を配列として取得します。
そのため、ユーザーがクリックした盤面上のマスをこれからコマを置く予定のplace areaと考えると、その座標に対し垂直方向の値(Vertical)は変えずに、水平方向(Horizontal)の値を1ずつ増やして盤面の端(board limit)になるまでの座標を取得します。
他の角度に対しても全く同じで、それぞれの角度ごとに垂直方向、水平方向に1ずつ増やすあるいは減らす処理を盤面端になるまで続けてその方向の座標全てを取得します。
この時点ではその方向に黒あるいは白のコマが置かれているかどうかは考慮していません。

どれも似たような処理をしていますが、角度によって少しずつ処理の仕方が変わるので、面倒でもこれら8種類は全部用意します。

各角度方向に存在するマス座標を取得したら、それらマス座標の現在の盤面状況をboard statusと照らし合わせて確認するためのサブVIがsee effect.viです。
Board statusとしては、何もコマが置かれていないemptyか、白コマが置かれているwhiteか黒コマが置かれているblackのいずれかになっているはずで、これらのboard statusの配列を取得します。
このsee effect.viは各方向に共通して使用されるサブVIなので、クローン設定にしています。

次に、各board statusの配列に対して判定を行い、実際に盤面が変わりうるかを確認するための関数がjudge place.viです。
あるマスにコマを配置できるかの判断は、そのマスからある方向(先ほど調べた8方位それぞれ)をみたときに
- すぐ隣のマスにコマが配置されている(マスが空でない)
- すぐ隣のマスに、反対の色のコマが配置されている
- 配置する予定のコマと異なる色のコマが1つ以上あったあとに配置する予定のコマと同じ色のコマがある
- 配置する予定のコマと同じ色のコマが出るまでに空のマスがない
という条件を満たすかで決めます。
judge place.viではこれらの条件判定を行っています。
(書いていて気づきましたが、「隣のマスにコマが配置されている」と「隣のマスに反対の色のコマが配置されている」は、後者のみ判定すれば十分ですね)

長かったですが、これでsearch around.viは終わりです。
あともう一つメインVIで使っていた、ユーザーがクリックした盤面が空であるかの判定を行うcheck empty.viは以下のようにいたってシンプルです。

search around.viとcheck empty.viの結果を踏まえて、ユーザーがクリックした場所にコマを配置できるとわかったら、board statusと実際のピクチャ上の表示状態を更新していくために、イベントストラクチャの中ケースストラクチャTrueの中身へ移ります。
ユーザーがクリックした場所に配置できるのは確定しているので、その部分のピクチャ表示を変えるためにdraw piece.viを実行しています。
また、どの方位でどのように盤面が変わるかもsearch around.viで既に情報を取得済み(coordinate and conditionクラスタ)なので、この情報を基にboard statusとピクチャの更新を行います。

Board statusの更新はupdate board status.viで行っています。
Search around.viの結果をboard statusに反映させています。

盤面の更新が終わったら、次は白黒どちらの手番かを判定します。
ゲームの序盤では白と黒が交互に順番が回ってくるはずですが、盤面の状態によっては、片方が置ける場所がなくなるという状態がありえて、その場合二連続で白、または黒の手番となる可能性があります。
実際はもっと効率のいい判定方法があるかもしれませんが今回やっていることは単純で、現在の盤面で空(empty)になっている座標を全て一度取得し、それらに対してsearch around.viを実行、その結果一つでも「次の手番の人がコマを配置できるマスがある」のであれば手番を更新、なければ引き続き同じ色の手番となる、といった判定をします。
この判定を行うnext player check.viにはdifferent?というブール出力を行っていますが、この値は記事後半で紹介するCPU対戦時に使用する値となります。

メインVIでイベントが終わったら、盤面の状態を確認し、ゲームが終了かどうかを判定します。
その判定はjudge game end.viで行います。
もし終了と判定されたら、勝ち負けを表したメッセージをcreate result message.viで作成し、1ボタンダイアログの関数でそのメッセージを表示します。

judge game end.viでは、board statusに対して「空が一つもない」「白が一つもない」「黒が一つもない」のいずれかの条件が成立しているかを判定しています。
このどれも条件を満たしていない場合にはゲームが続くと判定しています。
(実際はこの判定方法だと、「空となっているマスはあるが白も黒も置ける条件を満たせない」という場合に終了判定ができないという「ヌケモレ」が発生してしまいますが、今回はその判定は行わせていません。代わりに停止ボタンを押すことでゲームをいつでも止められるようにしています。)

create result message.viでは盤面の白の数、黒の数を数えてそれらの結果に応じmessage文字列出力を変えています。

CPUとの対戦モードも実装する
上記のオセロは二人対戦形式ですが、一人でも遊べるように、CPUとの対戦モードも付け加えてみます。
ただし、本格的なCPUではなく、あくまであるルールに従って動作する、ランダム性は多少ありますが、シンプルなCPUとします。

CPU機能を持たせるために、タイムアウトイベントを追加します。
CPUの動作はこのタイムアウトイベントの発生で模擬します。
そのために、タイムアウト値を決める部分含め少しだけプログラムに修正を加えます。

上記の図に示したタイムアウトイベント内では、コマを配置する場所をAI.viが判定します。
AI.viに好きな判定方法、ロジックを入れればいいのですが、今回はある単純なルールでコマを配置するマスを決定します。
その「ルール」とは、「自分の番で最も多くのコマをひっくり返せるマスの場所から一つをランダムで選ぶ」という内容とします。
簡単に言えば、常にその瞬間で反転させられるコマの数が最大となるような場所を選び続けるように動作するということです。
そのような手がオセロの最善手ではない(強いムーブではない)ですが、前準備も必要ない形で自動的にコマを置く場所を選ぶルールとしては実装しやすいためこのようにしました。
そのための処理として、AI.viではnext player check.vi同様、そもそも配置できる可能性がある空のマスを全て調べ上げてsearch around.viを適用し、それぞれのマスで変化させられるコマの数をまとめ、この結果をCPU hand.viに渡しています。

CPU hand.viは反転できるコマの数(score)が最大となる、コマを配置できるマス座標の配列からシャッフルして1つの要素を決定します。

なお、Ai.vi以降の、タイムアウトイベント内の処理は、board pictureのマウスダウンイベントと同じで、ケースストラクチャも同じものを流用出来ます。
あとは、AI.viの追加以外にCPU対戦用のプログラムではCPUの動作を行わせるためのタイムアウトイベントをいつ行わせるかを指定する必要があります。
イベントストラクチャのタイムアウト値に例えば100と入れたら100ミリ秒後にタイムアウトが発生しCPUがマスを選ぶのに対し、-1と入れたらタイムアウトが発生しない=プレイヤーの番と判断します。
このタイムアウト値をどうするかを決定するために、next player check.viのdifferent ?出力を用います。
board pictureのマウスダウンイベントで、next plaer check.viからのdifferent?出力からイベントストラクチャの右上の選択関数に渡し、Trueだったら0(つまりCPUの番)、Falseだったら-1(つまりプレイヤーの番)となるようにします。
イベントの中のケースストラクチャがFalseの場合には、自分の手番を継続させるためにFalse定数を選択関数に渡すようにします。

これはタイムアウトイベントでも同様ですが、Trueのときに-1、Falseのときに0と、先ほどと反対になります。

本記事ではLabVIEWでオセロを作る方法を紹介しました。
コマを配置できる場所の判断などもっと効率のいい判定の仕方等あるかと思いますが、今回紹介した実装方法でも途中で処理に時間がかかる部分があってテンポが悪くなるということもないので十分遊べます。
LabVIEWでのゲーム作りの参考になればうれしいです。
ここまで読んでいただきありがとうございました。
コメント