ブラックジャックゲームを作ってみた[Ruby]

## はじめに
[プログラミング入門者からの卒業試験は『ブラックジャック』を開発すべし](https://qiita.com/hirossyi73/items/cf8648c31898216312e5)という記事に影響されて、Rubyで作ってみました！
参考にさせていただいた記事の「開発開始！」まで読み進めて作成したものとなります。

### 作成者のスペック
- 実務経験なし
- 学習歴 約3ヶ月
- Ruby, Railsを学習中(Javaもいつかやりたいな・・・)。

未熟な部分しかございませんが、よろしくお願いします！


## ルールの整理(実際には様々なルールが存在しますが、今回は以下のようにしました。)
- カードは52枚, 同じカードは存在しない。
- Aは1点として扱う。(得点に応じて、1か11にする例外処理が実装できませんでした。)
- プレイヤーがバーストした時点で即敗北。(ディーラーのターンは来ない。)
- スコアが同じであれば、引き分け(両者ブラックジャックの場合も)
- プレイヤーのドロー終了後、ディーラーのターン開始
- プレイヤーのみ、特定条件および一定確率でデスティニードロー(イカサマ)発生。 ・・・ オリジナル

## 工夫したところ
一定確率で確実にブラックジャックになるカードを引くことのできる特殊演出を実装しました。
ゲームとしては、崩壊していますが遊び心として大目にみてください。
### 発生条件
- プレイヤーの得点が21未満11以上
- 手札が3枚以上
- ランダムで生成される数字がif文の条件と一致。(10%の確率)

コードとしては、「Playerクラスのdestiny_drawメソッド」と「Deckクラスのdestiny_draw_cardメソッド」になります。特定条件の判定はblackjack.rbにて行なっています。

## blackjack.rb
blackjack.rbは主にゲームの進行を取り扱うところです。
run_gameが実行されるとゲームが始まり、プレイヤーとディーラーお互いの手札が配分され、プレイヤーのターン開始となります。
putsの空行が至る所にありますが、これは出力結果を見やすくするために出力しています。

```ruby:blackjack.rb
require './card'
require './deck'
require './player'

# ゲームの進行
def run_game
  # 初期手札2枚の準備,手札公開,得点表示
  deck = Deck.new
  deck.shuffle
  player = Player.new
  player.first_draw(deck)
  player.score_count
  dealer = Dealer.new
  dealer.first_draw(deck)
  dealer.score_count

  #ユーザーのターン
  while true do
    number = rand(11)
    if number == 3 && (player.instance_variable_get :@hands).length >= 3 && player.score_count >= 11 && player.score_count != 21
      puts "山札が輝きだした！\n山札を信じて引きますか？[Y/N]"
      puts "あなたの得点: #{player.score_count}"
      answer = gets.chomp.to_s
      if answer == "Y" || answer == "y"
        player.destiny_draw(deck, player)
        unless blackjack?(player)
          break
        end
      elsif answer == "N" || answer == "n"
        puts
        puts "さらにカードを引きますか？[Y/N]"
        puts "あなたの得点: #{player.score_count}"
        answer = gets.chomp.to_s
        puts "Answer: #{answer}"
        if answer == "Y" || answer == "y"
          player.draw(deck, player)
          unless burst?(player)
            exit
          end
          unless blackjack?(player)
            break
          end
        elsif answer == "N" || answer == "n"
          break
        else
          puts "無効な値です。もう一度入力してください。"
        end
      else
        puts "無効な値です。もう一度入力してください。"
      end
    else # 通常時の処理
      puts
      puts "さらにカードを引きますか？[Y/N]"
      puts "あなたの得点: #{player.score_count}"
      answer = gets.chomp.to_s
      puts "Answer: #{answer}"
      if answer == "Y" || answer == "y"
        player.draw(deck, player)
        unless burst?(player)
          exit
        end
        unless blackjack?(player)
          break
        end
      elsif answer == "N" || answer == "n"
        break
      else
        puts "無効な値です。もう一度入力してください。"
      end
    end
  end

#ディーラーのターン
  while true do
    if dealer.score_count < 17
      dealer.draw(deck, dealer)
    else
      break
    end
  end

  judge(player, dealer)

end

# 勝敗判定
def judge(player, dealer)
  player_score = player.score_count
  dealer_score = dealer.score_count
  puts
  puts "----- あなたの得点 -----"
  puts "#{player_score}"
  puts "--- ディーラーの得点 ---"
  puts "#{dealer_score}"
  puts
  if dealer.score_count == player_score
    puts "引き分け"
  elsif player_score == 21
    puts "ブラックジャック！\nあなたの勝ちです！"
  elsif dealer_score == 21
    puts "ディーラーのブラックジャック！\nあなたの負けです..."
  elsif dealer_score > 21
    puts "ディーラーはバーストしました。"
    puts "あなたの勝ちです！"
  elsif dealer_score > player_score
    puts "あなたの負けです..."
  elsif dealer_score < player_score
    puts "あなたの勝ちです！"
  end
end

# バーストしたか？
def burst?(player)
  if player.score_count <= 21
    return true
  else
    puts
    puts "あなたの得点: #{player.score_count}"
    puts "バーストしました。あなたの負けです。"
    return false
  end
end

# ブラックジャックかどうか
def blackjack?(player)
  if player.score_count == 21
    return false
  else
    return true
  end
end

puts "---------------------"
puts "ブラックジャックへようこそ！"
puts "---------------------"
puts
run_game
```

## Playerクラス
プレイヤークラスには手札を持たせており、手札に関係する処理を行うメソッドを定義しました。
ディーラーとプレイヤーで処理を分けています。

```ruby:player.rb
# プレイヤークラス
class Player

  def initialize
    @hands = []
  end

# 一枚引く
  def draw(deck, player)
    card = deck.draw_card
    @hands.push(card)
    puts
    puts "あなたが引いたカードは#{card.show}です"
    puts "----- あなたの手札 -----"
    @hands.each_with_index do |hand, i|
      puts "[ #{hand.show} ]"
    end
    puts "---------------------"
  end

# 得点計算
  def score_count
    score = 0
    @hands.each do |hand|
      score += hand.count
    end
    return score
  end

# 最初のドローおよび手札公開
  def first_draw(deck)
    2.times do
      card = deck.draw_card
      @hands.push(card)
    end

    puts "----- あなたの手札 -----"
    @hands.each_with_index do |hand, i|
      puts "#{i+1}枚目: #{hand.show}"
    end
    puts "---------------------"
  end

# デスティニードロー
  def destiny_draw(deck, player)
    card = deck.destiny_draw_card(player)
    @hands.push(card)
    puts
    puts "あなたが引いたカードは#{card.show}です"
    puts "----- あなたの手札 -----"
    @hands.each_with_index do |hand, i|
      puts "[ #{hand.show} ]"
    end
    puts "---------------------"
  end

end

# ディーラークラス
class Dealer

  def initialize
    @dealer_hands = []
  end

# 一枚引く
  def draw(deck, player)
    card = deck.draw_card
    @dealer_hands.push(card)
    puts "ディーラーはカードを引きました"
  end

# 得点計算
  def score_count
    score = 0
    @dealer_hands.each do |hand|
      score += hand.count
    end
    return score
  end

# 最初のドローおよび手札公開
  def first_draw(deck)
    2.times do
      card = deck.draw_card
      @dealer_hands.push(card)
    end

    puts "--- ディーラーの手札 ---"
    puts "1枚目: #{@dealer_hands.first.show}"
    puts "2枚目: 伏せられている"
    puts "---------------------"
  end

end
```

## Deckクラス
デッキクラスでは、山札を作成する役割を与えています。
Deck.newと同時にbuildメソッドが動き、52枚のカードを配列内に作成します。
その後、shuffleメソッドを動かすことで配列内の順番をランダムにします。

```ruby:deck.rb
# デッキクラス
class Deck
  def initialize
    @cards = []
    build
  end

# 山札作成
  def build
    for suit in ["♡", "♠", "♦︎", "♣︎"] do
      for number in ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"] do
        card = Card.new(suit, number)
        @cards << card
      end
    end
  end

# 山札シャッフル
  def shuffle
    cards_length =(@cards.count) - 1
    cards_length.step(1,-1) do |i|
      r = rand(i)
      @cards[i] , @cards[r] = @cards[r], @cards[i]
    end
  end

# 山札の上から一枚引く
  def draw_card
    @cards.pop
  end

# 得点が21になるようなカードを探して引く
  def destiny_draw_card(player)
    target = (21 - player.score_count)
    @cards.find { |card| (card.instance_variable_get :@number) == target.to_s }
  end
end
```

## Cardクラス
ここでは、カードを定義しています。カードにスートに関するパラメータと数字に関するパラメータを持たせることでトランプとして再現しています。countメソッドでは、J, Q, Kを点数計算時のみ「10」に置き換えています。

```ruby:card.rb
# カードクラス
class Card
  def initialize(suit, number)
    @suit = suit
    @number = number
  end

  def show
    return "#{@suit}  of #{@number}"
  end

# J,Q,Kの処理
  def count
    if @number == "J" || @number == "Q" || @number == "K"
      return @number = 10
    else
      return @number.to_i
    end
  end
end

```

## 実行結果
こちらが実行結果になります。
### 通常時
```
---------------------
ブラックジャックへようこそ！
---------------------

----- あなたの手札 -----
1枚目: ♦︎  of 6
2枚目: ♣︎  of 4
---------------------
--- ディーラーの手札 ---
1枚目: ♣︎  of K
2枚目: 伏せられている
---------------------

さらにカードを引きますか？[Y/N]
あなたの得点: 10
y
Answer: y

あなたが引いたカードは♦︎  of 8です
----- あなたの手札 -----
[ ♦︎  of 6 ]
[ ♣︎  of 4 ]
[ ♦︎  of 8 ]
---------------------

さらにカードを引きますか？[Y/N]
あなたの得点: 18
n
Answer: n
ディーラーはカードを引きました

----- あなたの得点 -----
18
--- ディーラーの得点 ---
24

ディーラーはバーストしました。
あなたの勝ちです！
```

### 特殊演出
「山札を信じて引きますか？」で「N」を選択すると、通常処理に戻り、カードを引くか再び聞くようにしています。
以下の例では、「Y」を選択しています。

```
---------------------
ブラックジャックへようこそ！
---------------------

----- あなたの手札 -----
1枚目: ♦︎  of 5
2枚目: ♣︎  of 3
---------------------
--- ディーラーの手札 ---
1枚目: ♡  of 4
2枚目: 伏せられている
---------------------

さらにカードを引きますか？[Y/N]
あなたの得点: 8
y
Answer: y

あなたが引いたカードは♠  of 3です
----- あなたの手札 -----
[ ♦︎  of 5 ]
[ ♣︎  of 3 ]
[ ♠  of 3 ]
---------------------
山札が輝きだした！
山札を信じて引きますか？[Y/N]
あなたの得点: 11
y   

あなたが引いたカードは♡  of 10です
----- あなたの手札 -----
[ ♦︎  of 5 ]
[ ♣︎  of 3 ]
[ ♠  of 3 ]
[ ♡  of 10 ]
---------------------
ディーラーはカードを引きました

----- あなたの得点 -----
21
--- ディーラーの得点 ---
22

ブラックジャック！
あなたの勝ちです！
```

## 終わりに
思った以上にコード量が増えすぎてしまい、まだまだ可読性を上げる訓練が必要だなと感じました。
変数名やクラス名の命名や処理のメソッド化など改善の余地はたくさんありますね...。
プログラミング入門者の卒業試験！とのことでしたが、個人的には留年判定かと思います。
しかし、形はどうであれ、0から試行錯誤を繰り返して作り上げることはとても楽しかったです。
実装する機能も細分化してどういった順番で書いていけば、スムーズに開発できるか考えることもいい訓練になりました！

もし、僕と同じような初心者の方がいたら、ぜひ挑戦してみてください！
今回作成したコードのサンプルは以下にあります。
[コードサンプル](https://github.com/kazuki3/blackjack)

最後までお読みいただきありがとうございました。