Erlangとボウリングの点数 (2)

前回の続きです。前回、倒したピンの数をリストにして渡すとスコアが返ってくる関数を作りました。今回は以下のような点をいじってみたいと思います。

  • 途中経過が見えないのは寂しいので、各フレーム時点の合計得点が見たい
  • 10フレームすべての情報を与えないとエラーになってしまうのをなんとかしたい
  • せっかくなので見た目もスコアシートっぽく表示できるようにしたい

まず1つ目についてやってみます。各フレームの情報を、{倒したピンの数, 合計得点} というタプルのリストで返すような関数になるようscore2を改造します。

score3(L) ->
    score3(L, 1, 0, []).
score3(L, 10, Last, Res) -> lists:reverse([{L, Last + lists:sum(L)}|Res]);
score3([X, Y, Z|L], Frame, Last, Res) -> 
    if X =:= 10 ->
	    Score = Last + X + Y + Z,
	    score3([Y,Z|L], Frame + 1, Score, [{[X], Score}|Res]);
       X + Y =:= 10 ->
	    Score = Last + X + Y + Z,
	    score3([Z|L], Frame + 1, Score, [{[X,Y], Score}|Res]);
       true ->
	    Score = Last + X + Y,
	    score3([Z|L], Frame + 1, Score, [{[X,Y], Score}|Res])
    end;

つづいて2つ目...は、マッチの条件を増やせばいいかなと思いました。つまりこんな感じです。

score3([X, Y], _, Last, Res) -> 
    if X =:= 10 ->
	    lists:reverse([{[Y], undefined},{[X], undefined}|Res]);
       X + Y =:= 10 ->
	    lists:reverse([{[X, Y], undefined}|Res]);
       true ->
	    lists:reverse([{[X, Y], Last + X + Y}|Res])
    end;
score3([X], _, Last, Res) -> 
    if X =:= 10 ->
	    lists:reverse([{[X], undefined}|Res]);
       true ->
	    lists:reverse([{[X], Last + X}|Res])
    end.

ここまでで試してみましょう。

2> pairs:score3([5, 3, 7, 2, 8, 2, 10, 7, 1, 9, 0, 6, 2, 10, 6, 4, 8, 0]).
[{[5,3],8},
 {[7,2],17},
 {[8,2],37},
 {"\n",55},
 {[7,1],63},
 {[9,0],72},
 {[6,2],80},
 {"\n",100},
 {[6,4],118},
 {[8,0],126}]
3>

よいように見えますね("\n" となっているのは [10] に読み替えてください)。ではこのリストを受け取ってそれっぽく表示するための関数を作ります。名前はppでしょうか。

score3_pp(L) ->
    Res = score3(L),
    pp(Res).
    
pp(L) ->
    StrPins = 
	lists:map(
	  fun({Pins, _Score}) ->
		  case Pins of
		      [10] ->
			  "X| |";
		      Pins ->
			  {_, _, Str} = 
			      lists:foldl(
				fun(Pin, {No, Total, FStr}) ->
					{ No + 1,
					  case No of
					      2 ->
						  0;
					      No ->
						  Total + Pin
					  end,
					  FStr ++ 
					      case Pin of
						  undefined ->
						      " ";
						  0 when No =:= 2 ->
						      "-";
						  0 ->
						      "G";
						  10 ->
						      "X";
						  Pin when No =:= 2 andalso Total + Pin =:= 10 ->
						      "/";
						  Pin ->
						      integer_to_list(Pin)
					      end ++ "|"}
				end, {1, 0, ""}, Pins),
			  Str
		  end
	  end, L),
    StrScores = 
	lists:map(
	  fun({{_Pins, Score}, Pins}) ->
		  case Score of 
		      undefined ->
			  io_lib:format("~*s|", [iolist_size(Pins) - 1, ""]);
		      Score ->
			  io_lib:format("~*w|", [iolist_size(Pins) - 1, Score])
		  end
	  end, lists:zip(L, StrPins)),
    {Headers, Lines, _} = 
	lists:foldl(
	  fun(Str, {List, SLine, Frame}) ->
		  { [io_lib:format("|~*w", [iolist_size(Str) - 1, Frame])|List],
		    [io_lib:format("+~*..-s", [iolist_size(Str) - 1, ""])|SLine],
		    Frame + 1}
	  end, {[], [], 1}, StrPins),
    Line = string:join(lists:reverse(Lines), "") ++ "+",
    io:format("~s~n~s|~n~s~n|~s~n~s~n|~s~n~s~n", 
	      [
	       Line,
	       string:join(lists:reverse(Headers), ""), 
	       Line,
	       string:join(StrPins, ""),
	       Line,
	       string:join(StrScores, ""),
	       Line
	      ]).

まず倒したピンを表示する行を構成(StrPins)し、その長さを元にフレーム数の部分とスコアの部分、それから枠線を構成し、並べて表示させています。

  • 点数が確定しない(例: ストライク出した後に2投分の情報がない)フレーム以降は、スコア表示をしない
  • ストライク、スペアはそれぞれ記号 "X"、"/" で表示させる
  • 一本も倒せなかった場合は、投目によって記号を変える ("G" または "-")

といったあたりがキーポイントになります。また、枠線やフレーム数、スコアの部分はStrPinsの長さに依存するので、io_lib:formatの書式指定で構成。長さが事前に分からないので、長さ指定部分に "*" を与えることによって、外から長さを与えることができるようにしました。(以下参照)

13> io:format("~10..-s~n", ["ABCDE"]).
-----ABCDE
ok
14> io:format("~*..-s~n", [10,"ABCDE"]).
-----ABCDE
ok
15> 

さて、実際に試してみると...

3> pairs:score3_pp([5, 3, 7, 2, 8, 2, 10, 7, 1, 9, 0, 6, 2, 10, 6, 4, 8, 0]).
+---+---+---+---+---+---+---+---+---+---+
|  1|  2|  3|  4|  5|  6|  7|  8|  9| 10|
+---+---+---+---+---+---+---+---+---+---+
|5|3|7|2|8|/|X| |7|1|9|-|6|2|X| |6|/|8|-|
+---+---+---+---+---+---+---+---+---+---+
|  8| 17| 37| 55| 63| 72| 80|100|118|126|
+---+---+---+---+---+---+---+---+---+---+
ok
4> pairs:score3_pp(lists:duplicate(20,0)).
+---+---+---+---+---+---+---+---+---+---+
|  1|  2|  3|  4|  5|  6|  7|  8|  9| 10|
+---+---+---+---+---+---+---+---+---+---+
|G|-|G|-|G|-|G|-|G|-|G|-|G|-|G|-|G|-|G|-|
+---+---+---+---+---+---+---+---+---+---+
|  0|  0|  0|  0|  0|  0|  0|  0|  0|  0|
+---+---+---+---+---+---+---+---+---+---+
ok
5> pairs:score3_pp(lists:duplicate(12,10)).
+---+---+---+---+---+---+---+---+---+-----+
|  1|  2|  3|  4|  5|  6|  7|  8|  9|   10|
+---+---+---+---+---+---+---+---+---+-----+
|X| |X| |X| |X| |X| |X| |X| |X| |X| |X|X|X|
+---+---+---+---+---+---+---+---+---+-----+
| 30| 60| 90|120|150|180|210|240|270|  300|
+---+---+---+---+---+---+---+---+---+-----+
ok
6> pairs:score3_pp([10,1,2]).
+---+---+
|  1|  2|
+---+---+
|X| |1|2|
+---+---+
| 13| 16|
+---+---+
ok
7> pairs:score3_pp([0,0,8,2]).
+---+---+
|  1|  2|
+---+---+
|G|-|8|/|
+---+---+
|  0|   |
+---+---+
ok
8> 

大丈夫そうです。ppのあたりはもうちょっとスマートに書けないもんかと思います。また、点数が確定していないときは表示しないようにしていますが、最低でも〜〜点はとれるという情報を表示するのもありかなと思いました(つまり最後の例だと最低でも10点はとれていますので、カッコつきで表示させるなど)。

そもそも ppといいつつあんまりprettyな表示になっていないという問題がありますが、製造者のセンスではこのくらいが限界ですね。