今回は久々にカードゲームをつくります。トランプは本来は幾種類かのゲームのルールにある切り札の意味ですが、なぜか日本ではカードゲームそのものを指します。一説によるとカードゲームのなかには切り札の意味で「トランプ」という語をゲーム中で使うものがあるのですが、これをみた当時の日本人がカードゲーム=トランプと勘違いしたのではないかともいわれています。

神経衰弱をつくります。ただしおひとり様用です。なかなかカードがそろわずイライラしてしまうことからこのような名前がつけられています。真剣衰弱は誤字・誤称です。はじめは真剣でも最後まで続かないということでしょうか?

ではさっそく神経衰弱をつくってみましょう。

.NET Frameworkはバージョン4.8をもってメジャーアップデートを終了することがアナウンスされていて、新規開発における環境として.NET Coreの次期バージョン.NET 5が推奨されています。ということでWindows フォームアプリ(「Windows フォームアプリケーション(.NET Framework)」ではなく)でつくります。

カードを描画するための画像ですが、これを使います。

このファイルをリソースに追加します。Windows フォームアプリケーション(.NET Framework)のときはプロジェクト ⇒ プロパティを選択してリソースを選択すると下の画像のようになり、ファイルをドラッグアンドドロップすればリソースに追加することができました。

ところが.NET 5のWindows フォームアプリでは「規定のリソースファイルが追加されていません」と表示されます。この場合はこれをクリックするとWindows フォームアプリケーション(.NET Framework)と同じような状態になるので、ここで画像ファイルをドラッグアンドドロップします。

カードを表示させるためのクラスをつくる

まずはカードを表示させるためのクラスを作成しましょう。スート(トランプに書かれているマーク 英語: suit)と番号を指定すればカードを描画するために必要なBitmapを取得できるようにします。またカードが裏のときは裏面に相当するBitmapを取得できるようにします。

コンストラクタでスートと番号をプロパティにセットし、そのあとGetBitmapメソッドとGetBackBitmapメソッドを呼び出して必要なBitmapを取得します。Suit Numberプロパティを取得してからでないとBitmapが取得できません。あとはBitmapプロパティとBackBitmapプロパティで描画に必要なBitmapを取得できます。

Bitmapを取得する元になるBitmap(上の画像)はすべてのカードで使うのでstaticにしてあります。これをしておかないと処理に時間がかかるうえにメモリーの消費量が大きくなります。

神経衰弱用のカードを表示させるクラスをつくる

上記は他のカードゲームをつくるときも使える汎用的なものですが、神経衰弱の場合、カードは取られるとき以外は位置を移動することはありません。それぞれのカードの位置はゲーム開始の段階で決まってしまうので、カードを描画する位置も自動的に決まります。カードを描画する処理とあわせてクラスにしてしまうのがよいと考えられます。そこでCardを継承してCardExクラスを作成します。

コンストラクタでカードの種類と番号だけでなく配置位置も決めてしまいます。実際に描画される場所はカードの場所とサイズと余白から算出できます。あとはGraphicsオブジェクトを渡せば描画できます。

最後のIsPointCardは引数として渡されたPointがカードの内部かどうかを調べるものです。

Form1クラスはどう書くか?

Form1クラスですが、デザイナをつかってすることはとくにありません。画像をキャプチャして乗せるのが面倒だっただけです。

Form1クラスのコンストラクタを示します。自作メソッド CreateMenuでメニューを表示させています。そしてCreateCardsメソッドのなかで CardExオブジェクトを生成してリストに格納しています。カードをクリックしたときに選択できるようにMouseClickのイベントハンドラを追加しています。

フォームのサイズは(800, 580)と設定していますが、だいたいこれくらいが見やすいです。再描画にやや時間がかかるためDoubleBufferedをtrueにして、カードゲームアプリらしく背景をグリーンにしています。

CreateMenuメソッドを示します。スタートと終了用のメニューを表示させているだけです。メニューをクリックしたときに呼び出されるメソッドはちょっと後回しにします。

CreateCardsメソッドを示します。各種A~Kのカードを生成して、自作メソッド ShuffleCardsでシャッフルします。そのあと5行11列にならべます。シャッフルされた状態であれば要素順にならべても問題ないはずです。ただRandomクラスが生成する乱数では隣の数が前後の数になる場合がかなりあります。

カードをシャッフルするShuffleCardsメソッドを示します。

カードを配置する位置を決めるSetCardsメソッドを示します。5行11列で並べます。○行○列が実際に存在するかどうか調べて(列の最大値 × 行番号 + 列番号がcards.Countと同じかそれより大きいと存在しない)、存在するのであれば行と列の値をセットしています。これでOnPaintメソッドのなかで描画する処理ができるはずです。

OnPaintメソッドを示します。残されているカードが表か裏かを調べてその位置にBitmapを描画しているだけです。カードが選ばれているのであれば表、そうでないなら裏です。存在しないカードであればなにも描画しません。

カードがクリックされたときの処理を示します。カードをクリックしたらそのカードを表にします。クリックされた場所にカードがあるかどうかはCardEx.IsPointCardメソッドで判断できます。

クリックされたカードのCardEx.SelectedプロパティをtrueにしてInvalidateメソッドを呼べばOnPaintのなかでうまくやってくれます。

2枚表になった段階でカードの数字を比較して同じであればカードをとることができ、違っていたら元通り裏にします。2枚表になっているかどうかはCardEx.Selectedがtrueになっているものを数えればわかります。

カードを比較する処理はすぐにできますが、すぐにカードが消えたり裏返しにならないようにしばらく時間をおいています。そのあいだいくつもクリックしてカードを開くことができないようにフィールド変数 CanSelectCardがfalseの場合はなにもおきないようにしています。

カードがそろっているかどうかを判定するJugeAsyncメソッドを示します。すぐに処理をおこなわないで0.5秒から1秒待ちます。GUIアプリケーションの場合、Sleepメソッドを使うとそのあいだユーザーの操作を受け付けないフリーズ状態になるので、Delayメソッドを使うほうがよさそうです。

このメソッドが呼び出されるときは2枚のカードが表になっているはずなので、その2つのカードを取得します。あとはじっさいにCardExオブジェクトのNumberプロパティを比較すれば判定できます。

カードがそろっていたらCardEx.Existsプロパティをfalseにします。そろっていてもいなくても両方のカードのCardEx.Selectedプロパティをfalseにします。そのあとInvalidateメソッドを呼べば、数がそろってカードが取り除かれたり、そろわなかったのでカードが裏返しになったようにみえます。

最後にメニューがクリックされたときの処理を示します。

スタートがクリックされたときはカードをシャッフルして並べ直します。

すでにカードは生成されているのでこれまで使ってきた物をそのまま使います。ShuffleCardsメソッドで順番を変更し、新しいカード配置位置を決めます。そのあと再描画の処理をおこないます。

次に終了を選択したときの処理を示します。ゲームの途中で終了を選択した場合は降参とみなして伏せてあるカードをすべて開きます。そしてメッセージボックスで”また遊んでね”と表示します。

右上の×ボタンをクリックしても終了させることができます。この場合は普通に終了するだけです。すべてのカードがオープンされたりメッセージボックスは表示されることはありません。