はじめに
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」にはしない
- ダブルダウンなし、スプリットなし、サレンダーなし、その他特殊そうなルールなし
さて、ここで上の赤字にモヤッとした方もいるはずです。
**「Aが1だけなんてブラックジャックなのか」「それはゲームとして面白いのか」**と。
実はこれが、上記で書いたブラックジャックの実装で難しい**「唯一の例外」**です。
Aを1 か 11というのは、それなりに複雑な処理が必要になります。
これをいきなり実装すると、とっても難しいので、まずは「Aは1」とだけ扱って、実装を進めています。
・・・とはいえ、Aがふつうの1のブラックジャックなんてあんまし面白くないですよね。
なので、ブラックジャックの雛形が出来たら、真っ先に機能拡張するという認識でいてください。
(※もちろん、自信のある方は、Aを1か11として扱う、通常のブラックジャックのルールで最初から開発してしまうのも良いと思います!)
##開発開始!
というわけで、早速開発してみましょう!
まずはノーヒントで。今までの皆さんの知識で開発してみてください。
ここから下は、実際にご自身で分かるところまで、開発を行ってから見て下さい!
NOW LOADING...
●ヽ(´・ω・`)ノ●
●ヽ(・ω・` )ノ●
●(ω・`ノ●
(・`ノ● )
(● )●
●ヽ( )ノ●
●( ´)ノ●
( ´ノ●
( ノ● )
●,´・ω)
●ヽ( ´・ω・)ノ●
●ヽ(´・ω・`)ノ●
●ヽ(・ω・` )ノ●
●(ω・`ノ●
(・`ノ● )
(● )●
●ヽ( )ノ●
●( ´)ノ●
( ´ノ●
( ノ● )
●,´・ω)
●ヽ( ´・ω・)ノ●
●ヽ(´・ω・`)ノ●
実装でよくある問題点・改善点・ヒント
ここからは実際に、開発にトライした方向けの内容です。
すんなり完成した方も、途中でこんがらがってしまった方も、いっぱいいらっしゃると思います。
ここでは、自分が行っている講習でブラックジャックを課題とした時に、
受講生が開発してくれた内容を元にしています。
カードをすべて「文字列型」として管理している
実施イメージ画像では、カードを出力する際、
以下のように個々のカードを出力していました。
ハートの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(string) decksとしていた部分を、
List(Card) decksとする。
こうすることで、後続処理がぐっと楽になります。
カードの「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による、継続・終了の判定)
Dealerクラス
【要素】
・手札 List(Card)
・現在の点数 int ※手札 List(Card)から計算して返却
・バーストかどうか bool ※現在の点数が21を超えていればtrue
【メソッド】
・カードを引く(17以上になれば終了、それまでカードを引き続ける)
ここで気付いた方もきっといるでしょう。
そう、「カードを引く」という処理の内容以外は、ユーザーとディーラーでまったく同じつくりをしています。
こういう時に何をしようするかというと、そう、クラスの継承です。
abstractで、PlayerBaseクラスを作成します。
abstract PlayerBaseクラス
【要素】
・手札 List(Card)
・現在の点数 int ※手札 List(Card)から計算して返却
・バーストかどうか bool ※現在の点数が21を超えていればtrue
【メソッド】
・カードを引く(abstract)
Userクラス(PlayerBaseを継承)
【メソッド】
・カードを引く(ユーザー入力Y・Nによる、継続・終了の判定。PlayerBaseで定義した『カードを引く』の実装)
Dealerクラス(PlayerBaseを継承)
【メソッド】
・カードを引く(17以上になれば終了、それまでカードを引き続ける。PlayerBaseで定義した『カードを引く』の実装)
同じ処理をPlayerBaseクラスにまとめてしまうことで、UserクラスとDealerクラスで個別に書く必要がなくなり、コードもシンプルになります。
実施する際には、「カードを引く」メソッドをそれぞれ、UserクラスとDealerクラスのインスタンスで実行すれば完了です。
勝敗の判定
ユーザーとディーラーがカードを引き終えた後、勝敗の判定を行います。
勝敗の条件としては、以下の内容になるはずです。
- ユーザーが21を超えていれば、ユーザーの負け
- ディーラーが21を超えていれば、ユーザーの勝ち
- 点の多いプレイヤーの勝ち
もっと言ってしまえば、1の「ユーザーが21を超えた」時点で、ディーラーがカードを引かずともプレイヤーの敗北にしてしまっていいでしょう。
点数計算は、上記で作成したPlayerBaseクラスの「現在の点数」メソッドや、「バーストかどうか」メソッドを使用するといいです。
【追記】Aを1か11として扱う方法
基本的な実装が完了したら、先延ばしにしていた、Aを1か11で扱う方法を検討していきましょう。
ところで、Aが手札にある場合、そもそもAの点数はどのように計算しているでしょうか?
例えば、手札が
(1)4 + A + 2 の場合、Aは11と扱われ、手札の点数は17として扱われます。
(2)4 + A + 2 + 9 の場合、Aは1として扱われ、手札の点数は16として扱われます。
(3)4 + A + 6の場合、Aは11と扱われ、手札の点数は21として扱われます。
この違いはどこにあるかというと、結論を言うと、
それは**A以外の手札の合計点**です。
(1)の場合は、A以外の合計点は4 + 2 = 6なため、Aが11でもまだバーストしないので、Aは11になります。
(2)の場合は、A以外の合計点は4 + 2 + 9 = 15なため、Aが11だとバーストしてしまうので、Aは1になります。
(3)の場合は、A以外の合計点は4 + 6 = 10なため、Aが11だとちょうど21になるので、Aは11になります。
ということで、Aをどのように計算する必要があるというと、
『A以外の合計点を計算し、Aが11でもバーストしないなら11、バーストするなら1』
として計算を行って下さい。
具体的には、PlayerBaseで実装している「現在の点数」要素を修正することになります。
ただし、このやり方だと、アプリなどでよくある『現在の点数は 5/15 です』のように、
Aが1でも11でも問題ない場合には対応できないと思います。
そのように表示したい場合の対応方法は・・・考えてみてください(・∀・)
おわりに
細かいことまで、色々と書いてしまいました。
「自分で作れるようになってほしい」という思いから、コードは極力書かず、改善点などのみ書くようにしましたが、分かりにくかったかもしれないですね。
実装したコードをgithubで公開することも検討します。
さて、ブラックジャックは上記のように、クラス・メソッド・継承など、コーディングする際に必要な概念をほぼ網羅しています。
なので研修の卒業試験としては最適でしょう。
ここまでの内容の開発が完了したら、機能拡張をどんどん行ってほしいです。
例として、
- ブラックジャック(Aと10の組み合わせによる特殊な手、という意味での)
- チップを賭けてゲームする
- ダブルダウン、スプリット、サレンダー実装
- 複数ユーザーでの実行
などなど。
後は皆さんの手で、どんどん改修を行ってみてください!
コーディングの難しさ、楽しさを体験してもらえれば、僕は嬉しいです。
【追記】実装方法の改善案について
コメント、本当にありがとうございます。
ここまでで、開発方法について「これが正解」のように色々書いてしまいましたが、
実際は、改善案はいくらでもあると思っています。
その際に、自分で考えて、「どうすればもっと分かりやすいか」「どうすればメンテナンスが楽になるか」「どうすればさらに良くなるか」などを検討して行動していくことは、とても大切だと思います。
これは現場でも発生しうることだと思います。
お客様から機能案を提案されたけど、さらにいい内容があるので、改善案を提示する。
先輩社員からソースコード受け取ったけど、なんだかとっても分かりにくいコードなので、「どうすれば分かりやすいコードなのか」を考えてみる。
また、自分の書いたコードが、人に渡したら分かりにくいと言われてしまったので、どうやったら改善できるか考える。
などなど。。。色んなパターンがありますね。
ここで記載したのは、あくまでも一案と思ってもらえると嬉しいです。
どうすればもっと良くなるか?どうすればメンテナンス性が上がるか?
それを皆さん自身で考えて、改善していく・・・それを出来るようなエンジニアになってもらえると、僕は嬉しいです。