creatur-5.3.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 #-}
module ALife.Creatur.Universe
(
-- * Constructors
Universe(..),
SimpleUniverse,
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,
endOfRound,
refresh,
markDone
) where
import Prelude hiding (lookup)
import qualified ALife.Creatur as A
import qualified ALife.Creatur.AgentNamer 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 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.AgentNamer (AgentNamer u), CL.Checklist (Checklist 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 AgentNamer u
agentNamer :: u -> AgentNamer u
setAgentNamer :: u -> AgentNamer u -> u
type Checklist u
checklist :: u -> Checklist u
setChecklist :: u -> Checklist 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
withAgentNamer
:: (Universe u, Monad m)
=> StateT (AgentNamer u) m a -> StateT u m a
withAgentNamer program = do
u <- get
stateMap (setAgentNamer 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
-- | 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 = withAgentNamer 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)
-- agentList <- agentIds
-- writeToLog $ "DEBUG agents:" ++ show agentList
-- writeToLog $ "DEBUG " ++ A.agentId a ++ " new? " ++ show newAgent
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"
-- | EXPORTED FOR TESTING ONLY
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 @'refresh'@ 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 the lineup is empty or if all of the agents in the
-- lineup have had their turn at the CPU.
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.
refresh :: Universe u => StateT u IO ()
refresh = 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
data SimpleUniverse a = SimpleUniverse
{
suClock :: K.PersistentCounter,
suLogger :: L.SimpleRotatingLogger,
suDB :: (FS.FSDatabase a),
suNamer :: N.SimpleAgentNamer,
suChecklist :: CL.PersistentChecklist
} deriving (Show, Eq)
instance (A.Agent a, Serialize 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) = L.SimpleRotatingLogger
logger = suLogger
setLogger u l = u { suLogger=l }
type AgentDB (SimpleUniverse a) = FS.FSDatabase a
agentDB = suDB
setAgentDB u d = u { suDB=d }
type AgentNamer (SimpleUniverse a) = N.SimpleAgentNamer
agentNamer = suNamer
setAgentNamer u n = u { suNamer=n }
type Checklist (SimpleUniverse a) = CL.PersistentChecklist
checklist = suChecklist
setChecklist u cl = u { suChecklist=cl }
mkSimpleUniverse :: String -> FilePath -> Int -> SimpleUniverse a
mkSimpleUniverse name dir rotateCount
= SimpleUniverse c l d n cl
where c = K.mkPersistentCounter (dir ++ "/clock")
l = L.mkSimpleRotatingLogger (dir ++ "/log/") name rotateCount
d = FS.mkFSDatabase (dir ++ "/db")
n = N.mkSimpleAgentNamer (name ++ "_") (dir ++ "/namer")
cl = CL.mkPersistentChecklist (dir ++ "/todo")
data CachedUniverse a = CachedUniverse
{
cuClock :: K.PersistentCounter,
cuLogger :: L.SimpleRotatingLogger,
cuDB :: (CFS.CachedFSDatabase a),
cuNamer :: N.SimpleAgentNamer,
cuChecklist :: CL.PersistentChecklist
} deriving (Show, Eq)
instance (A.Agent a, Serialize 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) = L.SimpleRotatingLogger
logger = cuLogger
setLogger u l = u { cuLogger=l }
type AgentDB (CachedUniverse a) = CFS.CachedFSDatabase a
agentDB = cuDB
setAgentDB u d = u { cuDB=d }
type AgentNamer (CachedUniverse a) = N.SimpleAgentNamer
agentNamer = cuNamer
setAgentNamer u n = u { cuNamer=n }
type Checklist (CachedUniverse a) = CL.PersistentChecklist
checklist = cuChecklist
setChecklist u cl = u { cuChecklist=cl }
mkCachedUniverse :: String -> FilePath -> Int -> Int -> CachedUniverse a
mkCachedUniverse name dir rotateCount cacheSize
= CachedUniverse c l d n cl
where c = K.mkPersistentCounter (dir ++ "/clock")
l = L.mkSimpleRotatingLogger (dir ++ "/log/") name rotateCount
d = CFS.mkCachedFSDatabase (dir ++ "/db") cacheSize
n = N.mkSimpleAgentNamer (name ++ "_") (dir ++ "/namer")
cl = CL.mkPersistentChecklist (dir ++ "/todo")