{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
--------------------------------------------------------------------------------
module Xanthous.Game.State
( GameState(..)
, entities
, revealedPositions
, messageHistory
, randomGen
, promptState
, characterEntityID
, GamePromptState(..)
-- * Messages
, MessageHistory(..)
, pushMessage
, popMessage
, hideMessage
-- * App monad
, AppT(..)
, AppM
-- * Entities
, Draw(..)
, Brain(..)
, Brainless(..)
, brainVia
, Entity(..)
, SomeEntity(..)
, downcastEntity
, _SomeEntity
, entityIs
) where
--------------------------------------------------------------------------------
import Xanthous.Prelude
--------------------------------------------------------------------------------
import Data.List.NonEmpty ( NonEmpty((:|)))
import qualified Data.List.NonEmpty as NonEmpty
import Data.Typeable
import Data.Coerce
import System.Random
import Test.QuickCheck
import Test.QuickCheck.Arbitrary.Generic
import Control.Monad.State.Class
import Control.Monad.State
import Control.Monad.Random.Class
import Brick (EventM, Widget)
--------------------------------------------------------------------------------
import Xanthous.Data.EntityMap (EntityMap, EntityID)
import Xanthous.Data (Positioned(..), Position(..), Neighbors)
import Xanthous.Orphans ()
import Xanthous.Game.Prompt
import Xanthous.Resource
--------------------------------------------------------------------------------
data MessageHistory
= NoMessageHistory
| MessageHistory (NonEmpty Text) Bool
deriving stock (Show, Eq, Generic)
deriving anyclass (NFData, CoArbitrary, Function)
instance Arbitrary MessageHistory where
arbitrary = genericArbitrary
pushMessage :: Text -> MessageHistory -> MessageHistory
pushMessage msg NoMessageHistory = MessageHistory (msg :| []) True
pushMessage msg (MessageHistory msgs _) = MessageHistory (NonEmpty.cons msg msgs) True
popMessage :: MessageHistory -> MessageHistory
popMessage NoMessageHistory = NoMessageHistory
popMessage (MessageHistory msgs False) = MessageHistory msgs True
popMessage (MessageHistory msgs@(_ :| []) _) = MessageHistory msgs True
popMessage (MessageHistory (_ :| (msg : msgs)) True) = MessageHistory (msg :| msgs) True
hideMessage :: MessageHistory -> MessageHistory
hideMessage NoMessageHistory = NoMessageHistory
hideMessage (MessageHistory msgs _) = MessageHistory msgs False
--------------------------------------------------------------------------------
data GamePromptState m where
NoPrompt :: GamePromptState m
WaitingPrompt :: Text -> Prompt m -> GamePromptState m
deriving stock (Show)
--------------------------------------------------------------------------------
newtype AppT m a
= AppT { unAppT :: StateT GameState m a }
deriving ( Functor
, Applicative
, Monad
, MonadState GameState
)
via (StateT GameState m)
type AppM = AppT (EventM Name)
--------------------------------------------------------------------------------
class Draw a where
drawWithNeighbors :: Neighbors (Vector SomeEntity) -> a -> Widget n
drawWithNeighbors = const draw
draw :: a -> Widget n
draw = drawWithNeighbors $ pure mempty
instance Draw a => Draw (Positioned a) where
drawWithNeighbors ns (Positioned _ a) = drawWithNeighbors ns a
draw (Positioned _ a) = draw a
--------------------------------------------------------------------------------
class Brain a where
step :: Positioned a -> AppM (Positioned a)
newtype Brainless a = Brainless a
instance Brain (Brainless a) where
step = pure
-- | Workaround for the inability to use DerivingVia on Brain due to the lack of
-- higher-order roles (specifically AppT not having its last type argument have
-- role representational bc of StateT)
brainVia
:: forall brain entity. (Coercible entity brain, Brain brain)
=> (entity -> brain) -- ^ constructor, ignored
-> (Positioned entity -> AppM (Positioned entity))
brainVia _ = fmap coerce . step . coerce @_ @(Positioned brain)
--------------------------------------------------------------------------------
class (Show a, Eq a, Draw a, Brain a) => Entity a where
blocksVision :: a -> Bool
description :: a -> Text
data SomeEntity where
SomeEntity :: forall a. (Entity a, Typeable a) => a -> SomeEntity
instance Show SomeEntity where
show (SomeEntity e) = "SomeEntity (" <> show e <> ")"
instance Eq SomeEntity where
(SomeEntity (a :: ea)) == (SomeEntity (b :: eb)) = case eqT @ea @eb of
Just Refl -> a == b
_ -> False
instance Draw (SomeEntity) where
drawWithNeighbors ns (SomeEntity ent) = drawWithNeighbors ns ent
instance Brain SomeEntity where
step (Positioned pos (SomeEntity ent)) =
fmap SomeEntity <$> step (Positioned pos ent)
instance Entity SomeEntity where
blocksVision (SomeEntity ent) = blocksVision ent
description (SomeEntity ent) = description ent
downcastEntity :: forall a. (Entity a, Typeable a) => SomeEntity -> Maybe a
downcastEntity (SomeEntity e) = cast e
entityIs :: forall a. (Entity a, Typeable a) => SomeEntity -> Bool
entityIs = isJust . downcastEntity @a
_SomeEntity :: forall a. (Entity a, Typeable a) => Prism' SomeEntity a
_SomeEntity = prism' SomeEntity downcastEntity
--------------------------------------------------------------------------------
data GameState = GameState
{ _entities :: !(EntityMap SomeEntity)
, _revealedPositions :: !(Set Position)
, _characterEntityID :: !EntityID
, _messageHistory :: !MessageHistory
, _randomGen :: !StdGen
, _promptState :: !(GamePromptState AppM)
}
deriving stock (Show)
makeLenses ''GameState
instance Eq GameState where
(==) = (==) `on` \gs ->
( gs ^. entities
, gs ^. revealedPositions
, gs ^. characterEntityID
, gs ^. messageHistory
)
--------------------------------------------------------------------------------
instance MonadTrans AppT where
lift = AppT . lift
instance (Monad m) => MonadRandom (AppT m) where
getRandomR rng = randomGen %%= randomR rng
getRandom = randomGen %%= random
getRandomRs rng = uses randomGen $ randomRs rng
getRandoms = uses randomGen randoms