【Python】レッスン5-S1:モンスター捕獲ゲームを作ろう

一つ前の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でのゲームアプリ開発は こちらをクリック
モンスター捕獲ゲームを作ろう|オブジェクト指向を総復習
オブジェクト指向(抽象クラス/継承/オーバーライド)を用いて、サイコロ勝負でモンスターを捕獲していくコンソールゲームを作成してください。
プレイヤーは3種類のモンスターに順に挑み、出目の比較で捕獲可否を判定します。
捕獲に成功したモンスターを記録し、ゲームの最後に一覧表示します。
モンスター捕獲ゲームのルールとプログラムの仕様
以下の要件に従ってゲームを作成してください。
- 登場モンスター:スライム、ゴブリン、ドラゴンの3体に挑戦する。
- 進行:各モンスターの出現時に固有の行動メッセージを表示し、モンスターとプレイヤーがサイコロを振って出目を比較する。
- 入力:毎回、プレイヤーは「サイコロを振るか」を
yes/no
で選択できる。no
の場合はそのモンスターをスキップして次へ進む。 - 判定:プレイヤーの出目が大きければ捕獲成功。小さければその時点でゲーム終了。同値は引き分けとして次の挑戦へ進む。
- 結果表示:捕獲に成功したモンスター名をリストで管理し、最後に一覧で表示する。
また、オブジェクト指向を身に付けるため、以下の条件も全て満足して下さい。
- 抽象クラス:
Monster
を抽象基底クラスとして定義し、各モンスターが実装すべき抽象メソッド(固有の行動を文字列で返す)を持たせる。 - カプセル化:モンスターは名前とHPを属性として保持し、ゲッター/セッター等で適切にカプセル化する。
- 継承:各モンスタークラスは
Monster
をら継承し、固有の行動メッセージを実装する。 - 静的メソッド:サイコロ機能はユーティリティクラス
Dice
の 静的メソッド で 1〜6 の乱数を返す。 - オーバーライド:
Dragon
はサイコロ処理を オーバーライド し、2回振って大きい方の出目を採用する。
ただし、以下のような実行結果となるコードを書くこと。
スライムが現れた! スライムがサイコロを振った。出目は 3 サイコロを振りますか? (yes/no): yes プレイヤーがサイコロを振った。出目は 5 スライムを捕まえた! ゴブリンが現れた! ゴブリンがサイコロを振った。出目は 4 サイコロを振りますか? (yes/no): yes プレイヤーがサイコロを振った。出目は 2 ゴブリンを捕まえられなかった。ゲームオーバー!
【ヒント】難しいと感じる人だけ見よう
1からコードを組み立てることが難しい場合は、以下のヒントを開いて参考にしましょう。
- ヒント1【コードの構成を見る】
-
正解のコードは上から順に以下のような構成となっています。
1:乱数で1〜6の目を返す「サイコロ」の共通部品を用意する
・乱数を扱う標準ライブラリを使って、1〜6の整数を返すようにする
・インスタンスに依存しない処理は静的メソッドとして定義する
・サイコロの処理はクラスにまとめ、どの場面からも同じ呼び出し方で使えるようにする2:モンスターの共通仕様を抽象クラスで定義し、名前/HPのカプセル化とサイコロ処理をまとめる
・このクラスは直接インスタンス化せず、派生クラスで固有の行動メソッドを必ず実装する
・名前とHPは外から直接触らず、取得・更新用メソッドを経由し、HPは0未満を拒否する
・サイコロ処理はユーティリティに任せ、ここではそれを呼び出すだけにする3:抽象クラスを継承した具体モンスター(スライム/ゴブリン)の固有行動を定義
・基底クラスを継承して、抽象メソッドを「必ず」具体化する
・固有行動は文字列メッセージとして返す(表示は呼び出し側で行う)
・名前の参照は基底クラスの取得メソッドを使って文を組み立てる4:ドラゴンだけサイコロ規則をオーバーライドして「2回振って高い目」を採用
・親クラスと同じメソッド名・引数で定義し、中身の処理だけを差し替える
・サイコロは2回振って、大きい方の値を返す
・固有の行動メッセージを返すメソッドも忘れずに実装する5:ゲームの初期状態を用意し、登場モンスターと捕獲記録の入れ物を準備
・コンストラクタで、対戦相手となるモンスターのインスタンスを順序つきで保持する
・捕獲成功時に追加していくための空のリストを用意する
・モンスターには表示用の名前を渡しておき、後でそのまま出力に使えるようにする6:ゲームの進行ループを実装し、入出力・勝敗判定・記録・終了処理をまとめて行う
・モンスターごとに同じ手順を回すため、順序つきのループで処理する
・入力は余分な空白を取り除き、小文字にそろえて判定ミスを避ける
・勝敗は「勝ち→記録して続行」「引き分け→次へ進む」「負け→即終了」という3分岐で整理7:スクリプトとして起動されたときだけゲームを開始するエントリーポイントを用意
・「直接実行」と「モジュールとしてインポート」の違いを判定する条件を使う
・ゲーム用のクラスからインスタンスを1つ作り、進行用メソッドを呼び出す
・起動処理はエントリーポイントの中に閉じ込め、再利用時に副作用が出ないようにする
- ヒント2【穴埋め問題にする】
-
以下のコードをコピーし、コメントに従ってコードを完成させて下さい。
import random '''(穴埋め)abcモジュールからABCとabstractmethodをインポートする''' # from abc import ABC, abstractmethod # サイコロクラス(ユーティリティ) class Dice: @'''(穴埋め)''' def roll(): '''(穴埋め)1〜6の乱数を返す処理(random.randint(1, 6))を書く''' # return random.randint(1, 6) # モンスター基底クラス(抽象クラス) class Monster('''(穴埋め)'''): def __init__(self, name: str, hp: int = 10): '''(穴埋め)名前とHPを受け取り、_nameと_hpに代入する''' # self._name = name # プライベート属性(名前) # self._hp = hp # プライベート属性(HP) def get_name(self): return self._name def get_hp(self): return self._hp def set_hp(self, value: int): if value >= 0: self._hp = value else: print("HPは0以上でなければなりません") def dice_roll(self): '''(穴埋め)サイコロを振るユーティリティを呼び出して出目を返す''' # return Dice.roll() @'''(穴埋め)''' # 各モンスターが持つ特別な動きを表現する抽象メソッド def special_move(self): pass # スライムクラス(Monsterを継承) class Slime(Monster): def special_move(self): return f"{self.get_name()}は体当たりを仕掛けた!" # ゴブリンクラス(Monsterを継承) class Goblin(Monster): def special_move(self): return f"{self.get_name()}は棍棒を振り回した!" # ドラゴンクラス(オーバーライドあり) class Dragon(Monster): def dice_roll(self): # ドラゴンは2回振って大きい方を採用 '''(穴埋め)サイコロを2回振り、それぞれの結果から大きい方を返す処理を書く''' # roll1, roll2 = Dice.roll(), Dice.roll() # return max(roll1, roll2) def special_move(self): return f"{self.get_name()}は火を吹いた!" # ゲーム本体 class MonsterCaptureGame: def __init__(self): self.monsters = [Slime("スライム"), Goblin("ゴブリン"), Dragon("ドラゴン")] self.captured_monsters = [] def play(self): for monster in self.monsters: print(f"{monster.get_name()}が現れた!") '''(穴埋め)モンスターの固有行動メッセージを表示する''' # print(monster.special_move()) monster_roll = monster.dice_roll() print(f"{monster.get_name()}がサイコロを振った。出目は {monster_roll}") response = input("サイコロを振りますか? (yes/no): ").strip().lower() if response == "no": print(f"{monster.get_name()}をスキップしました。") continue # 次のモンスターへ '''(穴埋め)プレイヤーのサイコロを振り、出目をplayer_rollに代入する''' # player_roll = Dice.roll() print(f"プレイヤーがサイコロを振った。出目は {player_roll}") if player_roll > monster_roll: print(f"{monster.get_name()}を捕まえた!") self.captured_monsters.append(monster.get_name()) elif player_roll == monster_roll: print("引き分けでもう一度!") # 再戦扱いで同じモンスターに再挑戦 continue else: print(f"{monster.get_name()}を捕まえられなかった。ゲームオーバー!") break print("捕まえたモンスター: " + ", ".join(self.captured_monsters)) print("ゲーム終了") # 実行 if __name__ == "__main__": game = MonsterCaptureGame() game.play()
このヒントを見てもまだ回答を導き出すのが難しいと感じる場合は、先に正解のコードと解説を見て内容を理解するようにしましょう。
当サイトではテキストベースのゲームコードを多数紹介していますが、それだけでなく 画面上で遊べるゲームアプリ の作り方を紹介たコースも用意してあります。
Pythonの実践力強化にもなりますので、ぜひ挑戦して下さい^^
解答と解説|モンスター捕獲ゲームのサンプルコード
この問題の一つの正解例とそのコードの解説を以下に示します。
解答例|モンスター捕獲ゲームプログラム
例えば以下のようなプログラムが考えられます。
- 正解コード
-
import random from abc import ABC, abstractmethod # サイコロクラス(ユーティリティ) class Dice: @staticmethod def roll(): return random.randint(1, 6) # モンスター基底クラス(抽象クラス) class Monster(ABC): def __init__(self, name: str, hp: int = 10): self._name = name # プライベート属性(名前) self._hp = hp # プライベート属性(HP) def get_name(self): return self._name def get_hp(self): return self._hp def set_hp(self, value: int): if value >= 0: self._hp = value else: print("HPは0以上でなければなりません") def dice_roll(self): return Dice.roll() @abstractmethod # 各モンスターが持つ特別な動きを表現する抽象メソッド def special_move(self): pass # スライムクラス(Monsterを継承) class Slime(Monster): def special_move(self): return f"{self.get_name()}は体当たりを仕掛けた!" # ゴブリンクラス(Monsterを継承) class Goblin(Monster): def special_move(self): return f"{self.get_name()}は棍棒を振り回した!" # ドラゴンクラス(オーバーライドあり) class Dragon(Monster): def dice_roll(self): # ドラゴンは2回振って大きい方を採用 roll1, roll2 = Dice.roll(), Dice.roll() return max(roll1, roll2) def special_move(self): return f"{self.get_name()}は火を吹いた!" # ゲーム本体 class MonsterCaptureGame: def __init__(self): self.monsters = [Slime("スライム"), Goblin("ゴブリン"), Dragon("ドラゴン")] self.captured_monsters = [] def play(self): for monster in self.monsters: print(f"{monster.get_name()}が現れた!") print(monster.special_move()) monster_roll = monster.dice_roll() print(f"{monster.get_name()}がサイコロを振った。出目は {monster_roll}") response = input("サイコロを振りますか? (yes/no): ").strip().lower() if response == "no": print(f"{monster.get_name()}をスキップしました。") continue # 次のモンスターへ player_roll = Dice.roll() print(f"プレイヤーがサイコロを振った。出目は {player_roll}") if player_roll > monster_roll: print(f"{monster.get_name()}を捕まえた!") self.captured_monsters.append(monster.get_name()) elif player_roll == monster_roll: print("引き分けでもう一度!") # 再戦扱いで同じモンスターに再挑戦 continue else: print(f"{monster.get_name()}を捕まえられなかった。ゲームオーバー!") break print("捕まえたモンスター: " + ", ".join(self.captured_monsters)) print("ゲーム終了") # 実行 if __name__ == "__main__": game = MonsterCaptureGame() game.play()
解答例の解説|モンスター捕獲ゲームの考え方
解答例の詳細解説は以下の通りです。
- 詳細解説
-
インポートとサイコロクラス定義
import random from abc import ABC, abstractmethod # サイコロクラス(ユーティリティ) class Dice: @staticmethod def roll(): return random.randint(1, 6)
ここでは、ゲーム全体で使い回すサイコロ機能をひとつの部品として切り出しています。
ランダムな数値を得るために標準ライブラリを読み込み、サイコロの出目(1〜6)を返す処理をまとめました。
サイコロは内部の状態を持たないため、インスタンスを作らなくても呼び出せる静的メソッドとして定義します。
これにより、どこからでも同じやり方で出目を取得でき、テストや再利用がしやすくなります。
あわせて、後続の章で使う抽象クラス用の仕組みも読み込んでおきます。モンスター基底クラス(抽象クラス)の定義
# モンスター基底クラス(抽象クラス) class Monster(ABC): def __init__(self, name: str, hp: int = 10): self._name = name # プライベート属性(名前) self._hp = hp # プライベート属性(HP) def get_name(self): return self._name def get_hp(self): return self._hp def set_hp(self, value: int): if value >= 0: self._hp = value else: print("HPは0以上でなければなりません") def dice_roll(self): return Dice.roll() @abstractmethod # 各モンスターが持つ特別な動きを表現する抽象メソッド def special_move(self): pass
ここでは、すべてのモンスターに共通する振る舞いとデータをひとつの設計図として用意しています。
抽象クラスを使うことで、この設計図そのものは直接使わず、後で作る各モンスターが必ず固有の行動メソッドを実装する約束を課せます。
名前とHPは外部から勝手に書き換えられないように非公開相当の属性として持たせ、値の読み取り・更新は専用メソッド経由に限定します(更新時には不正な値をはじきます)。
また、サイコロを振る処理は共通のユーティリティに委ね、どのモンスターでも同じ方法で実行できるようにしています。
これにより、共通部分はここに集約し、違いは派生クラスで表現するというオブジェクト指向の基本形が出来上がります。スライムクラスとゴブリンクラスの定義
# スライムクラス(Monsterを継承) class Slime(Monster): def special_move(self): return f"{self.get_name()}は体当たりを仕掛けた!" # ゴブリンクラス(Monsterを継承) class Goblin(Monster): def special_move(self): return f"{self.get_name()}は棍棒を振り回した!"
ここでは、設計図である抽象クラスを土台にして、実際に使えるモンスターを作っています。
スライムとゴブリンは、共通の性質(名前やHP、サイコロ処理)は基底クラスから受け継ぎ、各自の「固有の行動」を表す抽象メソッドを具体的なメッセージで実装します。
これにより、同じインターフェースを持ちながら中身が異なる振る舞いを実現でき、後で多態性として一括処理しやすくなります。
今回の2クラスは追加の状態や初期化が不要なため、基底クラスの初期化そのままで十分です。
名前は基底クラスの取得メソッドを利用して、行動メッセージに埋め込むだけで目的を達成できます。ドラゴンクラスの定義
# ドラゴンクラス(オーバーライドあり) class Dragon(Monster): def dice_roll(self): # ドラゴンは2回振って大きい方を採用 roll1, roll2 = Dice.roll(), Dice.roll() return max(roll1, roll2) def special_move(self): return f"{self.get_name()}は火を吹いた!"
この部分では、基底クラスで用意したサイコロ処理をドラゴンだけ特別仕様に差し替えています。
親と同じメソッド名で処理を入れ替えることをオーバーライドと呼び、同じインターフェースのまま振る舞いだけを変えられます。
ここではユーティリティのサイコロを2回使い、そのうち大きい出目を選ぶことでドラゴンの強さを表現します。
こうしておくと、ゲーム側はモンスターの種類を意識せずに同じ呼び出しを行い、結果だけが違って返るという多態性のメリットを得られます。
あわせて、ドラゴン固有の行動メッセージも定義して、出現時に特色を演出できるようにしています。ゲーム本体のクラス定義
# ゲーム本体 class MonsterCaptureGame: def __init__(self): self.monsters = [Slime("スライム"), Goblin("ゴブリン"), Dragon("ドラゴン")] self.captured_monsters = []
ここでは、ゲーム開始時に必要となるデータをまとめて作っています。
まず、挑戦する相手を順番つきで管理するために、モンスターのインスタンスを並べたリストを用意します。
こうしておくと、ゲーム進行ではこの並びを先頭から処理するだけで出現順を制御できます。
次に、捕獲できたモンスターの名前を蓄えるための空のリストを用意します。
ゲーム中は捕獲に成功するたびにこのリストへ追加し、最後にまとめて表示します。
初期化の段階で「誰に挑むのか」と「結果をどこに記録するのか」をはっきり分けておくことで、後続の処理が読みやすくなります。ゲームの進行ループ
def play(self): for monster in self.monsters: print(f"{monster.get_name()}が現れた!") print(monster.special_move()) monster_roll = monster.dice_roll() print(f"{monster.get_name()}がサイコロを振った。出目は {monster_roll}") response = input("サイコロを振りますか? (yes/no): ").strip().lower() if response == "no": print(f"{monster.get_name()}をスキップしました。") continue # 次のモンスターへ player_roll = Dice.roll() print(f"プレイヤーがサイコロを振った。出目は {player_roll}") if player_roll > monster_roll: print(f"{monster.get_name()}を捕まえた!") self.captured_monsters.append(monster.get_name()) elif player_roll == monster_roll: print("引き分けでもう一度!") # 再戦扱いで同じモンスターに再挑戦 continue else: print(f"{monster.get_name()}を捕まえられなかった。ゲームオーバー!") break print("捕まえたモンスター: " + ", ".join(self.captured_monsters)) print("ゲーム終了")
ここでは、1体ずつモンスターに挑む一連の流れをまとめて作っています。
まず、用意したモンスターの並びを先頭から処理し、出現メッセージと固有の行動を表示します。
次に、モンスターがサイコロを振った出目を知らせ、プレイヤーに実行可否を尋ねます。
プレイヤーが振らないと答えた場合は、そのモンスターの勝負をスキップして次へ進みます。
プレイヤーが振ると決めたら出目を表示し、モンスターの出目と比較して判定します。
勝てば捕獲に成功し、名前を記録用リストへ追加します。
引き分けになった場合は、その勝負を打ち切って次のモンスターへ移ります。負けた場合はその時点でゲームを終了します。
全体の処理が終わったら、捕獲できたモンスターの一覧をまとめて表示し、ゲームの終了を告げます。
分岐ごとに「続ける・飛ばす・終わる」の流れを明確にし、読みやすさと挙動の一貫性を重視しています。ゲームのエントリーポイント
# 実行 if __name__ == "__main__": game = MonsterCaptureGame() game.play()
この部分は、ファイルを直接実行した場合にだけゲームを動かすための「入口」の役割を持ちます。
Pythonでは、ファイルがそのまま起動されたときと、ほかのファイルから読み込まれたときで内部の識別子が変わります。
ここでは「直接起動されたとき」にだけゲームのオブジェクトを作って、進行メソッドを呼び出しています。
こうしておくと、同じファイルを別のプロジェクトから部品として再利用する場合でも、勝手にゲームが動き出さず、安全にインポートできるようになります。
Pythonのゲームコード一覧は こちらをクリック
Pythonでのゲームアプリ開発は こちらをクリック
- サイト改善アンケート|ご意見をお聞かせください(1分で終わります)
-
本サイトでは、みなさまの学習をよりサポートできるサービスを目指しております。
そのため、みなさまの「プログラミングを学習する理由」などをアンケート形式でお伺いしています。1分だけ、ご協力いただけますと幸いです。
【Python】サイト改善アンケート