🐷

【後編】初学者がRubyで「トランプの戦争」を作ってみた【備忘録】

に公開

前編では、ルールから役割を分解してCard/Player/Dealerを作ったものの

  • 「Playerがプレイヤーっぽくない(手札がない)」
  • 「Dealerが全部やることになりそう」

という違和感が出てきました。

ただ、前編の通り「動くこと」を最優先にしていたので、まずは作り切った結果を備忘録として残します。
そして、作り終えた今の視点で、「何を改善したくなったか」を整理しておきます。

4. まず「Playerをプレイヤーにする」ために手札を持たせた

前編の時点では Player は名前しか持っていませんでした。
これだと「カードを出す」も「勝ったカードを受け取る」もできないので、最初に手札を持てるようにしました。

class Player
  attr_reader :name, :hand_cards
  attr_writer :hand_cards

  def initialize(name = 1)
    @name = "プレイヤー#{name}"
    @hand_cards = []
  end

  def put_down_card
    @hand_cards.pop
  end
end

この時点でようやく「プレイヤーっぽい」状態になりました。
なお、attr_writer :hand_cardsを付けていますが、結局この実装ではhand_cards=は使っていません。
また、外部から手札を差し替えられる形にもなるので、ここは不要でした。

5. 「Dealerが全部やる」前提でとにかくゲームを回すことにした

クラス構成はCard/Player/Dealerにすると決めていたので、それ以外の処理はDealerに寄せて進めました。
理由は単純で、現時点での理解だと「手続きとしての進行」が一番書けそうだったからです。
そのため、次の処理をDealerに集約してゴールを目指しました。

  • カード生成
  • 配布
  • 1ターンの進行
  • 勝敗判定
  • 勝者への回収
  • 終了判定

6. 山札(52枚)は「カード1枚=配列」で作る

カードがないとゲームが始まらないので、まずは山札を作りました。
事前に作ったCardインスタンスが持つranksuitを使って、[suit, rank]の配列としてカードを表現しています。

def make_cards(card)
  card.suit.each do |s|
    card.rank.each do |r|
      @playing_cards << [s, r]
    end
  end
end

これで「作る→配る→出す」が一気に書きやすくなりました。
一方でCardクラスが「カード1枚」ではなくカードを作る材料っぽくなってしまいました。

7. 配布は % を使って交互に配る

配る処理は、人数に応じて順番に配れるようにしました。

def deal_cards
  players = @players_list.size

  @playing_cards.shuffle.each_with_index do |card, num|
    @players_list[num % players].hand_cards << card
  end
end

ただ、ここでhand_cardsをDealerが直接操作してしまっています。
結果として「Playerが手札を管理する」という役割が外に漏れてしまい、責務が崩れた形になりました。

8. 引き分けルールは “簡略化”

前編では「引き分けならもう一度カードを出す」と書きましたが、
今回の実装では、引き分け分をused_cardにプールして、次の勝負に持ち越す形にしています。
結果として引き分け後もゲームは続き、どこかの勝負で決着がついたタイミングで、プール分もまとめて回収されます。

if set_card.strength[played[0][:rank].to_sym] == set_card.strength[played[1][:rank].to_sym]
  puts "引き分けです。"
  @used_card.concat(battlefield)
  battlefield.clear
end

9. 動かして分かったこと:違和感が“確信”に変わった

最後まで回したことで、前編の違和感が「やっぱりここがしんどい」に変わりました。

改善ポイント1 Dealerが重すぎる

game_startにロジックも表示も詰まって、修正するのが怖い状態になります。
勝ち/負け/引き分けで似たコードが増えて、読み返すだけで疲れます。

game_start
def game_start(set_card)
    puts "戦争を開始します。"
    puts "カードが配られました。"

    battlefield = []

    while (@players_list[0].hand_cards.empty? || @players_list[1].hand_cards.empty?) == false do

      puts "戦争!"
      played = []

      @players_list.each_with_index do |player, num|
        card = players_list[num].put_down_card
        battlefield << card
        suit, rank = card
        played << { player: player.name, hand_cards: player.hand_cards, suit: suit, rank: rank }
      end

      if set_card.strength[(played[0][:rank].to_sym)] == set_card.strength[(played[1][:rank].to_sym)]
        puts "#{played[0][:player]}のカードは#{played[0][:suit]}#{played[0][:rank]}です。"
        puts "#{played[1][:player]}のカードは#{played[1][:suit]}#{played[1][:rank]}です。"
        @used_card.concat(battlefield)
        battlefield.clear
        puts "引き分けです。"
      elsif set_card.strength[(played[0][:rank].to_sym)] > set_card.strength[(played[1][:rank].to_sym)]
        puts "#{played[0][:player]}のカードは#{played[0][:suit]}#{played[0][:rank]}です。"
        puts "#{played[1][:player]}のカードは#{played[1][:suit]}#{played[1][:rank]}です。"
        puts "#{played[0][:player]}の勝ちです。"
        puts "#{played[0][:player]}はカードを#{battlefield.size + @used_card.size}枚もらいました。"

        played[0][:hand_cards].concat(@used_card).concat(battlefield).shuffle!
        @used_card.clear
        battlefield.clear
      else
        puts "#{played[0][:player]}のカードは#{played[0][:suit]}#{played[0][:rank]}です。"
        puts "#{played[1][:player]}のカードは#{played[1][:suit]}#{played[1][:rank]}です。"
        puts "#{played[1][:player]}の勝ちです。"
        puts "#{played[1][:player]}はカードを#{battlefield.size + @used_card.size}枚もらいました。"
        played[1][:hand_cards].concat(@used_card).concat(battlefield).shuffle!
        @used_card.clear
        battlefield.clear
      end
    end
    if @players_list[0].hand_cards.empty?
      puts "#{@players_list[0].name}の手札がなくなりました。"
      puts "#{@players_list[1].name}の手札は#{@players_list[1].hand_cards.size}枚です。#{@players_list[0].name}の手札は0枚です。"
      puts "#{@players_list[1].name}が1位、#{@players_list[0].name}が2位です。"
    elsif @players_list[1].hand_cards.empty?
      puts "#{@players_list[1].name}の手札がなくなりました。"
      puts "#{@players_list[0].name}の手札は#{@players_list[0].hand_cards.size}枚です。#{@players_list[1].name}の手札は0枚です。"
      puts "#{@players_list[0].name}が1位、#{@players_list[1].name}が2位です。"
    end
    puts "戦争を終了します。"
  end

→ 次はメソッドを分けて、機能を細かく作ります。

改善ポイント2 Playerの手札を外から触りすぎ

配布も回収もhand_cards <<concatを外からやっています。
「Playerが手札を管理する」という役割が崩れてしまいました。

→ 次はPlayerクラスで手札を操作するようにし、Dealerは手札配列に触れないようにします。

改善ポイント3 Cardが“カード1枚”じゃなく、材料の集まりになっている

Cardにrank/suit/strengthが集まり、カードは配列で扱っています。
今のルール範囲なら十分シンプルで扱いやすい一方、ルールを増やすならカード表現は見直しポイントになりそうです。

→ 次はCardを1枚オブジェクトとして切り出します。

まとめ 今回の成果は「動かしたから改善点が見えた」こと

設計もコードも全く綺麗ではありません。
でも、動くところまでやったことで

  • どこが重いか(Dealer)
  • どこが漏れてるか(Playerの手札)
  • どこが役割ズレしてるか(Card)

が初学者なりに見えるようになりました。

Discussion