Orasund/elm-cellautomata/1.0.0

NOTE: the latest version is 3.1.1

CellAutomata

If you are new to this package, consider checking out CellAutomata.LifeLike first. It is written as an introduction to this module.

If you want to create automatas with more then one state, but that still operates on a 2D-Grid, than this package is the right one for you.

First start by writing your own state type. As an example, lets try to simulate an ant that always follows the right wall.

Our state will now be the following

type State = Wall
    | Up
    | Down
    | Left
    | Right

The Basics

Types

type alias Order state =
Maybe state -> Int

Every state needs a defined order. (A function that gives each state a unique integer)

For our ant example we define the following order:

order maybeState =
    case maybeState of
        Nothing ->
            0


        Just Wall ->
            1


        Just Up ->
            2


        Just Down ->
            3


        Just Left ->
            4


        Just Right ->
            5
type alias Grid state =
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.

Cells have the type Maybe state where state should be a custom type.

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).

Rule

type RuleExpression state
= Exactly state
| Anything

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

Saying something is Anything, it means its value is ignored.

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

The Neighborhood of a cell consists of the 8 surounding cells.

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 north neighbor, we might specify it the following way

{ anyNeighborhood
    | north = Exactly a
}
type alias Rule state =
{ from : Maybe state
, neighbors : Neighborhood (RuleExpression (Maybe state))
, to : Maybe state
}

A rule consist of the following elements:

  • from - the state of the cell
  • neighbors - the nessesary pattern of the neighbors
  • to - the state, the cell will transition in, if the rule applies

Automata Without Symmetry

step : Automata state -> Grid state -> 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 state =
Automata
(Neighborhood (Maybe state))
(Neighborhood (RuleExpression (Maybe state)))
Location
state

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.

automataWithoutSymmetry : Order state -> List (Rule state) -> Automata state

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.

Checkout CellAutomata.LifeLike for a more detailed description.

Our example with the ant, can now be implemented the following way:

[ { from = Just Up, neighbors = anyNeighborhood, to = Nothing }
, { from = Just Down, neighbors = anyNeighborhood, to = Nothing }
, { from = Just Left, neighbors = anyNeighborhood, to = Nothing }
, { from = Just Right, neighbors = anyNeighborhood, to = Nothing }
, { from = Nothing, to = Just Up, neighbors = { anyNeighborhood | south = Exactly <| Just Up, southEast = Exactly <| Just Wall } }
, { from = Nothing, to = Just Right, neighbors = { anyNeighborhood | south = Exactly <| Just Wall, west = Exactly <| Just Up } }
, { from = Nothing, to = Just Right, neighbors = { anyNeighborhood | west = Exactly <| Just Right, southWest = Exactly <| Just Wall } }
, { from = Nothing, to = Just Down, neighbors = { anyNeighborhood | west = Exactly <| Just Wall, north = Exactly <| Just Right } }
, { from = Nothing, to = Just Down, neighbors = { anyNeighborhood | north = Exactly <| Just Right, northWest = Exactly <| Just Wall } }
, { from = Nothing, to = Just Left, neighbors = { anyNeighborhood | north = Exactly <| Just Wall, east = Exactly <| Just Down } }
, { from = Nothing, to = Just Left, neighbors = { anyNeighborhood | east = Exactly <| Just Right, northEast = Exactly <| Just Wall } }
, { from = Nothing, to = Just Up, neighbors = { anyNeighborhood | east = Exactly <| Just Wall, south = Exactly <| Just Left } }
]
    |> automataWithoutSymmetry order

This code is on purpose more complicated as it should be. If possible one should always use a custom symmetry.

Symmetries

Symmetry

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

A symmetry is just a function that determines if a rule is sucessfully applied. If so, it returns the new state.

Maybe state
-> Neighborhood (Maybe state)
-> Rule state
-> Maybe (Maybe state)

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

(state -> state)
-> Maybe state
-> Neighborhood (Maybe state)
-> Rule state
-> Maybe (Maybe state)

Pattern may be horizontally mirrored

The first argument is a function (state -> state) that states how the values of the state can be mirrored (horizontally). Use the identity function if you do not see a need in specifing the first arguement.

As example, given the state

State = Up
    | Down
    | Left
    | Right

We can specify a symmetry the following way

horMirrorSymmetry
    (\state ->
        case state of
            Up ->
                Down


            Down ->
                Up


            a ->
                a
    )
(state -> state)
-> Maybe state
-> Neighborhood (Maybe state)
-> Rule state
-> Maybe (Maybe state)

Pattern may be vertically mirrored

The first argument is a function (state -> state) that states how the values of the state can be mirrored (vertically). Use the identity function if you dont see a need in specifing the first arguement.

As example, given the state

State = Up
    | Down
    | Left
    | Right

We can specify the symmetry the following way

vertMirrorSymmetry
    (\state ->
        case state of
            Left ->
                Right


            Right ->
                Left


            a ->
                a
    )
(state -> state)
-> Maybe state
-> Neighborhood (Maybe state)
-> Rule state
-> Maybe (Maybe state)

Pattern may be rotated in any position.

The first argument is a function (state -> state) that states how the values of the state can be rotated (clock-wise). Use the identity function if you dont see a need in specifing the first arguement.

As example, given the state

State = North
    | NorthEast
    | East
    | SouthEast
    | South
    | SouthWest
    | West
    | NorthWest

We can specify the symmetry the following way

rot45Symmetry
    (\state ->
        case state of
            North ->
                NorthEast


            NorthEast ->
                East


            East ->
                SouthEast


            SouthEast ->
                South


            South ->
                SouthWest


            SouthWest ->
                West


            South ->
                NorthWest


            NorthWest ->
                North
    )
(state -> state)
-> Maybe state
-> Neighborhood (Maybe state)
-> Rule state
-> Maybe (Maybe state)

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

The first argument is a function (state -> state) that states how the values of the state can be rotated (clock-wise). Use the identity function if you dont see a need in specifing the first arguement.

As example, given the state

State = Left
    | Right
    | Up
    | Down

We can specify the symmetry the following way

rot90Symmetry
    (\state ->
        case state of
            Left ->
                Down


            Down ->
                Right


            Right ->
                Up


            Up ->
                Left
    )

Automata

automata : Symmetry state -> Order state -> List (Rule state) -> Automata state

With this function we can now add our own symmetry.

The ant example can now be implemented the following way:

let
    rotState : State -> State
    rotState state =
        case state of
            Up ->
                Right


            Right ->
                Down


            Down ->
                Left


            Left ->
                Up


            a ->
                a
in
( List.concat
    [   [Up,Left,Right,Down]
        |> List.map
            (\dir ->
                { from = Just dir
                , neighbors = anyNeighborhood
                , to = Nothing
                }
            )
    ,   [   { from = Nothing
            , to = Just Up
            , neighbors =
                    { anyNeighborhood
                    | south = Exactly <| Just Up
                    , southEast = Exactly <| Just Wall
                    }
            }
        ,   { from = Nothing
            , to = Just Right
            , neighbors =
                { anyNeighborhood
                | south = Exactly <| Just Wall
                , west = Exactly <| Just Up }
                }
        ,   { from = Nothing
            , to = Just Right
            , neighbors =
                { anyNeighborhood
                | southWest = Exactly <| Just Wall
                , west = Exactly <| Just Down }
                }
        ]
    ]
)
    |> automata (rot90Symmetry rotState) order

Helpful sub-functions

mapNeighborhood : (a -> b) -> Neighborhood a -> Neighborhood b

Transforms a neighborhood.

It takes a function a -> b in order to transform a Neighborhood a to Neighborhood b