packages feed

ca-patterns-0.1.0.0: src/Data/CA/Pattern.hs

{-|
The functions in this module allow you to create, transform,
and combine CA patterns.
-}

{-# LANGUAGE BlockArguments, LambdaCase, OverloadedStrings #-}

module Data.CA.Pattern
  ( -- * Cells
    Cell(..), isDead, isAlive
    -- * Patterns
  , Pattern, lookup, generate, height, width, dimensions, valid
    -- * Vector conversions
  , fromRectVector, fromVector, toVector
    -- * List conversions
  , fromRectList, fromList, toList
    -- * Text and string conversions
  , toText, toString
    -- * Trimming
  , trimTop, trimBottom, trimLeft, trimRight, trim
    -- * Cropping and padding
  , setHeight, setWidth, setDimensions
    -- * Transformations
  , reflectX, reflectY, rotateL, rotateR
    -- * Combining
  , combine
  ) where

import Prelude hiding (lookup)
import qualified Data.Maybe as Maybe
import Data.String (IsString)

import qualified Data.Vector as Vec
import Data.Vector (Vector, (!?))

import qualified Data.Text as Text
import Data.Text (Text)

padEnd :: Int -> a -> Vector a -> Vector a
padEnd n val row = row <> Vec.replicate (n - length row) val

dropWhileEnd :: (a -> Bool) -> Vector a -> Vector a
dropWhileEnd f row
  | Vec.null row = Vec.empty
  | f (Vec.last row) = dropWhileEnd f (Vec.init row)
  | otherwise = row

dropFirst :: Vector a -> Vector a
dropFirst = Vec.drop 1

dropLast :: Vector a -> Vector a
dropLast row
  | Vec.null row = Vec.empty
  | otherwise = Vec.init row

firstCell :: Vector Cell -> Cell
firstCell row
  | Vec.null row = Dead
  | otherwise = Vec.head row

lastCell :: Vector Cell -> Cell
lastCell row
  | Vec.null row = Dead
  | otherwise = Vec.last row

{-|
The state of a cell.
-}
data Cell = Dead | Alive
  deriving (Eq, Ord, Show)

isDead :: Cell -> Bool
isDead = \case { Dead -> True; Alive -> False }

isAlive :: Cell -> Bool
isAlive = \case { Dead -> False; Alive -> True }

{-|
A pattern in a 2-dimensional 2-state cellular automaton.
-}
newtype Pattern = Pattern (Vector (Vector Cell))

transform :: (Vector (Vector Cell) -> Vector (Vector Cell)) -> Pattern -> Pattern
transform f (Pattern rows) = Pattern (f rows)

{-|
Get the state of one of the cells in a pattern. @lookup 0 0@
returns the cell in the upper-left corner. If the row
or column number is out of range, this function will
return 'Dead'.
-}
lookup
  :: Int -- ^ row
  -> Int -- ^ column
  -> Pattern -> Cell
lookup r c (Pattern rows) = Maybe.fromMaybe Dead (rows !? r >>= (!? c))

{-|
Generate a pattern from a function.
-}
generate
  :: Int -- ^ height
  -> Int -- ^ width
  -> (Int -> Int -> Cell) -- ^ function taking row and column
  -> Pattern
generate h w f = Pattern (Vec.generate h \r -> Vec.generate w \c -> f r c)

height :: Pattern -> Int
height (Pattern rows) = Vec.length rows

width :: Pattern -> Int
width (Pattern rows)
  | Vec.null rows = 0
  | otherwise = Vec.length (Vec.head rows)

{-|
Get the height and width of a pattern.
-}
dimensions :: Pattern -> (Int, Int)
dimensions pat = (height pat, width pat)

{-|
Test if a pattern is valid, i.e. rectangular.
Some of the functions in this module only behave
properly on rectangular patterns.
-}
valid :: Pattern -> Bool
valid pat@(Pattern rows) = let
  w = width pat
  validRow row = Vec.length row == w
  in all validRow rows

{-|
Convert a vector of rows into a pattern, assuming
the rows are all the same length.
-}
fromRectVector :: Vector (Vector Cell) -> Pattern
fromRectVector = Pattern

{-|
Convert a vector of rows into a pattern. If
the rows are not all the same length, they will
padded with dead cells until the pattern is
rectangular.
-}
fromVector :: Vector (Vector Cell) -> Pattern
fromVector rows = let
  maxWidth = foldr (max . Vec.length) 0 rows
  padded = fmap (padEnd maxWidth Dead) rows
  in Pattern padded

{-|
Convert a pattern into a vector of rows.
-}
toVector :: Pattern -> Vector (Vector Cell)
toVector (Pattern rows) = rows

{-|
Convert a list of rows into a pattern, assuming
the rows are all the same length.
-}
fromRectList :: [[Cell]] -> Pattern
fromRectList = fromRectVector . Vec.fromList . map Vec.fromList

{-|
Convert a list of rows into a pattern. If
the rows are not all the same length, they will
padded with dead cells until the pattern is
rectangular.
-}
fromList :: [[Cell]] -> Pattern
fromList = fromVector . Vec.fromList . map Vec.fromList

{-|
Convert a pattern into a list of rows.
-}
toList :: Pattern -> [[Cell]]
toList (Pattern rows) = map Vec.toList (Vec.toList rows)

toSomeString :: (Monoid s, IsString s) => (Vector Char -> s) -> Char -> Char -> Pattern -> s
toSomeString makeStr dead alive (Pattern rows) = let
  toChar = \case { Dead -> dead; Alive -> alive }
  makeLine row = makeStr (fmap toChar row) <> "\n"
  in foldMap makeLine rows

{-|
Convert a pattern into text. For example, @toText \'.' \'Z'@
will replace each dead cell with a @.@ and each live cell
with a @Z@.
-}
toText
  :: Char -- ^ dead cell
  -> Char -- ^ live cell
  -> Pattern -> Text
toText = let
  add c text = Text.singleton c <> text
  in toSomeString (foldr add "")

{-|
Convert a pattern into a string.
-}
toString
  :: Char -- ^ dead cell
  -> Char -- ^ live cell
  -> Pattern -> String
toString = toSomeString Vec.toList

{-|
Remove rows of dead cells from the top of a pattern.
-}
trimTop :: Pattern -> Pattern
trimTop = transform (Vec.dropWhile (all isDead))

{-|
Remove rows of dead cells from the bottom of a pattern.
-}
trimBottom :: Pattern -> Pattern
trimBottom = transform (dropWhileEnd (all isDead))

trimLeftV :: Vector (Vector Cell) -> Vector (Vector Cell)
trimLeftV rows
  | all (isDead . firstCell) rows = trimLeftV (fmap dropFirst rows)
  | otherwise = rows

trimRightV :: Vector (Vector Cell) -> Vector (Vector Cell)
trimRightV rows
  | all (isDead . lastCell) rows = trimRightV (fmap dropLast rows)
  | otherwise = rows

{-|
Remove columns of dead cells from the left side of a pattern.
-}
trimLeft :: Pattern -> Pattern
trimLeft = transform trimLeftV

{-|
You get the idea.
-}
trimRight :: Pattern -> Pattern
trimRight = transform trimRightV

{-|
A composition of 'trimTop', 'trimBottom', 'trimLeft', and 'trimRight'.
Removes as many dead cells from the pattern as possible while
keeping it rectangular.
-}
trim :: Pattern -> Pattern
trim = trimTop . trimBottom . trimLeft . trimRight

{-|
Force a pattern to have the given height by removing rows
from the bottom or by adding rows of dead cells.
-}
setHeight :: Int -> Pattern -> Pattern
setHeight h pat =
  case compare h (height pat) of
    LT -> transform (Vec.take h) pat
    EQ -> pat
    GT -> let
      row = Vec.replicate (width pat) Dead
      in transform (padEnd h row) pat

{-|
Force a pattern to have the given width by remove columns
from the right or by adding columns of dead cells.
-}
setWidth :: Int -> Pattern -> Pattern
setWidth w pat =
  case compare w (width pat) of
    LT -> transform (fmap (Vec.take w)) pat
    EQ -> pat
    GT -> transform (fmap (padEnd w Dead)) pat

{-|
Set the height and width of a pattern.
-}
setDimensions :: Int -> Int -> Pattern -> Pattern
setDimensions h w = setHeight h . setWidth w

{-|
Reflect vertically, switching the top and the bottom.
-}
reflectY :: Pattern -> Pattern
reflectY = transform Vec.reverse

{-|
Reflect horizontally, switching the left and the right.
-}
reflectX :: Pattern -> Pattern
reflectX = transform (fmap Vec.reverse)

{-|
Rotate counterclockwise by a quarter turn.
-}
rotateL :: Pattern -> Pattern
rotateL pat = let
  (h, w) = dimensions pat
  in generate w h \r c -> lookup c (w - r - 1) pat

{-|
Rotate clockwise by a quarter turn.
-}
rotateR :: Pattern -> Pattern
rotateR pat = let
  (h, w) = dimensions pat
  in generate w h \r c -> lookup (h - c - 1) r pat

{-|
Combine two patterns given a vertical and horizontal offset,
which describe the displacement of the second pattern relative
to the first one.
-}
combine
  :: Int -- ^ vertical offset
  -> Int -- ^ horizontal offset
  -> Pattern -> Pattern -> Pattern
combine y x pat1 pat2 = let
  (y1, y2) = if y < 0 then (-y, 0) else (0, y)
  (x1, x2) = if x < 0 then (-x, 0) else (0, x)
  h = max (height pat1 + y1) (height pat2 + y2)
  w = max (width pat1 + x1) (width pat2 + x2)
  in generate h w \r c -> let
    cell1 = lookup (r - y1) (c - x1) pat1
    cell2 = lookup (r - y2) (c - x2) pat2
    in max cell1 cell2