Play the Busy Beaver Game through a simulator | Opensource.com

Play the Busy Beaver Game through a simulator

A simulator written in C helps solve one of the most complex games in mathematics.

Woman sitting in front of her computer
Image credits : 
Ray Smith
x

Subscribe now

Get the highlights in your inbox every week.

It's hard to find a game that combines the difficulty of, say, Dark Souls with the elegance of Conway's Game of Life. In a 1962 paper, Hungarian mathematician Tibor Radó came up with just such a game, which he called the Busy Beaver Game (BBG).

To play BBG is to create a program that writes 1s on a machine's tape whose cells initially hold 0s; the 1s need not be consecutive, but the program must halt for the 1s to count. The winning program has the most 1s on its tape after halting.

I'll start with an overview of the machine, its programming language, and the game's constraints.

The BBG machine, language, and game constraints

Imagine an abstract computing machine (indeed, a Turing machine) with these features:

  • A two-character alphabet. In general, any two characters will do for a Turing machine; by tradition, BBG uses 0 and 1.
  • A tape of cells laid out horizontally. Each cell holds a single character from the alphabet. The tape is initialized to 0s, which counts as a blank tape. A Turing machine's tape must be unbounded in at least one direction (typically the right), which is computationally equivalent to a tape unbounded in both directions. Again by tradition, the BBG uses a tape that is unbounded in both directions:
       +-+-+-+-+-+
    ...|0|0|0|0|0|...
       +-+-+-+-+-+
    The tape is the machine's memory.
  • A read-write marker, which identifies the current memory cell. During each step in a computation, the machine reads and then overwrites the current cell's contents. The machine can replace a 0 with a 1, a 1 with a 0, or either character with the same one. The caret ^ beneath a cell represents the marker:
       +-+-+-+-+-+
    ...|0|0|0|0|0|...
       +-+-+-+-+-+
            ^
    The marker (here beneath the middle cell) acts as an index into the machine's memory. A Turing machine has only linear rather than random access to memory cells because it moves just one cell, either left or right, per executed instruction.

A BBG program, like any Turing program, consists of instructions such as a0b1L, which are known as quintuples for the number of parts; in BBG, there is one character per part. A quintuple's first two characters (in this example, a0) represent a condition that captures two aspects of the computation:

  • The current state of the computation, which is a in the a0b1L example; BBG uses single letters (in my examples, lowercase ones) to identify the state
  • The contents of the currently marked cell: 0 or 1

The condition a0 means "in state a scanning a 0," whereas h1 means "in state h scanning a 1."

The last three characters in a quintuple specify the action to be taken if the condition is satisfied.

Here are two examples (with ## introducing my comments):

a0b1R ## in state a scanning a 0: transition to state b, write a 1, and move one cell right
p1p1L ## in state p scanning a 1: stay in state p, write a 1, and move one cell left

Quintuples can be visualized as rules with an arrow separating the condition from the action:

a0-->b1R

If condition a0 is satisfied, then action b1R occurs. By tradition, a is the start state and, therefore, a0 is the start condition. Forward-chaining rule systems such as OPS5 use a similar flow-of-control mechanism.

In summary, the quintuples in a BBG program can occur in any order because condition-matching determines which instruction executes next. No two quintuples in a program should have the same condition.

The halting problem

A BBC program executes until reaching a halt state—a state that does not occur in the matched condition of any instruction. Consider the program below, which is the BBG winner for a program with two non-halting states, a and b:

# bb2 winner
a0b1R  ## a0-->b1R
a1b1L  ## a1-->b1L
b0a1L  ## b0-->a1L
b1h1R  ## b1-->b1h1R (halt state)

The last instruction b1h1R contains the traditional halt state h in the action. (There can be multiple halt states but one is enough.) If the condition b1 is satisfied, then h becomes the new state when instruction b1h1R executes. However, no instruction in the program has a condition that begins with h, which means that the machine halts in state h. Reaching a halt state h represents normal program termination.

For a BBG, as for Turing computations in general, a program must halt for the computation to complete. For example, this instruction would write infinitely many 1s to the right on an initially blank tape:

a0a1R ## "infinitely touring" instruction on a blank tape

Scanning a 0 in state a, the machine stays in state a, overwrites the 0 with a 1, and moves right to another 0; hence, instruction a0a1R executes again. A program in which this instruction executes, with only 0s to the right, would never halt and, therefore, could not qualify as a BBG winner.

Among the legendary unsolvable problems in computing is whether a Turing machine halts when executing a given program on given data inputs—the halting problem. Accordingly, there is no way to know whether a given BBG program will halt. My simulator (introduced below) suspects "infinite touring" after executing a million instructions and therefore exits.

From BBG to BBG-N

BBG covers an indefinitely large set of games, each with a number that identifies how many non-halting states a game-playing program may use. For example, the sample program shown earlier is the winner of the BBG-2 game because the program is restricted to two non-halting states, a and b. (The BBG-2 winner produces four 1s on an initially blank tape.) The BBG-3 game uses three non-halting states, whereas the BBG-744 game uses 744 non-halting states.

In summary, a BBG-N winner produces the most 1s, given N non-halting states and starting on a blank tape. The winner must halt. Proving that a contender wins the BBG-N game is non-trivial. At present, there are proven winners of the BBG-1 through the BBG-4 games, but none for games with more than four non-halting states. For example, the BBG-5 game has only a best contender. Running some BBG winners and the BBG-5 best contender on the simulator should clarify the computation in detail and underscore how ingenious the winning and contending programs are.

BBG-N examples on the simulator

I'll start with the winner of the trivial BBG-1 game:

# bb1 winner
a0h1R  ## a is the single non-halting state

Here's how the simulator's tape looks to begin, with the computation in start state a scanning a 0:

Current state: a
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
...|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|...
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                                    ^

The program's single instruction transitions to the halting state h, writes a 1, and moves one cell to the right:

Current state: h
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
...|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|...
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                                    ^

The winner thus produces a single 1 in one step, using one non-halting state. No BBG-1 program can do better. An equivalent BBG-1 program might move left instead of right after writing a 1, but the winning total of one 1 would be the same.

The winner of the more interesting BBG-2 game is this program (shown earlier), which has two non-halting states, a and b:

# bb2 winner
a0b1R  ## a0-->b1R
a1b1L  ## a1-->b1L
b0a1L  ## b0-->a1L
b1h1R  ## b1-->h1R  (halt state)

The program produces four 1s in six steps:

Current state: h
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
...|0|0|0|0|0|0|0|0|0|0|0|0|0|1|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|...
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                                  ^

 Let's examine the BBG-3 winner in detail:

# bb3 winner
a0b1R
a1h1R # (halt state)
b0c0R
b1b1R
c0c1L
c1a1L

The program produces six 1s in 14 steps and has three non-halting states: a, b, and c. A trace from the simulator clarifies how the computation works, and in particular, how it loops.

As usual, the tape is initially blank. The marker is in the middle, and the machine is in start state a and scanning a 0. The instruction with the matching condition a0 is the first one, a0b1R. This instruction transitions to state b, writes a 1, and moves one cell to the right:

Current state: b
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
...|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|...
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                                    ^

The condition is now b0, which identifies instruction b0c0R. The machine accordingly transitions to state c, writes a 0, and moves one cell to the right:

Current state: c
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
...|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|...
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                                      ^

The new condition is c0 with c0c1L as the matching instruction. The machine thus overwrites the currently scanned 0 with a 1, remains in state c, and moves one cell to the left:

Current state: c
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
...|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|...
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                                    ^

The condition remains c0 and the matching instruction remains c0c1L; the action, therefore, writes another 1 between the other two. The state stays the same but the left move places the marker on a 1 rather than a 0. Accordingly, the condition changes to c1 and the matching instruction to c1a1L. The action for this instruction moves the marker to the 0 cell immediately to the left of the leftmost 1 with a as the new state:

Current state: a
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
...|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|...
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                                ^

Now the condition is a0 just as it was at the start of the computation—the program is, in effect, looping. The full instruction is a0b1R, which transitions the machine to state b, writes a fourth 1 on the left of the other three, and then moves right. The machine keeps moving right (and overwriting 1s with 1s) via instruction b1b1R until hitting the first 0 to the right:

Current state: b
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
...|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|...
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                                        ^

In state b and scanning a 0, the machine executes instruction b0c0R once again, thereby transitioning to state c, overwriting a 0 with a 0, and moving right to another blank cell:

   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
...|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|...
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                                          ^

In state c and scanning a 0, the machine now executes instruction c0c1L twice in a row to produce a tape with six consecutive 1s. Also, the machine has transitioned into state a:

Current state: a
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
...|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|1|1|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|...
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                                    ^

At this point, the matching instruction a1h1R transitions the machine into halt state h and moves one cell to the right. The final tape configuration is thus:

Current state: h
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
...|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|1|1|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|...
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                                      ^

The BBG-3 winner produces six 1s in 14 steps.

How can we be sure that these winners deserve the title? There are rigorous mathematical proofs that the winning BBG-1, BBG-2, BBG-3, and BBG-4 programs cannot be bested. The creator of the BBG once believed it impossible to prove a winner for BBG-4, but eventually, there was a proof for the BBG-4 winner. Here's the proven BBG-4 winner:

# bb4 winner
a0b1R
a1b1L
b0a1L
b1c0L
c0h1R # (halt state)
c1d1L
d0d1R
d1a0R

This program takes 107 steps to produce 13 1s, which are not consecutive:

Current state: h
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
...|0|0|0|0|0|1|0|1|1|1|1|1|1|1|1|1|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|...
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                ^

For games BBG-5 and beyond, there are best contenders rather than proven winners. Long-lived fame (but probably not fortune) awaits anyone who can prove a BBG-5 winner. For reference, here's the BBG-5 best contender:

# bb5 best contender
a0b1R
a1c1L
b0c1R
b1b1R
c0d1R
c1e0L
d0a1L
d1d1L
e0h1R # (halt state)
e1a0L

As shown below, this program produces 4,098 non-consecutive 1s in an astonishing 47,176,870 steps—a mind-boggling caution about just how hard BBG-N games become for N>4. Might there be a better contender for BBG-5 or even a proof that this program wins BBG-5? To date, these questions are open, and the experts expect them to remain so.

Running the simulator

The Turing program (written in C) simulates a single-tape Universal Turing Machine (UTM) by being general-purpose: the simulator can play BBG games but also, for example, perform mathematical operations such as multiplication and exponentiation on values represented in unary, given the appropriate program as an input. The UTM simulator presents the tape as if it were unbounded in both directions. The unavoidable shortcoming of any UTM simulator is finite tape size, of course; the abstract UTM has unbounded memory, a magical feature that no simulator can capture.

The simulator, together with the BBG programs discussed so far, is available from my website. For reference, here's the C source code for the simulator:

.Turing machine simulator
=========================
-----
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MaxQuintuples     128 /* expand as needed */
#define QuintupleLen        5
#define MaxBuffer         128
#define MaxTape            33 /* expand as needed: 1 line of display */
#define MaxSteps      1000000 /* assume 'infinite looping' thereafter */
#define Blank            '0'  /* 2-character alphabet: 0 and 1 */
#define StartState       'a'

#define TapeBorder       "+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+"
#define CellSide         '|'
#define Ellipsis         "..."
#define CellLen          2  

enum { NewState = 2, NewSymbol = 3, Direction = 4 };
char quintuples[MaxQuintuples][QuintupleLen + 1]; /* array of strings */
unsigned qlen = 0;        /* number of entries from input file */
unsigned displayFlag = 1; /* 2nd command-line arg turns this off */

char tape[MaxTape];
unsigned currentCell = MaxTape / 2; /* tape middle */
char currentState = StartState;
unsigned instructionsExecuted = 0;

void die(const char* msg) {
  puts(msg);
  exit(EXIT_FAILURE);
}

void print_cell(unsigned ind) {
  putchar(CellSide);
  putchar(tape[ind]);
}

void pause() {
  puts("Hit RETURN to continue...");
  getchar();
}

void display_tape() {
  printf("\nCurrent state: %c\n", currentState);
  printf("   %s\n", TapeBorder);
  printf("%s", Ellipsis);
 
  unsigned i;
  for (i = 0; i < MaxTape; i++) print_cell(i);
  printf("%c%s\n   %s\n", CellSide, Ellipsis, TapeBorder);

  i = (CellLen * currentCell) + 2;
  printf("%*s%c\n", i, "", '^');
  pause();
}

int comp(const void* e1, const void* e2) { /* qsort and bsearch callback */
  const char* s1 = (char*) e1;
  const char* s2 = (char*) e2;
  return strncmp(s1, s2, 2); /* < 0 = s1 < s2; 0 = s1 == s2; > 0 = s1 > s2 */
}

void read_program(const char* file) {
  FILE* infile = fopen(file, "r");
  char buff[MaxBuffer + 1];
  if (NULL == infile) {
    sprintf(buff, "Can't open program file %s", file);
    die(buff);
  }
   
  while (fgets(buff, MaxBuffer, infile)) { /* read until end-of-file */
    if ('#' == buff[0]) continue;              /* ignore comments */
    if (strlen(buff) < QuintupleLen) continue; /* ignore faulty lines */
    strncpy(quintuples[qlen], buff, QuintupleLen);
    qlen++;
  }
  fclose(infile);

  qsort(quintuples, qlen, sizeof(quintuples[0]), comp); /* sort for easy access */
  memset(tape, Blank, MaxTape); /* blank out the tape */
}

void report() {
  /* Show instructions. */
  printf("%i instructions:\n", qlen);
  unsigned i, count = 0;
  for (i = 0; i < qlen; i++) puts(quintuples[i]);
  for (i = 0; i < MaxTape; i++) if ('1' == tape[i]) count++;
  printf("Total 1s on tape:      %8i\n", count);
  printf("Instructions executed: %8i\n", instructionsExecuted);
}

void check_for_errors(const char* action) {
  if (0 == (currentCell - 1) && 'L' == action[Direction]) die("Can't move left...");
  if (currentCell >= MaxTape - 1 && 'R' == action[Direction]) die("Can't move right...");
  if (instructionsExecuted >= MaxSteps) die("Seems to be infinitely touring...");
}

void run_simulation() {
  while (1) {
     if (displayFlag) display_tape();
 
    /* Get the action for the current key. */
    char key[3];
    sprintf(key, "%c%c", currentState, tape[currentCell - 1]);
    char* action = bsearch(key, quintuples, qlen, sizeof(quintuples[0]), comp);
    if (NULL == action) break; /* no match == normal termination */

    check_for_errors(action);

    /* Update system. */
    currentState = action[NewState];
    tape[currentCell - 1] = action[NewSymbol];
    if ('L' == action[Direction]) currentCell--; /* move left */
    else currentCell++;                          /* move right */
    instructionsExecuted++;                      /* update step counter */
  }
}

int main(int argc, char* argv[]) {
  if (argc < 2) die("Usage: turing <program file>");
  if (argc > 2 && 0 == strcmp(argv[2], "off")) displayFlag = 0;
 
  read_program(argv[1]);
  run_simulation();
  report();
  return 0;
}

The code is straightforward and there are about 130 lines of it. Here's a summary of the control flow:

  • The Turing program reads from an input file (given as a command-line argument), that contains quintuples (one per line), as in the BBG-N examples.
  • The simulator then loops until one of these conditions occurs:
    • If the input program reaches a halt state, the simulator exits normally after reporting on the program's instructions, the number of 1s produced, and the number of steps required to do so.
    • If the computation tries to move either left from the leftmost cell or right from the rightmost cell, the simulator exits with an error message. The tape is not big enough for the computation.
    • If the computation hits MaxSteps (currently set at a million), the simulator terminates on suspicion of "infinite touring."

The simulator expects, as a command-line argument, the name of a file that contains a BBG-N or other program. With % as the command-line prompt, this command runs the simulator on the bb4.prog file introduced earlier:

% ./turing bb4.prog

By default, the simulator displays the tape and pauses after each instruction executes. However, the displays can be turned off from the command line:

% ./turing bb5.prog off  

This produces a report on the BBG-5 best contender, which takes more than 47 million steps before halting:

10 instructions:
a0b1R
a1c1L
b0c1R
b1b1R
c0d1R
c1e0L
d0a1L
d1d1L
e0h1R
e1a0L
Total 1s on tape:          4098
Instructions executed: 47176870

A program file should terminate each line, including the last one, with a newline; otherwise, the simulator may fail to read the line. The simulator ignores comments (which start with a #) and empty lines. Here, for review, is the bb4.prog input file:

# bb4 winner
a0b1R
a1b1L
b0a1L
b1c0L
c0h1R # (halt state)
c1d1L
d0d1R
d1a0R

At the top of the Turing source file are various macros (in C, #define directives) that specify sizes. These are of particular interest:

#define MaxQuintuples  128 /* expand as needed */
#define MaxTape         33 /* expand as needed */
#define MaxSteps   1000000 /* assume 'infinite touring' thereafter */

The specified sizes can be increased as needed, but even the best BBG-5 contender has only 10 instructions. The tape for a contender such as BBG-5 must be very big, and this contender requires more than 47 million steps to complete the computation. These settings would suffice:

#define MaxTape     100000000 /* 100M */
#define MaxSteps    100000000 /* 100M */

BBG-5 and other best contenders should be run with the off flag because the simulator, at present, displays the MaxTape cells on a single line.

Wrapping up

BBGs should appeal to recreational problem solvers and especially programmers. To program a BBG is to work in the machine language of the abstract computer—the Turing machine—that defines what computable means. One way to get started is by composing the BBG-2 and the BBG-3 winners from scratch. These exercises help to reveal the programming patterns used in the truly daunting challenges such as the BBG-4 winner and the BBG-5 best contender.

Another starting exercise is to write a program that first initializes a tape to two numeric values (in unary) separated by a 0:

   +-+-+-+-+-+-+-+-+
...|0|1|1|0|1|1|1|0|...  ## two and three in unary
   +-+-+-+-+-+-+-+-+
          ^

The program then computes, for example, the product in unary. Other arithmetic examples abound.

BBGs also are of ongoing interest to theoreticians in logic, mathematics, and computer science. For a brief history and overview of them, see How the slowest computer programs illuminate math's fundamental limits.

Woman sitting in front of her computer

WebAssembly is a good option for computationally expensive tasks due to its predefined execution environment and memory granularity.
Business woman on laptop sitting in front of window

This "guess the number" game is a great introductory program when learning a new programming language. Here's how to write it in C.
Person reading a book and digital copy

Sharing knowledge with others is often a great way to refresh and update your own expertise.
Math formulas in green writing

Play the numbers game from the popular British game show "Countdown" at home.

Topics

About the author

Marty Kalin - I'm an academic in computer science (College of Computing and Digital Media, DePaul University) with wide experience in software development, mostly in production planning and scheduling (steel industry) and product configuration (truck and bus manufacturing). Details on books and other publications are available at Marty Kalin's hompage