wolfram.sh

An elementary cellular automaton, based on the Wolfram code and implemented as a shell script.

Usage

./wolfram.sh [options]
-r
Which rule to use in the simulation. Defaults to choosing a random rule.
-w
The width of the simulation. Defaults to the number of columns in the terminal.
-g
The number of generations printed. Produces generations indefinitely by default.
-d
The delay before printing the next generation. Defaults to 0.1 seconds.
./wolfram.sh -r 30 -w 20 -g 10 -d 0
         █         
        ███        
       ██  █       
      ██ ████      
     ██  █   █     
    ██ ████ ███    
   ██  █    █  █   
  ██ ████  ██████  
 ██  █   ███     █ 
██ ████ ██  █   ███

As a terminal screensaver

An interesting use of wolfram.sh is using it as a “terminal screensaver”.

An example using Alacritty opens the terminal in full screen mode, decreases the font size to increase the resolution, and disables the history. It then starts the program with a random pick from a selection of interesting rules:

alacritty --option font.size=2 \
          --option font.offset={x=3} \
          --option window.startup_mode='"Fullscreen"' \
          --option scrolling.history=0 \
          --command ./wolfram.sh -r $(shuf -e 30 45 60 90 110 150 -n 1)

Implementation

Set the initial state

The simulation emerges from an initial configuration of cells, which represent the first generation. Because the elementary cellular automaton is one-dimensional, its state can be represented as an array.

To generate an initial state, first determine the width of the simulation.

width=$(tput cols)

The width is set to the number of columns in the terminal window, but it can be configured with the -w flag.

while getopts "w:" flag; do
    case "$flag" in
        'w') width=$OPTARG;;
        *) exit
    esac
done

With the width set, generate the initial state by populating an array with zeroes. There is one live cell in the middle, represented by a 1 as the middle array element.

for ((i=0;i<=width-1;i++)); do
    state+=($((i == width/2)))
done

For example, running the code above in a terminal that’s 9 cells wide produces an initial state with a width of 9 cells, with a single live cell in the middle:

(0 0 0 0 1 0 0 0 0)

Draw the current state

The draw function prints the current state. Whenever it’s called, it prints a line with either or a space for each cell in the generation.

live="█"
dead=" "

draw() {
    local line=""
    for value in "$@"; do
          line+=$([ "$value" -eq 1 ] && echo "$live" || echo "$dead");
    done
    printf "%b" "${line}"
}

To draw the current state, pass it to the draw() function as an array.

draw "${state[@]}"

With the initial state state, the draw() function prints a single box in the middle of the screen to represent a single living cell.

░░░░█░░░░

Calculate cell values

To determine the next state of a cell, the program considers the cell’s neighborhood in the previous generation. In the elementary cellular automaton, a cell’s neighborhood consists of the cell in the same position in the previous generation, and the cell on the left and right of it.

neighborhood=$((state[i-1]))$((state[i]))$((${state[i+1]:-${state[0]}}))

When determining the state of the 5th cell in the second generation, the 4th, 5th and 6th cell in the previous generation are considered its neighborhood. For example, using the initial state, calculating the neighborhood for the 5th element of the second generation:

010

After finding the neighborhood, a rule is applied to decide what the next state should be.

There are 256 rules in the elementary cellular automaton, each one derived from is binary representation. To extract the rule set, the rule number is converted to binary, split into an array, and finally reversed.

ruleset=(0 0 0 0 0 0 0 0)
for i in {0..7}; do
    ruleset[i]=$((rule % 2))
    rule=$((rule / 2))
done

For example, setting the rule variable to 30 produces the ruleset for rule 30:

(0 1 1 1 1 0 0 0)

Finally, with both the ruleset and neighborhood set, calculate the value for the new cell and add it to a new array named new_state. This is done by converting the neighborhood to a decimal value, and using that value as the index to look up the new value in ruleset array.

new_state+=("${ruleset[$((2#$neighborhood))]}")

Now that we have the logic to create a ruleset from a rule number and a way to calculate new generation cell values, the program can print new generations in a loop. For this, it needs to know which rule to use, and how many generations to print.

By default, one of the 256 rules is chosen at random. The generations variable is set to 0, which means the program keeps printing new generations until it’s terminated manually. Finally, delay variable is set to 0.1, which determines how long the program sleeps between drawing generations.

rule=$((RANDOM % 256))
generations=0
delay=0.1

The rule, generations and delay variables are configurable, through the -r, -g, and -d command line options, respectively.

while getopts "d:g:r:w:" flag; do
    case "$flag" in
        'd') delay=$OPTARG;;
        'g') generations=$OPTARG;;
        'r') rule=$OPTARG;;
        'w') width=$OPTARG;;
        *) exit
    esac
done

With everything set up, the program can print generations by populating a new_state array, populating it, replacing the state variable, and then printing the new state with the draw() function in a loop.

count=1

while [ "$generations" = 0 ] || [ $count -lt "$generations" ]; do
    sleep "$delay"
    new_state=()

    for ((i=0;i<=width-1;i++)); do
        neighborhood=$((state[i-1]))$((state[i]))$((${state[i+1]:-${state[0]}}))
        new_state+=("${ruleset[$((2#$neighborhood))]}")
    done

    state=("${new_state[@]}")

    printf "\n"
    draw "${state[@]}"

    ((count++))
done

Hide the cursor

To prevent flickering while drawing, the program hides the cursor before printing the first generation. Then, right before exiting, the cursor is set back to normal.

hide-cursor() {
    tput civis
}

show-cursor() {
    tput cnorm
}

trap show-cursor EXIT
hide-cursor

Testing

The program is tested with bats, a testing framework for bash. To run the tests, evaluate test.bats.

./test.bats