Erlangとボウリングの点数とリストのパターンマッチ

Re: 配列の隣接する2項にそれぞれ演算を施した配列で、overlappingPairsCollect: というSmalltalkのメソッドが紹介されていました。面白そうなのでErlangで実装してみました。

-module(pairs).
-compile(export_all).

overlapping_pairs_collect(F, L) -> 
    overlapping_pairs_collect(F, L, []).

overlapping_pairs_collect(_F, [], Res) -> lists:reverse(Res);
overlapping_pairs_collect(_F, [_], Res) -> lists:reverse(Res);
overlapping_pairs_collect(F, [X, Y | L], Res) -> 
    overlapping_pairs_collect(F, [Y|L], [F(X, Y) | Res]).

実際に試してみます...

75> pairs:overlapping_pairs_collect(fun(X,Y) -> X + Y end, lists:seq(1,5)).
[3,5,7,9]
76> 

よいようです。実はこれを試すまで、Erlangでリストの先頭2項目以上のマッチングや連結ができるということを完全に失念しておりました。
というよりできないものだと勝手に思いこんでいて、でも文字列だと複数文字のマッチングできるしなあ...と若干納得のいかないところがあり、今回できることがきちんと確認できてよかったです。

できることが分かったので、同じくsumimさんのTDDBC 札幌 2.3 で遊びで書いてみたボウリングスコア集計もできそうだなと思いました。Haskell Bowling | xProgramming.comが元の問題のようです。Haskellのコードを素直に真似て書いたのが下記のコードです。

score([]) -> 0;
score([X]) -> X;
score([X,Y]) -> X + Y;
score([X,Y,Z]) -> X + Y + Z;
score([X,Y,Z|L]) ->
    if
	X =:= 10 -> X + Y + Z + score([Y,Z|L]);
	X + Y =:= 10 -> X + Y + Z + score([Z|L]);
	true -> X + Y + score([Z|L])
    end.

しかしこれがどうも思わしくありません。具体的には下記のようなリストで期待した結果が来ません。

76> pairs:score([10, 10, 10, 10, 10, 10, 10, 10, 10, 3, 3]).
249
77> 

本当は255点を期待しています(手で計算してもやはり255点でした)。そしてオリジナルのHaskellバージョンでも同じく249という誤ったスコアになってしまいます。

インタプリタになったつもりでトレースしてみたのですが、 前から順番に処理して、[10, 3, 3] にさしかかったとき、最初の10というのは第9フレームで倒したピンの数ですので、第9フレームとしての得点は10 + 3 + 3 = 16となり、第10フレームはストライク・スペアにかかわらず単に倒したピンの数を足せばいいので(ストライク・スペアが出たときは3投できますので3回で倒したピンの数を足す) 3 + 3 = 6 となります。したがって第9〜10フレームの合計スコアは16 + 6 = 22 です。第8フレームまでの合計スコアは233点なので、あわせて255点がゲーム終了時の点数になります。

上記コードでは、[10, 3, 3]にさしかかったとき、 score([X,Y,Z]) にマッチしてそれを全部足すという処理を行っています。これは第10フレームで10本、3本、3本と倒した場合は正しいのですが、今回はそうではありませんので、最後のパターンにマッチして、第9フレームとして10 + 3 + 3 を加算した後、第10フレームとして 3 + 3 を加算しなければいけません。でも、どっちのパターンにマッチするべきなのかは現在のフレームによって決まりますので、あれこれ考えてみたのですが、結局、フレーム数を保持しなければどうしようもないのでは?という結論に至りました。書きなおしたコードとテスト用コードは下記の通りです。

score2(L) ->
    score2(L,1).
score2(L, 10) -> lists:sum(L);
score2([X,Y,Z|L], Frame) -> 
    if
	X =:= 10 -> X + Y + Z + score2([Y,Z|L], Frame + 1);
	X + Y =:= 10 -> X + Y + Z + score2([Z|L], Frame + 1);
	true -> X + Y + score2([Z|L], Frame + 1)
    end.

test() -> 
    0 = score2([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]),
    300 = score2(lists:duplicate(12,10)),
    30 = score2(lists:flatten([10, 5, 5, lists:duplicate(17, 0)])),
    255 = score2([10, 10, 10, 10, 10, 10, 10, 10, 10, 3, 3]),
    161 = score2([0, 0, 10, 8, 2, 10, 10, 10, 5, 3, 8, 2, 10, 2, 3]),
    126 = score2([5, 3, 7, 2, 8, 2, 10, 7, 1, 9, 0, 6, 2, 10, 6, 4, 8, 0]),
    ok.

テストは無事に通り、そしてプログラムも若干単純になったように思います。第10フレームで何投したのか気にせず足すだけなど手抜きをしていますが... 一方、不完全なリスト(10フレームまで行っていないもの)ではエラーとなります。

今回久しぶりにボウリングのスコアの計算を試みたのですが、当時(10歳前後?)の理解がいろいろ間違っていたこともあり追跡に苦労しました。具体的には10フレーム目だけストライク・スペア出しても次に倒したピンの数が2倍のスコアにならないということを知りませんでした(そのかわりに3投できるというボーナスがつくのですね)。過去に自力で計算したスコアは多めに計算していたのかもしれません。もっとも中学時代はほとんどボウリングしなかったし、高校時代になればもうスコア計算は勝手にやってくれていたように思いますので、小学生時代の短い期間だけですね。そしてそのときはストライク・スペアなんてろくに出なかったのでまず問題ないでしょう... (どうでもいい)