神経衰弱をつくる(後編)

こんにちは、きうちです!

ちょっと間が空いてしまいましたが、今回は前回の神経衰弱をつくる(前編)の続きをお届けしたいと思います。

前回までで、カードを並べてシャッフルしたり、めくったりすることができるようになりました。

そして、以下が課題として残っていました。

  • 2枚めくった時点でペアができているかどうかの判定をする
  • ペアが成立していればその2枚を場から除去する。また、取得できた枚数を2増やす。
  • 成立していなければ裏返す
  • 全部取得できたらクリア!
  • カードの種類を数字ではなく色で識別できるようにする

では、参りましょう。

★ ★ ★

まずは、めくりの処理に手を加えていきます。

フィールド変数として「今何枚めくったか?」を保持するものを追加します(_shownCount)。
それから、一定時間後に「めくったカードが戻る」または「めくったカードが除去される」ために、タイマーを1つ追加します(_timer)。
なぜ戻しや除去を一定時間後にするかといえば、「めくった状態」がしばらく表示された状態にしておかないと、プレイヤー(人)には何が起きているかわからなくなるから。
また、めくったカードの座標を記録しておくための配列変数を追加します(_shownCardsXY)。
取得したカードの数を記録する変数も追加します(_getCards)。

private int _shownCount = 0; // 【後編追加】めくった枚数
private System.Windows.Forms.Timer _timer = new System.Windows.Forms.Timer(); // 【後編追加】タイマー
private Point[] _shownCardsXY = new Point[2]; // 【後編追加】めくったカードの座標の記録
private int _getCards = 0; // 【後編追加】取得したカードの数

タイマーのセットアップ処理は次のようにコンストラクタに追加します。
ここで参照している timer_tick のところで「そんなのありません」的なエラーが出ると思いますが、
これについては後で追加しますので問題なしです。

public Form1() // コンストラクタ
{
    InitializeComponent();

    Text = "神経衰弱";
    BackColor = Color.Black;
    SetBounds(0, 0, 64 * 13 + 17, 88 * 4 + 40, BoundsSpecified.Size);
    DoubleBuffered = true;

    Paint += Form1_Paint;
    MouseClick += Form1_MouseClick; // 【追加2】マウスクリック紐づけ

    _timer.Interval = 1000; // 【後編追加】
    _timer.Tick += new EventHandler(timer_tick); // 【後編追加】

    StartNewGame();
}

めくる処理は Form1_MouseClick イベントメソッドに書いていました。
これをこうします。

private void Form1_MouseClick(object sender, MouseEventArgs e) // マウスクリック処理
{
    if (_timer.Enabled) return; // 【後編追加】タイマーが既に開始していた(裏返し待ち)ら、何もせずreturn

    if (_shownCount == 2) return; // 【後編追加】既に2枚めくっている場合は新たにクリックされても何も起きないようにする

    int x = e.X / 64; // クリックされたX座標を64で割る
    int y = e.Y / 88; // クリックされたY座標を88で割る

    if (x >= 0 && x < _cells.GetLength(0) && y >= 0 && y < _cells.GetLength(1)) // カードじゃないところをクリックしていないかチェックする
    {
        if (_cells[x, y].Shown) return; // 【後編追加】既にめくっているカードを再度クリックしても何も起きないようにする
         _cells[x, y].Shown = !_cells[x, y].Shown; // 表返りフラグを立てる
        Refresh(); // 画面再描画
    }

    // === 【後編追加】===
    _shownCardsXY[_shownCount] = new Point(x, y); // 表返したカードの座標を控える
    _shownCount++; // めくったカードの枚数を+1する
    if (_shownCount == 2) // 表返したカードの数が2枚になったら
    {
        _timer.Start(); // タイマースタート
    }
}

既に2枚めくっている場合は新たにクリックされても何も起きないようにします。

また、前回のコードだと「めくった状態のカードを再度クリックしてしまう」ことができていたので、
既にめくっているカードをクリックしても何も起きないようにします。

上記の問題がない場合、めくったカードの座標を _shownCardsXY に控えます。
そしてめくったカードの枚数を+1します。
また、裏返したカードが2枚のときはタイマーを始動させます。
なお、タイマーが既に動いている時は2枚めくり済みということなので、それ以上クリックしてもめくれないように先頭に if 文を追加します。

※なお上記追加部分、わかりやすさのために前回からの単純な追加で済むように書いていますが、なんか全体としての読みやすさが微妙な気がするのでソース公開する際にはちょっといい感じに直しておこうと思います。

タイマーのTICK処理は次のようにします。

private void timer_tick(object? sender, EventArgs e) // 【後編追加】タイマーTICK時処理
{
    _timer.Stop(); // タイマー停止(繰り返し実行されてしまわないようにする)
    Card card1 = _cells[_shownCardsXY[0].X, _shownCardsXY[0].Y];
    Card card2 = _cells[_shownCardsXY[1].X, _shownCardsXY[1].Y];
    if (card1.Number == card2.Number) // 1枚目と2枚目の数値が一致していたら
    {
        _cells[_shownCardsXY[0].X, _shownCardsXY[0].Y] = null; // 1枚目のカードを_cellsから除去する(nullにする)
        _cells[_shownCardsXY[1].X, _shownCardsXY[1].Y] = null; // 2枚目のカードを_cellsから除去する
        _shownCount = 0; // めくったカードの数を0に戻す
        _getCards += 2; // 取得したカードの数を+2する
        if (_getCards == 52) // 取得したカードの数が52に達したら(=場のカードを全て取得できたら)
        {
            MessageBox.Show("おめでとう!"); // おめでとうメッセージ表示
            StartNewGame(); // メッセージボックスが閉じたら次のゲームを開始
        }
    }
    else  // 1枚目と2枚目の数値が一致していなかったら
    {
        _cells[_shownCardsXY[0].X, _shownCardsXY[0].Y].Shown = false; // 1枚目のカードのめくりフラグを降ろす
        _cells[_shownCardsXY[1].X, _shownCardsXY[1].Y].Shown = false; // 2枚目のカードのめくりフラグを降ろす
        _shownCount = 0; // めくったカードの数を0に戻す
    }
    Refresh(); // 再描画
}

あ。よくみたら「めくったカードの枚数を0に戻す」処理は if節にもelse節にもありますね。
ムダなんで外に出しましょうか。

private void timer_tick(object? sender, EventArgs e) // 【後編追加】タイマーTICK時処理
{
    _timer.Stop(); // タイマー停止(繰り返し実行されてしまわないようにする)
    _shownCount = 0; // めくったカードの数を0に戻す
    Card card1 = _cells[_shownCardsXY[0].X, _shownCardsXY[0].Y];
    Card card2 = _cells[_shownCardsXY[1].X, _shownCardsXY[1].Y];
    if (card1.Number == card2.Number) // 1枚目と2枚目の数値が一致していたら
    {
        _cells[_shownCardsXY[0].X, _shownCardsXY[0].Y] = null; // 1枚目のカードを_cellsから除去する(nullにする)
        _cells[_shownCardsXY[1].X, _shownCardsXY[1].Y] = null; // 2枚目のカードを_cellsから除去する
        _getCards += 2; // 取得したカードの数を+2する
        if (_getCards == 52) // 取得したカードの数が52に達したら(=場のカードを全て取得できたら)
        {
            MessageBox.Show("おめでとう!"); // おめでとうメッセージ表示
            StartNewGame(); // メッセージボックスが閉じたら次のゲームを開始
        }
    }
    else  // 1枚目と2枚目の数値が一致していなかったら
    {
        _cells[_shownCardsXY[0].X, _shownCardsXY[0].Y].Shown = false; // 1枚目のカードのめくりフラグを降ろす
        _cells[_shownCardsXY[1].X, _shownCardsXY[1].Y].Shown = false; // 2枚目のカードのめくりフラグを降ろす
    }
    Refresh(); // 再描画
}

これで実行してみましょう。

2枚ずつしかめくれなくなり、ペアができたら除去、できなかったら裏返る。
そして全部除去できたらおめでとうメッセージが出ましたね!

★ ★ ★

では、最後に「カードの種類を数字ではなく色で識別できるようにする」をやりましょうか。

カードは1~13の13種類ですので、色も13種用意します。
ここではサンプルとして 青、赤、紫、緑、水色、黄色、白、薄青、薄赤、薄紫、薄緑、薄水色、薄黄色 にしていますが、お好みの色にしていただければと思います。

private static readonly Color[] CardColors =  // 【後編追加】色の配列
{
    Color.Blue, Color.Red, Color.Magenta, Color.Green, Color.Cyan, Color.Yellow, Color.White,
    Color.DeepSkyBlue, Color.Pink, Color.Purple, Color.LightGreen, Color.LightCyan, Color.LightYellow
};

カード表示処理を変更し、数字を描く代わりに色を付けるようにします。

private void DrawCells(Graphics g) //「場」を描く
{
    ...
        if (_cells[x, y] != null) // カードが存在する状態であれば
        {
            //g.FillRectangle(Brushes.White, x * 64 + 1, y * 88 + 1, 62, 86); // 白い塗りつぶしの四角を描く // 【後編変更】コメントアウト
            //g.DrawString($"({x + 1},{y + 1})\n{_cells[x, y].Number}", new Font("MS ゴシック", 16), Brushes.Black, x * 64 + 2, y * 88 + 2); // 座標と番号を描く // 【後編変更】コメントアウト
            Brush b = new SolidBrush(CardColors[_cells[x, y].Number - 1]); // 【後編追加】描画色を取得する
            g.FillRectangle(b, x * 64 + 1, y * 88 + 1, 62, 86); // 【後編追加】取得した描画色で塗りつぶしの四角を描く
        }
        else // 裏の場合
        {
            g.FillRectangle(Brushes.Gray, x * 64 + 1, y * 88 + 1, 62, 86); // グレーの塗りつぶしの四角を描く
        }
    ...
}

描画色を取得する際、Numberから1を引いているのは「カードの番号としては1始まりの数値で持っている」「でも配列の添字は0スタート」であることが理由です。

実行してみます。

数字ではなく色で示されるようになりましたね!
ちゃんと同じ色をめくれれば消えていきます。

★ ★ ★

というわけで、神経衰弱の続きをお伝えしました。いかがだったでしょうか。

コード自体はシンプルなんですが、いろいろとアレンジを加えていくと
どんどん面白くなっていくと思います!

また、データの持たせ方なんかも今回ご紹介したやり方以外にもいろいろありますね。

たまにはお仕事ではなく、こんなお遊びプログラムを書いてみるのって結構気分転換になります。

また、時間を見つけてちょいちょい作っていきたいなと思っています!

機会があれば、こちらで書かせていただくかもしれません。

それではまた!

追伸

ソースを公開しました。よろしければご覧ください^^
https://github.com/YasuhikoKiuchi/ShinkeiSuijaku/