C# ブラックジャックの実装

#はじめに
Qiitaで色々検索をしていると
[プログラミング入門者からの卒業試験は『ブラックジャック』を開発すべし](https://qiita.com/hirossyi73/items/cf8648c31898216312e5)
という記事を見つけたので、学習中のC＃で作成してみました。

ソースコードを載せています。
実装方法など改善すべき箇所や、もっと一般的な方法などがありましたらコメントを頂けると助かります。

#ブラックジャックのルール
基本ルールはそのままです。

・カードの点数
Aは、1または11として計算する
2 ~ 10 は数値がそのまま計算する
J, Q, K は 10点として計算する

・参加人数
1~4人
コマンドライン上の入力で参加人数を決定します。
勝敗は各プレイヤーとディーラーの点数で判定します。

・実装していないルール
サレンダー、ダブルダウン、スプリットは実装していません。


#完成品
コマンドライン上で実行できるものを作成しました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1667587/891eb05e-dab3-6018-64ae-b42966a157a2.png" alt="代替テキスト" width="400" height="500">


#ソースコード

###山札クラス
```C#:Deck.cs
public class Deck
{
    /// <summary>
    /// 山札用管理リスト
    /// </summary>
    private List<Card> cards = new List<Card>();

    /// <summary>
    /// 引いたカードの枚数を管理
    /// 山札からカードを引くときに使用
    /// </summary>
    private int _drawn_Card_Number = 0;

    public Deck()
    {
        // 山札の生成
        Deck_Prepare();
    }


    public Card this[int i]
    {
        get => this.cards[i];
    }

    /// <summary>
    /// カードを52枚分生成して山札とする
    /// </summary>
    private void Deck_Prepare()
    {
        if (cards == null)
        {
            return;
        }

        // Cardを生成して山札とする(52枚)
        for (int mark_i = 1; mark_i <= 4; mark_i++)
        {
            for (int no_i = 1; no_i <= 13; no_i++)
            {
                cards.Add(new Card(mark_i, no_i));
            }
        }

        // 山札をシャッフルする
        Shuffle();
    }

    /// <summary>
    /// 山札をシャッフルする
    /// ゲームの途中でシャッフルする可能性があるためメソッド化
    /// </summary>
    public void Shuffle()
    {
        if (cards != null)
        {
            // 山札をシャッフルする
            cards = cards.OrderBy(a => Guid.NewGuid()).ToList();
        }
    }

    /// <summary>
    /// 山札からカードを一枚引く
    /// </summary>
    /// <returns></returns>
    public Card Hit_Card()
    {
        try
        {
            return cards[_drawn_Card_Number];
        }
        finally
        {
            // 必ず+1させる。(次のカードを引くため)
            _drawn_Card_Number++;
        }
    }
}
```

###カードクラス
```C#:Card.cs
/// <summary>
/// トランプのカードを表すクラス
/// </summary>
public class Card
{
    // マーク
    private int _mark;
    /// <summary>
    /// トランプのマーク(1～4)
    /// 1:ハート　2:スペード　3:クローバー　4:ダイヤ
    /// </summary>
    public int Mark
    {
        get => _mark;
        private set
        {
            if (value > 0 && value < 5)
            {
                _mark = value;
            }
        }
    }

    private int _no;
    /// <summary>
    /// トランプの数字(1～13)
    /// </summary>
    public int No
    {
        get => _no;
        private set
        {
            if (value > 0 && value < 14)
            {
                _no = value;
            }
        }
    }

    /// <summary>
    /// コンストラクタ
    /// カード情報を設定
    /// </summary>
    /// <param name="mark">マーク</param>
    /// <param name="no">数字</param>
    public Card(int mark, int no)
    {
        _mark = mark;
        _no = no;
    }

    /// <summary>
    /// ToStringメソッドのオーバーライド
    /// カードのマークと数字を返す
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return string.Format("{0}の{1}", MarkToString(), NoToString());
    }

    /// <summary>
    /// マークを文字列に変換
    /// </summary>
    public string MarkToString()
    {
        switch (Mark)
        {
            case 1:
                return "ハート";
            case 2:
                return "スペード";
            case 3:
                return "クローバー";
            case 4:
                return "ダイヤ";
            default:
                throw new ArgumentOutOfRangeException(nameof(Mark));
        }
    }

    /// <summary>
    /// カードの数字を文字列に変換
    /// </summary>
    public string NoToString()
    {
        switch (No)
        {
            case 1:
                return "A";
            case 11:
                return "J";
            case 12:
                return "Q";
            case 13:
                return "K";
            default:
                return No.ToString();
        }
    }

}
```

###プレイヤー(ディーラー)インターフェース
```C#:Iplayer.cs
public interface IPlayer
{
    /// <summary>
    /// 合計値が21を超えたか判定
    /// </summary>
    bool isBusted { get; }


    /// <summary>
    /// 手札の合計値
    /// </summary>
    int TotalValue { get; }

    /// <summary>
    /// 自分の手札
    /// </summary>
    List<Card> MyHand { get; }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="card"></param>
    void Add_MyHand(Card card);

    /// <summary>
    /// 自分のターンで実行する処理
    /// </summary>
    void MyTurn(Deck cards);

    /// <summary>
    /// A(エース)を考慮して最善手の合計値を求める
    /// </summary>
    void BestSelect_TotalValue();
}
```

###プレイヤー(ディーラー)抽象クラス
```C#:BasePlayer.cs
abstract class BasePlayer : IPlayer
{
    /// <summary>
    /// 21を超えたか判定
    /// true:21以上
    /// </summary>
    public bool isBusted
    {
        get
        {
            return TotalValue > 21;
        }
    }

    /// <summary>
    /// 手札の合計値
    /// </summary>
    public int TotalValue { get; private set; } = 0;


    /// <summary>
    /// 自分の手札
    /// </summary>
    public List<Card> MyHand { get; set; } = new List<Card>();


    /// <summary>
    /// 自分のターンの処理
    /// </summary>
    /// <param name="cards">山札</param>
    public void MyTurn(Deck cards)
    {
        Exe_MyTurn(cards);
    }


    /// <summary>
    /// 手札にカードを加える
    /// <param name="card">手札に追加するカード</param>
    /// </summary>
    public virtual void Add_MyHand(Card card)
    {
        MyHand.Add(card);
        Calculate_TotalValue(card.No);

        Console.WriteLine(Properties.Resource.Msg_DrawCard, this.ToString(), card.ToString());
        Disp_MyHand();
    }


    /// <summary>
    /// 手札の合計値の計算
    /// バーストしているかの判定するための計算処理なので、A(エース)は1として計算する
    /// </summary>
    /// <param name="no">手札に追加されたカードの数値</param>
    protected void Calculate_TotalValue(int no)
    {
        // J(11)、Q(12)、K(13)は10として計算する
        if (no > 10)
        {
            no = 10;
        }

        TotalValue += no;
    }

    /// <summary>
    /// 自分の手札を画面表示する
    /// </summary>
    protected void Disp_MyHand()
    {
        var dispStr = "";

        foreach (var card in MyHand)
        {
            dispStr += "[" + card.ToString() + "]";
        }
        Console.WriteLine(Properties.Resource.Msg_MyHand, this.ToString(), dispStr);
    }

    /// <summary>
    /// A(エース)を考慮した最善手の合計値に更新する
    /// </summary>
    public void BestSelect_TotalValue()
    {
        Disp_MyHand();

        // 手札にA(エース)が含まれない場合は終了
        if (MyHand.Count(x => x.No == 1) == 0)
        {
            return;
        }

        // 合計値計算用
        var tmpVal = 0;

        // A(エース)以外を抽出して計算
        var otherA_MyHand = MyHand.Where(x  => x.No != 1);

        foreach (var card in otherA_MyHand)
        {
            if (card.No > 10)
            {
                tmpVal += 10;
            }
            else
            {
                tmpVal += card.No;
            }
        }

        // A(エース)を抽出して計算
        var onlyA_MyHand = MyHand.Where(x => x.No ==1);

        foreach (var card in onlyA_MyHand)
        {
            // バーストしなければA(エース)を[11」として計算
            // バーストする場合は、A(エース)を「1」として計算
            if ((tmpVal + 11) > 21 )
            {
                tmpVal += 1;
            }
            else
            {
                tmpVal += 11;
            }
        }

        // 合計値を更新する
        TotalValue = tmpVal;
    }


    /// <summary>
    /// 抽象メソッド
    /// 継承先で自分のターンの処理を実行する
    /// </summary>
    /// <param name="cards"></param>
    protected abstract void Exe_MyTurn(Deck cards);

}
```
バーストしているかの判定時は、Aは1として計算します。
勝敗を決定する際はBestSelect_TotalValueメソッドにて、Aを1とするか、11とするか決定して計算します。

###プレイヤークラス
```C#:Player.cs
class Player : BasePlayer
{
    /// <summary>
    /// プレイヤー番号
    /// </summary>
    private int playerNumber;

    /// <summary>
    /// ToStringのオーバーライド
    /// プレイヤー+番号を返す
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return string.Format("プレイヤー{0}", playerNumber);
    }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="num">プレイヤー番号</param>
    public Player(int num)
    {
        // プレイヤー番号の割り当て
        playerNumber = num;
    }

    /// <summary>
    /// プレイヤーのターンに実行する処理
    /// </summary>
    /// <param name="cards">山札</param>
    protected override void Exe_MyTurn(Deck cards)
    {
        Console.WriteLine();
        Console.WriteLine(Properties.Resource.Msg_Turn, this.ToString());
        Disp_MyHand();


        // バーストしていない間(isBusted=false)はループを続行
        // プレイヤーがスタンドした場合はループを終了
        while (! isBusted)
        {
            // カードを引くか尋ねる(false:カードを引くのをやめる)
            if (! Game_Controller.Confilm(Properties.Resource.Msg_Stand))
            {
                return;
            }

            // カードを手札に加える
            var card = cards.Hit_Card();
            Add_MyHand(card);
        }

        if (isBusted)
        {
            // バーストした
            Console.WriteLine(Properties.Resource.MSg_Busted);
        }
    }
}
```

###ディーラークラス
```C#:Dealer.cs
class Dealer : BasePlayer
{
    /// <summary>
    /// ディーラーがカードを引き続ける合計値のボーダーライン
    /// </summary>
    private readonly int Boarder_TotalValue = 17;

    /// <summary>
    /// ToStringのオーバーライド
    /// ディーラーを返す
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return "ディーラー";
    }

    /// <summary>
    /// 手札にカードを加える
    /// </summary>
    /// <param name="card">手札に追加するカード</param>
    public override void Add_MyHand(Card card)
    {
        MyHand.Add(card);
        Calculate_TotalValue(card.No);

        // ディーラーの初期手札の2枚目を表示しない考慮
        if (MyHand.Count != 2)
        {
            Console.WriteLine(Properties.Resource.Msg_DrawCard, this.ToString(), card.ToString());
            Disp_MyHand();
        }
    }

    /// <summary>
    /// ディーラーのターンの処理
    /// 自分の手札の合計値が17以上になるまでカードを引く
    /// </summary>
    /// <param name="cards">山札</param>
    protected override void Exe_MyTurn(Deck cards)
    {

        Console.WriteLine();
        Console.WriteLine(Properties.Resource.Msg_Turn, this.ToString());

        // ディーラーの2枚目のカードをオープンする
        Console.WriteLine(Properties.Resource.Msg_Dealers_2nd, MyHand[1].ToString());
        Disp_MyHand();

        // 自分の手札の合計値が17以上になるまでカードを引く
        while (TotalValue < Boarder_TotalValue)
        {
            // カードを手札に加える
            var card = cards.Hit_Card();
            Add_MyHand(card);
        }

        if (isBusted)
        {
            // バーストした
            Console.WriteLine(Properties.Resource.MSg_Busted);
        }
    }
}
```

###ゲーム管理クラス
```C#:Game_Controller.cs
public class Game_Controller
{
    // ディーラー
    private IPlayer _dealer;
    // 山札
    private Deck cards;
    // 参加プレイヤー管理リスト
    private List<IPlayer> players;


    // ゲームを開始するメソッド
    public void Start_Game()
    {
        // 参加プレイヤー数を取得する
        var entryNum = Get_EntryNumber_Player();

        // 初期化
        Initialize(entryNum);

        // 最初の手札を配る
        Deal_Card();

        // プレイヤーのターン
        foreach (var player in players)
        {
            player.MyTurn(cards);
        }

        // ディーラーのターン
        _dealer.MyTurn(cards);

        // 判定
        Judge();
    }

    /// <summary>
    /// 初期化処理
    /// </summary>
    private void Initialize(int entryNum)
    {
        _dealer = new Dealer();
        cards = new Deck();
        players = new List<IPlayer>();

        // プレイヤーをリストで管理
        for (int i = 1; i <= entryNum; i++)
        {
            players.Add(new Player(i));
        }
    }

    /// <summary>
    /// 参加プレイヤー数を取得する(1～4人)
    /// </summary>
    /// <returns>参加プレイヤー数</returns>
    private int Get_EntryNumber_Player()
    {
        while (true)
        {
            Console.WriteLine(Properties.Resource.Msg_EntryNum);
            var input = Console.ReadLine();
            if (int.TryParse(input, out int num))
            {
                if (num > 0 && num < 5)
                {
                    return num;
                }
            }
        }
    }

    /// <summary>
    /// プレイヤーとディーラーに最初の手札を2枚ずつ配る
    /// </summary>
    private void Deal_Card()
    {
        // カードを引いて画面に表示する
        for (int i = 1; i <= 2; i++)
        {
            foreach (var player in players)
            {
                player.Add_MyHand(cards.Hit_Card());
            }

            _dealer.Add_MyHand(cards.Hit_Card());
        }
    }

    /// <summary>
    /// 勝敗の判定
    /// </summary>
    private void Judge()
    {
        /* ブラックジャックの勝敗 */
        // プレイヤーとディーラーの両方がバースト　＝＞　引き分け
        // プレイヤーがバースト　＝＞　ディーラーの勝ち
        // ディーラーがバースト　＝＞　プレイヤーの勝ち
        // プレイヤーの得点がディーラーより大きい　＝＞　プレイヤーの勝ち
        // ディーラーの得点がプレイヤーより大きい　＝＞　ディーラーの勝ち
        // 得点が同じ　＝＞　引き分け
            
        // プレイヤーごとにディーラーとの勝敗を表示
        foreach (var player in players)
        {
            Console.WriteLine();
            Console.WriteLine(Properties.Resource.Msg_Judge, player.ToString());

            // A(エース)を考慮して再計算
            player.BestSelect_TotalValue();
            _dealer.BestSelect_TotalValue();

            if (player.isBusted && _dealer.isBusted)
            {
                Console.WriteLine(Properties.Resource.Msg_Draw);
            }
            else if (player.isBusted)
            {
                Console.WriteLine(Properties.Resource.Msg_Win, _dealer.ToString());
            }
            else if (_dealer.isBusted)
            {
                Console.WriteLine(Properties.Resource.Msg_Win, player.ToString());
            }
            else if (player.TotalValue > _dealer.TotalValue)
            {
                Console.WriteLine(Properties.Resource.Msg_Win, player.ToString());
                if (player.TotalValue == 21)
                {
                    Console.WriteLine(Properties.Resource.Msg_BlackJack);
                }
            }
            else if (player.TotalValue < _dealer.TotalValue)
            {
                Console.WriteLine(Properties.Resource.Msg_Win, _dealer.ToString());
            }
            else
            {
                // ここまで来たら同点
                Console.WriteLine(Properties.Resource.Msg_Draw);
            }
            Console.WriteLine(Properties.Resource.Msg_JudgeValue, player.ToString(), player.TotalValue, _dealer.TotalValue);
        }
    }


    /// <summary>
    /// プレイヤーにYes/Noの確認をする
    /// </summary>
    /// <param name="msg">表示メッセージ</param>
    /// <returns>true:Yes false:No</returns>
    public static bool Confilm(string msg)
    {
        while (true)
        {
            try
            {
                Console.WriteLine(msg + " y/n");

                ConsoleKeyInfo key = Console.ReadKey();

                switch (key.KeyChar)
                {
                    case 'y':
                        return true;
                    case 'n':
                        return false;
                    default:
                        break;
                }
            }
            finally
            {
                Console.WriteLine();
            }
        }
    }
}
```
###メッセージリソース
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1667587/fcc8fd29-ec9e-c263-3433-fd34584eab50.png" width="400" height="500">

リソースファイルの載せ方が分からなかったので画像です。


###メイン
```C#:Program.cs
class Program
{
    static void Main(string[] args)
    {
        Game_Controller game_Controller = new Game_Controller();

        while (true)
        {
            game_Controller.Start_Game();
            Console.WriteLine();
            Console.WriteLine();
            if (! Game_Controller.Confilm("もう一度遊びますか?"))
            {
                break;
            }
            Console.Clear();
        }
    }
}
```

#最後に
もっとソースに触れて、数をこなしていく必要があるなと感じました。
冒頭に記載しましたが、今後の学習のためにも改善点などありましたらコメントをよろしくお願いします。
また、こういう課題に取り組んだ方が良いなどのアドバイスもあればよろしくお願いします。