Section 6 - Implementing M. Mitchell Rules
3A.6.1 The Plan
You have to always think in terms of objects when you start a project like this. What will the objects be? In CA, a natural answer is that there should be two classes:
- A Rule class, which encapsulates all the information of a rule.
- An Automaton class, which accepts a rule in its constructor, and has methods that compute and display new generations as a result of repeatedly applying the rule.
There is a lot of leeway here. For example, should our Automaton class print out only one generation and allow the client to decide how to display the results of many generations, or should the class display all the generations (up to some value) by itself with no client intervention? Also, the Rule class might not be necessary if rules are really simple. For example, using M. Mitchell's encoding scheme from the last section, a rule is nothing more than a number. There is hardly a need for a separate class in that case.
With this in mind, I will create an Automaton class that only provides a propagateNextGeneration() method (to compute the next gen based on the rule and current gen) and a toStringCurrentGen() (which returns a string, like "* * ** * *"). We will let the client do the modest work of looping to produce multiple generations and displaying each generation, one-beneath-the-previous. We will always assume the seed is a single asterisk surrounded by infinite blanks, like, "... * ... ". Also, we can dispense with the "Rule class" (which would no doubt please Herr Marx).
I often like to start with a proposed main() that shows how we will use the class. From there, it makes the class design a little easier.
int main() { int rule, k; // get rule from user do { cout << "Enter Rule (0 - 255): "; cin >> rule; } while (rule < 0 || rule > 255); // create automaton with this rule and single central dot Automaton aut(rule); // now show it cout << " start" << endl; for (k = 0; k < 100; k++) { cout << aut.toStringCurrentGen() << endl; aut.propagateNewGeneration(); } cout << " end" << endl; return 0; }
We immediately see that the constructor takes the rule (as an int). That implies that it is stored internally. While an int is, indeed, a compact way to represent a rule, it is not necessarily the most useful. We should probably devise a slightly more intuitive internal representation of a rule. Remembering that a rule needs to give an output, on or off, which results from one of eight possible input combinations, we can store this cleanly as an array of eight bools. So rule[0] will give the output (true=ON or false=OFF) from the input combination "000". rule[1] would give the output for "001". rule[2], the output for "010", rule[3] the output for "011", and so on, up to rule[7] which tells the output for the input combo "111".
We should also store a string that represents the current generation or, as I call it, thisGen.
3A.6.2 Class Automaton
With this introduction, we can now give a reasonable prototype for class Automaton:
class Automaton { private: bool rules[8]; string thisGen; string extremeBit; // bit, "*" or " ", implied everywhere "outside" int displayWidth; // an odd number so it can be perfectly centered public: static const int MAX_DISPLAY_WIDTH = 121; Automaton(int rule); string toStringCurrentGen(); bool setRule(int rule); // change rule set void resetFirstGen(); bool setDisplayWidth(int width); void propagateNewGeneration(); };
displayWidth tells us how much of the thisGen string to send to the console. This enables thisGen to get extremely long (thousands of characters) without causing a mess on the console. If thisGen is, in fact, longer than displayWidth, we have to decide which sub-portion of thisGen to display. The answer is the exact center section, cropping off excess characters equally from the left and right. Typically displayWidth will be 79, but we allow it to grow to 121 (we want to be an odd number for technical reasons.) MAX_DISPLAY_WIDTH determines the maximum displayWidth allowed.
extremeBit
What is this strange sounding member? It is a single character (in a string form) which is either blank, " ", or an asterisk, "*". It represents the value we imagine stored in every string position to the far left and far right (out to infinity), beyond the actual, interesting, middle portion of the string.
To do this automaton correctly, we have to imagine that every generation is infinite in both directions. We begin by assuming that everything to the left and right, which is not the central, single "*" asterisk, is a blank, " ". However, after we start applying the rule to that initial generation, it might change those blanks to asterisks. How? In one of two ways:
- When we are testing the input values near the center, the initial "*", or 1 bit, will join with neighboring 0 bits to form a number like 1, 2 or 4 as an input to the rule. To make this extremely clear, apply what you learned in the last page to see what happens if you apply the rule 255 to a single "*". The second generation will be "***" in the center. [If it takes you more than a 30 or 60 seconds to compute this result, then you need to reread the last section.] The "*" had two children. In the third generation, those three may spread to five non-blank characters. So the original "*" causes a spread of both "*"s and " "s from the center, depending on what the rule says. This is one way that the original 0s in the wings can turn to 1s: they get converted by the spreading of the original, central "*" as a result of rule application.
- Very far from this central interesting part, everything is 0s, initially. So every triplet is 000, which means the input to the rule (in the first generation) is always 0 way out there. But the rule might well have rule[0] = true. If so, all those distant 0s will turn into 1s in the second generation: the sea of blanks will become a sea of "*"s.
Either way, besides the growing central signal, or interesting part, there will be a boring sea of either all 0s or all 1s. The extremeBit string is a single character string that tells us which it is. If it is " ", it means our generation is swimming in a sea of 0s. If it is a "*", it means we are swimming in a sea of 1s. This is needed to compute the next generation.
The Member Methods
Let's describe the methods.
- Automaton(int rule) - a constructor. Through the mutator, below, we'll sanitize rule and then convert it to our internal representation. We also need to establish the seed: a single 1 in a sea of 0s. This is our first generation. Do this by setting firstGen to "*" and extremeBit to " ".
- string toStringCurrentGen() - This returns a string consisting of the current generation, thisGen, but does so by embedding it in a returnString whose length is exactly displayWidth long. If thisGen is smaller than displayWidth, it is positioned in the center of the larger returnString, with the excess padded by extremeBit characters. If thisGen is longer than displayWidth, it has to be truncated (on both ends) so that it is perfectly centered in the returned returnString, any excess trimmed off both ends, equally.
- Two Mutators:
- bool setRule(int newRule) - converts the int newRule, a number from 0-255, to an array of eight bools. For example, if the newRule is 182, then this is the binary number 1 0 1 1 0 1 1 0, which means that the resultant array will be
rule[7] = true,
I have color coded two of the bits in 182 and their corresponding bools in the array rule[] so you can easily see how the conversion needs to work. As usual, this must sanitize the int so that only values in the legal range 0-255 are allowed.
rule[6] = false ,
rule[5] = true,
rule[4] = true,
rule[3] = false,
rule[2] = true,
rule[1] = true ,
rule[0] = false. - bool setDisplayWidth(int width) - this is described by the earlier description of displayWidth and MAX_DISPLAY_WIDTH. I repeat that only odd widths are allowed (for centering purposes).
- bool setRule(int newRule) - converts the int newRule, a number from 0-255, to an array of eight bools. For example, if the newRule is 182, then this is the binary number 1 0 1 1 0 1 1 0, which means that the resultant array will be
- void propagateNewGeneration() - this is the workhorse function. It will use the three private members thisGen, extremeBit and rule[], and create the next generation from them. This method must first append two extremeBits to each side of thisGen in order to provide it with enough bits (or chars) on each end needed by rule. This adds four chars to thisGen, temporarily. We then apply rule in a loop to the new, larger, thisGen, which creates a separate (local) nextGen string. If you do this correctly, nextGen will be two characters smaller than the recently augmented thisGen (but still two larger than the original thisGen that entered the method). [You must understand this statement, conceptually, before you write your first line of code, or you will be doomed.] Then, you replace the old thisGen with our new nextGen.
In brief, we pad thisGen with four extremeBits, then apply rule, and we have a new nextGen, two larger than we started out with. We copy that back to thisGen to complete the cycle.
Finally, we have to apply rule to three consecutive extremeBits to figure out what the new extremeBit will be for the next generation (" " or "*"?) . What do I mean by "three consecutive"? We apply rule to an int representing a 3-bit pattern inside the old generation. In this case, we are doing this way out where all the bits are the same: extremeBit. So each input will be three 0s or three 1s -- an int 0 or an int 7 -- depending on the value of extremeBit. The result will enable us determine the new value of extremeBit. We must do this before we return, so extremeBit is correct for the next call to this method.
3A.6.3 The Implementation
You will be implementing this as part of your lab assignment this week.