From 71b628c604556bc2d829f12980db99c9a526ec84 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Sat, 30 Nov 2019 19:55:43 -0500 Subject: Add messages on the ground Add support for a "GroundMessage" entity type, support for a Read command to read them, and randomly place an initial, tone-setting tutorial message on the ground near the character at the beginning of the game. --- src/Xanthous/App.hs | 29 +++++++++++++- src/Xanthous/Command.hs | 2 + src/Xanthous/Entities/Entities.hs | 2 + src/Xanthous/Entities/Environment.hs | 37 ++++++++++++++---- src/Xanthous/Game/Lenses.hs | 8 +++- src/Xanthous/Game/State.hs | 65 +++++++++++++++++++++++++++----- src/Xanthous/Generators.hs | 3 ++ src/Xanthous/Generators/LevelContents.hs | 29 ++++++++++++-- src/Xanthous/Messages.hs | 29 ++++++++------ src/Xanthous/Util.hs | 29 +++++++++++--- src/Xanthous/messages.yaml | 10 ++++- 11 files changed, 203 insertions(+), 40 deletions(-) (limited to 'src/Xanthous') diff --git a/src/Xanthous/App.hs b/src/Xanthous/App.hs index b8cda3b77721..df76eadc3bbc 100644 --- a/src/Xanthous/App.hs +++ b/src/Xanthous/App.hs @@ -44,7 +44,8 @@ import Xanthous.Entities.Item (Item) import qualified Xanthous.Entities.Item as Item import Xanthous.Entities.Creature (Creature) import qualified Xanthous.Entities.Creature as Creature -import Xanthous.Entities.Environment (Door, open, locked) +import Xanthous.Entities.Environment + (Door, open, locked, GroundMessage(..)) import Xanthous.Entities.RawTypes (edible, eatMessage, hitpointsHealed) import Xanthous.Generators import qualified Xanthous.Generators.CaveAutomata as CaveAutomata @@ -84,6 +85,7 @@ initLevel = do entities <>= (SomeEntity <$> level ^. levelWalls) entities <>= (SomeEntity <$> level ^. levelItems) entities <>= (SomeEntity <$> level ^. levelCreatures) + entities <>= (SomeEntity <$> level ^. levelTutorialMessage) characterPosition .= level ^. levelCharacterPosition @@ -206,6 +208,29 @@ handleCommand Eat = do stepGame -- TODO continue +handleCommand Read = do + -- TODO allow reading things in the inventory (combo direction+menu prompt?) + prompt_ @'DirectionPrompt ["read", "prompt"] Cancellable + $ \(DirectionResult dir) -> do + pos <- uses characterPosition $ move dir + uses entities + (fmap snd . entitiesAtPositionWithType @GroundMessage pos) >>= \case + Empty -> say_ ["read", "nothing"] + GroundMessage msg :< Empty -> + say ["read", "result"] $ object ["message" A..= msg] + msgs -> + let readAndContinue Empty = pure () + readAndContinue (msg :< msgs') = + prompt @'Continue + ["read", "result"] + (object ["message" A..= msg]) + Cancellable + . const + $ readAndContinue msgs' + readAndContinue _ = error "this is total" + in readAndContinue msgs + continue + handleCommand Save = do -- TODO default save locations / config file? prompt_ @'StringPrompt ["save", "location"] Cancellable @@ -413,3 +438,5 @@ entityMenu_ = mkMenuItems @[_] . map entityMenuItem -- entityMenu :: Entity entity => [entity] -> Map Char (MenuOption entity) -- entityMenu = map (map runIdentity) . entityMenu_ . fmap Identity + +-------------------------------------------------------------------------------- diff --git a/src/Xanthous/Command.hs b/src/Xanthous/Command.hs index 35a8ce367269..61fb11b22e77 100644 --- a/src/Xanthous/Command.hs +++ b/src/Xanthous/Command.hs @@ -19,6 +19,7 @@ data Command | Eat | Look | Save + | Read -- | TODO replace with `:` commands | ToggleRevealAll @@ -33,6 +34,7 @@ commandFromKey (KChar 'o') [] = Just Open commandFromKey (KChar ';') [] = Just Look commandFromKey (KChar 'e') [] = Just Eat commandFromKey (KChar 'S') [] = Just Save +commandFromKey (KChar 'r') [] = Just Read commandFromKey (KChar 'r') [MMeta] = Just ToggleRevealAll diff --git a/src/Xanthous/Entities/Entities.hs b/src/Xanthous/Entities/Entities.hs index 7e41fc8b7b3a..802aecddebdf 100644 --- a/src/Xanthous/Entities/Entities.hs +++ b/src/Xanthous/Entities/Entities.hs @@ -26,6 +26,7 @@ instance Arbitrary SomeEntity where , SomeEntity <$> arbitrary @Creature , SomeEntity <$> arbitrary @Wall , SomeEntity <$> arbitrary @Door + , SomeEntity <$> arbitrary @GroundMessage ] instance FromJSON SomeEntity where @@ -37,6 +38,7 @@ instance FromJSON SomeEntity where "Creature" -> SomeEntity @Creature <$> obj .: "data" "Wall" -> SomeEntity @Wall <$> obj .: "data" "Door" -> SomeEntity @Door <$> obj .: "data" + "GroundMessage" -> SomeEntity @GroundMessage <$> obj .: "data" _ -> fail . unpack $ "Invalid entity type \"" <> entityType <> "\"" deriving via WithOptions '[ FieldLabelModifier '[Drop 1] ] GameState diff --git a/src/Xanthous/Entities/Environment.hs b/src/Xanthous/Entities/Environment.hs index 8baa07650f7c..0690e47e5441 100644 --- a/src/Xanthous/Entities/Environment.hs +++ b/src/Xanthous/Entities/Environment.hs @@ -1,22 +1,29 @@ {-# LANGUAGE TemplateHaskell #-} module Xanthous.Entities.Environment - ( Wall(..) + ( + -- * Walls + Wall(..) + -- * Doors , Door(..) , open , locked + -- * Messages + , GroundMessage(..) ) where -------------------------------------------------------------------------------- import Xanthous.Prelude +-------------------------------------------------------------------------------- import Test.QuickCheck -import Test.QuickCheck.Arbitrary.Generic import Brick (str) import Brick.Widgets.Border.Style (unicode) import Brick.Types (Edges(..)) import Data.Aeson +import Data.Aeson.Generic.DerivingVia -------------------------------------------------------------------------------- import Xanthous.Entities.Draw.Util import Xanthous.Data import Xanthous.Game.State +import Xanthous.Util.QuickCheck -------------------------------------------------------------------------------- data Wall = Wall @@ -31,7 +38,6 @@ instance FromJSON Wall where "Wall" -> pure Wall _ -> fail "Invalid Wall: expected Wall" --- deriving via Brainless Wall instance Brain Wall instance Brain Wall where step = brainVia Brainless instance Entity Wall where @@ -56,11 +62,9 @@ data Door = Door } deriving stock (Show, Eq, Ord, Generic) deriving anyclass (NFData, CoArbitrary, Function, ToJSON, FromJSON) + deriving Arbitrary via GenericArbitrary Door makeLenses ''Door -instance Arbitrary Door where - arbitrary = genericArbitrary - instance Draw Door where drawWithNeighbors neighs door | door ^. open @@ -77,10 +81,29 @@ instance Draw Door where horizDoor = '␣' vertDoor = '[' --- deriving via Brainless Door instance Brain Door instance Brain Door where step = brainVia Brainless instance Entity Door where blocksVision = not . view open description _ = "a door" entityChar _ = "d" + +-------------------------------------------------------------------------------- + +newtype GroundMessage = GroundMessage Text + deriving stock (Show, Eq, Ord, Generic) + deriving anyclass (NFData, CoArbitrary, Function) + deriving Arbitrary via GenericArbitrary GroundMessage + deriving (ToJSON, FromJSON) + via WithOptions '[ 'TagSingleConstructors 'True + , 'SumEnc 'ObjWithSingleField + ] + GroundMessage + deriving Draw + via DrawStyledCharacter ('Just 'Yellow) 'Nothing "≈" + GroundMessage + deriving Entity + via DeriveEntity 'False "a message on the ground. Press r. to read it." + "≈" + GroundMessage +instance Brain GroundMessage where step = brainVia Brainless diff --git a/src/Xanthous/Game/Lenses.hs b/src/Xanthous/Game/Lenses.hs index 7dbd60290144..13f4b89314f7 100644 --- a/src/Xanthous/Game/Lenses.hs +++ b/src/Xanthous/Game/Lenses.hs @@ -25,7 +25,7 @@ import Xanthous.Data import qualified Xanthous.Data.EntityMap as EntityMap import Xanthous.Data.EntityMap.Graphics (visiblePositions) import Xanthous.Entities.Character (Character, mkCharacter) -import Xanthous.Entities.Environment (Door, open) +import Xanthous.Entities.Environment (Door, open, GroundMessage) import Xanthous.Entities.Item (Item) import Xanthous.Entities.Creature (Creature) import Xanthous.Entities.Entities () @@ -105,8 +105,12 @@ entityCollision -> Maybe Collision entityCollision Empty = Nothing entityCollision ents + -- TODO track entity collision in the Entity class | any (entityIs @Creature) ents = pure Combat - | all (entityIs @Item) ents = Nothing + | all (\e -> + entityIs @Item e + || entityIs @GroundMessage e + ) ents = Nothing | doors@(_ : _) <- ents ^.. folded . _SomeEntity @Door , all (view open) doors = Nothing | otherwise = pure Stop diff --git a/src/Xanthous/Game/State.hs b/src/Xanthous/Game/State.hs index 028688542a25..5ddb7de7e9b8 100644 --- a/src/Xanthous/Game/State.hs +++ b/src/Xanthous/Game/State.hs @@ -1,8 +1,8 @@ -{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE UndecidableInstances #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE AllowAmbiguousTypes #-} -------------------------------------------------------------------------------- module Xanthous.Game.State ( GameState(..) @@ -37,10 +37,14 @@ module Xanthous.Game.State , downcastEntity , _SomeEntity , entityIs + -- ** Vias + , Color(..) + , DrawNothing(..) , DrawRawChar(..) , DrawRawCharPriority(..) , DrawCharacter(..) , DrawStyledCharacter(..) + , DeriveEntity(..) -- ** Field classes , HasChar(..) , HasStyle(..) @@ -63,7 +67,7 @@ import Test.QuickCheck.Arbitrary.Generic import Control.Monad.State.Class import Control.Monad.State import Control.Monad.Random.Class -import Brick (EventM, Widget, raw, str) +import Brick (EventM, Widget, raw, str, emptyWidget) import Data.Aeson (ToJSON(..), FromJSON(..), Value(Null)) import qualified Data.Aeson as JSON import Data.Aeson.Generic.DerivingVia @@ -71,6 +75,7 @@ import Data.Generics.Product.Fields import qualified Graphics.Vty.Attributes as Vty import qualified Graphics.Vty.Image as Vty -------------------------------------------------------------------------------- +import Xanthous.Util (KnownBool(..)) import Xanthous.Data import Xanthous.Data.EntityMap (EntityMap, EntityID) import Xanthous.Data.EntityChar @@ -213,20 +218,29 @@ instance KnownColor 'Magenta where colorVal _ = Vty.magenta instance KnownColor 'Cyan where colorVal _ = Vty.cyan instance KnownColor 'White where colorVal _ = Vty.white -newtype DrawStyledCharacter (fg :: Color) (bg :: Color) (char :: Symbol) (a :: Type) where +class KnownMaybeColor (maybeColor :: Maybe Color) where + maybeColorVal :: forall proxy. proxy maybeColor -> Maybe Vty.Color + +instance KnownMaybeColor 'Nothing where maybeColorVal _ = Nothing +instance KnownColor color => KnownMaybeColor ('Just color) where + maybeColorVal _ = Just $ colorVal @color Proxy + +newtype DrawStyledCharacter (fg :: Maybe Color) (bg :: Maybe Color) (char :: Symbol) (a :: Type) where DrawStyledCharacter :: a -> DrawStyledCharacter fg bg char a instance - ( KnownColor fg - , KnownColor bg + ( KnownMaybeColor fg + , KnownMaybeColor bg , KnownSymbol char ) => Draw (DrawStyledCharacter fg bg char a) where draw _ = raw $ Vty.string attr $ symbolVal @char Proxy where attr = Vty.Attr { Vty.attrStyle = Vty.Default - , Vty.attrForeColor = Vty.SetTo $ colorVal @fg Proxy - , Vty.attrBackColor = Vty.SetTo $ colorVal @bg Proxy + , Vty.attrForeColor = maybe Vty.Default Vty.SetTo + $ maybeColorVal @fg Proxy + , Vty.attrBackColor = maybe Vty.Default Vty.SetTo + $ maybeColorVal @bg Proxy , Vty.attrURL = Vty.Default } @@ -235,6 +249,12 @@ instance Draw EntityChar where -------------------------------------------------------------------------------- +newtype DrawNothing (a :: Type) = DrawNothing a + +instance Draw (DrawNothing a) where + draw = const emptyWidget + drawPriority = const 0 + newtype DrawRawChar (rawField :: Symbol) (a :: Type) = DrawRawChar a instance @@ -336,6 +356,31 @@ entityIs = isJust . downcastEntity @a _SomeEntity :: forall a. (Entity a, Typeable a) => Prism' SomeEntity a _SomeEntity = prism' SomeEntity downcastEntity +newtype DeriveEntity + (blocksVision :: Bool) + (description :: Symbol) + (entityChar :: Symbol) + (entity :: Type) + = DeriveEntity entity + deriving newtype (Show, Eq, Ord, NFData, ToJSON, FromJSON, Draw) + +instance Brain entity => Brain (DeriveEntity b d c entity) where + step = brainVia $ \(DeriveEntity e) -> e + +instance + ( KnownBool blocksVision + , KnownSymbol description + , KnownSymbol entityChar + , Show entity, Eq entity, Ord entity, NFData entity + , ToJSON entity, FromJSON entity + , Draw entity, Brain entity + ) + => Entity (DeriveEntity blocksVision description entityChar entity) where + + blocksVision _ = boolVal @blocksVision + description _ = pack . symbolVal $ Proxy @description + entityChar _ = fromString . symbolVal $ Proxy @entityChar + -------------------------------------------------------------------------------- data DebugState = DebugState diff --git a/src/Xanthous/Generators.hs b/src/Xanthous/Generators.hs index 6b1a57299ea1..490e50ea60a8 100644 --- a/src/Xanthous/Generators.hs +++ b/src/Xanthous/Generators.hs @@ -14,6 +14,7 @@ module Xanthous.Generators , levelItems , levelCreatures , levelCharacterPosition + , levelTutorialMessage , generateLevel ) where -------------------------------------------------------------------------------- @@ -91,6 +92,7 @@ data Level = Level { _levelWalls :: !(EntityMap Wall) , _levelItems :: !(EntityMap Item) , _levelCreatures :: !(EntityMap Creature) + , _levelTutorialMessage :: !(EntityMap GroundMessage) , _levelCharacterPosition :: !Position } makeLenses ''Level @@ -103,4 +105,5 @@ generateLevel gen ps dims = do _levelItems <- randomItems cells _levelCreatures <- randomCreatures cells _levelCharacterPosition <- chooseCharacterPosition cells + _levelTutorialMessage <- tutorialMessage cells _levelCharacterPosition pure Level {..} diff --git a/src/Xanthous/Generators/LevelContents.hs b/src/Xanthous/Generators/LevelContents.hs index 583bdcbd6729..91a7d38019c8 100644 --- a/src/Xanthous/Generators/LevelContents.hs +++ b/src/Xanthous/Generators/LevelContents.hs @@ -3,22 +3,26 @@ module Xanthous.Generators.LevelContents ( chooseCharacterPosition , randomItems , randomCreatures + , tutorialMessage ) where -------------------------------------------------------------------------------- import Xanthous.Prelude -------------------------------------------------------------------------------- import Control.Monad.Random -import Data.Array.IArray (amap, bounds, rangeSize) +import Data.Array.IArray (amap, bounds, rangeSize, (!)) -------------------------------------------------------------------------------- import Xanthous.Generators.Util import Xanthous.Random -import Xanthous.Data (Position, positionFromPair) +import Xanthous.Data (Position, _Position, positionFromPair) import Xanthous.Data.EntityMap (EntityMap, _EntityMap) import Xanthous.Entities.Raws (rawsWithType, RawType) import qualified Xanthous.Entities.Item as Item import Xanthous.Entities.Item (Item) import qualified Xanthous.Entities.Creature as Creature import Xanthous.Entities.Creature (Creature) +import Xanthous.Entities.Environment (GroundMessage(..)) +import Xanthous.Messages (message_) +import Xanthous.Util.Graphics (circle) -------------------------------------------------------------------------------- chooseCharacterPosition :: MonadRandom m => Cells -> m Position @@ -30,6 +34,24 @@ randomItems = randomEntities Item.newWithType (0.0004, 0.001) randomCreatures :: MonadRandom m => Cells -> m (EntityMap Creature) randomCreatures = randomEntities Creature.newWithType (0.0007, 0.003) +tutorialMessage :: MonadRandom m + => Cells + -> Position -- ^ CharacterPosition + -> m (EntityMap GroundMessage) +tutorialMessage cells characterPosition = do + let distance = 2 + pos <- fmap (fromMaybe (error "No valid positions for tutorial message?")) + . choose . ChooseElement + $ accessiblePositionsWithin distance cells characterPosition + msg <- message_ ["tutorial", "message1"] + pure $ _EntityMap # [(pos, GroundMessage msg)] + where + accessiblePositionsWithin :: Int -> Cells -> Position -> [Position] + accessiblePositionsWithin dist valid pos = + review _Position + <$> filter (\(px, py) -> not $ valid ! (fromIntegral px, fromIntegral py)) + (circle (pos ^. _Position) dist) + randomEntities :: forall entity raw m. (MonadRandom m, RawType raw) => (raw -> entity) @@ -41,7 +63,8 @@ randomEntities newWithType sizeRange cells = Nothing -> pure mempty Just raws -> do let len = rangeSize $ bounds cells - (numEntities :: Int) <- floor . (* fromIntegral len) <$> getRandomR sizeRange + (numEntities :: Int) <- + floor . (* fromIntegral len) <$> getRandomR sizeRange entities <- for [0..numEntities] $ const $ do pos <- randomPosition cells raw <- choose raws diff --git a/src/Xanthous/Messages.hs b/src/Xanthous/Messages.hs index b0dc0e4ae9d2..2b1b3da1e8c1 100644 --- a/src/Xanthous/Messages.hs +++ b/src/Xanthous/Messages.hs @@ -11,23 +11,25 @@ module Xanthous.Messages , render , lookup , message + , message_ ) where -------------------------------------------------------------------------------- import Xanthous.Prelude hiding (lookup) -------------------------------------------------------------------------------- -import Control.Monad.Random.Class (MonadRandom) -import Data.Aeson (FromJSON, ToJSON, toJSON) -import Data.Aeson.Generic.DerivingVia -import Data.FileEmbed -import Data.List.NonEmpty -import Test.QuickCheck hiding (choose) -import Test.QuickCheck.Arbitrary.Generic -import Test.QuickCheck.Instances.UnorderedContainers () -import Text.Mustache +import Control.Monad.Random.Class (MonadRandom) +import Data.Aeson (FromJSON, ToJSON, toJSON) +import qualified Data.Aeson as JSON +import Data.Aeson.Generic.DerivingVia +import Data.FileEmbed +import Data.List.NonEmpty +import Test.QuickCheck hiding (choose) +import Test.QuickCheck.Arbitrary.Generic +import Test.QuickCheck.Instances.UnorderedContainers () +import Text.Mustache import qualified Data.Yaml as Yaml -------------------------------------------------------------------------------- -import Xanthous.Random -import Xanthous.Orphans () +import Xanthous.Random +import Xanthous.Orphans () -------------------------------------------------------------------------------- data Message = Single Template | Choice (NonEmpty Template) @@ -98,3 +100,8 @@ message :: (MonadRandom m, ToJSON params) => [Text] -> params -> m Text message path params = maybe notFound (`render` params) $ messages ^? ix path where notFound = pure "Message not found" + +message_ :: (MonadRandom m) => [Text] -> m Text +message_ path = maybe notFound (`render` JSON.object []) $ messages ^? ix path + where + notFound = pure "Message not found" diff --git a/src/Xanthous/Util.hs b/src/Xanthous/Util.hs index 3a7c10ace18e..814f9371150f 100644 --- a/src/Xanthous/Util.hs +++ b/src/Xanthous/Util.hs @@ -1,7 +1,7 @@ -{-# LANGUAGE BangPatterns #-} -{-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE QuantifiedConstraints #-} - +-------------------------------------------------------------------------------- module Xanthous.Util ( EqEqProp(..) , EqProp(..) @@ -25,13 +25,18 @@ module Xanthous.Util -- ** Bag sequence algorithms , takeWhileInclusive , smallestNotIn - ) where + -- * Type-level programming utils + , KnownBool(..) + ) where +-------------------------------------------------------------------------------- import Xanthous.Prelude hiding (foldr) - +-------------------------------------------------------------------------------- import Test.QuickCheck.Checkers import Data.Foldable (foldr) import Data.Monoid +import Data.Proxy +-------------------------------------------------------------------------------- newtype EqEqProp a = EqEqProp a deriving newtype Eq @@ -204,3 +209,17 @@ smallestNotIn xs = case uniq $ sort xs of | x > minBound -> minBound | otherwise -> snd . headEx . filter (uncurry (/=)) $ zip (xs' ++ [minBound]) [minBound..] + +-------------------------------------------------------------------------------- + +-- | This class gives a boolean associated with a type-level bool, a'la +-- 'KnownSymbol', 'KnownNat' etc. +class KnownBool (bool :: Bool) where + boolVal' :: forall proxy. proxy bool -> Bool + boolVal' _ = boolVal @bool + + boolVal :: Bool + boolVal = boolVal' $ Proxy @bool + +instance KnownBool 'True where boolVal = True +instance KnownBool 'False where boolVal = False diff --git a/src/Xanthous/messages.yaml b/src/Xanthous/messages.yaml index ae9ca060bfef..1c5692ddfec0 100644 --- a/src/Xanthous/messages.yaml +++ b/src/Xanthous/messages.yaml @@ -1,4 +1,4 @@ -welcome: Welcome to Xanthous, {{characterName}}! It's dangerous out there, why not stay inside? +welcome: Welcome to Xanthous, {{characterName}}! It's dangerous out there, why not stay inside? Use hjklybnu to move. dead: - You have died... - You die... @@ -54,3 +54,11 @@ eat: - You search your pockets for something edible, and come up short. menuPrompt: What would you like to eat? eat: You eat the {{item.itemType.name}}. + +read: + prompt: Direction to read (hjklybnu.)? + nothing: "There's nothing there to read" + result: "\"{{message}}\"" + +tutorial: + message1: The caves are dark and full of nightmarish creatures - and you are likely to perish without food. Seek out sustenance, and pick it up with , -- cgit 1.4.1