MIND IF I DO A J

Scoring bowling in J

Fuck it Dude, let’s go bowling.

Inspired by both The Dude and Ron Jeffries who was working on this problem as an exercise in learning J.

Of Note

I’m pretty new to J, so please do contact me if you notice a mistake. Thanks to Tracy Harms for spotting a few mistakes.

There is a known bug computing scores with 0 10 spares, so don’t take this code as gospel.

How bowling is scored

There are 10 frames, of 2 balls. If a player knocks down N pins in the frame, they are awarded N points. If a player knocks down all the pins in 2 balls (a spare) they are awarded bonus points equivalent to the score of their next ball. If a player knocks down all the pins with the first ball of a frame (a strike) they are awarded bonus points equivalent to the next two balls. Check out Wikipedia if this riveting, gripping scoring structure has you yearning for more.

Data format

Let’s store our rolls in an array:

    rolls=: 4 5 5 5 10 2 3 4 5 4 5 4 5 10 5 5 4 5

A strike is listed as a frame with a single ball, there’s no padding in our input.

Solving overview

Our general array-centric approach to this problem will be:

Finding balls that start a frame

This is a pretty simple task, and if there were no strikes in the game it would be even simpler! For a game in which each frame has two balls per frame, every other ball starts a frame: balls 0 2 4 6 8 10 12 14 16 18. Unfortunately, sometimes the bowlers get strikes, and that throws our ‘every other ball’ technique off.

To start, let’s index all the balls and find all the strikes.

    i.#rolls    NB. indexes of all the rolls
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
    10=rolls    NB. where we have a strike
0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0

Using these two arrays we can pad our data, enabling us to take every other item from an array of roll indexes. A simple way to do this is with # (Copy).

    1+10=rolls                  NB. 1 of every ball, 2 of every strike ball
1 1 1 1 2 1 1 1 1 1 1 1 1 2 1 1 1 1
    (1+10=rolls)#i.#rolls       NB. do our copies
0 1 2 3 4 4 5 6 7 8 9 10 11 12 13 13 14 15 16 17

Cool, now we have our input padded out and we can take every other item to get the balls that start each frame.

fx=:13 : ‘(2*i.<.2%~(20<.+/1+10=y)){(1+10=y)#i.#y’

    2*i.2%~+/1+10=rolls         NB. even items up to array length
0 2 4 6 8 10 12 14 16 18
    2*i.2%~(20<.+/1+10=rolls)   NB. even items up to array length MAX of 20
0 2 4 6 8 10 12 14 16 18
    (2*i.2%~(20<.+/1+10=rolls)) { (1+10=rolls)#i.#rolls  NB. take the indexes we need
0 2 4 5 7 9 11 13 14 16

Let’s write a function to do this next time:

fx=: 13 : '(2*i.2%~(20<.+/1+10=y)){(1+10=y)#i.#y'

Scoring each ball

Every ball that starts a frame has the score of itself, plus the next ball and maybe plus a third ball. If you roll a 2 3, your score for the first ball is 5 (2 balls + 0 bonus balls). If you roll a 10 2 3 your score for the first ball is 15 (1 strike + 2 bonus balls). If you roll a 7 3 2, your score for the first ball is 12 (2 ball spare + 1 bonus ball). The third ball is included only if the sum of the first two balls is 10 or greater (either a strike + 2 balls, or a spare + 1 ball).

So to sum the first ball, add rolls to rolls shifted left by 1 position:

    1|.!.0 rolls                NB. shift and pad with 0
5 5 5 10 2 3 4 5 4 5 4 5 10 5 5 4 5 0
    rolls+1|.!.0 rolls          NB. add rolls to shifted by 1 rolls
9 10 10 15 12 5 7 9 9 9 9 9 15 15 10 9 9 5

Then find the third ball where the sum of the first two is 10 or greater:

    9<rolls+1|.!.0 rolls        NB. where 10 or greater
0 1 1 1 1 0 0 0 0 0 0 0 1 1 1 0 0 0
    (2|.!.0 rolls)*9<rolls+1|.!.0 rolls     NB. where 10 or greater times shifted by 2
0 5 10 2 3 0 0 0 0 0 0 0 5 5 4 0 0 0

And add the first two to the potential third ball:

    (rolls + 1|.!.0 rolls)+(2|.!.0 rolls)*9<rolls + 1|.!.0 rolls
9 15 20 17 15 5 7 9 9 9 9 9 20 20 14 9 9 5

Write a function for this:

    sf=: 13 : '(y+1|.!.0 y)+(2|.!.0 y)*9<y+1|.!.0 y'

Putting it all together

Take the frame starting balls from our potential scores, sum them and we have the final score.

    +/(fx rolls){sf rolls   NB. our final score!
119

And as a function (throwing in a little bit of tacit programming):

    fx=: 13 : '(2*i.2%~(20<.+/1+10=y)){(1+10=y)#i.#y'
    sf=: 13 : '(y+1|.!.0 y)+(2|.!.0 y)*9<y+1|.!.0 y'
    sc=: [:+/fx{sf
    sc rolls
119

And that’s how you score a bowling game.