TDDBC 札幌 2.3 で遊びで書いてみたボウリングスコア集計


札幌名物の Smalltalk ペアとして、@t_hachinoheさんと組んで楽しく参加しました。

TDD の演習の成果物としてはちゃんとオーソドックスな実装をしたのですが、それとは別に、http://xprogramming.com/xpmag/dbcHaskellBowling でリストを切り分ける動きを Smalltalk のストリームオブジェクトの振る舞いに(少々無理矢理)置き換えてみるとこんな感じかなとというのを書いてみたので紹介します(当日はこれとほぼ同じ処理を SequenceableCollection>>#score として実装)。日本語版Squeak4.2などのワークスペースへのコピペ→全選択→alt/cmd + p で試せます。

| score |
score := [:pins |
   | framePoints |
   pins := pins readStream.
   framePoints := OrderedCollection new.
   9 timesRepeat: [
      framePoints add: (pins next: 3) asOrderedCollection.
      pins back.
      framePoints last first = 10 ifTrue: [pins back] ifFalse: [
         (framePoints last first: 2) sum = 10
            ifFalse: [framePoints last removeLast]]].
   framePoints add: pins upToEnd.
   (framePoints collect: #sum) sum].

score value: #(10 10 10 10 10 10 10 10 10 10 10 10).  "=> 300 "
score value: #(10 10 10 10 10 10 10 10 10 3 3).  "=> 255 "
score value: #(0 0 10 8 2 10 10 10 5 3 8 2 10 2 3).  "=> 161 "
score value: #(5 3 7 2 8 2 10 7 1 9 0 6 2 10 6 4 8 0).  "=> 126 "


各投球で倒したピンの数の列である pins をストリームでラップして順次読んでゆくのがミソで、まず(パターンマッチなどという便利なものはないので―)無条件でごっそり3つ先読みし(pins next: 3)これをスコア計算用配列(framePoints)に追加します。その後、ストライクやスペアでなければ最後のひとつを捨て(framePoints last removeLast)、同時にストリームのカーソルも通常は1つ、ストライクの場合2つ戻しつつこれを9回繰り返す、というものです。最終フレームのスコアは単純な合計なのでストリームの読み残し(pins upToEnd)を framePoints に追加しておしまいです。

順次加算してスコアだけ戻してもよかったのですが、最後の一行の「要素の合計を合計する」((framePoints collect: #sum) sum)が書きたくて、計算の途中経過も残す実装にしてあります。こうしたほうが動作確認もしやすいですしね。



もうひとつ、お題にある(ただし演習では @t_hachinoheが Smalltalk初体験であることを考えて簡単のためあえて無視した―)リアルタイム集計を意識して実験的に組んでみたのがこちらです。

| score |
score := [:frames |
   | rolls nRoll framePoints |
   rolls := OrderedCollection new.
   nRoll := 0.
   framePoints := frames collectWithIndex: [:framePins :idx |
      | framePoint |
      framePins do: [:pin | (rolls at: (nRoll := nRoll + 1) ifAbsentPut: [{nil}]) at: 1 put: pin].
      framePoint := OrderedCollection with: framePins sum.
      (idx < 10 and: [framePoint first = 10]) ifTrue: [
         (1 to: 3 - framePins size) do: [:delta |
            framePoint add: (rolls at: nRoll + delta ifAbsentPut: [{nil}])]].
      framePoint asArray].
   (framePoints collect: #sum) sum first].

score value: #((10) (10) (10) (10) (10) (10) (10) (10) (10) (10 10 10)).  "=> 300 "
score value: #((10) (10) (10) (10) (10) (10) (10) (10) (10) (3 3)).  "=> 255 "
score value: #((0 0) (10) (8 2) (10) (10) (10) (5 3) (8 2) (10 2 3)).  "=> 156 "
score value: #((5 3) (7 2) (8 2) (10) (7 1) (9 0) (6 2) (10) (6 4) (8 0)).  "=> 126 "


前のものと違って、こちらではお題と似たふうにしてフレーム毎に倒したピン数の列を要素に持つ配列の入力を想定しています。

各投球で倒したピン数を収めるコレクション(rolls)を用意し、そこにフレーム毎の各々の投球で倒したピン数を属するフレームを考えずに順次登録している点が工夫のひとつです。この rolls には、各投球で倒したピン数を直接入れるのではなく、ピン数を要素1つの配列に収めてから、改めてそれを rolls に収めています。

ストライクやスペアでボーナスが発生した場合には、未来の投球で倒すことになるピン数を収める要素一つの配列を rolls に追加し(rolls at: nRoll + delta ifAbsentPut: [{nil}]))、フレームのピン数集計のときにすでに登録されているピン数を収めるべき要素一つの配列があるのならそれを使う((rolls at: (nRoll := nRoll + 1) ifAbsentPut: [{nil}]) at: 1 put: pin)という実装になっています。GUI にしたあかつきに、Objective-C の KVO のような仕組みの活用をちょっと意識した感じですね。

最後はお気に入りでおきまりの各要素の合計の合計(要素一つの配列が返るのでさらに first で要素取り出し)して集計値を返しています。