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

あきらめも肝心だ

 
 

Never Give Up?

 
 

作り上げた Hanoi で遊んでみましたか。解法は説明したものの、はじめはなかなか難しいと思います。おもわず、Give Up してしまいたくなりませんか。しかし、このプログラムには Give Up がありません。途中で終わらせるということは考えていなかったからです。

そこで、今回はゲームの途中で Give Up をするときのことを考えていきましょう。Give Up は入力をするときに g を入れてもらうことにしたいと思います。こんな感じにしてみましょう。

C:\>
java Hanoi

    *
   ***         |          |
  *****        |          |
 *******       |          |
*********      |          |
~~~~~~~~~  ~~~~~~~~~  ~~~~~~~~~
    0          1          2

どこのリングを動かしますか? (g で Give Up) [0-2] g
Give Up!!

C:\>

入力処理は Player クラスの nextStep 関数の中で行うので、ここを改造していきましょう。下に示したのは nextStep 関数の一部分ですが、Give Up の入力がされた場合の部分だけを追加しています。

 1:    public PileMovementInfo nextStep(List towers) {
 2:        while(true){
 3:            String inputText = null;
 4:            try{
 5:                System.out.print("どこのリングを動かしますか? (g で Give Up) [0-2] ");
 6:                inputText = reader.readLine();
 7:
 8:                // g or G が入力されたときは Give Up
 9:                if(inputText.equalsIgnoreCase("g")){
10:                    // ここの部分に Give Up の処理を記述する
11:                }
12:
13:                int source = Integer.parseInt(inputText);

入力文字列と "g" とのの比較を 9 行目で行っています。文字列の比較には == を使用せずに equals 関数を使用します。== はオブジェクトが同じかどうかを示すもので、equals 関数は String オブジェクトが保持している文字列同士を比較します。

しかし、9 行目では equals 関数を使わないで、eqaulsIgnoreCase 関数を使っています。equalsIgnoreCase も文字列の比較を行うのですが、大文字と小文字を区別しないで比較します。ですから、この場合は入力文字列が "g" でも "G" でも true になります。

Give Up の入力を受け付けることはできましたが、これをどうやって Hanoi オブジェクトに伝えればいいでしょうか。nextStep 関数の戻り値は PileMovementInfo オブジェクトなので、そのままでは Give Up の情報を Hanoi オブジェクトに伝えることはできません。考えられる方法を列挙してみましょう。

  1. Give Up の場合、例外を投げるようにする
  2. PileMovementInfo クラスを改造して、Give Up の情報を保持できるようにする
  3. PileMovementInfo クラスを派生させて、新たなクラスに Give Up の情報をもたせる
  4. 戻り値を boolean にして、Give Up の情報を戻すようにし、PileMovementInfo オブジェクトは引数とする

PileMovementInfo クラスは移動に関する情報をもつクラスなので、まったく意味の異なる情報をもたせるのには抵抗があります。とすると、1 か 4 の方法が残ります。今まで発生した例外を処理することはありましたが、例外を投げることはやったことがないので、1 の方法を試してみましょう。

関数が例外を投げるためには throws を使用します。たとえば SomeException を投げる関数 bar は次のようになります。

 1:    public void bar() throws SomeException {
 2:        // 何らかの処理
 3:    
 4:        // 例外を投げる
 5:        throw new SomeException();
 6:
 7:
 8:        // 処理
 9:    }

例外を投げるには throw を使用します。例外もクラスなので、実際に例外を投げる場合は 5行目で行っているように、例外のオブジェクトを作らなければなりません。

例外の使い方が分かったので、Give Up の例外を作りましょう。単純に GiveUpException という名前にしましょう。例外は Exceptio の派生クラスにします。

GiveUpException クラスのソースはこちらです GiveUpException.java

 1: public class GiveUpException extends Exception {
 2:     public GiveUpException(){
 3:         super();
 4:     }
 5:
 6:     public GiveUpException(String s){
 7:         super(s);
 8:    }
 9: }

Excpetion クラスはコンストラクタが引数なしと String オブジェクトが引数の 2 種類があります。GiveUpException クラスはこのコンストラクタを定義しなおしますが、スーパークラスのコンストラクタを呼ぶだけです。

GiveUpException を投げれるようにした nextStep 関数 (部分) を次に示します。

 1:    public PileMovementInfo nextStep(List towers) throws GiveUpException {
 2:        while(true){
 3:            String inputText = null;
 4:            try{
 5:                System.out.print("どこのリングを動かしますか? (g で Give Up) [0-2] ");
 6:                inputText = reader.readLine();
 7:
 8:                // g or G が入力されたときは Give Up
 9:                if(inputText.equalsIgnoreCase("g")){
10:                    throw new GiveUpException();
11:                }
12:
13:                int source = Integer.parseInt(inputText);

1 行目に throws を加え、10 行目で例外を投げるようにしました。

次は GiveUpExcpeiton オブジェクトを受け取るほうの、Hanoi クラスを改造しましょう。

nextStep 関数をコールしているのは startGame 関数の中です。例外を受け取れるように try ... catch を付け加えましょう。

 1:     public void startGame(){
 2:        List towers = makeTowers();
 3:
 4:        System.out.println("ハノイの塔");
 5:        printTowers(towers);
 6:        System.out.println(START + " から " + END + " へ移動させてください\n");
 7:
 8:        do{
 9:            PileMovementInfo pileMovementInfo;
10:            try{
11:                pileMovementInfo = player.nextStep(towers);
12:            }catch(GiveUpException ex){
13:                // Give Up
14:                System.out.println("Give Up!!");
15:                return;
16:            }
17:
18:            movePile(towers, pileMovementInfo.getSource(), pileMovementInfo.getDestination());
19:            printTowers(towers);
20:        }while(!checkCompletion(towers, END));
21:
22:        System.out.println("完成しました\nおめでとうございます");
23:    }

オレンジ色の部分が付け加えたところです。Give Up の時はメッセージを出力して、return で関数を抜け出しています。

 

 
  ハノイの塔を解いてみる  
 

Give Up できるようになったのはいいのですが、なんかものたりなくないですか。せっかくだから、アプリケーションでハノイの塔を解いてみましょう。

なんか難しそうな感じがしますが、意外と簡単にできます。一番はじめに説明したように n 枚の円盤の移動には、n - 1 枚の円盤を動かせるということを利用します。こんなときは再帰という手法を用いて解くことができます。再帰というのは自分で自分を呼び出すことです。たとえば、n の階乗を計算するにはこんなふうにします。

 1:    public int factorial(int n) {
 2: 
 3:        if(n = 0){
 4:            return 1;   // 0 の階乗は 1
 5:        }
 6: 
 7:        int x = factorial(n - 1);
 8:        return x * n;
 9:    }

7 行目で自分自身の関数を呼んでいますが、このとき引数は n - 1 にしています。n の階乗を計算するときには、まず n - 1 の階乗を計算して、それに n を掛ければいいのです。

ただし、自分自身を呼ぶときに無限に呼び出しては答えが戻ってこないので、どこかで再帰を止める必要があります。これを行っているのが 3 行目から 5 行目の if 文です。n が 0 の時は、0! = 1 ですから 1 を返すようにして、再帰をストップしています。

これと同じことをハノイの塔でも行えばいいわけですね。もう一度ハノイの塔の解法を思い出してみてください。

n - 1 枚を移動させることができたとき、n 枚を移動させるには次の手順で行う

  1. n - 1 枚を目的の塔以外の塔に移動させる
  2. 円盤 n を目的の塔に移動させる
  3. 退避させておいた n - 1 枚を円盤 n の上に移動させる

ハノイの塔を解く関数を Hanoi クラスに定義しましょう。関数の定義を下に示しました。

   public List solve(List towers, int n, int start, int end, int via);

towers はハノイの塔、n が枚数、start が現在の塔の番号、end が目的の塔、via が経由する塔だとします。すると上述した解法は次のようにすることができます。

  1. solve(towers, n -1, start, via, end) // n - 1 枚を start から via に移動させる
  2. movePile(towers, start, end); // n を start から end に移動させる
  3. solve(towers, n -1, start, via, end) //n - 1 枚を via から end に移動させる

忘れていけないのは再帰を止める条件を入れることです。n が 1 になれば、単純に移動するだけなので、再帰をストップさせることができます。このようにしてハノイの塔をアプリケーションで解くことができます。

それではソースを見ていきましょう。

 1:    // ハノイの塔を解く
 2:    private List solve(List towers, int n, int start, int end, int via){
 3:        if(n > 0){
 4:            // n-1 枚を経由塔に移動
 5:            towers = solve(towers, n-1, start, via, end);
 6:            
 7:            // n 枚目を目的の塔に移動
 8:            movePile(towers, start, end);
 9:            System.out.println("Move " + start + " to " + end);
10:            printTowers(towers);
11:
12:            try{
13:                Thread.sleep(500);
14:            }catch(InterruptedException e){}
15:
16:            // n-1 枚を経由塔から目的の塔に移動
17:            towers = solve(towers, n - 1, via, end, start);
18:        }
19:        return towers;
20:    }

3 行目の if 文は再帰を行うかどうかを判定しています。n が 0 ならばなにもしないで return します。n が 0 以上であれば、移動を行います。5 行目で n - 1 枚を経由塔に移動させています。via と end が入れ替わっているのにご注意ください。

8 行目で n 枚目の円盤を目的の塔に移動させます。移動させたら表示しましょう (10 行目)。この後スリープしているのは、こうしないとあまりにも速く表示が変化してしまうので、ゆっくりにさせるために入れてあります。特に処理の流れには関係ないので塔を表示しなければ入れなくてもかまいません。

そして、最後に n - 1 枚を目的の塔に移動させます (17 行目)。これでおしまいです。結構、あっけないと思いませんか。

 

 
 

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

(Jan. 2001)

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