[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投するテスト

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

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

addBowlResult

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

frameScore(分割後)

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

frameScoreFor10th code

frameScore code

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

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

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

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

calcStrikeFrame code

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

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

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

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

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

example code

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

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

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

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

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

test_recordScore_12Strikes_displayedCorrectly code

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

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

ビューの変更

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

BowlingGameViewModel の bowlAsText メソッド

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

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

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

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

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

リファクタリング

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

bowlAsText と bowlAsTextForFrame10

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

まとめ その5

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

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

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

コメントを残す

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