{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE RecordWildCards #-} -------------------------------------------------------------------------------- module Xanthous.Game ( GameState(..) , entities , revealedPositions , messageHistory , randomGen , promptState , GamePromptState(..) , getInitialState , positionedCharacter , character , characterPosition , updateCharacterVision , MessageHistory(..) , pushMessage , popMessage , hideMessage -- * collisions , Collision(..) , collisionAt -- * App monad , AppT(..) ) where -------------------------------------------------------------------------------- import Xanthous.Prelude -------------------------------------------------------------------------------- import Data.List.NonEmpty ( NonEmpty((:|))) import qualified Data.List.NonEmpty as NonEmpty 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 Xanthous.Data.EntityMap (EntityMap, EntityID) import qualified Xanthous.Data.EntityMap as EntityMap import Xanthous.Data.EntityMap.Graphics import Xanthous.Data (Positioned, Position(..), positioned, position) import Xanthous.Entities (SomeEntity(..), downcastEntity, entityIs, _SomeEntity) import Xanthous.Entities.Character import Xanthous.Entities.Creature import Xanthous.Entities.Item import Xanthous.Entities.Environment import Xanthous.Entities.Arbitrary () import Xanthous.Orphans () import Xanthous.Game.Prompt -------------------------------------------------------------------------------- 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) -------------------------------------------------------------------------------- data GameState = GameState { _entities :: !(EntityMap SomeEntity) , _revealedPositions :: !(Set Position) , _characterEntityID :: !EntityID , _messageHistory :: !MessageHistory , _randomGen :: !StdGen , _promptState :: !(GamePromptState (AppT Identity)) } deriving stock (Show) makeLenses ''GameState instance Eq GameState where (==) = (==) `on` \gs -> ( gs ^. entities , gs ^. revealedPositions , gs ^. characterEntityID , gs ^. messageHistory ) instance Arbitrary GameState where arbitrary = do char <- arbitrary @Character charPos <- arbitrary _messageHistory <- arbitrary (_characterEntityID, _entities) <- arbitrary <&> EntityMap.insertAtReturningID charPos (SomeEntity char) _revealedPositions <- fmap setFromList . sublistOf $ EntityMap.positions _entities _randomGen <- mkStdGen <$> arbitrary let _promptState = NoPrompt -- TODO pure $ GameState {..} getInitialState :: IO GameState getInitialState = do _randomGen <- getStdGen let char = mkCharacter (_characterEntityID, _entities) = EntityMap.insertAtReturningID (Position 0 0) (SomeEntity char) mempty _messageHistory = NoMessageHistory _revealedPositions = mempty _promptState = NoPrompt pure GameState {..} positionedCharacter :: Lens' GameState (Positioned Character) positionedCharacter = lens getPositionedCharacter setPositionedCharacter where setPositionedCharacter :: GameState -> Positioned Character -> GameState setPositionedCharacter game char = game & entities . at (game ^. characterEntityID) ?~ fmap SomeEntity char getPositionedCharacter :: GameState -> Positioned Character getPositionedCharacter game = over positioned ( fromMaybe (error "Invariant error: Character was not a character!") . downcastEntity ) . fromMaybe (error "Invariant error: Character not found!") $ EntityMap.lookupWithPosition (game ^. characterEntityID) (game ^. entities) character :: Lens' GameState Character character = positionedCharacter . positioned characterPosition :: Lens' GameState Position characterPosition = positionedCharacter . position visionRadius :: Word visionRadius = 12 -- TODO make this dynamic -- | Update the revealed entities at the character's position based on their vision updateCharacterVision :: GameState -> GameState updateCharacterVision game = let charPos = game ^. characterPosition visible = visiblePositions charPos visionRadius $ game ^. entities in game & revealedPositions <>~ visible -------------------------------------------------------------------------------- data Collision = Stop | Combat deriving stock (Show, Eq, Ord, Generic) deriving anyclass (NFData) collisionAt :: MonadState GameState m => Position -> m (Maybe Collision) collisionAt pos = do ents <- use $ entities . EntityMap.atPosition pos pure $ if | null ents -> Nothing | any (entityIs @Creature) ents -> pure Combat | all (entityIs @Item) ents -> Nothing | doors@(_ : _) <- ents ^.. folded . _SomeEntity @Door , all (view open) doors -> Nothing | otherwise -> pure Stop -------------------------------------------------------------------------------- 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