これからもっと大規模なプログラミングに取り組んでいく場合、 オブジェクト指向プログラミングの考え方が重要となる。本格的な オブジェクト指向の考え方を全て理解するのは難しいので、ここでは その入口だけ触れてみよう。
前回作成したブラック ジャックプログラムを改良することを考えよう。前回は52枚のトランプカー ドを表現する形式を「配列の中のハッシュ」にした。つまり、
0
1
……
51
52
"suit"
"n"
"ハート"
"A"
"suit"
"n"
"ハート"
"2"
……
"suit"
"n"
"クラブ"
"K"
"suit"
"n"
"その他"
"JOKER"
のようなイメージとなる。実際にはこの形式にしておいて困ることは当面考 えられないが、ここでは仮に
効率が悪いのでデータ形式を変える必要が生じた
ものとして話を進めよう。
配列の中のハッシュをやめて、全て通し番号管理 に変えたい。通し番号管理では、
[0] [1] [2] ………… [50] [51] [52] 22 3 27 ………… 19 33 4
というデータ格納形式となる。通し番号管理に変えるには、まず、最初に52
枚のカードを作成するメソッドを変える必要がある。これには、もともとの
bj.rb
にあった
createCardsHashArray
メソッドを置き換えて、通し番号を
生成するものにすればよい。たとえば、以下のようになるだろう(
前々回のもの
と同じ)。
def createCardsArray()
card=Array.new # 空の配列を作る
0.upto(51) do |x| # ジョーカー不要なので51まで
card << x
end
card # 最後に card配列 を呼び主に返す
end
今回やろうとしている改造は、カードの内部構造を変えるものなので、 カードを作成するメソッドを変更するのは当然であることが分かる。しかし、 構造を変えたからには、それを利用する部分にも影響が出る。まず、 シャッフルするメソッドを考えよう。
シャッフルするメソッドは、
def shuffle(a) srand 0.upto(a.length-1) do |i| j = rand(a.length) # 交換相手をランダムに選ぶ w = a[i] a[i] = a[j] a[j] = w end a end
となっていた。これは仮引数として a
を受け取ってはいるも
のの、それ(a
)がどんな構造になっているかはまったく関知してい
ない。たんに配列だと思って値を交換しているに過ぎない。よってこのメソッド
は、カードの内部保存形式を変えても全く影響が出ない。
続いて、ブラックジャックのゲームの進行部分を調べてみよう。ブラックジャッ
クでは、カードのスートは全く無視して、数のみを見ている。数を見るのは得点
を計算する部分と、画面に表示する部分である。つまり、
dispCard, point
両メソッドに関して、下線で
示した部分を書き換えなければならない。
def dispCard(card) printf("%s の %s\n", card["suit"], card["n"]) end def point(arr) ace = 0 # "A"の枚数 sum = 0 # 合計点 for card in arr # arr配列全ての要素に対して繰り返す n = card["n"] # card["n"]で数を表すvalueが得られる case n # nの値によって場合分け when "A" # "A"ならば… ace += 1 # 枚数カウンタを1増やしておく sum += 11 # まずは11点として加算 when "10", "J", "Q", "K" sum += 10 # 絵札と10は10点 else # それ以外(つまり2…9)はそのものを整数にして足す sum += n.to_i end # when終わり end # for終わり while sum > 21 && ace > 0 # 21より大きく、かつ、aceが1以上なら sum -= 10 # 10を引く ace -= 1 # aceを1つ使ったので減らす end sum end
さらにもう一点プログラミングに神経を遣わなければならないところがある。 それはカードを配るところで、コンピュータに配るところ
com << cards[cardNo+=1]
と人間に配るところ
player << cards[cardNo+=1]
が、プログラム中何箇所も登場する。これには以下のような問題点が 内在する。
cards
が配列でなくなった場合(たとえばハッシュのとき)
には、該当部分を全て書き換えなければならなくなる。
変数cardNo
をいちいち書くのが面倒(変数名を忘れそう)。
cardNo
という変数をどこか別の場所で変更したらゲーム
がおかしくなる。
52枚全部引いたとき(cardNo==52に達したとき)どうするか、という処 理をカードを引く箇所全てに書かなければならない。
結局このプログラムを、「通し番号形式」に変えるために
書き換えなければならないところをまとめると
bj-numbering.rb
となる。
変更点の多さに着目せよ。
この問題を整理すると以下のようにまとめられる。
カードの内部構造をゲームの進行部分で意識しなければならないのは あとあと融通が利かないのであまりよくない。
カードの通し番号のようなちょっとずれただけでゲームが台無しにな る繊細な情報をゲームの進行部分で何回も変更しなければいけないような プログラムは後で改良するときにとてもたいへんで間違えやすくなる。
このような場合には、カードを使ってゲームを作るときに、
カードってどんな処理ができるものなの?
ということに着目して考える。ブラックジャックゲームを作った経験からは、
ことが最低限必要である。逆に、この条件を満たせば内部構造が、ハッシュ のハッシュだろうが、通し番号の配列だろうが、どんなものでも構わない。
ここで今まで、ArrayやHashを利用した経験を思い出そう。Array(配列)を
使っているときには、我々はただ単に要素を追加したり取り出したりするだけで
よく、要素が今何個入っているかとかは普段は意識しなくてよかったし、
並べ換えるときはsort
メソッド任せで実際に並べ換えの具体的手
順はいっさい知らなくてよかった。Arrayクラスのメソッドが勝手にやってくれ
た。
そこでカードゲームでも同じように、トランプのカードにかかわるいろいろ な処理を持ったCardクラスがあったらいいなと考える。言い換えると、
のような便利なクラスが使えたらいいのに、と考える。
Rubyによるオブジェクト指向プログラミングでは、そのような性質を満たす ものを新しい クラス として宣言し、必要な処理をメソッドとして定義 し、別のプログラムでそれを利用する。具体的には、
Card
クラスを定義する
という流れとなる。
Card
クラスを利用したプログラム以上のことをふまえてブラックジャックを「Cardクラスを使う」方式に作り 直す。手順は、
の2段階となる。1番をcard.rb
に、2番を
bj2.rb
に作成する。
#
# 「カード」を表現するCardクラスを定義する
#
class Card # クラス名は必ず大文字で始める
# クラス定義におけるinitializeという名前のメソッドは、
# そのクラスの値(インスタンス)がnewされるときに自動的に
# 呼ばれるメソッド。
# 通常initializeメソッドは必ず定義する。
def initialize()
# @で始まる変数はインスタンス変数といい、クラスの中で
# しか利用できないつまりクラス定義の外からは一切いじる
# ことも見ることもできないので後で変えるかもしれない
# データ構造はインスタンス変数に閉じ込めるのがポイント
@card = Array.new # 全てのカードを保存する配列
@now = -1 # 今何枚目かをしまう変数
for suit in ["ハート", "スペード", "ダイヤ", "クラブ"]
@card << {"suit" => suit, "n" => "A"}
# ここで代入しているのがハッシュ
2.upto(10) do |n|
@card << {"suit" => suit, "n" => n.to_s}
end
for n in ["J", "Q", "K"]
@card << {"suit" => suit, "n" => n}
end
end
shuffle
end
def shuffle() # @cardをいじるので引数は要らない
srand
0.upto(@card.length-1) do |i|
j = rand(@card.length) # 交換相手をランダムに選ぶ
w = @card[i]
@card[i] = @card[j]
@card[j] = w
end
@now = -1 # カードの順番-1にしておく。
end
def next() # つぎの1枚を引く、メソッド
if @now >= @card.length
shuffle()
end
@card[@now += 1]
end
# クラスメソッドの定義
# クラスメソッドはそのクラスのインスタンス全体に共通のメソッド
# クラスメソッドは クラス名.メソッド名 で定義する
def Card.suit(card) # スートを得るメソッド
card["suit"]
end
def Card.n(card) # 数を得るメソッド
card["n"]
end
end
続いて、上記card.rb
を利用したブラックジャック定義
プログラムbj2.rb
を示す。先頭にある
require
は、別ファイルに書かれたRuby
プログラムを取り入れるためのものである。
#!/usr/koeki/bin/ruby # coding: utf-8 # ブラックジャック進行部 # Cardクラス定義ファイルを読み込む require 'card.rb' # これでCardクラスが使えるようになる # ブラックジャックに必要なメソッドの定義 # カードのスートや数を調べるときは、Cardクラスのメソッド # Card.suit() と Card.n() を利用している。 def dispCard(card) # Cardクラスのクラスメソッドを使ってスートと数を得る printf("%s の %s\n", Card.suit(card), Card.n(card)) end def point(arr) ace = 0 # "A"の枚数 sum = 0 # 合計点 for card in arr # arr配列全ての要素に対して繰り返す n = Card.n(card) # クラスメソッド経由で数を得る case n # nの値によって場合分け when "A" # "A"ならば… ace += 1 # 枚数カウンタを1増やしておく sum += 11 # まずは11点として加算 when "10", "J", "Q", "K" sum += 10 # 絵札と10は10点 else # それ以外(つまり2…9)はそのものを整数にして足す sum += n.to_i end # when終わり end # for終わり while sum > 21 && ace > 0 # 21より大きく、かつ、aceが1以上なら sum -= 10 # 10を引く ace -= 1 # aceを1つ使ったので減らす end sum end # Cardクラスのインスタンスを生成(既にシャッフルされている) cards=Card.new # コンピュータの持ちカードを格納する配列 com=[] com << cards.next # cardsに属するnextメソッドで次の1枚を引くようにする com << cards.next # プレイヤーの持ちカードを格納する配列 player=[] player << cards.next player << cards.next # 最初の手持ちを表示する puts "わたしの手:" # コンピュータの1枚目を表示 puts " [ ??? ]" # コンピュータの2枚目を表示 dispCard(com[1]) # 2枚目は com[1] に入っている # プレイヤー側 puts "あなたの手:" dispCard(player[0]) dispCard(player[1]) # プレイヤーが満足するまで引かせる while true STDERR.print "もう一枚引きますか?(yかnで) " answer = gets if /^y/i =~ answer player << cards.next puts "もう一枚引きます。引いたカードはこれです。" dispCard(player[-1]) else break # while true を抜ける end end # コンピュータが16以下なら引き続ける while point(com) <= 16 puts "わたしはもう一枚引きます" com << cards.next end # いよいよ勝負 puts "勝負!" puts "わたしの手:" for c in com # コンピュータの手持ちカード全て繰り返す dispCard(c) end puts "あなたの手:" player.each do |c| # プレイヤーの手持ちカード全て繰り返す dispCard(c) end # 得点を比べる cp, pp = point(com), point(player) printf("わたし %d : %d あなた\n", cp, pp) if cp >= 22 # 22以上なら0点ということにしてしまう cp = 0 # (コンピュータ) end if pp >= 22 # 22以上なら0点ということにしてしまう pp = 0 # (プレイヤー) end if pp > cp # コンピュータより点が高ければ puts "あなたの勝ちです。参りました!" else # そうでなければ(引き分け含む) puts "わたしの勝ちです。べろべろばあ…" end
ゲーム進行部でカードを配るときに、カード番号を逐一管理しなくてよくな
る。また、カードの内部保存構造を変えるのが容易になる。実際に変えてみよう。
カードを通し番号管理にしてみる。この場合、Card
クラスの
定義だけを変えるだけでよい。通し番号形式に変えたプログラム
card.rb
が以下のものである。
上記のものからの変更点を下線で示す。
ブラックジャックゲーム進行部(bj2.rb
)は、
以前と全く同じままでよいことに注目せよ。
# # 「カード」を表現するCardクラスを定義する # 通し番号方式バージョン # class Card def initialize() @card = Array.new # 全てのカードを保存する配列 @now = -1 # 今何枚目かをしまう変数 0.upto(51) do |x| # ジョーカー不要なので51まで @card << x end shuffle end def shuffle() srand 0.upto(@card.length-1) do |i| j = rand(@card.length) # 交換相手をランダムに選ぶ w = @card[i] @card[i] = @card[j] @card[j] = w end @now = -1 # カードの順番 end def Card.suit(card) s = card / 13 ["ハート", "スペード", "ダイヤ", "クラブ"][s] end def Card.n(card) n = card%13 + 1 case n when 1 "A" when 11 "J" when 12 "Q" when 13 "K" else n.to_s end end def next() if @now >= @card.length shuffle() end @card[@now += 1] end end