はじめに
この記事では、ブラックジャックゲームを作成しながら、Rubyのオブジェクト指向プログラミングについて学びます。
全体のコード
class Card
attr_reader :value, :suit # カードの値とスートの読み取り専用アクセサを定義
def initialize(value, suit) # Cardクラスの初期化。値とスートを引数に取る
@value = value # カードの値をインスタンス変数に設定
@suit = suit # カードのスートをインスタンス変数に設定
end
def to_s # カードの値とスートを文字列で返すメソッド
"#{suit}の#{value}"
end
def point # カードのポイントを計算するメソッド
case value
when 'A' then 11
when 'K', 'Q', 'J' then 10
else value.to_i
end
end
end
class Deck
attr_reader :cards # デッキ内のカードの読み取り専用アクセサを定義
def initialize # Deckクラスの初期化
@cards = build_deck # デッキを構築
end
def build_deck # 52枚のカードを含むデッキを構築してシャッフルするメソッド
suits = ["ハート", "ダイヤ", "クラブ", "スペード"] # 4種類のスートを表す配列
values = %w(A 2 3 4 5 6 7 8 9 10 J Q K) # 2から10までの数値およびA, K, Q, Jを表す配列
# スートと値のすべての組み合わせを生成し、それぞれの組み合わせに対してCardインスタンスを作成
# 最後に生成されたCardインスタンスの配列をシャッフルする
suits.product(values).map { |suit, value| Card.new(value, suit) }.shuffle
end
def draw # デッキから一枚のカードを引くメソッド
cards.pop
end
end
class Hand
attr_accessor :cards # 手札のカードのアクセサを定義
def initialize # Handクラスの初期化
@cards = []
end
def add_card(card) # 手札にカードを追加するメソッド
cards << card
end
def points # 手札の合計ポイントを計算するメソッド
aces = 0 # エースの数をカウントするための変数を初期化
# cards配列(手札)にあるすべてのカードに対して、合計ポイントを計算
total = cards.inject(0) do |sum, card|
aces += 1 if card.value == 'A' # カードがエースの場合、エースの数を1増やす
sum + card.point # 現在の合計ポイントにカードのポイントを加算
end
# エースの数だけループを回して、合計ポイントが21を超えている場合は10を引く
aces.times { total -= 10 if total > 21 }
total # 最終的な合計ポイントを返す
end
def busted? # 手札がバーストしているかどうかを判定するメソッド
points > 21
end
end
class Player
attr_reader :hand # プレイヤーの手札の読み取り専用アクセサを定義
def initialize # Playerクラスの初期化
@hand = Hand.new
end
def hit(card) # プレイヤーがカードをヒットするメソッド
hand.add_card(card)
end
end
class Dealer < Player # DealerクラスはPlayerクラスを継承
def show_one_card # ディーラーが一枚のカードを見せるメソッド
hand.cards.first
end
end
class Game
attr_reader :player, :dealer, :deck # プレイヤー、ディーラー、デッキの読み取り専用アクセサを定義
def initialize # Gameクラスの初期化
@player = Player.new
@dealer = Dealer.new
@deck = Deck.new
end
def deal_initial_cards # プレイヤーとディーラーに初期カードを配るメソッド
2.times do
player.hit(deck.draw)
dealer.hit(deck.draw)
end
end
def play # ゲームをプレイするメソッド
puts "ブラックジャックを開始します。"
deal_initial_cards
puts "あなたの引いたカードは#{player.hand.cards[0]}です。"
puts "あなたの引いたカードは#{player.hand.cards[1]}です。"
puts "ディーラーの引いたカードは#{dealer.show_one_card}です。"
puts "ディーラーの引いた2枚目のカードはわかりません。"
loop do
puts "あなたの現在の得点は#{player.hand.points}です。カードを引きますか?(Y/N)"
decision = gets.chomp.downcase
break if decision == "n"
if decision == "y"
card = deck.draw
player.hit(card)
puts "あなたの引いたカードは#{card}です。"
break if player.hand.busted?
end
end
if player.hand.busted?
puts "あなたの現在の得点は#{player.hand.points}です。"
puts "バーストしました、あなたの負けです..."
else
puts "ディーラーの引いた2枚目のカードは#{dealer.hand.cards[1]}でした。"
puts "ディーラーの現在の得点は#{dealer.hand.points}です。"
while dealer.hand.points < 17
card = deck.draw
dealer.hit(card)
puts "ディーラーの引いたカードは#{card}です。"
puts "ディーラーの現在の得点は#{dealer.hand.points}です。"
end
if dealer.hand.busted?
puts "ディーラーの得点は#{dealer.hand.points}です。あなたの勝ちです!"
elsif player.hand.points > dealer.hand.points
puts "あなたの得点は#{player.hand.points}です。"
puts "ディーラーの得点は#{dealer.hand.points}です。あなたの勝ちです!"
elsif player.hand.points < dealer.hand.points
puts "あなたの得点は#{player.hand.points}です。"
puts "ディーラーの得点は#{dealer.hand.points}です。あなたの負けです..."
else
puts "引き分けです。"
end
puts "ブラックジャックを終了します。"
end
end
end
Game.new.play
学ぶポイント
クラスとオブジェクト指向の概念
このゲームでは、Card, Deck, Hand, Player, Dealer, Gameといったクラスを定義しています。これらのクラスは、オブジェクト指向プログラミングにおいて、それぞれの役割や機能を持つオブジェクトを表現しています。例えば、Cardクラスはカードの情報を表現し、Deckクラスはカードのデッキを管理します。
class Card
...
end
class Deck
...
end
クラスの継承
Dealerクラスは、Playerクラスを継承しています。これにより、DealerクラスはPlayerクラスが持つすべてのメソッドやプロパティを引き継ぎ、共通の機能を再利用できます。継承を使うことで、コードの重複を避けることができ、保守性や拡張性を向上させることができます。
class Dealer < Player
...
end
カプセル化
オブジェクト指向プログラミングでは、データやメソッドをクラス内にカプセル化することで、外部から直接アクセスできないようにします。これにより、コードの安全性や保守性が向上します。attr_accessor
、attr_reader
、attr_writer
などのアクセサメソッドを使って、インスタンス変数にアクセスするためのメソッドを定義できます。例えば、Handクラスでは、cardsのアクセサを定義しています。
class Hand
attr_accessor :cards
...
end
ブロックを使った繰り返し処理
Rubyでは、eachやtimesなどの繰り返し処理を行うメソッドとブロックを組み合わせて使うことが一般的です。例えば、Handクラスのpointsメソッドでは、injectメソッドとブロックを使って、カードの合計ポイントを計算しています。
def points
...
total = cards.inject(0) do |sum, card|
...
end
...
end
配列の活用
Rubyの配列を活用して、コードをシンプルに保つことができます。例えば、Deckクラスのbuild_deckメソッドでは、suitsとvaluesという配列を用意し、productメソッドとmapメソッドを使って、52枚のカードを生成しています。
def build_deck
suits = ["ハート", "ダイヤ", "クラブ", "スペード"]
values = %w(A 2 3 4 5 6 7 8 9 10 J Q K)
suits.product(values).map { |suit, value| Card.new(value, suit) }.shuffle
end
条件分岐とループ処理
Rubyの条件分岐やループ処理を使って、ゲームのロジックを制御できます。例えば、Gameクラスのplayメソッドでは、loopを使ってユーザーの入力に応じてカードを引くか決め、条件分岐を使ってバーストや勝敗を判定しています。
def play
...
loop do
...
break if decision == "n"
...
end
if player.hand.busted?
...
else
...
if dealer.hand.busted?
...
elsif player.hand.points > dealer.hand.points
...
elsif player.hand.points < dealer.hand.points
...
else
...
end
end
end
メソッドチェーン
Rubyでは、メソッドチェーンを使って複数のメソッドを連結して呼び出すことができます。これにより、コードが簡潔で読みやすくなります。例えば、Deckクラスのbuild_deckメソッドでは、productメソッドとmapメソッドをチェーンしています。
def build_deck
...
suits.product(values).map { |suit, value| Card.new(value, suit) }.shuffle
end
以上のポイントを踏まえて、ブラックジャックゲームの実装を見ていきましょう。
ゲームの流れ
-
Game
クラスのインスタンスを作成し、play
メソッドを呼び出すことでゲームが開始されます。 - 初期カードをプレイヤーとディーラーに配ります。このとき、プレイヤーには2枚のカードが公開され、ディーラーには1枚のカードが公開されます。
- プレイヤーは、カードをヒットするかどうかを選択できます。カードを引くと、手札の合計ポイントが計算され、バーストしているかどうかが判定されます。
- プレイヤーがバーストした場合、ゲームは終了し、プレイヤーの負けとなります。
- プレイヤーがバーストしなかった場合、ディーラーが手番を実行します。ディーラーは、手札の合計ポイントが17以上になるまでカードを引き続けます。
- ディーラーがバーストした場合、ゲームは終了し、プレイヤーの勝ちとなります。
- 両者がバーストしていない場合、手札の合計ポイントが高い方が勝ちとなります。同点の場合は引き分けとなります。
クラス構成
ブラックジャックゲームでは、以下のクラスを使用します。
-
Card
: カードの値とスートを管理するクラス -
Deck
: デッキを管理するクラス -
Hand
: プレイヤーの手札を管理するクラス -
Player
: プレイヤーの振る舞いを管理するクラス -
Dealer
: ディーラーの振る舞いを管理するクラス(Player
を継承) -
Game
: ゲーム全体の流れを管理するクラス
Cardクラス
Card
クラスは、カードの値とスートを管理します。to_s
メソッドは、カードの情報を文字列で返すために使用されます。point
メソッドは、カードのポイントを計算します。
class Card
attr_reader :value, :suit # カードの値とスートの読み取り専用アクセサを定義
def initialize(value, suit) # Cardクラスの初期化。値とスートを引数に取る
@value = value # カードの値をインスタンス変数に設定
@suit = suit # カードのスートをインスタンス変数に設定
end
def to_s # カードの値とスートを文字列で返すメソッド
"#{suit}の#{value}"
end
def point # カードのポイントを計算するメソッド
case value
when 'A' then 11
when 'K', 'Q', 'J' then 10
else value.to_i
end
end
end
Deckクラス
Deck
クラスは、デッキを構築し、カードをシャッフルし、カードを引く処理を管理します。デッキは、build_deck
メソッドで生成され、draw
メソッドでカードを引きます。
class Deck
attr_reader :cards # デッキ内のカードの読み取り専用アクセサを定義
def initialize # Deckクラスの初期化
@cards = build_deck # デッキを構築
end
def build_deck # 52枚のカードを含むデッキを構築してシャッフルするメソッド
suits = ["ハート", "ダイヤ", "クラブ", "スペード"] # 4種類のスートを表す配列
values = %w(A 2 3 4 5 6 7 8 9 10 J Q K) # 2から10までの数値およびA, K, Q, Jを表す配列
# スートと値のすべての組み合わせを生成し、それぞれの組み合わせに対してCardインスタンスを作成
# 最後に生成されたCardインスタンスの配列をシャッフルする
suits.product(values).map { |suit, value| Card.new(value, suit) }.shuffle
end
def draw # デッキから一枚のカードを引くメソッド
cards.pop
end
end
Handクラス
Hand
クラスは、プレイヤーの手札を管理します。add_card
メソッドは、手札にカードを追加するために使用されます。points
メソッドは、手札の合計ポイントを計算します。また、busted?
メソッドは、手札がバーストしているかどうかを判定します。
class Hand
attr_accessor :cards # 手札のカードのアクセサを定義
def initialize # Handクラスの初期化
@cards = []
end
def add_card(card) # 手札にカードを追加するメソッド
cards << card
end
def points # 手札の合計ポイントを計算するメソッド
aces = 0 # エースの数をカウントするための変数を初期化
# cards配列(手札)にあるすべてのカードに対して、合計ポイントを計算
total = cards.inject(0) do |sum, card|
aces += 1 if card.value == 'A' # カードがエースの場合、エースの数を1増やす
sum + card.point # 現在の合計ポイントにカードのポイントを加算
end
# エースの数だけループを回して、合計ポイントが21を超えている場合は10を引く
aces.times { total -= 10 if total > 21 }
total # 最終的な合計ポイントを返す
end
def busted? # 手札がバーストしているかどうかを判定するメソッド
points > 21
end
end
Playerクラス
Player
クラスは、プレイヤーの振る舞いを管理します。プレイヤーがカードをヒットする処理は、hit
メソッドで実装されています。
class Player
attr_reader :hand # プレイヤーの手札の読み取り専用アクセサを定義
def initialize # Playerクラスの初期化
@hand = Hand.new
end
def hit(card) # プレイヤーがカードをヒットするメソッド
hand.add_card(card)
end
end
Dealerクラス
Dealer
クラスは、ディーラーの振る舞いを管理します。ディーラーが一枚のカードを見せる処理は、show_one_card
メソッドで実装されています。このクラスは、Player
クラスを継承しています。
class Dealer < Player # DealerクラスはPlayerクラスを継承
def show_one_card # ディーラーが一枚のカードを見せるメソッド
hand.cards.first
end
end
Gameクラス
Game
クラスは、ゲーム全体の流れを管理します。プレイヤー、ディーラー、デッキを初期化し、初期カードを配る処理します。
class Game
attr_reader :player, :dealer, :deck # プレイヤー、ディーラー、デッキの読み取り専用アクセサを定義
def initialize # Gameクラスの初期化
@player = Player.new
@dealer = Dealer.new
@deck = Deck.new
end
def deal_initial_cards # プレイヤーとディーラーに初期カードを配るメソッド
2.times do
player.hit(deck.draw)
dealer.hit(deck.draw)
end
end
def play # ゲームをプレイするメソッド
puts "ブラックジャックを開始します。"
deal_initial_cards
puts "あなたの引いたカードは#{player.hand.cards[0]}です。"
puts "あなたの引いたカードは#{player.hand.cards[1]}です。"
puts "ディーラーの引いたカードは#{dealer.show_one_card}です。"
puts "ディーラーの引いた2枚目のカードはわかりません。"
loop do
puts "あなたの現在の得点は#{player.hand.points}です。カードを引きますか?(Y/N)"
decision = gets.chomp.downcase
break if decision == "n"
if decision == "y"
card = deck.draw
player.hit(card)
puts "あなたの引いたカードは#{card}です。"
break if player.hand.busted?
end
end
if player.hand.busted?
puts "あなたの現在の得点は#{player.hand.points}です。"
puts "バーストしました、あなたの負けです..."
else
puts "ディーラーの引いた2枚目のカードは#{dealer.hand.cards[1]}でした。"
puts "ディーラーの現在の得点は#{dealer.hand.points}です。"
while dealer.hand.points < 17
card = deck.draw
dealer.hit(card)
puts "ディーラーの引いたカードは#{card}です。"
puts "ディーラーの現在の得点は#{dealer.hand.points}です。"
end
if dealer.hand.busted?
puts "ディーラーの得点は#{dealer.hand.points}です。あなたの勝ちです!"
elsif player.hand.points > dealer.hand.points
puts "あなたの得点は#{player.hand.points}です。"
puts "ディーラーの得点は#{dealer.hand.points}です。あなたの勝ちです!"
elsif player.hand.points < dealer.hand.points
puts "あなたの得点は#{player.hand.points}です。"
puts "ディーラーの得点は#{dealer.hand.points}です。あなたの負けです..."
else
puts "引き分けです。"
end
puts "ブラックジャックを終了します。"
end
end
end
Game.new.play
ruby blackjack.rb
を実行することで、ブラックジャックゲームが開始されます。
$ ruby blackjack.rb
ブラックジャックを開始します。
あなたの引いたカードはクラブの9です。
あなたの引いたカードはスペードの5です。
ディーラーの引いたカードはスペードの4です。
ディーラーの引いた2枚目のカードはわかりません。
あなたの現在の得点は14です。カードを引きますか?(Y/N)
Y
あなたの引いたカードはダイヤの8です。
あなたの現在の得点は22です。
バーストしました、あなたの負けです...
まとめ
この記事では、ブラックジャックゲームを作成することで、Rubyのオブジェクト指向プログラミングを学ぶ方法を紹介しました。この知識を活かして、さらに複雑なアプリケーションを開発する際に役立ててみてください。