JavaでRogueを書いてもらう(2)

さて、Rogueの続きを書いていきましょう。ソースがまだ、ダウンロードできないのですが私がちゃんと用意していないだけです。

今日は、次回の分量を少なくするために、簡単なところを加えておきましょう。

現状

現状では、モンスターやプレーヤが攻撃し合うこともできますし、それらのキャラクタがどの位置にいるのか、表示することもできます。もちろん、GUIなどがあるわけもなく、単純な基本クラスのみです。

攻撃と移動の融合

現在の移動メソッドは次のようになっています。

class Character{
   ...
   void move(int x, int y){
      this.x = x;
      this.y = y;
   }
}

これを変更して、移動先に敵(他のキャラクタ)がいたら攻撃を行い、いなければ実際に移動するようにしてみましょう。

class Character{
   ...
   Dungeon dungeon;
   void move(int x, int y){
      Character c = dungeon.getCharacter(x, y);
      if(c != null){
         this.x = x;
         this.y = y;
      }else{
         attack(c);
      }
   }
}

class Dungeon{
   private List monsters = new ArrayList();
   private Character[][] charmap;
   ...
   Character getCharacter(int x, int y){
      return charmap[y][x];
   } 
}

これによって、attack()はprivateにしてしまいましょう。(つまり、直接にattack()を呼び出すことはしないで、move()の結果、その移動先に敵がいたら自動的にattack()してくれるようにします。)このように、外部から直接呼び出せるインターフェース(メソッド)を少なくします。

壁に移動? そりゃあ無理

まず、もう少しメソッドを増やしておきましょう。実際にキーボードなりから発する命令は「左に移動せよ」などですから、それらを直接メソッドにします。(move()を直接呼び出すのも、これによって原則的に禁止します。)

class Character{
   ...
   public void left(){
      move(this.getX()-1, this.getY());
   }
   public void right(){
      move(this.getX()+1, this.getY());
   }
   public void up(){
      move(this.getX(), this.getY()-1);
   }
   public void down(){
      move(this.getX(), this.getY()+1);
   }
}

これでleft()は左に敵がいればその敵を攻撃しますし、いなければ左に移動できます。

しかし、左は壁かもしれません。壁に移動できては困ります。そのあたりを実装します。壁チェックはleft()だのup()だので個々にやる必要はなく、move()で行います。同じことは二箇所に書かない。これはソフトウェア開発の常識というものです。

class Character{
   ...
   boolean move(int x, int y){  // booleanに変更
      if(dungeon.movable(x, y)){
         Character c = dungeon.getCharacter(x, y);
         if(c == null){
            this.x = x;
            this.y = y;
         }else{
            attack(c);
         }
         return true;
      }else{
         System.out.println("その方向には動けない");
         return false;
      }
   }
   public boolean left(){
      return move(this.getX()-1, this.getY());
   }
   public boolean right(){
      return move(this.getX()+1, this.getY());
   }
   public boolean up(){
      return move(this.getX(), this.getY()-1);
   }
   public boolean down(){
      return move(this.getX(), this.getY()+1);
   }
}

class Dungeon{
   ...
   boolean movable(int x, int y){
      switch(map[y][x]){
      case WALL:
         return false;
      default:
         return true;
      }
   }

}

この変更で、move()やleft()その他はbooleanを返すことにしました。例えば左へ移動しようとしたが壁でしたという場合にはfalseを返すことで、「その移動が失敗した(だから別の選択肢を選んでくださいね)」というメッセージを呼び出し元(ユーザといってもよいです)に伝えられるからです。

実際に移動できるかどうかは、Characterクラスではなく、Dungeonクラスでないと判別できないわけですから、Dungeonクラスにmoveble()というメソッドを創設し、それをCharacterクラスで呼び出します。

メソッドの実装場所

今回に限りませんが、初心者レベルでも、開発では次のことに気を遣うべきです。

  • 同じことを二箇所に書かない
  • 同じクラスに似たことを集める

前者ですが例えば、

   public boolean left(){
       if(dungeon.moveble(x, y)){
           move(this.getX()-1, this.getY());
           return true;
       }else{
          return false;
       }
   }

とか、left()で動けるかどうかの判定をしない。move()に書いてしまえばleft()、right()その他に重複して書く必要がありませんし、処理が今後複雑になってもmove()さえ変更すればOK。upleft()とか斜め移動を追加したときも気にしなくてよいことになります。

次に、後者です。
例えば、movable()をDungeonでなくCharacterに実装することだってできます。それはしなかったわけですが、次のようになるでしょうか。

class Character{
   ...
   boolean movable(int x, int y){
      char[][] map = dungeon.getMap();
      switch(map[y][x]){
      case Dungeon.WALL:
         return false;
      default:
         return true;
      }
   }

}

class Dungeon{
   static char WALL = '#';
   ...
   char[][] getMap(){
      return map;
   }
}

別に動くといえば動くのですが、「この位置には動けるのか?」ということは、一括してDungeonに書くのが非常に自然です。

もしかしたら、今後Character以外でも、movable()が必要になるかもしれません。その時に個々に実装するのは愚かです。また、WALLの他に今後別の地図要素を導入するかもしれません。その時、Dungeonにその要素を書き加えるのは当然ですが、それに伴ってCharacterのmovable()にまで影響してくるようだとたまりません。一つの関心事項は一つのクラスに集め、他のクラスから例えばWALLとかそういったものにアクセスさせないのが、簡潔なプログラムにつながります。

次回予告

次回は、ついに、プレーヤを動かすと、敵が自動的に動くという仕掛けを作りたいと思います。