#10 PythonでRPGを作ろう!基礎が固まるコマンドラインで動くゲーム開発 | 初心者向け

python_rpg21

皆さんこんにちは。1つのゲームをやりこむよりも、たくさんのゲームを浅く楽しむ方が好きな小幡です。2000年代のMMORPGに貴重な学生時代の時間をつぎ込み、ひたすらマウスをクリックし続けている現状に、「これは何の生産性もない作業だったんだ」と悲しくなった記憶がまだ新しい今日この頃です。

それでも当時は、ネットの向こう側にいるプレイヤーがいる、というインターネットの魔力的なものにひかれてネットゲームやSNSと共に生きてきたのも事実です。こうしてWeb関連の仕事が出来ていることに感謝せずにはいられません。

最近はあまりゲームを楽しめていないことが悔やまれますが、まだまだ時間を作る努力が足りないのだと思います。それでもこうしてブログを通じてコミュニケーションが取れるだけでも嬉しいことです。2000年代に入って早々にブログを書いていた私ですが、飽き性な事もあり、これだけ長くブログを続けることができたのは初めての事だなぁと、10回目の記事を書きながら思うのでした。

話が大分脱線しましたが、前回の記事は以下リンクよりどうぞ

前回のおさらいです。
  • コードの整理をしてみた
  • Eventクラスを完成させた
  • Textクラスを完成させた
  • Processクラスを完成させた

というように、説明不足なところもあるかと思いますが、一気に完成まで近づけました。

今回も出来る限りたくさんのコードの記述を進め、一区切りつけたいなと考えています。残りのクラスも一気に記述を進めて、ほぼ完成形を目指します。頑張っていきましょう。

Mapクラスのコンストラクタを編集する

ここではPythonでは至って普通の事なので、勉強しなくても良いかもしれませんが、他の(特にJava言語を得意とする)人と一緒に開発を進めていく想定だとゲッターとセッターの問題が出てくると思います。興味がある人は是非読んでみてください。

Pythonの場合は、クラスで定義したメンバには簡単にアクセスできますが、Javaなどでは簡単にアクセスできないのが普通らしいです。Pythonの基礎を勉強してみた後、「Javaも少しかじっておこう」と少し勉強してみると、このゲッターとセッターで混乱が起きるかもしれません。

そんな時は以下リンクを参考にしてみるとスムーズに理解できるかもしれません。

ということで、Pythonはpublicを多用して単純なコードで全然構わないという事らしいですが、試しにゲッターとセッターのコードを記述してみます。

以前作成した、Mapクラスのコンストラクタは以下です。

    def __init__(self):
        self.now_h = 1
        self.now_w = 1
        self.counter = 0
        self.goal_flg = False
        self.game_over_flg = False
        self.show_item_flg = False
        self.field = ''

これにゲッターとセッターを追加すると以下になります。

    @property
    def counter(self):
        return self._counter

    @counter.setter
    def counter(self, value):
        self._counter = value

    @property
    def now_h(self):
        return self._now_h

    @now_h.setter
    def now_h(self, value):
        self._now_h = value

    @property
    def now_w(self):
        return self._now_w

    @now_w.setter
    def now_w(self, value):
        self._now_w = value

    @property
    def goal_flg(self):
        return self._goal_flg

    @goal_flg.setter
    def goal_flg(self, value):
        self._goal_flg = value

    @property
    def game_over_flg(self):
        return self._game_over_flg

    @game_over_flg.setter
    def game_over_flg(self, value):
        self._game_over_flg = value

    @property
    def show_item_flg(self):
        return self._show_item_flg

    @show_item_flg.setter
    def show_item_flg(self, value):
        self._show_item_flg = value

    @property
    def field(self):
        return self._field

    @field.setter
    def field(self, value):
        self._field = value

    def __init__(self):
        self._now_h = 1
        self._now_w = 1
        self._counter = 0
        self._goal_flg = False
        self._game_over_flg = False
        self._show_item_flg = False
        self._field = ''

注意して頂きたいのは、コンストラクタ内のselfの後に続く記述にアンダーバーが1つ追加されているということです。praivateなメンバを作成するのならば本来的にはアンダーバーが2つとなるはずですが、それはPython的には推奨していないようです。

Pythonではその場のプロジェクトに合わせて柔軟に対応するという、広い心を持って作業にあたることが重要ではないかと考えています。もし、「絶対にprivateを全てに当てはめなければならない」とか「正確に型も決めたいし、コーディング規約も全て定められていないと書けない」という様な人は、PythonではなくJavaなど他の言語で記述した方が上手くいくかもしれません。

Playerクラスにも追加する

ついでに、Playerクラスにもゲッターとセッターを追加してみます。以前までのPlayerクラスのコンストラクタは以下です。

    def __init__(self, name: str):
        self.name = name
        self.max_hp = 200
        self.hp = 200
        self.max_mp = 5
        self.mp = 5
        self.power = 10
        self.defense = 0
        self.exp = 0
        self.level = 1
        self.item_list = []

これにゲッターとセッターを追加すると以下になります。同時に定数も定義しています。これはもっといい方法があるとおもいますが、また機会があったら修正します。

    DEFAULT_INITIAL_HP = 200
    DEFAULT_INITIAL_MP = 5
    DEFAULT_INITIAL_POWER = 10
    DEFAULT_INITIAL_DEFENSE = 0
    DEFAULT_INITIAL_EXP = 0
    DEFAULT_INITIAL_LEVEL = 1
    DEFAULT_INITIAL_ITEM_LIST = []

    ADD_MAX_HP = 20
    ADD_POWER = 5
    ADD_DEFENCE = 1
    ADD_LEVEL = 1

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value

    @property
    def max_hp(self):
        return self._max_hp

    @max_hp.setter
    def max_hp(self, value):
        self._max_hp = value

    @property
    def hp(self):
        return self._hp

    @hp.setter
    def hp(self, value):
        self._hp = value

    @property
    def max_mp(self):
        return self._max_mp

    @max_mp.setter
    def max_mp(self, value):
        self._max_mp = value

    @property
    def mp(self):
        return self._mp

    @mp.setter
    def mp(self, value):
        self._mp = value

    @property
    def power(self):
        return self._power

    @power.setter
    def power(self, value):
        self._power = value

    @property
    def defense(self):
        return self._defense

    @defense.setter
    def defense(self, value):
        self._defense = value

    @property
    def exp(self):
        return self._exp

    @exp.setter
    def exp(self, value):
        self._exp = value

    @property
    def level(self):
        return self._level

    @level.setter
    def level(self, value):
        self._level = value

    @property
    def item_list(self):
        return self._item_list

    @item_list.setter
    def item_list(self, value):
        self._item_list = value

    def __init__(self, name: str):
        self._name = name
        self._max_hp = self.DEFAULT_INITIAL_HP
        self._hp = self.DEFAULT_INITIAL_HP
        self._max_mp = self.DEFAULT_INITIAL_MP
        self._mp = self.DEFAULT_INITIAL_MP
        self._power = self.DEFAULT_INITIAL_POWER
        self._defense = self.DEFAULT_INITIAL_DEFENSE
        self._exp = self.DEFAULT_INITIAL_EXP
        self._level = self.DEFAULT_INITIAL_LEVEL
        self._item_list = self.DEFAULT_INITIAL_ITEM_LIST

ゲッターとセッターを追加しましたが、インスタンス生成した後に使用するコードの記述は変わりませんので、他は修正せずにゲームを実行することができます。

Playerクラスを修正したので、ついでに完成形で使用する予定のメソッドも追加しておきます。

RPGはその名の通り、ロールをプレイするわけですが、今回のゲームでは「勇者的な存在になって魔王を倒す」というよりは「プレイヤーになってゴールを目座す」という内容ですので、残念ながらジョブ機能であったり、プレイヤーが喋ったりする機能は実装しません。

しかし、「敵と戦闘する」であったり「敵を倒したら強くなる」というような機能は追加します。「マップに落ちているアイテムを拾って使用する」ということも実装するので、この「レベルアップ」と「アイテム取得」をPlayerクラスに行ってもらう事にします。

追加するメソッドは2つでlevel_upとget_itemです。コードの内容は以下です。

    def level_up(self):
        """レベルアップ
        """
        self.max_hp += self.ADD_MAX_HP
        self.power += self.ADD_POWER
        self.defense += self.ADD_DEFENCE
        self.level += self.ADD_LEVEL

    def get_item(self, field: str):
        """アイテム一覧にフィールドのアイテム(MapItemインスタンス)を詰める

        Args:
            field (str): フィールドのアイテム
        """
        # 剣の場合
        if field == MapItem.WEAPON.value:
            self.power += 30
            print(Text.MES_GET_WEAPON)
            Event.input()

        # 盾の場合
        if field == MapItem.SIELD.value:
            self.defense += 10
            print(Text.MES_GET_SIELD)
            Event.input()

        # 薬の場合
        if field == MapItem.HERBS.value:
            print(Text.MES_GET_HERBS)
            Event.input()

        # アイテム一覧に詰める
        self.item_list.append(MapItem(field))

上のメソッド2つは引数にselfがあることからもわかる通り、インスタンスが生成されてから呼び出されることを想定しています。プレイヤーの名前が決定された後にプレイヤーのインスタンスが生成される流れになっていますが、このプレイヤーは実体として、自ら(self)「レベルアップ」したり「アイテム取得」するというイメージです。

自らがレベルアップしたことで、自分自身のHPやMPを上昇させていることがわかるかと思います。こうすることでインスタンスの値として保持されますので、もしもこのRPGを拡張させて、仲間を増やそうと思った時、2つめのプレイヤーインスタンスを生成することで簡単に2人目のキャラクターを作る事ができます。もちろん二人目のキャラクターは独自にHPやMPを持つことができます。

2つめのメソッドのget_itemは、フィールドに落ちているアイテムをプレイヤーインスタンスに保持させる処理です。

マップを移動する際に実装した処理の中でfiedに移動先のアイテムの文字列を格納したのを覚えているでしょうか?このfieldはここでも役に立ちます。もしも移動先にアイテムが落ちていた場合は、標準出力で「〇〇を手に入れた!」というような文字を出力させた後に、自分自身のアイテムリストにアイテムをappend(追加)しています。appendはその字の通りリストに何かを追加する処理で、その後のMapItem()では、fieldに入っている文字列を入れてMapItemのインスタンスを生成しています。リストにインスタンスを挿入しているということになります。

リストにインスタンスを挿入するという少し複雑なことをしていますが、ここも実験的に使っています。また標準出力も、「○○を手に入れた!」というフォーマットの文字列を用意しておいて、その手に入れたアイテムを「○○」に代入するという様なスマートな実装も可能かもしれません。

ぜひ、どんどんコードを改善させてみましょう!

最後にEventクラスやMapItemクラスを使用していますので、ファイルの上部でインポート文を追加させれば、Playerクラスの完成です。完成形のコードは以下です。

from event import Event
from text import Text
from map import MapItem


class Player:

    DEFAULT_INITIAL_HP = 200
    DEFAULT_INITIAL_MP = 5
    DEFAULT_INITIAL_POWER = 10
    DEFAULT_INITIAL_DEFENSE = 0
    DEFAULT_INITIAL_EXP = 0
    DEFAULT_INITIAL_LEVEL = 1
    DEFAULT_INITIAL_ITEM_LIST = []

    ADD_MAX_HP = 20
    ADD_POWER = 5
    ADD_DEFENCE = 1
    ADD_LEVEL = 1

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value

    @property
    def max_hp(self):
        return self._max_hp

    @max_hp.setter
    def max_hp(self, value):
        self._max_hp = value

    @property
    def hp(self):
        return self._hp

    @hp.setter
    def hp(self, value):
        self._hp = value

    @property
    def max_mp(self):
        return self._max_mp

    @max_mp.setter
    def max_mp(self, value):
        self._max_mp = value

    @property
    def mp(self):
        return self._mp

    @mp.setter
    def mp(self, value):
        self._mp = value

    @property
    def power(self):
        return self._power

    @power.setter
    def power(self, value):
        self._power = value

    @property
    def defense(self):
        return self._defense

    @defense.setter
    def defense(self, value):
        self._defense = value

    @property
    def exp(self):
        return self._exp

    @exp.setter
    def exp(self, value):
        self._exp = value

    @property
    def level(self):
        return self._level

    @level.setter
    def level(self, value):
        self._level = value

    @property
    def item_list(self):
        return self._item_list

    @item_list.setter
    def item_list(self, value):
        self._item_list = value

    def __init__(self, name: str):
        self._name = name
        self._max_hp = self.DEFAULT_INITIAL_HP
        self._hp = self.DEFAULT_INITIAL_HP
        self._max_mp = self.DEFAULT_INITIAL_MP
        self._mp = self.DEFAULT_INITIAL_MP
        self._power = self.DEFAULT_INITIAL_POWER
        self._defense = self.DEFAULT_INITIAL_DEFENSE
        self._exp = self.DEFAULT_INITIAL_EXP
        self._level = self.DEFAULT_INITIAL_LEVEL
        self._item_list = self.DEFAULT_INITIAL_ITEM_LIST

    def level_up(self):
        """レベルアップ
        """
        self.max_hp += self.ADD_MAX_HP
        self.power += self.ADD_POWER
        self.defense += self.ADD_DEFENCE
        self.level += self.ADD_LEVEL

    def get_item(self, field: str):
        """アイテム一覧にフィールドのアイテム(MapItemインスタンス)を詰める

        Args:
            field (str): フィールドのアイテム
        """
        # 剣の場合
        if field == MapItem.WEAPON.value:
            self.power += 30
            print(Text.MES_GET_WEAPON)
            Event.input()

        # 盾の場合
        if field == MapItem.SIELD.value:
            self.defense += 10
            print(Text.MES_GET_SIELD)
            Event.input()

        # 薬の場合
        if field == MapItem.HERBS.value:
            print(Text.MES_GET_HERBS)
            Event.input()

        # アイテム一覧に詰める
        self.item_list.append(MapItem(field))

最後の追い込みの場面で、MapItemクラスのインスタンスを生成してからリストに追加してselfに保持するという、一行にまとめるとかなり紛らわしい記述になりましたが、これまでの記事の内容を理解することができ、基礎力が十分に身についていれば、それほど難しいとは感じないと思います。もしも難しいと感じた場合でも、焦らず1つずつわからないところを読み解いてみましょう。

self、item_list、append、MapItem、インスタンス生成、field、どれがわからない部分でしょうか?逆に言うと「わかっている部分」もたくさんあるはずです。1つ1つ丁寧に読み進めることが重要です。

main関数をさらに記述する

ここで一旦、Processクラスや、Textクラスを完成させたことを踏まえて、簡単に記述していたmain関数の処理をよりRPGらしく修正していきたいと思います。

前回までのmain関数は以下です。

from map import Map
from player import Player


def main():
    # タイトルを表示
    input('=== Game Start! ===')

    # プレイヤー名の入力
    print('プレイヤー名を入力してください。')
    player_name = input()
    player = Player(player_name)
    print('ようこそ、{}さん!'.format(player.name ))

    # マップのインスタンス生成
    map = Map()

    # ループ処理
    while True:

        # マップを表示
        map.show()

        # キー入力受付
        key = input('キーを入力してください')

        # マップ移動
        map.move(key)

        # Qキーの場合、ループ終了
        if key == 'q':
            break

    # モンスターとバトル
    input('スライムがあらわれた!')

    # バトルに勝利
    input('スライムをたおした!')

    # バトルに勝利
    input('=== Game Clear! ===')


if __name__ == '__main__':
    main()

こうして見てみると、残りの必要な大きな処理は「バトル」だということがわかります。「ゴール判定」や「アイテムを使用する」など小さい処理はたくさんありますが、MonsterクラスとButtleクラスを作成すれば、全てのクラスは出そろう事になりそうです。

さて、話を戻してmain関数の処理ですが、最初の導入部分を少しリッチにしてみましょう。「タイトル表示」と「プレイヤー名の入力」を修正します。修正したコードは以下です。インポート文なども完成形をイメージして先に全て記述しておきます。

from buttle import Buttle
from event import Event
from map import Map, MapItem
from monster import Monster
from player import Player
from process import Process
from text import Text


def main():
    # タイトルの表示
    Event.clear()
    Process.show_title()
    Event.input()

    # プレイヤー名の入力
    player_name = ''
    while not player_name:
        Event.clear()
        player_name = Process.input_player_name()
        player_name = \
            Process.confirm_input_player_name(player_name)
注意

最初のインポート文でエラーが出ているかもしれませんが、MonsterクラスとButtleクラスがまだ作成されていないためです。テストする時は一旦削除するかコメントアウトしておきましょう。

コードの記述量に大きな変化は起きていない様に見えますが、Process.show_title()やProcess.input_player_name()、Process.confirm_input_player_name()で、それなりに処理を行っていますので、具体的に何をしているのかわからなくなってしまった場合は、それぞれ見返してみましょう、「タイトルを表示する」や「プレイヤー名を入力する」という大まかな流れはそのままだという事も注目して頂きたいところです。

これは、例えば本格的なゲームを作成しようと考えた場合でも、「タイトルを表示する」処理や「プレイヤー名の入力する」処理が必要な場合でも同じような作業を行う大まかなイメージを持つことができれば、その作業量が変化しているだけということです。今回はProcessクラスを用いて処理の拡張を行いましたが、もっとアニメーション的に自動で文字列が標準出力するような機能が欲しいという場合は標準ライブラリのsleep関数を使ってみたりしても良いと思います。

物語のプロローグを記述する

せっかくRPGを作成しているので、それなりにストーリも重視したいところです。

といっても、今回の実装はプレイヤーの名前を入力した後に「ようこそ、〇〇さん!」という様な、よくある初心者向けのコーディングを長文で置き換えたものです。しかし、簡単なコーディングだけでも発想力次第ではとても面白いゲームにすることも可能だと考えていますので、ぜひ色々な標準出力を実装してみてください。

先ほどのコードの下にプロローグを標準出力する処理を追加します。コード内容は以下です。

    # テキストを表示
    Event.clear()
    print(Text.MES_GAME_MISSION.format(player_name))
    Event.input()

ここので標準出力はTextクラスに予め、フォーマットされる想定の文字列を用意しておき、その穴埋めの部分にplayer_nameをフォーマット関数で当てはめるという処理になっています。これは「ようこそ、〇〇さん」という文を作っておいて「〇〇」にフォーマット文でplayer_nameを当てはめているということです。

そしてplyaer_nameを「test」として実行すると、以下のように標準出力されます。「ゴール条件」を示したり、「バトル」があることを示したり、「早くゴールを目指した方が良い」というヒントを示したりしています。

少しはRPGらしくなってきたでしょうか?

python_rpg20

ちなみに「早めにゴールを目指す」事をお薦めしているのは、プレイヤーにカウンターを保持させた事が関係しています。このカウンターはエンカウントさせる仕組みとして使用していると以前説明しましたが、それだけではなく「数値が大きくなればなるほど、強いモンスターと遭遇するようになる」という処理を追加する予定です。

モンスターが強くなるというのは、RPGあるあるかと思いますが、強いモンスターと弱いモンスターのすみわけが難しいと感じます。

この住み分けの実装は、様々な方法が思い浮かびます。フィールドの位置情報によって、出現するモンスターが変化したり、物語の進行度合いによって変化したり、特定のイベントをクリアすることによって変化したりと様々です。今回のフィールドは2次元配列を使用しているので、その配列にモンスターの出現する情報を持たせることは可能です。そちらの実装も試してみるのも良いかもしれません。

メインループとアイテム一覧表示ループ

プレイヤーのインスタンスとマップのインスタンスを作成した後、メインループに入ります。

一気にコードを記述しますが、肝心なのは大まかにコードを把握することです。まず、メインループがあります。その中には「キー入力」、「キー判定」、「エンカウント判定」、「ゲームクリア判定」があるイメージです。

「キー入力」と「キー判定」はMapクラスの移動処理で詳しく説明した通り、プレイヤーが入力したキーに応じて標準出力を行うわけですが、ここではさらに「Q・E・Z・X・C」キーを押した時の処理を追加しています。詳しくは後程説明します。

「エンカウント判定」がモンスターとのバトルを行うかどうか、を判定しTrueの場合は「バトル」に突入します。バトルはフィールドマップを表示しているループとはまったく違うスタイルとなりますので、また別のループ処理として扱います。

「ゲームクリア判定」は「ゴール」にたどり着いたか?を判定すると共に「ゲームオーバー」の判定も含んだ意味になります。これはゲームが終了する判定のようなイメージです。「HPがなくなった場合」と「Qキーからのゲーム終了を選択した場合」がゲーム終了となるシナリオです。

修正したコードは以下です。繰り返しになりますが、大まかな流れを読んでみてください。

    # プレイヤーとマップの作成
    player = Player(player_name)
    map = Map()

    # メインループ
    while True:
        map.field = ''
        map.show()
        player_key = Process.input_player_key()
        if not player_key:
            continue

        # ESCキーを押した場合
        if player_key == Process.ESC:
            if Event.confirmation():
                Event.clear()
                print(Text.GAME_OVER)
                break
            else:
                continue

        # ITEMキーを押した場合
        elif player_key == Process.ITEM:
            show_item_list(player)
            continue

        # HELPキーを押した場合
        elif player_key == Process.HELP:
            continue

        # STATUS木ーを押した場合
        elif player_key == Process.STATUS:
            Event.clear()
            print(Text.MES_HOW_TO_PLAY)
            Process.show_player_status(player)
            continue

        # DECISIONキーを押した場合
        elif player_key == Process.DECISION:
            continue

        # 移動キーを押した場合
        elif player_key == Process.UP or player_key == Process.DOWN or\
                player_key == Process.LEFT or player_key == Process.RIGHT:
            map.move(player_key)

        # その他のキーを押した場合
        else:
            continue

        # 移動先が、壁の場合
        if map.field == MapItem.BLOCK.value:
            print(Text.MES_CAN_NOT_MOVE)
            Event.input()
            continue

        # 移動先が、ゴールの場合
        if map.field == MapItem.GOAL.value:
            Event.clear()
            print(Text.GAME_CLEAR)
            break

        # 移動先が、空地の場合
        if map.field == MapItem.EMPTY.value:
            map.field = ''

        # 移動先が、アイテムの場合
        if map.field:
            player.get_item(map.field)

        # エンカウント判定
        if Event.is_encount(map.counter):
            monster = Monster(map.counter)
            Buttle().buttle(player, monster)

        # プレイヤーのHPが0以下になった場合
        if player.hp < 0:
            Event.clear()
            print(Text.GAME_OVER)
            break


def show_item_list(player: Player):
    """アイテム一覧を表示するループ
    """
    select_index = 0

    while True:
        Event.clear()
        print(Text.MES_HOW_TO_PLAY)
        print(Text.PLAYER_STATUS.format(
            player.name, player.hp, player.max_hp, player.mp, player.max_mp
        ))
        print(Text.ITEM_LIST_PREFIX)

        # アイテムがある場合
        if player.item_list:
            for index, item in enumerate(player.item_list):

                # 選択中のアイテムの場合
                if index == select_index:
                    print(Text.ICON_SELECTED + item.title)
                else:
                    print(Text.ICON_NOT_SELECTED + item.title)

        # アイテムがない場合
        else:
            print(Text.ITEM_LIST_NOTING)
            print(Text.ITEM_LIST_SUFFIX)
            Event.input()
            return
        print(Text.ITEM_LIST_SUFFIX)

        # キー入力待ち
        input_key = Process.input_player_key()

        # ITEMの場合、ESCの場合、
        if input_key == Process.ITEM or \
                input_key == Process.ESC:
            break

        # UPの場合
        elif input_key == Process.UP:
            select_index = select_index - 1 \
                if select_index > 0 else len(player.item_list) - 1

        # DOWNの場合
        elif input_key == Process.DOWN:
            select_index = select_index + 1 \
                if select_index < len(player.item_list) - 1 else 0

        # DECISIONの場合、未入力の場合
        elif input_key == Process.DECISION or \
                input_key == Process.EMPTY:
            item_object = player.item_list[select_index]
            print(Text.USE_ITEM_CONFIRM.format(item_object.description))
            answer = Event.input()

            # Yesの場合
            if Event.is_yes(answer):
                if item_object == MapItem.HERBS:
                    player.hp += 100
                    if player.hp > player.max_hp:
                        player.hp = player.max_hp
                    player.item_list.pop(select_index)
                    select_index = 0
                    print(Text.MES_USE_HERB)
                    Event.input()
                else:
                    print(Text.MES_USE_EQUIPMENT)
                    Event.input()

        else:
            break

全てを説明していると膨大な時間がかかってしまうので、注目して頂きたい箇所を抜粋して説明します。まずは、プレイヤーが取得したアイテムを一覧で表示する処理です。これは「E」キーが押された場合にアイテム一覧表示ループ処理へと移行します。メインループで以下のように記述されています。

        # ITEMキーを押した場合
        elif player_key == Process.ITEM:
            show_item_list(player)
            continue

これからもわかるようにItemキーである「E」キーが押された場合はshow_item_list(player)という処理が行われます。この処理を見ていきましょう。83行目からがこのメソッドを定義している部分となります。

114行目でもキー入力待ちをしていることがわかると思います。アイテムをそれなりに回収した後に「E」キーを押してアイテム一覧を表示した画面が以下です。

アイテムを回収後、アイテム一覧を表示した図

ここでは決定キーと上・下キーを使うことで、アイテムを使用することができるようになっています。

86行目のコードで現在選択されているアイテムのインデックスを初期化します。リストのインデックスは一番目が0でしたので0で初期化しているイメージです。

select_index = 0

これはアイテム一覧の中に3つのアイテムがある場合は、0番目、1番目、2番目というインデックスが与えられるので、そのインデックスを上・下のキーで加算・減算することで[*]のマークを移動させています。122行目からのコードです。以下にも再度示します。

        # UPの場合
        elif input_key == Process.UP:
            select_index = select_index - 1 \
                if select_index > 0 else len(player.item_list) - 1

        # DOWNの場合
        elif input_key == Process.DOWN:
            select_index = select_index + 1 \
                if select_index < len(player.item_list) - 1 else 0

        # DECISIONの場合、未入力の場合
        elif input_key == Process.DECISION or \
                input_key == Process.EMPTY:
            item_object = player.item_list[select_index]
            print(Text.USE_ITEM_CONFIRM.format(item_object.description))
            answer = Event.input()

UPキーとDOWNキーが入力された場合は、三項演算子で代入する値を決定しています。UPの場合で説明すると「もしも選択中インデックスが0よりも大きい場合、選択中インデックスから1引いた値を代入、そうでなければアイテムの総数から1引いた値を代入」という処理です。

初期値が0なので、0の場合はアイテムの総数から1引いた数、これはリストの末尾のインデックスを指していますので、一番最後のアイテムを選択中にするということになります。一番上を選択中の時にUPキーを押したら一番下にカーソルが移動するイメージです。2番目、3番目にカーソルがある場合は、1つ上に移動するだけです。

最後に「DECISIONの場合、未入力の場合」は選択中のアイテムを「本当に使いますか?」という確認を行う処理に移行します。そこで「Yes」の場合はアイテムを使用するという流れです。「未入力の場合」でもアイテムを使用する理由は、その方がEnterが持つ「決定」のイメージが感覚的にしっくり来たからです。わざわざ「X」キーを入力する手間が省けてストレスなくプレイできるかなと思いました。

モンスターとのエンカウント処理

そして、プレイヤーの移動や、アイテム一覧確認、などの処理をこえた後、71行目からモンスターのエンカウント処理が始まります。

        # エンカウント判定
        if Event.is_encount(map.counter):
            monster = Monster(map.counter)
            Buttle().buttle(player, monster)

Eventクラスのis_encountメソッドでTrueであれば、モンスタークラスからインスタンスを生成し、プレイヤーとモンスターのバトルが始まるという流れです。

先ほど説明した通り、MonsterクラスのコンストラクタにはMapインスタンスのcounterを渡しています。これは、マップを歩いた回数が多ければ多いほど強いモンスターを生成する処理のためです。そして歩数に応じたモンスターが生成された後に、Buttleクラスのbuttleメソッドが呼び出されます。引数にplayerとmonsterが渡され、バトル画面に遷移するようなイメージです。

ということで、少しながくなりましたので、それぞれのクラス・バトルの実装はまた次回に説明します。

まとめ

今回はPlayerクラスを完成させ、main関数も完成させました。後はMonsterクラスとButtleクラスを作成し、バトルを実装させれば全て完成です。

大変お疲れ様でした。長文を最後までお読みいただきありがとうございました。

今回も重要な処理の説明だけになりましたが、もしも「実装についてさらに詳しく聞きたい」あるいは「一緒にゲームを拡張してみたい」というような想いをお持ちであれば「Discordサーバ」に参加頂くか「お問い合わせフォーム」などからご連絡頂けると幸いです。

私自身コードをもっとよくしていきたいという想いはあるものの、なかなか先に進めていない状況ですので、様々な意見や情報を取り入れて行きたいと考えていますので、お気軽にご連絡ください。

いよいよ実装する処理も少なくなってきましたが、次回もまた皆さんにお会いできる事を楽しみにしております。

以下のDiscordサーバでも質問を受け付けています。お気軽に声をかけて頂けると幸いです!

この記事を書いた人

小幡 知弘

1990年茨城県神栖市生まれ
2013年大阪芸術大学卒業
Python×Webエンジニア