{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE AllowAmbiguousTypes #-} -------------------------------------------------------------------------------- module Xanthous.Game.State ( GameState(..) , entities , revealedPositions , messageHistory , randomGen , promptState , characterEntityID , GamePromptState(..) -- * Messages , MessageHistory(..) , HasMessages(..) , HasTurn(..) , HasDisplayedTurn(..) , pushMessage , previousMessage , nextTurn -- * App monad , AppT(..) , AppM -- * Entities , Draw(..) , Brain(..) , Brainless(..) , brainVia , Entity(..) , SomeEntity(..) , downcastEntity , _SomeEntity , entityIs -- * Debug State , DebugState(..) , debugState , allRevealed ) 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 = MessageHistory { _messages :: Map Word (NonEmpty Text) , _turn :: Word , _displayedTurn :: Maybe Word } deriving stock (Show, Eq, Generic) deriving anyclass (NFData, CoArbitrary, Function) makeFieldsNoPrefix ''MessageHistory instance Semigroup MessageHistory where (MessageHistory msgs₁ turn₁ dt₁) <> (MessageHistory msgs₂ turn₂ dt₂) = MessageHistory (msgs₁ <> msgs₂) (max turn₁ turn₂) $ case (dt₁, dt₂) of (_, Nothing) -> Nothing (Just t, _) -> Just t (Nothing, Just t) -> Just t instance Monoid MessageHistory where mempty = MessageHistory mempty 0 Nothing instance Arbitrary MessageHistory where arbitrary = genericArbitrary type instance Element MessageHistory = [Text] instance MonoFunctor MessageHistory where omap f mh@(MessageHistory _ t _) = mh & messages . at t %~ (NonEmpty.nonEmpty . f . toList =<<) instance MonoComonad MessageHistory where oextract (MessageHistory ms t dt) = maybe [] toList $ ms ^. at (fromMaybe t dt) oextend cok mh@(MessageHistory _ t dt) = mh & messages . at (fromMaybe t dt) .~ NonEmpty.nonEmpty (cok mh) pushMessage :: Text -> MessageHistory -> MessageHistory pushMessage msg mh@(MessageHistory _ turn' _) = mh & messages . at turn' %~ \case Nothing -> Just $ msg :| mempty Just msgs -> Just $ msg <| msgs & displayedTurn .~ Nothing nextTurn :: MessageHistory -> MessageHistory nextTurn = (turn +~ 1) . (displayedTurn .~ Nothing) previousMessage :: MessageHistory -> MessageHistory previousMessage mh = mh & displayedTurn .~ maximumOf (messages . ifolded . asIndex . filtered (< mh ^. turn)) mh -------------------------------------------------------------------------------- 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 :: Type). (Typeable a) => SomeEntity -> Maybe a downcastEntity (SomeEntity e) = cast e entityIs :: forall (a :: Type). (Typeable a) => SomeEntity -> Bool entityIs = isJust . downcastEntity @a _SomeEntity :: forall a. (Entity a, Typeable a) => Prism' SomeEntity a _SomeEntity = prism' SomeEntity downcastEntity -------------------------------------------------------------------------------- data DebugState = DebugState { _allRevealed :: !Bool } deriving stock (Show, Eq, Generic) deriving anyclass (NFData, CoArbitrary, Function) instance Arbitrary DebugState where arbitrary = genericArbitrary data GameState = GameState { _entities :: !(EntityMap SomeEntity) , _revealedPositions :: !(Set Position) , _characterEntityID :: !EntityID , _messageHistory :: !MessageHistory , _randomGen :: !StdGen , _promptState :: !(GamePromptState AppM) , _debugState :: DebugState } 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 -------------------------------------------------------------------------------- makeLenses ''DebugState