はじめに

4月もそろそろ後半。
新人研修でプログラミングを勉強している方は、そろそろ実践的な内容を取り組んでいる方も多いと思います。
課題をこなしていく中で、「そろそろ俺も出来るようになってきた」と自信を付けてくる方も多いはず。

一方で、「何に使うんだこれ」「何が便利なんだろう」と、初めてやる方にはメリットが分かりにくい技術があるのも事実。
例えばメソッド、クラス。極めていく中ではとても重要な技術なのですが、とっつきにくく感じるでしょう。
よく入門のときに使う、「Dogクラス」とか「walkメソッド」だと、なにが便利なのかイマイチ分からないんですよね。

また、はじめのうちは「奇数なら『はい』、偶数なら『いいえ』と出力」、「1から100まで出力」といった、なんとなく単調なプログラムばかりで、退屈してしまっている方もいるのではないでしょうか。

そこでご提案したいのは、あのトランプゲームの『ブラックジャック』を開発してみる、ということ。
いままで練習として開発してきたものと比べ、ゲーム性が高いので、きっと楽しく開発できるはずです。

自信を付けてきたあなたも、なんとなく行き詰まってきたあなたも、退屈してしまっているあなたも、
プログラミング実習の卒業試験として、『ブラックジャック』開発をやってみましょう!

なぜブラックジャック?

そもそも、なぜブラックジャックを卒業試験としてやるのでしょうか?
トランプゲームなら他にポーカーもあります。ゲームとしてもいろんなものがあるでしょう。
なぜブラックジャックが卒業試験として向いているのか、少し解説します。

基本ルールがシンプル

ブラックジャックの基本ルールは、非常にシンプルです。(一つを除いて)
21に近い方の勝ち。ただし21を超えてしまうとその時点で負け。基本はこれだけです。
これがポーカーだと、勝利を決定するための「役」を考えなければなりません。優先順位も考えなければなりません。
役を考えるためには、数字だけでなく記号も考慮する必要があるし、それ以外にも云々。。。
要するにブラックジャックの基本ルールはシンプルです。なので、卒業試験としてうってつけのゲームです。

かつ、戦術が複数あるので、さらなる高みを目指せる

ブラックジャックには、状況に応じて行使できる複数の戦術があります。
スピリット、ダブルダウン、サレンダー・・・・
しかし、もっとも肝な基本ルールを開発するときには、これらのルールを頑張って開発する必要はないでしょう。
その基本ルール開発が完了し、さらにスキルを上げたい時に、これらの戦術の開発にチャレンジ出来ます。

ディーラー(CPU)のカードを引くルールは絶対的

一般的に、CPUの開発は非常に悩ましいです。
ポーカーで考えてみましょう。初手をCPUが引いた時、CPUは「どのカードを捨てるか」の判断をする必要があります。
安定志向?博打を狙いやすい性格?
といった、「考え方の傾向」を実装しなければなりません。
また、うまく作らないと、例えば初手ですでにストレートが完成しているのに、「ペアがない!ポイ!!」と、全部捨ててしまうことも考えられます。
このような思考をすべて頑張って作るのは、非常に大変なのは分かると思います。

しかしブラックジャックのディーラー(CPU)は、驚くほどシンプルです。
それは「17以上になるまで引き続ける」、これだけ、ほんとこれだけ。
ポーカーに比べると、とてつもなくシンプルですよね?
このルールさえちゃんと実装できていれば、CPUをカンタンに実装できてしまうわけです。

開発してみよう 

さて、ブラックジャックを作りたくなってきましたよね?よね??
それでは早速、開発してみましょう。

この記事の方針

①この記事は、この記事を読みながら、読者の方が実装にチャレンジ出来るような構成になっています。
そのため、ストレートな答えとなるコードは載せません。
匂わせる程度には書きますが、基本的には自分で実装してもらいたく書いています。

②記事での言語はC#を使用してます。
ただ、それ以外のプログラミング言語でも開発できるはずです。
皆さんがいま勉強しているプログラミング言語で開発してみましょう。

開発するブラックジャックのルール

  • 初期カードは52枚。引く際にカードの重複は無いようにする
  • プレイヤーとディーラーの2人対戦。プレイヤーは実行者、ディーラーは自動的に実行
  • 実行開始時、プレイヤーとディーラーはそれぞれ、カードを2枚引く。引いたカードは画面に表示する。ただし、ディーラーの2枚目のカードは分からないようにする
  • その後、先にプレイヤーがカードを引く。プレイヤーが21を超えていたらバースト、その時点でゲーム終了
  • プレイヤーは、カードを引くたびに、次のカードを引くか選択できる
  • プレイヤーが引き終えたら、その後ディーラーは、自分の手札が17以上になるまで引き続ける
  • プレイヤーとディーラーが引き終えたら勝負。より21に近い方の勝ち
  • JとQとKは10として扱う
  • Aはとりあえず「1」としてだけ扱う。「11」にはしない
  • ダブルダウンなし、スピリットなし、サレンダーなし、その他特殊そうなルールなし

実施しているイメージです。
image.png

さて、ここで上の赤字にモヤッとした方もいるはずです。
「Aが1だけなんてブラックジャックなのか」と。

実はこれが、上記で書いたブラックジャックの実装で難しい「唯一の例外」です。
Aを1 11というのは、それなりに複雑な処理が必要になります。
これをいきなり実装すると、とっても難しいので、まずは「Aは1」とだけ扱って、実装を進めています。
・・・とはいえ、Aがふつうの1のブラックジャックなんてあんまし面白くないですよね。
なので、ブラックジャックの雛形が出来たら、真っ先に機能拡張するという認識でいてください。

開発開始!

というわけで、早速開発してみましょう!
まずはノーヒントで。今までの皆さんの知識で開発してみてください。

ここから下は、実際にご自身で分かるところまで、開発を行ってから見て下さい!

●ヽ(´・ω・)ノ●
●ヽ(・ω・
)ノ●
 ●(ω・ノ●
  (・
ノ● )
  (●  )●
●ヽ(   )ノ●
 ●(   ´)ノ●
  (   ´ノ●
  ( ノ● )
  ●,´・ω)
●ヽ( ´・ω・)ノ●
●ヽ(´・ω・)ノ●
●ヽ(・ω・
)ノ●
 ●(ω・ノ●
  (・
ノ● )
  (●  )●
●ヽ(   )ノ●
 ●(   ´)ノ●
  (   ´ノ●
  ( ノ● )
  ●,´・ω)
●ヽ( ´・ω・)ノ●
●ヽ(´・ω・`)ノ●

実装でよくある問題点・改善点・ヒント

ここからは実際に、開発にトライした方向けの内容です。
すんなり完成した方も、途中でこんがらがってしまった方も、いっぱいいらっしゃると思います。

ここでは、自分が行っている講習でブラックジャックを課題とした時に、
受講生が開発してくれた内容を元にしています。

カードをすべて「文字列型」として管理している

実施イメージ画像では、カードを出力する際、
以下のように個々のカードを出力していました。

ハートの5
スペードのJ
ダイヤのA
クラブの6

これらのカードをそのまま、文字列として管理しているケースがありました。
カードを引く際、次のような実装です。
(簡略化して記載します)

// マーク
string[] marks = new string[] { "ハート", "スペード", "クラブ", "ダイヤ" };
// 数字
string[] nos = new string[] { "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K" };

// 山札作成
List<string> decks = new List<string>();
foreach (var mark in marks)
{
    foreach (var no in nos)
    {
        //「ハートの5」、「スペードのJ」などの文字列が順番にdecksに代入される
        decks.Add($"{mark}{no}");
    }
}

// カードを引く
Random random = new Random();
return decks[random.Next(decks.Count)];

このような実装だと、後で手札の点数計算をめちゃくちゃ頑張らないといけないです。
JとQとKは10点扱いにしないといけないし、Aも1扱いだし。
そもそも、余計に「ハート」とか「クラブ」とか入っちゃってるし・・・・
「の」で文字列分割splitして、後半を取得すれば、引いたNOを取得は出来る・・・出来る・・・確かに出来ますが・・・

頑張れば出来るかもしれませんが、もう、こんな実装はさっさと捨てましょう。
どうするかというと、皆さんが習ったクラスを使用します。
具体的には、「Card」というクラスを作成する。
Cardクラスには、「記号」と「数字」というプロパティを持つ。

Card(クラス)
【要素】
Mark(記号)
No(数字)

こんなクラスを作成し、List decks = new List(); としていた部分を、
List decks = new List();とする。
こうすることで、後続処理がぐっと楽になります。

カードの「No(数字)」プロパティを文字列型としている

AやJやQやKと表示を行うために、上記で作成したCardクラスの「No(数字)」プロパティを、文字列型としているケースがありました。
しかし、Noを文字列型とすると、やはり後の手札の点数計算が非常に面倒です。
"J"や"Q"や"K"を10と戻す処理が必要ですし・・・

なので、「No(数字)」プロパティは文字列型ではなく、整数型として実装するのが吉です。
そもそも、カードを"J"や"Q"や"K"とさせたいのは、
画面上に引いたカードを表示するときのみなので、
その時だけ、11や12や13を"J"や"Q"や"K"と変換してあげましょう。

(難)カードの「No」は3つの意味がある

上に関連するのですが、
ブラックジャックを実装する場合、カードの「No」には実は、3つの意味があります。

①トランプの「数値」(1,2,3,4,5,6,7,8,9,10,11,12,13)
②トランプの「表示」(A,2,3,4,5,6,7,8,9,10,J,Q,K
③ブラックジャックの「点」(1,2,3,4,5,6,7,8,9,10,10,10,10)

①を基準にして、②、③の値が決まってきます。
つまり、CardのNoの読み書きプロパティを作成する場合、①のトランプの「数値」として実装を行い、
②と③については、①の値を使用した、読み取り専用プロパティとして実装すれば良いわけです。

コードを書いてしまえば、以下のようになります。

/// <summary>
/// カード
/// </summary>
public class Card
{
    public string Mark { get; set; }
    /// <summary>
    /// ①トランプの「数値」
    /// </summary>
    public int No { get; set; }

    /// <summary>
    /// ②トランプの「表示」
    /// </summary>
    public string NoString
    {
        get
        {
            //①トランプの「数値」を使用して判定する
            switch (No)
            {
                // ......ここで条件分岐。1と11と12と13の場合、AとJとQとKを返却する
            }

            return No.ToString();
        }
    }


    /// <summary>
    /// ③ブラックジャックの「点」
    /// </summary>
    public int Point
    {
        get
        {
            //①トランプの「数値」を使用して判定する
            switch (No)
            {
                // ......ここで条件分岐。11と12と13の場合、ともに10を返却する
            }

            return No;
        }
    }
}

カードを重複して引いてしまう

「カードを引く」という行為を、単に「52枚からランダムに引く」という実装にしてしまうと、
1ゲームで同じカードを引いてしまうというケースが生じます。
ラスベガスのカジノでやるブラックジャックだと、何百枚もある山札から引いたりするので、重複もあり得るかもしれませんが、
今回は52枚の山札なので、重複は有り得ません。

では、どうするかというと、以下の流れが必要なわけです。
【ゲーム開始時】
・52枚のカードを山札として、先ほど作成したCardクラスのListを作成する
・山札をシャッフルする

【カードを引く時】
・プレイヤーやディーラーがカードを引く時、山札のうち1枚を取得する
・その取得したCardを、山札からRemoveする
・取得したCardをreturnする
→これで、カードを引いたことになる

自分のやり方の場合、Deckクラスを作成します。

Deckクラス
【要素】
・山札 List(Card)

【メソッド】
・山札を作成、およびシャッフル(戻り値:void)
・カードを引く(戻り値:Card)

このようなクラスを作成することで、カードを引いたり山札を作成する処理が格段と分かりやすくなります。

ユーザー・ディーラークラスの作成→継承

ユーザーとディーラーのクラスを作成することで、カードを引く行為、自分の手札(カード一覧)の管理、自分の現在の点数取得がカンタンに行えます。
実装はこのようになります。

Userクラス
【要素】
・手札 List(Card)
・現在の点数 int ※手札 List(Card)から計算して返却
・バーストかどうか bool ※現在の点数が21を超えていればtrue
【メソッド】
・カードを引く(ユーザー入力Y・Nによる、継続・終了の判定)

Deelerクラス
【要素】
・手札 List(Card)
・現在の点数 int ※手札 Listから計算して返却
・バーストかどうか bool ※現在の点数が21を超えていればtrue
【メソッド】
・カードを引く(17以上になれば終了、それまでカードを引き続ける)

ここで気付いた方もきっといるでしょう。
そう、「カードを引く」という処理の内容以外は、ユーザーとディーラーでまったく同じつくりをしています。
こういう時に何をしようするかというと、そう、クラスの継承です。
abstractで、PlayerBaseクラスを作成します。

abstract PlayerBaseクラス
【要素】
・手札 List
・現在の点数 int ※手札 Listから計算して返却
・バーストかどうか bool ※現在の点数が21を超えていればtrue
【メソッド】
・カードを引く(abstract)

Userクラス(PlayerBaseを継承)
【メソッド】
・カードを引く(ユーザー入力Y・Nによる、継続・終了の判定。PlayerBaseで定義した『カードを引く』の実装)

Deelerクラス(PlayerBaseを継承)
【メソッド】
・カードを引く(17以上になれば終了、それまでカードを引き続ける。PlayerBaseで定義した『カードを引く』の実装)

同じ処理をPlayerBaseクラスにまとめてしまうことで、UserクラスとDeelerクラスで個別に書く必要がなくなり、コードもシンプルになります。
実施する際には、「カードを引く」メソッドをそれぞれ、UserクラスとDeelerクラスのインスタンスで実行すれば完了です。

勝敗の判定

ユーザーとディーラーがカードを引き終えた後、勝敗の判定を行います。
勝敗の条件としては、以下の内容になるはずです。
1. ユーザーが21を超えていれば、ユーザーの負け
2. ディーラーが21を超えていれば、ユーザーの勝ち
3. 点の多いプレイヤーの勝ち

もっと言ってしまえば、1の「ユーザーが21を超えた」時点で、ディーラーがカードを引かずともプレイヤーの敗北にしてしまっていいでしょう。
点数計算は、上記で作成したPlayerBaseクラスの「現在の点数」メソッドや、「バーストかどうか」メソッドを使用するといいです。

おわりに

細かいことまで、色々と書いてしまいました。
「自分で作れるようになってほしい」という思いから、コードは極力書かず、改善点などのみ書くようにしましたが、分かりにくかったかもしれないですね。
実装したコードをgithubで公開することも検討します。

さて、ブラックジャックは上記のように、クラス・メソッド・継承など、コーディングする際に必要な概念をほぼ網羅しています。
なので研修の卒業試験としては最適でしょう。

ここまでの内容の開発が完了したら、機能拡張をどんどん行ってほしいです。
例として、
- Aを1か11として扱う
- チップを掛けてゲームする
- ダブルダウン、スピリット、サレンダー実装
などなど。

後は皆さんの手で、どんどん改修を行ってみてください!
コーディングの難しさ、楽しさを体験してもらえれば、僕は嬉しいです。

16contribution

私の後輩もブラックジャック作ってました、練習の題材として最適ですよね。

ただ、abstract PlayerBaseクラスは個人的には非推奨ですね。
UserもDeelerもPlayerBase型で扱うのなら抽象化として間違っていないのですが、そうするとUserに対する機能追加が難しくなります。
UserもDeelerもそれぞれの型で扱うのなら、継承としての意味がないです。(継承による差分プログラミング)

継承は強力な分デメリットも多く、デザパタのような定石以外では使うべきではないと思っています。

6contribution

@sikani
継承しているのは共通の処理を何度も書かないようにするためでは?
is Aの関係もなりたっており継承することはただしく思うのですが。
Aを1か11として扱うというのもUser,Deelerで共通ですしBaseでいいと思います。
不勉強で見当違いのことを言ってたらすみません。

@hirossyi73
面白い記事でした!後輩ができたら試験的にやってみたいと思います!

836contribution

@yuu_j さん
推測ですが、継承より、インターフェースを作って処理を委譲しようと言うことではないでしょうか?
それなら拡張の余地を残したまま、同じ処理も二度と書かずにすみます。

16contribution

@yuu_j さん
@sanonosa さん

おっしゃるとおり、この継承は間違いではありません。
ただ、ポリモフィズムの伴わない継承はイマイチだと思います。

私ならば手札をListで持つのではなくこれをHandクラスにラップして、そこにエースの点数調整やブラックジャック判定、バスト判定などの処理を書きます。
これで、「継承よりコンポジションを選ぶ」・「ファーストクラスコレクションを使用する」を満たし、よりオブジェクト指向なコードになると思います。

ネタバレになってしまうのでコード例は控えます。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.