Skip to content

Java講座: オブジェクト指向構文(基本)演習回

今回の目的: 実際に手を動かしてクラスを実装することでJavaクラスとインスタンス化について理解を深めよう!

まず前回git cloneしてもらったプログラムにアップデートが入ったので反映させてください。

  1. cloneしたディレクトリに移動
  2. ターミナルで以下のコマンドを入力
bash
git pull origin main

コンフリクトが起こったりしてうまくいかない場合は教えてください。 主な変更点はダメージ調整と「すばやさ」ステータスの追加です。

「HP回復アイテム」を管理するクラスの設計

ポケモンには「攻撃」以外にも「アイテム(どうぐ)」を使って回復やポケモンの捕獲などを行うことができましたね。今回はまずきずぐすりなどの「HP回復アイテム」を管理するクラスを設計・実装してみましょう!

クラスを設計する際には、「そのオブジェクトがどんなパラメータと機能を持っているか」を考えることが重要でしたね。では、回復アイテムにはどんなパラメータ・機能があるでしょうか?

  • パラメータ
    • 名前
    • 回復量
    • 個数
  • 機能
    • 使用する(回復する)

ざっとこんな感じになりそうな気がします。ではこれをプログラムとして実装してしまえばクラスが作れそうですね。

では、これをHealItemクラスとして実装してみましょう。

フィールドの定義

まずはフィールド定義から始めましょう。フィールドは変数で定義するんでしたね。

java
public class HealItem {
    private String name;
    private int amount; // 回復量
    private int count; // 個数
}

こんな感じで書けばフィールド定義は大丈夫そうですね。

コンストラクタの定義

フィールドが決まったので、コンストラクタも定義してしまいましょう。「コンストラクタってなんだっけ?」となってしまった人は、前回の資料を見ましょう。

java
public HealItem(String name, int amount, int count) {
    this.name = name;
    this.amount = amount;
    this.count = count;
}

メソッドの定義

つぎに機能を定義していきましょう。機能はメソッドを使って定義するわけでしたが、今回作成する「使用する(回復する)」メソッドを書くのは少しばかり面倒です。ステップを踏んで説明していきます。

まず「使用する」という処理を実装するにはこのようにすれば良さそうです。

java
public boolean use() {
    if (count <= 0) {
        System.out.println("そのアイテムはもう使いきっている!");
        return false;
    }
    count--;
    return true;
}

このメソッドがboolean値を返すのは、「正しくアイテムを使用できたかどうか」をわかりやすくするためです。実際に、アイテムをもう使い切っている(countが0以下)場合にはもうアイテムを使用できないためfalseを返すような処理になっています。このように実装することで、「アイテムを使えなかったのにそのまま相手のターンになってしまった」というような状況を防いでいます。

しかし、このuseメソッドには問題があります。それは、回復する処理をせずに個数だけ減らして終わってしまっている点です。これは回復アイテムを使用するメソッドなのですから、ポケモンのHPを回復する処理が必要ですね。この問題点を解決するために、まずメソッドの引数にPokemon型の変数を受け取るようにしましょう。

java
public boolean use(Pokemon pokemon) {
    if (count <= 0) {
        System.out.println("そのアイテムはもう使いきっている!");
        return false;
    }

    /* なにかしらのポケモンのHPを回復する処理 */

    count--;
    return true;
}

ここでまたまた問題が発生します。PokemonクラスではHPがカプセル化されているため、HealItem内ではポケモンのHPを直接操作することができません。(実際にはできなくもありません。気になった人は「セッター」で検索してみてね!)

ポケモンのHPを操作できるのはPokemonクラス内だけなのですから、Pokemonクラスに「HPを回復する」という処理を行うメソッドを書いてあげて、HealItemクラスでそのメソッドを呼び出してあげればうまくいきそうです。

では、実際に Pokemonクラス内に HPを回復してくれるメソッド(heal())を書いてみましょう。

java
public void heal(int amount) {
    hp = hp + amount;
}

このメソッドの引数amountは回復量を表します。なので、HealItemクラスのuseメソッド内でpokemon.heal(amount)みたいに書けば正しく回復を行う処理が書けそうですね。

オブジェクト指向とは違った内容の話になりますが、このhealメソッドはこのままだとバグを引き起こします。気づいた人はそのバグを修正する方法も考えて実装してみてください。

ともかく、これでhealメソッドを通してHPの回復ができるようになりました。さっそくHealItemクラスのuseメソッド内でこれを呼び出すようにしましょう。

java
public boolean use(Pokemon pokemon) {
    if (count <= 0) {
        System.out.println("そのアイテムはもう使いきっている!");
        return false;
    }

    pokemon.heal(amount);

    count--;
    return true;
}

実はこのメソッドもこのままではバグを引き起こす可能性があります。原因はhealメソッドのバグと同じなので考えて修正できたらいいですね。

これでHealItemクラスの実装は完了です!やったね!でもまだこれで終わりではありません。

Gameクラスの修正

このポケモンゲームを実際に動かしている進行役はGameクラスです。そのため、作成した回復アイテムを実際にゲームで使用できるようにするにはGameクラスも修正する必要があります。

ですがその前に、従来のコードに少し使いづらいものがあったため修正させてください。すみません(泣)
まずGameクラスのvalidateSelectionメソッドを以下のように書き換えてください。

java
private static boolean validateSelection(String selection, List<?> list) {
    if (selection.matches("\\d+")) {
        int idx = Integer.parseInt(selection);
        if (0 < idx && idx <= list.size()) {
            return true;
        } else {
            return false;
        }
    }
    return false;
}

その後、handleMyBattleメソッド内のwhile文の中身を以下のように書き換えてください。

java
while (true) {
    selection = getInput();
    if (!validateSelection(selection, myPoke.getSkills())) {
        System.out.println("入力されたわざは存在しません。");
        continue;
    }
    
    selectedSkill = myPoke.getSkills().get(Integer.parseInt(selection) - 1);
    if (selectedSkill.getPP() <= 0) {
        System.out.println("そのわざのPPは0です!");
        continue;
    }
    
    break;
}

HealItemインスタンスの生成

クラスは設計図のため、実際に使用するためにはインスタンス化をする必要がありましたね。 インスタンス化はnew演算子を用いて行います。インスタンス化のときに必要な引数はコンストラクタを見ればわかりますね。今回必要な引数はname, amount, countの3つです。

例えば、こんな感じで書けば「きずぐすり」をインスタンス化できます。

java
HealItem kizugusuri = new HealItem("きずぐすり", 20, 5);

せっかくだし回復アイテムは複数個持てるようにListで管理してみましょう。例えば、以下のように書けば回復アイテムのリストを作成することができます。

java
private static List<HealItem> healItems = List.of(
            new HealItem("きずぐすり", 20, 5),
            new HealItem("いいきずぐすり", 60, 3),
            new HealItem("すごいきずぐすり", 120, 1));

こうすることで、例えばhealItem.get(0).use()とか書けばきずぐすりを使えるようになると理解できたらOKです。これでインスタンス化は完了です!

handleMyTurnメソッドの修正

現在のhandleMyTurnメソッドは問答無用で攻撃フェイズに移行するようになっています。これを改良して、攻撃するか回復するかを自分で選べるようにしましょう。waitForEnter()より下の行はそのまま残して大丈夫です。がんば!

「どうすれば良いのかわからない!」って人は、自分のターンになったときに以下のような処理をするようにプログラムを修正してみてください。

  1. まず攻撃と回復のどちらを行うかを標準入力で受け付ける。1なら攻撃、2なら回復
  2. もし攻撃が選ばれたなら攻撃フェイズに移行(handleMyBattle()を呼び出せばOK)、回復が選ばれたなら回復フェイズに移行(回復フェイズの処理は自分で実装する必要があります)

標準入力は私が作ったgetInputメソッドを使って受け取ってください。ちょっと面倒くさいかもしれませんが、普通にsc.next()みたいな感じで入力を受け付けてしまうとゲームがバグります!また、私のミスでgetInputメソッドの返り値はString型になっているため、入力を数値として扱いたい場合は型変換を行う必要があります。

回復フェイズの処理は、攻撃フェイズの処理を参考にして書くといい感じになるかもしれません。


ここまで全部できてしまった!というすごい人は、例えばPPを回復してくれるどうぐや、モンスターボールなどの実装をしてみると面白いと思います。

答えの例

HealItem.java

java
public class HealItem {
    private String name;
    private int amount; // 回復量
    private int count; // 個数

    public HealItem(String name, int amount, int count) {
        this.name = name;
        this.amount = amount;
        this.count = count;
    }

    public boolean use(Pokemon pokemon) {
        if (pokemon.getHp() <= 0) {
            System.out.println("そのポケモンのHPは0です");
            return false;
        } else if (pokemon.getHp() >= pokemon.getMaxHp()) {
            System.out.println("そのポケモンのHPはすでに満タンだ!");
            return false;
        }

        if (count <= 0) {
            System.out.println("そのアイテムはもう使いきっている!");
            return false;
        }

        pokemon.heal(amount);
        count--;

        return true;
    }

    public String getName() {
        return name;
    }

    public int getAmount() {
        return amount;
    }

    public int getCount() {
        return count;
    }

}

Pokemon.java

java
import java.util.List;

public class Pokemon {
    private String name; // ポケモンの名前
    private int hp; // ポケモンのHP
    private int maxHp; // 最大HP
    private int speed; // すばやさ
    private List<Skill> skills; // ポケモンが持っているわざ(Skillクラスで定義)
    private List<Type> types; // ポケモンのタイプ(ほのお、みず、⋯)

    // コンストラクタ
    public Pokemon(String name, int hp, int speed, List<Skill> skills, List<Type> types) {
        this.name = name;
        this.hp = hp;
        this.maxHp = hp;
        this.speed = speed;
        this.skills = skills;
        this.types = types;
    }

    /**
     * 相手ポケモンに攻撃する
     * 
     * @param opponent 攻撃対象のポケモン
     * @param mySkill  使用するわざ
     * @return 与えるダメージ
     */
    public int attack(Pokemon opponent, Skill mySkill) {
        // PPを一つ減らす
        if (mySkill.getPP() > 0) {
            mySkill.setPP(mySkill.getPP() - 1);
        }

        // getDamageそのままでは威力が高すぎるため調整
        int baseDamage = mySkill.getDamage() / 2;

        // 相手のタイプから最終的なダメージ倍率を取得
        double multiplier = mySkill.getTypeMultiplier(opponent.getTypes());

        // 乱数をかける(0.85~1.0)
        double random = 0.85 + (Math.random() * 0.15);

        // ダメージに倍率を掛けて、整数(int)に変換する
        int finalDamage = (int) (baseDamage * multiplier * random);

        EffectState state = mySkill.getEffectState(opponent.getTypes());
        switch (state) {
            case EFFECTIVE:
                System.out.println("こうかは ばつぐんだ!");
                break;
            case NOTVERYEFFECTIVE:
                System.out.println("こうかは いまひとつのようだ……");
                break;
            case NOEFFECT:
                System.out.println("こうかがないみたいだ……");
                break;
            default:
                // NORMAL(普通)のときは何も表示しない
                break;
        }

        return finalDamage;
    }

    /**
     * 相手の攻撃のダメージに応じてHPを減らす
     * 
     * @param damage 相手から受けるダメージ
     */
    public void takeDamage(int damage) {
        hp = Math.max(hp - damage, 0);
    }

    /**
     * HPを回復する
     * 
     * @param amount 回復量
     */
    public void heal(int amount) {
        hp = Math.min(hp + amount, maxHp);
    }

    /* ゲッター */
    public String getName() {
        return name;
    }

    public int getHp() {
        return hp;
    }

    public int getMaxHp() {
        return maxHp;
    }

    public int getSpeed() {
        return speed;
    }

    public List<Skill> getSkills() {
        return skills;
    }

    public List<Type> getTypes() {
        return types;
    }
}

Game.java

java
import java.util.List;
import java.util.Random;
import java.util.Scanner;

public final class Game {
    private static Scanner sc = new Scanner(System.in);
    private static Random rand = new Random();
    // 現在のゲームの状況を管理するステート(スタート/自分のターン/相手のターン/ゲーム終了)
    private static GameState state = GameState.START;

    // ゲーム終了を表すフラグ
    private static boolean endFlag = false;
    // 全ポケモンのリストを取得
    private static List<Pokemon> allPokemonList = PokemonPool.getPokemonList();
    // 全ポケモンからランダムに自分と相手のポケモンを取得
    private static Pokemon myPoke = allPokemonList.get(rand.nextInt(allPokemonList.size()));
    private static Pokemon enemyPoke = allPokemonList.get(rand.nextInt(allPokemonList.size()));

    private static List<HealItem> healItems = List.of(
            new HealItem("きずぐすり", 20, 5),
            new HealItem("いいきずぐすり", 60, 3),
            new HealItem("すごいきずぐすり", 120, 1));

    private Game() {
    }

    public static void start() {

        while (true) {
            switch (state) {
                case START:
                    handleStartPhase();
                    break;
                case MYTURN:
                    handleMyTurn();
                    break;
                case ENEMYTURN:
                    handleEnemyTurn();
                    break;
                case END:
                    handleEndPhase();
                    break;
            }

            if (endFlag) {
                break;
            }
        }
    }

    // ゲームスタート時の処理
    private static void handleStartPhase() {
        printStartPage();
        waitForEnter();
        System.out.println("========== バトル開始! ==========");

        if (myPoke.getSpeed() >= enemyPoke.getSpeed()) {
            System.out.println("あなたの先制攻撃!");
            state = GameState.MYTURN;
        } else {
            System.out.println("あいての先制攻撃!");
            state = GameState.ENEMYTURN;
        }
        waitForEnter();
    }

    // 自分ターンの処理
    private static void handleMyTurn() {
        System.out.println("========== あなたのターン ==========");

        while (true) {
            System.out.println("どうする?");
            System.out.print("1. たたかう  2. どうぐ: ");

            String selection = getInput();
            if (selection.equals("1")) {
                handleMyBattle();
                break;
            } else if (selection.equals("2")) {
                handleHeal();
                break;
            } else {
                System.out.println("不正な値です");
            }
        }

        waitForEnter();
        printMyStatus();
        printEnemyStatus();
        waitForEnter();

        if (isGameOver()) {
            state = GameState.END;
            return;
        }

        state = GameState.ENEMYTURN;
    }

    // 相手ターンの処理
    private static void handleEnemyTurn() {
        System.out.println("========== あいてのターン ==========");
        handleEnemyBattle();
        waitForEnter();
        printMyStatus();
        printEnemyStatus();
        waitForEnter();

        if (isGameOver()) {
            state = GameState.END;
            return;
        }

        state = GameState.MYTURN;
    }

    // ゲーム終了時の処理
    private static void handleEndPhase() {
        // どっちが勝ったか判定
        if (myPoke.getHp() <= 0) {
            System.out.println("あなたの負けです(T_T)");
        } else {
            System.out.println("あなたの勝ちです!!!");
        }

        endFlag = true;
    }

    private static void printStartPage() {
        System.out.println("========== ゲームスタート!!! ==========");
        printMyStatus();
        printAllMySkills();
        printEnemyStatus();
    }

    private static void printMyStatus() {
        System.out.println("あなたのポケモン: " + myPoke.getName() + " (" + getPokemonTypeString(myPoke) + ")");
        System.out.println("あなたのHP: " + myPoke.getHp());
        System.out.println();
    }

    private static void printEnemyStatus() {
        System.out.println("あいてのポケモン: " + enemyPoke.getName() + " (" + getPokemonTypeString(enemyPoke) + ")");
        System.out.println("あいてのHP: " + enemyPoke.getHp());
    }

    // 自分のわざ名・威力・PPをすべて表示する
    private static void printAllMySkills() {
        List<Skill> skills = myPoke.getSkills();
        for (int i = 0; i < skills.size(); i++) {
            System.out.println((i + 1) + ". わざ名: " + skills.get(i).getName() + " ["
                    + skills.get(i).getType().getJapaneseName() + "]");
            System.out.println("   威力: " + skills.get(i).getDamage());
            System.out.println("   PP: " + skills.get(i).getPP());
        }
        System.out.println();
    }

    // 回復アイテムを全て表示する
    private static void printAllHealItem() {
        for (int i = 0; i < healItems.size(); i++) {
            System.out.println((i + 1) + ". どうぐ: " + healItems.get(i).getName());
            System.out.println("   回復量: " + healItems.get(i).getAmount());
            System.out.println("   残り個数: " + healItems.get(i).getCount());
        }
        System.out.println();
    }

    // 自分ターンのバトルフェイズの処理
    private static void handleMyBattle() {
        printAllMySkills();
        System.out.print("使用するわざ(番号):");
        String selection = "";
        Skill selectedSkill = null;

        while (true) {
            selection = getInput();

            if (!validateSelection(selection, myPoke.getSkills())) {
                System.out.println("入力されたわざは存在しません。");
                continue;
            }

            selectedSkill = myPoke.getSkills().get(Integer.parseInt(selection) - 1);

            if (selectedSkill.getPP() <= 0) {
                System.out.println("そのわざのPPは0です!");
                continue;
            }

            break;
        }

        System.out.println(myPoke.getName() + " の " + selectedSkill.getName() + "!");

        int damage = myPoke.attack(enemyPoke, selectedSkill);
        enemyPoke.takeDamage(damage);

        System.out.println(enemyPoke.getName() + " に " + damage + " のダメージ!");
    }

    private static void handleHeal() {
        System.out.println("回復アイテム一覧");
        printAllHealItem();
        String selection = "";
        HealItem selectedItem = null;

        while (true) {
            System.out.print("使用するアイテム(番号):");
            selection = getInput();

            if (!validateSelection(selection, healItems)) {
                System.out.println("入力されたアイテムは存在しません");
                continue;
            }

            selectedItem = healItems.get(Integer.parseInt(selection) - 1);

            if (selectedItem.use(myPoke)) {
                System.out.println(myPoke.getName() + " は " + selectedItem.getAmount() + " だけ回復した!");
                break;
            }
        }
    }

    // 相手ターンのバトルフェイズの処理
    private static void handleEnemyBattle() {
        // あいては、わざをランダムに選択
        Skill selectedSkill = null;

        while (true) {
            selectedSkill = enemyPoke.getSkills().get(rand.nextInt(enemyPoke.getSkills().size()));
            if (selectedSkill.getPP() > 0) {
                break;
            }
        }

        System.out.println(enemyPoke.getName() + " の " + selectedSkill.getName() + "!");

        int damage = enemyPoke.attack(myPoke, selectedSkill);
        myPoke.takeDamage(damage);

        System.out.println(myPoke.getName() + " に " + damage + " のダメージ!");

        System.out.println();

    }

    private static void waitForEnter() {
        System.out.println("エンターキーを押して先へ進んでください...");
        sc.nextLine();
    }

    // 標準入力の値を取得する
    private static String getInput() {
        String in = sc.nextLine().strip();
        return in;
    }

    // 不正な入力ではないか判別する(受け付ける入力は番号のみ)
    private static boolean validateSelection(String selection, List<?> list) {
        if (selection.matches("\\d+")) {

            int idx = Integer.parseInt(selection);

            if (0 < idx && idx <= list.size()) {
                return true;
            } else {
                return false;
            }
        }
        return false;
    }

    private static String getPokemonTypeString(Pokemon pokemon) {
        List<Type> types = pokemon.getTypes();
        if (types.size() == 1) {
            return types.get(0).getJapaneseName();
        } else {
            // 複合タイプの場合は「ほのお / ひこう」のようにスラッシュで繋ぐ
            return types.get(0).getJapaneseName() + " / " + types.get(1).getJapaneseName();
        }
    }

    // ゲーム終了条件を満たしているかチェックする
    private static boolean isGameOver() {
        if (myPoke.getHp() <= 0 || enemyPoke.getHp() <= 0) {
            return true;
        }

        return false;
    }
}