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

昨日から、JavaRogueを実装しています。KrogueのようにJavaScriptで実装するならば独自の面白みもあるかもしれませんが、なぜJavaで実装するのか。

それには次のような理由があります。

  • 教育目的

以上です。

Rogueとは

Rogueというゲームをそもそも知らない方もいらっしゃるかも知れません。ここでRogueの特徴を説明しつくすことはできませんが、そのうち要点を省き、どうでもいいことだけを取り上げると以下のように言えるでしょう。(いや、本当は要点を書きます)

  • ダンジョン探検ゲームである
  • ダンジョンマップは遊ぶたびに自動的に生成される
  • 古典的にはテキストだけで表示されるが、最近はグラフィカルなものもある

よく分からない人は、以下で遊んでみてください。(本来のRogueとは違う面もありますが、雰囲気がたのしめます。)
Krogue
Krogueカラー版

ブラウザ上で遊べなかった人は、とりあえずあきらめて次の機会を待ってください。

教育目的のRogue

私の家族(Aさんとしましょう)にJavaを教えてくれといわれているのですが*1、とりあえず、Rogueを作りながら学んでみるという手段に出ることにしました。私が一人で実装するならばさっさと作ることもできるかと思いますが、コードを書くのは基本的にAさんに任せ、コードを書く指針を私の方で与えることにいたしました。

キャラクタークラス

  • Character クラス
    • 主人公、モンスターなどのスーパークラス
    • HP、経験値などの基本属性を持つ
    • 攻撃メソッドを持ち、他のCharacterを引数にとって攻撃できる
abstract class Character{
   private int hit_point, attack_point, exp, ....;
   private int x, y;
   private char symbol;
   private String name;

   Character(String name, char symbol, int hit_point, ....){
      ....
   }

   // ヒットポイントを取得
   int getHitPoint(){
      ...
   }
   // 攻撃力を取得
   int getAttackPoint(){
      ...
   }
   // 防御力を取得
   int getDeffencePoint(){
      ...
   }
   // 所有経験値を取得
   int getExp(){
      ...
   }
   // 名前を取得
   String getName(){
      ...
   }
   // シンボル(マップ上で現す一文字の名前)
   char getSymbol(){
      ...
   }
   // X座標を返す
   int getX(){
      ...
   }
   // Y座標を返す
   int getY(){
      ...
   }
   // 経験値を増やす
   void addExp(int addition){
      ...
   }
   // cを攻撃する
   void attack(Character c){
      ...
   }
   // attackの攻撃力で攻撃された
   // 返り値がtrueなら死んでしまった時
   boolean damage(int attack){
      ...
   }
   // (x, y)にワープ
   void move(int x, int y){
      this.x = x;
      this.y = y;
   }
}

細かなところまでは書きませんが、例えばattack()は次のように実装いたします。

void attack(Character c){
   if(c.damage(getAttackPoint())){
      addExp(c.getExp());
   }
}
boolean damage(int attack){
   // 敵の攻撃力-自分の防御力 = うけるダメージ
   int damage_point = attack - getDeffencePoint();
   if(damage_point <= 0){ // うけるダメージが0以下のときは強制的に1とする
      damage_point = 1;
   }
   this.hit_point -= damage_point;
   System.out.println(getName() + "は" + damage_point + "のダメージ");
   if(getHitPoint() <= 0){
      System.out.println(getName() + "は死んでしまった");
      return true;
   }
   return false;
}

void addExp(int exp){
   this.exp += exp;
   System.out.println("経験値" + exp + "を取得");
   // あとでレベルあげ処理などを書く
}

しかし、Aさんはこの処理がよく分からないと言っておりました。すなわち、攻撃主体と防御主体のどちらがどちらか分からないということかもしれません。attack()の処理では攻撃側の視点で書かれており、それがどのようなダメージに変換されたかは、防御側の視点であるdamage()に変わってしまっているのが分かりにくいのかもしれません。(私の書き方が悪いのか?)

主人公・モンスタークラス

今日の時点では、Monsterやプレーヤにはほとんど処理を行っていません。単純に、Characterで用いる変数の値を入れているだけです。

  • Monsterクラス
abstract class Monster extends Character{
   Monster(String name, ...){
      super(name, ...),
   }
   ... // 今日の時点では特に処理はなし
}
class Slime extends Monster{
   Slime(){
      super("スライム", 'S', 3, .....); // HPなどの初期化
   }
}
class Bat extends Monster{
   Bat(){
      super("こうもり", 'B', 5, .....); // HPなどの初期化
   }
}
class Player extends Character{
   Player(){
      super("あなた", '@', 10, .....); // HPなどの初期化
   }
}

テストコード

そして、Aさんには、ここまで実装させた時点で、テストで動作させてもらいました。

class Test{
   public static void main(String[] args){
      Player p = new Player();
      Slime s = new Slime();
      Slime s2 = new Slime();
      Bat b = new Bat();
      p.attack(s);
      s.attack(p);
      b.attack(s);
   }
}

そして、このコードで実際に攻撃しあうことを確認してもらいました。

ダンジョン

次に作ったのがダンジョンです。
このダンジョンは、単純にcharの二次元配列で現してもらうことにしています。

class Dungeon{
   private char[][] map;
   private int width, height;
   private static final char PATH = ' ';
   private static final char WALL = '#';
   Dungeon(int width, int height){
      this.width = width;
      this.height = height;
      map = new char[height][width];
      initMap();
   }
   // とりあえずテスト
   // 最外壁だけを壁にしてみる
   void initMap(){
      for(int y = 0; y < getHeight(); y++){
         for(int x = 0; x < getWidth(); x++){
            map[y][x] = PATH;
         }
      }
      for(int i = 0; i < getHeight(); i++){
         map[i][0] = WALL;
         map[i][getWidth()-1] = WALL;
      }
      for(int i = 0; i < getWidth(); i++){
         map[0][i] = WALL;
         map[getHeight()-1][i] = WALL;
      }
   }

   int getWidth(){
      return width;
   }
   int getHeight(){
      return height;
   }
   // デバッグ用マップ表示メソッド
   public void printMap(){
      for(int y = 0; y < getHeight(); y++){
         for(int x = 0; x < getWidth(); x++){
            System.out.print(map[y][x]);
         }
         System.out.println();
      }
   }
}

単純に、上記のようにしてみました。

ダンジョンクラスの拡張

ダンジョンとCharacterが関係を持っていませんので、当面Dungeonの中に、主人公やモンスターをListで持たせることにしました*2

現在は、Characterは、getX()やgetY()によって、存在する座標を返すことができますが、逆に(x, y)が与えられたときにどのモンスターがいるかを知りたいので、charmapという二次元配列を作成いたします。charmap[y][x]の値が(x, y)にいるモンスターや主人公といえます。(nullならば、何もいない。)

import java.util.*;

class Dungeon{
   private char[][] map;
   private int width, height;
   private static final char PATH = ' ';
   private static final char WALL = '#';
   private List monsters = new ArrayList(); // このダンジョンにいるモンスターのリスト
   private Player player = new Player(); // 主人公
   private Character[][] charmap;

   Dungeon(int width, int height){
      this.width = width;
      this.height = height;
      map = new char[height][width];
      initMap();
      charmap = new Character[height][width];
      player.move(2, 2);
      Monster s = new Slime();
      s.move(3, 3);
      monsters.add(s);
      Monster b = new Bat();
      b.move(1, 2);
      monsters.add(b);
      setCharMap(); // テスト
   }
   // 当面のcharmapのセット(後には使わないだろう)
   void setCharMap(){
      charmap[player.getY()][player.getX()] = player;
      for(Iterator it = monsters.iterator(); it.hasNext(); ){
         Character c = (Character)(it.next());
         charmap[c.getY()][c.getX()] = c;
      }
   }

   // 最外壁だけを壁にしてみる
   void initMap(){
      ...
   }

   int getWidth(){
      return width;
   }
   int getHeight(){
      return height;
   }
   // デバッグ用マップ表示メソッド
   public void printMap(){
      for(int y = 0; y < getHeight(); y++){
         for(int x = 0; x < getWidth(); x++){
            Character c = charmap[y][x];
            if(c == null)
               System.out.print(map[y][x]);
            else
               System.out.print(c.getSymbol());
         }
         System.out.println();
      }
   }
}

この時点で、静的な地図であるmapとキャラクターの地図であるcharmapの組合せが完成いたします。(ただし、モンスターや主人公の位置は決めていないので、当面は適当に配置するしかない。また、現状では同じ座標に複数のCharacterがいることもありえる。)

Listであるmonstersの更新

少しここで悩んだのですが、モンスターが新しく現れたり、死んだりしたときにはmonstersを更新せねばなりません。新しく現れる処理はまだしもDungeonクラス内で行うとして、死んだという処理はCharacterの中で行っています。CharacterはDungeonのことを知らないので、死んだときにDungeonの中にあるmonstersの値をいじることはできません。仕方ありませんので次のようにいたしました。

class Dungeon{
   ....
   Player player;
   Dungeon(int width, int height){
      ...
      Monster s = new Slime();
      s.move(3, 3);
      player = new Player();
      player.setDungeon(this);
      addMonster(s);
      Monster b = new Bat();
      b.move(1, 2);
      addMonster(b);
      setCharMap();
   }
   void addMonster(Character m){
      monsters.add(m);
      m.setDungeon(this); // モンスターにDungeonを参照させる
   }
   void deleteMonster(Character m){
      monsters.remove(m);
   }
}

abstract class Character{
   Dungeon dungeon;
   ...
   // ダンジョンを参照させるためにセット
   void setDungeon(Dungeon d){
      dungeon = d;
   }
   boolean damage(int attack){
      int damage_point = attack - getDeffencePoint();
      if(damage_point <= 0){
         damage_point = 1;
      }
      this.hit_point -= damage_point;
      System.out.println(getName() + "は" + damage_point + "のダメージ");
      if(getHitPoint() <= 0){
         System.out.println(getName() + "は死んでしまった");
         if(dungeon != null)
            dungeon.deleteMonster(this); // モンスターリストから取り去ってもらう
         return true;
      }
      return false;
   }
}

すなわち、CharacterクラスにDungeonを参照させ、死んだときにはdeleteMonster()を呼んで自分自身を取り除いてしまうのです。

今週はここまで

とりあえず、今週はここまで(私の監視下で)作ってもらいました。まだ、キャラクタの移動やターン制が全くできていませんが、基本の部分はこんなものではないかと思います。

では、また次にAさんに教えるときにまた書いていきましょう。なんとなくAさんはもううんざりしているような感じもなきにしもあらずですので、次回はないかもしれません。
なお、サンプルコードはあとでダウンロードできるようにしておきたいと思っています。

次回以降の予定

  1. 主人公を動かしたら、モンスターも動く(1)
  2. 主人公を動かしたら、モンスターも動く(2)
  3. ダンジョンを自動生成
  4. GUIの皮をかぶせる
  5. アイテム・武器の追加

*1:正確にいえばJSPStrutsを用いたウェブアプリを頼まれているのですが、まずは基本を知って欲しいということでオブジェクト指向Rogueで学んでもらい、この知識をJSPなどに応用してもらいたいと思っています。

*2:Genericを使っていないのですが、ここではAさんに教える内容を少なくしたかったのです。私が書くならGeneric機能を使います。