今回は久々にカードゲームをつくります。トランプは本来は幾種類かのゲームのルールにある切り札の意味ですが、なぜか日本ではカードゲームそのものを指します。一説によるとカードゲームのなかには切り札の意味で「トランプ」という語をゲーム中で使うものがあるのですが、これをみた当時の日本人がカードゲーム=トランプと勘違いしたのではないかともいわれています。
神経衰弱をつくります。ただしおひとり様用です。なかなかカードがそろわずイライラしてしまうことからこのような名前がつけられています。真剣衰弱は誤字・誤称です。はじめは真剣でも最後まで続かないということでしょうか?
ではさっそく神経衰弱をつくってみましょう。
.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にしてあります。これをしておかないと処理に時間がかかるうえにメモリーの消費量が大きくなります。
1 2 3 4 5 6 7 8 9 |
public enum CardSuit { Joker = 0, Spade = 1, Hart = 2, Dia = 3, Club = 4, Back = 5, // カードは裏向き } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
public class Card { public Card(CardSuit cardMark, int num) { Suit = cardMark; Number = num; // Suit Numberプロパティを取得してからBitmapを取得すること GetBitmap(); GetBackBitmap(); } // カードの種類 public CardSuit Suit { private set; get; } // カードの番号 public int Number { private set; get; } // カードの大きさ(縦横のサイズ) Size size = new Size(60, 90); public Size Size { set { size = value; } get { return size; } } Bitmap _bitmap = null; public Bitmap Bitmap { get { if (_bitmap == null || _bitmap.Size != this.Size) _bitmap = GetBitmap(); return _bitmap; } } // Properties.Resources.cardimageは一度だけ取得する static Bitmap CardsImage = null; Bitmap GetBitmap() { Bitmap bitmap = new Bitmap(Size.Width, Size.Height); if(CardsImage == null) CardsImage = Properties.Resources.cardimage; int x = 0; int y = 0; x = 382 * (Number - 1); if (Suit == CardSuit.Spade) y = 1800; if (Suit == CardSuit.Hart) y = 1200; if (Suit == CardSuit.Dia) y = 600; if (Suit == CardSuit.Club) y = 0; if (Suit == CardSuit.Spade && Number == 1) { x = 382 * 1; y = 2400; } if (Suit == CardSuit.Joker) { x = 0; y = 2400; } Rectangle srcRect = new Rectangle(new Point(x, y), new Size(354, 490)); Graphics graphics = Graphics.FromImage(bitmap); graphics.DrawImage(CardsImage, new Rectangle(0, 0, Size.Width, Size.Height), srcRect, GraphicsUnit.Pixel); graphics.Dispose(); return bitmap; } Bitmap _backBitmap = null; public Bitmap BackBitmap { get { if (_backBitmap == null || _backBitmap.Size != this.Size) _backBitmap = GetBackBitmap(); return _backBitmap; } } Bitmap GetBackBitmap() { Bitmap bitmap = new Bitmap(Size.Width, Size.Height); if (CardsImage == null) CardsImage = Properties.Resources.cardimage; int x = 382 * 2; int y = 2400; Rectangle srcRect = new Rectangle(new Point(x, y), new Size(354, 490)); Graphics graphics = Graphics.FromImage(bitmap); graphics.DrawImage(CardsImage, new Rectangle(0, 0, Size.Width, Size.Height), srcRect, GraphicsUnit.Pixel); graphics.Dispose(); return bitmap; } } |
神経衰弱用のカードを表示させるクラスをつくる
上記は他のカードゲームをつくるときも使える汎用的なものですが、神経衰弱の場合、カードは取られるとき以外は位置を移動することはありません。それぞれのカードの位置はゲーム開始の段階で決まってしまうので、カードを描画する位置も自動的に決まります。カードを描画する処理とあわせてクラスにしてしまうのがよいと考えられます。そこでCardを継承してCardExクラスを作成します。
コンストラクタでカードの種類と番号だけでなく配置位置も決めてしまいます。実際に描画される場所はカードの場所とサイズと余白から算出できます。あとはGraphicsオブジェクトを渡せば描画できます。
最後のIsPointCardは引数として渡されたPointがカードの内部かどうかを調べるものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
public class CardEx : Card { // 左上に配置されるカードの座標を(0, 0)にしないで余白をいれる int MarginTop = 30; int MarginLeft = 10; public CardEx(CardSuit cardMark, int num) : base(cardMark, num) { Row = 0; Colum = 0; Selected = false; Exists = true; } public bool Selected { get; set; } // カードは存在するか? public bool Exists { get; set; } // カードの位置(一番左なら0) public int Colum { get; set; } // カードの位置(一番上なら0) public int Row { get; set; } // カードが表示される位置(カードの左上の座標) public Point Point { get { int x = MarginLeft + (Size.Width + 10) * Colum; int y = MarginTop + (Size.Height + 10) * Row; return new Point(x, y); } } public void Draw(Graphics graphics) { graphics.DrawImage(this.Bitmap, this.Point); } public void DrawBack(Graphics graphics) { graphics.DrawImage(this.BackBitmap, this.Point); } public bool IsPointCard(Point point) { Rectangle rect = new Rectangle(this.Point, this.Size); bool b = point.X < rect.Left || rect.Right < point.X || point.Y < rect.Top || rect.Bottom < point.Y; return !b; } } |
Form1クラスはどう書くか?
Form1クラスですが、デザイナをつかってすることはとくにありません。画像をキャプチャして乗せるのが面倒だっただけです。
Form1クラスのコンストラクタを示します。自作メソッド CreateMenuでメニューを表示させています。そしてCreateCardsメソッドのなかで CardExオブジェクトを生成してリストに格納しています。カードをクリックしたときに選択できるようにMouseClickのイベントハンドラを追加しています。
フォームのサイズは(800, 580)と設定していますが、だいたいこれくらいが見やすいです。再描画にやや時間がかかるためDoubleBufferedをtrueにして、カードゲームアプリらしく背景をグリーンにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public partial class Form1 : Form { public Form1() { InitializeComponent(); CreateMenu(); Cards = CreateCards(); this.MouseClick += Form1_MouseClick; this.DoubleBuffered = true; this.BackColor = Color.Green; this.Size = new Size(800, 580); } } |
CreateMenuメソッドを示します。スタートと終了用のメニューを表示させているだけです。メニューをクリックしたときに呼び出されるメソッドはちょっと後回しにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public partial class Form1 : Form { void CreateMenu() { ToolStripMenuItem menuItemStart = new ToolStripMenuItem("スタート"); menuItemStart.Click += MenuItemStart_Click; ToolStripMenuItem menuItemEnd = new ToolStripMenuItem("終了"); menuItemEnd.Click += MenuItemEnd_Click; MenuStrip menuStrip = new MenuStrip(); this.Controls.Add(menuStrip); menuStrip.Items.Add(menuItemStart); menuStrip.Items.Add(menuItemEnd); } } |
CreateCardsメソッドを示します。各種A~Kのカードを生成して、自作メソッド ShuffleCardsでシャッフルします。そのあと5行11列にならべます。シャッフルされた状態であれば要素順にならべても問題ないはずです。ただRandomクラスが生成する乱数では隣の数が前後の数になる場合がかなりあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public partial class Form1 : Form { List<CardEx> Cards = null; List<CardEx> CreateCards() { List<CardEx> cards = new List<CardEx>(); // カードを生成する for (int i = 1; i <= 13; i++) { cards.Add(new CardEx(CardSuit.Spade, i)); cards.Add(new CardEx(CardSuit.Hart, i)); cards.Add(new CardEx(CardSuit.Dia, i)); cards.Add(new CardEx(CardSuit.Club, i)); } // カードをシャッフルする cards = ShuffleCards(cards); // カードを配置する位置を決める SetCards(cards); return cards; } } |
カードをシャッフルするShuffleCardsメソッドを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public partial class Form1 : Form { List<CardEx> ShuffleCards(List<CardEx> cards) { Random random = new Random(); List<CardEx> ret = new List<CardEx>(); while (cards.Count > 0) { int r = random.Next(cards.Count); ret.Add(cards[r]); cards.RemoveAt(r); } return ret; } } |
カードを配置する位置を決めるSetCardsメソッドを示します。5行11列で並べます。○行○列が実際に存在するかどうか調べて(列の最大値 × 行番号 + 列番号がcards.Countと同じかそれより大きいと存在しない)、存在するのであれば行と列の値をセットしています。これでOnPaintメソッドのなかで描画する処理ができるはずです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public partial class Form1 : Form { void SetCards(List<CardEx> cards) { int columMax = 11; int rowMax = 5; int count = cards.Count; for (int i = 0; i < rowMax; i++) { for (int j = 0; j < columMax; j++) { if (count <= columMax * i + j) continue; cards[columMax * i + j].Row = i; cards[columMax * i + j].Colum = j; } } } } |
OnPaintメソッドを示します。残されているカードが表か裏かを調べてその位置にBitmapを描画しているだけです。カードが選ばれているのであれば表、そうでないなら裏です。存在しないカードであればなにも描画しません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public partial class Form1 : Form { protected override void OnPaint(PaintEventArgs e) { foreach (CardEx card in Cards) { if (!card.Exists) continue; if(card.Selected) card.Draw(e.Graphics); else card.DrawBack(e.Graphics); } base.OnPaint(e); } } |
カードがクリックされたときの処理を示します。カードをクリックしたらそのカードを表にします。クリックされた場所にカードがあるかどうかはCardEx.IsPointCardメソッドで判断できます。
クリックされたカードのCardEx.SelectedプロパティをtrueにしてInvalidateメソッドを呼べばOnPaintのなかでうまくやってくれます。
2枚表になった段階でカードの数字を比較して同じであればカードをとることができ、違っていたら元通り裏にします。2枚表になっているかどうかはCardEx.Selectedがtrueになっているものを数えればわかります。
カードを比較する処理はすぐにできますが、すぐにカードが消えたり裏返しにならないようにしばらく時間をおいています。そのあいだいくつもクリックしてカードを開くことができないようにフィールド変数 CanSelectCardがfalseの場合はなにもおきないようにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public partial class Form1 : Form { bool CanSelectCard = true; private async void Form1_MouseClick(object sender, MouseEventArgs e) { if (!CanSelectCard) return; CardEx cardEx = Cards.FirstOrDefault(x => x.IsPointCard(new Point(e.X, e.Y))); if (cardEx == null) return; cardEx.Selected = true; Invalidate(); if (Cards.Count(x => x.Selected) >= 2) { CanSelectCard = false; await JugeAsync(); CanSelectCard = true; } } } |
カードがそろっているかどうかを判定するJugeAsyncメソッドを示します。すぐに処理をおこなわないで0.5秒から1秒待ちます。GUIアプリケーションの場合、Sleepメソッドを使うとそのあいだユーザーの操作を受け付けないフリーズ状態になるので、Delayメソッドを使うほうがよさそうです。
このメソッドが呼び出されるときは2枚のカードが表になっているはずなので、その2つのカードを取得します。あとはじっさいにCardExオブジェクトのNumberプロパティを比較すれば判定できます。
カードがそろっていたらCardEx.Existsプロパティをfalseにします。そろっていてもいなくても両方のカードのCardEx.Selectedプロパティをfalseにします。そのあとInvalidateメソッドを呼べば、数がそろってカードが取り除かれたり、そろわなかったのでカードが裏返しになったようにみえます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public partial class Form1 : Form { async Task JugeAsync() { CardEx[] cards = Cards.Where(x => x.Selected).ToArray(); if (cards[0].Number == cards[1].Number) { await Task.Delay(500); // カードは存在しないことにする cards[0].Exists = false; cards[1].Exists = false; } else { await Task.Delay(1000); } // cards[0].Number == cards[1].NumberであってもなくてもSelectedプロパティはfalseに戻す cards[0].Selected = false; cards[1].Selected = false; Invalidate(); } } |
最後にメニューがクリックされたときの処理を示します。
スタートがクリックされたときはカードをシャッフルして並べ直します。
すでにカードは生成されているのでこれまで使ってきた物をそのまま使います。ShuffleCardsメソッドで順番を変更し、新しいカード配置位置を決めます。そのあと再描画の処理をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public partial class Form1 : Form { private void MenuItemStart_Click(object sender, EventArgs e) { Restart(); } void Restart() { // カードをシャッフルして再配置 Cards = ShuffleCards(Cards); SetCards(Cards); foreach (CardEx card in Cards) { card.Selected = false; card.Exists = true; } Invalidate(); } } |
次に終了を選択したときの処理を示します。ゲームの途中で終了を選択した場合は降参とみなして伏せてあるカードをすべて開きます。そしてメッセージボックスで”また遊んでね”と表示します。
右上の×ボタンをクリックしても終了させることができます。この場合は普通に終了するだけです。すべてのカードがオープンされたりメッセージボックスは表示されることはありません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public partial class Form1 : Form { private void MenuItemEnd_Click(object sender, EventArgs e) { End(); } private void End() { foreach (CardEx card in Cards) card.Selected = true; Invalidate(); MessageBox.Show("また遊んでね"); Application.Exit(); } } |