Orasund/elm-cellautomata/1.0.0

NOTE: the latest version is 3.1.1

CellAutomata.LifeLike

This module was created to give a smooth introduction to the main module,
but also because for a lot of use cases this simpler version is already enough.

If you know of Conway's Game of Life and want to make something similiar,
this module is the right one for you.

In this module, a few assumptions about the automata were made:

  • The cells are organized in a two dimensional grid.
  • Each cell can have one of two states: Dead(Nothing) or Alive(Just Alive)
  • Each cell has eight neighbors
  • A rule can only take the amount of alive neighbored cells into account. (Not the pattern itself)

Note: This last assumption can be ignored by using automataWithoutSymmetry or automataWithCustomSymmetry. We will look at an example further down the document.

The Basics

Basic Types

type alias Grid =
Dict Location State

The grid is the "model" of this module.

You might want to write your own view function for it or else you can't see what the automata has done.

In your head you should think of this as a grid, where some cells are filled in.
In fact, only the filled cells are stored in the Dict.
Filled cells have the value Just Alive while empty cells have the value Nothing.
This is why we represent the grid as a dictionary.

type State
= Alive

The State will specify all posible states a cell can be in (besides a empty Cell)

This means cells will be of type Maybe State.
This way it should be clear that, the default value for a cell is Nothing.
In this module there is just one other state: Just Alive.
If you want to add more states, then you should go to the main module.

type alias Location =
( Int, Int )

The location is the unique identifier for any cell.
For our purpose we use (x,y)-coordinates.

Note: The south of (0,0) is (0,y) while the north is (0,-y).

Basic Automata

= AllDead
| OneAlive
| TwoAlive
| ThreeAlive
| FourAlive
| FiveAlive
| SixAlive
| SevenAlive
| EightAlive
| AnyAmount

This type specifies how many neighbors may be alive.

step : Automata -> Grid -> Location -> Maybe State -> Maybe State

This is the main function.
It has a wierd type, but thats because it is meant to be used with ``Dict.update`:

List.range 0 12
|> List.foldl
    (\x g ->
        List.range 0 10
        |> List.foldl
            (\y ->
                Dict.update
                    (x,y)
                    ( (x,y) |> step automata grid )
            )
            g
    )
    grid
type alias Automata =
Automata
(Neighborhood (Maybe State))
(Neighborhood (RuleExpression (Maybe State)))
Location

The Automata type can be seen as a config type.
Its stores all information to specify the behaviour of a cell automata.
Sometimes more then one automata should act on to the same Grid.
For this reason it is its own type.

List
{ from : Maybe State
, neighbors : AliveNeighbors
, to : Maybe State
}
-> Automata

The input is a list of rules.
As an example, lets look at the rules for conway's game of life:

  • Alive cells survive if they have 2 or 3 alive neighbors, else they die.
  • Dead cells turn alive if exactly 3 neighbors are alive.

Implemented, these rules look like this:

[ { from = Just Alive
  , neighbors = TwoAlive
  , to = Just Alive
  }
, { from = Just Alive
  , neighbors = ThreeAlive
  , to = Just Alive
  }
, { from = Just Alive
  , neighbors = AnyAmount
  , to = Nothing
  }
, { from = Nothing
  , neighbors = ThreeAlive
  , to = Just Alive
  }
]
|> automata

The order of the rules are important: the automata will go through the list,
and use the first rule it can apply.

Rule Expressions and Symmetries

Up until now, only the amount of living neighbors was important, but not their position.

For the remaining documentation we use a modified version of game of life,
where only the four direct neighbors (North,South,East,West) are considered.

Rule

type RuleExpression state
= Exactly state
| Anything

RuleExpressions give us a very flexible way of talking about neighbors.

When writing a rule for the neighbors, they can now have one of the following values:

  • (Exactly <| Just Alive) - its alive
  • (Exactly <| Nothing) - its dead
  • Anything - it may be dead or alive - we dont care.

Note: If you would go back to counting the alive neighbors, the Anything-expression will act like an optional neighbor.
For example a rule that looks for 3 Alive and 2 Anything will be successfull if it finds either 3,4 or 5 alive neighbors.

type alias Neighborhood state =
{ north : state
, northEast : state
, east : state
, southEast : state
, south : state
, southWest : state
, west : state
, northWest : state
}

This replaces the AliveNeighbors type.

Instead of saying "one alive neighbor", we now need to explicitly specify where this neighbor is located.

If some neighbor may have any value (that is most often the case),
its best to use the anyNeighborhood template and start from there.

anyNeighborhood : Neighborhood (RuleExpression (Maybe State))

this template helps defining a Neighborhood.

For example, if we would want to only consider the 4 adjacent neighbors, we might specify it the following way

{anyNeighborhood
| north : Exactly a
, east : Exactly b
, south : Exactly c
, west : Exactly d
}
type alias Rule =
{ from : Maybe State
, neighbors : Neighborhood (RuleExpression (Maybe State))
, to : Maybe State
}

A rule now needs a Neighborhood instead of an AliveNeighbors-value.

Automata

automataWithoutSymmetry : List Rule -> Automata

This function uses no symmetry. This means every possible combination must be specified.

automataWithoutSymmetry =
    automataWithCustomSymmetry noSymmetry

This function is not useful in practice.
Most often you want at least rotational or mirrored symmetry
This function is only included for demonstration purposes

For example, lets say we want to modify conway's game of life, such that it only considers the four adjacent neighbors:

  • Alive cells survive if they have exactly 1 adjacent neighbors
  • Dead cells turn alive if exactly 1 neighbors are alive

The implementation would be the following list:

let
    neighbors a b c d = 
        {anyNeighborhood
        | north = Exactly a
        , east = Exactly b
        , south = Exactly c
        , west = Exactly d
        }
in
[ {from = Just Alive
  , to = Just Alive
  , neighbors = neighbors (Just Alive) Nothing Nothing Nothing
  }
, {from = Just Alive
  , to = Just Alive
  , neighbors = neighbors Nothing (Just Alive) Nothing Nothing
  }
, {from = Just Alive
  , to = Just Alive
  , neighbors = neighbors Nothing Nothing (Just Alive) Nothing
  }
, {from = Just Alive
  , to = Just Alive
  , neighbors = neighbors Nothing Nothing Nothing (Just Alive)
  }
, {from = Just Alive, to = Nothing, neighbors = anyNeighborhood}
, {from = Nothing
  , to = Just Alive
  , neighbors = neighbors (Just Alive) Nothing Nothing Nothing
  }
, {from = Nothing
  , to = Just Alive
  , neighbors = neighbors Nothing (Just Alive) Nothing Nothing
  }
, {from = Nothing
  , to = Just Alive
  , neighbors = neighbors Nothing Nothing (Just Alive) Nothing
  }
, {from = Nothing
  , to = Just Alive
  , neighbors = neighbors Nothing Nothing Nothing (Just Alive)
  }
]

As one can tell, that example blew up. Thats because we did not use any symmetry.

Symmetry

type alias Symmetry =
Maybe State
-> Neighborhood (Maybe State)
-> Rule
-> Maybe (Maybe State)

A symmetry is just a function that determines if a rule is sucessfully applied. If so, it returns the new state. During this documentation we have already encountered two different symmetries:
fullSymmetry and noSymmetry.

Maybe State
-> Neighborhood (Maybe State)
-> Rule
-> Maybe (Maybe State)

The position of the neighbors is not important, only the amount.

Maybe State
-> Neighborhood (Maybe State)
-> Rule
-> Maybe (Maybe State)

Every possible way the neighbors might be arranged needs its own rule.

Maybe State
-> Neighborhood (Maybe State)
-> Rule
-> Maybe (Maybe State)

Pattern may be horizontally mirrored.

Maybe State
-> Neighborhood (Maybe State)
-> Rule
-> Maybe (Maybe State)

Pattern may be vertically mirrored.

Maybe State
-> Neighborhood (Maybe State)
-> Rule
-> Maybe (Maybe State)

Pattern may be rotated in any position.

Maybe State
-> Neighborhood (Maybe State)
-> Rule
-> Maybe (Maybe State)

Pattern may be rotated in 90,180 and 270 degree angles.

Automata with Symmetry

automataWithCustomSymmetry : Symmetry -> List Rule -> Automata

With this function we can now add our own symmetry.

Going back to the old example, this can now be done the following way:

let
    neighbors a b c d = 
        {anyNeighborhood
        | north = Exactly a
        , east = Exactly b
        , south = Exactly c
        , west = Exactly d
        }
in
[ {from = Just Alive
  , to = Just Alive
  , neighbors = neighbors (Just Alive) Nothing Nothing Nothing
  }
, {from = Just Alive
  , to = Nothing
  , neighbors = anyNeighborhood
  }
, {from = Nothing
  , to = Just Alive
  , neighbors = neighbors (Just Alive) Nothing Nothing Nothing
  }
]
    |> automataWithCustomSymmetry rot90Symmetry