ボウリングのスコア計算プログラム オブジェクト指向版

以前、Rubyの標準ライブラリのみで実装したボウリングプログラムを、オブジェクト指向版として実装しました。

1. 要件(ルール)
  • 1ゲーム=10フレーム
  • 1フレーム=2投(例外あり)
  • ピンの数は10本
  • 1投目で10本倒したらストライク
  • ストライクの場合は2投目は表記しない
  • 1投目で全て倒せなかった時、2投目で全て倒したらスペア
  • スペアのフレームの得点は次の1投の点を加算する
    • 例: 6 4 5 = 15
    • 例: 6 4 10 = 20
  • ストライクのフレームの得点は次の2投の点を加算する
    • 例: 10 5 2 = 17
    • 例: 10 10 10 = 30
  • 10フレーム目は1投目がストライクもしくは2投目がスペアだった場合、3投目が投げられる
  • ありえない投球数やありえない数字・記号がこない前提

bowling.rb(実行ファイル)

#!/usr/bin/env ruby
# frozen_string_literal: true

require_relative 'game'

shots = ARGV[0].split(',')

game = Game.new(shots)
puts game.score
  1. ARGV[0]コマンドライン引数を取得し、カンマで区切られたショットのデータを配列に分割。
  2. Gameクラスのインスタンスを生成し、ショットのデータを渡してゲームを開始。
  3. 最後に、ゲームのスコアを表示。

game.rb

# frozen_string_literal: true

require_relative 'frame'

class Game
  def initialize(shots)
    @shots = shots
  end

  def score
    @frames = create_frames
    total_score = 0
    (0..9).each do |frame_index|
      frame = Frame.new(@frames[frame_index])
      total_score += frame.score
      @frames[frame_index + 1] ||= []
      @frames[frame_index + 2] ||= []
      total_score += calculate_bonus_points(frame_index, frame)
    end

    total_score
  end

  def create_frames
    frame = []
    frames = []
    @shots.each do |s|
      frame << s
      if frames.length < 10
        if frame.length == 2 || s == 'X'
          frames << frame.dup
          frame.clear
        end
      else
        frames.last << s
      end
    end
    frames
  end

  def calculate_bonus_points(frame_index, frame)
    if frame.strike?
      next_two_shots = (@frames[frame_index + 1] + @frames[frame_index + 2]).slice(0, 2)
      next_two_shots.sum { |s| Shot.new(s).score }
    elsif frame.spare?
      next_shot = @frames[frame_index + 1][0]
      Shot.new(next_shot).score
    else
      0
    end
  end
end
  1. initializeメソッドでは、ショットのデータを受け取り、インスタンス変数@shotsに格納。
  2. scoreメソッドはゲームのスコアを計算して返す。
    • まず、create_framesメソッドを呼び出してフレームの配列@framesを作成。
    • 次に、total_score変数を0で初期化。10フレーム分の処理を繰り返し、各フレームのスコアを計算し、total_scoreに加算。
    • また、ボーナスポイントも計算して加算。
    • 最後に、計算された総合スコアを返す。
  3. create_framesメソッドは、ショットのデータをフレームごとに分割し、フレームの配列framesを作成。
    • frame配列にショットを追加していき、フレーム数が10未満の場合はフレームが完了した時点で新しいフレームを作成し、フレーム数が10の場合は最後のフレームに追加のショットを結合。
  4. calculate_bonus_pointsメソッドは、フレームのインデックスとフレームオブジェクトを受け取り、ボーナスポイントを計算して返す。 また、与えられたフレームがストライクの場合とスペアの場合で異なるボーナスポイントの計算を行う。
    • まず、フレームがストライクの場合は、次の2つのフレームのショットを取得し、それらのスコアを計算してボーナスポイントとする。@frames[frame_index + 1]@frames[frame_index + 2]を連結して、最初の2つのショットを取得。
    • そして、それぞれのショットをShotクラスのインスタンスとして生成し、スコアを取得。取得したスコアを合計した値がボーナスポイントとなる。
  5. フレームがスペアの場合は、次のフレームの最初のショットのスコアをボーナスポイントとする。@frames[frame_index + 1]の最初の要素を取得し、それをShotクラスのインスタンスとして生成し、スコアを取得。
  6. 最後に、フレームがストライクでもスペアでもない場合は、ボーナスポイントを0とする。 これにより、ゲームのスコア計算においてストライクやスペアの場合のボーナスポイントが正しく計算され、総合スコアに加算される。

(0..9).each do |frame_index|...endの箇所については、injectを使っても良い。

frame.rb

# frozen_string_literal: true

require_relative 'shot'

class Frame
  def initialize(frame)
    @first_shot = Shot.new(frame[0])
    @second_shot = Shot.new(frame[1])
    @third_shot = Shot.new(frame[2])
  end

  def score
    [
      @first_shot.score,
      @second_shot.score,
      @third_shot.score
    ].sum
  end

  def strike?
    @first_shot.score == 10
  end

  def spare?
    score == 10
  end
end
  1. initializeメソッドでは、frameという引数を受け取り、その値を元にフレーム内の各ショットを表すインスタンス変数@first_shot@second_shot@third_shotを作成。
  2. scoreメソッドは、各ショットのスコアを取得し、それらの合計値を返す。
  3. strike?メソッドは、フレームの最初のショットがストライクであるかどうかを確認。
  4. spare?メソッドは、フレームのスコアがスペアであるかどうかを確認。

shot.rb

# frozen_string_literal: true

class Shot
  def initialize(mark)
    @mark = mark
  end

  def score
    @mark == 'X' ? 10 : @mark.to_i
  end
end
  1. initializeメソッドでは、markという引数を受け取り、その値をインスタンス変数@markに格納。
  2. scoreメソッドは、ショットのスコアを計算して返す。もし@markが'X'(ストライク)であれば10を返し、そうでなければ@markを整数に変換して返す。