質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.37%

ブラックジャックのコードのリファクタリング (関数かクラスか)

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 2,045

score 107

Qiitaの記事を見て、ブラックジャックのコードをPythonで書いてみました。
基本的な機能は簡単にコードにできたのですが、機能拡張を図る上でどのように設計すればいいかで悩んでいます。

# コードの概略
class Card
class Deck
class Player:
  def deal()
class Dealer(Player):
  def deal()

def blackjack():
  # initialize deck, player, dealer
  # player.deal()
  # dealer.deal()
  # showdown

def confirm()
def main():
  while True:
    blackjack()
    confirm()


player vs dealerの1対1の想定で、
ゲームのinitial deal -> player action -> dealer action -> showdownの4段階のうち、initial dealshowdownの2つがblackjack()内で、残りの2つがそれぞれのクラス内でdeal()として実装されています。
また、player.deal()でもう1枚カードを引くかをプレイヤーに確認するために、クラス外のconfirm()を呼び出しています。

ルールの拡張やプレイヤーの追加、各クラスを他のトランプゲームに使い回すことを考えると、ブラックジャック特有の機能をPlayerDealerから分けるべきだと考えています。
ここで悩んでいるのが、blackjack()の関数内に全ての処理を記述すべきなのか、新たにBlackjackクラスを用意して、その中で細かく処理を分けたほうがいいのかという点です。

関数内の処理は30-40行以内に収まるのが良いとも聞いたことがあります。
処理が複雑な場合、どういった基準で関数やクラス、あるいは他の形での実装を選ぶべきなのか、アドバイスをいただければ幸いです。

# コード全文
# blackjack.py
from random import shuffle


class Card:
    suits = ["H", "D", "S", "C"]
    numbers = [i for i in range(1, 14)]
    cards_converter = {**{i: i for i in range(2, 11)}, **{1: "A", 11: "J", 12: "Q", 13: "K"}}

    def __init__(self, suit, number):
        self.suit = suit
        self.number = number

    def __repr__(self):
        return f"{self.suit}-{self.cards_converter[self.number]}"


class Deck(list):
    def __init__(self):
        super().__init__(Card(suit, num) for suit in Card.suits for num in Card.numbers)
        shuffle(self)


class Player:
    def __init__(self, deck: "Deck"):
        self.deck = deck
        self.hands = []
        self._initial_deal()

    def __repr__(self):
        return f"{self.__class__.__name__}: {self.score} pts / {self.hands}"

    def _initial_deal(self) -> None:
        for _ in range(2):
            self.draw()
        print(self)

    def draw(self) -> int:
        card = self.deck.pop()
        self.hands.append(card)
        return self.score

    def deal(self) -> bool:
        while confirm("Keep drawing a card?"):
            score = self.draw()
            print(self)
            if score == 21:
                print("<You win!>\n")
                return False
            elif score > 21:
                print("<You lose...>\n")
                return False
        print_line()
        return True

    @property
    def score(self) -> int:
        # count royal cards as 10
        score = sum(min(card.number, 10) for card in self.hands)
        # exception of ace
        if score <= 11 and 1 in {card.number for card in self.hands}:
            score += 10
        return score


class Dealer(Player):
    def _initial_deal(self) -> None:
        self.draw()
        print(self)
        self.draw()  # hide the second hand

    def deal(self) -> bool:
        while self.score < 17:
            self.draw()
        if self.score > 21:
            print(self)
            print("<You win!>\n")  # dealer loses, and player wins
            return False
        print_line()
        return True


def blackjack() -> None:
    # initialize / initial deal
    deck = Deck()
    player = Player(deck)
    if player.score == 21:
        print("<You win!>\n")
        return
    dealer = Dealer(deck)

    # showdown
    if player.deal() and dealer.deal():
        print(player, dealer, sep="\n")
        if player.score == dealer.score:
            print("<Draw>\n")
        elif player.score > dealer.score:
            print("<You win!>\n")
        else:
            print("<You lose...>\n")
    print_line()


def confirm(message: str) -> bool:
    return input(f"{message} [y/n]").lower()[0] == "y"


def print_line(length: int = 55) -> None:
    print("-" * length)


def main():
    blackjack()
    while confirm("Play again?"):
        print_line()
        blackjack()


if __name__ == "__main__":
    main()
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • shiracamus

    2019/12/06 15:21 編集

    deal するから dealer。
    Player は play します。deal しません。

    キャンセル

  • solzard

    2019/12/06 16:55

    その通りですね。
    手元のコードでは修正します。

    キャンセル

回答 1

checkベストアンサー

+1

力作ですね。
しかし良く練られている一方で、少し読みづらさを感じるところもあります。

  • ゲーム進行のロジックとプレイヤーの行動が混じってしまっていること
    ・ 特に勝敗判定の分散が気になる

以上の点については、solzardさんも別の形で既にお気付きになっているとおりです。

ルールの拡張やプレイヤーの追加、各クラスを他のトランプゲームに使い回すことを考えると、ブラックジャック特有の機能をPlayer, Dealerから分けるべきだと考えています。

プレイヤーの分離を考える

プレイヤーをゲームそのものから分離するにあたって、プレイヤーの役割について再考しましょう。
トランプゲームにおけるプレイヤーの共通の振る舞いって何でしょう?

私は次のように考えました。

  • 特定の判断に従って、与えられたデッキからカードを引く
  • 特定の判断に従って、手札を場に出す/捨てる
  • 特定の判断に従って、自らの得点を申告する

『特定の判断』を分離できれば、プレイヤークラスを他のゲームにも流用できそうです。

class Player:
    ...

    def draw(self, deck, draw_strategy):
        if draw_strategy(self.hand):
            card = deck.pop()
            self.hands.append(card)
            return True   # Enumの方が良いかも

        return False          

いわゆるStrategyパターンです。
次のような呼び方をすればユーザの判断を仰ぐことができます。

def interactive_strategy(hand):
    print(hand)
    return input('引く? [y/n]')[0] == 'y'

user.draw(deck, interactive_strategy)

コンピュータもこんなふうに作れます。

def random_strategy(hand):
    return random.random() < 0.5
def score_based_strategy(hand):
    score = スコアを計算
    return score < 17

ただし外部の関数からユーザの手札を覗き見ることになりますので、
それが気になるようであればブラックジャック専用のプレイヤークラスを作れば良いです。

def BlackJackPlayer(Player):
    def draw(self, deck):
        super().draw(self, deck, self.xxx_strategy)

    @staticmethod
    def xxx_strategy(hand):
        ...

勝敗判定の分離を考える

スコア計算はゲームに強く依存しますので、特にプレイヤークラスから引き離すべきです。
先と同じように、計算用の関数オブジェクトを外部から受け取れば良いでしょう。

class Player:
    ...

    def report_score(self, scorer):
        return scorer(self.hand)

プレイヤーは得点を申告するだけで、勝敗判定は呼び出し側の責任で行います。
毎度スコア計算方法をしていするのが面倒であれば、インスタンス化の際に一緒に渡してしまいます。

呼び出し側

次のように書けそうです。

def scorer(hand):
    ...

def player_strategy(hand):
    ...

def dealer_strategy(hand):
    ...

...

#
#
user = Player()
dealer = Player()
deck = Deck()

player_to_strategy = {
    user: user_strategy, dealer: dealer_strategy
}

#
# 
for _ in range(2):
    user.draw(deck, lambda _: True)

dealer.draw(deck, lambda _: True)
プレイヤーとディーラーのカードを公開
dealer.draw(deck, lambda _: True)

#
#
players = [user, dealer]    # プレイ順
while True:
    for player in players:
        did_draw = player.draw(
            deck, player_to_strategy[player]
        )

        score = player.report_score(scorer)
        if 22 < score:
            バーストの発生を通知
            break
    else:
        勝敗判定
        if どっちかが勝った or 引き分け:
            break

        continue

     勝敗の表示
     break

想像で書いているので、実際にはもうちょっと調整が必要かと思います。
例えば賭けの実装は考慮していません。


ここで悩んでいるのが、blackjack()の関数内に全ての処理を記述すべきなのか、新たにBlackjackクラスを用意して、その中で細かく処理を分けたほうがいいのかという点です。

個人的には関数で充分かと思います。
継承したり、インスタンスを複数作ったりする必要性を感じません。

ただし別のゲームも実装するならば、モジュールは分けた方が良いです。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/12/06 10:31

    詳しい回答、本当にありがとうございます。
    Stragety patternという単語を調べてみて、デザインパターンというのがまさに今自分が必要としている知識だとわかりました。
    最近スクリプトを超える長さのコードをちらほら書くようになり、可読性の悪さに悩んでいたので、さっそくいくつか本を買ってこようと思います。

    > プレイヤーは得点を申告するだけで、勝敗判定は呼び出し側の責任で行います。
    この部分をどう分ければ良いかが一番の課題だったので、このアドバイスはありがたいです。

    キャンセル

  • 2019/12/06 10:43

    出版から四半世紀を既に迎えており、GoFのデザインパターンは前時代的だと考える方も多いようです。
    書籍を買うのであれば、できるだけ新しいものを選んだ方が良いです。案外GoFに批判的な書籍の方が参考になるかもしれません。

    Quora:一時期プログラミングのデザインパターンというものが大流行しましたが、現在ではどのように評価されているのでしょうか?
    https://jp.quora.com/%E4%B8%80%E6%99%82%E6%9C%9F%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%81%AE%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3%E3%81%A8%E3%81%84%E3%81%86%E3%82%82

    キャンセル

  • 2019/12/06 11:05

    なるほど、鵜呑みにはしないように用心しながら勉強してみます

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.37%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る