Pythonのリストを使って簡単なコマンド式RPG(仮)を作る。ver0.0.1

目次

Python勉強して1ヶ月

Pythonを勉強し始めて一月、今回はその復習も兼ねて簡単なコマンド式RPG「PYRPG(仮)」を作ってみた。

と言っても今回ご紹介するコードはまだコマンドバトルの基礎的な部分のみでロールプレイング要素は皆無。強いて言うなら「勇者」という文字列くらい。若干タイトル詐欺感はあるがまぁ(仮)なのでいいでしょう。

ちなみに私が一番好きなコマンドRPGはFF9だ。(正確に言うと”コマンド”じゃないんだけどまぁ細かいことは気にしない、気にしない。


このプログラムを作ってみて分かったのは自分の定着力の悪さ。アウトプットしてみると痛いほどわかる。

そんなヤツが作ったので、以下に紹介しているコードは記事にしてよいものか迷うくらいの代物。もちろんPEP8(Pythonの書き方ルール)なんて知らないし、Pythonらしいシンプルで美しいコードとは全く対極に位置するものだ。

まともな仕様書なんてものは書かず(書けず)バイキングレストランの様に思いつきで加えたり減らしたり、エラー対処に追われつづけ、取りあえず動くを第一に考えて作ってできたのがこれ。削れる部分もとってもあるのだろうしもっと効率的な冴えたやり方もとってもあるのだと思う。自分でも見返してみて改善点の多さに頭を抱えため息で窒息しそうな状態だ。

要するに、もっと早く書いておけばよかったのだが「このコードから学べる事はなにもない」ということ。だからここまで読んでくれた皆さんにはすみません。

特に「Python ゲーム作り方」等のキーワードでググり、このページを見ている方ならこれ以上時間を無駄にしないためにもブラウザバック推奨。

もし、チラ見ぐらいはしてやるって方がいるのなら、それはとっても感謝感激なのだけれど「学ぶ」というのは今ここに置いていってください

コマンド式RPG「PYRPG(仮)」

import random


p_life = []
p_magi = []
p_actp = []

e_life = []
e_magi = []
e_actp = []

dp = 2
ap = 2

vic = r"""
V      V  III   CCCC  TTTTT  OOO   RRRR   Y   Y
 V    V    I   C        T   O   O  R   R   Y Y
  V  V     I   C        T   O   O  RRRR     Y
   V      III   C        T   O   O  R   R    Y
   V        I    CCCC    T    OOO   R   R    Y
"""

gam = r'''
  GGGGG   AAAAA  M     M  EEEEE  OOOOO  V       V  EEEEE  RRRRR
 G       A     A MM   MM  E      O     O  V     V   E      R    R
 G  GGG  AAAAAAA M M M M  EEEE   O     O   V   V    EEEE   RRRRR
 G    G  A     A M  M  M  E      O     O    V V     E      R  R
  GGGGG  A     A M     M  EEEEE  OOOOO      V      EEEEE  R   R
'''
enemy_art = """
       _.-- ,.--.
     .'   .'    /
    /    /     /
   /    /     /
  /    /     /
 /    /_.-._ /
(       _  _`\\
 `-.`.____`\\\\ \\
    //      `\\`\\
   //        `\\`\\
  ||    __    `\\`\\
  \\_\\   \\_\\_ (c))
  (           )
   \\          /
    `-._____.-'
"""



class character(object):
    def __init__(self, name):
        self.name = name


class Player(character):
    def __init__(self, name='hero'):
        super().__init__(name)

    def attack(self):
        global dp
        global ap
        damage = min(dp, len(e_life))
        if len(p_actp) >= ap:
            print(f'{self.name}たたかう')
            for i in range(ap):
                p_actp.pop(0)
            for i in range(damage):
                e_life.pop(0)
        else:
            print('ACTPが足りない!!')
            pass

    def action(self):
        print(f'[][][][]{self.name}のターン[][][][]')
        print("""
たたかう:1
まほう :2
アイテム:3
リミット:4
        """)
        a = int(input('コマンド>>>'))
        if a == 1:
            self.attack()
        elif a == 2:
            print('つくってない')
            pass
        elif a == 3:
            print('つくってない')
            pass
        elif a == 2:
            print('つくってない')
            pass
        else:
            print('つくってない')
            pass


class Enemy(character):
    def __init__(self, name='demo'):
        super().__init__(name)

    def attack(self):
        global dp
        global ap
        damage = min(dp, len(p_life))
        if len(e_actp) >= ap:
            print(f'{self.name}たたかう')
            for i in range(ap):
                e_actp.pop(0)
            for i in range(damage):
                p_life.pop(0)
        else:
            print('NOT_ACTPが足りない!!')
            pass

    def miss(self):
        if len(e_actp) >= ap:
            print(f'{self.name}ミス!!')
            for i in range(ap):
                e_actp.pop(0)
        else:
            print(f'{self.name}ミス!!!!')
            pass

        pass

    def action(self):
        print(f'[][][][]{self.name}のターン[][][][]')
        anum = random.random()
        if anum >= 0.5:
            self.attack()
        else:
            self.miss()


def ui():
    print(enemy_art)
    print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
    print(f'LIFE{p_life}')
    print(f'MAGI{p_magi}')
    print(f'ACTP{p_actp}')
    print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')


def Rac():
    p_actp.append(0)
    e_actp.append(0)


def start_battle():
    for i in range(10):
        p_life.append(0)
    for i in range(0):
        p_magi.append(0)
    for i in range(3):
        p_actp.append(0)
    for i in range(5):
        e_life.append(0)
    for i in range(0):
        e_magi.append(0)
    for i in range(3):
        e_actp.append(0)


def battle():
    start_battle()
    player = Player('勇者A')
    enemy = Enemy('ファルシ=ラビット')
    turn = random.random()

    if turn >= 0.5:
        ui()
        while True:
            if not p_life:
                print(gam)
                break
            if not e_life:
                print(vic)
                break
            player.action()
            enemy.action()
            ui()
            Rac()


    else:
        ui()
        while True:
            if not p_life:
                print(gam)
                break
            if not e_life:
                print(vic)
                break
            enemy.action()
            ui()
            player.action()
            Rac()


def run():

    print(r"""
    PPPPPPPPPPPPPPPP   YYYYYYY       YYYYYYY   RRRRRRRRRRRRRRRRR   PPPPPPPPPPPPPPPPP   GG@yaneuraGGG
P::::::::::::::::P  Y:::::Y       Y:::::Y   R::::::::::::::::R  P::::::::::::::::P G:::::::::::::::::G
P::::::PPPPPP:::::P Y:::::Y       Y:::::Y   R::::::RRRRRR:::::R P::::::PPPPPP:::::PG::::::PPPPPP:::::G
PP:::::P     P:::::PY::::::Y     Y::::::Y   RR:::::R     R:::::RPP:::::P     P:::::PG:::::P     P:::::G
  P::::P     P:::::PYYY:::::Y   Y:::::YYY     R::::R     R:::::R  P::::P     P:::::PG:::::P     P:::::G
  P::::P     P:::::P   Y:::::Y Y:::::Y        R::::R     R:::::R  P::::P     P:::::PG:::::PPPPPP:::::G
  P::::PPPPPP:::::P     Y:::::Y:::::Y         R::::RRRRRR:::::R   P::::PPPPPP:::::P G::::::::::::::::G
  P:::::::::::::PP       Y:::::::::Y          R::ver.0.0.1::RR    P:::::::::::::PP  G::::::::::::::::G
  P::::PPPPPPPPP          Y:::::::Y           R::::RRRRRR:::::R   P::::PPPPPPPPP    G::::::GGGGGGGGG
  P::::P                  Y:::::Y            R::::R     R:::::R  P::::P            G:::::G
  P::::P                  Y:::::Y            R::::R     R:::::R  P::::P            G:::::G
  P::::P                 Y:::::Y             R::::R     R:::::R  P::::P            G:::::G    GGGGGGGGGG
PP::::::PP               Y:::::Y           RR:::::R     R:::::RPP::::::PP          G::::::GGGGGG::::G
P::::::::P            YYYY:::::YYYY        R::::::R     R:::::RP::::::::P           GG:::::::::::::G
P::::::::P            Y:::::::::::Y        R::::::R     R:::::RP::::::::P             GGG::::::GGG:::G
PPPPPPPPPP            YYYYYYYYYYYYY        RRRRRRRR     RRRRRRRPPPPPPPPP                GGGGGG   GGGG   

    """)
    print('====================================================================================================')
    battle()


run()

仕様の解説

PYRPG(仮)の大まかな流れ

汚い字で申し訳ないが簡単なゲームの概要をまとめてみた。

ちなみに、ステータス管理に数字ではなくリストを用いているのはリストや辞書等のデータ構造系分野が身についていないなと常々思っていたから。

各コード解説

では、以上を踏まえて各コードの解説をしていく。

初めに書いた通り初心者の初心者による出来損ないのコードなので得るものはなにもないよ

基礎部分

import random

enemyの行動やターンの割り振りに乱数を使ってるのでrandomライブラリをインポート。

p_life = []
p_magi = []
p_actp = []

e_life = []
e_magi = []
e_actp = []

dp = 2
ap = 2

#GAMEOVERとVICTORYと敵のアスキーアート

空のリストは、PlayerとEnemyそれぞれのlife(体力)値、magi(魔力)値、acts(アクションポイント)値を表し、後々要素として”0”が挿入(append)される。

dpとapはそれぞれ

dp:ダメージを与える回数=ダメージ量

ap:アクションポイントを消費する回数=アクションポイントの消費量

を表わす。”回数”という表現を使っている理由は後ほど

3種類のアスキーアートはChatGPTにて生成した。
動物なのか機械なのか良くわからん敵のデザインは彼曰く「失神するほど恐ろしいモンスター」らしい

キャラクターのアクション部分

class character(object):
    def __init__(self,name):
        self.name = name

PlayerとEnemyの骨組みである characterクラス。

class Player(character):
    def __init__(self,name):
        super().__init__(name)



    def attack(self):
        global dp
        global ap
        damage = min(dp, len(e_life))
        if len(p_actp) >= ap:
            print(f'{self.name}たたかう')
            for i in range(ap):
                p_actp.pop(0)
            for i in range(damage):
                e_life.pop(0)
        else:
            print('ACTPが足りない!!')
            pass


    def action(self):
        print(f'[][][][]{self.name}のターン[][][][]')
        print("""
        たたかう:1
        まほう:2
        アイテム:3
        リミット:4
        """)
        a = int(input('コマンド>>>'))
        if a == 1:
            self.attack()
        elif a == 2:
            print('覚えてない!!')
            pass
        elif a == 3:
            print('持ってない!!')
            pass
        elif a == 2:
            print('痛いからヤダ!')
            pass
        else:
            print('どんくさい!!')
            pass

characterクラスを継承したPlayerクラス。

attack関数は文字通り相手にダメージを与える「たたかう」コマンド。

「たたかう」コマンドはアクションポイントダメージポイントという2つの構造で成り立っている。

アクションポイント

if len(p_actp) >= ap:
(略)
for i in range(ap):
p_life.pop(0)
else:
print(‘ACTPが足りない!!’)
pass

PYRPG(仮)で各コマンドを行うにはアクションポイントを消費しなければならない。
モンハンで言えばスタミナ値、Slay the Spireで言えばエナジーといえばイメージしやすいだろう。

アクションポイントである”0”は「actpリスト」に格納されている。今回の「たたかう」コマンドを行うにはアクションポイントが2個必要になるのでp_life.pop(0)でリストから”0”を2つ取り出す。行動に必要なアクションポイントが無ければ行動することは当然出来ない。


まぁ思いつきで導入したものなので現在のコードではあまり意味が無く、ただ行動を制限する不愉快な概念になってしまっている。うまく調理できれば戦略性というものを生み出せると思うんだけど、いかせん、私は「レベルを上げて圧倒的に勝利する」がRPGを進めるうえでの信条である脳筋、つまり「戦略」というのは私にとって縁遠い存在なため近いうちに消滅するかもしれない。。。(FFタクティクスは序盤で詰んでる。

ダメージポイント

damage = min(dp, len(e_life))
(略)
for i in range(damage):
e_life.pop(0)

PYRPGのダメージ概念は数字をマイナスするのでなく、「lifeリスト」から要素を決められた回数(forループ)分取り出す(pop)事によって行われる。


例えば、今回のコードでは上の方で取り出す回数(dp)を”2”と指定したので「相手のlifeリスト」から2個要素を取り出す=2ダメージ。


また、取り出すライフ数より相手のライフ数が少なかった場合(例:ライフを2個取り出したいのに1個しか無い)はエラーが発生するので、それを防ぐためdamage = min(dp, len(e_life))で「ライフ数とdp、どちらか少ない方を引数としてforループに渡す」という処理を行っている。

action関数はコマンドを一括管理する関数なんだけど「たたかう」コマンドしか作ってないので、現在はただUIを表示するだけの関数である。

class Enemy(character):
    def __init__(self, name):
        super().__init__(name)

    def attack(self):
        global dp
        global ap
        damage = min(dp, len(p_life))
        if len(e_actp) >= ap:
            print(f'{self.name}たたかう')
            for i in range(ap):
                e_actp.pop(0)
            for i in range(damage):
                p_life.pop(0)
        else:
            print('NOT_ACTPが足りない!!')
            pass
    def miss(self):
        if len(e_actp) >= ap:
            print(f'{self.name}ミス!!')
            for i in range(ap):
                e_actp.pop(0)
        else:
            print(f'{self.name}ミス!!!!')
            pass

        pass

    def action(self):
        print(f'[][][][]{self.name}のターン[][][][]')
        anum = random.random()
        if anum >= 0.5:
            self.attack()
        else:
            self.miss()

敵であるEnemyクラスもPlayerクラスと基本は一緒。ただmiss関数と乱数で「攻撃をmissする」という概念を導入している。

その他(UI、ステータス等)

def ui():
    print(enemy_art)
    print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
    print(f'敵のLIFE{e_life}')
    print(f'敵のMAGI{e_magi}')
    print(f'敵のACTP{e_actp}')
    print('=====')
    print(f'LIFE{p_life}')
    print(f'MAGI{p_magi}')
    print(f'ACTP{p_actp}')
    print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')

def Rac():
    p_actp.append(0)
    e_actp.append(0)

def start_battle():
    for i in range(10):
        p_life.append(0)
    for i in range(0):
        p_magi.append(0)
    for i in range(3):
        p_actp.append(0)
    for i in range(5):
        e_life.append(0)
    for i in range(0):
        e_magi.append(0)
    for i in range(3):
        e_actp.append(0)

ui関数は文字通りユーザインターフェイスを表示する関数。

Rac関数は下のバトルシステムでも触れるが、ターン終了時にアクションポイントを1つ増やす関数。

status関数は一番上の各リスト(life,、magi、actp)にポイント”0″を挿入する関数。(magiはMP的立ち位置、「まほう」コマンドがない現状ではいらないので何も入れてない)

バトルシステム部分

def battle():
    status()
    player = Player('勇者A')
    enemy = Enemy('ファルシ')
    turn = random.random()

    if turn >= 0.5:
        ui()
        while True:
            if not p_life:
                print(gam)
                break
            if not e_life:
                print(vic)
                break
            player.action()
            enemy.action()
            ui()
            Rac()


    else:
        ui()
        while True:
            if not p_life:
                print(gam)
                break
            if not e_life:
                print(vic)
                break
            enemy.action()
            ui()
            player.action()
            Rac()

このプログラムの心臓部分であるbattle関数

  1. PlayerクラスとEnemyクラスを呼び出してそれぞれplayerとenemyに格納。今回はnameの引数に’勇者A’と’ファルシ’を渡している。
  2. 先行or後攻は乱数で決定。
  3. whileループでターンを切り替え。
  4. 最後にRac()でatcpリストに”0”を追加、これがないと行動できなくなってしまう。
  5. ループの最初の部分で各々のライフ数を確認し「lifeリスト」の要素数を確認し空であれば「VICTORY!」「GAMEOVER」の判定を下す。

うん、とってもぐちゃぐちゃ、動くには動くが無駄が多い。要改善

実行部分

def run():

    print(r"""
    PPPPPPPPPPPPPPPP   YYYYYYY       YYYYYYY   RRRRRRRRRRRRRRRRR   PPPPPPPPPPPPPPPPP   GG@yaneuraGGG
P::::::::::::::::P  Y:::::Y       Y:::::Y   R::::::::::::::::R  P::::::::::::::::P G:::::::::::::::::G
P::::::PPPPPP:::::P Y:::::Y       Y:::::Y   R::::::RRRRRR:::::R P::::::PPPPPP:::::PG::::::PPPPPP:::::G
PP:::::P     P:::::PY::::::Y     Y::::::Y   RR:::::R     R:::::RPP:::::P     P:::::PG:::::P     P:::::G
  P::::P     P:::::PYYY:::::Y   Y:::::YYY     R::::R     R:::::R  P::::P     P:::::PG:::::P     P:::::G
  P::::P     P:::::P   Y:::::Y Y:::::Y        R::::R     R:::::R  P::::P     P:::::PG:::::PPPPPP:::::G
  P::::PPPPPP:::::P     Y:::::Y:::::Y         R::::RRRRRR:::::R   P::::PPPPPP:::::P G::::::::::::::::G
  P:::::::::::::PP       Y:::::::::Y          R::ver.0.0.1::RR    P:::::::::::::PP  G::::::::::::::::G
  P::::PPPPPPPPP          Y:::::::Y           R::::RRRRRR:::::R   P::::PPPPPPPPP    G::::::GGGGGGGGG
  P::::P                  Y:::::Y            R::::R     R:::::R  P::::P            G:::::G
  P::::P                  Y:::::Y            R::::R     R:::::R  P::::P            G:::::G
  P::::P                 Y:::::Y             R::::R     R:::::R  P::::P            G:::::G    GGGGGGGGGG
PP::::::PP               Y:::::Y           RR:::::R     R:::::RPP::::::PP          G::::::GGGGGG::::G
P::::::::P            YYYY:::::YYYY        R::::::R     R:::::RP::::::::P           GG:::::::::::::G
P::::::::P            Y:::::::::::Y        R::::::R     R:::::RP::::::::P             GGG::::::GGG:::G
PPPPPPPPPP            YYYYYYYYYYYYY        RRRRRRRR     RRRRRRRPPPPPPPPP                GGGGGG   GGGG   

    """)
    print('====================================================================================================')
    battle()

run()実行時に「PYRPG」というAAを表示させるようにした。映画とかゲームでも初めに迫力のあるシーンを持ってくると良いらしいのでそれに倣って。

これがあるだけで少しは中身がごまかせる。

今後の課題

改めて自分のコードを見直して見てうんざりしている。

書き留めるのが面倒になるくらい直したい箇所はあるんだけど、とりあえず当面の目標としてこの3つに取り組みたいと思う。

  • 整理整頓
  • 「まほう」、「アイテム」、「リミット」コマンドの実装
  • おもしろさ

整理整頓
「洗練を突きつめるとシンプルになる」と彼のレオナルド・ダ・ヴィンチは言ったそうだが、PYRPG(仮)に必要なのはまさにこの「洗練」だろう。
ここまで読んでいたみなさんならおわかりだとは思うが、現在のPYRPG(仮)はPYthonRolePlayingGameの略でなくPainfullYRolePlayingGameの略といったほうがしっくり来る。例えばTemplateを利用して文字列を格納するとか色々やりようはあるはずだ。

各種コマンドの実装
PYRPG(仮)は「たたかう」コマンドしかアクションがない。つまりプレイ中は1キーを連打するだけ。。。これはゲームではなく作業である。だから他のコマンドの実装は欠かせない。選択肢があるというだけでまだ マ シ に見えるから

おもしろさ
ゲームとは楽しむためにあるものである。故に「おもしろさ」が欠けてしまったらそれはもうゲームとは言えない。コマンド追加もそうだが他にも色々追加するものがあるだろう?でなければそれはコマンド R  P  G とは言えないのだから。

目次