Go to Previous Page Go to Contents Go to Java Page Go to Next Page
Second Step of Java
 
 

Mastermind の実装

 
  Solver クラスの実装  
 

設計の時は、Mastermind クラスを先に考えましたが、実装では Solver クラスから作っていきましょう。というのも、Solver クラスの方が Mastermind クラスよりも簡単そうだからです。

できあがったソースファイルは Solver.java です。

まずはじめにクラスの定義から。

public class Solver {

}

次に属性を定義しましょう。前節で示したように、属性は回答、Blow の数、Hit の数、正解かどうかのフラグです。

public class Solver {
 
    // 回答
    private int[] answer;
 
    // Blow の数
    private int blow;
 
    // Hit の数
    private int hit;
 
    // 正解かどうかを示すフラグ
    private boolean result;
}

前節でも説明しましたが、属性は派生クラスで使用することがなければ、基本的に private にします。

それでは、関数の実装を行っていきましょう。Solver のメンバ関数は getAnswer, setBlowAndHit, setResult の 3 種類ですが、もうひとつ重要な関数があります。それはコンストラクタです。

コンストラクタはオブジェクトの初期化を行う関数です。コンストラクタでよく行われる処理として属性の初期化があります。Solver も初期化が必要な変数があるので、これを初期化しましょう。

    public Solver(){
        answer = new int[4];
    }

次にメンバ関数です。簡単な処理になる setBlowAndHit 関数と setResult 関数から書いていきましょう。

Blow の数と Hit の数を設定する setBlowAndHit 関数は次のようになります。

    public void setBlowAndHit(int blow, int hit){
        this.blow = blow;
        this.hit = hit;
        System.out.println("Blow : " + blow + " Hit : " + hit);
    }

引数の blow と hit を属性の blow と hit に代入しています。

関数の引数やテンポラリな変数の名前が属性と同じ場合、引数やテンポラリ変数が優先されます。属性を示したいときは、変数の前に this をつけます。setBlowAndHit 関数のように、コンストラクタや set 関数などで属性に値を代入するなどの処理には使われることがありますが、これ以外では同じ名前の変数を使うのはバグの温床になりかねないのでやめておいたほうが無難です。

属性への代入が終わったら、Blow と Hit を出力します。これが次の入力の時のヒントになります。

setResult 関数も同様に属性に代入を行います。result が true の時は正解なので、メッセージを出力するようにしました。

    public void setResult(boolean result){
        this.result = result;
        if(result){
            System.out.println("正解です!!!!");
        }
    }

さて、Solver クラスの中心となるのは getAnswer 関数です。getAnswer 関数の中ではユーザからの入力を処理して、回答を作ります。

このときに重要なのは、ユーザの入力が正しいわけではないということです。このため、入力データのチェックを行います。チェック項目は

  • 入力文字がすべて数字かどうか
  • 同じ数字が使われていないか

の 2 種類です。getAnswer 関数の中ではこの処理を行う必要があります。すこし長いですが、getAnswer 関数を示します。

 1: public int[] getAnswer(){
 2:     System.out.println("数字を入力してください");
 3:     System.out.print(">");
 4:
 5:     // リーダの設定
 6:     // 標準入力からリーダを取り出して使用する
 7:     InputStreamReader inputStreamReader = new InputStreamReader(System.in);
 8:     BufferedReader reader = new BufferedReader(inputStreamReader);
 9:
10:     while(true){
11:         try{
12:             String answerText = reader.readLine();
13:
14:             boolean checkFlag = true;
15:             for(int i = 0 ; i < answerText.length() ; i++){
16:                 int number;
17:                 try{
18:                     number = Integer.parseInt(answerText.substring(i, i+1));
19:                 }catch(NumberFormatException ex){
20:                     checkFlag = false;
21:                     System.out.println("数字以外の文字が使われています");
22:                     System.out.println("もう一度入力してください");
23:                     System.out.print(">");
24:                     break;
25:                 }
26:                        
27:                 // 同じ数字が使われていないかのチェック
28:                 for(int j = 0 ; j < i ; j++){
29:                     if(number == answer[j]){
30:                         checkFlag = false;
31:                         System.out.println("同じ数字が複数回使われています");
32:                         System.out.println("もう一度入力してください");
33:                         System.out.print(">");
34:                         break;
35:                     }
36:                 }
37:                 if(checkFlag){
38:                     answer[i] = number;
39:                 }else{
40:                     break;
41:                 }
42:             }
43:                           
44:             if(checkFlag){
45:                 return answer;
46:             }
47:
48: }catch(IOException ex){ 49: ex.printStackTrace(); 50: System.exit(1); 51: } 52: } 53: }

まず行うのが、入力を促すメッセージを出力することです (2, 3 行目)。次に入力を行うための Reader を設定します。Java では入出力を行うために 2 種類のクラスを使用します。ストリームと Reader/Writer です。ストリームはバイナリの入出力を行い、Reader/Writer は文字の入出力を行う時に使用します。ここでは、コンソールからの入力なので、Reader を使用します。

Java の標準入力は System.in です。標準出力は System.out、標準エラー出力が System.err になります。System.in は java.io.InputStream クラスのオブジェクトなので、ストリームから Reader を抽出することが必要になります。これには java.io.InputStreamReader クラスを使います。パッケージが異なりますから import 文で java.io パッケージを使用できるようにする必要があります。

同様にストリームから Writer を取り出すときは java.io.OutputStreamWriter を使用します。

7, 8 行目でストリームから Reader を取り出しています。ここでは、Reader を直接使わないで BufferedReader クラスを使用しています。BufferedReader クラスは文字通りバッファを利用している Reader です。バッファすることで効率よく入力を行うことができるため、処理が高速になります。また、1 行単位の読み込みもできるようになります。

10 行目からが実際の入力部分になります。ループになっているのは正しく入力されなかったときに、入力のやり直しを行うためです。

12 行目で入力を行います。BufferedStream#readLine 関数は 1 行単位で読み込みを行う関数です。したがって、リターンキーもしくはエンターキーが押されたときに、読み込みを行います。

14 行目の boolean 型の checkFlag は入力が正しいかどうかを示すフラグです。

15 行目からのループは読み込んだ文字列を 1 文字ずつばらして、数値として配列に入れなおす処理を行っています。文字列中の 1 部分を取り出すには String#substring 関数を使用します。18 行目では 1 文字を取り出して、int に変換を行っています。

17:                try{
18:                    number = Integer.parseInt(answerText.substring(i, i+1));
19:                }catch(NumberFormatException ex){
20:                    checkFlag = false;
21:                    System.out.println("数字以外の文字が使われています");
22:                    System.out.println("もう一度入力してください");
23:                    System.out.print(">");
24:                    break;
25:                }

文字列を数値に変換するために次のような関数が用意されています。

クラス 関数
int Integer static int parseInt(String s)
short Short static short parseShort(String s)
long Long static long paseLong(String s)
float Float static float parseFloat(String s)
double Double static double parseDouble(String s)
byte Byte static byte parseByte(String s)

 

よく使用するのは parseInt 関数と parseDouble 関数だと思います。ただし、parseDouble 関数は Java 2 から導入された関数なので、JDK1.1 ベースで作る Applet などには使用できないことを注意する必要があります。

これらの関数は数字以外の文字を数字に変換しようとすると java.lang.NumberFormatException が発生します。このことを利用すると入力された文字が数字かどうかのチェックを行うことができます。

20 から 24 行目の例外処理で数字以外の文字を入力したときに、チェック用のフラグを false にし、数字の再入力を促すメッセージを出力しています。

一方、同じ数字を使っていないかどうかのチェックは 28 から 36 行目で行っています。

28:                 for(int j = 0 ; j < i ; j++){
29:                     if(number == answer[j]){
30:                         checkFlag = false;
31:                         System.out.println("同じ数字が複数回使われています");
32:                         System.out.println("もう一度入力してください");
33:                         System.out.print(">");
34:                         break;
35:                     }
36:                 }

number は入力文字列の i 番目の数字を表しています。この数字と i より前の数字と比較して、同じ場合は再入力をさせるようにします。前の数字と比較するので j は 0 から i までとなります。

i 番目の文字が数字であり、かつ同じ数字は使用していない場合は、配列 answer に代入し、次の文字のチェックを行います。数字でなかったり、同じ数字を使用している場合は 40 行目の break でループから出るようにしています。

37:                 if(checkFlag){
38:                     answer[i] = number;
39:                 }else{
40:                     break;
41:                 }

15 から 42 行目のループを抜けると、もう一度 checkFlag の確認を行います。true であれば answer を戻り値とし、false の場合は再入力するために while ループでもう一度読み込み処理を繰り返します。

最終的には入力文字を 1 文字づつ切り出して、int 型の配列に入れたものを戻り値とします (45 行目)。

これで、Solver クラスの実装の説明は終わりました。次は Mastermind クラスです。

 
  Mastermind クラスの実装  
 

Mastermind クラスは Solver より少し複雑になります。

完成版はこちらで参照できます Mastermind.java

Mastermind クラスでも、Solver と同様に属性から定義していきましょう。Mastermind クラスの属性は正解と正解の桁数です。

public class Mastermind {
    
    // 正解
    private int[] correctAnswer;
 
    // 正解の桁数
    private static final int answerFigure = 4;

正解の桁数は正解と異なり、static な変数になっています。これは変数をそのクラスのオブジェクトで共通した定数とするときによく使われます。

static な変数や関数はクラスに固有な変数となります。図 2-4 は int 型の val という static な変数を持つ Clazz クラスを表しています。Clazz をインスタンス化してできたオブジェクトはそれぞれ val を持っているように見えますが、その実体は Clazz の static 変数をさしているだけです。図 2-4 では val の値は 100 なので、objectA の val も、objectB の val も、objectC の val もすべて 100 になっています。たとえば、objectA が val の値を変更したら、objectB でも objectC でも val は変更された値になります。

定数
図 1-8 定数

また、public でかつ static 変数は クラス名.変数名 で使用することができます。たとえば、よく使われる例としてπを表す Math.M_PI などがあます。これらの public static で表した定数はそのクラスのオブジェクトがなくても、いつでも使用することができます。

定数であることを明確にする場合には final という修飾詞を変数につけます。final 宣言することで、変数の値を変更することはできなくなります。answerFigure も final になっているので、値をプログラム中で変更することはできません。

次にコンストラクタをつくります。コンストラクタでは変数などの初期化を行うと上述しましたが、図 1-7 の1. の正解を作成することはゲームの中で一番はじめに一度だけ行えばいいので、correctAnswer の初期化というふうにも考えられます。

    public Mastermind(){
        makeCorrectAnswer();
    }

したがって、コンストラクタでは正解を作成する関数 makeCorrectAnswer を呼び出すだけにしました。

それでは、makeCorrectAnswer 関数も見ていきましょう。

 1:    private void makeCorrectAnswer(){
 2:
 3:        correctAnswer = new int[answerFigure];
 4:
 5:        Random random = new Random();
 6:        int numberCount = 0;
 7:        
 8:        while(numberCount < answerFigure){
 9:            int number = random.nextInt(10);
10:            boolean checkFlag = true;
11:            for(int i = 0 ; i < numberCount ; i++){
12:                // 使われている数字かどうかをチェックする
13:                if(number == correctAnswer[i]){
14:                    checkFlag = false;
15:                    break;
16:                }
17:            }
18:            if(checkFlag){
19:                correctAnswer[numberCount] = number;
20:                numberCount++;
21:            }
22:        }
23:    }

はじめに属性の correctAnswer の初期化を行います (3 行目)。配列の大きさは数字の桁数と同じなので、answerFigure を使用して大きさを指定しています。

5 行目で登場する java.util.Random クラスは乱数を発生させるためのクラスです。乱数を発生させるには 0 から[引数で指定した値] - 1 までの整数の乱数を発生させる nextInt 関数や、0.0 から 1.0 までの乱数を発生させる nextDouble 関数などがあります。9 行目で使っている nextInt は引数が 10 なので 0 から 9 までの乱数を発生させます。

8 行目からの while ループで一桁づつ正解を作るのですが、乱数はこのときに使用します。

発生させた乱数が前の桁で使用されていないかどうかをチェックするのが 11 行目から 17 行目までの for ループです。チェックの方法は Solver クラスの getAnswer で行った方法とほとんど同じで、前の桁の数字と比較して同じであればフラグを立てるという方法です。前の桁だけを調べるので for ループの i は 0 から numberCount までのループになっています。

数字が使われていなければ、correctAnswer に代入して、桁を一桁あげます。桁が answerFigure と同じになればループを抜けます。これで正解ができあがります。

正解ができたので、Blow と Hit を調べる関数を作りましょう。

countBlow 関数と countHit 関数は内部の処理がほとんど同じなので、countBlow 関数だけ説明します。

 1:    private int countBlow(int[] answer){
 2:        int blow = 0;
 3:        for(int i = 0 ; i < answerFigure ; i++){
 4:            for(int j = 0 ; j < answerFigure ; j++){
 5:                if(answer[i] == correctAnswer[j]){
 6:                    blow++;
 7:                }
 8:            }
 9:        }
10:        return blow;
11:    }

回答と正解で同じ数字が使われているかどうかを調べるために、countBlow 関数は二重の for ループになっています。回答の i 桁目が、正解に含まれているかを調べるため内側のループを使用します。もし、回答の i 桁目と正解の j 桁目が同じならば blow を一つ増やします。

countHit の場合は回答と正解が同じ数字でかつ同じ位置になくてはならないので、二重ループは使わなずにすみます。

checkAnswer 関数は単純に hit と正解の桁数を比較しているだけです。同じならば true、異なれば false を返します。

 1:    private boolean checkAnswer(int hit){
 2:        if(hit == answerFigure){
 3:            return true;
 4:        }else{
 5:            return false;
 6:        }
 7:    }

ゲーム自体を行うのが public 関数である playGame です。

 1:    public void playGame(){
 2:        Solver solver = new Solver();
 3:
 4:        System.out.println("Let's Play Mastermind.");
 5:
 6:        for(int i = 0 ; i < 10 ; i++){
 7:            System.out.println((i+1) + "回目のトライ");
 8:            int[] answer = solver.getAnswer();
 9:
10:            int blow = countBlow(answer);
11:            int hit = countHit(answer);
12:            solver.setBlowAndHit(blow, hit);
13:
14:            boolean result = checkAnswer(hit);
15:            solver.setResult(result);
16:            if(result){
17:                break;
18:            }
19:        }
20:
21:        System.out.print("正解は ");
22:        for(int i = 0 ; i < answerFigure ; i++){
23:            System.out.print(correctAnswer[i]);
24:        }
25:        System.out.println(" でした");
26:    }

playGame がコールされると一番先に行うのが Solver オブジェクトを生成することです。

生成した Solver オブジェクトに規定回数だけ回答して、Blow、Hit をカウントするのが 6 行目から 19 行目までの for ループです。

ループのはじめに現在の回数を出力します。そして、8 行目で solver の getAnswer 関数をコールすることで、回答を受け取ります。配列に入れられた回答を countBlow 関数、countHit 関数でカウントします。その結果は solver の setBlowAndHit 関数を使用して、slover に伝えられます。

もし、14 行目の checkAnswer 関数で hit の数と正解の桁数の比較し、結果を setResult 関数を用いて solver に知らせます。

もし、回答が正解ならループを抜けて (17 行目)、正解を出力します (21 行目から 25 行目)。

規定回数であたらなかった場合も、21 行目から 25 行目を処理するため、正解が出力されます。

この関数を抜けるとゲームは終わりです。

これで、Mastermind クラスの実装は終わりでしょうか。ちょっと、待ってください。Java のプログラムを実行させるには main 関数が必要ですね。

main 関数には何を記述すればいいのでしょうか。main 関数はオブジェクト指向の世界と非オブジェクト指向の世界を結びつけるためにあります。ということは main 関数は非オブジェクト指向の世界にあります。ここにいろいろな処理を記述することは、非オブジェクト指向の部分が増えてしまい、プログラム全体の統一感が薄れてしまうような気がするのです。そこで、main の役割は本当にオブジェクト指向の世界と非オブジェクト指向の世界を結びつけるだけ、すなわちメインとなるオブジェクトを生成して関数をコールするだけと割り切ったほうがいいと筆者は考えています。したがって、main 関数に記述する内容は

  1. プログラムの実行時の引数の処理
  2. プログラムのメインとなるオブジェクトを生成し、そのクラスの関数のコール

main 関数の引数である args が実行するときの引数となります。この引数を生成したオブジェクトに引き渡すのが、1 になります。

Mastermind では特に実行時の引数はないので、Mastermind オブジェクトを生成して、ゲームを開始するだけですみます。したがって、main 関数は下のようになります。

    public static void main(String[] args){
        Mastermind mastermind = new Mastermind();
        mastermind.playGame();
    }

 

 
  遊んでみよう  
 

さて、それでは Mastermind を実行させて見ましょう。動作させる前にはコンパイルを行わなくてはなりません。ソースが C:\java\mastermind にあるとしたら、

C:\java\mastermind>javac Mastermind.java

Mastermind.java をコンパイルすると、芋づる式に使用したクラスをコンパイルするので、Solver.java もコンパイルできます。コンパイルが終わったら、実行してみましょう。

C:\java\mastermind>java Mastermind
Let's Play Mastermind.
1回目のトライ
数字を入力してください
>1234
Blow : 2 Hit : 1
2回目のトライ
数字を入力してください
>3569
Blow : 0 Hit : 0
3回目のトライ
数字を入力してください
>1247
Blow : 3 Hit : 1
4回目のトライ
数字を入力してください
>1235
Blow : 2 Hit : 1
5回目のトライ
数字を入力してください
>3567
Blow : 1 Hit : 0
6回目のトライ
数字を入力してください
>1278
Blow : 4 Hit : 1
7回目のトライ
数字を入力してください
>2178
Blow : 4 Hit : 0
8回目のトライ
数字を入力してください
>1728
Blow : 4 Hit : 0
9回目のトライ
数字を入力してください
>7281
Blow : 4 Hit : 4
正解です!!!!
正解は 7281 でした

9 回目でやっとあたりました。読者の皆さんも遊んでみてください。

 

 
 

作成したソースファイルとコンパイルを行ったクラスファイルはここでダウンロードできます mastermind1.zip

(Sep. 2000)

 
 
Go to Previous Page Go to Contents Go to Java Page Go to Next Page