[TDD][SwiftUI] SwiftUI と TDD で作る ボーリングスコアアプリ(その5 10フレーム対応)

ボーリングアプリ(スコアを記録するアプリ)を、SwiftUI と TDD で作ってみます。
[TDD][SwiftUI] SwiftUI と TDD で作る ボーリングスコアアプリ(その1 モデル作成 Part1) [TDD][SwiftUI] SwiftUI と TDD で作る ボーリングスコアアプリ(その2 View と ViewModelの作成 Part1) [TDD][SwiftUI] SwiftUI と TDD で作る ボーリングスコアアプリ(その3 スコア計算 モデルの追加実装) [TDD][SwiftUI] SwiftUI と TDD で作る ボーリングスコアアプリ(その4 スペア・ストライクへの対応)

ここまでの経緯は、上記記事を参照ください。

アプリ設計:振り返り

これまでのところ、前回作った設計に沿って作ってこれています。

今回は、10フレームの特別処理への対応を行います。

10フレームの特別処理とは、以下のことです。

  • スペアの場合は、さらに1投投げられる(3投目を投げられる)
  • 1投目がストライクの場合は、さらに2投を投げられる
  • 1、2投目がストライクの場合は、さらに1投投げられる

スコア計算も少し特別です。

  • 10フレームでの計算は、ストライク・スペアにかかわらず、2投もしくは3投の合計とする

# 詳細は、ボーリングのスコア計算を調べてみてください

10フレームでの3投を記録できるように

これまでの機能追加と同じように、<テスト追加>→<テスト失敗確認>→<機能実装追加>→<テスト成功確認>→<リファクタリング> というサイクルを実行していきます。

TDD では、テスト結果の色の変化をイメージして red-green-refactoring サイクルと呼ばれたりします。

10フレーム目に3投するテストの追加

ルールでは、10フレームは、最大3投投げられるのですが、今回作っているモデルでは、そのための処理を追加していないので、投げられません。

まずは、10フレームで3投投げられることをテストします。

3投できるケースは、以下のケースです。

  • ストライクx3
  • ストライクx2 +1投
  • ストライク+2投
  • スペア+1投

パーフェクトと呼ばれますが、全ての投球がストライクだった時には、合計で12回(各1回@1〜9フレーム+3回@10フレーム)のストライクが記録されます。

上記の4ケースのテストを作ります。

10フレーム目で3投するテスト

    func test_recordScore_12Strikes_shouldBeRecorded() {
        var bowlingGame = BowlingGame()

        for index in 0..<12 {
            XCTAssertTrue(bowlingGame.addBowlResult(10), "failed to record \(index+1)-th strike ")
        }
    }

    func test_recordScore_11StrikesPlus1_shouldBeRecorded() {
        var bowlingGame = BowlingGame()

        for index in 0..<11 {
            XCTAssertTrue(bowlingGame.addBowlResult(10), "failed to record \(index+1)-th strike ")
        }
        XCTAssertTrue(bowlingGame.addBowlResult(3), "failed to record 3rd throw in 10th frame")
    }

    func test_recordScore_10StrikesPlus2_shouldBeRecorded() {
        var bowlingGame = BowlingGame()

        for index in 0..<10 {
            XCTAssertTrue(bowlingGame.addBowlResult(10), "failed to record \(index+1)-th strike ")
        }
        XCTAssertTrue(bowlingGame.addBowlResult(3), "failed to record 2nd throw in 10th frame")
        XCTAssertTrue(bowlingGame.addBowlResult(3), "failed to record 3rd throw in 10th frame")
    }

    func test_recordScore_9StrikesPlusSparePlus1_shouldBeRecorded() {
        var bowlingGame = BowlingGame()

        for index in 0..<9 {
            XCTAssertTrue(bowlingGame.addBowlResult(10), "failed to record \(index+1)-th strike ")
        }
        XCTAssertTrue(bowlingGame.addBowlResult(4), "failed to record 1st throw in 10th frame")
        XCTAssertTrue(bowlingGame.addBowlResult(6), "failed to record 2nd throw in 10th frame")
        XCTAssertTrue(bowlingGame.addBowlResult(5), "failed to record 3rd throw in 10th frame")
    }

実行してみると最初のテストでは、”XCTAssertTrue failed - failed to record 11-th strike” というエラーになります。11番目のストライクは 10フレーム目の2投目のストライクに該当します。

現在のスコア記録は、以下の関数で行なっています。

addBowlResult

    mutating func addBowlResult(_ num: Int) -> Bool {
        if let addIndex = self.findRecordableFrameBowl() {
            self.frames[addIndex.frameIndex].bowls[addIndex.bowlIndex] = .Done(num)
            // case Strike
            if (num == 10) && (addIndex.bowlIndex == 0) { // Strike !
                self.frames[addIndex.frameIndex].bowls[1] = .NoNeed
            }
            return true
        }
        return false

ストライクだった時に、2投目を .NoNeed に設定するだけで、10フレーム目の特別ルールを実装していないため、1投目がストライクであれば、2投目が記録できなくなっています。

10フレーム目でのストライクの記録に対応する

ということで、10フレーム目のときの処理を追加します。

追加する処理は、以下です。

  • 10フレーム目の1投目がストライクであっても2投目を .NotYet のままにし、3投目を .NoNeed から .NotYet に変更する
  • 10フレーム目の2投目でスペアになったら、3投目を .NoNeed から .NotYet に変更する

10フレーム目の2投目を .NotYet にキープするだけでなく、3投目を記録可能にするように処理しておきます。

少し複雑ですが、以下のようになりました。

addBowlResult パーフェクトゲーム対応

    mutating func addBowlResult(_ num: Int) -> Bool {
        if let addIndex = self.findRecordableFrameBowl() {
            self.frames[addIndex.frameIndex].bowls[addIndex.bowlIndex] = .Done(num)
            if (addIndex.frameIndex == 9) { // new 
                if addIndex.bowlIndex != 2 {
                    if num == 10 {
                        self.frames[9].bowls[2] = .NotYet
                    }
                    if addIndex.bowlIndex == 1 {
                        if let bowl1 = bowlResult(frameIndex: 9, bowlIndex: 0) {
                            if bowl1 + num == 10 {
                                self.frames[9].bowls[2] = .NotYet
                            }
                        }
                    }
                }
                return true
            } // new 
            // case Strike
            if (num == 10) && (addIndex.bowlIndex == 0) { // Strike !
                self.frames[addIndex.frameIndex].bowls[1] = .NoNeed
            }
            return true
        }
        return false
    }

10フレーム目に3投するテスト

上記のコードで、先に追加した4つのテストケースはパスすることがわかります。

10フレーム目で3投するケースでのスコア計算

さきの 10フレーム目で3投する4つのケースで、スコア計算も正しいかを確認することにします。

10フレーム目が3投になっても、3投を合計するだけで、スペアやストライクの計算は行いません。

ただし、9フレーム目がストライクであれば、10フレーム目の1投目2投目を考慮して計算します。1投目2投目がストライクであれば、ターキーとなるように計算するということです。

テストに、スコア計算も追加

プログラミングの知識ではなく、ボーリングの知識が必要となります・・・

それぞれの正しいスコアを計算して、チェックするようにしました。10フレームの影響が考えられる、9フレーム目、10フレーム目のスコアをテスト対象としています。

チェック対象は、スコアの値だけでなく、どのタイミングで計算可能になっているかもテストしています。

10フレーム目で3投するテスト スコア計算付き

    func test_recordScore_12Strikes_shouldBeRecorded() {
        var bowlingGame = BowlingGame()

        for index in 0..<9 {
            XCTAssertTrue(bowlingGame.addBowlResult(10), "failed to record \(index+1)-th strike ")
        }

        XCTAssertTrue(bowlingGame.addBowlResult(10), "failed to record 10-th strike ")

        XCTAssertNil(bowlingGame.frameScore(frameIndex: 8))
        XCTAssertNil(bowlingGame.frameScore(frameIndex: 9))

        XCTAssertTrue(bowlingGame.addBowlResult(10), "failed to record 11-th strike ")

        XCTAssertEqual(bowlingGame.frameScore(frameIndex: 8), 270)
        XCTAssertNil(bowlingGame.frameScore(frameIndex: 9))

        XCTAssertTrue(bowlingGame.addBowlResult(10), "failed to record 12-th strike ")

        XCTAssertEqual(bowlingGame.frameScore(frameIndex: 9), 300)
    }

    func test_recordScore_11StrikesPlus1_shouldBeRecorded() {
        var bowlingGame = BowlingGame()

        for index in 0..<11 {
            XCTAssertTrue(bowlingGame.addBowlResult(10), "failed to record \(index+1)-th strike ")
        }
        XCTAssertEqual(bowlingGame.frameScore(frameIndex: 8), 270)
        XCTAssertNil(bowlingGame.frameScore(frameIndex: 9))

        XCTAssertTrue(bowlingGame.addBowlResult(3), "failed to record 3rd throw in 10th frame")

        XCTAssertEqual(bowlingGame.frameScore(frameIndex: 9), 293)
    }

    func test_recordScore_10StrikesPlus2_shouldBeRecorded() {
        var bowlingGame = BowlingGame()

        for index in 0..<10 {
            XCTAssertTrue(bowlingGame.addBowlResult(10), "failed to record \(index+1)-th strike ")
        }
        XCTAssertNil(bowlingGame.frameScore(frameIndex: 8))
        XCTAssertNil(bowlingGame.frameScore(frameIndex: 9))

        XCTAssertTrue(bowlingGame.addBowlResult(3), "failed to record 2nd throw in 10th frame")

        XCTAssertEqual(bowlingGame.frameScore(frameIndex: 8), 263)
        XCTAssertNil(bowlingGame.frameScore(frameIndex: 9))
        
        XCTAssertTrue(bowlingGame.addBowlResult(3), "failed to record 3rd throw in 10th frame")

        XCTAssertEqual(bowlingGame.frameScore(frameIndex: 9), 279)
    }

    func test_recordScore_9StrikesPlusSparePlus1_shouldBeRecorded() {
        var bowlingGame = BowlingGame()

        for index in 0..<9 {
            XCTAssertTrue(bowlingGame.addBowlResult(10), "failed to record \(index+1)-th strike ")
        }
        XCTAssertNil(bowlingGame.frameScore(frameIndex: 8))

        XCTAssertTrue(bowlingGame.addBowlResult(4), "failed to record 1st throw in 10th frame")
        
        XCTAssertNil(bowlingGame.frameScore(frameIndex: 8))
        XCTAssertNil(bowlingGame.frameScore(frameIndex: 9))
        
        XCTAssertTrue(bowlingGame.addBowlResult(6), "failed to record 2nd throw in 10th frame")
        
        XCTAssertEqual(bowlingGame.frameScore(frameIndex: 8), 254)
        XCTAssertNil(bowlingGame.frameScore(frameIndex: 9))
        
        XCTAssertTrue(bowlingGame.addBowlResult(5), "failed to record 3rd throw in 10th frame")
        
        XCTAssertEqual(bowlingGame.frameScore(frameIndex: 9), 269)
    }

実行すると、テストが失敗するのではなく、Index out of range という Fatal Error が発生します。

ストライクやスペアのスコア計算をするために、次の投球や、次々の投球の値が必要になりますが、10フレーム目での次の投球や次々の投球の値を取得しようとして、11フレーム目を探してエラーとなっています。

10フレーム目で3投するスコア計算の実装

テストでエラーになることを確認したので、実装していきます。

特定の投球結果を取得する bowlResult と フレームでのスコアを計算する frameScore は、以下のような実装になっています。

bowlResult と frameScore の実装(これまで)

    func bowlResult(frameIndex: Int, bowlIndex: Int)  -> Int? {
        let bowl = frames[frameIndex].bowls[bowlIndex]
        
        switch bowl {
            case .Done(let num):
                return num
            default:
                return nil
        }
    }
    
    func frameScore(frameIndex: Int) -> Int? {
        if let lastResult = frameIndex == 0 ? 0 : frameScore(frameIndex: frameIndex - 1) {
            switch frameState(frameIndex: frameIndex) {
                case .Spare:
                    if let nextBowl = bowlResult(frameIndex: frameIndex+1, bowlIndex: 0) {
                        return lastResult + 10 + nextBowl
                    }
                    return nil // need further info
                case .Strike:
                    if let nextBowl = bowlResult(frameIndex: frameIndex+1, bowlIndex: 0) {
                        switch frameState(frameIndex: frameIndex+1) {
                            case .Strike:
                                if let nextNextBowl = bowlResult(frameIndex: frameIndex+2, bowlIndex: 0) {
                                    return lastResult + 10 + 10 + nextNextBowl
                                }
                            case .Spare:
                                return lastResult + 10 + 10
                            case .Others:
                                if let nextNextBowl = bowlResult(frameIndex: frameIndex+1, bowlIndex: 1) {
                                    return lastResult + 10 + nextBowl + nextNextBowl
                                }
                        }
                    }
                    return nil // need further info
                case .Others:
                    if let bowl0 = bowlResult(frameIndex: frameIndex, bowlIndex: 0) {
                        if let bowl1 = bowlResult(frameIndex: frameIndex, bowlIndex: 1) {
                            return lastResult + bowl0 + bowl1
                        }
                    }
            }
        }
        return nil
    }
}

みてわかるように、単純に次の投球を探して辿っています。

現在の実装に追加していっても良いのですが、コードが複雑になりそうですので、ストライクのケース、スペアのケース、通常ケースに分けて、前者2つを別メソッドにしてしまいましょう。

以下が、分割した、frameScore です。(単純に該当部分を外部メソッドに分割しただけです)

frameScore(分割後)

    func frameScore(frameIndex: Int) -> Int? {
        if let prevFrameResult = frameIndex == 0 ? 0 : frameScore(frameIndex: frameIndex - 1) {
            switch frameState(frameIndex: frameIndex) {
                case .Spare:
                    return calcSpareFrame(frameIndex: frameIndex, prevFrameResult: prevFrameResult)
                case .Strike:
                    return calcStrikeFrame(frameIndex: frameIndex, prevFrameResult: prevFrameResult)
                case .Others:
                    if let bowl0 = bowlResult(frameIndex: frameIndex, bowlIndex: 0) {
                        if let bowl1 = bowlResult(frameIndex: frameIndex, bowlIndex: 1) {
                            return prevFrameResult + bowl0 + bowl1
                        }
                    }
            }
        }
        return nil
    }
    
    func calcSpareFrame(frameIndex:Int, prevFrameResult: Int) -> Int? {
        if let nextBowl = bowlResult(frameIndex: frameIndex+1, bowlIndex: 0) {
            return prevFrameResult + 10 + nextBowl
        }
        return nil // need further info
    }
    
    func calcStrikeFrame(frameIndex:Int, prevFrameResult: Int) -> Int? {
        if let nextBowl = bowlResult(frameIndex: frameIndex+1, bowlIndex: 0) {
            switch frameState(frameIndex: frameIndex+1) {
                case .Strike:
                    if let nextNextBowl = bowlResult(frameIndex: frameIndex+2, bowlIndex: 0) {
                        return prevFrameResult + 10 + 10 + nextNextBowl
                    }
                case .Spare:
                    return prevFrameResult + 10 + 10
                case .Others:
                    if let nextNextBowl = bowlResult(frameIndex: frameIndex+1, bowlIndex: 1) {
                        return prevFrameResult + 10 + nextBowl + nextNextBowl
                    }
            }
        }
        return nil // need further info
    }
}

10フレームの計算は、単純に、2投分もしくは3投分を計算すれば良いので、簡単です。以下のようなメソッドを作り、10フレーム目の計算を要求された時に使用します。

frameScoreFor10th code

    func frameScoreFor10th(prevFrameResult:Int) -> Int? {
        if let f10b1 = bowlResult(frameIndex: 9, bowlIndex: 0) {
            if let f10b2 = bowlResult(frameIndex: 9, bowlIndex: 1) {
                if let f10b3 = bowlResult(frameIndex: 9, bowlIndex: 2) {
                    return prevFrameResult + f10b1 + f10b2 + f10b3
                }
                return prevFrameResult + f10b1 + f10b2
            }
        }
        return nil
    }
frameScore code

    func frameScore(frameIndex: Int) -> Int? {
        if let prevFrameResult = frameIndex == 0 ? 0 : frameScore(frameIndex: frameIndex - 1) {
            if frameIndex == 9 {
                return frameScoreFor10th(prevFrameResult: prevFrameResult) // NEW !!
            }
            switch frameState(frameIndex: frameIndex) {
                case .Spare:
                    return calcSpareFrame(frameIndex: frameIndex, prevFrameResult: prevFrameResult)
                case .Strike:
                    return calcStrikeFrame(frameIndex: frameIndex, prevFrameResult: prevFrameResult)
                case .Others:
                    if let bowl0 = bowlResult(frameIndex: frameIndex, bowlIndex: 0) {
                        if let bowl1 = bowlResult(frameIndex: frameIndex, bowlIndex: 1) {
                            return prevFrameResult + bowl0 + bowl1
                        }
                    }
            }
        }
        return nil
    }

10フレームの途中であれば、計算終了できないので、nil を返しています。

次に、10フレームが影響を与える9フレームのスコア計算のケースを考えると、以下のようになります。

  • 9フレーム目がストライクだった時は、10フレーム目で次の投球と次々の投球を探す。このとき:10フレーム目の1投目と2投目が必要な情報となる(10フレーム目の1投目がストライクかどうかにかかわらず)
  • 9フレーム目がスペアだったときは、10フレーム目で次の投球を探す。このとき、10フレーム目の1投目が必要な情報となる。が、これは、通常の処理と変わらない

上記より、9フレーム目がストライクだった時に特殊処理を入れれば良いはずなので、ストライク時の計算を行う calcStrikeFrame に特殊処理を追加。

calcStrikeFrame code

    func calcStrikeFrame(frameIndex:Int, prevFrameResult: Int) -> Int? {
        if (frameIndex == 8) {
            if let f10b1 = bowlResult(frameIndex: 9, bowlIndex: 0) {
                if let f10b2 = bowlResult(frameIndex: 9, bowlIndex: 1) {
                    return prevFrameResult + 10 + f10b1 + f10b2
                }
            }
            return nil
        }
        if let nextBowl = bowlResult(frameIndex: frameIndex+1, bowlIndex: 0) {
            switch frameState(frameIndex: frameIndex+1) {
                case .Strike:
                    if let nextNextBowl = bowlResult(frameIndex: frameIndex+2, bowlIndex: 0) {
                        return prevFrameResult + 10 + 10 + nextNextBowl
                    }
                case .Spare:
                    return prevFrameResult + 10 + 10
                case .Others:
                    if let nextNextBowl = bowlResult(frameIndex: frameIndex+1, bowlIndex: 1) {
                        return prevFrameResult + 10 + nextBowl + nextNextBowl
                    }
            }
        }
        return nil // need further info
    }
}

改めて、テストを通してみると、全てパスすることが確認できます。

UI を使って、試そうとすると、10フレーム目の表示がおかしい & 3投分の表示枠がないことに気づきます。

ビューを10フレームの3投に対応

10フレーム3投目の表示枠

FrameView で、10フレームの時だけ、3つ表示するようにします。

example code

struct FrameView: View {
    @ObservedObject var viewModel: BowlingGameViewModel
    let index:Int
    var body: some View {
        VStack(spacing:0) {
            FrameIndexView(index: index)
            HStack(spacing: 0) {
                FrameBowlView(viewModel: viewModel, frameIndex: index, bowlIndex: 0)
                FrameBowlView(viewModel: viewModel, frameIndex: index, bowlIndex: 1)
                if (index == 9) {                                                         // NEW
                    FrameBowlView(viewModel: viewModel, frameIndex: index, bowlIndex: 2)  // NEW
                }                                                                         // NEW
            }
            FrameScoreView(viewModel: viewModel, frameIndex: index)
        }
    }
}

これまでは、frame を固定値指定だったので、少し表示が崩れます。そこを整えるようにして以下のような表示になります。

10フレーム3投対応した表示

「10フレーム3投対応した表示」

10フレーム目の3投の表示をテスト

モデルで使ったテストをベースに、各投球の表示と各フレームのスコアをテストしています。 (長いので、全投球ストライクのテストケースのみ抜粋しました)

test_recordScore_12Strikes_displayedCorrectly code

    func test_recordScore_12Strikes_displayedCorrectly() {
        for _ in 0..<9 {
            bowlButtons[10].tap()
        }
        bowlButtons[10].tap()
        XCTAssertEqual(frameScoreLabels[8].label, "-")
        XCTAssertEqual(frameScoreLabels[9].label, "-")
        XCTAssertEqual(frameBowlLabels[9][0].label, "X")
        XCTAssertEqual(frameBowlLabels[9][1].label, "-")
        XCTAssertEqual(frameBowlLabels[9][2].label, "-")

        bowlButtons[10].tap()
        XCTAssertEqual(frameScoreLabels[8].label, "270")
        XCTAssertEqual(frameScoreLabels[9].label, "-")
        XCTAssertEqual(frameBowlLabels[9][0].label, "X")
        XCTAssertEqual(frameBowlLabels[9][1].label, "X")
        XCTAssertEqual(frameBowlLabels[9][2].label, "-")

        bowlButtons[10].tap()
        XCTAssertEqual(frameScoreLabels[9].label, "300")
        XCTAssertEqual(frameBowlLabels[9][0].label, "X")
        XCTAssertEqual(frameBowlLabels[9][1].label, "X")
        XCTAssertEqual(frameBowlLabels[9][2].label, "X")
    }

このテストを動かしてみると 第10フレームの2投目、3投目の表示で失敗していることがわかります。通常のフレームであれば、1投目がストライクであれば、2投目の表示は、””になります。

その点が、10フレーム目での期待する表示と異なるために失敗となっています。

ビューの変更

この振る舞いは、FrameBowlView ビューではなく、BowlingGameViewModel で制御されています。

BowlingGameViewModel の bowlAsText メソッド

    func bowlAsText(frameIndex:Int, bowlIndex: Int) -> String {
        switch game.frameState(frameIndex: frameIndex) {
            case .Strike:
                return bowlIndex == 0 ? "X" : ""
            case .Spare:
                if bowlIndex == 1 { return "/" }
                fallthrough
            case .Others:
                if let num = game.bowlResult(frameIndex: frameIndex, bowlIndex: bowlIndex)  {
                    return String(num)
                }
                return "-"
        }
    }

表示要素を決める時に、フレームの状態を最初にチェックしているため、1投目がストライクのフレームでは、2投目の枠が””になっています。

特殊な表示を必要とするのは、10フレーム目だけなので、分岐させて処理してしまいます。

10フレーム目の表示をまとめると、以下になります。

  • 1投目が10であれば、ストライク表示、それ以外は、該当数字表示
  • 2投目は、1投目がストライクであれば、2投目は、該当数字表示(ストライクであればストライク表示)
  • 2投目は、1投目が通常投球であれば、スペア表示するか、該当数字表示
  • 3投目は、ストライク表示 or (該当すれば)スペア表示 or 該当数字表示
bowlAsText 10フレーム3投対応版

    func bowlAsText(frameIndex:Int, bowlIndex: Int) -> String {
        if (frameIndex == 9) {
            switch bowlIndex {
                case 0:
                    if let bowl0 = game.bowlResult(frameIndex: 9, bowlIndex: 0) {
                        if bowl0 == 10 {
                            return "X"
                        }
                        return String(bowl0)
                    }
                    return "-"
                case 1:
                    if let bowl0 = game.bowlResult(frameIndex: 9, bowlIndex: 0) {
                        if let bowl1 = game.bowlResult(frameIndex: 9, bowlIndex: 1) {
                            if bowl0 == 10 {
                                if bowl1 == 10 { return "X" }
                                return String(bowl1)
                            }
                            if (bowl0 + bowl1 == 10) { return "/" }
                            return String(bowl1)
                        }
                    }
                    return "-"
                case 2:
                    if let bowl1 = game.bowlResult(frameIndex: 9, bowlIndex: 1) {
                        if let bowl2 = game.bowlResult(frameIndex: 9, bowlIndex: 2) {
                            if bowl2 == 10 { return "X" }
                            if bowl1 + bowl2 == 10 { return "/" }
                            return String(bowl2)
                        }
                    }
                    return "-"
                default:
                    return "-"
            }
        }
        switch game.frameState(frameIndex: frameIndex) {
            case .Strike:
                return bowlIndex == 0 ? "X" : ""
            case .Spare:
                if bowlIndex == 1 { return "/" }
                fallthrough
            case .Others:
                if let num = game.bowlResult(frameIndex: frameIndex, bowlIndex: bowlIndex)  {
                    return String(num)
                }
                return "-"
        }
    }

上記コードで、UITest も通るようになりました。

リファクタリング

先ほどの、bowlAsText メソッドで、10フレーム対応のコードの方が、通常フレーム処理のコードよりも大きくなってしまっていますので、別メソッドに分けることにしました。

bowlAsText と bowlAsTextForFrame10

    func bowlAsTextForFrame10(bowlIndex: Int) -> String {
        switch bowlIndex {
            case 0:
                if let bowl0 = game.bowlResult(frameIndex: 9, bowlIndex: 0) {
                    if bowl0 == 10 {
                        return "X"
                    }
                    return String(bowl0)
                }
                return "-"
            case 1:
                if let bowl0 = game.bowlResult(frameIndex: 9, bowlIndex: 0) {
                    if let bowl1 = game.bowlResult(frameIndex: 9, bowlIndex: 1) {
                        if bowl0 == 10 {
                            if bowl1 == 10 { return "X" }
                            return String(bowl1)
                        }
                        if (bowl0 + bowl1 == 10) { return "/" }
                        return String(bowl1)
                    }
                }
                return "-"
            case 2:
                if let bowl1 = game.bowlResult(frameIndex: 9, bowlIndex: 1) {
                    if let bowl2 = game.bowlResult(frameIndex: 9, bowlIndex: 2) {
                        if bowl2 == 10 { return "X" }
                        if bowl1 + bowl2 == 10 { return "/" }
                        return String(bowl2)
                    }
                }
                return "-"
            default:
                return "-"
        }
    }
    
    func bowlAsText(frameIndex:Int, bowlIndex: Int) -> String {
        if (frameIndex == 9) {
            return bowlAsTextForFrame10(bowlIndex: bowlIndex)
        }
        switch game.frameState(frameIndex: frameIndex) {
            case .Strike:
                return bowlIndex == 0 ? "X" : ""
            case .Spare:
                if bowlIndex == 1 { return "/" }
                fallthrough
            case .Others:
                if let num = game.bowlResult(frameIndex: frameIndex, bowlIndex: bowlIndex)  {
                    return String(num)
                }
                return "-"
        }
    }

メインとなるフローが見やすくなりました。

まとめ その5

ほとんど完成していますが、手動でアプリをテストしていると、1フレームに対して合計で10を超える入力ができてしまうことが目につきました。

次回、入力できないボタンを disable にするのと、ボタンの見た目を少し変更して終わりにしようかと思います。

説明は以上です。次回に続きます。Happy Coding!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です