packages feed

smarties-1.2.1: examples/slimes/Main.hs

{-|
Module      : Main
Description : Slime Game
Copyright   : (c) Peter Lu, 2018
License     : GPL-3
Maintainer  : chippermonky@gmail.com
Stability   : experimental

Example simulating colony of slimes with feelings using smarties.
Unfinished, not working, and entirely unintuitive. You're welcome to fix it for me :).
-}


{-# LANGUAGE TypeSynonymInstances           #-}

module Main where

import Smarties
import System.Random
import Control.Concurrent
import Control.Monad hiding (sequence)
import Control.Applicative ((<$>))
import Control.Monad.ST
import Lens.Micro
import Lens.Micro.TH
import Prelude hiding (sequence)
import Data.List
import Data.List.Index (ifoldl)
import Data.Maybe
import qualified Data.Vector as V
import qualified Data.Vector.Mutable as MV
import Text.Printf

type Pos = (Int, Int)

addPos :: Pos -> Pos -> Pos
addPos (x1,y1) (x2,y2) = (x1+x2, y1+y2)

data Feelings = Happy | Sad | Hungry | Apathy deriving (Show, Eq)
data Doings = BlowingBubbles | Eating | Bored deriving (Show)

data Slime = Slime {
  _pos :: Pos,
  _feeling :: Feelings,
  _doings :: Doings,
  _weight :: Int
} deriving (Show)

makeLenses ''Slime

-- world size parameters
width :: Int
width = 20
height :: Int
height = 20
numberCells :: Int
numberCells = width * height
neighbors :: [Pos]
neighbors = [(-1,0),(1,0),(0,-1),(0,1)]


wrapCoords :: Pos -> Pos
wrapCoords (x,y) = (x `mod` width, y `mod` height)

-- TODO make sure this does what you want with neg coordinates...
wrapFlattenCoords :: Pos -> Int
wrapFlattenCoords (x,y) = (y `mod` height) * width + x `mod` width

-- behavior tree types
type Slimes = [Slime]
type SlimeGrid = V.Vector (Maybe Slime)
type PerceptionType = (SlimeGrid, Slime)

-- slime action is a little weird, for example if the output is [\x->[x],\x->[x]] this will actually make 2 copies of the slime in the same spot.
type ActionType = (Slime -> Slimes)

-- | extract slime that is being operated on from behavior tree perception
getMyself :: NodeSequence g PerceptionType ActionType Slime
getMyself = do
  (_, s) <- getPerception
  return s

-- | extract a neighboring slime
getSlimeRel :: Pos -> NodeSequence g PerceptionType ActionType (Maybe Slime)
getSlimeRel p = do
  (grid, _) <- getPerception
  return $ grid V.! wrapFlattenCoords p

-- | get a list of neigboring slimes
getNeighborSlimes :: NodeSequence g PerceptionType ActionType Slimes
getNeighborSlimes = do
  (grid, s) <- getPerception
  return . mapMaybe ((grid V.!) . wrapFlattenCoords . addPos (_pos s)) $ neighbors

-- behavior tree nodes
-- DELETE
conditionSlimeIsFeeling :: Feelings -> Slime -> NodeSequence g PerceptionType ActionType ()
conditionSlimeIsFeeling f s = fromCondition $
  SimpleCondition (\_ -> _feeling s == f)

actionMoveSlime :: Pos -> NodeSequence g PerceptionType ActionType ()
actionMoveSlime p = fromAction $
  SimpleAction (\_ -> \s -> [set pos (wrapCoords p) s])

-- |
actionMoveSlimeRel :: Pos -> NodeSequence g PerceptionType ActionType ()
actionMoveSlimeRel p = fromAction $
  SimpleAction (\_ -> \s -> [over pos (wrapCoords . addPos p) s])

actionSlime :: NodeSequence g PerceptionType ActionType ()
actionSlime = actionMoveSlimeRel (0,0)

actionCloneSlimeRel :: Pos -> NodeSequence g PerceptionType ActionType ()
actionCloneSlimeRel p = do
  actionMoveSlimeRel p
  actionSlime


-- | for testing
potatoTree :: NodeSequence g PerceptionType ActionType ()
potatoTree = do
  actionMoveSlimeRel (1,-1)

-- | our behavior tree
slimeTree :: NodeSequence g PerceptionType ActionType ()
slimeTree = do
  nbs <- getNeighborSlimes
  s <- getMyself
  selector [
    -- no neighbors
    do
      condition (null nbs)
      case _feeling s of
        Happy -> actionCloneSlimeRel (1,0)
        Sad -> actionCloneSlimeRel (-1,0)
        Hungry -> actionMoveSlimeRel (0,1)
        _ -> actionSlime
    -- 1 neighbor
    , do
      condition (length nbs == 1)
      let
        nb = head nbs
      case (_feeling nb, _feeling s) of
        (Happy, Happy) -> actionMoveSlimeRel (0,1)
        (Sad, Sad) -> actionCloneSlimeRel (0,-1)
        (Sad, Happy) -> actionMoveSlime (_pos nb)
        _ -> actionSlime
    -- >1 neighbors
    , actionMoveSlimeRel (0,1)
    ]

    -- > 1 neighbor case
    -- don't do anything, this means the slime will die :(


-- | DELETE
-- our slime action is a special case of behavior tree action type where there should only ever be one action in the output of the tree
-- we do a runtime check here to make sure this is the case
-- unfortunately smarties currently does not support type level checking of this constraint :(.
extractHead :: [ActionType] -> ActionType
extractHead fs
  | null fs = (: [])
  | length fs == 1 = head fs
  | otherwise = error "slime behavior tree must only have one output"

-- | puts slimes in a grid
makeSlimeGrid :: Slimes -> SlimeGrid
makeSlimeGrid slimes = runST $ do
  grid <- MV.replicate numberCells Nothing
  forM_ slimes $ \s@(Slime (x,y) _ _ _) -> MV.write grid (y*width+x) (Just s)
  V.freeze grid

-- | helper for writing slimes to console :)
renderSlime :: Slime -> String
renderSlime (Slime _ f _ _) = case f of
  Happy -> "😊"
  Sad -> "😟"
  Hungry -> "πŸ˜‹"
  Apathy -> "😐"

-- | helper for writing slimes to console :)
renderSlimes :: Slimes -> String
renderSlimes = Data.List.Index.ifoldl func "" . V.toList . makeSlimeGrid where
  func acc i x = output where
    nl = if (i+1) `mod` width == 0 then "\n" else ""
    se = case x of
      Just s -> renderSlime s
      Nothing -> "🌱"
    output = printf "%s%s%s" acc se nl

-- | fuse slimes that share the same cell
fuseSlimes :: Slimes -> Slimes
fuseSlimes slimes =  runST $ do
  grid <- MV.replicate numberCells Nothing
  forM_ slimes $ \s@(Slime (x,y) _ _ w) ->
    MV.modify grid
      (\case
        -- fused slimes just ate each other so they are
        Just (Slime _ _ _ w2) -> Just $ Slime (x,y) Happy Eating (w+w2)
        Nothing -> Just s)
      (y*width+x)
  catMaybes . V.toList <$> V.freeze grid

-- | run slimeTree for each slime collecting results
slimeCycle :: g -> Slimes -> (g, Slimes)
slimeCycle g0 slimes = over _2 (fuseSlimes . concat) (mapAccumL runSlimeTree g0 slimes) where
  -- function to run slime tree over all slimes accumulating the RNG
  runSlimeTree g slime = (g', concat (map ($ slime) os)) where
    (g', _, _, os) = execNodeSequence slimeTree g (makeSlimeGrid slimes, slime)

applyNtimes :: (Num n, Ord n) => n -> (a -> a) -> a -> a
applyNtimes 1 f x = f x
applyNtimes n f x = f (applyNtimes (n-1) f x)


{-exitLoop :: IO ()
exitLoop = do
  minput <- getInputChar "% "
  case minput of
    Nothing -> return ()
    Just 'q' -> exitSuccess-}

main :: IO ()
main = do
  --forkIO exitLoop
  stdgen <- getStdGen
  let
    genesis = [Slime (0,0) Sad Bored 1] -- 😒
    --outSlimes = applyNtimes 100 (\(g,s) -> slimeCycle g s) (stdgen, genesis)
    cycleOnce (n :: Int) (g,s) = do
      putStrLn "done"
      putStrLn $ renderSlimes s
      let (g',s') = slimeCycle g s
      putStrLn $ "gen " ++ show n
      threadDelay 100000
      cycleOnce (n+1) (g',s')
  cycleOnce 0 (stdgen, genesis)