LoginSignup
2
3

More than 3 years have passed since last update.

Pythonで作る じゃんけんゲーム いただいたコード消化編

Last updated at Posted at 2019-09-17

じゃんけんゲームのコード解析をするぞ

前回の記事@shiracamus さんから
コメントにて参考のコードを頂きました。ありがとうございます^^

自分の拙い最低限のコードとは違って実務的というか、なんか感動しました!
今後、より規模の大きいコードを書く際に必要な要素が詰まってそうです♪
ということでまだまだ初学者である私にはわからなかったところを重点的に
自分なりに解読していきたいと思います。
結構長くなります。

いただいたコードは以下の通り

import random


class Hand:

    def __init__(self, shape, stronger_than):
        self.shape = shape
        self.stronger_than = stronger_than

    def __repr__(self):
        return self.shape

    def is_stronger_than(self, enemy):
        return self.stronger_than == enemy.shape


ROCK = Hand("グー", "チョキ")
SCESSORS = Hand("チョキ", "パー")
PAPER = Hand("パー", "グー")


class Record:

    def __init__(self):
        self.win = 0
        self.lose = 0
        self.draw = 0

    def __str__(self):
        return f"{self.win}{self.lose}{self.draw}引き分け"


class Player:

    def __init__(self, name):
        self.name = name
        self.hand = ROCK  # 最初はグー
        self.record = Record()

    def show_hand(self):
        print(f"{self.name}の手は{self.hand}")

    def show_record(self):
        print(f"戦績: {self.record}")

    def is_stronger_than(self, enemy):
        return self.hand.is_stronger_than(enemy.hand)

    def judge(self, enemy):
        if self.is_stronger_than(enemy):  # わての方があんたより強いねん
            print(f"{self.name}の勝ち!")
            self.record.win += 1
        elif enemy.is_stronger_than(self):  #  あんたの方がわてより強いだと!?
            print(f"{self.name}の負け><")  # しゃーない、わての負けや
            self.record.lose += 1
        else:
            print("あいこ~")
            self.record.draw += 1


class Human(Player):
    HANDS = {0: ROCK, 1: SCESSORS, 2: PAPER}

    def select_hand(self):
        while True:
            try:
                hand = int(input(f"{self.name}の手は? {self.HANDS} "))
                if hand in self.HANDS:
                    self.hand = self.HANDS[hand]
                    return
            except ValueError:
                pass
            print("無効な手です!0~2の数字で入力してください!")


class Computer(Player):
    HANDS = ROCK, SCESSORS, PAPER

    def select_hand(self):
        self.hand = random.choice(self.HANDS)


def main():
    you = Human("あなた")
    com = Computer("コンピューター")

    times = 0
    one_more_play = True
    while one_more_play:
        times += 1
        print(f"ジャンケン{times}回目")

        com.select_hand()
        you.select_hand()
        com.show_hand()
        you.judge(com)
        you.show_record()

        one_more_play = input("もう一度やる? 1:やる! 0:もうええわ > ") == '1'

if __name__ == "__main__":
    main()

コードの解読

実行される順番を追っていきながら、気になった点、わからなかった点を掘り下げつつ進行します。

class Hand:

    def __init__(self, shape, stronger_than):
        self.shape = shape
        self.stronger_than = stronger_than

    def __repr__(self):
        return self.shape

    def is_stronger_than(self, enemy):
        return self.stronger_than == enemy.shape


ROCK = Hand("グー", "チョキ")
SCESSORS = Hand("チョキ", "パー")
PAPER = Hand("パー", "グー")

ここが一番最初のブロックです。じゃんけんの手である、グー、チョキ、パーの手とその手で勝てる相手をセットで記録します。
Handクラスの__init__(初期化メソッド)でそれが行われます。
ROCK ,SCESSORS ,PAPER の変数にグー、チョキ、パーの組み合せ(インスタンス)が保存されます。

次に

if __name__ == "__main__":
    main()

最下部のここでmain()の呼び出しを行っており、ここから本格的な処理がスタートします。

if __name__ == "__main__": について 私はしらなかったので調べました!

__name__には
・importされたときはモジュール名
・直接実行した場合には __main__
が自動的に入るようになっているそうです。
importした時点でそのモジュールは実行されるため、
この判定を行うことで、importなのか直接実行なのかを判定し、
モジュールをimportしただけで強制的にmain()が実行されるのを防止できます。
当じゃんけんゲームをjanken.pyとしたとき
import jankenとしただけで起動されるのと
明示的に janken.main()で実行されるのとではかなり違いますね。
少し規模が大きなコードを書く際には必須になりそうです。

def main():
    you = Human("あなた")
    com = Computer("コンピューター")

main()が走りはじめました~
早速 Humanクラスでyouインスタンス、Computerクラスでcomが作られます。

Humanクラスでは

class Human(Player):
    HANDS = {0: ROCK, 1: SCESSORS, 2: PAPER}

    def select_hand(self):
        while True:
            try:
                hand = int(input(f"{self.name}の手は? {self.HANDS} "))
                if hand in self.HANDS:
                    self.hand = self.HANDS[hand]
                    return
            except ValueError:
                pass
            print("無効な手です!0~2の数字で入力してください!")

となっていますが、インスタンス作成時は
HANDS = {0: ROCK, 1: SCESSORS, 2: PAPER}
のみが実行されます。
input で選択させるために取り出しやすいよう辞書型にいれています。
(リストでも挙動的には問題でないですが、あとでいい感じにそのまま文字列として出力されています。ここでは辞書型が正解ですね~)
HumanクラスもComputerクラスもPlayerクラスの子クラスになっています。
共通するパートをPlayerクラスにまとめてどっちからも利用可能にされています。
ここで最初、私は躓いたのですが、
・子クラスにコンストラクタが存在せず、親クラスにコンストラクタがある場合、
インスタンス化される際には親クラスのコンストラクタが実行されます。
・さらに親クラスの機能(メソッド)やインスタンス変数は子クラスに継承されます。
この2点が分かっていなかったため、解読がスムーズに行きませんでした。。。
クラスと継承についてしっかりと理解しておかないとだめですね~

なので Humanクラスに渡された値 'あなた' は

class Player:

    def __init__(self, name):
        self.name = name
        self.hand = ROCK  # 最初はグー
        self.record = Record()

Playerクラスのコンストラクタの処理を通じてself.nameに代入されることになります。
ここでついでに初期値として グーの手と、 戦績がリセットされます。
インスタンス変数をさらにインスタンス化の対象とすることも可能で、
self.record = Record() の通り、Recordクラスでインスタンス化されています。
これも知らない部分でした。
一旦Recordクラスに飛んで~

class Record:

    def __init__(self):
        self.win = 0
        self.lose = 0
        self.draw = 0

コンストラクタでwin lose draw を初期化しています。

そしてこれでyouインスタンス化が一旦完了します。

com = Computer("コンピューター")
お次はComputerクラスでcomをインスタンス化します。

class Computer(Player):
    HANDS = ROCK, SCESSORS, PAPER

    def select_hand(self):
        self.hand = random.choice(self.HANDS)

まず、HANDS(タプル型)でROCK,SCESSORS,PAPERを定義します。
今回はランダムでチョイスするのでリストかタプルということでしょうか。
タプルのほうが変更が効かないので安全、また実行もわずかながら早いそうです。
先程のHumanと同じく Player クラスを継承しています。
なのでcom に対しても上記Humanクラスと同様に初期化が行われます。

main()
    times = 0
    one_more_play = True
    while one_more_play:
        times += 1
        print(f"ジャンケン{times}回目")

times = 0 で 対戦回数を初期化
one_more_play = True でループ処理をスタート可能にします。
while one_more_play で one_more_play がTrueの間はループされ続けます。

one_more_play = input("もう一度やる? 1:やる! 0:もうええわ > ") == '1'

main()最後で inputにて 1 なら True 、 それ以外は False と条件分岐させて
ループの行く末をきめています。

main()
com.select_hand()
#↓↓↓
class Computer(Player):
    HANDS = ROCK, SCESSORS, PAPER

    def select_hand(self):
        self.hand = random.choice(self.HANDS)

com.select_hand()
comインスタンスからComputerクラスのselect_handを実行します。
先程インスタンス化した際に定義されたHANDS(タプル型)からランダムでチョイスをし、self.hand(com.hand) に代入します。
これでコンピューターの手は決まります。

main()
you.select_hand()
#↓↓↓
class Human(Player):
    HANDS = {0: ROCK, 1: SCESSORS, 2: PAPER}

    def select_hand(self):
        while True:
            try:
                hand = int(input(f"{self.name}の手は? {self.HANDS} "))
                if hand in self.HANDS:
                    self.hand = self.HANDS[hand]
                    return
            except ValueError:
                pass
            print("無効な手です!0~2の数字で入力してください!")

you.select_hand()

youインスタンスからHumanクラスのselect_handを実行します。
先程インスタンス化した際に定義されたHANDS(辞書型)
それをinputから数字を入力させて 手を決めさせます。
f"{self.name}の手は? {self.HANDS} "
f~は ~.formatの省略形みたいです。スマートな書き方ですね。

実行結果
あなたの手は[0: グー, 1: チョキ, 2: パー]

と表示され、入力を受け付けますー
あれ? ROCK,SCESSORS,PAPER がそれぞれ変化していますね~

Hand
def __repr__(self):
        return self.shape

__repr__はオブジェクトを参照されたら自動的に実行みたいな感じらしいです。
これも初見でした。
self.HANDS のようにオブジェクトを返す参照のときにメソッドが実行されて
return self.shape を返す
なのでROCK,SCESSORS,PAPER がそれぞれがshapeの内容に変化されているのです。
すごい~

if hand in self.HANDS:
で input された値がhand に入っているので その値が HANDSの中にあるか調べ
あればreturnでループを抜ける
入ってなければ 例外処理に飛んでエラー文言を出してループを続行しています。

main()
com.show_hand()
#↓↓↓
class Player:
#途中省略
    def show_hand(self):
        print(f"{self.name}の手は{self.hand}")

comの手を表示しています。

main()
you.judge(com)
#↓↓↓
class Player:
#途中省略
    def judge(self, enemy):
        if self.is_stronger_than(enemy):  # わての方があんたより強いねん
            print(f"{self.name}の勝ち!")
            self.record.win += 1
        elif enemy.is_stronger_than(self):  #  あんたの方がわてより強いだと!?
            print(f"{self.name}の負け><")  # しゃーない、わての負けや
            self.record.lose += 1
        else:
            print("あいこ~")
            self.record.draw += 1

you.judge(com)
親クラス Player のjudgeを実行します。enemy に comを渡しています。

if self.is_stronger_than(enemy):
ここで is_stronger_thanを実行

Player
def is_stronger_than(self, enemy):
        return self.hand.is_stronger_than(enemy.hand)

ここがすこしややこしかったですが、脳内変換していきます
self.hand.is_stronger_than(enemy.hand)
のself.hand は先程 you.select_hand()で選択した手(実際はオブジェクト)に置き換えることができますね。0(グー)を選択していたらROCK.is_stronger_than(enemy.hand)と置き換えると・・・あ、ってなりますね。ついでにもしcom.select_hand()の結果がチョキならROCK.is_stronger_than(SCESSORS)と置き換えることができますね
Handクラスのis_stronger_thanにSCESSORSを渡しているのです。

Hand
def is_stronger_than(self, enemy):
    return self.stronger_than == enemy.shape

self.stronger_than は さっきの例ですと ROCK.stornger_thanなので 最初に定義した
チョキになりますね。
enemy.shapeSCESSORS.shapeなのでチョキになります
== で比較演算してるので return には True or False を返します。
今回はTrueですね
結果 if self.is_stronger_than(enemy): はTrueになるので 勝ちになります。

ここがFalseの場合、次はcomを主体に考えて同様の比較を行っています。
なるほど、勝敗の判定が確かに行われておりました!

最後にyou.show_record()で結果を表示しています。

Player
    def show_record(self):
        print(f"戦績: {self.record}")

ここでprint(f"戦績: {self.record}")しただけで
Recordクラスの__str__が実行されています。
__str__はPrintなど文字列参照された際に自動的に実行される特殊なメソッドです。

Record
def __str__(self):
        return f"{self.win}{self.lose}{self.draw}引き分け"

が実行されて

実行結果
戦績: 1 0 0引き分け

のように表示されます。
これですべての処理が実行され、無事じゃんけんがつつがなく終えることができました!

学べたこと まとめ

__repr__:オブジェクト参照で自動起動するメソッド
__str__:文字列参照で自動起動するメソッド
__name__:import時と実行時の判定に使える !重要!
・親クラス子クラスの継承やコンストラクタの挙動について
・親クラス子クラスの使い方の一例
・f"{foo}" は"{}".format(foo)のスマートな書き方
・無限ループを使ったシンプルなコーディングと例外対策

人のコードを読むのは大変勉強になります。

自分自身だけの考えでは視野が狭くなりがちですね。
人が書いたコードを読むことで、新しい発見・知識の供給であったり、考え方の違いを感じれたり、
いろいろと得るものがたくさんあります。特に私のような初学者は書くことも大事ですけど、
それと同じくらい読んで行かないと駄目だな~と感じました。

最後に

この素晴らしい機会を与えていただいた@shiracamusさま、本当にありがとうございました。
またお手隙の際はご助力・ご指導いただけますと幸いです。
また、拙い文章で大変読みずらい記事をここまで読んでいただけた方にも感謝いたします!
間違いや勘違いしている部分等あると思います。遠慮なくご指摘いただけますと幸いです。
ありがとうございました。

2
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3