カードゲームのスピードをつくります。
スピードがどのようなゲームかはYouTubeでも動画がアップされています。ただローカルゲームもあるらしく、ここでは最初にカードを赤と黒に分けるか分けないか、場から台札にカードを重ねたときはそのつど山札からカードを補わなければならないルール(つまり山札にカードがある限り、場のカードは2枚以下になることはない)とそうではないルールが混在します。
下の動画では最初にカードを赤と黒にわけたうえで、場から台札にカードを出すときは場から出せるカードを連続で出してもかまわないと説明されています。
一方、こちらの動画ではカードを赤と黒にわけず、場から台札にカードを出したら山札から必ず出してしまった場のカードを補わなければならないと説明されています。
今回は最初にカードを赤と黒にわける、場から台札にカードを出すときは場から出せるカードを連続で出してもかまわない、ただしコンピュータは場から台札にカードを出したら山札から場のカードを補うことにします。あまりコンピュータに本気を出されると人間に勝ち目がないからです。
Contents
Cardクラスをつくる
まずCardクラスをつくりますが、これは素数大富豪で使ったものと同じものを使います。
カードを表示させてゲームらしくする C#で素数大富豪をつくる(6)
ただひとつだけプロパティを追加します。Pointプロパティです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Card { public Point Point { set { X = value.X; Y = value.Y; } get { return new Point(X, Y); } } // X、Yプロパティは再掲 public int X { get; set; } public int Y { get; set; } } |
これでカードを移動させやすくなります。
Form1クラス
ではForm1クラスをみていくことにします。
フィールド変数と初期化
まずフィールド変数を示します。
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 |
public partial class Form1 : Form { Timer Timer1 = new Timer(); // 山札 List<Card> CardsDeckPlayer1 = new List<Card>(); List<Card> CardsDeckPlayer2 = new List<Card>(); // プレイヤーとコンピュータが場に出したカード(最大4) Card[] CardsPutIntoPlay1 = new Card[4]; Card[] CardsPutIntoPlay2 = new Card[4]; // 台札 List<Card> CardsLedgerPlayer1 = new List<Card>(); List<Card> CardsLedgerPlayer2 = new List<Card>(); // カードの幅と高さ int CardWidth = 70; int CardHeight = 95; // 左上に表示されるカードの座標 int MarginLeft = 70; int MarginTop = 40; // 縦横のカードの隙間 int VerticalGapCard = 20; int HorizontalGapCard = 20; // ゲームは終了したか? bool IsGameSet = true; } |
次にコンストラクタを示します。
ここでやっていることは、ちらつきを抑えるためにダブルバッファを有効にしているのと背景をカードゲームらしく緑色にしていること、タイマー(コンピュータがカードを出すときの処理で必要)のTickイベントに対応するためのイベントハンドラを設定していることです。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public partial class Form1 : Form { public Form1() { InitializeComponent(); BackColor = Color.Green; DoubleBuffered = true; Timer1.Interval = 1500; Timer1.Tick += Timer1_Tick; } } |
ゲームスタートの処理
ゲームスタートの処理を示します。
ゲームセットであるかどうかを表わすIsGameSetフラグをクリアし、新しくカードを生成します。スピードではジョーカーは使いません。
各プレイヤーの場は4つ存在します。これをnullで初期化します。
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 |
public partial class Form1 : Form { private void MenuItemGameStart_Click(object sender, EventArgs e) { GameStart(); } void GameStart() { // ゲームを開始するということは「ゲームは終了していない状態」に以降する IsGameSet = false; // 前のゲームのデータをクリアする // 「場」にカードは存在しない状態に(「場」にはカードが4枚存在する Length == 4) for (int i = 0; i < CardsPutIntoPlay1.Length; i++) CardsPutIntoPlay1[i] = null; for(int i = 0; i < CardsPutIntoPlay2.Length; i++) CardsPutIntoPlay2[i] = null; // 「台札」にカードは存在しない状態に CardsLedgerPlayer1.Clear(); CardsLedgerPlayer2.Clear(); // 各プレイヤーが持っているカードは存在しない状態に CardsDeckPlayer1.Clear(); CardsDeckPlayer2.Clear(); // カードを生成してシャッフルする CreateCards(); ShuffleCards(); // 各プレイヤーの「場」にカードをセットする // 山札から4枚カードを移動(したがって山札からカードが4枚取り除かれる) for (int i = 0; i < CardsPutIntoPlay1.Length; i++) CardsPutIntoPlay1[i] = CardsDeckPlayer1[i]; CardsDeckPlayer1.RemoveRange(0, 4); for (int i = 0; i < CardsPutIntoPlay2.Length; i++) CardsPutIntoPlay2[i] = CardsDeckPlayer2[i]; CardsDeckPlayer2.RemoveRange(0, 4); Invalidate(); } } |
カードを生成とシャッフル
カードを生成する処理を示します。この処理がおこなわれるときには山札はクリアされているはずなので、赤のカードはコンピュータの山札に、黒のカードはプレイヤーの山札に追加していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public partial class Form1 : Form { void CreateCards() { // 山札はクリアされている for (int i = 1; i <= 13; i++) { CardsDeckPlayer1.Add(new Card(Suit.Spade, i, CardWidth, CardHeight)); CardsDeckPlayer2.Add(new Card(Suit.Heart, i, CardWidth, CardHeight)); CardsDeckPlayer2.Add(new Card(Suit.Diamond, i, CardWidth, CardHeight)); CardsDeckPlayer1.Add(new Card(Suit.Club, i, CardWidth, CardHeight)); } } } |
カードを生成したらシャッフルします。プレイヤーのカードとコンピュータのカードをそれぞれシャッフルします。
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 |
public partial class Form1 : Form { void ShuffleCards() { CardsDeckPlayer1 = ShuffleCards(CardsDeckPlayer1); CardsDeckPlayer2 = ShuffleCards(CardsDeckPlayer2); } Random random = new Random(); List<Card> ShuffleCards(List<Card> cards) { List<Card> ret = new List<Card>(); // 乱数を生成してCardsからカードを抜き取りretへ追加する // 最後にretをCardsに代入する while (true) { int count = cards.Count(); if (count == 0) break; int r = random.Next(count); ret.Add(cards[r]); cards.RemoveAt(r); } return ret; } } |
カードを表示させる位置を取得する
カードを表示させるためにはカードを表示させたい位置に移動させなければなりません。各プレイヤーの山札、場、台札はどの座標にあるのか? それを取得するのが以下のメソッドです。
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 |
public partial class Form1 : Form { // プレイヤー(Player1)とコンピュータ(Player2)の山札の座標を取得する Point GetDeckPoint1() { return new Point(MarginLeft + (CardWidth + HorizontalGapCard) * 4, MarginTop + (CardHeight + VerticalGapCard) * 2); } Point GetDeckPoint2() { return new Point(MarginLeft, MarginTop); } // 各プレイヤーの場におかれるカードの4箇所の座標 List<Point> GetPutIntoPlayPoints1() { List<Point> points = new List<Point>(); for (int i = 0; i < 4; i++) points.Add(new Point(MarginLeft + (CardWidth + HorizontalGapCard) * i, MarginTop + (CardHeight + VerticalGapCard) * 2)); return points; } List<Point> GetPutIntoPlayPoints2() { List<Point> points = new List<Point>(); for(int i=0; i<4; i++) points.Add(new Point(MarginLeft + (CardWidth + HorizontalGapCard) * (i + 1), MarginTop)); return points; } // 各プレイヤーの台札がおかれるカードの座標 Point GetLedgerPoint1() { return new Point(MarginLeft + (CardWidth + HorizontalGapCard) * 3 - CardWidth/ 2, MarginTop + (CardHeight + VerticalGapCard) * 1); } Point GetLedgerPoint2() { return new Point(MarginLeft + (CardWidth + HorizontalGapCard) * 1 + CardWidth / 2, MarginTop + (CardHeight + VerticalGapCard) * 1); } } |
カードがクリックされたとき
カードがクリックされたときにそこにカードがあれば、出せるカードであれば出し、移動できるカードであるなら移動させます。
そのまえにゲームが終了していないか調べる必要があります。すでに決着がついている(コンピュータに敗北している)のにクリックしたカードが出せてしまってはおかしいです。ゲームが終了していないのであれば出すことができる場のカードがクリックされたら台札のうえに重ね、山札がクリックされた場合で場のカードで欠損している部分があればそこにカードを移動させます。
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 { protected override void OnMouseDown(MouseEventArgs e) { // ゲームは決着していない if (!IsGameSet) { Point point = new Point(e.X, e.Y); // 山札のカードがクリックされて場のカードに欠損している部分があれば移動する // PutCardFromDeckIfCanメソッドの戻り値はクリックされた座標に山札があればtrue // そうでないなら false if (!PutCardFromDeckIfCan(point)) { // 存在する場のカードがクリックされて出せるカードであれば台札に移動する PutCardFromIntoPlay(point); } } base.OnMouseDown(e); } } |
IsPointDeckPlayer1メソッドは引数(クリックされた部分の座標)が山札がある部分なのかどうかを調べるためのものです。
1 2 3 4 5 6 7 8 9 10 11 12 |
public partial class Form1 : Form { bool IsPointDeckPlayer1(Point point) { if (CardsDeckPlayer1.Count == 0) return false; // プレイヤーの山札がある部分の矩形を取得する Rectangle rect = new Rectangle(GetDeckPoint1(), new Size(CardWidth, CardHeight)); return IsRectangle(point, rect); } } |
またIsRectangleメソッドは第一引数の座標が第二引数の矩形のなかに存在するかどうかを返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public partial class Form1 : Form { bool IsRectangle(Point point, Rectangle rect) { if (point.X < rect.Left) return false; if (rect.Right < point.X) return false; if (point.Y < rect.Top) return false; if (rect.Bottom < point.Y) return false; return true; } } |
カードを出せるか?
CanPutNextCardメソッドはプレイヤーとコンピュータのうち、少なくとも片方がカードを出せるかどうかを調べるためのものです。このメソッドがfalseを返すということはどちらもカードを出すことができないということです。スピードのルールでは「スピード!」のかけ声とともに自分の山札のカード(ない場合は場のカード)を1枚、台札として出します。
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 |
public partial class Form1 : Form { bool CanPutNextCard() { if (CardsDeckPlayer1.Count > 0 && CardsPutIntoPlay1.Any(x => x == null)) return true; if (CardsDeckPlayer2.Count > 0 && CardsPutIntoPlay2.Any(x => x == null)) return true; foreach (Card card in CardsPutIntoPlay1) { if (card == null) continue; if(CanPutCard(card.Number) != null) return true; } foreach (Card card in CardsPutIntoPlay2) { if (card == null) continue; if (CanPutCard(card.Number) != null) return true; } return false; } } |
CanPutCardメソッドは引数で渡された番号のカードを台札のうえに出せるかを調べます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public partial class Form1 : Form { List<Card> CanPutCard(int number) { if(CardsLedgerPlayer1.Count == 0) return null; int num1 = CardsLedgerPlayer1.Last().Number; if (Math.Abs(num1 - number) == 1 || Math.Abs(num1 - number) == 12) return CardsLedgerPlayer1; int num2 = CardsLedgerPlayer2.Last().Number; if (Math.Abs(num2 - number) == 1 || Math.Abs(num2 - number) == 12) return CardsLedgerPlayer2; return null; } } |
双方出すことができるカードがない場合、両者が「スピード!」のかけ声とともに台札のうえにカードを出しますが、CallSpeedメソッドはそのときの処理をしています。
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 |
public partial class Form1 : Form { void CallSpeed() { CallSpeedInner(true); CallSpeedInner(false); Timer1.Start(); void CallSpeedInner(bool isPlayer1) { List<Card> cardsDeckPlayer = CardsDeckPlayer1; List<Card> cardsLedgerPlayer = CardsLedgerPlayer1; Card[] cardsPutIntoPlay = CardsPutIntoPlay1; if (!isPlayer1) { cardsDeckPlayer = CardsDeckPlayer2; cardsLedgerPlayer = CardsLedgerPlayer2; cardsPutIntoPlay = CardsPutIntoPlay2; } // 山札にカードがあるなら山札から // ないなら場にあるカードを台札に出す if (cardsDeckPlayer.Count > 0) { cardsLedgerPlayer.Add(cardsDeckPlayer[0]); cardsDeckPlayer.RemoveAt(0); } else { for (int i = 0; i < cardsPutIntoPlay.Length; i++) { if (cardsPutIntoPlay[i] != null) { cardsLedgerPlayer.Add(cardsPutIntoPlay[i]); cardsPutIntoPlay[i] = null; break; } } } } } } |
カードを移動させる
PutCardFromDeckIfCanメソッドは山札がある部分がクリックされると場にカードがない部分があればそこにカードを移動させます。山札がある部分ではない部分がクリックされるとなにもおきません。
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 |
public partial class Form1 : Form { bool PutCardFromDeckIfCan(Point point) { if (IsPointDeckPlayer1(point)) { int index = -1; for (int i = 0; i < 4; i++) { if (CardsPutIntoPlay1[i] == null) { index = i; break; } } if (index != -1) { CardsPutIntoPlay1[index] = CardsDeckPlayer1[0]; CardsDeckPlayer1.RemoveAt(0); } else { if (!CanPutNextCard()) { CallSpeed(); } } Invalidate(); return true; } return false; } } |
PutCardFromIntoPlayメソッドは場でカードがある部分がクリックされると、出せるカードであれば台札のうえに出します。
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 |
public partial class Form1 : Form { bool PutCardFromIntoPlay(Point point) { int index = IsPointPutIntoCards1(point); if (index != -1) { Card card = CardsPutIntoPlay1[index]; if (card == null) return false; List<Card> ledger = CanPutCard(card.Number); if (ledger != null) { ledger.Add(card); CardsPutIntoPlay1[index] = null; } else { if (!CanPutNextCard()) { CallSpeed(); } } Invalidate(); return true; } return false; } } |
IsPointPutIntoCards1メソッドは引数として渡された座標(クリックされた座標)がプレイヤーの場でカードが置かれている矩形の内部であるか調べ、矩形の内部であるなら何番目のものかを返します(場には4枚カードが存在するので戻り値は0~3)。どの矩形の内部でもない場合は-1を返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public partial class Form1 : Form { int IsPointPutIntoCards1(Point point) { for (int i = 0; i < 4; i++) { if (CardsPutIntoPlay1[i] == null) continue; Rectangle rect = new Rectangle(GetPutIntoPlayPoints1()[i], new Size(CardWidth, CardHeight)); if (IsRectangle(point, rect)) return i; } return -1; } } |
カードを描画する
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public partial class Form1 : Form { protected override void OnPaint(PaintEventArgs e) { DrawDeckCards(e.Graphics); DrawPutIntoPlayCards(e.Graphics); DrawLedgerCards(e.Graphics); DrawCardCount(e.Graphics); GetSizeSpeedText(); DrawSpeedIfNeed(e.Graphics); base.OnPaint(e); } } |
DrawDeckCardsメソッドはプレイヤー双方の山札を描画します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public partial class Form1 : Form { void DrawDeckCards(Graphics graphics) { if (CardsDeckPlayer1.Count > 0) { CardsDeckPlayer1[0].Point = GetDeckPoint1(); CardsDeckPlayer1[0].DrawBack(graphics); } if (CardsDeckPlayer2.Count > 0) { CardsDeckPlayer2[0].Point = GetDeckPoint2(); CardsDeckPlayer2[0].DrawBack(graphics); } } } |
DrawPutIntoPlayCardsメソッドはプレイヤー双方の場に出されているカードを描画します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public partial class Form1 : Form { void DrawPutIntoPlayCards(Graphics graphics) { for (int i = 0; i < CardsPutIntoPlay1.Length; i++) { if (CardsPutIntoPlay1[i] != null) { CardsPutIntoPlay1[i].Point = GetPutIntoPlayPoints1()[i]; CardsPutIntoPlay1[i].Draw(graphics); } } for (int i = 0; i < CardsPutIntoPlay2.Length; i++) { if (CardsPutIntoPlay2[i] != null) { CardsPutIntoPlay2[i].Point = GetPutIntoPlayPoints2()[i]; CardsPutIntoPlay2[i].Draw(graphics); } } } } |
DrawLedgerCardsメソッドはプレイヤー双方の台札のカードを描画します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public partial class Form1 : Form { void DrawLedgerCards(Graphics graphics) { if (CardsLedgerPlayer1.Count > 0) { CardsLedgerPlayer1.Last().Point = GetLedgerPoint1(); CardsLedgerPlayer1.Last().Draw(graphics); } if (CardsLedgerPlayer2.Count > 0) { CardsLedgerPlayer2.Last().Point = GetLedgerPoint2(); CardsLedgerPlayer2.Last().Draw(graphics); } } } |
DrawCardCountメソッドは各プレイヤーの残りのカードを描画します。
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 { Font StringFont = new Font("MS ゴシック", 12, FontStyle.Bold); void DrawCardCount(Graphics graphics) { Point point1 = GetDeckPoint1(); point1.X += CardWidth + 20; point1.Y += 20; Point point2 = GetDeckPoint2(); point2.X = point1.X; point2.Y += 20; if (IsInTheGame()) { graphics.DrawString("残り " + CardsDeckPlayer1.Count.ToString(), StringFont, Brushes.White, point1); graphics.DrawString("残り " + CardsDeckPlayer2.Count.ToString(), StringFont, Brushes.White, point2); } } } |
IsInTheGameメソッドはゲームが行なわれている最中かどうかを調べます。どちらかのプレイヤーの山札や場にカードが存在する場合はゲーム中であることになります。ただこれでは片方のカードがなくなったとき(勝負がついたとき)もtrueを返します。そこで勝者判定は別におこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public partial class Form1 : Form { bool IsInTheGame() { if (CardsDeckPlayer1.Count > 0) return true; if (CardsDeckPlayer2.Count > 0) return true; if(CardsPutIntoPlay1.Any(x => x != null)) return true; if (CardsPutIntoPlay2.Any(x => x != null)) return true; return false; } } |
GetSizeSpeedTextメソッドは”SPEED!!”と画面中央に表示させるときの背景の矩形のサイズを取得します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public partial class Form1 : Form { string SpeedText = "SPEED!!"; Font SpeedFont = new Font("MS ゴシック", 32, FontStyle.Bold); Size SpeedSize = Size.Empty; void GetSizeSpeedText() { if (SpeedSize == Size.Empty) { SpeedSize = TextRenderer.MeasureText(SpeedText, SpeedFont); SpeedSize.Width += 20; // 文字列よりも幅を広くする } } } |
プレイヤーが双方ともにカードを出せない場合、中央に”SPEED!!”の文字を表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public partial class Form1 : Form { void DrawSpeedIfNeed(Graphics graphics) { if (!CheckGameSet(graphics) && !CanPutNextCard() && IsInTheGame()) { Point point = new Point(195, 170); graphics.FillRectangle(Brushes.Red, new Rectangle(point.X - 50, point.Y, SpeedSize.Width + 80, SpeedSize.Height)); TextRenderer.DrawText(graphics, SpeedText, SpeedFont, point, Color.White); Timer1.Stop(); } } } |
ゲーム終了の判定
どちらか、または両方が同じタイミングでカードを使い切ったとき、勝負の結果を画面中央に表示します。
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 |
public partial class Form1 : Form { bool CheckGameSet(Graphics graphics) { Point point = new Point(195, 170); bool winPlayer1 = CardsDeckPlayer1.Count == 0 && !CardsPutIntoPlay1.Any(x => x != null) && CardsLedgerPlayer1.Count > 0; bool winPlayer2 = CardsDeckPlayer2.Count == 0 && !CardsPutIntoPlay2.Any(x => x != null) && CardsLedgerPlayer2.Count > 0; if (winPlayer1 && winPlayer2) { graphics.FillRectangle(Brushes.Red, new Rectangle(point, SpeedSize)); TextRenderer.DrawText(graphics, "Draw", SpeedFont, point, Color.White); Timer1.Stop(); return true; } else if (winPlayer1) { graphics.FillRectangle(Brushes.Red, new Rectangle(point, SpeedSize)); TextRenderer.DrawText(graphics, "You Win", SpeedFont, point, Color.White); Timer1.Stop(); return true; } else if (winPlayer2) { graphics.FillRectangle(Brushes.Red, new Rectangle(point, SpeedSize)); TextRenderer.DrawText(graphics, "You Lose", SpeedFont, point, Color.White); Timer1.Stop(); return true; } return false; } } |
現状ではコンピュータがカードを出す処理を実装していません。ただクリックすれば自分のカードを出してアガることはできます。