smarties: Haskell Behavior Tree Library

This is a package candidate release! Here you can preview how this package release will appear once published to the main package index (which can be accomplished via the 'maintain' link below). Please note that once a package has been published to the main package index it cannot be undone! Please consult the package uploading documentation for more information.

[maintain] [Publish]

Please see the README on Github at https://github.com/githubuser/smarties#readme


[Skip to Readme]

Properties

Versions 1.0.0, 1.2.1
Change log ChangeLog.md
Dependencies base (>=4.7 && <5.0), lens, monadplus, MonadRandom, mtl, QuickCheck (>=2.11), random, smarties, text [details]
License BSD-3-Clause
Copyright 2018 Peter Lu
Author pdlla
Maintainer chippermonky@gmail.com
Category Games
Home page https://github.com/pdlla/smarties#readme
Bug tracker https://github.com/pdlla/smarties/issues
Source repo head: git clone https://github.com/pdlla/smarties
Uploaded by pdlla at 2018-04-28T19:13:26Z

Modules

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees


Readme for smarties-1.0.0

[back to package description]

smarties

Smarties is a general purpose behavior tree library written in Haskell. The library supports utility AI for advanced decision making. Smarties implements many of the design patterns outlined in this paper and some that aren't.

Behavior trees are written in a DSL built with the NodeSequence monad. Monadic return values are used for computing utility.

Example

data Pronoun = HeHim | SheHer | TheyThem | FooBar | Other | Undecided deriving (Eq, Show)

data Student = Student {
    assignedPronoun :: Pronoun,
    preferredPronoun :: Pronoun,
    openlyChange :: Bool,
    jeans :: Int
} deriving (Show)

type School = [Student]
type SchoolTreeState = (School, Student)
instance TreeState SchoolTreeState 
type ActionType = (Student -> Student)

assignedPronounIs :: Pronoun -> Student -> Bool
assignedPronounIs p s = assignedPronoun s == p

preferredPronounIs :: Pronoun -> Student -> Bool
preferredPronounIs p s = preferredPronoun s == p

feminimity :: Student -> Float
feminimity = (/100.0) . fst . randomR (0.0,100.0) . mkStdGen . (+0) . jeans

masculinity :: Student -> Float
masculinity = (/100.0) . fst . randomR (0.0,100.0) . mkStdGen . (+1) . jeans

chromeXX :: Student -> Bool
chromeXX = (<50) . fst . randomR ((0,100)::(Int,Int)) . mkStdGen . (+2) . jeans

chromeXY :: Student -> Bool
chromeXY = (>50) . fst . randomR ((0,100)::(Int,Int)) . mkStdGen . (+2) . jeans

chromeNeither :: Student -> Bool
chromeNeither s = not (chromeXX s) && not (chromeXY s)

noneOfTheAbove :: Student -> Float
noneOfTheAbove = (/100.0) . fst . randomR (0.0,100.0) . mkStdGen . (+3) . jeans

developer :: Student -> Float
developer = (/100.0) . fst . randomR (0.0,100.0) . mkStdGen . (+4) . jeans

indecisiveness :: Student -> Float
indecisiveness = (/100.0) . fst . randomR (0.0,100.0) . mkStdGen . (+5) . jeans

-- totally cool if she or he keeps it him or herself ;)
-- for the purpose of this demo, this is determined by the kind of jeans a student wears. This is not true IRL.
dogmaticBeliefInBinaryBiologicalDeterminism :: Student -> Bool
dogmaticBeliefInBinaryBiologicalDeterminism s = b s && not (chromeNeither s) where
    b = (>99) . fst . randomR ((0,100)::(Int,Int)) . mkStdGen . (+2) . jeans

toZeroOne :: Bool -> Float
toZeroOne x = if x then 1.0 else 0.0

The first part of the code outlines the state vars that we want operate with/on with our behavior tree plus several helper functions. In particular, we define SchoolTreeState which will be the input/computation state (called perception) to our behavior tree and ActionType which will be its output type.

actionChangePronoun :: Pronoun -> NodeSequence g SchoolTreeState ActionType ()
actionChangePronoun p = fromAction $ 
    SimpleAction (\_ -> (\(Student a _ _ d) -> Student a p True d))

actionChangeBack :: NodeSequence g SchoolTreeState ActionType ()
actionChangeBack = fromAction $
    SimpleAction (\_ -> (\(Student a _ c d) -> Student a a c d))

conditionHasProperty :: (Student -> Bool) -> NodeSequence g SchoolTreeState ActionType ()
conditionHasProperty f = fromCondition $
    SimpleCondition (\(_, st) -> f st)

utilityProperty :: (Student -> Float) -> NodeSequence g SchoolTreeState ActionType Float
utilityProperty f = fromUtility $
    SimpleUtility (\(_, st) -> f st)

utilityNormalness :: (Student -> Float) -> NodeSequence g SchoolTreeState ActionType Float
utilityNormalness f = fromUtility $
    SimpleUtility (\(sc, _) -> (sum . map f $ sc) / (fromIntegral $ length sc))

Next we create several NodeSequences which will be the building blocks for our behavior tree. Smarties contains 4 helpers to facilitate making nodes: Action, Condition, Perception and Utility. Each helper has several constructors that represent subsets of a behavior tree operation. You can also use monadic syntax to create NodeSequences. There's a little more boiler plate and the syntax is a little more human readable.

studentTree :: (RandomGen g) => NodeSequence g SchoolTreeState ActionType Float
studentTree = utilityWeightedSelector
    [return . (*0.15) . (+0.01) =<< utilityWeightedSelector 
        [sequence $ do
            a <- utilityNormalness (toZeroOne . openlyChange)
            b <- utilityProperty feminimity
            actionChangePronoun SheHer
            return $ a * b 
        ,sequence $ do
            a <- utilityNormalness (toZeroOne . openlyChange)
            b <- utilityProperty masculinity
            actionChangePronoun HeHim
            return $ a * b 
        ,sequence $ do
            a <- utilityNormalness (toZeroOne . openlyChange)
            b <- utilityProperty developer
            actionChangePronoun FooBar
            return $ a * b 
        ,sequence $ do
            a <- utilityNormalness (toZeroOne . openlyChange)
            b <- utilityProperty noneOfTheAbove
            actionChangePronoun Other
            return $ a * b 
        ,sequence $ do
            a <- utilityNormalness (toZeroOne . openlyChange)
            m <- utilityProperty masculinity
            f <- utilityProperty feminimity
            actionChangePronoun TheyThem
            return $ a * ((1.0-m)+(1.0-f)) / 2.0 
        ]
    ,sequence $ do
        a <- utilityProperty indecisiveness
        actionChangeBack
        return $ 0.01 * a
    ,sequence $ do
        a <- utilityNormalness ((1-) . toZeroOne . openlyChange)
        result SUCCESS 
        return a
    ]

Using our NodeSequences, we define the tree itself. Note that sequence $ do is the same as just do and used for semantic clarity.

makeStudent :: (RandomGen g) => Rand g Student
makeStudent = do
	(isFemale::Bool) <- getRandom 
	(sJeans::Int) <- getRandom
	let 
		pronoun = if isFemale then SheHer else HeHim
	return $ Student pronoun pronoun False sJeans
        
main :: IO ()
main = do
	stdgen <- getStdGen
	students <- replicateM 100 $ evalRandIO makeStudent
	let
		tree = getTree studentTree
		studentfn g s = (g', (foldl1 (.) o) s) where
			(rslt, (BasicTreeState _ g'), o) = tickTree tree $ BasicTreeState (students, s) g
		ticktStudents sts = snd $ mapAccumL studentfn stdgen sts
		loop 0 sts = return ()
		loop n sts = do 
			let
				nextsts = ticktStudents sts
			putStrLn . show $ nextsts
			loop (n-1) nextsts
	loop 365 students

Finally, we run the tree and output the results :D.

Terminology

Understanding NodeSequence

NodeSequence is a computation that executes all it's internal nodes. At each >>= it will check the output and early exit if it reaches a FAIL. sequence is exactly the same as NodeSequence except that it will create a scope on the perception.

NodeSequence has the following definition

data NodeSequence g p o a =  NodeSequence { runNodes :: g -> p -> (a, g, p, Status, [o]) }

The sequence represents a computation that takes a generator and perception and returns an output with the following types:

NodeSequence looks a lot like Statet (p,g) Writer [o] except with an addition Status output. The difference lies that with each >>= if the input computation has Status FAIL, the monad will stop passing on p and appending to [o]. It will continue to pass through g and evaluate a. Thus running NodeSequence produces an a and two thunks representing the collected state and output of the represented sequence up until the first FAIL. These thunks may or may not be evaluated depending on the decisions made by the parent nodes.

Advanced features

conditionBestFriendFoo = ... 
conditionWorstFriendFoo = ...
conditionBestFriendOfBestFriendFoo = ...
conditionBestFriendOfWorstFriendFoo = ...

instead, we can mark students in our tree's computation state allowing us to structure our nodes as such:

markSelf = ...
markBestFriendOfMarkedStudent = ...
markCrushOfMarkedStudent = ...
conditionMarkedStudentFoo = ...

allowing nodes to change the computation context of future nodes. This is both more general and signficantly reduces the amount of needed nodes for creating more complex computation spaces.

It is expected that perception implements Scopeable if you want to do this. Scopeable creates a scoping behavior for the computational variables. (TreeStackInfo x) => TreeState x y offers a default implementation of this where x is the scopeable computational state and y is the (not actually immutable) input state. Scopes must be explicitly created using the scope decorator. For example:

-- TODO

Future Development:

	scope $ selectorForEach $ do
		selectNthSquare
		i <- getSelection
		guard $ i `mod` 2 == 0
		colorSelectedSquare
	sequence $ do
		selector 
			[sequence $ do
				conditionA
				changePerceptionA
			,sequence $ do
				conditionB
				changePerceptionB
			]
		-- the perception will not contain any info from the previously run selector node
		...