2014年4月29日火曜日

Java ソースコードのじゃんけんゲーム


私は初めてJava言語を勉強した時に最高な本を読んだ。是非Java言語の初心者にオススメです。


早速じゃんけんゲームの説明を入ります。まずはPlayer クラスから実装をしていきましょう。できあがりは Player.java になります。
なにはなくとも、クラスの定義からはじめましょう。
public class Player{

}

次に定数を定義してしまいましょう。前節で示した「グー」「チョキ」「パー」に対応する int 型の定数です。
public class Player{

    // じゃんけんの手
    public final static int GOO = 0;
    public final static int CHOKI = 1;
    public final static int PA = 2;
}
次に属性を定義しましょう。これは単純に String クラスの name オブジェクトを加えるだけです。
public class Player{

    // じゃんけんの手
    public final static int GOO = 0;
    public final static int CHOKI = 1;
    public final static int PA = 2;
 
    // Player の名前
    private String name;
}


Player の関数はコンストラクタと getName、getHand、selectHand の 3 種類です。はじめにコンストラクタですが、Mastermind の時に説明したように、コンストラクタでよく行われる処理として属性の初期化があります。Player クラスのコンストラクタも属性の初期化を行いましょう。とはいっても属性はプレイヤーの名前しかないのですが。
    public Player(String name) {
        this.name = name;
    }
関数の引数やテンポラリな変数の名前が属性と同じ場合、引数やテンポラリ変数が優先されます。属性を示したいときは、変数の前に this をつけます。コンストラクタや set 関数などで属性に値を代入するなどの処理には使われることがありますが、これ以外では同じ名前の変数を使うのはバグの温床になりかねないのでやめておいたほうが無難です。
名前をかえす getName 関数は次のようになります。
    public String getName() {
        return name;
    }
さて、Player クラスの一番重要な関数なのは、手を返す getHand 関数でしょう。とはいうものの getHand 関数の中身はたった 1 行です。前節の図 2-3 のシーケンス図で示したように、getHand が呼び出されると内部的に手を選択する関数を呼び出しています。ここでは手を選択する関数 selectHand を呼び出すだけです。
    public int getHand(){
        // 手を選ぶ
        return selectHand();
    }

selctHand 関数は Player クラスの中だけで使用され、他のオブジェクトからはコールされることはないので、private 関数にしてあります。
    // 手の選択
    private int selectHand(){
        // 乱数を利用して手を選択する
        return (int)(Math.random() * 3);
    }
Mastermind の時には java.util.Random クラスを利用して乱数を得ていましたが、ここでは java.lang.Math クラスの random 関数を使って乱数を発生させています。Math クラスは java.lang パッケージにあり、数学的な演算 (sin, cos, べき乗演算など) が定義されています。これらの演算を行う関数はほとんど static 関数なので、Math クラスのオブジェクトを生成しなくとも使用することができます。random 関数は 0 から 1 までの double の乱数を発生させます。じゃんけんの手は 3 種類なので、3 を掛けて、int 型にキャストしています。こうすると、0、1、2 のいずれかの整数になり、じゃんけんの手を表すことができます。
ところで、Mastermind の時にマジックナンバについて説明しましたが、selectHand 関数の中で使用している 3 という数字もマジックナンバです。じゃんけんの手の数は 3 以外になることはないと思いますが、やはり直接数字を使うよりは定数にしておきましょう。
これを変更した selectHand 関数は次のようになります。
selectHand の内部は乱数を用いて、手を選ぶという処理を行っています。random 関数は 0 から 1 までの乱数を出力します。手の数は 3 種類なので、手数を示す定数 maxSize をかけます。この値は double なので、int にキャストすると 0, 1, 2 のいずれかの整数になり、じゃんけんの手を表すことができます。
    private int maxSize = 3;
 
    // 手の選択
    private int selectHand(){
        // 乱数を利用して手を選択する
        return (int)(Math.random() * maxSize);
    }
 
 Judge クラスの実装 
 次に Judge クラスを実装していきましょう。完成版はこちらで参照できます Judge.java
Judge クラスの属性は前節で示したように、Player オブジェクトをまとめる players と、じゃんけんの手を表す hands があります。
import java.util.*;
 
public class Judge {
    private List players;
    private List hands;

List クラスは java.util パッケージにあるので、一行目で import しておきます。players と hands の初期化はコンストラクタで行うことにしましょう。
 1:    // コンストラクタ
 2:    //   引数はプレイヤーの数
 3:    public Judge(int playerSize){
 4:        players = new ArrayList();
 5:        hands = new ArrayList();
 6: 
 7:        for(int i = 0 ; i < playerSize ; i++){
 8:            players.add(new Player("Player" + i));
 9:        }
10:    }
players と hands は List インタフェースで定義してありますが、実際に使用するのは ArrayList クラスです。4, 5 行目の new ArrayList() で初期化を行っています。players と hands は List なのに、new するのは ArrayList というのはなんか変ですね。ArrayList クラスは List クラスをインプリメントしているので、このようなことができます。同じようなことはあるクラスを派生させて子クラスを作ったときも可能です。子どものクラスは親のクラスに成り代わることができるのです。
親のクラスに成り代わることができると、なにがうれしいのでしょう。たとえば、List インタフェースをインプリメントした 2 つのクラス、ArrayList と LinkedList を考えてみます。ArrayList クラスはコレクションを作るのに配列を利用しています。かたや、LikedList はリスト構造と呼ばれる方法を用いてコレクションを作ります。2 つのクラスは実装はまったく異なるのですが、コレクションに要素を追加することや削除するなどの機能は同一です。List を使う側から見れば、実装が異なっていても、機能が同じであればまったく問題ありません。それならば、実装を表す ArrayList や LinkedList というクラスよりも、機能を表す List というインタフェースが重要になります。
同じことを車にたとえてみましょう。車のエンジンにはガソリンエンジンやディーゼルエンジンなど複数あります。ガソリンエンジンもレシプロやロータリーという種類もあり、カムやターボなども考えると多くの種類に分けることができます。しかし、ドライバから見れば、エンジンの種類は異なっても、同じ方法で運転はできます。車という機能さえ同じであれば、運転ができるわけです。
ただし、実装がどうでもいいというわけではありません。車でも、それぞれのエンジンの特性を知ってこそうまく乗りこなすことができるように、プログラムでも実装クラスの特徴を把握することでよりよいプログラムを書くことができます。
たとえば、ArrayList は要素へのアクセスは速いのですが、任意の場所に要素を追加したり削除するのは時間がかかります。これとは逆に LinkedList は要素へのアクセスは遅いのですが、要素の追加・削除は高速に行うことができます。
このような特徴はすぐに覚えられるものではないので、プログラミングの経験を積んでいくことでやしなっていく部分だと思います。
さて、プログラムに戻りましょう。 4 行目で初期化した players に要素を追加していくのが、7 から 9 行目の for ループです。プレイヤーの人数が分からないと要素数が決まらないので、コンストラクタの引数としてプレイヤーの人数をとるようにしました。
ループの中では Player オブジェクトを生成して、players に追加 (add) しています。Player クラスのコンストラクタはプレイヤー名が引数なので、players に追加する順番を名前にしています。
次にゲームのメインルーチンである playGame 関数を説明しましょう。
 1:    public void playGame(){
 2:        // プレイヤーに手を問い合わせる
 3:        for(int i = 0 ; i < players.size() ; i++){
 4:            Player player = (Player)players.get(i);
 5:            int hand = player.getHand();
 6:            hands.add(new Integer(hand));
 7:        }
 8:
 9:        // 判定
10:        List winners = judge(hands);
11:
12:        if(winners.isEmpty()){
13:            System.out.println("Draw!");
14:        }else{
15:            for(int i = 0 ; i < winners.size() ; i++){
16:                Player player = (Player)winners.get(i);
17:                System.out.println("Winner is " + player.getName());
18:            }
19:        }
20:    }
図 2-3 のシーケンス図で示したように、ゲームの流れは「プレイヤー手を問い合わせて」から「判定」を行います。3 から 7 行目のループで手を問い合わせて、10 行目の judge 関数で判定を行います。
プレイヤーの手を問い合わせるために、4 行目に示したように players の要素をまず取り出します。List はどのようなオブジェクトでも持つようにするため、要素を取り出す get 関数の戻り値は Object クラスになっています。そこで、Players クラスにキャストすることが必要になります。
取り出した player に対して getHand 関数をコールして、手を問い合わせます。6 行目で問い合わせた手を hands に追加していきます。List には int や double などのクラスでないものは要素にできないので、int の代わりに Integer クラスを使用します。
10 行目で判定を行います。judge 関数については後で説明することにして、先に進みましょう。
12 から 19 行目で判定の結果を出力しています。勝者を要素に持つ winners の要素がなければ、「あいこ」なので、13 行目で出力します。要素数のチェックには isEmpty 関数を使用しています。もし、リストに要素がなければ isEmpty 関数の戻り値は true になります。
勝負がついた場合は 15 か 18 行目に示したように勝者を出力します。16 行目で winners から Player オブジェクトを取り出して、17 行目で Player オブジェクトの名前を出力するようにしています。
判定はいろいろな方法で行うことができるのですが、今回は次のように記述してみました。少し長いのですが、難しい処理はないので、容易に理解できると思います。
 1:    private List judge(List hands){
 2:        List gooPlayers = new ArrayList();
 3:        List chokiPlayers = new ArrayList();
 4:        List paPlayers = new ArrayList();
 5:
 6:        classify(gooPlayers, chokiPlayers, paPlayers, hands);
 7:
 8:        if(gooPlayers.isEmpty()){
 9:            if(chokiPlayers.isEmpty()){
10:                // 全員パー
11:                return new ArrayList(); // あいこ
12:            }else{
13:                if(paPlayers.isEmpty()){
14:                    // 全員チョキ
15:                    return new ArrayList(); // あいこ
16:                }else{
17:                    // チョキとパーでチョキの勝ち
18:                    return chokiPlayers;
19:                }
20:            }
21:        }else if(chokiPlayers.isEmpty()){
22:            if(paPlayers.isEmpty()){
23:                // 全員グー
24:                return new ArrayList();
25:            }else{
26:                // パーとグーでパーの勝ち
27:                return paPlayers;
28:            }
29:        }else if(paPlayers.isEmpty()){
30:            // グーとチョキでグーの勝ち
31:            return gooPlayers;
32:        } else {
33:            // グー、チョキ、パーであいこ
34:            return new ArrayList();
35:        }
36:    }
37: 
38:    private void classify(List gooPlayers,
39:                          List chokiPlayers,
40:                          List paPlayers,
41:                          List hands){
42: 
43:        for(int i = 0 ; i < hands.size() ; i++){
44:            int hand = ((Integer)hands.get(i)).intValue();
45:            switch(hand){
46:              case Player.GOO:
47:                gooPlayers.add(players.get(i));
48:                break;
49:              case Player.CHOKI:
50:                chokiPlayers.add(players.get(i));
51:                break;
52:              case Player.PA:
53:                paPlayers.add(players.get(i));
54:                break;
55:              default:
56:                break;
57:            }
58:        }
59:    }
処理の大まかな流れは
  1. プレイヤーの手をグーとチョキ、パーに分ける
  2. グー、チョキ、パーの組み合わせにより勝敗を決める
  3. グー、チョキがいなければ、パーだけなのであいこ
  4. グー、パーがいなければ、チョキだけなのであいこ
  5. グーがいなくて、チョキとパーだけであれば、チョキの勝ち
  6. グーだけの時は、あいこ
  7. グーとパーがいるときは、パーの勝ち
  8. グーとチョキなら、グーの勝ち
  9. グー、チョキ、パーがすべて出揃っていたら、あいこ
1. の処理を行う前に、それぞれの手を保持しておく List オブジェクトを準備しておきます (2 から 4 行目)。関数 classify で、それぞれの手によって分ける。classify 関数の中は、45 行の switch 文で手の場合わけを行い、それぞれの手の List オブジェクトに追加します。この処理を人数分行います。
2. から 9. の処理が 8 行目から始まる if 文の中で行っています。たとえば、8 行目でグーがいるかどうか調べて、いなければ 9 行目の if 文でチョキがいるかどうか調べます。もし、チョキもいなければバーだけなのであいこになります。
勝負が決まったときは、勝った手の List オブジェクトを戻り値とします。あいこなら、勝者はいないので、たとえば 11 行目のように空の List オブジェクトを戻り値とします。
 
 
 スタートアップルーチンの実装 
 さて、ここまででじゃんけんゲームの実装ができたのですが、これをどうやって動かしましょうか。プログラムを動作させるには main 関数を使用することはお分かりですね。
Judge クラスに main 関数をつけてもいいのですが、これだと実行するときに
C:\>
java Judge
となってしまって、じゃんけんをするようには見えないですね。そこで、main 関数だけをもったスタートアップ用のクラスを作ってしまいましょう。クラス名はもちろん Janken です。
完成した Janken.java はこちらから参照できます Janken.java
Janken クラスの main 関数を次に示します。

 1:    public static void main(String[] args){
 2:        Judge judge;
 3:        if(args.length == 1){
 4:            try{
 5:                judge = new Judge(Integer.parseInt(args[0]));
 6:            }catch(NumberFormatException ex){
 7:                System.out.println("Usage: java Janken [number of Players]");
 8:                return;
 9:            }
10:        }else{
11:            System.out.println("Usage: java Janken [number of Players]");
12:            return;
13:        }
14:
15:        judge.playGame();
16:    }
main 関数の役割は繰り返しになりますが、
  1. プログラムの実行時の引数の処理
  2. プログラムのメインとなるオブジェクトを生成し、そのクラスの関数のコール
になります。Janken クラスではコンストラクタの引数としてプレイヤーの人数を指定するようにしましたので、これを Judge クラスの生成のときに引数として引き渡すことを行います。その後、メインルーチンである playGame 関数をコールするだけです。
プログラムの引数は String の配列で渡されるので、引数の数を調べているのが 3 行目です。配列の大きさは length で調べることができます。引数の数が正しければ、5 行目で Judge オブジェクトを生成します。Judge クラスのコンストラクタは引数にプレイヤーの人数で int ですから、String を int に変換します。これには Integer クラスの parseInt 関数を使用することができます。parseInt 関数は static 関数なので、オブジェクトがなくても使用することができます。また、この関数は引数が数字に変換できないと NumberFormatException という例外を発生します。例外が発生したときは、7 行目で示したように、使い方を示して、プログラムを終了させます。
プログラムの引数の数が 1 以外の時は、7 行目と同様に使い方を示して、プログラムを終了させています。
生成された Judge オブジェクトに対して、15 行目で playGame 関数をコールします。playGame 関数がコールされると、ゲームが開始するわけです。
 
 
 じゃんけんゲームの実行 
 じゃんけんゲームができあがったので、コンパイルして、それから実行してみましょう。ここではソースが C:\java\janken にあった場合を示しています。
C:\java\janken>javac Janken.java

C:\java\janken>java Janken 3
Winner is Player2
これだと、誰がどのような手を出して勝負が決まったのか分からないですね。誰がどのような手を出したのか分かるようにしてみましょう。単に手を出力するだけだと、手は int 型なので数字が出力されてしまいます。そこで、Player クラスに手をあらわす文字列を戻すような関数を作ってみました。hand の値に応じて、文字列を作っているだけの単純な関数です。
    public static String valueOf(int hand){
        String handText = null;

        switch(hand){
          case GOO:
            handText = "GOO";
            break;
          case CHOKI:
            handText = "CHOKI";
            break;
          case PA:
            handText = "PA";
            break;
          default:
            handText = "";
            break;
        }
        return handText;
    }

これを使用して Judge クラスの playGame 関数を書きかえてみました。
    public void playGame(){
        // プレイヤーに手を問い合わせる
        for(int i = 0 ; i < players.size() ; i++){
            Player player = (Player)players.get(i);
            int hand = player.getHand();
            hands.add(new Integer(hand));
        }

        // 判定
        List winners = judge(hands);

        // 手の出力
        for(int i = 0 ; i < players.size() ; i++){
            Player player = (Player)players.get(i);
            int hand = ((Integer)hands.get(i)).intValue();
            System.out.println(player.getName() + "'s hand is "
                               + Player.valueOf(hand));
        }

        if(winners.isEmpty()){
            System.out.println("Draw!");


最後は現在のじゃんけんゲームではどういう方法で勝率が高いか、まだ色々研究をやっている。こんな素晴らしいアルゴリズムを提案する方を待っている。