【Python】レッスン5-S2:モンスターと戦うバトルゲームを作ろう

一つ前のLessonではモンスター捕獲ゲームを作成しました。
今回はモンスターとのバトルゲームを作成しましょう。
Lesson1:基礎文法編
Lesson2:制御構造編
Lesson3:関数とスコープ編
Lesson4:データ構造編
Lesson5:オブジェクト指向編
・Lesson5-1:クラスの基本を理解しよう
・Lesson5-2:メソッドの基本を理解しよう
・Lesson5-3:カプセル化を理解しよう
・Lesson5-4:プロパティを理解しよう
・Lesson5-5:クラスの継承を理解しよう
・Lesson5-6:メソッドのオーバーライドを理解しよう
・Lesson5-7:静的メソッドを理解しよう
・Lesson5-8:モジュールを使いこなそう
・Lesson5-9:抽象クラスを理解しよう
・Lesson5-10:ミックスインを理解しよう
・Lesson5-11:データクラスを理解しよう
・練習問題5-1:モンスター捕獲ゲームを作ろう
・練習問題5-2:モンスターとのバトルゲームを作ろう ◁今回はココ
次のステップ:Python基礎習得者にお勧めの道5選(実務or副業)
Pythonのゲームコード一覧は こちらをクリック
Pythonでのゲームアプリ開発は こちらをクリック
バトルゲームを作ろう|オブジェクト指向を総復習
勇者とドラゴンが交互に行動するコンソール版のターン制バトルゲームを作成しましょう。
攻撃・防御の選択と、確率で成否が変化するドラゴンの攻撃を実装し、HPが0になった側が敗北します。
第5章で学んだ設計要素(カプセル化/継承/オーバーライド/静的メソッド/抽象クラス)を用いて、読みやすく拡張しやすい構成で完成させてください。
バトルゲームのルールとプログラムの仕様
以下の要件に従ってコードを完成させてください。
- 初期化:勇者(HP=100, 攻撃力=20)とドラゴン(HP=120, 攻撃力=25)を用意する。
- 行動選択:各ターン、勇者は「攻撃」または「防御」を選べる。無効入力はやり直し。
- 攻撃処理:勇者が攻撃を選ぶと、ドラゴンのHPを勇者の攻撃力分だけ減らす。
- 防御処理:勇者が防御を選ぶと、そのターンのドラゴンの攻撃は無効化される。
- ドラゴンの攻撃:ドラゴンが生存している場合のみ行う。攻撃は確率判定で成功/失敗が決まる。
失敗するたびに攻撃成功率を+20する。
成功した場合、または防御で無効化された場合は攻撃成功率を60にリセットする。 - 終了条件:どちらかのHPが0以下になった時点で終了し、残った側を勝者として表示する。
また、オブジェクト指向を身に付けるため、以下の条件も全て満足して下さい。
- 抽象クラス:
Character
を抽象基底クラス(ABC)とし、attack(self, opponent, defend=False)
を抽象メソッドとして定義する。 - カプセル化:
name
,hp
,attack_power
は非公開相当の属性で保持し、get_*
やset_hp
、is_alive
を用意して外部から直接操作しない。 - 継承/オーバーライド:
Player
とMonster
をCharacter
から継承し、それぞれのattack
をオーバーライドして挙動を実装する。Player
は必要に応じてdefend()
を持つ。 - 静的メソッド:ダメージ適用は
Character.damage(target, amount)
の静的メソッドとして実装し、Player
/Monster
から共通利用する。 - 確率管理:ドラゴンの攻撃成功率はモンスターのインスタンス属性として保持し、初期値60、判定は1〜100の乱数に基づく。
ただし、以下のような実行結果となるコードを書くこと。
勇者のHP: 100, ドラゴンのHP: 120 (成功率: 60%) 行動を選んでください (1: 攻撃, 2: 防御): 1 勇者の攻撃! ドラゴンに20のダメージ! ドラゴンの攻撃! 攻撃が成功! 勇者に25のダメージ! 勇者のHP: 75, ドラゴンのHP: 100 (成功率: 60%) 行動を選んでください (1: 攻撃, 2: 防御): 1 勇者の攻撃! ドラゴンに20のダメージ! ドラゴンの攻撃! 攻撃はミス! 勇者のHP: 75, ドラゴンのHP: 80 (成功率: 80%) 行動を選んでください (1: 攻撃, 2: 防御): 2 勇者は防御を選んだ! ドラゴンの攻撃! 攻撃は防がれた! 勇者のHP: 75, ドラゴンのHP: 80 (成功率: 60%) 行動を選んでください (1: 攻撃, 2: 防御):
【ヒント】難しいと感じる人だけ見よう
1からコードを組み立てることが難しい場合は、以下のヒントを開いて参考にしましょう。
- ヒント1【コードの構成を見る】
-
正解のコードは上から順に以下のような構成となっています。
1:確率判定と抽象クラス設計に必要な標準ライブラリを読み込む
・乱数を扱う標準ライブラリを読み込んでおく
・抽象基底クラスと抽象メソッドを提供する仕組みを読み込む
・インポート文はファイルの先頭にまとめて記述する2:キャラクター共通の状態と振る舞いを抽象クラスで定義
・Character は抽象基底クラスにし、attack を抽象メソッドとして宣言する
・name・hp・attack_power は前にアンダースコアを付けて保持し、getter/setter と is_alive を用意する
・共通のダメージ処理は静的メソッド damage にまとめ、対象のHPから与えた分だけ減らす3:プレイヤー固有の攻撃と防御を実装し、共通のダメージ処理を呼び出す
・抽象メソッドを親と同じシグネチャで具体化し、攻撃力に応じて相手へダメージを与える
・ダメージ適用は共通の静的メソッドを使い、対象と量を渡して処理する
・防御は「有効である」ことを示す真偽値を返し、敵の攻撃側でその値を参照して分岐する4:モンスター固有の「確率つき攻撃」と成功率の増減ロジックを実装する
・親クラスの初期化を呼んだうえで、成功率用の属性を初期値で保持する
・1〜100の乱数で判定し、防御中はダメージ無効化と成功率リセットを行う
・失敗時は成功率を増加、成功時はダメージ適用後に成功率を初期値へ戻す5:メインループで行動選択・ターン処理・勝敗判定までを一括管理する
・ループは両者の生存判定を条件にして継続・終了を管理する
・プレイヤーの入力は文字列で受け取り、無効な場合はメッセージを出して続行する
・防御の有無は真偽値のフラグで持ち、敵の攻撃処理で参照して分岐する6:スクリプトとして実行されたときだけゲームを起動するエントリーポイントを用意する
・「直接実行」と「インポート時」を判定する条件を使って起動コードを囲む
・エントリーポイントの中でゲーム開始用の関数を1回だけ呼び出す
・インポート時に副作用が出ないよう、起動処理は必ずガードの内側に置く
- ヒント2【穴埋め問題にする】
-
以下のコードをコピーし、コメントに従ってコードを完成させて下さい。
import random '''(穴埋め)抽象基底クラスと抽象メソッドをインポートする(from abc import ABC, abstractmethod)''' # 抽象クラス: キャラクターの共通基盤 class Character('''(穴埋め)'''): # コンストラクタ: 名前・HP・攻撃力を設定する def __init__(self, name, hp, attack_power): '''(穴埋め)名前・HP・攻撃力を非公開属性に代入する(self._name = name / self._hp = hp / self._attack_power = attack_power)''' # 名前を取得する def get_name(self): return self._name # HPを取得する def get_hp(self): return self._hp # HPを更新する(0未満にはしない) def set_hp(self, value): if value >= 0: self._hp = value else: self._hp = 0 # 生存判定(HPが0より大きいかどうか) def is_alive(self): '''(穴埋め)HPが0より大きいかを判定して返す(例:return self._hp > 0)''' # 抽象メソッド: 攻撃処理を子クラスに実装させる '''(穴埋め)抽象メソッド attack を定義する(@abstractmethod と def attack(self, opponent, defend=False): ... を書く)''' # 静的メソッド: ダメージを与える処理 '''(穴埋め)静的メソッドとして宣言する(@staticmethod を書く)''' def damage(target, amount): '''(穴埋め)対象のHPから与ダメージ量を差し引いて更新する(target.set_hp(target.get_hp() - amount))''' # プレイヤークラス class Player(Character): # プレイヤーの攻撃処理 def attack(self, opponent, defend=False): print(f"{self._name}の攻撃!") '''(穴埋め)共通のダメージ処理を呼び出して相手にダメージを与える(Character.damage(opponent, self._attack_power))''' print(f"{opponent.get_name()}に{self._attack_power}のダメージ!") # プレイヤーの防御処理 def defend(self): print(f"{self._name}は防御を選んだ!") return True # モンスタークラス class Monster(Character): # コンストラクタ: モンスターの初期化(攻撃成功率を持つ) def __init__(self, name, hp, attack_power): super().__init__(name, hp, attack_power) '''(穴埋め)攻撃成功率の初期値を設定する(self.attack_chance = 60)''' # モンスターの攻撃処理(成功率つき) def attack(self, opponent, defend=False): '''(穴埋め)1〜100の乱数を取得して成功可否を判定するための値を用意する(roll = random.randint(1, 100))''' print(f"{self._name}の攻撃!") if roll <= self.attack_chance: if defend: print("攻撃は防がれた!") self.attack_chance = 60 else: print("攻撃が成功!") Character.damage(opponent, self._attack_power) print(f"{opponent.get_name()}に{self._attack_power}のダメージ!") self.attack_chance = 60 else: print("攻撃はミス!") self.attack_chance += 20 # ゲーム進行を管理する関数 def battle_game(): # プレイヤーとモンスターを生成 player = Player("勇者", 100, 20) monster = Monster("ドラゴン", 120, 25) # 両者が生存している間ゲームを続ける while player.is_alive() and monster.is_alive(): print(f"\n{player.get_name()}のHP: {player.get_hp()}, {monster.get_name()}のHP: {monster.get_hp()} (成功率: {monster.attack_chance}%)") action = input("行動を選んでください (1: 攻撃, 2: 防御): ") defend = False # プレイヤーの行動 if action == "1": player.attack(monster) elif action == "2": defend = player.defend() else: print("無効な入力です。もう一度選んでください。") continue # モンスターの行動 if monster.is_alive(): monster.attack(player, defend) # ゲーム終了時の勝敗判定 if player.is_alive(): print(f"\n{monster.get_name()}を倒した!{player.get_name()}の勝利!") else: print(f"\n{player.get_name()}は倒された…{monster.get_name()}の勝利!") # メイン処理: ゲームを開始する if __name__ == "__main__": battle_game()
このヒントを見てもまだ回答を導き出すのが難しいと感じる場合は、先に正解のコードと解説を見て内容を理解するようにしましょう。
当サイトではテキストベースのゲームコードを多数紹介していますが、それだけでなく 画面上で遊べるゲームアプリ の作り方を紹介たコースも用意してあります。
Pythonの実践力強化にもなりますので、ぜひ挑戦して下さい^^
解答と解説|バトルのサンプルコード
この問題の一つの正解例とそのコードの解説を以下に示します。
解答例|バトルゲームプログラム
例えば以下のようなプログラムが考えられます。
- 正解コード
-
import random from abc import ABC, abstractmethod # 抽象クラス: キャラクターの共通基盤 class Character(ABC): # コンストラクタ: 名前・HP・攻撃力を設定する def __init__(self, name, hp, attack_power): self._name = name self._hp = hp self._attack_power = attack_power # 名前を取得する def get_name(self): return self._name # HPを取得する def get_hp(self): return self._hp # HPを更新する(0未満にはしない) def set_hp(self, value): if value >= 0: self._hp = value else: self._hp = 0 # 生存判定(HPが0より大きいかどうか) def is_alive(self): return self._hp > 0 # 抽象メソッド: 攻撃処理を子クラスに実装させる @abstractmethod def attack(self, opponent, defend=False): pass # 静的メソッド: ダメージを与える処理 @staticmethod def damage(target, amount): target.set_hp(target.get_hp() - amount) # プレイヤークラス class Player(Character): # プレイヤーの攻撃処理 def attack(self, opponent, defend=False): print(f"{self._name}の攻撃!") Character.damage(opponent, self._attack_power) print(f"{opponent.get_name()}に{self._attack_power}のダメージ!") # プレイヤーの防御処理 def defend(self): print(f"{self._name}は防御を選んだ!") return True # モンスタークラス class Monster(Character): # コンストラクタ: モンスターの初期化(攻撃成功率を持つ) def __init__(self, name, hp, attack_power): super().__init__(name, hp, attack_power) self.attack_chance = 60 # モンスターの攻撃処理(成功率つき) def attack(self, opponent, defend=False): roll = random.randint(1, 100) print(f"{self._name}の攻撃!") if roll <= self.attack_chance: if defend: print("攻撃は防がれた!") self.attack_chance = 60 else: print("攻撃が成功!") Character.damage(opponent, self._attack_power) print(f"{opponent.get_name()}に{self._attack_power}のダメージ!") self.attack_chance = 60 else: print("攻撃はミス!") self.attack_chance += 20 # ゲーム進行を管理する関数 def battle_game(): # プレイヤーとモンスターを生成 player = Player("勇者", 100, 20) monster = Monster("ドラゴン", 120, 25) # 両者が生存している間ゲームを続ける while player.is_alive() and monster.is_alive(): print(f"\n{player.get_name()}のHP: {player.get_hp()}, {monster.get_name()}のHP: {monster.get_hp()} (成功率: {monster.attack_chance}%)") action = input("行動を選んでください (1: 攻撃, 2: 防御): ") defend = False # プレイヤーの行動 if action == "1": player.attack(monster) elif action == "2": defend = player.defend() else: print("無効な入力です。もう一度選んでください。") continue # モンスターの行動 if monster.is_alive(): monster.attack(player, defend) # ゲーム終了時の勝敗判定 if player.is_alive(): print(f"\n{monster.get_name()}を倒した!{player.get_name()}の勝利!") else: print(f"\n{player.get_name()}は倒された…{monster.get_name()}の勝利!") # メイン処理: ゲームを開始する if __name__ == "__main__": battle_game()
解答例の解説|バトルゲームの考え方
解答例の詳細解説は以下の通りです。
- 詳細解説
-
インポート文
import random from abc import ABC, abstractmethod
この部分では、ゲームの処理で使う外部機能を最初に読み込んでいます。
確率による攻撃の成否やダメージ計算では乱数が必要になるため、乱数を扱う標準ライブラリを準備します。
また、キャラクターの共通仕様を設計図として表現するために、抽象基底クラスと抽象メソッドを提供する仕組みも読み込みます。
こうすることで、後続のコードで「共通のインターフェースは保ちつつ振る舞いは各クラスで実装する」という設計が可能になります。
インポートはファイルの先頭にまとめるのが一般的です。抽象クラス: キャラクターの共通基盤
# 抽象クラス: キャラクターの共通基盤 class Character(ABC): # コンストラクタ: 名前・HP・攻撃力を設定する def __init__(self, name, hp, attack_power): self._name = name self._hp = hp self._attack_power = attack_power # 名前を取得する def get_name(self): return self._name # HPを取得する def get_hp(self): return self._hp # HPを更新する(0未満にはしない) def set_hp(self, value): if value >= 0: self._hp = value else: self._hp = 0 # 生存判定(HPが0より大きいかどうか) def is_alive(self): return self._hp > 0 # 抽象メソッド: 攻撃処理を子クラスに実装させる @abstractmethod def attack(self, opponent, defend=False): pass # 静的メソッド: ダメージを与える処理 @staticmethod def damage(target, amount): target.set_hp(target.get_hp() - amount)
この部分では、ゲーム内の登場人物が共有する性質をひとつにまとめています。
名前・HP・攻撃力は外から直接いじれないように非公開相当の属性として保持し、値の取得や更新は専用のメソッド経由で行います。
HPは負の値にならないように下限を設け、現在生きているかどうかは生存判定メソッドで判断します。
攻撃はキャラクターごとに振る舞いが異なるため、抽象メソッドとして定義して子クラスに実装を義務づけます。
さらに、ダメージ適用の処理は静的メソッドに切り出し、共通の手順で相手のHPを減らせるようにしています。
これにより、共通仕様はここに集約し、違いは派生クラス側で表現するという設計が自然に実現できます。プレイヤークラスの定義
# プレイヤークラス class Player(Character): # プレイヤーの攻撃処理 def attack(self, opponent, defend=False): print(f"{self._name}の攻撃!") Character.damage(opponent, self._attack_power) print(f"{opponent.get_name()}に{self._attack_power}のダメージ!") # プレイヤーの防御処理 def defend(self): print(f"{self._name}は防御を選んだ!") return True
ここでは、抽象クラスで約束した攻撃の振る舞いをプレイヤー用に具体化します。
攻撃では自分の攻撃力に基づいて相手のHPを減らし、その手続きは共通のダメージ処理に任せます。
表示用のメッセージもここでまとめて出力します。
防御は自分の行動として選べるもう一つの選択肢で、次の相手ターンに備えて「防御中である」ことを示す真偽値を返します。
プレイヤー自身は防御でダメージを与えないため、実際の無効化は相手側の攻撃処理でこのフラグを読み取り、攻撃結果を分岐させる形で実現します。モンスタークラスの定義
# モンスタークラス class Monster(Character): # コンストラクタ: モンスターの初期化(攻撃成功率を持つ) def __init__(self, name, hp, attack_power): super().__init__(name, hp, attack_power) self.attack_chance = 60 # モンスターの攻撃処理(成功率つき) def attack(self, opponent, defend=False): roll = random.randint(1, 100) print(f"{self._name}の攻撃!") if roll <= self.attack_chance: if defend: print("攻撃は防がれた!") self.attack_chance = 60 else: print("攻撃が成功!") Character.damage(opponent, self._attack_power) print(f"{opponent.get_name()}に{self._attack_power}のダメージ!") self.attack_chance = 60 else: print("攻撃はミス!") self.attack_chance += 20
この部分では、モンスターの行動をプレイヤーとは別の仕組みで表現しています。
まず、継承元の初期化に続けて攻撃成功率をもつ状態を追加し、戦闘中にその値を更新できるようにします。
攻撃時は乱数を使って成功か失敗かを判定し、成功したら共通のダメージ処理で相手の体力を減らします。
プレイヤーが防御している場合は、攻撃自体は行われたことを示しつつもダメージは通さず、次の攻撃に向けて成功率を初期値に戻します。
攻撃が外れた場合は次こそ当たりやすくなるように成功率を上げていく設計です。
これにより、戦闘に緊張感と駆け引きが生まれ、確率の変化がゲーム体験に反映されます。ゲーム進行を管理する関数
# ゲーム進行を管理する関数 def battle_game(): # プレイヤーとモンスターを生成 player = Player("勇者", 100, 20) monster = Monster("ドラゴン", 120, 25) # 両者が生存している間ゲームを続ける while player.is_alive() and monster.is_alive(): print(f"\n{player.get_name()}のHP: {player.get_hp()}, {monster.get_name()}のHP: {monster.get_hp()} (成功率: {monster.attack_chance}%)") action = input("行動を選んでください (1: 攻撃, 2: 防御): ") defend = False # プレイヤーの行動 if action == "1": player.attack(monster) elif action == "2": defend = player.defend() else: print("無効な入力です。もう一度選んでください。") continue # モンスターの行動 if monster.is_alive(): monster.attack(player, defend) # ゲーム終了時の勝敗判定 if player.is_alive(): print(f"\n{monster.get_name()}を倒した!{player.get_name()}の勝利!") else: print(f"\n{player.get_name()}は倒された…{monster.get_name()}の勝利!")
ここではゲーム全体の流れをまとめています。最初に勇者とドラゴンのインスタンスを作り、両者が生きている限りターンを繰り返します。
各ターンの冒頭で現在のHPとモンスターの成功率を表示し、プレイヤーに行動を選んでもらいます。
攻撃なら相手の体力を減らし、防御なら次の敵の攻撃を無効化できる状態を示すフラグを立てます。
無効な入力はやり直させて、処理が破綻しないようにしています。
プレイヤーの行動の後、モンスターがまだ生きていれば攻撃を試み、プレイヤーの防御フラグを参照して結果を分岐させます。
どちらかのHPが尽きたらループを抜け、最後に勝者を表示して戦闘を締めくくります。ゲームのエントリーポイント
# メイン処理: ゲームを開始する if __name__ == "__main__": battle_game()
この部分は、ファイルが「直接実行」されたときにだけゲームを始めるための入口です。
Python では、ファイルを直接実行すると特別な識別子が設定され、他のファイルから読み込まれた場合とは区別できます。
ここで起動処理を囲っておくことで、別のプログラムからこのファイルをインポートしても勝手にゲームが動き出しません。
結果として、テストや再利用がしやすくなり、起動の責務を一箇所にまとめられます。
このガードの中では用意した関数を一度だけ呼び出し、ゲームのメインループへ処理を渡します。
Pythonのゲームコード一覧は こちらをクリック
Pythonでのゲームアプリ開発は こちらをクリック
- サイト改善アンケート|ご意見をお聞かせください(1分で終わります)
-
本サイトでは、みなさまの学習をよりサポートできるサービスを目指しております。
そのため、みなさまの「プログラミングを学習する理由」などをアンケート形式でお伺いしています。1分だけ、ご協力いただけますと幸いです。
【Python】サイト改善アンケート