Harvesting the Rules
One of the great things about the Game of Life is the sheer simplicity of its rules. The idea is that in a grid of arbitrary rows and columns, a single given cell can have one of two states: Alive or Dead. The initial state of the board becomes the "seed" conditions, and once those are specified, it's no longer necessary to provide any external input into the system. The current state of the system is used as the inputs into the next state of the system following a "tick", or processing turn. Whether or not a Cell "IsAlive" or not is decided by the present state of the 8 neighbors of the given Cell. Each "birth" or "death" happens simultaneously during the "tick"
Right away, we can derive a number of useful Rules and Rule artifacts from this description. Here's what comes out of our initial analysis:
- Our universe consists of a grid
- A grid has a collection of cells
- Each cell has a state, IsAlive, that can be either true or false along with its X and Y coordinates on the grid
- The state of each cell is computed any time the universe "ticks"
- The determination of a cell's fate depends on the current states of the individual cells surrounding it
- The computed (future) state of a cell should not affect the computation of the state of surrounding cells - state changes are simultaneous
Here's what the resultant entity structure looks like along with the Rule Sets we've identified from our requirements. You can see that I added a calculated field, Index, which is a convenient way to avoid needing to compute it from scratch each time I need to know the value of the expression X + (Y*Grid.GridSize). NextState and PreviousState are temporary fields used to hold past and future state values - they will be used to fulfill the requirement of simultaneous evaluation.
Now we can start to look at how we're going to structure execution of the rules. The first thing to do is to identify the root entity that will serve as the basis for rule execution. This is easy for us in this case - it's the Conway entity (the small blue plus sign in the Entity icon signifies that the entity has no parent referencing it). Once we've done that, we need some way to compute the next state of each cell. By creating an explicit rule set "OnTick", I can use the ExecuteMemberRule Set action to iterate over all of the grid's cells, calling the rule set on each.
The OnTick rule set can't directly set the IsAlive property because it would violate the need to compute future states simultaneously. Here, simultaneous doesn't necessarily mean that it actually occurs at the same time, it just means that mutation of the IsAlive state flag should never occur until every cell has had its future state computed. We accomplish this by storing the results of the state computation in that NextState temporary field. Once we've gone through all the cells and set that value, we can iterate over the collection once more to copy the value in the temporary field to the actual field. Once again, ExecuteMemberRule Set is our friend here, allowing us to do this in a clean fashion, as the screenshot below demonstrates.
This is all well and good, but what about the heart of these rules -- the actual state computation? I'll let the Business Language of those rules tell the story:
The slightly awkward phrasing of the "number of neighbors having an IsAlive of…" comes courtesy of a vocabulary template that I threw together to abstract away the semantics of calculating the number of alive or dead "neighbors" a cell has. Though vocabulary templates may seem like syntactic sugar, they're actually one of the more potent weapons in the rule integration and authoring tool chest. Here's what
the GetNeighbors vocabulary template looks like. Note the use of the Current(Field) function to ensure proper resolution of instances within the aggregate Count() expression
Prepare to Integrate
There's not a lot of HTML that needs to be written here, just enough to render a simple grid along with a button to initiate a "Tick" and a place for feedback from the rules engine to be displayed to the user (for troubleshooting purposes):
We don't want to paint ourselves into a corner by mixing the display of the grid with the logic governing its behavior, so we'll instead define the ROW_COUNT and COLUMN_COUNT values as constants in our JS code. It's a trivial exercise to turn that into a parameter that can be passed in via, say the query string of the URL, but for the purposes of keeping things simple, we'll make do with constants.
The creation of the Rule Session and the registration of the "Conway" entity with our JS Conway entity are two critical steps for integration. The first prepares the runtime for execution while the second specifies the data state that will be used by the engine during execution. We could actually execute the rules at this point, but there wouldn't be much to see since we haven't populated our entity state, nor have we written any integration code to display that state in the UI.
To start, we need to generate the HTML Table cells that will comprise the UI of our grid, and at the same time populate the initial entity state that we're going to use to execute rules. This is accomplished via a simple set of nested for (…) loops and some jQuery. The relevant part of this is shown below:
This gets us to the point where we can see a table of cells, and that table of cells matches the entity state that we're going to be applying the rules against. A jQuery-based click handler provides the functionality to allow users to click a cell to toggle its initial state, which fulfills the rest of the requirements needed to prepare for rules execution, while another handler hooks up the "Tick" button to a handler we'll get to in a bit. Before we can discuss the Tick logic however, we need to address two fundamental needs:
- Before each "Tick", we need to update entity state in response to a user having clicked a cell to toggle its state in the view
- Conversely, after every "Tick" we need to update the UI to match the current entity state so the user can see what's happened
There are tons of different ways to accomplish these goals, but I'm going to go with the naïve approach of simply looping through our cells, looking up the matching HTML cell, and updating either entity state or the CSS class as appropriate. Thus, we come up with the following implementation pattern:
The "Tick" rule set is an explicit rule set, which means I use a slightly different invocation pattern from automatic rule sets. Instead of invoking off of the session, I invoke off of the entity to which the explicit rule set belongs. I pass in the name of the rule set, an empty array to signify that I'm not passing in any parameters to this rule set, and lastly, a callback to be fired upon completion of rule execution.
Integration tasks completed, it's time to see how emergent our complexity really can be! Loading up the page in a web browser and setting the initial state for a repeating pattern known as "R-Pentomino" (see https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life#Examples_of_patterns for more examples!), I spam the "Tick" button and watch as my sandbox takes on a life of its own.
The astute reader may notice that this isn’t the first time that I’ve played around with implementing the Game of Life using a framework or tool that wasn’t originally designed for the task – indeed, I demonstrated an alternative implementation of Life as part of a talk I gave on SpecFlow a few years back. I’ve found that Life makes for an eminently suitable reference implementation in many different milieus because it’s just complicated enough to be both interesting and non-trivial, while still remaining quite simple enough to not detract from the main point and allow irrelevant details to intrude upon the topic at hand. Though not many developers or businesses have a need to run simulations of this particular variety, there are several lessons to take away from this type of application:
- Effective rule harvesting and design can and should be performed outside of the tooling that will be used to implement the system
- Relationships between pieces of data may have a temporal component in addition to any other (e.g. referential) requirements. This means that…
- It is important to understand as early in the process as possible what shape the initial and the final (expected) states of the data will take as this will become the boundary conditions for your system
- Simplicity can lead to some wonderfully complex and rich interactions. Design components as simple as possible (KISS never goes out of fashion!) and allow the interactions between components to be the primary place for the expression of complexity.
What sort of lessons does Conway’s Game of Life teach you?
Resources and further information:
Conway's Game of Life: