Extreme branchless: Langton Ant
Continuing my branchless journey.
This time: Langton Ant kata.
It's not a kata I'm used to doing, it has only two rules:
- At a white square, turn 90° right, flip the color of the square, move forward one unit
- At a black square, turn 90° left, flip the color of the square, move forward one unit
At a bird's eye view it's a Mar Rover.
Let's start with our test cases:
describe "Langton Ant" $ do
forM_ [
(white, north, black, east)
, (white, east, black, south)
, (white, south, black, west)
, (white, west, black, north)
, (black, north, white, west)
, (black, west, white, south)
, (black, south, white, east)
, (black, east, white, north)
] $ \(startSquare, startDirection, expectedSquare, expectedDirection) ->
it ("when facing " <> show startDirection <> " on a " <> show startSquare <> " square should face " <> show expectedDirection <> " on a " <> show expectedSquare <> " square") $
next (startSquare, startDirection) `shouldBe` (expectedSquare, expectedDirection)
We have few cycles:
- Square (color)
- Direction (on both rotations)
We can reuse our chaining tactics.
First on directions:
data Direction = Direction
{ name :: String
, rotateLeft :: Direction
, rotateRight :: Direction
}
show = (.name)
(==) = (==) `on` (.name)
We have:
- a name (mainly for display and tests)
- the result of a left rotation
- the result of a right rotation
We can define each Direction
s:
north = Direction
{ name = "north"
, rotateLeft = west
, rotateRight = east
}
south = Direction
{ name = "south"
, rotateLeft = east
, rotateRight = west
}
west = Direction
{ name = "west"
, rotateLeft = south
, rotateRight = north
}
east = Direction
{ name = "east"
, rotateLeft = north
, rotateRight = south
}
Then, we have the Square
:
data Square = Square
{ name :: String
, flip :: Square
, rotate :: Direction -> Direction
}
show = (.name)
(==) = (==) `on` (.name)
It's a bit more complex to design:
- a name (mainly for display and tests)
- the result of a flip
- a way to fetch the result of the correct rotation
We can define our two squares:
black = Square
{ name = "black"
, flip = white
, rotate = (.rotateLeft)
}
white = Square
{ name = "white"
, flip = black
, rotate = (.rotateRight)
}
Finally, we can compose everything:
next (square, direction) = (square.flip, square.rotate direction)
At this point, the process is simple: the new Square
is the flipped result,
and the new Direction
is the rotation result selected by the Square
rotate
strategy.
We could add an extra color, it would only take to a link in the chain:
red = Square
{ name = "red"
, flip = white
, rotate = id
}
Note: id
will keep current Direction