creatur-5.6.0: src/ALife/Creatur/Universe.hs
------------------------------------------------------------------------
-- |
-- Module : ALife.Creatur.Universe
-- Copyright : (c) Amy de Buitléir 2012-2014
-- License : BSD-style
-- Maintainer : amy@nualeargais.ie
-- Stability : experimental
-- Portability : portable
--
-- Provides a habitat for artificial life.
--
------------------------------------------------------------------------
{-# LANGUAGE TypeFamilies, FlexibleContexts, FlexibleInstances, ScopedTypeVariables #-}
module ALife.Creatur.Universe
(
-- * Constructors
Universe(..),
SimpleUniverse,
CachedUniverse,
mkSimpleUniverse,
mkCachedUniverse,
-- * Clock
currentTime,
incTime,
-- * Logging
writeToLog,
-- * Database
agentIds,
archivedAgentIds,
popSize,
getAgent,
getAgentFromArchive,
getAgents,
-- addAgent,
store,
-- * Names
genName,
-- * Agent programs
AgentProgram,
withAgent,
AgentsProgram,
withAgents,
isNew, -- exported for testing only
-- * Agent rotation
lineup,
startOfRound,
endOfRound,
refreshLineup,
markDone,
replenishEnergyPool,
withdrawEnergy
) where
import Prelude hiding (lookup)
import qualified ALife.Creatur as A
import qualified ALife.Creatur.Namer as N
import qualified ALife.Creatur.Checklist as CL
import qualified ALife.Creatur.Clock as C
import qualified ALife.Creatur.Counter as K
import qualified ALife.Creatur.Database as D
import qualified ALife.Creatur.Database.FileSystem as FS
import qualified ALife.Creatur.Database.CachedFileSystem as CFS
import qualified ALife.Creatur.Logger as L
import qualified ALife.Creatur.Logger.SimpleLogger as SL
import qualified ALife.Creatur.EnergyPool as E
import ALife.Creatur.Util (stateMap, shuffle)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Random (evalRandIO)
import Control.Monad.State (StateT, get)
import Data.Either (partitionEithers)
import Data.Serialize (Serialize)
-- | A habitat containing artificial life.
class (C.Clock (Clock u), L.Logger (Logger u), D.Database (AgentDB u),
N.Namer (Namer u), CL.Checklist (Checklist u),
E.EnergyPool (EnergyPool u), A.Agent (Agent u), D.Record (Agent u),
Agent u ~ D.DBRecord (AgentDB u))
=> Universe u where
type Agent u
type Clock u
clock :: u -> Clock u
setClock :: u -> Clock u -> u
type Logger u
logger :: u -> Logger u
setLogger :: u -> Logger u -> u
type AgentDB u
agentDB :: u -> AgentDB u
setAgentDB :: u -> AgentDB u -> u
type Namer u
agentNamer :: u -> Namer u
setNamer :: u -> Namer u -> u
type Checklist u
checklist :: u -> Checklist u
setChecklist :: u -> Checklist u -> u
type EnergyPool u
energyPool :: u -> EnergyPool u
setEnergyPool :: u -> EnergyPool u -> u
withClock :: (Universe u, Monad m) => StateT (Clock u) m a -> StateT u m a
withClock program = do
u <- get
stateMap (setClock u) clock program
withLogger
:: (Universe u, Monad m)
=> StateT (Logger u) m a -> StateT u m a
withLogger program = do
u <- get
stateMap (setLogger u) logger program
withAgentDB
:: (Universe u, Monad m)
=> StateT (AgentDB u) m a -> StateT u m a
withAgentDB program = do
u <- get
stateMap (setAgentDB u) agentDB program
withNamer
:: (Universe u, Monad m)
=> StateT (Namer u) m a -> StateT u m a
withNamer program = do
u <- get
stateMap (setNamer u) agentNamer program
withChecklist
:: (Universe u, Monad m)
=> StateT (Checklist u) m a -> StateT u m a
withChecklist program = do
u <- get
stateMap (setChecklist u) checklist program
withEnergyPool
:: (Universe u, Monad m)
=> StateT (EnergyPool u) m a -> StateT u m a
withEnergyPool program = do
u <- get
stateMap (setEnergyPool u) energyPool program
-- | The current "time" (counter) in this universe
currentTime :: Universe u => StateT u IO A.Time
currentTime = withClock C.currentTime
-- | Increment the current "time" (counter) in this universe.
incTime :: Universe u => StateT u IO ()
incTime = withClock C.incTime
-- | Write a message to the log file for this universe.
writeToLog :: Universe u => String -> StateT u IO ()
writeToLog msg = do
t <- currentTime
let logMsg = show t ++ "\t" ++ msg
withLogger $ L.writeToLog logMsg
-- | Generate a unique name for a new agent.
genName :: Universe u => StateT u IO A.AgentId
genName = withNamer N.genName
-- | Returns the list of agents in the population.
agentIds :: Universe u => StateT u IO [A.AgentId]
agentIds = withAgentDB D.keys
-- | Returns the list of (dead) agents in the archive.
archivedAgentIds :: Universe u => StateT u IO [A.AgentId]
archivedAgentIds = withAgentDB D.archivedKeys
-- | Returns the current size of the population.
popSize :: Universe u => StateT u IO Int
popSize = withAgentDB D.numRecords
-- | Fetches the agent with the specified ID from the population.
-- Note: Changes made to this agent will not "take" until
-- @'store'@ is called.
getAgent
:: (Universe u, Serialize (Agent u))
=> A.AgentId -> StateT u IO (Either String (Agent u))
getAgent name = withAgentDB (D.lookup name)
-- | Fetches the agent with the specified ID from the archive.
getAgentFromArchive
:: (Universe u, Serialize (Agent u))
=> A.AgentId -> StateT u IO (Either String (Agent u))
getAgentFromArchive name = withAgentDB (D.lookupInArchive name)
-- | Fetches the agents with the specified IDs from the population.
getAgents
:: (Universe u, Serialize (Agent u))
=> [A.AgentId] -> StateT u IO (Either String [Agent u])
getAgents names = do
selected <- mapM getAgent names
let (msgs, agents) = partitionEithers selected
if null msgs
then return $ Right agents
else return . Left $ show msgs
-- | If the agent is alive, adds it to the population (replacing the
-- the previous copy of that agent, if any). If the agent is dead,
-- places it in the archive.
store
:: (Universe u, Serialize (Agent u))
=> Agent u -> StateT u IO ()
store a = do
newAgent <- isNew (A.agentId a)
withAgentDB (D.store a) -- Even dead agents should be stored (prior to archiving)
if A.isAlive a
then
if newAgent
then writeToLog $ A.agentId a ++ " added to population"
else writeToLog $ A.agentId a ++ " returned to population"
else do
withAgentDB (D.delete $ A.agentId a)
withChecklist $ CL.delete (A.agentId a)
writeToLog $ (A.agentId a) ++ " archived and removed from lineup"
isNew :: Universe u => A.AgentId -> StateT u IO Bool
isNew name = fmap (name `notElem`) agentIds
-- -- | Adds an agent to the universe.
-- addAgent
-- :: (Universe u, Serialize (Agent u))
-- => Agent u -> StateT u IO ()
-- addAgent a = withAgentDB $ D.store a
-- | A program involving one agent.
-- The input parameter is the agent.
-- The program must return the agent (which may have been modified).
type AgentProgram u = Agent u -> StateT u IO (Agent u)
-- | Run a program involving one agent
withAgent
:: (Universe u, Serialize (Agent u))
=> AgentProgram u -> A.AgentId -> StateT u IO ()
withAgent program name = do
result <- getAgent name
case result of
Left msg ->
writeToLog $ "Unable to read '" ++ name ++ "': " ++ msg
Right a ->
program a >>= store
-- | A program involving multiple agents.
-- The input parameter is a list of agents.
-- The program must return a list of agents that have been *modified*.
-- The order of the output list is not important.
type AgentsProgram u = [Agent u] -> StateT u IO [Agent u]
-- Run a program involving multiple agents.
withAgents
:: (Universe u, Serialize (Agent u))
=> AgentsProgram u -> [A.AgentId] -> StateT u IO ()
withAgents program names = do
result <- getAgents names
case result of
Left msg ->
writeToLog $ "Unable to read '" ++ show names ++ "': " ++ msg
Right as ->
program as >>= mapM_ store
-- | Returns the current lineup of (living) agents in the universe.
-- Note: Check for @'endOfRound'@ and call @'refreshLineup'@ if needed
-- before invoking this function.
lineup :: Universe u => StateT u IO [A.AgentId]
lineup = do
(xs,ys) <- withChecklist CL.status
return $ xs ++ ys
-- | Returns true if no agents have yet their turn at the CPU for this
-- round.
startOfRound :: Universe u => StateT u IO Bool
startOfRound = withChecklist CL.notStarted
-- | Returns true if the lineup is empty or if all of the agents in the
-- lineup have had their turn at the CPU for this round.
endOfRound :: Universe u => StateT u IO Bool
endOfRound = withChecklist CL.done
-- | Creates a fresh lineup containing all of the agents in the
-- population, in random order.
refreshLineup :: Universe u => StateT u IO ()
refreshLineup = do
as <- shuffledAgentIds
withChecklist (CL.setItems as)
-- | Mark the current agent done. If it is still alive, it will move
-- to the end of the lineup.
markDone :: Universe u => A.AgentId -> StateT u IO ()
markDone = withChecklist . CL.markDone
shuffledAgentIds :: Universe u => StateT u IO [String]
shuffledAgentIds
= agentIds >>= liftIO . evalRandIO . shuffle
-- | Replenish the energy pool
replenishEnergyPool :: Universe u => Double -> StateT u IO ()
replenishEnergyPool e = do
withEnergyPool (E.replenish e)
y <- withEnergyPool E.available
writeToLog $ "Replenished energy pool, " ++ show y ++ " available"
-- | Withdraw energy from the pool
withdrawEnergy :: Universe u => Double -> StateT u IO Double
withdrawEnergy e = do
x <- withEnergyPool (E.withdraw e)
y <- withEnergyPool E.available
writeToLog $ "Withdrew " ++ show x ++ " from energy pool, "
++ show y ++ " available"
return x
data SimpleUniverse a = SimpleUniverse
{
suClock :: K.PersistentCounter,
suLogger :: SL.SimpleLogger,
suDB :: FS.FSDatabase a,
suNamer :: N.SimpleNamer,
suChecklist :: CL.PersistentChecklist,
suEnergyPool :: E.PersistentEnergyPool
} deriving (Show, Eq)
instance (A.Agent a, D.Record a) => Universe (SimpleUniverse a) where
type Agent (SimpleUniverse a) = a
type Clock (SimpleUniverse a) = K.PersistentCounter
clock = suClock
setClock u c = u { suClock=c }
type Logger (SimpleUniverse a) = SL.SimpleLogger
logger = suLogger
setLogger u l = u { suLogger=l }
type AgentDB (SimpleUniverse a) = FS.FSDatabase a
agentDB = suDB
setAgentDB u d = u { suDB=d }
type Namer (SimpleUniverse a) = N.SimpleNamer
agentNamer = suNamer
setNamer u n = u { suNamer=n }
type Checklist (SimpleUniverse a) = CL.PersistentChecklist
checklist = suChecklist
setChecklist u cl = u { suChecklist=cl }
type EnergyPool (SimpleUniverse a) = E.PersistentEnergyPool
energyPool = suEnergyPool
setEnergyPool u cl = u { suEnergyPool=cl }
mkSimpleUniverse :: String -> FilePath -> SimpleUniverse a
mkSimpleUniverse name dir
= SimpleUniverse c l d n cl e
where c = K.mkPersistentCounter (dir ++ "/clock")
l = SL.mkSimpleLogger (dir ++ "/log/" ++ name ++ ".log")
d = FS.mkFSDatabase (dir ++ "/db")
n = N.mkSimpleNamer (name ++ "_") (dir ++ "/namer")
cl = CL.mkPersistentChecklist (dir ++ "/todo")
e = E.mkPersistentEnergyPool (dir ++ "/energy")
data CachedUniverse a = CachedUniverse
{
cuClock :: K.PersistentCounter,
cuLogger :: SL.SimpleLogger,
cuDB :: CFS.CachedFSDatabase a,
cuNamer :: N.SimpleNamer,
cuChecklist :: CL.PersistentChecklist,
cuEnergyPool :: E.PersistentEnergyPool
} deriving (Show, Eq)
instance (A.Agent a, D.SizedRecord a) => Universe (CachedUniverse a) where
type Agent (CachedUniverse a) = a
type Clock (CachedUniverse a) = K.PersistentCounter
clock = cuClock
setClock u c = u { cuClock=c }
type Logger (CachedUniverse a) = SL.SimpleLogger
logger = cuLogger
setLogger u l = u { cuLogger=l }
type AgentDB (CachedUniverse a) = CFS.CachedFSDatabase a
agentDB = cuDB
setAgentDB u d = u { cuDB=d }
type Namer (CachedUniverse a) = N.SimpleNamer
agentNamer = cuNamer
setNamer u n = u { cuNamer=n }
type Checklist (CachedUniverse a) = CL.PersistentChecklist
checklist = cuChecklist
setChecklist u cl = u { cuChecklist=cl }
type EnergyPool (CachedUniverse a) = E.PersistentEnergyPool
energyPool = cuEnergyPool
setEnergyPool u cl = u { cuEnergyPool=cl }
mkCachedUniverse :: String -> FilePath -> Int -> CachedUniverse a
mkCachedUniverse name dir cacheSize
= CachedUniverse c l d n cl e
where c = K.mkPersistentCounter (dir ++ "/clock")
l = SL.mkSimpleLogger (dir ++ "/log/" ++ name ++ ".log")
d = CFS.mkCachedFSDatabase (dir ++ "/db") cacheSize
n = N.mkSimpleNamer (name ++ "_") (dir ++ "/namer")
cl = CL.mkPersistentChecklist (dir ++ "/todo")
e = E.mkPersistentEnergyPool (dir ++ "/energy")