|
ハノイの塔を組み立てる |
|||||||
プレイヤーから作ってみよう |
|||||||
それでは、さっそく作っていきましょう。 まずは、Player クラスからと思ったのですが、その前に簡単な PileMovementInfo クラスを片付けてしまいましょう。 PileMovementInfo クラスは単に 2 つの属性と、それを取り出す関数があるだけです。ソースも短いので全部示してみました。
PileMovementInfo のコンストラクタで 2 つの属性、移動元と移動先の塔番号をセットします。移動元を取り出すのが getSource 関数、移動先を取り出すのが getDestination 関数です。 次は Player クラスです。 Player クラスのソースはこちらです Player.java Player クラスの属性は前節で示したように Reader オブジェクトです。ただし、ここでも今までと同じように BufferedReader を使用しました。
コンストラクタは、reader を生成するだけです。 その他の関数に関しては処理の流れに沿って見ていくことにしましょう。ゲームが始まると、まずコールされるのが nextStep 関数です。
ここで行う処理はユーザからの入力処理と円盤が移動できるかどうかのチェックです。 3 行目の while ループは後においておいて、入力処理を行う 6 行目から 12行目をまず説明しましょう。6, 7, 8 行は移動させる円盤を入力させる部分です。6 行目で入力を促すための出力を行います。7 行目で実際の入力を取り込みます。readLine 関数を使用しているので、リターンキーを押すまで入力されないようになります。 8 行目で入力した文字列を数値に変換します。数値への変換にはおなじみの Interger#parseInt 関数を使用しました。もし、入力が数字以外の場合は parseInt 関数で NumberFormatException の例外が発生します。 10, 11, 12 行目で移動先の塔を入力させるのですが、処理的には 6, 7, 8 行目と同じです。 それぞれ、移動元の塔番号が x, 移動先の塔番号が y に代入されます。x から y に移動できるかチェックするのが 14 行目の isMovable 関数です。isMovable 関数は現在の塔の状態を表す towers と、x, y が引数になります。isMovable 関数の戻り値が true の時は移動でき、false の時は移動できません。true の時には、移動元と移動先を用いて PileMovementInfo オブジェクトを生成し、それを nextStep 関数の戻り値とします。 入力された文字が数字以外の場合 (NumberFormatException が発生した場合) と、円盤が移動できない場合 (isMovable 関数の戻り値が false だった場合) には、ユーザに再び入力をしてもらいます。4 行目の while ループはそのためにあるのです。正しい入力があるまで、while ループを抜け出せないようになります。 14 行目でコールされる isMovable 関数を次に見ていきます。
移動のチェックは次に示した 4 つのチェックをクリアしなければなりません。
List オブジェクトの towers から、i 番目の塔の Stack オブジェクトを取り出すには次に示すように get 関数を使用します。
get 関数の戻り値は Object なので、実際に使用するときにはキャストを行い使用するクラスにあわせる必要があります。towers が i 番目の要素をもっていないときには、IndexOutOfBoundsException が発生します。これを利用して 1. の「移動元の塔があるかどうか」をチェックします。 ソースでは 5 行目で get 関数を使用して Stack オブジェクトを取り出します。この時の例外処理が 8 から 11 行目になります。IndexOutOfBounds が発生するときは、移動元の塔がないと考えられるので 10 行目で false を戻り値とします。 2. の「移動元の塔に円盤があるかどうか」は Stack クラスの empty 関数を使用します。この関数は Stack が空の時に true になります。5 行目で empty 関数を使用しており、Stack が空の時すなわち塔に円盤がないときには 6 行目で false で戻るようになります。 3. のチェックは 1. と同じで IndexOutOfBoundsException を利用しています。それが 20 から 23 行です。 最後の「移動する円盤の大きさが移動先の塔の一番上の円盤より小さいかどうか」をチェックします。ところで、移動先に円盤が一枚もなければ、円盤を常に置くことができます。これを利用したのが、17, 18, 19 行目の if ブロックです。このチェックは 4. の亜流といったところでしょうか。 円盤の大きさを比較するには円盤の大きさを取り出す必要があります。13 行目では移動元の塔の一番上の円盤 (移動する円盤)、25 行目では移動先の一番上の円盤の大きさを取り出しています。この取出しには前節で説明した Stack の peek 関数を利用します。 それぞれの大きさは Integer オブジェクトなので、intValue 関数を用いて int にしてから、比較を行います (28 行目)。移動元の円盤が小さければ 29 行目、大きければ 31 行目になります。
|
Hanoi クラスの実装 | ||||||||
最後に残ったのが Hanoi クラスです。Player クラスと同じように属性から見ていきます。
一番先に定義を行っている NUMBER_OF_TOWERS, START, END が定数になっています。それぞれ、塔の本数、ハノイの塔を開始したときに円盤が刺さっている塔の番号、最終的に移動させる塔の番号を示しています。定数なので、static で final な変数にしてあります。 次に普通の属性として size と player があります。size はハノイの塔の最大の円盤の大きさを示しています。player は言わずもがなですね。 コンストラクタでは、通常の 2 つの属性の初期化を行っています。最大の円盤の大きさはコンストラクタの引数として与えようにしました。これは、ゲームをするときコマンドラインから大きさを指定できるようにしようと考えているからです。したがって、main 関数は次のようになっています。
コマンドラインで指定された文字列は args に入っています。そこで、第一引数である args[0] を Hanoi クラスのコンストラクタに与えることで、遊ぶときにハノイの塔の大きさを指定できるようになっているわけです。main 関数では Hanoi オブジェクトを生成した後に、startGame 関数をコールしてゲームを開始します。 さて、その startGame 関数ですが、次のようになっています。
3 行目で makeTowers 関数をコールして towers を初期化します。ここから後は、この towers を使用してハノイの塔を表していきます。生成した塔を 6 行目で printTowers をコールすることで表示します。表示の処理は後述します。 さて、実際のゲームに入りましょう。9 行目から始まるループがゲームの本体になります。ループに入るとまず player の nextStep 関数をコールします。戻り値の pileMovementInfo には円盤の移動の情報が入っていることは前述したとおりです。すでに移動のチェックは終わっているので、12 行目で円盤の移動を行います。movePile 関数の第 2, 3 引数が移動元と移動先の塔番号ですが、int 型なので、PileMovementInfo クラスの getSource 関数と getDestination 関数を使用して値を取りだします。 移動をしたら、その結果を表示します (14 行目)。ループの最後の行である 15 行目で、ゲームが終了したかどうかを checkCompletion 関数でチェックします。checkCompletion の戻り値は boolean で完成したら true になります。ですから、戻り値が false の間はループを回るように checkCompletion の前にエクスクラメーションマークをつけています。 17 行目にくるときは、移動が完了しているので、それに応じた文字列を表示しました。 ループの中をまとめると
を繰りかえすわけです。図 3-8 で示したシーケンス図がちゃんと実装できているのが分かると思います。 さて、その他の private 関数も説明を加えていきましょう。makeTowers 関数を次に示します。
ハノイの塔の表し方は前節の図 3-13 で示したように、List オブジェクトに 3 個の Stack オブジェクトが入った形になります。それぞれの Stack オブジェクトが一本の塔を表し、Stack に入れられている要素が円盤に相当します。Stack に入れられる要素は Integer 型で円盤の大きさになります。 したがって、makeTowers 関数で一番はじめに行うことは、List オブジェクトの生成です (3 行目)。List はインタフェースなので、直接生成することはできません。この章でも今までと同じように ArrayList を利用します。生成した towers に、3 個のスタックオブジェクトを生成して、追加するのが 5 から 7 行目のループです。最後に生成した Stack オブジェクトの内、はじめに円盤がおかれている塔には円盤の最大の大きさを示した size の枚数だけ Integer オブジェクトを push します。 円盤の移動は Stack クラスを利用したためとても簡単に行うことができます。
移動元の塔から一番上にささった円盤を取りだしているのが 3, 4 行目です。まず towers から移動元の塔を取り出すのに get 関数を使用します。get 関数の戻り値は Object オブジェクトなので、これを Stack オブジェクトにキャストしています。そして、pop 関数を用いて、一番上の Integer オブジェクトを取りだします。Stack オブジェクトですから、pop 関数を使用して簡単に一番上のものを取り出せます。 その次に移動先の塔に円盤をさす処理です (5, 6 行目)。同じように towers から get 関数を使用して移動先の塔を取りだします。そして、その塔に push 関数を使用して円盤を積んでいきます。 さて、残ったものは printTowers 関数と checkCompletion 関数ですが、printTowers 関数はちょっと後回しにして、先に checkCompletion 関数を見ていきます。
引数の goal が目的の塔の番号ですから、3 行目で get 関数を使用して目的の塔を取りだすことができます。その塔にささっている円盤の枚数は size 関数で調べることができます。塔にささっている枚数と size が同じであれば全部移動したことになります。
|
ハノイの塔を表示する | |||||||||||||||||||
ハノイの塔を表示するわけですが、完成予想図はこんな感じです。
DOS 窓やシェルでこのように描画するにはちょっと工夫がいりました。塔の高さは size と同じにします。円盤は x の大きさであれば 2 ・x - 1 個のアスタリスクで表示するようにしました。 はじめからすべてを表示するのはたいへんなので、まず 1 つの塔に円盤がささっていない状態を表示してみましょう。台座の大きさは、2・size - 1 になります。真中に塔があるので、その両側には size - 1 個の空白を表示する必要があります。 また、表示は上の方から順に行います。塔の高さは size ですから、台座も含めると size + 1 行の表示を上の方から表示していきます。
空白を size - 1 表示し、縦棒、再び空白を size - 1 表示することを繰り返せばいいはずですね。
ソースの中で 4 行目や 8 行目の出力する部分の関数が println ではなくて print であることに注意してください。println 関数と print 関数の違いは、println 関数は出力した時に改行されるのに対して print 関数は改行が行われないことです。したがって、size - 1 個の空白、縦棒、再び size - 1 個の空白を出力するときは print 関数を使用し、一行分を表示し終えたときに println を行って改行するようにしています (14 行目および 21 行目)。println 関数に引数がない場合は単に改行だけを行います。 次はこれに円盤がささっている場合です。塔の状態は Stack オブジェクトの tower で表しているとすれば、塔にささっている円盤の枚数は tower.getSize() で得ることができます。
塔にささっている円盤の大きさは tower.pop() で取り出せるので、それを diameter とすると、表示するアスタリスクの個数は 2・diameter - 1 になります。 上から size - tower.size() 行は円盤がないので、円盤がささっていないときと同じです。その後の行は (size - 1) - (diameter - 1) = size - diameter 個の空白を表示して、2・diameter - 1 個のアスタリスクを表示します。そして、再び size - diameter 個の空白を表示します。 次の行を出力するときは、前の行で tower.pop() をしているため、その高さにある円盤は再び tower.pop() をすれば得ることができます。
1 行目の for ループで変数 i が size から始まって、0 で終わるのには理由があります。こうすると、i が塔の現在出力を行っている高さを表すことができるからです。2 行目で tower.size() で円盤の枚数を取り出していますが、円盤の枚数は円盤がつまれている高さと考えることもできます。したがって、今出力を行っているところが、円盤があるかどうかは i と tower.size() を比べるだけで済むわけです。 ここを 2 回の for ループを用いて、
というように記述しなかったのは、塔が 3 本ありそれぞれ円盤のつまれている高さが異なるため size - tower.size() が塔によって異なってしまうからです。 2 行目の if の条件が分かってしまえば、後はもう説明したとおりです。円盤があるときは size - diameter 個の空白を表示して、2・diameter - 1 個のアスタリスク、そして、再び size - diameter 個の空白を表示します。円盤がないときはsize - 1 個の空白、縦棒、再び size - 1 個の空白を出力します。 最後に台座を出力すればおしまいです。 最後に塔を 3 本にしてみましょう。ここまでくれば後は簡単で、1 本の塔の出力を 3 回繰り返せばいいだけです。
2 行目から始まるループが 3 本の塔に対応するものです。3 行目で、j 番目の塔を取りだしており、j 番目の塔の高さと現在出力している高さの比較を行って、出力する内容を決めているのは今までと同じです。 ところで、Stack オブジェクトの tower の中身はこの関数の後はどうなっているでしょうか。6 行目で pop 関数を使用しているので、tower の中身はどんどんなくなっていってしまい、最後には空っぽになってしまいます。これでは、printTowers 関数から戻ったときに tower が空になってしまうので、困ってしまいますね。 こんなときにはコピーを取っておきましょう。コピーを取るようにするにはクラスに java.lang.Cloneable インタフェースをインプリメントする必要があります。すべてのクラスのスーパークラスである java.lang.Object には clone という関数があり、この関数を使用することでコピーを取ることができます。ただし、Coneable インタフェースをインプリメントしておかないと clone 関数をコールしたときにjava.lang.CloneNotSupportException が発生します。 ライブラリにあるクラスはかなりの割合で Cloneable をインプリメントしています。Stack クラスもスーパークラスの Vector クラスが Cloneable をインプリメントしているので、コピーをすることができます。そこで実際の表示を行う前に Stack オブジェクトをコピーしておきましょう。
これで printTowers 関数の全容が見渡せました。しかし、ちょっと分かりにくいと思いませんか。ループの 3 重と if 文がからみあって、かなりプログラムの可読性が悪くなっています。これを直していきましょう。 まず、ソースを意味のある部分に分けてみましょう。
緑の部分は一枚の円盤を表示している部分、オレンジの部分は塔の表示、青の部分は台座の表示になっています。それぞれの部分は他の部分とそれほど関係があるわけではないのでこの部分を関数として独立させてしまいましょう。緑の部分を printPile、オレンジを printPole、青を printBase という関数にしてみました。また、おまけとして台座の下に塔の番号も出力するようにし、これを printPoleNumber 関数としました。 こうして完成した printTowers 関数は次のようになります。
ここで行ったようにプログラムの機能を変化させずに分かりやすいプログラムに直していくことをリファクタリング[1]とよびます。一度書いたプログラムはそこで終わりにしないで、ぜひもう一度見直してリファクタリングすることをおすすめします。 可読性の高いプログラムはそれだけバグの発見もしやすくなりますし、作者以外の人がプログラムを触るとしても理解するのが容易になります。なるべく、分かりやすいプログラムを書くようにし、定期的にリファクタリングも行うようにしましょう。 参考文献
|
●作成したソースファイルとコンパイルを行ったクラスファイルはここでダウンロードできます hanoi1.zip (Jan. 2001) |
|