この記事で扱っていること
- ポーカーを作る方法
を紹介しています。
注意:すべてのエラーを確認しているわけではないので、記事の内容を実装する際には自己責任でお願いします。また、エラー配線は適当な部分があるので適宜修正してください。
今回はポーカーをLabVIEWで表現していきます。
世の中には複数の種類のポーカーがあると思いますが、イメージとしては某有名RPGゲーム中のカジノで遊べるポーカーで
- 5枚の手札が全てオープンの状態で配られる
- 交換したいものを選択する
- 交換後、揃った役を確認する
といった単純なルールとしました。
作成するうえで、それぞれの役の判定をどうするかがポイントになりますが、トランプの種類(13×4に加えてジョーカー1枚で合計53)にそれぞれ固有の数字を割り当てることで処理するようにしました。
どんな結果になるか
フロントパネルには、手札を表示するためのピクチャ表示器、ゲームを進めるうえで必要になるベット数を指定するための数値制御器や残高表示のための数値表示器および各決定ボタンと現在のゲームの状況などを表す文字列表示器を配置しています。

ゲーム開始時には適当な初期値残高があり、一度のゲームでのベット数は、最低1、多くても10(残高が10より小さいのであればその数)として賭けていきます。

ゲームは、残高がなくなるまで止まりません。

プログラムの構造
ゲーム全体の構造はイベントを検出してそのイベントごとに次の動作を決める仕組みを取っています。
そのために基本的な考え方はイベントベースのステートマシンですが、せっかくなので小さなキューメッセージハンドラ風のデザインとしました。
ただし、ある程度決まった順番でイベントが発生する必要があるため、想定していない順番でイベントが発生することを防ぐために工夫が必要です。

Whileループに入る前の段階で、後で必要な要素を集めておきます。
イベントストラクチャを使用したプログラムを使っている場合にプログラムを終わらせるための作法としてユーザイベントを用意しているのに加え、ピクチャ表示器のリファレンスを配列として用意しています。
これは、後で各ピクチャでトランプを表現する際にローカル変数を使用するよりも「場所をとらずに」書く為です。

また、プログラム実行時にユーザに次に何の操作をしてほしいかを表示する部分については、一つの文字列表示器に様々な内容を表示するために、これだけの機能を独立したWhileループに入れています。
メインの処理を行う真ん中のWhileループのところどころのケース(ステート)にて、このメッセージループに対して文字列データを送るようにしています。

一番上のWhileループではイベントストラクチャによってユーザーからの各ボタン操作に反応するようにしています。
イベントストラクチャの中身は、基本的に各イベントが発生したら下のメインループにデータを流していく操作を行っています。
例外は、ユーザイベントと「総入れ替え」のボタンが押された時で、これら以外はキューで下のWhileループに命令を渡していきます。

以下で説明していきますが、実際各イベントはほぼ決まった順番でしか実行させることができないようにしています。
メインループの方ではまずinitializeが実行されます。
ここでは、このメインループをステートマシンの形にしてこのステートマシンの中で扱うデータであるクラスタの一部パラメータ、「残高」を初期化しています。
Initializeが終わったら何のイベントも起こらなくても次に進むようにキューでRefreshステートに移るようにします。

Refreshステートは毎ゲームで盤面をリセットする役割があります。
前回のゲームで押されたボタンの状態なり、BET指定した値なりをリセットします。
このRefreshの時点でブールボタンの一部を無効にすることで、次に発生するイベントが強制されます。
具体的には、ベット数を変更するかベット数を決定するという選択肢しかできないようにしています。

Betステートでは、ユーザーが指定したベット数をステートマシンデータのBET数に入れます。
このとき、ベット数に制限を設けています。
このステートはMAX BETのボタンを押したときにも発生しますが、ベット数の制限は常にこのステートの中で行われるようにしています。

ユーザーがベット数を決定したら、つまりBET決定のボタンが押されたら実行されるのがOpen handステートです。
ここではトランプの出目となりうる53種類の数字をシャッフルし、これをピクチャに反映させていきます。
1D配列シャッフルの関数に渡しているのは0~52までの配列(つまりサイズは53です)となっています。
ピクチャに反映させる際に、単にシャッフル後の出目配列を上から順番に5個選ぶだけでいいのですが、単純に部分配列などで5枚とるのではなく、合計何回入れ替え操作をしたかを数えて、その回数を指標番号と見なして出目配列から指標配列で要素を取得するようにしています。
こうすることで、一度配られたカードの一部を入れ替えることになっても全く同じ処理で入れ替え操作を表現できます。

使用しているサブVI、assign hand.viは、別のステートでも使用しており、ユーザーの「手」と出目を紐づけるためのサブVIになっています。

assign hand.viの中身は以下の通りで、入れ替え指定のクラスタ(を配列に変換したもの)がTrueであれば、state_infoのクラスタの中身のうち、「手」の内容を、出目配列(0~52までの値をシャッフルした配列)の特定の指標(「入れ替え回数」)の内容で置換するという処理を行っています。
Open handのステートの時点では、入れ替え指定のクラスタは全てTrueになっているので、1D配列シャッフルした数字の最初の5この値で「手」の中身を変更します。

picture images.viとhand.viは大したことはしていません。
picture images.viは、あらかじめ用意したトランプのマークの画像を読み込んでいるだけです。

hand.viは、読み込んだ画像情報とともに、0~52の値を13で割った余りに1を加えた数を加えて、ピクチャデータとして返しています。
13で割っている理由は、トランプの一つのマークにつき数字が13まであることに由来し、13で割った商の部分はマークを決定するのに役立ちます。

メインのプログラムに戻り、Open handのステートの実行によって、次に押せるボタンは入れ替え決定ボタンだけになります。
入れ替え決定ボタンが押されると実行されるのがChoose handステートで、入れ替えボタンが押されたカードに対してだけ、先ほどのOpen handで行った操作をassign hand.viによって行うことで、カードの入れ替えを表現しています。

この後はResultステートに進みます。
Resultステートでは、judge.viによってどの役が揃ったかを判定し、それに応じて残高を計算したり、残高が0になったら強制的に終了するといった処理を行います。

役に応じてどれくらいベット数に対してのリターンがあるかは自由に決められます。以下は一例です。

役の判定を行っているサブVI、judge.viの中ではそれぞれの役の判定を各サブVIで行っています。
下位の役から順に、その役が成り立っているかを判定していき、成り立っていたら文字列を上書きするというスタイルとしました。

judge.viにある各サブVIでは、各役を判定するために「13で割る」という操作を多用しています。
ある数を13で割った場合の、余りは0~12の13通りです。
なので、余りの数+1をトランプの数字に見立てることができます。
さらに、割ったときの商をトランプのマークに見立てれば、0~51の数字でジョーカー以外の全てのトランプを一意に表現できます。
そしてジョーカーは割ったときの商が4であるとき、と定義することでジョーカー含めた全てのトランプの種類を判定でき、これらの演算によってツーペアだったりストレートだったり各ポーカーの役を判定します。
各役の判定処理について紹介していきます。
まずは、ツーペアかスリーカードを判定するjudge pairs.viです。
実際の判定はさらに別のcount pairs.viの結果をもとに行っています。

judge pairs.viで使用している二つのサブVIの中身は以下の通りです。
joker_check.viは、ジョーカーがあるかを判定します。
ジョーカーは「手」の中に52があるかどうかで判定しています。
count pairs.viでは「手」の中に同じトランプの数字が1~13でそれぞれどれくらいあるかを判定します。
13で割った余りを一度文字列にして、文字列の検索と置換の関数を使うことで、文字列の中に重複した数字がいくつかあるかを判定し、その重複数として「2」か「3」が何個あるかをさらに判定しています。(言葉で説明するのが難しい部分なので、ぜひこの部分だけ単体で動かして動作を確認してみてください)

次にjudge straight.viです。
ストレートであるかの判定を行うだけですが、ジョーカーがある場合にはそのジョーカーがストレートのどの位置の代替となるかを判定する必要があり、両端か中の3つの数字のいずれかによって処理を分けることになります。

ジョーカーがない場合には、「手」の数値の最小と最大の数の差が4でかつ値がどれも1つずつしか表れないという条件でストレートであるかを判定しています。

judge flash.viではフラッシュであるかを判定します。
これはマークが揃っていればいいので、マークを確認するために13で割った商の部分が全て同じ(最小と最大が同じ)であるかどうかで判定しています。

judge full house.viはcount pairs.viの結果の組み合わせを活用します。
ジョーカーがある場合にはジョーカーを除いてツーペアがあるかスリーカードがあれば成立します。

judge four card.viではジョーカーがある場合にはジョーカーを除いてcount pairs.viでスリーカードがあれば成立、ジョーカーがない場合には重複数が「4」になっているものがあるかで判定します。
実は「ジョーカー抜いた4枚でスリーカードがある」という判定は、フルハウスの成立と同じ条件となりますが、強さ的にはフォーカードが上なので、フルハウス成立を上書きすることになります。

judge straight flash.viでは、これまでに使ってきたストレートとフラッシュがどちらも成立しているかを判定します。

judge five card.viでは、ジョーカーがある場合には残りの手札でフォーカードが成立しているかを判定、ジョーカーがない場合には成立しえないので常にFalseとします。

最後にjudge royal straight flash.viです。
ジョーカーがない場合には、特定の数値の組み合わせが成立するかを集合比較で判定します。

ジョーカーがある場合には要素比較で判定し、特定の組み合わせが4つ以上当てはまっているかで判定しています。

上記の判定方法は一例です。
全ての組み合わせを確認しているわけではないので、ヌケモレがあるかもしれないですし、もっと効率のいい判定の仕方があるかもしれない点はご了承ください。
メインのVIに戻り、最後残ったFinishステートでは、ユーザーイベントを発生させてループを終わらせています。

ダブルアップチャレンジの実装
今回のポーカーは冒頭にも書いた通り某有名RPGのカジノのポーカーを参考に作りました。
その「本家」では、役が揃った場合にはダブルアップチャンスがあり、当たり金を2倍、4倍、8倍、・・・と増やすことができるかもしれないゲームに挑戦できます。
そのゲームには2種類あり、
(a)一つのカードがオープンになっていて、他に4枚のカードが伏せられており、オープンになったものよりも強いカードを4枚の中から当てる
(b)一つのカードがオープンになっていて、他に1枚のカードが伏せられており、伏せられたカードが、オープンになっているカードよりも強いか弱いかを当てる
という種類があります。
(a)の方はカードを5枚使用するので、元々のポーカーゲームのユーザーインタフェースをそのまま流用出来るものの、うまく作らないと「そもそも伏せられている4枚がどれもオープンになっているカードより強くない」状態になりえます。
つまり、絶対勝てない状態がありえてしまうわけで、この状況が起こらないように作らないと公平なゲームにはなりません。
そこで今回はより実装が簡単な(b)のルールとして、ダブルアップ専用の画面をサブviとして表示する方法を紹介します。

ダブルアップで、表示されるカードよりも数字が大きいか小さいかをひたすら判定していきます。
途中でやめることもできますが、一度でも間違えると掛け金が0になってダブルアップは終了します。

メインのVIのResultステートに以下のように変更を加えます。
BET数の結果が0以外であったらダブルアップに挑戦するかのポップアップを表示させ、それ次第でダブルアップ用のviの画面を表示させます。

ダブルアップのゲームのフロントパネルはシンプルに二つのピクチャ表示器とブールボタン二つ、単にゲームの説明を表示するステータスとしての文字列表示器と現在の当たり額を表示する文字列表示器で構成しています。
ただし、この画面の外に、メインVIからベット数を受け取るための数値制御器と、メインVIにダブルアップ終了後のベット数の値を返すための数値表示器があります。

ブロックダイアグラムは以下のようにしました。
小さいステートマシンとしています。
initステートではBET beforeとして、開始時のベット数をメインVIから受け取るようにしています。

refreshステートではピクチャの表示を一度まっさら(空)にし、クラスタについてはユーザーが選択した値をリセットするようにしています。

judgeステートでは、大小を比べる対象のピクチャを表示し、ユーザーが「HIGH」を選択したかどうかをクラスタに入れています。
イベントはこのHIGH、LOWの値変更イベントしか用意していません。
内部的にはこの時点で二つの数字は決定されていて、「答えがわかっている」状態としています(なので勝ち続けると勝てなくなるといった仕様にはなっていません)。

resultステートではHIGHを押したかどうかという情報と実際に大小関係がどうだったかを判定しています。
このゲームのやり方だと、同じ数字つまり引き分けの場合があり得ます。
それ以外は、ベット数に2を掛けるか0を掛けて、0を掛けた場合にはゲームを終了するようにしています。
ゲームに成功して再び続ける場合にはrefreshステートに戻り、終了する場合にはfinishステートに進みます。

ダブルアップゲームはfinishステートが実行されてWhileループの条件端子にTrueが渡ることで終了します。

本記事ではLabVIEWでポーカーを作る方法を紹介しました。
トランプを扱ったゲームで今回のような役を作っていくものは処理の工夫次第でより簡単にあるいは「上手く」実装でき、その処理方法を考えるのがこういったゲームを作る醍醐味だったりします。
自分でオリジナルのトランプゲームを作ってみたい、という方などに、考え方の一つとして参考になればうれしいです。
ここまで読んでいただきありがとうございました。
コメント