はじめに
今回は、ボウリングのスコアを算出するプログラムの作成を行いました。
自分が書いたソースコードと、先輩にいただいた回答例を紹介していきます。
ボウリングスコア計算問題
- 投球を完了した状態で最終的なスコアを算出する。
- 10フレーム分の投球を配列とする。
- 各フレームも配列とする。
- ストライクがある場合、フレームの配列は 1つの要素とする。
- 10フレーム目は、スペアまたはストライクの場合、3つの要素とする。
- ガーターの場合、0 とする。
入力例
[
[1, 1], # 1フレーム目
[0, 2], # 2フレーム目
[3, 0], # 3フレーム目
[4, 4], # 4フレーム目
[5, 5], # 5フレーム目
[1, 1], # 6フレーム目
[10], # 7フレーム目
[4, 4], # 8フレーム目
[1, 1], # 9フレーム目
[5, 5, 5] # 10フレーム目
]
要求仕様
- 最終スコアを数値(整数)で返す。
- 入力値が整数ではない、ボウリングのルール上計算出来ない値の場合、例外を返す(例外はカスタム例外を作成する)。
- カレントフレームスコアシステムは適用しない。(これまで通りの計算方法)
- ファールの投球はないものとする。
自分が書いたソースコード
class InvalidError < StandardError
end
class Bowling
def initialize(score)
@score = score
end
def result
total_score = 0
@score.each.with_index(1) do |points, frame|
raise InvalidError unless valid?(points)
total_score += calculate_score(points, frame)
end
total_score
end
private
def calculate_score(points, frame)
total = points.sum
total += strike_bonus(frame) if strike?(points)
total += spare_bonus(frame) if spare?(points)
total
end
def spare_bonus(frame)
@score[frame].first
end
def strike_bonus(frame)
if double_or_turkey?(frame)
@score[frame].first + @score[frame + 1].first
else
@score[frame].first + @score[frame].last
end
end
def valid?(points)
return false if points.empty?
return false if points.count > 3
return false unless @score.count == 10
return false unless points.all? { |point| (0..10).include?(point) && point.integer? }
true
end
def spare?(points)
points.count == 2 && points.sum == 10
end
def strike?(points)
points.count == 1 && points.first == 10
end
def double_or_turkey?(frame)
@score[frame].count == 1
end
end
処理について
def result
total_score = 0
@score.each.with_index(1) do |points, frame|
raise InvalidError unless valid?(points)
total_score += calculate_score(points, frame)
end
total_score
end
result
メソッドの中で、計算結果を入れるためのtotal_score
を定義しています。
each.with_index()
を使用して、各フレーム数と点数を元にcalculate_score
で計算した値を足し込む処理を作成しました。
フレーム数を1
から始めると、実際のボウリングのスコアと同じ感覚で進められそうと思ったので、each.with_index(1)
を使用しています。
また、計算を行う前に、フレーム毎の点数をvalid?
メソッドで有効な値かどうか判定しており、不正な値の場合は例外を発生させています。
class InvalidError < StandardError
end
カスタム例外は、InvalidError
を作成しています。
def valid?(points)
return false if points.empty?
return false if points.count > 3
return false unless @score.count == 10
return false unless points.all? { |point| (0..10).include?(point) && point.integer? }
true
end
不正な値の判定基準は以下としていました。
- フレームに対する点数が空の場合
- フレームに対する点数が3つより多い場合
- フレーム数が10ではない場合
- フレームに対する点数の全てが0~10の整数でない場合
def calculate_score(points, frame)
total = points.sum
total += strike_bonus(frame) if strike?(points)
total += spare_bonus(frame) if spare?(points)
total
end
calculate_score
では、フレーム毎の点数を合計し、ストライク(ダブルとターキーも含む)やスペアの場合は、それぞれボウリングの計算方法に則って加点をする処理を行っています。
課題
- 想定される値に考慮漏れがあり、不正な値が弾けていない。
- 10フレーム目の条件が全く考慮できていない。
-
result
メソッドで、total_score
を無駄に宣言している。
回答例
class InvalidScoreError < StandardError; end
class Bowling
attr_reader :score
def initialize(score)
@score = score
end
def result
raise InvalidScoreError unless score_valid?
calculate_total_score
end
private
def score_valid?
return false unless valid_game?
return false unless valid_pitching?
true
end
def valid_game?
score.is_a?(Array) && score.size == 10
end
def valid_pitching?
return false unless score.all? { |flame| flame.is_a?(Array) }
return false unless score[0..8].all?(&method(:valid_flame_score?))
return false unless valid_final_flame?
true
end
def valid_flame_score?(flame)
return false unless flame.all? { |pitching| pitching.is_a?(Integer) && (0..10).cover?(pitching) }
flame.size == 1 && flame[0] == 10 ||
flame.size == 2 && (0..10).cover?(flame.sum)
end
def valid_final_flame?
return true if final_flame_strike_or_spare?
return true if valid_final_flame_score?
false
end
def final_flame_strike_or_spare?
score[9].size == 3 &&
(score[9][0] == 10 || score[9][0..1].sum == 10)
end
def valid_final_flame_score?
score[9].size == 2 && score[9][0..1].sum < 10
end
def calculate_total_score
score.each.with_index(1).inject(0) do |total_score, (flame, index)|
total_score + calculate_flame_score(index, flame)
end
end
def calculate_flame_score(index, flame)
return flame.sum if index == 10
calculate_score(index, flame)
end
def calculate_score(index, flame)
calculated_score = flame.sum
calculated_score += strike_score(index) if strike?(flame)
calculated_score += spare_score(index) if spare?(flame)
calculated_score
end
def strike?(flame)
flame.size == 1 && flame[0] == 10
end
def strike_score(index)
return score[index][0] + score[index + 1][0] if score[index].size == 1
score[index][0..1].sum
end
def spare?(flame)
flame.size == 2 && flame.sum == 10
end
def spare_score(index)
score[index][0]
end
end
処理について
attr_reader :score
まず、attr_reader
でscore
が定義されています。
(以下と同じ意味)
def score
@score
end
(attr_reader
をよく知らなかったので、覚えておこうと思いました。)
def result
raise InvalidScoreError unless score_valid?
calculate_total_score
end
result
メソッドでは、計算処理を行う前に、score_valid?
で不正なスコアが弾かれています。
def score_valid?
return false unless valid_game?
return false unless valid_pitching?
true
end
def valid_game?
score.is_a?(Array) && score.size == 10
end
def valid_pitching?
return false unless score.all? { |flame| flame.is_a?(Array) }
return false unless score[0..8].all?(&method(:valid_flame_score?))
return false unless valid_final_flame?
true
end
def valid_flame_score?(flame)
return false unless flame.all? { |pitching| pitching.is_a?(Integer) && (0..10).cover?(pitching) }
flame.size == 1 && flame[0] == 10 ||
flame.size == 2 && (0..10).cover?(flame.sum)
end
def valid_final_flame?
return true if final_flame_strike_or_spare?
return true if valid_final_flame_score?
false
end
def final_flame_strike_or_spare?
score[9].size == 3 &&
(score[9][0] == 10 || score[9][0..1].sum == 10)
end
def valid_final_flame_score?
score[9].size == 2 && score[9][0..1].sum < 10
end
score_valid?
では、ボウリングゲーム全体に関する判定(valid_game?
)と、投球に関する判定(valid_pitching?
)に分かれており、有効な値のみtrue
が返されています。
valid_pitching?
内では、各フレーム毎のスコアが有効かどうか(valid_flame_score?
)、10フレーム目として有効かどうか(valid_final_flame?
)を判定しています。
さらに、valid_final_flame?
内では、10フレーム目の有効なスコアかどうか(valid_final_flame_score
)、10フレーム目のスペアかストライクか(final_flame_strike_or_spare
)を判定しています。
これで、ボウリングのスコアとして有効な値のみが抽出されたことになります。
def calculate_total_score
score.each.with_index(1).inject(0) do |total_score, (flame, index)|
total_score + calculate_flame_score(index, flame)
end
end
計算処理は、each.with_index(1)
にinject(0)
がチェインされているので、
事前にtotal_score
を定義しなくても良くなっています。
学んだこと
- パターンの洗い出しに漏れがないようにする。
- 起こり得る全ての場合を想像する。
- テストファーストで実装すると、contextの洗い出しのタイミンングで漏れに気付きやすい。
- コードの統一感を意識する。
- 配列の要素の取得方法が混在していた。(
@score[frame].first
→@score[frame][0]
)
- 配列の要素の取得方法が混在していた。(
-
Enamerable
のはメソッドはいくらでもチェインして使える。-
with_index
+inject
等
-
- レシーバが配列であれば、
count
ではなくsize
を使う方が良い。-
count
は、ActiveRecordの件数を取得する際に使うため、配列の要素数を取得する際は、size
を使用する方が良い。 -
size
の方が処理速度が早い。
-
- クラスやメソッドの中身を書かない場合は、
end
だけで改行せずに1行で書ける。class InvalidScoreError < StandardError; end
まとめ
今回は、ボウリングのスコア計算問題ということで、想定されるパターンが多く、パターンの洗い出しが不足していたことを感じました。
実際に、どのような値が入るかというイメージを実装前に固めておくことが大切だと実感したので、今後意識して取り組みたいと思います。