はじめに
この記事は、プログラミング未経験者である私が、ブラックジャックゲームの作成を通じて学んだことや感じたことを、思いつくまま書き連ねたものになります。
このような記事を書くのも初めてなので、まとまりのない雑記になってしまいそうですが、今後も記事を書きつつ精度を上げていきたいと思いますので、何卒ご容赦ください
使用教材
・プロを目指す人のためのRuby入門[改訂2版]
言語仕様からテスト駆動開発・デバッグ技法まで (Software Design plus)
伊藤 淳一 (著)
ブラックジャックゲームについて
今回はプレイヤーとディーラーの1対1の対戦というシンプルなケースで作成します。
なお、今後はCPUありのプレイヤーが複数人いるパターンも作成する予定です。
ルール上の注意点としてはトランプのA(エース)を引いた場合の点数は1点または11点となり、持ち札の合計点が21点に近くなるよう(かつ21点は超えないよう)自動的に決定するという点です。
(ブラックジャックの詳しいルールについては各サイトをご確認ください)
それでは作成開始です!
STEP1:全体像を把握し作成するクラスを決める
まずはゲームの全体像を把握し、必要なクラスを決めていきました。
今回はオブジェクト指向でプログラムを書いていくため、まずは実際にブラックジャックで遊んでいるところを想像します(カジノ行ったことないですけど)
オブジェクト指向は、現実世界そのままをプログラムの世界に落とし込み、開発効率をあげる手法(というか考え方)なので、まずは想像することが大事だと思っています。
・まずは当然プレイヤーとディーラがいる(→playerクラスとdealerクラスが必要)
・カードの山(デッキ)がある(→cardクラスが必要)
この3つのクラスでディーラーとプレイヤーがカードのやり取りをしている姿が目に浮かびます。
あと、得点をつける人・・・というのはいませんが、得点集計は別のクラスに任せた方が良さそうです。
・得点集計はこれまで用意したクラスとは別のクラスを作成する(→scoreクラスが必要)
また、プレイヤーとディーラーの挙動はほとんど同じものになるので、基本的な機能を実装したクラスを別に作り、それをplayaerクラスとdealerクラスに継承させるのが良さそうです。
そこで、baseplayerクラスを作成し、基本的な機能を書いていきます
・プレイヤーとディーラーに同一の機能を継承させる(→baseplayerクラスを作成)
class BasePlayer
end
class Player < BasePlayer
end
class Dealer < BasePlayer
end
最後に、ゲームの全体を統括するクラスとして、Blackjackクラスも作成しました。
STEP2:コメントを書きつつ流れを確認
クラスを作ったので、さて作成開始・・・といきたいところですが、このままだとどこから着手すればよいかわかりません。まずはBlackjackクラスで、処理の流れをコメントで書いていくことにしました。
# ブラックジャックのゲームを開始します
# 開始するには game_start メソッドを実行してください
class Blackjack
def game_start
#ブラックジャックを開始します。
#あなたの引いたカードはハートの7です。
#あなたの引いたカードはクラブの8です。
#ディーラーの引いたカードはダイヤのQです。
#...(以下続く)
end
end
STEP3:各クラスにメソッドを追加していく
さて、ブラックジャッククラスで書いたコメントからも分かる通り、ディーラーやプレイヤーがカードを引くためのメソッドが必要です。また引いたカードを手札として保持する配列も必要そうです。
そこでBasePlayerクラスに以下のように記載しました。
class BasePlayer
# 手札の配列を作成
def initialize
@hand = []
end
# カードを山札から引いて手札に格納する
def get_card(card)
end
end
その前にカードクラスで山札を作る必要がありました!
山札がなければカード引くことができません。
# 52枚のトランプを管理するクラス
class Card
attr_reader :deck
def initialize
# 52枚のカードを用意する
numbers = ['A', 2, 3, 4, 5, 6, 7, 8, 9, 'J', 'Q', 'K']
marks = %w[ハート スペード ダイヤ クラブ]
@deck = []
marks.each do |m|
numbers.each do |n|
@deck << [m, n]
end
end
end
end
numbersにトランプの数字を、marksにトランプのマークを格納し、ブロックを入れ子にして、52枚のトランプを再現しました。initializeメソッドに入れいてるため、newでインスタンス化された時点で山札が作成されます。
cardクラスができたので、baseplayerクラスに山札からカードを引く機能を実装します。
class BasePlayer
# 手札の配列を作成
def initialize
@hand = []
end
# カードを山札から引いて手札に格納する
def get_card(card)
hand << card.deck.shuffle!.first
card.deck.shift
hand
end
end
get_cardメソッドは、インスタンス変数handに、cardクラスの配列deck(山札)からひいたカードを格納するメソッドです。
shuffle!で配列をランダムに入れ替えした後、firstで配列先頭のカードを取得します。
また、shiftで配列先頭のカードを削除するので、取得したカードは山札に残りません。
このBasePlayerクラスをplayerクラスとdealerクラスが継承しているため、どちらのクラスも同じ機能(メソッド)が利用できるようになります。
現在の得点を見たり最終得点を比較するためScoreクラスを実装する
Scoreクラスの中身は以下の通りです
# プレイヤー、ディーラークラスの手札の得点を集計する
class Score
# 各プレイヤーの現在の得点を集計する
def get_score(player)
sum = 0
have_a = nil # 手札にエースがあるか判定するための変数
# 手札の合計得点を求める
player.hand.each do |p|
if p[1] == 'A'
sum += 1
have_a = true # 手札にエースがあることを判定するためtrueにする
elsif p[1] == 'J' || p[1] == 'Q' || p[1] == 'K'
sum += 10
else
sum += p[1]
end
end
# エース有かつ手札の合計得点が11点以下なら10点追加(エースを11点としてカウント)
sum += 10 if have_a && sum <= 11
sum
end
end
get_scoreメソッドはその名の通りプレイヤーまたはディーラの合計点を求めるメソッドです。
Playerクラスのインスタンス変数hand(手札)をeachで回し、変数sumに順に足していき、合計点を求めています。
その際、A,J,Q,K はそのままだと文字列で計算ができないので、if文でそれぞれ対応した点数を足しています。
ただ、A(エース)は1点または11点のため、まずは1点で計算し、最後合計点sumが11以下であれば10点を足すという順序で計算しています。
終わりに
技術ブログというよりは私が実装した順序を振り返るだけになってしまいましたが、こんな感じの流れで完成させることができました。
これからも適宜コードを見直し、加筆修正していくつもりですので、何卒よろしくお願いします。