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

引き続き、Rogueを書いていきましょう。Javaの文法とオブジェクト指向の基礎は知ってるという程度の人に教えていくための練習問題です。

今回は、キャラクタの座標を保存するcharmapをキャラクタが動作したら自動的に更新するようにします。そして、自分が動作したら、モンスターも動作させるというものも作りましょう。

キャラクタのマップ

abstract class Character{
   private int x = -1, y = -1;
   ...
   boolean move(int x, int y){  // booleanに変更
      if(dungeon.movable(x, y)){
         Character c = dungeon.getCharacter(x, y);
         if(c == null){
            System.out.println(getName() + "は (" + x + ", " + y + ")に移動した");
            dungeon.updateMap(this, x, y);
            this.x = x;
            this.y = y;
         }else if(findEnemy(c)){
            attack(c);
         }
         return true;
      }else{
         return false;
      }
   }
   abstract boolean findEnemy(Character c);
}

abstract class Monster extends Character{
   ...
   boolean findEnemy(Character c){
      return c instanceof Player;
   }
}

class Player extends Character{
   ...
   boolean findEnemy(Character c){
      return c instanceof Monster;
   }
}

class Dungeon{
   ...
   void updateMap(Character c, int x, int y){
      if(c != null && c.getX() >= 0){
         charmap[c.getY()][c.getX()] = null;
      }
      charmap[y][x] = c;
   }
   void deleteMonster(Character m){
      updateMap(null, m.getX(), m.getY());
      monsters.remove(m);
   }
}

ここで、変更があったのは、updateMap()です。move()によって動作したときにこれによってDungeonのマップを更新してしまいます。この更新とは「前にいた座標にはnullを入れて、新しい座標にキャラクタを代入する」というものです。

初期配置もmove()で行っておりますが「前にいた座標」というのが存在しません。仕方ないので、xやyの初期値を-1にして、そのときは「前にいた座標にnullを入れる」処理をしないことにしています。

さらに、モンスターが死んだときも処理をせねばなりませんが、そのときにはnullを第一引数に入れるだけで終わりにしています。これはdeleteMonster()から呼び出しています。

また、ついでに、モンスター同士の攻撃が起こらないように、findEnemy()というメソッドを追加しています。味方の方向に動作しようとしたときには何もしないということにしています。

ランダム動作

モンスターの動作は、とりあえず「ランダム」ということにします。

Monsterクラスに次のメソッドを追加します。このメソッドが実行されると、(動作できるなら)ランダムに動作します。ターンが実行されるとこのupdate()が起動するわけですね。

void update(){
   OUT:for(int counter = 0; counter < 10; counter++){
      int random = (int)(Math.random() * 4);
      switch(random){
      case 0:
         if(up()){
            break OUT;
         }
         break;
      case 1:
         if(down()){
            break OUT;
         }
         break;
      case 2:
         if(left()){
            break OUT;
         }
         break;
      case 3:
         if(right()){
            break OUT;
         }
         break;
      }
   }
   rest();
}

また、ついでにrest()というメソッドを追加します。これは、ターンが実行されると休憩してHPを回復させるものです。これはCharacterに定義します。本当は最大HPとかを考慮するべきでしょうが、とりあえずは無視しちゃいます。

abstract class Character{
   ...
   void rest(){
      hit_point++;
   }
}

モンスター全体の動作

敵すべてが動作しないといけません。敵を持っているのはDungeonクラスのmonstersというListなので、このListのすべての要素に対して、update()を実行します。

class Dungeon{
   ...
   void update(){
      for(Iterator it = monsters.iterator(); it.hasNext(); ){
         ((Monster)(it.next())).update();
      }
   }
}

これにより、Dungeonに対してupdate()を実行するとすべての敵が一動作することになります。

プレーヤが動作したら敵全部が動作

プレーヤがひとつの動作を行ったら敵が動作するようにしなければなりません。プレーヤの動作はmove()で定義されていますが、これはCharacterクラスのものをそのまま継承しています。オーバーライドしてしまいましょう。

class Player extends Character{
   ...
   boolean move(int x, int y){
      boolean b = super.move(x, y);
      if(b){
         rest();
         dungeon.update();
      }
      return b;
   }
}

move()が成功したとき(動いた、あるいは攻撃したとき)に限りupdate()を発動させます。

現在の状況

現在の状況をまとめてみますと、

  • プレーヤを動作させる(たとえば右に動かすright()を実行する)と、モンスターはランダムに何か動作する
  • プレーヤ、モンスターとも動作しようとした方向に敵(プレーヤにとってはモンスター、モンスターにとってはプレーヤー)がいたら攻撃する
  • ある程度の攻撃を受けるとモンスターは死ぬ

というところまではできています。

敵は最初に配置せねばなりませんし、プレーヤが死ぬ判定もありませんが、一応はなんとなくターン制のあるゲームっぽくなっています。

現在までのソースを以下にまとめておきます。暇でしょうがない人は展開してコンパイルし、java Character で実行してください。
なお、新しいJDKではコンパイル時に警告が出ますが、ジェネリックを使わないで書いたせいですので無視してかまいません。

download

次回以降の予定

結構間が開いてしまいました。最初に考えた順序と違うかもしれませんが、以下を行いたいと思います。とりあえずキーボードから遊べることを優先したいということです。

  • 次回
    • キーボードからプレーヤを操作
    • 死んだらエンディング
  • その次
    • 広いマップの一部だけを表示
    • 敵の自動生成
  • さらに次
    • ダンジョン自動生成
    • 地下への移動

アイテムやその他はさらにその後ということになりそうです。