Thursday, April 14

r-nd-m: r-nd-m: TDD bowling, and J, revised, part3

This is a continuation of TDD bowling, and J, revised, part 1 and TDD bowling, and J, revised, part 2

I left off with:

 isScore=: -:&(+/@,) , (10 3 >: $@]) , 2 -: #@$@]
 F=: ] + 1 + 10 > {.@{~
 bowlingScores=: ({~ F^:(i.10)&0)@(3 ]\ ])

 assert 300 isScore bowlingScores 12#10
 assert 190 isScore bowlingScores 21$9 1

...and the first thing we need to do is find a better name for F, one that will be useful without having to go back and re-read yesterday's blog.  I'm going to go with nextFrameIndex.

 nextFrameIndex=: ] + 1 + 10 > {.@{~
 bowlingScores=: ({~ nextFrameIndex^:(i.10)&0)@(3 ]\ ])

and with that, I can get on to my next test:

 assert 0 isScore bowlingScores 20#0

This fails:

    assert 0 isScore bowlingScores 20#0
 |index error: bowlingScores
 |   assert 0 isScore     bowlingScores 20#0

My problem here is that I am indexing off the end of my array.  3 ]\ 20$0 has 18 elements but the ninth (and last) invocation of nextFrameIndex will give me 18 as its result, and the largest valid index is 17.

The simple solution here, I think, is to pad out my result so that 18 will always be a valid index.  Since I only have 10 frames (corresponding to 0 through 9 invocations of nextFrameIndex) that should be enough.  And, since I am ultimately adding up the results, padding with zeros will not change the ultimate score.  (And, in fact, I have been planning on using zeros, all along, to deal with an issue that I do not yet have a test for.)

 bowlingScores=: ({~ nextFrameIndex^:(i.10)&0)@(3 ]\ ])@{.~&21

That's good enough for now, though my definition is getting a bit long-winded.  I also have multiple "magic numbers" here.  I am not sure that that is bad, but here they are:

10:  The number of frames in a complete game
21:  The number of balls in the longest possible game
3:   The number of balls in the longest possible frame

I think that that information is interesting enough that I would leave it in a comment in the completed program.

But now we need another test:

   assert 90 isScore bowlingScores 20$8 1

And the current implementation of bowlingScores gives me a score of 162 -- it needs to deal with frame that are neither spares nor strikes.  In other words, if the sum of the first two elements of a frame is less than 10 the third element of my representation of that frame must be a zero.

One approach could be:
   (0&{, 1&{, 2&{ * 10 > 0&{ + 1&{)

For example:

    3 ]\ 7$8 1
 8 1 8
 1 8 1
 8 1 8
 1 8 1
 8 1 8
    (0&{, 1&{, 2&{ * 10 <: 0&{ + 1&{)"1 (3 ]\ 7$8 1)
 8 1 0
 1 8 0
 8 1 0
 1 8 0
 8 1 0


But that is long-winded for what seems like it ought to be a simple concept.  Can we do better?

Well.. one possbility involves multiplication.  From this point of view, we want to multiply each row by either 1 1 1 or 1 1 0, with that last bit depending on the sum of the previous two values.  That previous sum can be expressed as the inner product of 1 1 0 and the three numbers from my potential frame.

In other words, something like this:

    (3 ]\ 7$8 1) +/ .* 1 1 0
 9 9 9 9 9
    10 <: (3 ]\ 7$8 1) +/ .* 1 1 0
 0 0 0 0 0
    1,.1,.10 <: (3 ]\ 7$8 1) +/ .* 1 1 0
 1 1 0
 1 1 0
 1 1 0
 1 1 0
 1 1 0

    (3 ]\ 7$8 1) * 1,.1,.10 <: (3 ]\ 7$8 1) +/ .* 1 1 0
 8 1 0
 1 8 0
 8 1 0
 1 8 0
 8 1 0

    (] * 1,.1,.10 <: ] +/ .* 1 1 0"_) 3 ]\ 7$8 1
 8 1 0
 1 8 0
 8 1 0
 1 8 0
 8 1 0


Except, I have some bad experiences with how people react to expressions of the form: noun"_  Some people do not look at that and see the resemblance to diaeresis and realize that this is a variation on the old "each" operator from APL.  Instead, they see the double quotes and wonder why I have an unterminated string constant.

I could avoid that kind of reaction be rephrasing:


   (] * 1,.1,.10 <: +/ .*&1 1 0) 3 ]\ 7$8 1
8 1 0
1 8 0
8 1 0
1 8 0
8 1 0

Here, I have curried the inner product (+/ .*) with the vector 1 1 0 (on its right).  That derives a verb which only needs one argument, which will appear on the right. And, that is a slight be more concise than my previous expression so I think I will use this variation.

(People also react badly to my "unbalanced brackets".  They do not see them as left and right identity functions but as some unknown construct that would satisfy rules for some other programming language.  But I find myself often using them, with no better alternatives.)

   limitSpares=:  ] * 1,.1,.10 <: +/ .*&1 1 0
   bowlingScores=: limitSpares@({~ nextFrameIndex^:(i.10)&0)@(3 ]\ ])@{.~&21

And that satisfies my current set of tests

Is that getting too long?

   #'bowlingScores=: limitSpares@({~ nextFrameIndex^:(i.10)&0)@(3 ]\ ])@{.~&21'
73

I think I can live with a 73 character long line.  But if it were much longer, I would be feel I should refactor that definition.  As it is, I would not reject a refactoring.  But I do not feel like doing that right now.

So, here is my current set of definitions:

 NB. 10:  The number of frames in a complete game
 NB. 10:  The highest possible score with one ball
 NB. 21:  The number of balls in the longest possible game
 NB. 3:   The number of balls in the longest possible frame
 NB. 2:   The number of dimensions in the desired result (frames and balls)


 nextFrameIndex=: ] + 1 + 10 > {.@{~
 limitSpares=: ] * 1,.1,.10 <: +/ .*&1 1 0
 bowlingScores=: limitSpares@({~ nextFrameIndex^:(i.10)&0)@(3 ]\ ])@{.~&21


 isScore=: -:&(+/@,) , (10 3 >: $@]) , 2 -: #@$@]
 assert 300 isScore bowlingScores 12#10
 assert 190 isScore bowlingScores 21$9 1
 assert 0 isScore bowlingScores 20$0
 assert 90 isScore bowlingScores 20$8 1

And, I think that that I have an adequate set of definitions.  I have a slight temptation to try and distinguish between the two meanings for the number 10, but that is inherent in the context of bowling -- you do not know if a bowler saying "10" means "10 pins" or "10 frames" until you get a bit more information.

Next up:  I should perhaps go back and take some of these sentences and show how J evaluates them.  I think I did an ok job of illustrating "limitSpares", but some of the other lines I just kind of thew out there.  And, if in so doing I feel inspired to distinguish between the two meanings for "10", or if I think up better names for some of my words, ... well... it could happen.  And I hope my next post will not be too tedious.

Update: I could have defined:

   limitSpares=:  #"1~ 1,.1,.10 <: +/ .*&1 1 0

This would give me a two column result for games that had no spares nor strikes.  But  I do not see a use for that complication.

No comments:

Post a Comment