入門書は読み終わったけど、なにを作ればいいのかわからない…
基本的なスキルの組み合わせで、簡単なアプリを作れないかな?
こんにちは。Javaを使い始めて8年目のテックライター・エンジニア、平山です。
突然ですが、皆さんこんな経験はありませんか?
- 入門書を読み終えて、書いてあることは一通りわかったんだけど、そこからなにを作ればいいのかわからない。
- 先輩に相談してみたら、なにか簡単なものを作ってみたら?とアドバイスをもらったけど、何が作れるのかわからない。
- ぼんやりとWebアプリやゲームを作ってみたいけど、手持ちの技術ではどう頑張ってもできそうにない。
じつはこの経験、プログラミングを学び始めてある程度進んだ人の多くが引っかかる、罠のような地点なんです。
この罠をうまく乗り越えられた人は、プログラミングの学習が1段階上がり、中級者として成長していけます。ですが、ここで罠にハマってしまうと、プログラミング学習を挫折してしまう危険地帯でもあるんです。
この記事では、なぜこのような危険地帯があるのか?そして、どうやって乗り越えていけばいいのかを紹介します。さらに、なにか作りたい人のために、具体的なプログラムの例を紹介し、改善する方法も解説していきます。ぜひ実際に手を動かしながら読んでみてください。
それでは早速行ってみましょう!
- 入門書を終了したら簡単なゲーム開発からはじめるのがおすすめ
- ブラックジャックゲーム開発で基礎スキルをアウトプットできる
- 簡単な課題からスタートすれば挫折しにくい
入門書を終了した人がぶつかる【なにか作りたいけど作れないの壁】とは!?
冒頭で書きましたが、プログラミング学習には何度か超えなければいけない壁にぶつかる時期があり、その一つが入門書を読み終えた直後です。
その壁をここではなにか作りたいけど作れないの壁と呼んでいきます。
この壁は、真面目に入門書をこなしてきた人ほどぶつかりやすいもので、実際に筆者もこの壁を超えられずに悩んだ時期がありました。
なぜ、入門書を読み終えたタイミングで壁にぶつかるのかというと、次の理由が考えられます。
- 入門書終了時点で作りたいものが明確になっていない
- 作りたいものはあるが、どうやって作っていいのかわからない
- 基本文法はわかったが、それらを組み合わせる方法がわからない
1つ目は、将来のため、スキル習得のために学習を始めた方に多く見られるパターンです。必要に迫られてとりあえず、入門書を読み終えたけど、さて次に何をしたものか、と路頭に迷った状態ですね。
2つ目は、作りたいものがイメージできてはいるけれど、それを実現するための方法が入門書段階では学べなかった、というパターンです。
プログラミング初学者や独習者にありがちな勘違いとして、プログラミング言語を理解すれば何でも作れるようになる、というものがあります。ですが、これは大いなる勘違いなのです。実際はライブラリやフレームワークといった周辺の知識、サーバやデータベースといった特定言語以外の知識、設計方法や様々な概念など、言語を横断する知識などなど。知識だけでも様々なものが何かをつくるには必要なんですね。
3つ目の理由は、実はプログラミング教育の現場でもちょくちょく話題になる問題点です。
プログラミングの入門書は基本文法の習得を目標に書かれていることがほとんどです。ですがこれ、日曜大工に例えると、玄能(ハンマー)やドライバー、かんななどの工具の使い方を理解した状態とほとんど変わりません。
多くの人にとって、かんなを上手に使えることが最終目的ではないですよね。犬小屋だったり、机や本棚だったりを自分で作れるようになることが目的のはずです。
ここで足りないのは、犬小屋など作りたいものの設計図と組み立てるための手順です。あとは材料さえあれば、道具の使い方は理解しているはずなので、はじめての作品を自力で作り始めることができるでしょう。
ひるがえってプログラミングに話を戻すと、3つ目の理由で壁にぶつかっている人は、作りたいものの設計図と手順がわかっていないからこそ、壁を乗り越えることができていないと言えます。
では、これらの壁をどうやって乗り越えればいいのか?
一番簡単な方法は、実際に何かを作ってみることです。自分に興味のあるものが望ましいですが、いきなり大規模なものを作ろうとすると、2番目の理由で紹介したように、必要な知識が多すぎて挫折してしまいかねません。
そこで、今回提案したいのが、簡単なブラックジャックゲームを作ってみてはいかがでしょうか?というものです。
どれくらい簡単かというと、GUIを実装しません。オブジェクト指向も使いません。Aは常に1としてしか判定されません。
ここまで簡略化しても、実際に0から組み立てようとすると、200行前後は必要になるはずです。そして、入門書で出てくる変数、条件分岐、ループ、メソッド、標準入出力、ライブラリの利用がバランスよく必要になってきます。
簡単なゲームを実装することは、3番目の理由で壁にぶつかっている人以外にも良い影響をもたらすのです。
2番めの大きなアプリを作成したい人にとっても、小規模なアプリを完成させることは準備としていい経験になります。
1番目の目標が明確になっていないとっても、簡単なアプリを作っていく中で、試行錯誤することでプログラミングの面白さを見つけることができるかもしれません。
というわけで、入門書を読み終えた方にはぜひ、これから紹介するような簡単なゲームを作ってみていただきたいのです。
今回はJavaをつかって、簡単なブラックジャック風のゲームを実装していきましょう。必要な知識はJavaの基本的な文法とJavaを実行できる環境です。
この記事はPleiades 2018-12 をつかってソースコードを記述、実行確認しました。
Javaで簡単ブラックジャックの仕様策定!
それでは、早速開発をしていきたいところですが、まずは基本的な仕様を確認しましょう。
今回の仕様は以下の通りとします。
カード枚数は52枚。ジョーカーは含めない。カードの重複が無いように山札を構築する。
プレイヤー、ディーラーの一対一で対戦するものとし、以下の挙動を取る
初期設定として、プレイヤー・ディーラーが交互に1枚ずつ山札からカードを取り手札とする。
プレイヤーからは自分の手札すべてと、ディーラーの1枚めの手札が確認できる。(ディーラーの2枚目移行の手札はわからない)
手札はAが1ポイント、2-10がそれぞれ2-10ポイント、J/Q/Kが10ポイントとして計算される。
プレイヤーは手札を1枚追加するか、しないかを選択できる。
手札を追加した場合、21ポイントを超えるとバーストとなり、ゲームに敗北する。
プレイヤーはバーストするか、好きなタイミングで止めるまで手札にカードを追加できる。
ディーラーは手札の合計ポイントが17以上になるまで山札を引き続ける。
ディーラーの手札が21ポイントを超えた場合、バーストしてプレイヤーの勝利。
ディーラーの手札が18以上21以下になったとき次の段階に移行する。
プレイヤー・ディーラーの手札のポイントを比較して、大きいほうが勝利。
ダブルダウンやスプリットなどの特殊ルールは無し。
基本仕様をざっと確認してみると、おおよそ一般的なブラックジャックといってよさそうです。
ですが普通のブラックジャックと違い、Aは1ポイントとしてしか計算しません。これはコードの簡略化のための措置です。最初からAを1と11どちらで扱ってもいいようにすると、コードの組み方が難しくなります。なのでまずはAを1とだけ扱って、その後、余力があれば1と11に切り替えられる仕組みを作るようにしています。
最初から難しいことをいきなり実装せず、簡単なものを作ってから機能追加していく方法は、挫折しないための重要なポイントです。
では、この流れをコメントにしたjavaファイルを作ってみましょう。クラス名はBlackjackClass、パッケージはnet.sejukuとしました。
package net.sejuku; import java.util.ArrayList; import java.util.List; import java.util.Collections; import java.util.Scanner; /*カード枚数は52枚。ジョーカーは含めない。カードの重複が無いように山札を構築する。 プレイヤー、ディーラーの一対一で対戦するものとし、以下の挙動を取る 初期設定として、プレイヤー・ディーラーが交互に1枚ずつ山札からカードを取り手札とする。 プレイヤーからは自分の手札すべてと、ディーラーの1枚めの手札が確認できる。(ディーラーの2枚目移行の手札はわからない) 手札はAが1ポイント、2-10がそれぞれ2-10ポイント、J/Q/Kが10ポイントとして計算される。 プレイヤーは手札を1枚追加するか、しないかを選択できる。 手札を追加した場合、21ポイントを超えるとバーストとなり、ゲームに敗北する。 プレイヤーはバーストするか、好きなタイミングで止めるまで手札にカードを追加できる。 ディーラーは手札の合計ポイントが17以上になるまで山札を引き続ける。 ディーラーの手札が21ポイントを超えた場合、バーストしてプレイヤーの勝利。 ディーラーの手札が18以上21以下になったとき次の段階に移行する。 プレイヤー・ディーラーの手札のポイントを比較して、大きいほうが勝利。 ダブルダウン・スプリット・サレンダーなどの特殊ルールは無し。*/ public class BlackjackClass { public static void main(String[] args) { System.out.println("ゲームを開始します"); //空の山札を作成 //山札をシャッフル //プレイヤー・ディーラーの手札リストを生成 //プレイヤー・ディーラーがカードを2枚引く //山札の進行状況を記録する変数deckCountを定義 //プレイヤーの手札枚数を記録する変数playerHandsを定義 //プレイヤー・ディーラーの手札のポイントを表示 System.out.println("あなたの1枚目のカードは" + toDescription(player.get(0))); System.out.println("ディーラーの1枚目のカードは" + toDescription(dealer.get(0))); System.out.println("あなたの2枚めのカードは" + toDescription(player.get(1))); System.out.println("ディーラーの2枚めのカードは秘密です。"); //プレイヤー・ディーラーのポイントを集計 System.out.println("あなたの現在のポイントは" + playerPoint + "です。" ); //プレイヤーがカードを引くフェーズ //ディーラーが手札を17以上にするまでカードを引くフェーズ //ポイントを比較する System.out.println("あなたのポイントは" + playerPoint); System.out.println("ディーラーのポイントは"+ dealerPoint); System.out.println("引き分けです。"); System.out.println("勝ちました!"); System.out.println("負けました・・・"); } //山札(deck)に値を入れ、シャッフルするメソッド private static void shuffleDeck(List<Integer> deck) { // リストに1-52の連番を代入 //山札をシャッフル //リストの中身を確認(デバッグ用) } //手札がバーストしているか判定するメソッド private static boolean isBusted(int point) { } //現在の合計ポイントを計算するメソッド private static int sumPoint(List<Integer> list) { } //山札の通し番号を得点計算用のポイントに変換するメソッド.J/Q/Kは10とする private static int toPoint(int num) { } //山札の数を(スート)の(ランク)の文字列に置き換えるメソッド private static String toDescription(int cardNumber) { } //山札の数をカードの数に置き換えるメソッド private static int toNumber(int cardNumber) { } //カード番号をランク(AやJ,Q,K)に変換するメソッド private static String toRank(int number) { } //山札の数をスート(ハートやスペードなどのマーク)に置き換えるメソッド private static String toSuit(int cardNumber) { } }
コメントの流れを追っていくことで、おおよそどんな処理が必要なのか想像できたのではないでしょうか?流れを文章化してみると以下のようになります。
- シャッフルされた山札を作る
- プレイヤー・ディーラーの手札リストを定義する
- それぞれ2枚ずつカードを山札から引く
- 山札とプレイヤー手札の進行状況を記録する変数を用意する
- それぞれの手札のポイントを表示
- プレイヤーがカードを引くか、引くのを辞めるかループ
- ディーラーが手札のポイントが17以上になるまでカードを引き続ける
- それぞれのポイントを比較して、勝敗判定
ざっくりとプログラムを開発するための骨格が見えてきましたね。ここからは実際にコメント部分のコードを実装していきます。
自分で手を動かして考えてみたい方は、ぜひこの段階でプログラムを組んでみてください。わからない部分があったら次の章で確認するのがオススメです。
次の章では設計図の上から実際にどう組むかを解説していきます。
Javaの簡単ブラックジャック実装(前半)
では、Javaの簡単ブラックジャックの実装を見ていきましょう。章全体が長くなっているので、前半後半で分割しました。
前半では山札の生成や手札のセッティング、手札を表示する方法とこれらを支えるメソッドの設計を見ていきます。
リストについて
各論に入る前に、全体を通して使われているリストについて解説します。
入門書終了段階ではリストについて学習していないかもしれません。ですが、リストを使うことでこのプログラムはとても見通しがよくなるので、今回は採用しています。
リストはJavaのコレクションフレームワークという仕組みに含まれているもので、ざっくりいうと配列のスゴイ版です。どこらへんがスゴイのかというと
- 宣言の際に要素数の指定が不要
- forEachやラムダ式への対応が配列より簡単
- 便利なメソッドがたくさんある
と、配列に比べて柔軟性・利便性がとても高くなっています。
ここでは簡単な使い方を紹介します。詳しく知りたい方はこちらの記事をご覧ください。
まず、リストを使うためには以下のパッケージをインポートする必要があります。
import java.util.ArrayList; import java.util.List;
次に、リストの宣言方法です。リスト型のオブジェクトは以下のように宣言します。
List<データ型名> オブジェクト名 = new ArrayList<データ型名>();
たとえば、int型の数値を格納するためのリスト、playerは次のように表記します。
List <Integer> player = new ArrayList<>();
データ型にintではなくIntegerという見慣れないものが入っていることに引っかかるかたもいるかも知れません。ココらへんは話すと長くなるので、概要だけお伝えします。
リストを含むコレクションはインスタンスのみを格納でき、プリミティブ型(int,char,booleanなど)を格納することができません。そのため、プリミティブ型をインスタンスに変換するラッパークラス、というものに置き換えます。
ラッパークラスについて詳しく知りたい方はこちらをご覧ください。
山札の生成とシャッフル
ここから実際のコードを見ていきます。まずは山札を生成して、シャッフルするところまでです。
山札についてですが、トランプはクラブ、ダイヤ、ハート、スペードの4種の記号(これをスートといいます)とA、2-10、J、Q、Kの13種類の数(こちらをランクといいます)、あわせて52種類のカードが存在します。
スートとランクの組み合わせでカードを特定する方法もあるのですが、今回はカードに1-52までの数値を振り分け、その数値をスートとランクに変換する方法を利用します。
まずは、山札の生成とシャッフルの段階なので、52枚のカードをリストに格納し、順番をシャッフルする方法を考えましょう。
空の山札を生成するには先に説明したリストを使うのが良さそうです。そこで、以下のようにdeckリストを作ります。
List <Integer> deck = new ArrayList<>(52);
この要素数52個のdeckリストに番号を格納し、シャッフルされた状態を作っていきましょう。シャッフル部分はmainメソッド中に書く必要がないため、分離してshuffleDeckメソッドを作成します。
shuffleDeckメソッドに求められているのは、リストに1-52の連番を入れることと、そのリストをランダムにシャッフルすることです。
この機能を実現するために以下のようにメソッドを組みました。
import java.util.Collections; private static void shuffleDeck(List<Integer> deck) { // リストに1-52の連番を代入 for(int i = 1; i <= 52; i++){ deck.add(i); } //山札をシャッフル Collections.shuffle(deck); //リストの中身を確認(デバッグ用) /* for (int i=0; i<deck.size(); i++) { System.out.println(deck.get(i)); }*/ }
リストに値を追加するにはadd、リストの中身をランダムに入れ替えるにはCollectionsパッケージのshuffleメソッドを利用しました。これを利用するため、冒頭にimport java.util.Collections;が必要です。
Javaには配列の中身をシャッフルするためのメソッドがありません。リストにはメソッドとして実装されています。ココらへんにリストの強みを感じていただけたらと思うのですが、いかがでしょうか。
また、リストの中身が本当にシャッフルされているのか、確認用に吐き出すためのfor文も書いてあります。数値確認だったり、本当にディーラーが機能しているか確認するのにこういった機能があると便利ですね。
手札のセッティング
続いて、プレイヤー・ディーラーそれぞれの手札をセットして行きましょう。手札は枚数が何枚になるか事前にわからないので、リストで作るのが良さそうです。
ということで、手札リストをこのように設計します。
//プレイヤー・ディーラーの手札リストを生成 List <Integer> player = new ArrayList<>(); List <Integer> dealer = new ArrayList<>();
次に、山札から2枚ずつ交互にカードを引いてきます。リストから値を取得するにはgetメソッドが使えます。このため、次のような書き方になるでしょう。
//プレイヤー・ディーラーがカードを2枚引く player.add(deck.get(0)); dealer.add(deck.get(1)); player.add(deck.get(2)); dealer.add(deck.get(3));
このタイミングで、山札と手札の進行状況を記録する変数も用意しておきましょう。山札の進行状況はプレイヤー・ディーラーがカードを引くたびに変動しますし、手札の進行状況は現在が何枚目のカードなのかをプレイヤーに伝えるために利用します。
なお、プレイヤーに何枚目のカードかを伝える必要がなければプレイヤーの進行状況は不要になります。同様の理由で、ディーラー側の進行状況は記録しません。
//山札の進行状況を記録する変数deckCountを定義 int deckCount = 4; //プレイヤーの手札枚数を記録する変数playerHandsを定義 int playerHands = 2;
なお、カードを2枚引く段階からdeckCountを利用してもいいのですが、行数が増えそうなので、ここまで後回しにしました。ハードコーディングと怒られてしまいそうですが、まあ、ブラックジャックのルールが変わらない限り変更する必要もない部分なので、そこまで丁寧にやらなくてもいいだろうという判断です。
引いたカードの表示
今度は、引いたカードを表示する部分を作り込んでいきます。設計図でいうと//プレイヤー・ディーラーの手札のポイントを表示、の部分ですね。
現段階では、プレイヤー・ディーラーそれぞれの手札リストに1-52のいずれかの数が2個格納されている状態です。
これを、あなたの1枚目のカードはハートの8、のように表示するには、数値をカードのスートとランクに変換してあげなければいけません。この変換部分を作るのがこの節の目的です。
まずは、スートに変換する方法を考えましょう。
これは、答えを言ってしまうと、割り算(/)を利用します。カードの番号を13で割り、その商が0だったらクラブ、1だったらダイヤ、2だったらハート、3だったらスペードというふうに対応させることで、カードを4種のスートに分類することができました。
これを実装すると次のようになります。
//山札の数をスート(ハートやスペードなどのマーク)に置き換えるメソッド private static String toSuit(int cardNumber) { switch((cardNumber - 1)/13) { case 0: return "クラブ"; case 1: return "ダイヤ"; case 2: return "ハート"; case 3: return "スペード"; default: return "例外です"; } }
注意点として、入力されるcardNumberから1を引くことが必要です。これをしないと、cardNumber13がクラブではなくダイヤに属してしまいますし、cardNumber52は商が4になるため例外になってしまいます。
続いて、カード番号をランク(A、J、Q、Kなど)に変換するメソッドを見ていきましょう。
こちらは一工夫して、まずカード番号をトランプの数字に変換するメソッドとトランプの数字をランクに変換するメソッドの二段構えで実装していきます。
なぜ、このような手間を掛けるのかというと、後のポイント計算で、トランプの数字をベースに計算する必要があるためです。再利用を考えると、この一手間が後の手間を削減してくれるわけです。
では、カード番号をトランプの数字、1-13に変換していきましょう。
こちらで使うのが剰余(%)です。剰余は割り算のあまりを返す演算ですが、今回は52個の数字を0-12のいずれかに分類するのに使います。
実装は以下の通りです。
//山札の数をカードの数に置き換えるメソッド private static int toNumber(int cardNumber) { int number = cardNumber % 13; if(number == 0) { number = 13; } return number; }
注意点として、カード番号13の倍数の剰余が0になっているので、これを13に戻すためにif文を使っています。
カード番号がトランプの数字に変換できたので、今度はこれをランクに変換しましょう。こちらは実装に関して特に難しい部分は無いはずです。
//カード番号をランク(AやJ,Q,K)に変換するメソッド private static String toRank(int number) { switch(number) { case 1: return "A"; case 11: return "J"; case 12: return "Q"; case 13: return "K"; default: String str = String.valueOf(number); return str; } }
難しい部分は無いといいましたが、注意点として、このメソッドは返り値にStringを設定しているため、2-10の数値もString型で返す必要があります。うっかりミスをしがちなポイントですので、お気をつけください。
ここまでの成果を合体させて、カード番号を(スート)の(ランク)の文字列に置き換えるメソッドを作りましょう。組み合わせるだけなので、特に問題は無いはずです。
//山札の数を(スート)の(ランク)の文字列に置き換えるメソッド private static String toDescription(int cardNumber) { String rank = toRank(toNumber(cardNumber)); String suit = toSuit(cardNumber); return suit + "の" + rank; }
toDescriptionメソッドを使った手札の表示は以下のようになります。
//プレイヤー・ディーラーの手札のポイントを表示 System.out.println("あなたの1枚目のカードは" + toDescription(player.get(0))); System.out.println("ディーラーの1枚目のカードは" + toDescription(dealer.get(0))); System.out.println("あなたの2枚めのカードは" + toDescription(player.get(1))); System.out.println("ディーラーの2枚めのカードは秘密です。");
プレイヤー・ディーラーのポイントを集計
プレイヤー・ディーラーの手札が揃ったところで、ポイントの計算を行い、結果を表示しましょう。
この簡易ブラックジャックではAを1、2-10をそのままの数、J、Q、Kを10としてポイント計算します。そこで、カード番号をポイントに変換するメソッドとそれを合算するメソッドを作成しましょう。
まずはカード番号をポイントに合算するメソッドです。これはカード番号をトランプの数字に置き換えるメソッドがあるので、これを流用して、変換された1-13の数字をポイントに対応させるメソッドとします。
実装は以下の通りです。
//トランプの数字を得点計算用のポイントに変換するメソッド.J/Q/Kは10とする private static int toPoint(int num) { if(num ==11||num == 12||num == 13) { num = 10; } return num; }
続いて、手札のポイントを合算するメソッドです。こちらは、手札リストから手札の番号を取り出し、ポイントに変換して合算していく、という動作になります。
実装は以下の通りです。
//現在の合計ポイントを計算するメソッド private static int sumPoint(List<Integer> list) { int sum = 0; for(int i =0;i < list.size();i++) { sum = sum + toPoint(toNumber(list.get(i))); } return sum; }
リスト型はsizeメソッドでリストの要素数を得ることができます。手札は要素数が変動するため、リストにしておくと便利ですね。
sumPointメソッドを使ったポイント集計は以下の通りです。
//プレイヤー・ディーラーのポイントを集計 int playerPoint = sumPoint(player); int dealerPoint = sumPoint(dealer); System.out.println("あなたの現在のポイントは" + playerPoint + "です。" );
ここまでの成果を画面に表示すると以下のようになります。
今のところ、ゲーム性はサッパリありませんが、インタラクティブな部分は後半で実装していきますので、お楽しみに。
Javaの簡単ブラックジャック実装(後半)
後半戦では、プレイヤーが実際にカードを引くかどうかの判断を求めたり
、ディーラーがカードを引く処理、最後にポイントを比較して勝敗を判定する処理を作っていきます。
プレイヤーがカードを引くフェーズ
まずはプレイヤーがカードを引くフェーズを実装していきましょう。このフェーズで必要な要素は次のとおりです。
- プレイヤーに追加で引くかを尋ねる表示
- キーボードの入力結果で処理を分岐
- yが入力された際は手札に山札から1枚加える
- 山札と手札の進行状況を1進める
- 引いたカードの表示
- 現在の合計ポイントの計算と表示
- プレイヤーのバーストをチェック
- 以上をキーボードでnが入力されるまでループ
最初に文字入力と分岐・ループの大枠を見ていきます。この処理の大枠はwhileループとif文を使って以下のように表現できます。
//プレイヤーがカードを引くフェーズ while(true) { System.out.println("カードを引きますか?Yes:y or No:n"); //キーボードの入力を受け付けて、変数strに代入する if("n".equals(str)) { break; } else if("y".equals(str)) { //手札の追加とバーストチェック } else { System.out.println("あなたの入力は" + str + " です。y か n を入力してください。"); } }
キーボードでnが入力されるまで延々とループが回り続ける処理ですね。これをベースに必要な機能を盛り込んでいきましょう。
まずはキーボードの入力部分です。こちらはScannerクラスを使うことで実現できます。Scannerクラスの詳しい説明はこちらをご覧ください。
キーボードの入力を受け取るだけなので、いきなり実例です。なお、Scannerクラスの利用にはjava.util.Scannerパッケージが必要ですので、冒頭部分でインポートしておきます。
import java.util.Scanner; Scanner scan = new Scanner(System.in); String str = scan.next();
さて、無事にキーボードの入力を取得できたら、条件分岐の中身を実装していきましょう。
ですが、その前に、1点だけ補足を。if文の条件で、”n”.equals(str)という書き方が気になった人もいるのではないでしょうか?普通に str == “n” ではだめなのか、という疑問ですね。
残念ながら、== を使った比較では条件を正しく動かすことができません。これは参照型とプリミティブ型の違いに由来するもので、以下の記事で詳しく解説しています。
ざっくり説明すると、Javaの==は参照先の比較を行い、equalsメソッドは値の比較を行っています。しっかりとした理解をしたい方はこれを機にString型について調べてみるといいでしょう。
また、equals()はnullにつなぐとエラーを出してしまいます。その対策として、判断用の文字列”n”,”y”に対してequals()をつないでいます。ちょっとしたテクニックですが、意外と役立つケースもあるんですよ。
手札の追加とバーストチェック
この節ではプレイヤーがキーボードでyを入力した際の処理を実装していきます。具体的には以下の処理を作ります。
- 手札に山札から1枚加える
- 山札と手札の進行状況を1進める
- 引いたカードの表示
- 現在の合計ポイントの計算と表示
- プレイヤーのバーストをチェック
上から順に処理していきましょう。
手札の追加と進行状況の追加は前半で行ったことの組み合わせです。playerリストにdeckリストからdeckCount番目のカードを追加して、deckCount と playerHands をそれぞれ1進めればいいわけですね。
実装は以下の通りです。
//手札に山札から1枚加える player.add(deck.get(deckCount)); //山札と手札を一枚進める deckCount++; playerHands ++;
引いたカードの表示と合計ポイントの計算も前半戦の使い回しでOKです。手札の進行状況とplayerリストのインデックスの対応にだけ注意してください。
System.out.println("あなたの" + playerHands + "枚目のカードは" + toDescription(player.get(playerHands - 1))); playerPoint = sumPoint(player); System.out.println("現在の合計は" + playerPoint );
バーストのチェックは新しいメソッドが必要になります。とはいえ、実装に必要な考えはそれほど難しくありません。要するに、入力したポイントが21より大きければバーストしたとしてtrueを返し、それ以外ならばfalseを返せばいいのです。
というわけで、実装は以下の通りです。
//手札がバーストしているか判定するメソッド private static boolean isBusted(int point) { if (point <= 21) { return false; } else { return true; } }
実際のバーストチェックはバーストした瞬間にゲームが終了するようにreturnを返します。
//プレイヤーのバーストチェック if(isBusted(playerPoint)) { System.out.println("残念、バーストしてしまいました。"); return; }
以上でプレイヤーがカードを引くフェーズの実装が完了しました。
最後に1点注意点というか、気になる人向けの情報を。
Eclipseでコードを組んでいる場合、このブロックに対して警告が表示されるはずです。内容は、リソース・リーク <オブジェクト名>がこのロケーションで閉じられていません、というものです。
これは、Scannerクラスを使ってストリームをオープンしたものが閉じられていないよ、という警告なんですが、この場合は無視してしまって問題ありません。
というのも、System.inに関しては明示的にストリームをクローズする必要性が無いからです。逆にクローズしてしまうと、このプログラムを実行中はキーボードからの入力を受け付けなくなります。
入門段階ではちょっと難し目の内容ですが、気になる人向けに一応の補足でした。
ディーラーが手札を17以上にするまでカードを引くフェーズ
さて、プレイヤーに続いて、ディーラーがカードを引くフェーズです。このフェーズで実装すべき内容は以下の通りです。
- 手札が17以上の場合、ブレーク
- 手札が17より小さい場合
- 手札に山札から1枚加える
- 山札と手札の進行状況を1進める
- 現在の合計ポイントの計算
- バーストをチェック
- 以上を手札が17以上になるか、バーストするまでループ
こうしてみると、プレイヤーの処理とかなり多くの部分が共通していることに気づくのではないでしょうか。実際、ディーラー側の処理はプレイヤーからキーボード入力を省いて、手札が17以上になったらブレークする処理を加えただけです。
そのため、いきなり実装例です。
//ディーラーが手札を17以上にするまでカードを引くフェーズ while(true) { //手札が17以上の場合ブレーク if(dealerPoint >= 17) { break; } else { //手札に山札から1枚加える dealer.add(deck.get(deckCount)); //山札を一枚進める deckCount++; //ディーラーの合計ポイントを計算 dealerPoint = sumPoint(dealer); //ディーラーのバーストチェック if(isBusted(dealerPoint)) { System.out.println("ディーラーがバーストしました。あなたの勝ちです!"); return; } } }
並べてみると、playerがdealerになっただけでほとんど共通処理であることがわかるでしょう。
得点を比較して勝敗を表示するフェーズ
プレイヤーとディーラーのフェーズが終了したら、最後にポイントを比較して、勝ち負けを判定するフェーズです。
とはいえ、ここまでの段階で、常に合計ポイントは計算しています。なので、あとはそのポイントを表示して、比較結果で勝ち負けを表示するだけです。
そこまで難しくないのでいきなり実例をどうぞ。
System.out.println("あなたのポイントは" + playerPoint); System.out.println("ディーラーのポイントは"+ dealerPoint); if(playerPoint == dealerPoint) { System.out.println("引き分けです。"); } else if(playerPoint > dealerPoint) { System.out.println("勝ちました!"); } else { System.out.println("負けました・・・"); }
以上で簡単ブラックジャックの実装は完了です。最後に実際の実行画面とソースコード全体を掲載します。
package net.sejuku; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Scanner; /*カード枚数は52枚。ジョーカーは含めない。カードの重複が無いように山札を構築する。 プレイヤー、ディーラーの一対一で対戦するものとし、以下の挙動を取る 初期設定として、プレイヤー・ディーラーが交互に1枚ずつ山札からカードを取り手札とする。 プレイヤーからは自分の手札すべてと、ディーラーの1枚めの手札が確認できる。(ディーラーの2枚目移行の手札はわからない) 手札はAが1ポイント、2-10がそれぞれ2-10ポイント、J/Q/Kが10ポイントとして計算される。 プレイヤーは手札を1枚追加するか、しないかを選択できる。 手札を追加した場合、21ポイントを超えるとバーストとなり、ゲームに敗北する。 プレイヤーはバーストするか、好きなタイミングで止めるまで手札にカードを追加できる。 ディーラーは手札の合計ポイントが17以上になるまで山札を引き続ける。 ディーラーの手札が21ポイントを超えた場合、バーストしてプレイヤーの勝利。 ディーラーの手札が18以上21以下になったとき次の段階に移行する。 プレイヤー・ディーラーの手札のポイントを比較して、大きいほうが勝利。 ダブルダウン・スプリット・サレンダーなどの特殊ルールは無し。*/ public class BlackjackClass { public static void main(String[] args) { System.out.println("ゲームを開始します"); //空の山札を作成 List <Integer> deck = new ArrayList<>(52); //山札をシャッフル shuffleDeck(deck); //プレイヤー・ディーラーの手札リストを生成 List <Integer> player = new ArrayList<>(); List <Integer> dealer = new ArrayList<>(); //プレイヤー・ディーラーがカードを2枚引く player.add(deck.get(0)); dealer.add(deck.get(1)); player.add(deck.get(2)); dealer.add(deck.get(3)); //山札の進行状況を記録する変数deckCountを定義 int deckCount = 4; //プレイヤーの手札枚数を記録する変数playerHandsを定義 int playerHands = 2; //プレイヤー・ディーラーの手札のポイントを表示 System.out.println("あなたの1枚目のカードは" + toDescription(player.get(0))); System.out.println("ディーラーの1枚目のカードは" + toDescription(dealer.get(0))); System.out.println("あなたの2枚めのカードは" + toDescription(player.get(1))); System.out.println("ディーラーの2枚めのカードは秘密です。"); //プレイヤー・ディーラーのポイントを集計 int playerPoint = sumPoint(player); int dealerPoint = sumPoint(dealer); System.out.println("あなたの現在のポイントは" + playerPoint + "です。" ); //プレイヤーがカードを引くフェーズ while(true) { System.out.println("カードを引きますか?Yes:y or No:n"); Scanner scan = new Scanner(System.in); String str = scan.next(); if("n".equals(str)) { break; } else if("y".equals(str)) { //手札に山札から1枚加える player.add(deck.get(deckCount)); //山札と手札を一枚進める deckCount++; playerHands ++; System.out.println("あなたの" + playerHands + "枚目のカードは" + toDescription(player.get(playerHands - 1))); playerPoint = sumPoint(player); System.out.println("現在の合計は" + playerPoint ); //プレイヤーのバーストチェック if(isBusted(playerPoint)) { System.out.println("残念、バーストしてしまいました。"); return; } } else { System.out.println("あなたの入力は" + str + " です。y か n を入力してください。"); } } //ディーラーが手札を17以上にするまでカードを引くフェーズ while(true) { //手札が17以上の場合ブレーク if(dealerPoint >= 17) { break; } else { //手札に山札から1枚加える dealer.add(deck.get(deckCount)); //山札を一枚進める deckCount++; //ディーラーの合計ポイントを計算 dealerPoint = sumPoint(dealer); //ディーラーのバーストチェック if(isBusted(dealerPoint)) { System.out.println("ディーラーがバーストしました。あなたの勝ちです!"); return; } } } System.out.println("あなたのポイントは" + playerPoint); System.out.println("ディーラーのポイントは"+ dealerPoint); if(playerPoint == dealerPoint) { System.out.println("引き分けです。"); } else if(playerPoint > dealerPoint) { System.out.println("勝ちました!"); } else { System.out.println("負けました・・・"); } } //山札(deck)に値を入れ、シャッフルするメソッド private static void shuffleDeck(List<Integer> deck) { // リストに1-52の連番を代入 for(int i = 1; i <= 52; i++){ deck.add(i); } //山札をシャッフル Collections.shuffle(deck); //リストの中身を確認(デバッグ用) /* for (int i=0; i<deck.size(); i++) { System.out.println(deck.get(i)); }*/ } //手札がバーストしているか判定するメソッド private static boolean isBusted(int point) { if (point <= 21) { return false; } else { return true; } } //現在の合計ポイントを計算するメソッド private static int sumPoint(List<Integer> list) { int sum = 0; for(int i =0;i < list.size();i++) { sum = sum + toPoint(toNumber(list.get(i))); } return sum; } //トランプの数字を得点計算用のポイントに変換するメソッド.J/Q/Kは10とする private static int toPoint(int num) { if(num ==11||num == 12||num == 13) { num = 10; } return num; } //山札の数を(スート)の(ランク)の文字列に置き換えるメソッド private static String toDescription(int cardNumber) { String rank = toRank(toNumber(cardNumber)); String suit = toSuit(cardNumber); return suit + "の" + rank; } //山札の数をカードの数に置き換えるメソッド private static int toNumber(int cardNumber) { int number = cardNumber % 13; if(number == 0) { number = 13; } return number; } //カード番号をランク(AやJ,Q,K)に変換するメソッド private static String toRank(int number) { switch(number) { case 1: return "A"; case 11: return "J"; case 12: return "Q"; case 13: return "K"; default: String str = String.valueOf(number); return str; } } //山札の数をスート(ハートやスペードなどのマーク)に置き換えるメソッド private static String toSuit(int cardNumber) { switch((cardNumber - 1)/13) { case 0: return "クラブ"; case 1: return "ダイヤ"; case 2: return "ハート"; case 3: return "スペード"; default: return "例外です"; } } }
簡単と言いながらかなりの分量になりましたね。簡易版ではありますが、これも立派な成果物です。作成お疲れ様でした、そして完成おめでとうございます!
簡単ブラックジャックの改善ポイント
せっかく完成の余韻に浸っていたいところですが、この章ではこのプログラムの改善案を紹介します。いずれもJavaのレベルアップには欠かせないことなので、今後のレベルアップを目指す人はぜひ目を通して見てください。
OOPを導入してみよう
このサンプルプログラムは、入門者が本格的なプログラミングの第一歩を踏み出すために作られました。そのため、意図的に避けられた部分があります。
それがオブジェクト指向プログラミング object-oriented programming 、略してOOPです。
なぜOOPを避けたのかというと、1番の理由は初心者にはOOPを使わないプログラミングの方が慣れ親しんでいるし、わかりやすいであろう、と考えたからです。
厳密にいえばクラスやメソッドを利用してる時点で100%OOPを使っていない、とは言い切れないんですが、書き方としては初心者の方が最初に学習する手続き型に限りなく寄っているはずです。
そして、2番目の理由は、OOPは手続き型と比較することで初めて深い理解を得ることができると考えたからです。
オブジェクト指向は単体で学ぶと概念的な部分が多く、腹の底からわかった!とは感じにくいものです。ですので、実際に手続き型で組んだものとオブジェクト指向で組んだもの。具体的なソースを比べることで理解を深めていただきたい。そう考えた結果でもあります。
このプログラムに関してのヒントをお伝えすると、Deck,Player,Dealerの3種のオブジェクトを作り、Player,Dealerに対して抽象クラスを作ることで多くの処理が共通化できます。
OOPはこの先エンジニアとしてやっていく上では必須のスキルですので、ぜひこの機会に習得できるよう頑張ってください。
Aを1と11どちらでも判定できるようにしてみよう
今回は計算の簡略化のために、Aを常に1として計算しました。ですが、実際のブラックジャックに寄せるためにはここが11に判定できるようになるのは欠かせない改良です。
改良のヒントですが、ポイント表示を1/11のように2種類用意することで、この問題に対応することができます。そのためには、 sumPointメソッドかtoPointメソッドを改良する必要があるでしょう。
さらに、次の節の内容とかぶりますが、ダブルダウンやスプリットといった特殊ルールの実装にチャレンジするのも面白いです。
チップをつかってゲーム性を高めよう
ある意味いちばん大事な要素なのに、ここまで無視されてきたものがあります。それは、ブラックジャックがギャンブルであり、チップのやり取りが本来の目的である、という点です。
チップのやり取りを行うには、まずゲームを継続して複数回実行できる必要があります。今まではメインクラスに直接ブラックジャックの処理を書いていましたが、この段階では別クラスに移して、メインクラスは繰り返し実行の役割をもたせたほうがわかりやすいでしょう。
また、チップの導入に合わせて特殊ルールの追加も可能になります。スプリットなどが実装できれば、ぐっと本格的なブラックジャックが楽しめるようになりますよ。
GUIを作ろう
ここまでのすべての表示先はコンソールでした。このアプリはいわゆるCUI、コンソールアプリケーションです。ですが、Javaには当然GUIを作るための仕組みが用意されています。
GUIを作るのは、これまでやってきたこととはまた違った知識、経験が必要になります。JavaFXなどのフレームワークに触れることになるでしょう。
フレームワークはこれからJavaの学習を進めていく際に様々な場面でお世話になるものです。これを機にフレームワークに取り組むのも非常にオススメの選択です。
まとめ
Javaで簡単ブラックジャックを開発してきましたが、いかがでしたか?
すべてのソースについて解説を入れていった結果、かなり膨大な記事になってしまった感は否めません。一日で一気に理解しようとするのはまず不可能なので、少しずつ着実に進めていっていただければと思います。
この経験がJavaのなにか作りたいけど、どうしていいかわからない壁にぶつかっている皆さんの、後押しとなってくれることを願ってやみません。
お相手は、Javaの卒業試験として先輩からもらった課題がブラックジャックだった平山でした。
この記事の監修者
株式会社SAMURAI
独学でプログラミング学習を始めるも挫折。プログラミングスクール「SAMURAI ENGINEER」を受講し、Web制作を学ぶ。副業でWeb制作を行いつつ、「初心者がプログラミングで挫折しないためのコンテンツ制作」をモットーにWebライターとして侍エンジニアブログ編集部に従事。