Aggregated bowling score simulation with VueJS recursion

Why I made this

Today I found an interesting job posting that required the applicant to complete an acceptence task in order to be considered for the position.
The task was interesting because of several parameters

  • Well defined
  • Small but with complexity
  • Able to include recursion
  • Required demonstration of using API endpoints but GET and POST
  • Required to use token based API
  • Demonstrated TDD

Essentially I find it to be an interesting project that at the same time tackles many of common problems that is required for a job in the web tech industry.

The task

The task that needs to be completed the score of a simulated ten pin bowling game. The rules were not explained but instead there's a reference to the Wikipedia page https://en.wikipedia.org/wiki/Ten-pin_bowling#Traditional_scoring. So already here you have to show, that you can read a defined task and pin it down to subtasks.

Also given are 2 API endpoints, one that returns a JSON of rounds and a token, the other that validates your calculations and verifies the game by using the token. So here you have the demonstration of API endpoints and the requirements of using token based API. I'm not saying token based APIs are complex, but it's a neat example on how it works.

API is defined as:

  • Method: GET

  • URL: http://37.139.2.74/api/points

  • Description:

    • Returning a JSON list of random number of rounds and valid score combinations.
    • Strike is represented as [10, 0]
    • Spare is represented as for example [7, 3], [0, 10] or [5, 5]
  • Method: POST

  • URL: http://37.139.2.74/api/points

  • Parameters:

    • token: string
    • points: json
  • Description:

    • Token as received in GET
    • Points as calcaluated sums
    • Returning HTTP status 200 upon received and { "success": true } if valid otherwise { "success: false }
    • The sum is a list of accumulated results.
    • Example game
      • The rounds [[3,7],[10,0],[8,2],[8,1],[10,0],[3,4],[7,0],[5,5],[3,2],[2,5]]
      • Gives the sums [20,40,58,67,84,91,98,111,116,123]
      • Wit the total score of 123 in this case after 10 rounds

How I solved it

In this task you have been given a test case. First I must admit I failed to implement automated tests, but this is actually very easy since you can obviously use the example game for this. I did it the manual way while developing, but you can as well make your test() method for that.

So what you first do is instead of using the API GET endpoint for a random set of rounds you use the given example as you in this case both know the input and the desired output.

Once this is done you will see in the rules for strikes and spares that these are edge cases. Especially if the game ends with a spare or a strike it's an edge case. So what I did next was to take the game where you only have strikes:

[10, 0], [10, 0], [10, 0], [10, 0], [10, 0], [10, 0], [10, 0], [10, 0], [10, 0], [10, 0], [10, 10]

And make this valid. Note the last round is because you have 2 extra shots as you finish on a strike. Finishing on a spare will give you 1 extra shot. As any game exists of 10 rounds, technically I overcome this to make it apply for my algorithm by dividing any 11th round into 2 arrays instead of 1, so it becomes normalized.

The result

My final code example is made of Vue and Vue-Resource for the AJAX requests. Feel free to leave any comments :-)

<!doctype html>

<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Bowling Game</title>
    </head>

    <body>
        <div id="app">
            {{ rounds }}
            <br>
            <br>
            {{ points }}
            <br>
            <br>
            {{ score }}
        </div>
        
        <script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
        <script src="//cdnjs.cloudflare.com/ajax/libs/vue-resource/1.0.3/vue-resource.js"></script>
        <script>
            new Vue({
                /**
                 * Application binding.
                 */
                el: &#039;#app&#039;,

                /**
                 * Application variables.
                 */
                data: {
                    token: null,
                    points: [],
                    rounds: []
                },

                /**
                 * Start the application
                 *
                 * Initializes the applications, performs remote requests and outputs result to console.
                 */
                mounted() {
                    this.$http.get(&#039;http://37.139.2.74/api/points&#039;).then(function(response) {
                        this.token  = response.data.token;
                        this.rounds = this.prepare(response.data.points);
                        this.points = this.calculate(this.rounds);

                        this.$http.post(&#039;http://37.139.2.74/api/points&#039;, {
                                &#039;token&#039;:  this.token,
                                &#039;points&#039;: this.score
                            }).then(function(response) {
                                console.log(&#039;Is valid: &#039;+ response.data.success);
                            }.bind(this));
                    }.bind(this));
                },

                computed: {
                    /**
                     * Get final computed score array.
                     * Will add each round of points together increasingly to form an array of the scoreboard.
                     * 
                     * @return Array
                     */
                    score: function() {
                        var sum   = 0;
                        var score = []

                        this.forEach(this.points, function(point) {
                            sum += point;
                            score.push(sum);
                        });

                        return score;
                    }
                },

                methods: {
                    /**
                     * Calculate round results.
                     * Will return an array of points per round.
                     * 
                     * @param  Array    rounds  Rounds in game.
                     * @return Array
                     */
                    calculate(rounds) {
                        var points = [];

                        this.forEach(rounds, function(round, index) {
                            if (index > 9) return;

                            points.push(this.getPoints(round, index, rounds, (index + 2)));
                        }.bind(this));

                        return points;
                    },

                    /**
                     * Determine if round is strike.
                     * 
                     * @param  Array    round   Round to examine.
                     * @return Boolean
                     */
                    isStrike: function(round) {
                        return round[0] === 10;
                    },

                    /**
                     * Determine if round is spare.
                     * 
                     * @param  Array    round   Round to examine.
                     * @return Boolean
                     */
                    isSpare: function(round) {
                        return round[0] < 10 &amp;&amp; round[0] + round[1] === 10;
                    },

                    /**
                     * Determines if index is last entry of array.
                     * 
                     * @param  Number   index    Entry index.
                     * @param  Array    array    Array to analyze.
                     * @return Boolean
                     */
                    isLast: function(index, array) {
                        return index + 1 === array.length;
                    },

                    /**
                     * Sum points for round.
                     *
                     * The method will recursively sum up for strikes and spares. The limit is needed for
                     * the method to know, how far ahead in the rounds array it is allowed to sum st rikes.
                     * For default rules in a ten-pin bowling games should 2.
                     * 
                     * @param  Array    round   Round to examine.
                     * @paream Number   index   Current index in rounds array. Should in most cases be 0.
                     * @param  Array    rounds  All rounds in game.
                     * @param  Number   limit   How far in the round array to sum up for strike. Usually (index + 2)
                     * @return Number           Total score for round.
                     */
                    getPoints: function(round, index, rounds, limit) {
                        var score = round[0] + round[1];

                        if (this.isStrike(round) &amp;&amp; (index < limit &amp;&amp; index < 11 &amp;&amp; ! this.isLast(index, rounds)))
                            score += this.getPoints(rounds[index + 1], (index + 1), rounds, limit);

                        if (this.isSpare(round) &amp;&amp; ! this.isLast(index, rounds))
                            score += rounds[index + 1][0];

                        return score;
                    },

                    /**
                     * If extra 11th round is given, prepare the array for the calculation algorithms.
                     * This will form the eventually extra 11th round from 1 into 2 subsets calcuable
                     * by the algorithm. 
                     * 
                     * @param  Array    rounds  Rounds of game.
                     * @return Array
                     */
                    prepare: function(rounds) {
                        if (rounds.length > 10) {
                            rounds.push([rounds[10][1], 0]);
                            rounds[10][1] = 0;
                        }

                        return rounds;
                    },

                    /**
                     * Map array.
                     * 
                     * @param  Array        array      Array to be mapped.
                     * @param  Function     callback   Callback to perform on each entry.
                     */
                    forEach: function(array, callback) {
                        for (var i = 0; i < array.length; i++)
                            callback(array[i], i);
                    }
                }
            })
        </script>
    </body>
</html>