Bowling in Bash: Complete Solution & Deep Dive Guide

grayscale photo of escalator in tunnel

From Zero to Hero: Master Bowling Game Logic with Bash Scripting

Calculating a bowling score involves surprisingly complex logic, with strikes, spares, and a unique 10th frame creating tricky edge cases. This guide provides a complete walkthrough for building a robust bowling score calculator in Bash, transforming a challenging algorithmic problem into a practical lesson in advanced shell scripting.


The Challenge: More Than Just Adding Numbers

You've just finished a game of bowling with friends. As you look at the scoring monitor, you see the numbers update automatically. A strike here adds the next two balls, a spare adds the next one. It seems simple on the surface, but have you ever tried to calculate it manually? It's a mental juggling act of remembering future rolls to calculate past frames.

Now, imagine being tasked with automating this process using only a Bash script. Your first thought might be, "Isn't Bash just for moving files and running commands?" While it excels at that, this very challenge is what makes it a powerful learning tool. Tackling the bowling score problem in Bash forces you to master core concepts like array manipulation, arithmetic expansion, and state management in a way that simple file operations never could. This guide will walk you through every step, turning a daunting task into a clear and achievable goal.


What is the Bowling Score Problem?

The bowling score problem, a popular challenge in the exclusive kodikra learning path, requires writing a program that correctly calculates the score of a single player's bowling game. A game consists of 10 frames, and the rules for scoring are what introduce the complexity.

To solve this, we must accurately model these rules in our code:

  • The Frame: A game is divided into 10 frames. In the first nine frames, a player gets up to two rolls to knock down 10 pins.
  • Open Frame: If the player knocks down fewer than 10 pins in two rolls, the score for that frame is simply the total number of pins knocked down. For example, rolling a 3 and then a 5 results in a frame score of 8.
  • Spare: If the player knocks down all 10 pins with two rolls, it's a "spare". The frame score is 10, plus a bonus equal to the number of pins knocked down on the very next roll.
  • Strike: If the player knocks down all 10 pins on the first roll of a frame, it's a "strike". The frame is over. The score is 10, plus a bonus equal to the total pins knocked down on the next two rolls.
  • The 10th Frame: This frame is special.
    • If a player rolls a spare in the 10th frame, they get one bonus roll.
    • If a player rolls a strike in the 10th frame, they get two bonus rolls.
    • These bonus rolls are only for scoring; they do not create an 11th or 12th frame. The game always ends after the 10th frame is complete.

A perfect game consists of 12 consecutive strikes (one for each of the first 9 frames, and three in the 10th frame), resulting in a maximum score of 300.


Why Use Bash for This Algorithmic Challenge?

At first glance, a language like Python or Java might seem more suited for this task. They have more complex built-in data structures and a more conventional syntax for algorithmic problems. However, choosing Bash offers unique and valuable learning opportunities for anyone serious about system administration, DevOps, or automation.

Solving this problem in Bash forces you to:

  • Master Array Manipulation: You'll learn the ins and outs of declaring, populating, and iterating over arrays in Bash, a skill crucial for processing lists of files, servers, or, in this case, bowling rolls.
  • Leverage Arithmetic Expansion: You will become intimately familiar with the ((...)) syntax for performing integer arithmetic, a cornerstone of any non-trivial script.
  • Implement State Management: Your script needs to track its position in the array of rolls and the current frame number. This teaches you how to manage state within a shell script, a fundamental concept in programming.
  • Think Within Constraints: Bash has its limitations. Working within them encourages creative problem-solving and a deeper understanding of how the shell works under the hood. It builds a foundational skill set that makes you a more effective scripter.

Ultimately, if you can solve the bowling problem in Bash, you can confidently handle most data processing and automation tasks that come your way in a Linux/macOS environment. For more foundational knowledge, explore our complete Bash with our in-depth guides.

Pros and Cons of Using Bash

Aspect Pros (Advantages of Using Bash) Cons (Challenges and Risks)
Environment Ubiquitous on Linux and macOS systems. No setup or compiler needed. Excellent for quick prototyping. Less portable to Windows without subsystems like WSL or Cygwin.
Syntax Concise for command-line operations and file manipulation. Can be quirky and less intuitive for complex algorithms compared to general-purpose languages. Error handling is more verbose.
Data Types Forces a deep understanding of how to handle data as strings and perform explicit arithmetic conversions. Essentially typeless (everything is a string). Lacks built-in complex data structures like dictionaries or objects.
Debugging Simple debugging using set -x to trace command execution. Lacks sophisticated debugging tools like breakpoints and step-through inspectors found in IDEs.
Performance Fast for I/O-bound tasks and orchestrating other command-line tools. Significantly slower for CPU-intensive calculations due to its interpreted nature.

How to Structure the Bash Solution: The Complete Code

Our approach will be to create a single, self-contained Bash script that takes the sequence of rolls as command-line arguments. The core of the script will be a loop that iterates 10 times, once for each frame. Inside the loop, we'll analyze the rolls for the current frame to determine if it's a strike, a spare, or an open frame, and calculate the score accordingly.

We need a variable to keep track of our current position in the list of rolls, as a strike consumes one roll from the list for the frame, while a spare or open frame consumes two.

The Main Logic Flow

Here is a high-level visual representation of how our script will process the game frame by frame.

    ● Start
    │
    ▼
  ┌───────────────────┐
  │ Read Rolls into Array │
  └──────────┬──────────┘
             │
             ▼
  ┌───────────────────┐
  │ Loop 1 to 10 (Frames) │
  └──────────┬──────────┘
             │
             ├───▶ Is Current Frame a Strike? ───▶ Calculate Strike Score
             │
             ├───▶ Is Current Frame a Spare? ────▶ Calculate Spare Score
             │
             └───▶ Otherwise (Open Frame) ─────▶ Calculate Open Frame Score
             │
             ▼
  ┌───────────────────┐
  │ Add Frame Score to Total │
  └──────────┬──────────┘
             │
             ▼
  ┌───────────────────┐
  │ Advance Roll Index      │
  └──────────┬──────────┘
             │
             ▼
    ● End Loop
    │
    ▼
  ┌───────────────────┐
  │ Print Total Score       │
  └──────────┬──────────┘
             │
             ▼
    ● End

The Complete Bash Script

Here is the fully commented, production-ready script. Save it as bowling.sh.


#!/usr/bin/env bash

# bowling.sh
# A script to calculate the score of a bowling game.
# Sourced from the kodikra.com exclusive curriculum.

# Enable strict mode
set -o errexit
set -o nounset
set -o pipefail

# --- Main Scoring Function ---
main() {
  # Store all command-line arguments (the rolls) into an array
  local rolls=("$@")
  
  # Validate the input before proceeding
  validate_input "${rolls[@]}"

  local total_score=0
  local roll_index=0
  
  # Loop through the 10 frames of the game
  for frame in {1..10}; do
    # Check for a strike
    if is_strike "$roll_index" "${rolls[@]}"; then
      # A strike is 10 plus the next two rolls
      (( total_score += 10 + ${rolls[roll_index + 1]} + ${rolls[roll_index + 2]} ))
      # A strike consumes only one roll in the frame
      (( roll_index++ ))
    # Check for a spare
    elif is_spare "$roll_index" "${rolls[@]}"; then
      # A spare is 10 plus the next one roll
      (( total_score += 10 + ${rolls[roll_index + 2]} ))
      # A spare consumes two rolls in the frame
      (( roll_index += 2 ))
    # Otherwise, it's an open frame
    else
      # An open frame is the sum of its two rolls
      (( total_score += ${rolls[roll_index]} + ${rolls[roll_index + 1]} ))
      # An open frame consumes two rolls
      (( roll_index += 2 ))
    fi
  done
  
  echo "$total_score"
}

# --- Helper Functions ---

# Checks if the current position represents a strike
is_strike() {
  local index=$1
  shift
  local -a current_rolls=("$@")
  [[ ${current_rolls[index]} -eq 10 ]]
}

# Checks if the current position represents a spare
is_spare() {
  local index=$1
  shift
  local -a current_rolls=("$@")
  # A spare requires two rolls that sum to 10
  [[ $((current_rolls[index] + current_rolls[index + 1])) -eq 10 ]]
}

# --- Input Validation Function ---
validate_input() {
  local -a rolls_to_validate=("$@")
  local num_rolls=${#rolls_to_validate[@]}
  
  if [[ $num_rolls -lt 12 ]]; then
    echo "Error: Incomplete game. At least 12 rolls are needed for a valid game with strikes." >&2
    exit 1
  fi
  
  local current_roll_index=0
  for frame in {1..10}; do
    if [[ $current_roll_index -ge $num_rolls ]]; then
      echo "Error: Not enough rolls for 10 frames." >&2
      exit 1
    fi

    local roll1=${rolls_to_validate[current_roll_index]}

    # Validate individual roll values
    if ! [[ "$roll1" =~ ^[0-9]+$ ]] || [[ "$roll1" -lt 0 ]] || [[ "$roll1" -gt 10 ]]; then
      echo "Error: Invalid pin count. Rolls must be between 0 and 10." >&2
      exit 1
    fi
    
    # Strike logic
    if [[ $roll1 -eq 10 ]]; then
      (( current_roll_index++ ))
      # In the 10th frame, bonus rolls must be valid
      if [[ $frame -eq 10 ]]; then
        local bonus1=${rolls_to_validate[current_roll_index]}
        local bonus2=${rolls_to_validate[current_roll_index+1]}
        if [[ $bonus1 -lt 0 ]] || [[ $bonus1 -gt 10 ]]; then
          echo "Error: Invalid pin count on bonus roll." >&2; exit 1
        fi
        if [[ $bonus2 -lt 0 ]] || [[ $bonus2 -gt 10 ]]; then
          echo "Error: Invalid pin count on bonus roll." >&2; exit 1
        fi
        # If the first bonus roll is not a strike, the next two can't sum to more than 10
        if [[ $bonus1 -ne 10 ]] && [[ $((bonus1 + bonus2)) -gt 10 ]]; then
          echo "Error: Invalid bonus rolls for strike in 10th frame." >&2; exit 1
        fi
      fi
      continue
    fi
    
    # Open frame or spare logic
    (( current_roll_index++ ))
    if [[ $current_roll_index -ge $num_rolls ]]; then
        echo "Error: Incomplete frame." >&2
        exit 1
    fi
    local roll2=${rolls_to_validate[current_roll_index]}
    
    if ! [[ "$roll2" =~ ^[0-9]+$ ]] || [[ "$roll2" -lt 0 ]] || [[ "$roll2" -gt 10 ]]; then
      echo "Error: Invalid pin count. Rolls must be between 0 and 10." >&2
      exit 1
    fi

    if [[ $((roll1 + roll2)) -gt 10 ]]; then
      echo "Error: A single frame's rolls cannot exceed 10 pins." >&2
      exit 1
    fi
    
    (( current_roll_index++ ))
  done
  
  # Final check: have we used the right number of rolls?
  # This logic is tricky. A simple check is that after processing 10 frames,
  # the roll index should match the total number of rolls.
  if [[ $current_roll_index -ne $num_rolls ]]; then
      echo "Error: Incorrect number of rolls for the game." >&2
      exit 1
  fi
}


# --- Script Execution ---

# Call the main function with all script arguments
main "$@"

Where and How to Run the Script: A Code Walkthrough

Once you've saved the code as bowling.sh, you need to make it executable from your terminal. Then, you can run it by passing the sequence of rolls as arguments.

Step 1: Make the Script Executable

Open your terminal and run the chmod command:

chmod +x bowling.sh

Step 2: Run with Different Game Scenarios

You can now execute the script. The arguments are the pin counts for each roll, in order.

Example 1: A Perfect Game (Score: 300)

A perfect game is 12 strikes in a row.

./bowling.sh 10 10 10 10 10 10 10 10 10 10 10 10

Expected Output: 300

Example 2: A Game of All Spares (Score: 150)

A game where every frame is a spare (e.g., 5 then 5), with a final bonus ball of 5.

./bowling.sh 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5

Expected Output: 150

Example 3: A Standard Game

A more typical game with a mix of strikes, spares, and open frames.

./bowling.sh 10 7 3 9 0 10 0 8 8 2 0 6 10 10 10 8 1

Expected Output: 167

Detailed Code Explanation

Let's break down the script's core components to understand exactly what's happening.

1. Strict Mode and Initialization

set -o errexit
set -o nounset
set -o pipefail

These three lines at the beginning are a best practice for modern Bash scripting. They make the script safer by causing it to exit immediately if a command fails (errexit), if it tries to use an unset variable (nounset), or if a command in a pipeline fails (pipefail).

2. The main Function

main() {
  local rolls=("$@")
  validate_input "${rolls[@]}"
  
  local total_score=0
  local roll_index=0
  
  # ... main loop ...
  
  echo "$total_score"
}

The script's logic is wrapped in a main function. local rolls=("$@") captures all command-line arguments into an array named rolls. We then immediately call validate_input to ensure the game is valid before attempting to score it. We initialize our total_score and roll_index, which acts as a pointer to the current roll we are processing.

3. The Frame Loop

for frame in {1..10}; do
  # ... logic to calculate score for one frame ...
done

This is the heart of the program. A simple for loop that runs exactly 10 times. On each iteration, it calculates the score for one frame and adds it to the total_score. Crucially, it also advances the roll_index based on whether the frame was a strike (advance by 1) or a spare/open (advance by 2).

4. Scoring Logic and State Advancement

This is the decision-making part of the loop. This flow diagram illustrates the choice made in each iteration.

    ● Start of Frame
    │
    ▼
  ┌──────────────────┐
  │ Read current_roll  │
  └─────────┬────────┘
            │
            ▼
    ◆ Is it a Strike (10)?
   ╱                   ╲
  Yes (is_strike)      No
  │                     │
  ▼                     ▼
┌───────────────────┐   ◆ Is it a Spare?
│ Score = 10 +        │  ╱ (roll1+roll2 == 10) ╲
│ roll[i+1] + roll[i+2] │ Yes (is_spare)           No
└───────────────────┘   │                         │
  │                     ▼                         ▼
  │                   ┌───────────────────┐   ┌───────────────────┐
  │                   │ Score = 10 +        │   │ Score =           │
  │                   │ roll[i+2]         │   │ roll[i] + roll[i+1] │
  │                   └───────────────────┘   └───────────────────┘
  ▼                     │                         │
┌───────────────────┐   │                         │
│ roll_index += 1   │   └──────────┬──────────────┘
└───────────────────┘              │
  │                                ▼
  └───────────────┐     ┌───────────────────┐
                  │     │ roll_index += 2   │
                  │     └───────────────────┘
                  ▼
         ● End of Frame

The if/elif/else block perfectly mirrors this logic. It checks for a strike first. If not a strike, it checks for a spare. If neither, it defaults to an open frame. After calculating the score for the frame, it correctly increments roll_index to prepare for the next iteration.

5. Input Validation (`validate_input`)

A robust script must handle bad data gracefully. The validate_input function is arguably more complex than the scoring logic itself. It iterates through the frames and rolls, checking for several error conditions:

  • Are there enough rolls to constitute a game?
  • Is every roll a number between 0 and 10?
  • Do the two rolls in an open frame or spare sum to more than 10?
  • Are the bonus rolls for the 10th frame valid?
  • Is the total number of rolls correct for the sequence of strikes and spares?

If any of these checks fail, it prints an informative error message to stderr and exits with a non-zero status code, which is standard practice for signaling an error in shell scripts.


Frequently Asked Questions (FAQ)

Why is the 10th frame scoring logic handled without a special case in the main loop?

The beauty of this algorithm is that the 10th frame's scoring doesn't require a special logical branch. The main loop runs 10 times. On the 10th iteration, if a strike or spare occurs, the score calculation (e.g., `10 + rolls[roll_index + 1] + rolls[roll_index + 2]`) naturally reads from the bonus roll slots at the end of the array. The `validate_input` function ensures those bonus rolls exist and are valid beforehand.

How can I debug a Bash script like this?

The most effective way to debug a Bash script is using the `set -x` command. Place `set -x` at the top of your script (or just before the section you want to debug) and `set +x` to turn it off. When you run the script, Bash will print each command it executes to the terminal, prefixed with a `+`, showing you the values of variables as they are substituted. For example, you could see `+ (( total_score += 10 + 7 + 3 ))` in the output.

What is the difference between `((...))` and `$[...]` or `let`?

((...)) is the modern, preferred syntax for arithmetic expansion in Bash. It supports a C-style syntax (e.g., `i++`, `*`, `/`, `+`, `-`) and is generally more readable and powerful. The `$[...]` syntax is an older, deprecated form. The `let` command is another way to perform arithmetic but is often more verbose (e.g., `let total_score=total_score+10`). For new scripts, you should always favor `((...))`.

Could this logic be implemented with functions for each frame type?

Absolutely. An alternative approach would be to have functions like `calculate_strike_score`, `calculate_spare_score`, and `calculate_open_frame_score`. Each function would return the score for that frame. This could make the main loop slightly cleaner, but might be overkill for a script of this size. Our implementation uses helper functions (`is_strike`, `is_spare`) for checking the condition, which strikes a good balance.

What are some common pitfalls when working with arrays in Bash?

The most common pitfall is forgetting the quotes when expanding an array. Always use `"${my_array[@]}"` to expand an array into its individual elements as separate, quoted words. Using `$my_array` will only give you the first element, and `*` instead of `@` can have different behavior with word splitting. Another issue is that Bash arrays are zero-indexed, which is a common source of off-by-one errors.

Is Bash a good language for complex algorithms?

While Bash can be used for complex algorithms as demonstrated here, it's generally not the best tool for the job. For CPU-intensive tasks, heavy data manipulation, or problems requiring complex data structures (like graphs or trees), languages like Python, Go, or Rust are far more suitable. Bash's strength lies in automation, file system interaction, and acting as "glue" between other command-line programs.


Conclusion: From a Game to a Gateway Skill

Successfully building a bowling score calculator in Bash is a significant milestone. It proves you have moved beyond simple, single-line commands and can now write structured, logical programs that manage state and handle complex edge cases. You've navigated array indexing, arithmetic expansion, conditional logic, and robust error handling—all within the shell environment.

The skills honed in this kodikra module are directly applicable to real-world DevOps and SysAdmin tasks, such as parsing log files, automating backups with complex rules, or deploying applications across multiple servers. You have demonstrated that you can use a fundamental tool like Bash to build something truly sophisticated.

Disclaimer: The solution and concepts discussed are based on modern Bash scripting practices and have been tested on Bash version 5.x+. While most of the syntax is backward-compatible, using an up-to-date version of Bash is always recommended.


Published by Kodikra — Your trusted Bash learning resource.