LambdaHack 0.2.6 → 0.2.6.5
raw patch · 69 files changed
+1442/−1279 lines, 69 filesdep +miniutterdep +textdep ~base
Dependencies added: miniutter, text
Dependency ranges changed: base
Files
- Game/LambdaHack/Action.hs +57/−57
- Game/LambdaHack/Action/ActionLift.hs +14/−14
- Game/LambdaHack/Action/ConfigIO.hs +138/−47
- Game/LambdaHack/Action/Frontend/Chosen.hs +4/−2
- Game/LambdaHack/Action/Frontend/Curses.hs +3/−2
- Game/LambdaHack/Action/Frontend/Gtk.hs +5/−2
- Game/LambdaHack/Action/Frontend/Std.hs +2/−1
- Game/LambdaHack/Action/Frontend/Vty.hs +3/−3
- Game/LambdaHack/Action/HighScore.hs +44/−43
- Game/LambdaHack/Action/Save.hs +55/−76
- Game/LambdaHack/Actions.hs +48/−35
- Game/LambdaHack/Actor.hs +16/−8
- Game/LambdaHack/ActorState.hs +24/−17
- Game/LambdaHack/Animation.hs +3/−2
- Game/LambdaHack/Binding.hs +11/−23
- Game/LambdaHack/BindingAction.hs +7/−6
- Game/LambdaHack/CDefs.hs +3/−1
- Game/LambdaHack/Cave.hs +5/−3
- Game/LambdaHack/Command.hs +17/−13
- Game/LambdaHack/CommandAction.hs +9/−14
- Game/LambdaHack/Config.hs +113/−47
- Game/LambdaHack/Content/ActorKind.hs +2/−1
- Game/LambdaHack/Content/CaveKind.hs +10/−8
- Game/LambdaHack/Content/FactionKind.hs +6/−5
- Game/LambdaHack/Content/ItemKind.hs +15/−11
- Game/LambdaHack/Content/PlaceKind.hs +9/−4
- Game/LambdaHack/Content/RuleKind.hs +13/−11
- Game/LambdaHack/Content/StrategyKind.hs +3/−1
- Game/LambdaHack/Content/TileKind.hs +2/−1
- Game/LambdaHack/Draw.hs +20/−17
- Game/LambdaHack/DungeonState.hs +17/−15
- Game/LambdaHack/Effect.hs +6/−2
- Game/LambdaHack/EffectAction.hs +65/−45
- Game/LambdaHack/FOV.hs +2/−11
- Game/LambdaHack/Feature.hs +3/−1
- Game/LambdaHack/Flavour.hs +5/−3
- Game/LambdaHack/Grammar.hs +0/−258
- Game/LambdaHack/Item.hs +14/−10
- Game/LambdaHack/ItemAction.hs +68/−50
- Game/LambdaHack/Key.hs +54/−9
- Game/LambdaHack/Kind.hs +29/−17
- Game/LambdaHack/Level.hs +3/−2
- Game/LambdaHack/Misc.hs +3/−2
- Game/LambdaHack/Msg.hs +52/−37
- Game/LambdaHack/Perception.hs +8/−17
- Game/LambdaHack/Place.hs +9/−5
- Game/LambdaHack/Point.hs +4/−2
- Game/LambdaHack/State.hs +72/−6
- Game/LambdaHack/Strategy.hs +6/−3
- Game/LambdaHack/StrategyAction.hs +7/−3
- Game/LambdaHack/Turn.hs +9/−8
- Game/LambdaHack/Utils/Assert.hs +2/−2
- Game/LambdaHack/Utils/Frequency.hs +16/−12
- LambdaHack.cabal +187/−162
- LambdaHack/Content/ActorKind.hs +1/−0
- LambdaHack/Content/CaveKind.hs +1/−0
- LambdaHack/Content/FactionKind.hs +1/−0
- LambdaHack/Content/ItemKind.hs +5/−4
- LambdaHack/Content/PlaceKind.hs +1/−0
- LambdaHack/Content/RuleKind.hs +15/−11
- LambdaHack/Content/StrategyKind.hs +1/−0
- LambdaHack/Content/TileKind.hs +1/−0
- PLAYING.md +1/−1
- README.md +2/−2
- config.bot +0/−9
- config.default +0/−95
- config.rules.bot +9/−0
- config.rules.default +30/−0
- config.ui.default +72/−0
Game/LambdaHack/Action.hs view
@@ -1,9 +1,10 @@+{-# LANGUAGE OverloadedStrings #-} -- | Game action monad and basic building blocks for player and monster -- actions. Uses @liftIO@ of the @Action@ monad, but does not export it. -- Has no direct access to the Action monad implementation. module Game.LambdaHack.Action ( -- * Actions and accessors- Action, getPerception, getCOps, getBinding+ Action, getPerception, getCOps, getBinding, getConfigUI -- * Actions returning frames , ActionFrame, returnNoFrame, returnFrame, whenFrame, inFrame, tryWithFrame -- * Various ways to abort action@@ -34,6 +35,8 @@ import Data.Maybe import Control.Concurrent import Control.Exception (finally)+import Data.Text (Text)+import qualified Data.Text as T -- import System.IO (hPutStrLn, stderr) -- just for debugging import Game.LambdaHack.Action.ActionLift@@ -50,7 +53,7 @@ import qualified Game.LambdaHack.Key as K import Game.LambdaHack.Binding import Game.LambdaHack.Action.HighScore (register)-import qualified Game.LambdaHack.Config as Config+import Game.LambdaHack.Config import qualified Game.LambdaHack.Action.ConfigIO as ConfigIO import Game.LambdaHack.Animation (SingleFrame(..)) import Game.LambdaHack.Point@@ -79,10 +82,9 @@ recordHistory = do Diary{sreport, shistory} <- getDiary unless (nullReport sreport) $ do- config <- gets sconfig- let historyMax = Config.get config "ui" "historyMax"+ ConfigUI{configHistoryMax} <- getConfigUI msgReset ""- historyReset $ takeHistory historyMax $ addReport sreport shistory+ historyReset $ takeHistory configHistoryMax $ addReport sreport shistory -- | Wait for a player command. getKeyCommand :: Maybe Bool -> Action (K.Key, K.Modifier)@@ -138,8 +140,7 @@ -- TODO: perhaps add prompt to Report instead? promptAdd :: Msg -> Msg -> Msg-promptAdd "" msg = msg-promptAdd prompt msg = prompt ++ " " ++ msg+promptAdd prompt msg = prompt <+> msg -- | Display a msg with a @more@ prompt. Return value indicates if the player -- tried to cancel/escape.@@ -192,7 +193,7 @@ [x] -> (x, [], "", [], keys) x:xs -> (x, xs, ", SPACE", [moreMsg], (K.Space, K.NoModifier) : keys) legalKeys = (K.Esc, K.NoModifier) : keysS- frame <- drawOverlay ColorFull (prompt ++ spc ++ ", ESC]") (over ++ more)+ frame <- drawOverlay ColorFull (prompt <> spc <> ", ESC]") (over ++ more) fs <- getFrontendSession (key, modifier) <- liftIO $ promptGetKey fs legalKeys frame case key of@@ -289,13 +290,14 @@ saveGameBkp = do state <- get diary <- getDiary- liftIO $ Save.saveGameBkp state diary+ configUI <- getConfigUI+ liftIO $ Save.saveGameBkp configUI state diary --- | Dumps the current configuration to a file.------ See 'Config.dump'.-dumpCfg :: FilePath -> Config.CP -> Action ()-dumpCfg fn config = liftIO $ ConfigIO.dump fn config+-- | Dumps the current game rules configuration to a file.+dumpCfg :: FilePath -> Action ()+dumpCfg fn = do+ config <- gets sconfig+ liftIO $ ConfigIO.dump config fn -- | Handle current score and display it with the high scores. -- Aborts if display of the scores was interrupted by the user.@@ -306,10 +308,10 @@ handleScores :: Bool -> Status -> Int -> Action () handleScores write status total = when (total /= 0) $ do- config <- gets sconfig- time <- gets stime+ configUI <- getConfigUI+ time <- gets stime curDate <- liftIO getClockTime- let score = register config write total time curDate status+ let score = register configUI write total time curDate status (placeMsg, slideshow) <- liftIO score displayOverAbort placeMsg slideshow @@ -319,6 +321,7 @@ squit <- gets squit Kind.COps{coitem} <- getCOps s <- get+ configUI <- getConfigUI let (_, total) = calculateTotal coitem s -- The first, boolean component of squit determines -- if ending screens should be shown, the other argument describes@@ -328,7 +331,8 @@ Just (_, status@Camping) -> do -- Save and display in parallel. mv <- liftIO newEmptyMVar- liftIO $ void $ forkIO (Save.saveGameFile s `finally` putMVar mv ())+ liftIO $ void $ forkIO (Save.saveGameFile configUI s+ `finally` putMVar mv ()) tryIgnore $ do handleScores False status total void $ displayMore ColorFull "See you soon, stronger and braver!"@@ -343,7 +347,7 @@ tryWith (\ finalMsg -> let highScoreMsg = "Let's hope another party can save the day!"- msg = if null finalMsg then highScoreMsg else finalMsg+ msg = if T.null finalMsg then highScoreMsg else finalMsg in void $ displayMore ColorBW msg -- Do nothing, that is, quit the game loop. )@@ -371,33 +375,31 @@ restartGame handleTurn = do -- Take the original config from config file, to reroll RNG, if needed -- (the current config file has the RNG rolled for the previous game).- config <- getOrigConfig+ configUI <- getConfigUI cops <- getCOps- state <- gameResetAction config cops+ state <- gameResetAction configUI cops modify $ const state saveGameBkp handleTurn -- TODO: do this inside Action ()-gameReset :: Config.CP -> Kind.COps -> IO State-gameReset config1 cops@Kind.COps{ coitem- , cofact=Kind.Ops{opick}} = do- (g2, config2) <- ConfigIO.getSetGen config1 "dungeonRandomGenerator"- let (DungeonState.FreshDungeon{..}, ag) =- runState (DungeonState.generate cops config2) g2- (sflavour, ag2) = runState (dungeonFlavourMap coitem) ag- factionName = Config.getOption config2 "heroes" "faction"- sfaction =- evalState- (opick (fromMaybe "playable" factionName) (const True)) ag2- (g3, config3) <- ConfigIO.getSetGen config2 "startingRandomGenerator"- let state =- defaultState- config3 sfaction sflavour freshDungeon entryLevel entryLoc g3- hstate = initialHeroes cops entryLoc state+gameReset :: ConfigUI -> Kind.COps -> IO State+gameReset configUI cops@Kind.COps{ coitem+ , corule+ , cofact=Kind.Ops{opick}} = do+ -- Rules config reloaded at each new game start.+ (configRules, dungeonGen, startingGen) <- ConfigIO.mkConfigRules corule+ let (DungeonState.FreshDungeon{..}, gen2) =+ runState (DungeonState.generate cops configRules) dungeonGen+ (sflavour, gen3) = runState (dungeonFlavourMap coitem) gen2+ factionName = configFaction configRules+ sfaction = evalState (opick factionName (const True)) gen3+ let state = defaultState configRules sfaction sflavour freshDungeon+ entryLevel entryLoc startingGen+ hstate = initialHeroes cops entryLoc configUI state return hstate-gameResetAction :: Config.CP -> Kind.COps -> Action State-gameResetAction config cops = liftIO $ gameReset config cops+gameResetAction :: ConfigUI -> Kind.COps -> Action State+gameResetAction configUI cops = liftIO $ gameReset configUI cops -- | Wire together content, the definitions of game commands, -- config and a high-level startup function@@ -405,25 +407,22 @@ -- in particular verify content consistency. -- Then create the starting game config from the default config file -- and initialize the engine with the starting session.-startFrontend :: Kind.COps -> (Config.CP -> Binding (ActionFrame ()))+startFrontend :: Kind.COps -> (ConfigUI -> Binding (ActionFrame ())) -> Action () -> IO () startFrontend !scops@Kind.COps{corule} stdBinding handleTurn = do- let configDefault = rconfigDefault $ Kind.stdRuleset corule- sconfig <- ConfigIO.mkConfig configDefault- let !sbinding = stdBinding sconfig- !sorigConfig = sconfig- -- The only option taken not from config in savegame,- -- but from current config file, possibly from user directory.- configFont = fromMaybe "" $ Config.getOption sconfig "ui" "font"+ -- UI config reloaded at each client start.+ sconfigUI <- ConfigIO.mkConfigUI corule+ let !sbinding = stdBinding sconfigUI+ font = configFont sconfigUI -- In addition to handling the turn, if the game ends or exits, -- handle the diary and backup savefile. handleGame = do handleTurn diary <- getDiary -- Save diary often, at each game exit, in case of crashes.- liftIO $ Save.rmBkpSaveDiary sconfig diary- loop sfs = start sconfig Session{..} handleGame- startup configFont loop+ liftIO $ Save.rmBkpSaveDiary sconfigUI diary+ loop sfs = start Session{..} handleGame+ startup font loop -- | Compute and insert auxiliary optimized components into game content, -- to be used in time-critical sections of the code.@@ -436,25 +435,26 @@ -- | Either restore a saved game, or setup a new game. -- Then call the main game loop.-start :: Config.CP -> Session -> Action () -> IO ()-start config slowSess handleGame = do- let sess@Session{scops = cops@Kind.COps{ corule }} = speedupCops slowSess+start :: Session -> Action () -> IO ()+start slowSess handleGame = do+ let sess@Session{scops = cops@Kind.COps{corule}, sconfigUI} =+ speedupCops slowSess title = rtitle $ Kind.stdRuleset corule pathsDataFile = rpathsDataFile $ Kind.stdRuleset corule- restored <- Save.restoreGame pathsDataFile config title+ restored <- Save.restoreGame sconfigUI pathsDataFile title case restored of Right (diary, msg) -> do -- Starting a new game.- state <- gameReset config cops+ state <- gameReset sconfigUI cops handlerToIO sess state diary{sreport = singletonReport msg} -- TODO: gameReset >> handleTurn or defaultState {squit=Reset} handleGame- Left (state, diary, msg) -> -- Running a restored a game.+ Left (state, diary, msg) -> -- Running a restored game. handlerToIO sess state -- This overwrites the "Really save/quit?" messages. diary{sreport = singletonReport msg} handleGame -- | Debugging.-debug :: String -> Action ()+debug :: Text -> Action () debug _x = return () -- liftIO $ hPutStrLn stderr _x
Game/LambdaHack/Action/ActionLift.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | Game action monad and basic building blocks for player and monster -- actions. Exports 'liftIO' for injecting 'IO' into the 'Action' monad, -- but does not export the implementation of the Action monad.@@ -9,7 +10,7 @@ -- * Actions returning frames , ActionFrame, returnNoFrame, returnFrame, whenFrame, inFrame -- * Game session and assessors to its components- , Session(..), getFrontendSession, getCOps, getBinding, getOrigConfig+ , Session(..), getFrontendSession, getCOps, getBinding, getConfigUI -- * Various ways to abort action , abort, abortWith, abortIfWith, neverMind -- * Abort exception handlers@@ -21,6 +22,7 @@ import Control.Monad.State hiding (State, state, liftIO) import qualified Data.List as L import Data.Maybe+import qualified Data.Text as T import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Perception@@ -29,7 +31,7 @@ import Game.LambdaHack.State import qualified Game.LambdaHack.Kind as Kind import Game.LambdaHack.Binding-import qualified Game.LambdaHack.Config as Config+import Game.LambdaHack.Config import Game.LambdaHack.Animation (SingleFrame(..)) -- | The type of the function inside any action.@@ -91,8 +93,8 @@ sess (dungeonPerception scops state) -- create and cache perception (\ _ _ x -> return x) -- final continuation returns result- (\ msg ->- ioError $ userError $ "unhandled abort " ++ msg) -- e.g., in AI code+ (\ msg -> -- e.g., in AI code+ ioError $ userError $ T.unpack $ "unhandled abort:" <+> msg) state diary @@ -132,11 +134,10 @@ -- including many consecutive games in a single session, -- but is completely disregarded and reset when a new playing session starts. data Session = Session- { sfs :: FrontendSession -- ^ frontend session information- , scops :: Kind.COps -- ^ game content- , sbinding :: Binding (ActionFrame ())- -- ^ binding of keys to commands- , sorigConfig :: Config.CP -- ^ config from the config file+ { sfs :: FrontendSession -- ^ frontend session information+ , scops :: Kind.COps -- ^ game content+ , sbinding :: Binding (ActionFrame ()) -- ^ binding of keys to commands+ , sconfigUI :: ConfigUI -- ^ the UI config for this session } -- | Get the frontend session.@@ -152,9 +153,8 @@ getBinding = Action (\ Session{sbinding} _p k _a st ms -> k st ms sbinding) -- | Get the config from the config file.-getOrigConfig :: Action (Config.CP)-getOrigConfig =- Action (\ Session{sorigConfig} _p k _a st ms -> k st ms sorigConfig)+getConfigUI :: Action ConfigUI+getConfigUI = Action (\ Session{sconfigUI} _p k _a st ms -> k st ms sconfigUI) -- | Reset the state and resume from the last backup point, i.e., invoke -- the failure continuation.@@ -190,9 +190,9 @@ -- | Try the given computation and silently catch failure. tryIgnore :: Action () -> Action () tryIgnore =- tryWith (\ msg -> if null msg+ tryWith (\ msg -> if T.null msg then return ()- else assert `failure` (msg, "in tryIgnore"))+ else assert `failure` msg <+> "in tryIgnore") -- | Get the current diary. getDiary :: Action Diary
Game/LambdaHack/Action/ConfigIO.hs view
@@ -1,6 +1,6 @@ -- | Personal game configuration file support. module Game.LambdaHack.Action.ConfigIO- ( mkConfig, appDataDir, getFile, dump, getSetGen+ ( mkConfigRules, mkConfigUI, dump ) where import System.Directory@@ -8,72 +8,53 @@ import System.Environment import qualified Data.ConfigFile as CF import qualified Data.Char as Char-import qualified Data.List as L+import Data.List import qualified System.Random as R+import qualified Data.Text as T import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Config+import qualified Game.LambdaHack.Key as K+import qualified Game.LambdaHack.Kind as Kind+import Game.LambdaHack.Content.RuleKind overrideCP :: CP -> FilePath -> IO CP-overrideCP (CP defCF) cfile = do- c <- CF.readfile defCF cfile- return $ toCP $ forceEither c+overrideCP cp@(CP defCF) cfile = do+ b <- doesFileExist cfile+ if not b+ then return cp+ else do+ c <- CF.readfile defCF cfile+ return $ toCP $ forceEither c --- | Read the player configuration file and use it to override--- any default config options. Currently we can't unset options, only override.------ The default config, passed in argument @configDefault@,--- is expected to come from the default configuration file included via CPP--- in file @ConfigDefault.hs@.-mkConfig :: String -> IO CP-mkConfig configDefault = do- let delFileMarker = L.init $ L.drop 3 $ lines configDefault- delComment = L.map (L.drop 2) delFileMarker+-- | Read a player configuration file and use it to override+-- options from a default config. Currently we can't unset options,+-- only override. The default config, passed in argument @configDefault@,+-- is expected to come from a default configuration file included via CPP.+-- The player configuration comes from file @cfile@.+mkConfig :: String -> FilePath -> IO CP+mkConfig configDefault cfile = do+ let delComment = map (drop 2) $ lines configDefault unConfig = unlines delComment -- Evaluate, to catch config errors ASAP. !defCF = forceEither $ CF.readstring CF.emptyCP unConfig- !defConfig = toCP defCF- cfile <- configFile- b <- doesFileExist cfile- if not b- then return defConfig- else overrideCP defConfig cfile+ !defCP = toCP defCF+ overrideCP defCP cfile -- | Personal data directory for the game. Depends on the OS and the game, -- e.g., for LambdaHack under Linux it's @~\/.LambdaHack\/@. appDataDir :: IO FilePath appDataDir = do progName <- getProgName- let name = L.takeWhile Char.isAlphaNum progName+ let name = takeWhile Char.isAlphaNum progName getAppUserDataDirectory name --- | Path to the user configuration file in the personal data directory.-configFile :: IO FilePath-configFile = do- appData <- appDataDir- return $ combine appData "config"---- | Looks up a file path in the config file and makes it absolute.--- If the game's configuration directory exists,--- the file path is appended to it; otherwise, it's appended--- to the current directory.-getFile :: CP -> CF.SectionSpec -> CF.OptionSpec -> IO FilePath-getFile conf s o = do- current <- getCurrentDirectory- appData <- appDataDir- let path = get conf s o- appPath = combine appData path- curPath = combine current path- b <- doesDirectoryExist appData- return $ if b then appPath else curPath- -- | Dumps the current configuration to a file.-dump :: FilePath -> CP -> IO ()-dump fn (CP conf) = do+dump :: Config -> FilePath -> IO ()+dump Config{configSelfString} fn = do current <- getCurrentDirectory- let path = combine current fn- sdump = CF.to_string conf- writeFile path sdump+ let path = current </> fn+ writeFile path configSelfString -- | Simplified setting of an option in a given section. Overwriting forbidden. set :: CP -> CF.SectionSpec -> CF.OptionSpec -> String -> CP@@ -97,3 +78,113 @@ let gs = show g c = set config "engine" option gs return (g, c)++-- | The content of the configuration file. It's parsed+-- in a case sensitive way (unlike by default in ConfigFile).+newtype CP = CP CF.ConfigParser++instance Show CP where+ show (CP conf) = show $ CF.to_string conf++-- | Switches all names to case sensitive (unlike by default in+-- the "ConfigFile" library) and wraps in the constructor.+toCP :: CF.ConfigParser -> CP+toCP cf = CP $ cf {CF.optionxform = id}++-- | In case of corruption, just fail.+forceEither :: Show a => Either a b -> b+forceEither (Left a) = assert `failure` a+forceEither (Right b) = b++-- | A simplified access to an option in a given section,+-- with simple error reporting (no internal errors are caught nor hidden).+-- If there is no such option, gives Nothing.+getOption :: CF.Get_C a => CP -> CF.SectionSpec -> CF.OptionSpec -> Maybe a+getOption (CP conf) s o =+ if CF.has_option conf s o+ then Just $ forceEither $ CF.get conf s o+ else Nothing++-- | Simplified access to an option in a given section.+-- Fails if the option is not present.+get :: CF.Get_C a => CP -> CF.SectionSpec -> CF.OptionSpec -> a+get (CP conf) s o =+ if CF.has_option conf s o+ then forceEither $ CF.get conf s o+ else assert `failure` "Unknown config option: " ++ s ++ "." ++ o++-- | An association list corresponding to a section. Fails if no such section.+getItems :: CP -> CF.SectionSpec -> [(String, String)]+getItems (CP conf) s =+ if CF.has_section conf s+ then forceEither $ CF.items conf s+ else assert `failure` "Unknown config section: " ++ s++parseConfigRules :: CP -> Config+parseConfigRules cp =+ let configSelfString = let CP conf = cp in CF.to_string conf+ configCaves = map (\(n, t) -> (T.pack n, T.pack t)) $ getItems cp "caves"+ configDepth = get cp "dungeon" "depth"+ configFovMode = get cp "engine" "fovMode"+ configSmellTimeout = get cp "engine" "smellTimeout"+ configBaseHP = get cp "heroes" "baseHP"+ configExtraHeroes = get cp "heroes" "extraHeroes"+ configFirstDeathEnds = get cp "heroes" "firstDeathEnds"+ configFaction = T.pack $ get cp "heroes" "faction"+ in Config{..}++parseConfigUI :: FilePath -> CP -> ConfigUI+parseConfigUI dataDir cp =+ let mkKey s =+ case K.keyTranslate s of+ K.Unknown _ ->+ assert `failure` ("unknown config file key <" ++ s ++ ">")+ key -> key+ configCommands =+ let mkCommand (key, def) = (mkKey key, def)+ section = getItems cp "commands"+ in map mkCommand section+ configAppDataDir = dataDir+ configDiaryFile = dataDir </> get cp "files" "diaryFile"+ configSaveFile = dataDir </> get cp "files" "saveFile"+ configBkpFile = dataDir </> get cp "files" "saveFile" <.> ".bkp"+ configScoresFile = dataDir </> get cp "files" "scoresFile"+ configRulesCfgFile = dataDir </> "config.rules"+ configUICfgFile = dataDir </> "config.ui"+ configHeroNames =+ let toNumber (ident, name) =+ case stripPrefix "HeroName_" ident of+ Just n -> (read n, T.pack name)+ Nothing -> assert `failure` ("wrong hero name id " ++ ident)+ section = getItems cp "heroNames"+ in map toNumber section+ configMacros =+ let trMacro (from, to) =+ let !fromTr = mkKey from+ !toTr = mkKey to+ in if fromTr == toTr+ then assert `failure` "degenerate alias: " ++ show toTr+ else (fromTr, toTr)+ section = getItems cp "macros"+ in map trMacro section+ configFont = get cp "ui" "font"+ configHistoryMax = get cp "ui" "historyMax"+ in ConfigUI{..}++-- | Read and parse rules config file and supplement it with random seeds.+mkConfigRules :: Kind.Ops RuleKind -> IO (Config, R.StdGen, R.StdGen)+mkConfigRules corule = do+ let cpRulesDefault = rcfgRulesDefault $ Kind.stdRuleset corule+ appData <- appDataDir+ cpRules <- mkConfig cpRulesDefault $ appData </> "config.rules.ini"+ (dungeonGen, cp2) <- getSetGen cpRules "dungeonRandomGenerator"+ (startingGen, cp3) <- getSetGen cp2 "startingRandomGenerator"+ return (parseConfigRules cp3, dungeonGen, startingGen)++-- | Read and parse UI config file.+mkConfigUI :: Kind.Ops RuleKind -> IO ConfigUI+mkConfigUI corule = do+ let cpUIDefault = rcfgUIDefault $ Kind.stdRuleset corule+ appData <- appDataDir+ cpUI <- mkConfig cpUIDefault $ appData </> "config.ui.ini"+ return $ parseConfigUI appData cpUI
Game/LambdaHack/Action/Frontend/Chosen.hs view
@@ -8,10 +8,12 @@ -- Wrapper for selected Display frontend. -#ifdef CURSES-import Game.LambdaHack.Action.Frontend.Curses as D+#ifdef GTK+import Game.LambdaHack.Action.Frontend.Gtk as D #elif VTY import Game.LambdaHack.Action.Frontend.Vty as D+#elif CURSES+import Game.LambdaHack.Action.Frontend.Curses as D #elif STD import Game.LambdaHack.Action.Frontend.Std as D #else
Game/LambdaHack/Action/Frontend/Curses.hs view
@@ -14,6 +14,7 @@ import qualified Data.Map as M import Control.Monad import Data.Char (ord, chr)+import qualified Data.Text as T import Game.LambdaHack.Utils.Assert import qualified Game.LambdaHack.Key as K (Key(..), Modifier(..))@@ -65,11 +66,11 @@ let defaultStyle = sstyles M.! Color.defaultAttr C.erase C.setStyle defaultStyle- C.mvWAddStr swin 0 0 sfTop+ C.mvWAddStr swin 0 0 (T.unpack sfTop) -- We need to remove the last character from the status line, -- because otherwise it would overflow a standard size xterm window, -- due to the curses historical limitations.- C.mvWAddStr swin (L.length sfLevel + 1) 0 (L.init sfBottom)+ C.mvWAddStr swin (L.length sfLevel + 1) 0 (L.init $ T.unpack sfBottom) let nm = L.zip [0..] $ L.map (L.zip [0..]) sfLevel sequence_ [ C.setStyle (M.findWithDefault defaultStyle acAttr sstyles) >> C.mvWAddStr swin (y + 1) x [acChar]
Game/LambdaHack/Action/Frontend/Gtk.hs view
@@ -21,6 +21,8 @@ import qualified Data.Map as M import qualified Data.ByteString.Char8 as BS import System.Time+import qualified Data.Text as T+import Data.Text.Encoding (encodeUtf8) import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Utils.LQueue@@ -319,8 +321,9 @@ evalFrame :: FrontendSession -> SingleFrame -> GtkFrame evalFrame FrontendSession{stags} SingleFrame{..} =- let levelChar = L.map (L.map Color.acChar) sfLevel- gfChar = BS.pack $ L.intercalate "\n" $ sfTop : levelChar ++ [sfBottom]+ let levelChar = L.map (T.pack . L.map Color.acChar) sfLevel+ gfChar = encodeUtf8 $ T.intercalate (T.singleton '\n')+ $ sfTop : levelChar ++ [sfBottom] -- Strict version of @L.map (L.map ((stags M.!) . fst)) sfLevel@. gfAttr = L.reverse $ L.foldl' ff [] sfLevel ff ll l = (L.reverse $ L.foldl' f [] l) : ll
Game/LambdaHack/Action/Frontend/Std.hs view
@@ -11,6 +11,7 @@ import qualified Data.List as L import qualified Data.ByteString.Char8 as BS import qualified System.IO as SIO+import Data.Text.Encoding (encodeUtf8) import qualified Game.LambdaHack.Key as K (Key(..), Modifier(..)) import qualified Game.LambdaHack.Color as Color@@ -36,7 +37,7 @@ display _ _ _ Nothing = return () display _ _ _ (Just SingleFrame{..}) = let chars = L.map (BS.pack . L.map Color.acChar) sfLevel- bs = [BS.pack sfTop, BS.empty] ++ chars ++ [BS.pack sfBottom, BS.empty]+ bs = [encodeUtf8 sfTop, BS.empty] ++ chars ++ [encodeUtf8 sfBottom, BS.empty] in mapM_ BS.putStrLn bs -- | Input key via the frontend.
Game/LambdaHack/Action/Frontend/Vty.hs view
@@ -11,7 +11,7 @@ import Graphics.Vty import qualified Graphics.Vty as Vty import qualified Data.List as L-import qualified Data.ByteString.Char8 as BS+import Data.Text.Encoding (encodeUtf8) import qualified Game.LambdaHack.Key as K (Key(..), Modifier(..)) import qualified Game.LambdaHack.Color as Color@@ -45,9 +45,9 @@ char (setAttr acAttr) acChar))) sfLevel pic = pic_for_image $- utf8_bytestring (setAttr Color.defaultAttr) (BS.pack sfTop)+ utf8_bytestring (setAttr Color.defaultAttr) (encodeUtf8 sfTop) <-> img <->- utf8_bytestring (setAttr Color.defaultAttr) (BS.pack sfBottom)+ utf8_bytestring (setAttr Color.defaultAttr) (encodeUtf8 sfBottom) in update vty pic -- | Input key via the frontend.
Game/LambdaHack/Action/HighScore.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | High score table operations. module Game.LambdaHack.Action.HighScore ( register@@ -9,10 +10,12 @@ import System.Time import Data.Binary import qualified Data.List as L+import Data.Text (Text)+import qualified Data.Text as T+import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Utils.File-import qualified Game.LambdaHack.Config as Config-import qualified Game.LambdaHack.Action.ConfigIO as ConfigIO+import Game.LambdaHack.Config import Game.LambdaHack.Dungeon import Game.LambdaHack.Misc import Game.LambdaHack.Time@@ -45,7 +48,7 @@ return (ScoreRecord p n (TOD cs cp) s) -- | Show a single high score, from the given ranking in the high score table.-showScore :: (Int, ScoreRecord) -> [String]+showScore :: (Int, ScoreRecord) -> [Text] showScore (pos, score) = let died = case status score of Killed lvl -> "perished on level " ++ show (levelNumber lvl) ++ ","@@ -53,21 +56,18 @@ Victor -> "emerged victorious" Restart -> "resigned prematurely" curDate = calendarTimeToString . toUTCTime . date $ score- big = " "- lil = " "+ big, lil :: String+ big = " "+ lil = " " turns = - (negTime score `timeFit` timeTurn) -- TODO: the spaces at the end are hand-crafted. Remove when display -- of overlays adds such spaces automatically.- in [ printf- "%s"- big- , printf- "%4d. %6d This adventuring party %s after %d turns "- pos (points score) died turns- , printf- "%son %s. "- lil curDate- ]+ in map T.pack+ [ big+ , printf "%4d. %6d This adventuring party %s after %d turns "+ pos (points score) died turns+ , lil ++ printf "on %s. " curDate+ ] -- | The list of scores, in decreasing order. type ScoreTable = [ScoreRecord]@@ -76,24 +76,17 @@ empty :: ScoreTable empty = [] --- | Name of the high scores file.-scoresFile :: Config.CP -> IO String-scoresFile config = ConfigIO.getFile config "files" "scoresFile"- -- | Save a simple serialized version of the high scores table.-save :: Config.CP -> ScoreTable -> IO ()-save config scores = do- f <- scoresFile config- encodeEOF f scores+save :: ConfigUI -> ScoreTable -> IO ()+save ConfigUI{configScoresFile} scores = encodeEOF configScoresFile scores -- | Read the high scores table. Return the empty table if no file.-restore :: Config.CP -> IO ScoreTable-restore config = do- f <- scoresFile config- b <- doesFileExist f+restore :: ConfigUI -> IO ScoreTable+restore ConfigUI{configScoresFile} = do+ b <- doesFileExist configScoresFile if not b then return empty- else strictDecodeEOF f+ else strictDecodeEOF configScoresFile -- | Insert a new score into the table, Return new table and the ranking. insertPos :: ScoreRecord -> ScoreTable -> (ScoreTable, Int)@@ -119,15 +112,15 @@ -- | Take care of saving a new score to the table -- and return a list of messages to display.-register :: Config.CP -- ^ the config file+register :: ConfigUI -- ^ the config file -> Bool -- ^ whether to write or only render -> Int -- ^ the total score. not halved yet -> Time -- ^ game time spent -> ClockTime -- ^ date of the last game interruption -> Status -- ^ reason of the game interruption- -> IO (String, [Overlay])-register config write total time date status = do- h <- restore config+ -> IO (Msg, [Overlay])+register configUI write total time date status = do+ h <- restore configUI let points = case status of Killed _ -> (total + 1) `div` 2 _ -> total@@ -136,16 +129,24 @@ (h', pos) = insertPos score h (_, nlines) = normalLevelBound -- TODO: query terminal size instead height = nlines `div` 3- (msgCurrent, msgUnless) =+ (subject, person, msgUnless) = case status of- Killed _ -> ("short-lived", " (score halved)")- Camping -> ("current", " (unless you are slain)")- Victor -> ("glorious",- if pos <= height- then " among the greatest heroes"- else "")- Restart -> ("abortive", " (score halved)")- msg = printf "Your %s exploits award you place >> %d <<%s."- msgCurrent pos msgUnless- when write $ save config h'+ Killed lvl | levelNumber lvl <= 1 ->+ ("your short-lived struggle", MU.Sg3rd, "(score halved)")+ Killed _ ->+ ("your heroic deeds", MU.PlEtc, "(score halved)")+ Camping ->+ ("your valiant exploits", MU.PlEtc, "(unless you are slain)")+ Victor ->+ ("your glorious victory", MU.Sg3rd,+ if pos <= height+ then "among the greatest heroes"+ else "")+ Restart ->+ ("your abortive attempt", MU.Sg3rd, "(score halved)")+ msg = makeSentence+ [ MU.SubjectVerb person MU.Yes subject "award you"+ , MU.Ordinal pos, "place"+ , msgUnless ]+ when write $ save configUI h' return (msg, slideshow pos h' height)
Game/LambdaHack/Action/Save.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | Saving and restoring games and player diaries. module Game.LambdaHack.Action.Save ( saveGameFile, restoreGame, rmBkpSaveDiary, saveGameBkp@@ -9,32 +10,17 @@ import Control.Monad import Control.Concurrent import System.IO.Unsafe (unsafePerformIO) -- horrors+import Data.Text (Text)+import qualified Data.Text as T import Game.LambdaHack.Utils.File import Game.LambdaHack.State-import qualified Game.LambdaHack.Config as Config-import qualified Game.LambdaHack.Action.ConfigIO as ConfigIO import Game.LambdaHack.Msg---- | Name of the save game.-saveFile :: Config.CP -> IO FilePath-saveFile config = ConfigIO.getFile config "files" "saveFile"---- | Name of the backup of the save game.-bkpFile :: Config.CP -> IO FilePath-bkpFile config = do- sfile <- saveFile config- return $ sfile ++ ".bkp"---- | Name of the persistent player diary.-diaryFile :: Config.CP -> IO FilePath-diaryFile config = ConfigIO.getFile config "files" "diaryFile"+import Game.LambdaHack.Config -- | Save a simple serialized version of the current player diary.-saveDiary :: Config.CP -> Diary -> IO ()-saveDiary config diary = do- dfile <- diaryFile config- encodeEOF dfile diary+saveDiary :: FilePath -> Diary -> IO ()+saveDiary configDiaryFile diary = encodeEOF configDiaryFile diary saveLock :: MVar () {-# NOINLINE saveLock #-}@@ -42,11 +28,10 @@ -- | Save a simple serialized version of the current state. -- Protected by a lock to avoid corrupting the file.-saveGameFile :: State -> IO ()-saveGameFile state = do+saveGameFile :: ConfigUI -> State -> IO ()+saveGameFile ConfigUI{configSaveFile} state = do putMVar saveLock ()- sfile <- saveFile (sconfig state)- encodeEOF sfile state+ encodeEOF configSaveFile state takeMVar saveLock -- | Try to create a directory. Hide errors due to,@@ -58,42 +43,45 @@ (createDirectory dir) (\ e -> case e :: Ex.IOException of _ -> return ()) --- TODO: perhaps take the target "scores" file name from config.--- TODO: perhaps source and "config", too, to be able to change all--- in one place. -- | Try to copy over data files. Hide errors due to, -- e.g., insufficient permissions, because the game can run -- without data files just as well.-tryCopyDataFiles :: (FilePath -> IO FilePath) -> FilePath -> IO ()-tryCopyDataFiles pathsDataFile dirNew = do- configFile <- pathsDataFile "config.default"- scoresFile <- pathsDataFile "scores"- let configNew = combine dirNew "config"- scoresNew = combine dirNew "scores"+tryCopyDataFiles :: ConfigUI -> (FilePath -> IO FilePath) -> IO ()+tryCopyDataFiles ConfigUI{ configScoresFile+ , configRulesCfgFile+ , configUICfgFile } pathsDataFile = do+ rulesFile <- pathsDataFile $ takeFileName configRulesCfgFile <.> ".default"+ uiFile <- pathsDataFile $ takeFileName configUICfgFile <.> ".default"+ scoresFile <- pathsDataFile $ takeFileName configScoresFile+ let newRulesFile = configRulesCfgFile <.> ".ini"+ newUIFile = configUICfgFile <.> ".ini"+ newScoresFile = configScoresFile Ex.catch- (copyFile configFile configNew >>- copyFile scoresFile scoresNew)+ (copyFile rulesFile newRulesFile >>+ copyFile uiFile newUIFile >>+ copyFile scoresFile newScoresFile) (\ e -> case e :: Ex.IOException of _ -> return ()) -- | Restore a saved game, if it exists. Initialize directory structure, -- if needed.-restoreGame :: (FilePath -> IO FilePath) -> Config.CP -> String+restoreGame :: ConfigUI -> (FilePath -> IO FilePath) -> Text -> IO (Either (State, Diary, Msg) (Diary, Msg))-restoreGame pathsDataFile config title = do- appData <- ConfigIO.appDataDir- ab <- doesDirectoryExist appData+restoreGame config@ConfigUI{ configAppDataDir+ , configDiaryFile+ , configSaveFile+ , configBkpFile } pathsDataFile title = do+ ab <- doesDirectoryExist configAppDataDir -- If the directory can't be created, the current directory will be used. unless ab $ do- tryCreateDir appData+ tryCreateDir configAppDataDir -- Possibly copy over data files. No problem if it fails.- tryCopyDataFiles pathsDataFile appData+ tryCopyDataFiles config pathsDataFile -- If the diary file does not exist, create an empty diary. -- TODO: when diary gets corrupted, start a new one, too. diary <-- do dfile <- diaryFile config- db <- doesFileExist dfile+ do db <- doesFileExist configDiaryFile if db- then strictDecodeEOF dfile+ then strictDecodeEOF configDiaryFile else defaultDiary -- If the savefile exists but we get IO errors, we show them, -- back up the savefile and move it out of the way and start a new game.@@ -101,51 +89,42 @@ -- that should solve the problem. If the problems are more serious, -- the other functions will most probably also throw exceptions, -- this time without trying to fix it up.- sfile <- saveFile config- bfile <- bkpFile config- sb <- doesFileExist sfile- bb <- doesFileExist bfile+ sb <- doesFileExist configSaveFile+ bb <- doesFileExist configBkpFile Ex.catch (if sb then do- mvBkp config- state <- strictDecodeEOF bfile- let msg = "Welcome back to " ++ title ++ "."+ renameFile configSaveFile configBkpFile+ state <- strictDecodeEOF configBkpFile+ let msg = "Welcome back to" <+> title <> "." return $ Left (state, diary, msg) else if bb then do- state <- strictDecodeEOF bfile+ state <- strictDecodeEOF configBkpFile let msg = "No savefile found. Restoring from a backup savefile." return $ Left (state, diary, msg)- else return $ Right (diary, "Welcome to " ++ title ++ "!"))+ else return $ Right (diary, "Welcome to" <+> title <> "!")) (\ e -> case e :: Ex.SomeException of- _ -> let msg = "Starting a new game, because restore failed. "- ++ "The error message was: "- ++ (unwords . lines) (show e)+ _ -> let msg = "Starting a new game, because restore failed."+ <+> "The error message was:"+ <+> (T.unwords . T.lines) (showT e) in return $ Right (diary, msg)) --- | Move the savegame file to a backup slot.-mvBkp :: Config.CP -> IO ()-mvBkp config = do- sfile <- saveFile config- bfile <- bkpFile config- renameFile sfile bfile- -- | Save the diary and a backup of the save game file, in case of crashes. -- This is only a backup, so no problem is the game is shut down -- before saving finishes, so we don't wait on the mvar. However, -- if a previous save is already in progress, we skip this save.-saveGameBkp :: State -> Diary -> IO ()-saveGameBkp state diary = do+saveGameBkp :: ConfigUI -> State -> Diary -> IO ()+saveGameBkp ConfigUI{ configDiaryFile+ , configSaveFile+ , configBkpFile } state diary = do b <- tryPutMVar saveLock ()- let config = sconfig state when b $ void $ forkIO $ do- saveDiary config diary -- save the diary often in case of crashes- sfile <- saveFile config- encodeEOF sfile state- mvBkp (sconfig state)+ saveDiary configDiaryFile diary -- save diary often in case of crashes+ encodeEOF configSaveFile state+ renameFile configSaveFile configBkpFile takeMVar saveLock -- | Remove the backup of the savegame and save the player diary.@@ -154,11 +133,11 @@ -- We don't bother reporting any other removal exceptions, either, -- because the backup file is relatively unimportant. -- We wait on the mvar, because saving the diary at game shutdown is important.-rmBkpSaveDiary :: Config.CP -> Diary -> IO ()-rmBkpSaveDiary config diary = do+rmBkpSaveDiary :: ConfigUI -> Diary -> IO ()+rmBkpSaveDiary ConfigUI{ configDiaryFile+ , configBkpFile } diary = do putMVar saveLock ()- saveDiary config diary -- save the diary often in case of crashes- bfile <- bkpFile config- bb <- doesFileExist bfile- when bb $ removeFile bfile+ saveDiary configDiaryFile diary -- save diary often in case of crashes+ bb <- doesFileExist configBkpFile+ when bb $ removeFile configBkpFile takeMVar saveLock
Game/LambdaHack/Actions.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE OverloadedStrings, ExtendedDefaultRules #-}+{-# OPTIONS_GHC -fno-warn-type-defaults #-} -- | The game action stuff that is independent from ItemAction.hs -- (both depend on EffectAction.hs). -- TODO: Add an export list and document after it's rewritten according to #17.@@ -15,12 +17,14 @@ import Data.Version import qualified Data.IntSet as IS import Data.Ratio+import Data.Text (Text)+import qualified Data.Text as T+import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Action import Game.LambdaHack.Point import Game.LambdaHack.Vector-import Game.LambdaHack.Grammar import qualified Game.LambdaHack.Dungeon as Dungeon import Game.LambdaHack.Item import qualified Game.LambdaHack.Key as K@@ -47,7 +51,10 @@ import Game.LambdaHack.Animation (swapPlaces, blockMiss) import Game.LambdaHack.Draw import qualified Game.LambdaHack.Command as Command+import Game.LambdaHack.Config +default (Text)+ gameSave :: Action () gameSave = do saveGameBkp@@ -155,10 +162,11 @@ mapM_ f $ TileKind.tfeature $ okind $ lvl `at` dloc -- | Ask for a direction and trigger a tile, if possible.-playerTriggerDir :: F.Feature -> Verb -> Action ()+playerTriggerDir :: F.Feature -> MU.Part -> Action () playerTriggerDir feat verb = do let keys = zip K.dirAllMoveKey $ repeat K.NoModifier- e <- displayChoiceUI ("What to " ++ verb ++ "? [movement key") [] keys+ prompt = makePhrase ["What to", verb MU.:> "? [movement key"]+ e <- displayChoiceUI prompt [] keys lxsize <- gets (lxsize . slevel) K.handleDir lxsize e (playerBumpDir feat) (neverMind True) @@ -405,27 +413,28 @@ _ -> assert `failure` bitems else case strongestSword cops bitems of Nothing -> (h2hItem, False, 0,- iverbApply $ okind $ h2hKind) -- hand-to-hand+ iverbApply $ okind $ h2hKind) -- hand to hand combat Just w -> (w, True, 0, iverbApply $ okind $ jkind w) -- weapon- single = stack { jcount = 1 } -- The msg describes the source part of the action. -- TODO: right now it also describes the victim and weapon; -- perhaps, when a weapon is equipped, just say "you hit" -- or "you miss" and then "nose dies" or "nose yells in pain".- msg = actorVerbActor coactor sm verb tm $- if tell- then "with " ++ objectItem coitem state single- else ""- msgMiss = init (actorVerb coactor sm ("try to " ++ verb) "")- ++ ", but "- ++ let tmSubject = objectActor coactor tm- in tmSubject ++ " "- ++ conjugate tmSubject "block" ++ "."+ msg = makeSentence $+ [ MU.SubjectVerbSg (partActor coactor sm) verb+ , partActor coactor tm ]+ ++ if tell+ then ["with", MU.AW $ partItem coitem state stack]+ else []+ msgMiss = makeSentence+ [ MU.SubjectVerbSg (partActor coactor sm) "try to"+ , verb MU.:> ", but"+ , MU.SubjectVerbSg (partActor coactor tm) "block"+ ] let performHit block = do when (svisible || tvisible) $ msgAdd msg -- Msgs inside itemEffectAction describe the target part.- itemEffectAction verbosity source target single block+ itemEffectAction verbosity source target stack block -- Projectiles can't be blocked, can be sidestepped. if braced tm time && not (bproj sm) then do@@ -457,7 +466,9 @@ per <- getPerception let visible = sloc `IS.member` totalVisible per || tloc `IS.member` totalVisible per- msg = actorVerbActor coactor sm "displace" tm ""+ msg = makeSentence+ [ MU.SubjectVerbSg (partActor coactor sm) "displace"+ , partActor coactor tm ] when visible $ msgAdd msg diary <- getDiary -- here diary possibly contains the new msg s <- get@@ -547,10 +558,9 @@ displayMainMenu = do Kind.COps{corule} <- getCOps Binding{krevMap} <- getBinding- let stripFrame ('\n' : art) =- let interior = tail . init- in map interior $ interior $ lines art- stripFrame art = assert `failure` "displayMainMenu: " ++ art+ let stripFrame t = case T.uncons t of+ Just ('\n', art) -> map (T.tail . T.init) $ tail . init $ T.lines art+ _ -> assert `failure` "displayMainMenu:" <+> t pasteVersion art = let pathsVersion = rpathsVersion $ Kind.stdRuleset corule version = " Version " ++ showVersion pathsVersion@@ -560,7 +570,7 @@ versionLen = length version in init art ++ [take (80 - versionLen) (last art) ++ version] kds = -- key-description pairs- let showKD cmd key = (show key, Command.cmdDescription cmd)+ let showKD cmd key = (showT key, Command.cmdDescription cmd) revLookup cmd = maybe ("", "") (showKD cmd) $ M.lookup cmd krevMap cmds = [Command.GameSave, Command.GameExit, Command.GameRestart, Command.Help]@@ -568,21 +578,22 @@ bindings = -- key bindings to display let bindingLen = 25 fmt (k, d) =- let gapLen = (8 - length k) `max` 1- padLen = bindingLen - length k - gapLen - length d- in k ++ replicate gapLen ' ' ++ d ++ replicate padLen ' '+ let gapLen = (8 - T.length k) `max` 1+ padLen = bindingLen - T.length k - gapLen - T.length d+ in k <> T.replicate gapLen " " <> d <> T.replicate padLen " " in map fmt kds overwrite = -- overwrite the art with key bindings- let over [] line = ([], line)+ let over [] line = ([], T.pack line) over bs@(binding : bsRest) line = let (prefix, lineRest) = break (=='{') line (braces, suffix) = span (=='{') lineRest in if length braces == 25- then (bsRest, prefix ++ binding ++ suffix)- else (bs, line)+ then (bsRest, T.pack prefix <> binding <> T.pack suffix)+ else (bs, T.pack line) in snd . L.mapAccumL over bindings mainMenuArt = rmainMenuArt $ Kind.stdRuleset corule- menuOverlay = overwrite $ pasteVersion $ stripFrame mainMenuArt+ menuOverlay = -- TODO: switch to Text and use T.justifyLeft+ overwrite $ pasteVersion $ map T.unpack $ stripFrame $ mainMenuArt case menuOverlay of [] -> assert `failure` "empty Main Menu overlay" hd : tl -> displayOverlays hd "" [tl]@@ -592,18 +603,20 @@ Diary{shistory} <- getDiary time <- gets stime lysize <- gets (lysize . slevel)- let turn = show $ time `timeFit` timeTurn- msg = "You survived for " ++ turn- ++ " half-second turns. Past messages:"+ let turn = time `timeFit` timeTurn+ msg = makeSentence [ "You survived for"+ , MU.NWs turn "half-second turn" ]+ <+> "Past messages:" displayOverlays msg "" $ splitOverlay lysize $ renderHistory shistory dumpConfig :: Action () dumpConfig = do- config <- gets sconfig- let fn = "config.dump"- msg = "Current configuration dumped to file " ++ fn ++ "."- dumpCfg fn config+ ConfigUI{configRulesCfgFile} <- getConfigUI+ let fn = configRulesCfgFile ++ ".dump"+ msg = "Current game rules configuration dumped to file"+ <+> T.pack fn <> "."+ dumpCfg fn abortWith msg -- | Add new smell traces to the level. Only humans leave a strong scent.
Game/LambdaHack/Actor.hs view
@@ -1,8 +1,9 @@+{-# LANGUAGE OverloadedStrings #-} -- | Actors in the game: monsters and heroes. No operation in this module -- involves the 'State' or 'Action' type. module Game.LambdaHack.Actor ( -- * Actor identifiers and related operations- ActorId, findHeroName, monsterGenChance+ ActorId, findHeroName, monsterGenChance, partActor -- * The@ Acto@r type , Actor(..), template, addHp, timeAddFromSpeed, braced , unoccupied, heroKindId, projectileKindId, actorSpeed@@ -14,6 +15,8 @@ import Data.Binary import Data.Maybe import Data.Ratio+import Data.Text (Text)+import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Vector@@ -21,8 +24,9 @@ import Game.LambdaHack.Content.ActorKind import Game.LambdaHack.Content.FactionKind import qualified Game.LambdaHack.Kind as Kind+import Game.LambdaHack.Msg import Game.LambdaHack.Random-import qualified Game.LambdaHack.Config as Config+import Game.LambdaHack.Config import Game.LambdaHack.Time import qualified Game.LambdaHack.Color as Color @@ -33,7 +37,7 @@ data Actor = Actor { bkind :: !(Kind.Id ActorKind) -- ^ the kind of the actor , bsymbol :: !(Maybe Char) -- ^ individual map symbol- , bname :: !(Maybe String) -- ^ individual name+ , bname :: !(Maybe Text) -- ^ individual name , bcolor :: !(Maybe Color.Color) -- ^ individual map color , bspeed :: !(Maybe Speed) -- ^ individual speed , bhp :: !Int -- ^ current hit points@@ -88,10 +92,10 @@ type ActorId = Int -- | Find a hero name in the config file, or create a stock name.-findHeroName :: Config.CP -> Int -> String-findHeroName config n =- let heroName = Config.getOption config "heroes" ("HeroName_" ++ show n)- in fromMaybe ("hero number " ++ show n) heroName+findHeroName :: ConfigUI -> Int -> Text+findHeroName ConfigUI{configHeroNames} n =+ let heroName = lookup n configHeroNames+ in fromMaybe ("hero number" <+> showT n) heroName -- | Chance that a new monster is generated. Currently depends on the -- number of monsters already present, and on the level. In the future,@@ -103,11 +107,15 @@ monsterGenChance depth numMonsters = chance $ 1%(fromIntegral (30 * (numMonsters - depth)) `max` 5) +-- | The part of speech describing the actor.+partActor :: Kind.Ops ActorKind -> Actor -> MU.Part+partActor Kind.Ops{oname} a = MU.Text $ fromMaybe (oname $ bkind a) (bname a)+ -- Actor operations -- | A template for a new non-projectile actor. The initial target is invalid -- to force a reset ASAP.-template :: Kind.Id ActorKind -> Maybe Char -> Maybe String -> Int -> Point+template :: Kind.Id ActorKind -> Maybe Char -> Maybe Text -> Int -> Point -> Time -> Kind.Id FactionKind -> Bool -> Actor template bkind bsymbol bname bhp bloc btime bfaction bproj = let bcolor = Nothing
Game/LambdaHack/ActorState.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | Operations on the 'Actor' type that need the 'State' type, -- but not the 'Action' type. -- TODO: Document an export list after it's rewritten according to #17.@@ -16,6 +17,8 @@ import qualified Data.IntMap as IM import Data.Maybe import qualified Data.Char as Char+import Data.Text (Text)+import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Point@@ -25,13 +28,13 @@ import Game.LambdaHack.Level import Game.LambdaHack.Dungeon import Game.LambdaHack.State-import Game.LambdaHack.Grammar+import Game.LambdaHack.Msg import Game.LambdaHack.Item import Game.LambdaHack.Content.ActorKind import Game.LambdaHack.Content.FactionKind import Game.LambdaHack.Content.TileKind import Game.LambdaHack.Content.ItemKind-import qualified Game.LambdaHack.Config as Config+import Game.LambdaHack.Config import qualified Game.LambdaHack.Tile as Tile import qualified Game.LambdaHack.Kind as Kind import qualified Game.LambdaHack.Feature as F@@ -55,8 +58,8 @@ -- | How long until an actor's smell vanishes from a tile. smellTimeout :: State -> Time smellTimeout s =- let smellTurns = Config.get (sconfig s) "monsters" "smellTimeout"- in timeScale timeTurn smellTurns+ let Config{configSmellTimeout} = sconfig s+ in timeScale timeTurn configSmellTimeout -- The operations with "Any", and those that use them, -- consider all the dungeon.@@ -206,7 +209,8 @@ locs = start : L.nub (concatMap (vicinity lxsize lysize) locs) good loc = Tile.hasFeature cotile F.Walkable (lvl `at` loc) && unoccupied (IM.elems lactor) loc- in fromMaybe (assert `failure` "too crowded map") $ L.find good locs+ in fromMaybe (assert `failure` ("too crowded map" :: Text))+ $ L.find good locs -- | Calculate loot's worth for heroes on the current level. calculateTotal :: Kind.Ops ItemKind -> State -> ([Item], Int)@@ -232,26 +236,27 @@ in fmap fst $ tryFindActor s ((== Just c) . bsymbol) -- | Create a new hero on the current level, close to the given location.-addHero :: Kind.COps -> Point -> State -> State-addHero Kind.COps{coactor, cotile} ploc state@State{scounter, sfaction} =- let config = sconfig state- bHP = Config.get config "heroes" "baseHP"+addHero :: Kind.COps -> Point -> ConfigUI -> State -> State+addHero Kind.COps{coactor, cotile} ploc configUI+ state@State{scounter, sfaction} =+ let Config{configBaseHP} = sconfig state loc = nearbyFreeLoc cotile ploc state freeHeroK = L.elemIndex Nothing $ map (tryFindHeroK state) [0..9] n = fromMaybe 100 freeHeroK symbol = if n < 1 || n > 9 then '@' else Char.intToDigit n- name = findHeroName config n- startHP = bHP - (bHP `div` 5) * min 3 n+ name = findHeroName configUI n+ startHP = configBaseHP - (configBaseHP `div` 5) * min 3 n m = template (heroKindId coactor) (Just symbol) (Just name) startHP loc (stime state) sfaction False cstate = state { scounter = scounter + 1 } in updateLevel (updateActorDict (IM.insert scounter m)) cstate -- | Create a set of initial heroes on the current level, at location ploc.-initialHeroes :: Kind.COps -> Point -> State -> State-initialHeroes cops ploc state =- let k = 1 + Config.get (sconfig state) "heroes" "extraHeroes"- in iterate (addHero cops ploc) state !! k+initialHeroes :: Kind.COps -> Point -> ConfigUI -> State -> State+initialHeroes cops ploc configUI state =+ let Config{configExtraHeroes} = sconfig state+ k = 1 + configExtraHeroes+ in iterate (addHero cops ploc configUI) state !! k -- Adding monsters @@ -273,10 +278,12 @@ addProjectile Kind.COps{coactor, coitem=coitem@Kind.Ops{okind}} item loc bfaction path btime state@State{scounter} = let ik = okind (jkind item)- object = objectItem coitem state item- name = "a flying " ++ unwords (tail (words object)) speed = speedFromWeight (iweight ik) (itoThrow ik) range = rangeFromSpeed speed+ adj | range < 5 = "falling"+ | otherwise = "flying"+ object = partItem coitem state item+ name = makePhrase [MU.AW $ MU.Text adj, object] dirPath = take range $ displacePath path m = Actor { bkind = projectileKindId coactor
Game/LambdaHack/Animation.hs view
@@ -9,6 +9,7 @@ import Data.Maybe import qualified Data.List as L import Data.Monoid+import Data.Text (Text) import Game.LambdaHack.PointXY import Game.LambdaHack.Point@@ -17,8 +18,8 @@ -- | The data sufficent to draw a single game screen frame. data SingleFrame = SingleFrame { sfLevel :: ![[AttrChar]] -- ^ content of the screen, line by line- , sfTop :: String -- ^ an extra line to show at the top- , sfBottom :: String -- ^ an extra line to show at the bottom+ , sfTop :: Text -- ^ an extra line to show at the top+ , sfBottom :: Text -- ^ an extra line to show at the bottom } deriving Eq
Game/LambdaHack/Binding.hs view
@@ -1,22 +1,24 @@+{-# LANGUAGE OverloadedStrings #-} -- | Generic binding of keys to commands, procesing macros, -- printing command help. No operation in this module -- involves the 'State' or 'Action' type. module Game.LambdaHack.Binding- ( Binding(..), macroKey, keyHelp,+ ( Binding(..), keyHelp, ) where import qualified Data.Map as M import qualified Data.List as L import qualified Data.Set as S+import Data.Text (Text)+import qualified Data.Text as T -import Game.LambdaHack.Utils.Assert import qualified Game.LambdaHack.Key as K import Game.LambdaHack.Msg import qualified Game.LambdaHack.Command as Command -- | Bindings and other information about player commands. data Binding a = Binding- { kcmd :: M.Map (K.Key, K.Modifier) (String, Bool, a)+ { kcmd :: M.Map (K.Key, K.Modifier) (Text, Bool, a) -- ^ binding keys to commands , kmacro :: M.Map K.Key K.Key -- ^ macro map , kmajor :: [K.Key] -- ^ major, most often used, commands@@ -25,21 +27,6 @@ -- ^ map from cmds to their main keys } --- | Produce the macro map from a macro association list--- taken from the config file. Macros cannot depend on each other.--- The map is fully evaluated to catch errors in macro definitions early.-macroKey :: [(String, String)] -> M.Map K.Key K.Key-macroKey section =- let trans k = case K.keyTranslate k of- K.Unknown s -> assert `failure` ("unknown macro key " ++ s)- kt -> kt- trMacro (from, to) = let !fromTr = trans from- !toTr = trans to- in if fromTr == toTr- then assert `failure` ("degenerate alias", toTr)- else (fromTr, toTr)- in M.fromList $ L.map trMacro section- coImage :: M.Map K.Key K.Key -> K.Key -> [K.Key] coImage kmacro k = let domain = M.keysSet kmacro@@ -79,16 +66,17 @@ , "For more playing instructions see file PLAYING.md." , "Press SPACE to clear the messages and see the map again." ]- fmt k h = replicate 16 ' ' ++ k ++ replicate ((15 - length k) `max` 1) ' '- ++ h ++ replicate ((41 - length h) `max` 1) ' '- fmts s = replicate 1 ' ' ++ s ++ replicate ((71 - length s) `max` 1) ' '+ fmt k h = T.replicate 16 " "+ <> T.justifyLeft 15 ' ' k+ <> T.justifyLeft 41 ' ' h+ fmts s = " " <> T.justifyLeft 71 ' ' s blank = fmt "" "" mov = map fmts movBlurb major = map fmts majorBlurb minor = map fmts minorBlurb keyCaption = fmt "keys" "command"- disp k = L.concatMap show $ coImage kmacro k- keys l = [ fmt (disp k) (h ++ if timed then "*" else "")+ disp k = T.concat $ map showT $ coImage kmacro k+ keys l = [ fmt (disp k) (h <> if timed then "*" else "") | ((k, _), (h, timed, _)) <- l, h /= "" ] (kcMajor, kcMinor) = L.partition ((`elem` kmajor) . fst . fst) (M.toAscList kcmd)
Game/LambdaHack/BindingAction.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | Binding of keys to commands implemented with the 'Action' monad. module Game.LambdaHack.BindingAction ( stdBinding@@ -8,10 +9,11 @@ import qualified Data.Map as M import qualified Data.Char as Char import Data.Tuple (swap)+import Data.Text (Text) import Game.LambdaHack.Action import Game.LambdaHack.State-import qualified Game.LambdaHack.Config as Config+import Game.LambdaHack.Config import Game.LambdaHack.Level import Game.LambdaHack.Actions import Game.LambdaHack.Running@@ -22,7 +24,7 @@ import Game.LambdaHack.Command import Game.LambdaHack.CommandAction -heroSelection :: [((K.Key, K.Modifier), (String, Bool, ActionFrame ()))]+heroSelection :: [((K.Key, K.Modifier), (Text, Bool, ActionFrame ()))] heroSelection = let select k = do s <- get@@ -36,11 +38,10 @@ -- | Binding of keys to movement and other standard commands, -- as well as commands defined in the config file.-stdBinding :: Config.CP -- ^ game config+stdBinding :: ConfigUI -- ^ game config -> Binding (ActionFrame ()) -- ^ concrete binding-stdBinding config =- let section = Config.getItems config "macros"- !kmacro = macroKey section+stdBinding !config@ConfigUI{configMacros} =+ let kmacro = M.fromList $ configMacros cmdList = configCmds config semList = semanticsCmds cmdList moveWidth f = do
Game/LambdaHack/CDefs.hs view
@@ -9,12 +9,14 @@ -- of the game code proper, with names corresponding to their kinds. module Game.LambdaHack.CDefs (CDefs(..)) where +import Data.Text (Text)+ import Game.LambdaHack.Misc -- | The general type of a particular game content, e.g., item kinds. data CDefs a = CDefs { getSymbol :: a -> Char -- ^ symbol, e.g., to print on the map- , getName :: a -> String -- ^ name, e.g., to show to the player+ , getName :: a -> Text -- ^ name, e.g., to show to the player , getFreq :: a -> Freqs -- ^ frequency within groups , validate :: [a] -> [a] -- ^ validate and catch some offenders, if any , content :: [a] -- ^ all the defined content of this type
Game/LambdaHack/Cave.hs view
@@ -7,6 +7,7 @@ import qualified Data.Map as M import qualified Data.List as L import Data.Maybe+import Data.Text (Text) import Game.LambdaHack.Utils.Assert import Game.LambdaHack.PointXY@@ -23,6 +24,7 @@ import qualified Game.LambdaHack.Place as Place import Game.LambdaHack.Misc import Game.LambdaHack.Time+import Game.LambdaHack.Msg -- | The map of tile kinds in a cave. -- The map is sparse. The default tile that eventually fills the empty spaces@@ -43,7 +45,7 @@ , dmap :: TileMapXY -- ^ tile kinds in the cave , dsecret :: SecretMapXY -- ^ secrecy strength of cave tiles , ditem :: ItemMapXY -- ^ starting items in the cave- , dmeta :: String -- ^ debug information about the cave+ , dmeta :: Text -- ^ debug information about the cave , dplaces :: [Place] -- ^ places generated in the cave } deriving Show@@ -149,7 +151,7 @@ , dsecret = secretMap , ditem = M.empty , dmap- , dmeta = show allConnects+ , dmeta = showT allConnects , dplaces } return cave@@ -182,7 +184,7 @@ passable :: [F.Feature] passable = [F.Walkable, F.Openable, F.Hidden] -mapToHidden :: Kind.Ops TileKind -> String+mapToHidden :: Kind.Ops TileKind -> Text -> Rnd (M.Map (Kind.Id TileKind) (Kind.Id TileKind)) mapToHidden cotile@Kind.Ops{ofoldrWithKey, opick} chiddenTile = let getHidden ti tk acc =
Game/LambdaHack/Command.hs view
@@ -1,20 +1,24 @@+{-# LANGUAGE OverloadedStrings #-} -- | Abstract syntax of player commands. module Game.LambdaHack.Command ( Cmd(..), majorCmd, timedCmd, cmdDescription ) where +import Data.Text (Text)+import qualified NLP.Miniutter.English as MU+ import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Grammar import qualified Game.LambdaHack.Feature as F+import Game.LambdaHack.Msg -- | Abstract syntax of player commands. The type is abstract, but the values -- are created outside this module via the Read class (from config file) . data Cmd = -- These take time.- Apply { verb :: Verb, object :: Object, syms :: [Char] }- | Project { verb :: Verb, object :: Object, syms :: [Char] }- | TriggerDir { verb :: Verb, object :: Object, feature :: F.Feature }- | TriggerTile { verb :: Verb, object :: Object, feature :: F.Feature }+ Apply { verb :: MU.Part, object :: MU.Part, syms :: [Char] }+ | Project { verb :: MU.Part, object :: MU.Part, syms :: [Char] }+ | TriggerDir { verb :: MU.Part, object :: MU.Part, feature :: F.Feature }+ | TriggerTile { verb :: MU.Part, object :: MU.Part, feature :: F.Feature } | Pickup | Drop | Wait@@ -70,12 +74,12 @@ _ -> False -- | Description of player commands.-cmdDescription :: Cmd -> String+cmdDescription :: Cmd -> Text cmdDescription cmd = case cmd of- Apply{..} -> verb ++ " " ++ addIndefinite object- Project{..} -> verb ++ " " ++ addIndefinite object- TriggerDir{..} -> verb ++ " " ++ addIndefinite object- TriggerTile{..} -> verb ++ " " ++ addIndefinite object+ Apply{..} -> makePhrase [verb, MU.AW object]+ Project{..} -> makePhrase [verb, MU.AW object]+ TriggerDir{..} -> makePhrase [verb, MU.AW object]+ TriggerTile{..} -> makePhrase [verb, MU.AW object] Pickup -> "get an object" Drop -> "drop an object" Wait -> ""@@ -87,11 +91,11 @@ TgtFloor -> "target location" TgtEnemy -> "target monster" TgtAscend k | k == 1 -> "target next shallower level"- TgtAscend k | k >= 2 -> "target " ++ show k ++ " levels shallower"+ TgtAscend k | k >= 2 -> "target" <+> showT k <+> "levels shallower" TgtAscend k | k == -1 -> "target next deeper level"- TgtAscend k | k <= -2 -> "target " ++ show (-k) ++ " levels deeper"+ TgtAscend k | k <= -2 -> "target" <+> showT (-k) <+> "levels deeper" TgtAscend _ ->- assert `failure` "void level change in targeting mode in config file"+ assert `failure` ("void level change in targeting in config file" :: Text) EpsIncr True -> "swerve targeting line" EpsIncr False -> "unswerve targeting line" Cancel -> "cancel action"
Game/LambdaHack/CommandAction.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | Semantics of player commands. module Game.LambdaHack.CommandAction ( configCmds, semanticsCmds@@ -5,15 +6,15 @@ import Control.Monad.State hiding (State, state) import qualified Data.List as L+import Data.Text (Text) import Game.LambdaHack.Action import Game.LambdaHack.Actions import Game.LambdaHack.ItemAction import Game.LambdaHack.State import Game.LambdaHack.Command-import qualified Game.LambdaHack.Config as Config+import Game.LambdaHack.Config import qualified Game.LambdaHack.Key as K-import Game.LambdaHack.Utils.Assert -- | The semantics of player commands in terms of the @Action@ monad. cmdAction :: Cmd -> ActionFrame ()@@ -44,20 +45,14 @@ Help -> displayHelp -- | The associaction of commands to keys defined in config.-configCmds :: Config.CP -> [(K.Key, Cmd)]-configCmds config =- let section = Config.getItems config "commands"- mkKey s =- case K.keyTranslate s of- K.Unknown _ -> assert `failure` ("unknown command key <" ++ s ++ ">")- key -> key- mkCmd s = read s :: Cmd- mkCommand (key, def) = (mkKey key, mkCmd def)- in L.map mkCommand section+configCmds :: ConfigUI -> [(K.Key, Cmd)]+configCmds ConfigUI{configCommands} =+ let mkCommand (key, def) = (key, read def :: Cmd)+ in L.map mkCommand configCommands -- | The list of semantics and other info for all commands from config. semanticsCmds :: [(K.Key, Cmd)]- -> [((K.Key, K.Modifier), (String, Bool, ActionFrame ()))]+ -> [((K.Key, K.Modifier), (Text, Bool, ActionFrame ()))] semanticsCmds cmdList = let mkDescribed cmd = let semantics = if timedCmd cmd@@ -75,4 +70,4 @@ slid <- gets slid if creturnLn cursor == slid then h- else abortWith "this command does not work on remote levels"+ else abortWith "[targeting] you inspect a remote level, press ESC to switch back"
Game/LambdaHack/Config.hs view
@@ -1,57 +1,123 @@--- | Personal game configuration file support.+-- | Personal game configuration file type definitions. module Game.LambdaHack.Config- ( CP(..), toCP, forceEither, getOption, get, getItems+ ( Config(..), ConfigUI(..), FovMode(..) ) where -import qualified Data.ConfigFile as CF-import qualified Data.Binary as Binary--import Game.LambdaHack.Utils.Assert---- | The content of the configuration file. It's parsed--- in a case sensitive way (unlike by default in ConfigFile).-newtype CP = CP CF.ConfigParser+import Data.Text (Text)+import Data.Binary -instance Binary.Binary CP where- put (CP conf) = Binary.put $ CF.to_string conf- get = do- string <- Binary.get- let c = CF.readstring CF.emptyCP string- return $ toCP $ forceEither c+import qualified Game.LambdaHack.Key as K -instance Show CP where- show (CP conf) = show $ CF.to_string conf+-- TODO: should Blind really be a FovMode, or a modifier? Let's decide+-- when other similar modifiers are added.+-- | Field Of View scanning mode.+data FovMode =+ Shadow -- ^ restrictive shadow casting+ | Permissive -- ^ permissive FOV+ | Digital Int -- ^ digital FOV with the given radius+ | Blind -- ^ only feeling out adjacent tiles by touch+ deriving (Show, Read) --- | Switches all names to case sensitive (unlike by default in--- the "ConfigFile" library) and wraps in the constructor.-toCP :: CF.ConfigParser -> CP-toCP cf = CP $ cf {CF.optionxform = id}+-- | Fully typed contents of the rules config file.+data Config = Config+ { configSelfString :: !String+ -- caves+ , configCaves :: ![(Text, Text)]+ -- dungeon+ , configDepth :: !Int+ -- engine+ , configFovMode :: !FovMode+ , configSmellTimeout :: !Int+ -- heroes+ , configBaseHP :: !Int+ , configExtraHeroes :: !Int+ , configFirstDeathEnds :: !Bool+ , configFaction :: !Text+ } deriving Show --- | In case of corruption, just fail.-forceEither :: Show a => Either a b -> b-forceEither (Left a) = assert `failure` a-forceEither (Right b) = b+-- | Fully typed contents of the UI config file.+data ConfigUI = ConfigUI+ { -- commands+ configCommands :: ![(K.Key, String)] -- TODO: define Binary Cmd+ -- files+ , configAppDataDir :: !FilePath+ , configDiaryFile :: !FilePath+ , configSaveFile :: !FilePath+ , configBkpFile :: !FilePath+ , configScoresFile :: !FilePath+ , configRulesCfgFile :: !FilePath+ , configUICfgFile :: !FilePath+ -- heroNames+ , configHeroNames :: ![(Int, Text)]+ -- macros+ , configMacros :: ![(K.Key, K.Key)]+ -- ui+ , configFont :: !String+ , configHistoryMax :: !Int+ } deriving Show --- | A simplified access to an option in a given section,--- with simple error reporting (no internal errors are caught nor hidden).--- If there is no such option, gives Nothing.-getOption :: CF.Get_C a => CP -> CF.SectionSpec -> CF.OptionSpec -> Maybe a-getOption (CP conf) s o =- if CF.has_option conf s o- then Just $ forceEither $ CF.get conf s o- else Nothing+instance Binary FovMode where+ put Shadow = putWord8 0+ put Permissive = putWord8 1+ put (Digital r) = putWord8 2 >> put r+ put Blind = putWord8 3+ get = do+ tag <- getWord8+ case tag of+ 0 -> return Shadow+ 1 -> return Permissive+ 2 -> fmap Digital get+ 3 -> return Blind+ _ -> fail "no parse (FovMode)" --- | Simplified access to an option in a given section.--- Fails if the option is not present.-get :: CF.Get_C a => CP -> CF.SectionSpec -> CF.OptionSpec -> a-get (CP conf) s o =- if CF.has_option conf s o- then forceEither $ CF.get conf s o- else assert `failure` "Unknown config option: " ++ s ++ "." ++ o+instance Binary Config where+ put Config{..} = do+ put configSelfString+ put configCaves+ put configDepth+ put configFovMode+ put configSmellTimeout+ put configBaseHP+ put configExtraHeroes+ put configFirstDeathEnds+ put configFaction+ get = do+ configSelfString <- get+ configCaves <- get+ configDepth <- get+ configFovMode <- get+ configSmellTimeout <- get+ configBaseHP <- get+ configExtraHeroes <- get+ configFirstDeathEnds <- get+ configFaction <- get+ return Config{..} --- | An association list corresponding to a section. Fails if no such section.-getItems :: CP -> CF.SectionSpec -> [(String, String)]-getItems (CP conf) s =- if CF.has_section conf s- then forceEither $ CF.items conf s- else assert `failure` "Unknown config section: " ++ s+instance Binary ConfigUI where+ put ConfigUI{..} = do+ put configCommands+ put configAppDataDir+ put configDiaryFile+ put configSaveFile+ put configBkpFile+ put configRulesCfgFile+ put configUICfgFile+ put configScoresFile+ put configHeroNames+ put configMacros+ put configFont+ put configHistoryMax+ get = do+ configCommands <- get+ configAppDataDir <- get+ configDiaryFile <- get+ configSaveFile <- get+ configBkpFile <- get+ configScoresFile <- get+ configRulesCfgFile <- get+ configUICfgFile <- get+ configHeroNames <- get+ configMacros <- get+ configFont <- get+ configHistoryMax <- get+ return ConfigUI{..}
Game/LambdaHack/Content/ActorKind.hs view
@@ -5,6 +5,7 @@ import qualified Data.List as L import qualified Data.Ord as Ord+import Data.Text (Text) import Game.LambdaHack.Ability import Game.LambdaHack.Color@@ -17,7 +18,7 @@ -- | Actor properties that are fixed for a given kind of actors. data ActorKind = ActorKind { asymbol :: !Char -- ^ map symbol- , aname :: !String -- ^ short description+ , aname :: !Text -- ^ short description , afreq :: !Freqs -- ^ frequency within groups , acolor :: !Color -- ^ map color , aspeed :: !Speed -- ^ natural speed in m/s
Game/LambdaHack/Content/CaveKind.hs view
@@ -4,6 +4,8 @@ ) where import qualified Data.List as L+import Data.Text (Text)+import qualified Data.Text as T import Game.LambdaHack.PointXY import Game.LambdaHack.Random@@ -12,7 +14,7 @@ -- | Parameters for the generation of dungeon levels. data CaveKind = CaveKind { csymbol :: Char -- ^ a symbol- , cname :: String -- ^ short description+ , cname :: Text -- ^ short description , cfreq :: Freqs -- ^ frequency within groups , cxsize :: X -- ^ X size of the whole cave , cysize :: Y -- ^ Y size of the whole cave@@ -27,12 +29,12 @@ , copenChance :: Chance -- ^ if there's a door, is it open? , chiddenChance :: Chance -- ^ if not open, is it hidden? , citemNum :: RollDice -- ^ the number of items in the cave- , cdefaultTile :: String -- ^ the default cave tile group name- , ccorridorTile :: String -- ^ the cave corridor tile group name- , cfillerTile :: String -- ^ the filler wall group name- , cdarkLegendTile :: String -- ^ the dark place plan legend ground name- , clitLegendTile :: String -- ^ the lit place plan legend ground name- , chiddenTile :: String -- ^ the hidden tiles ground name+ , cdefaultTile :: Text -- ^ the default cave tile group name+ , ccorridorTile :: Text -- ^ the cave corridor tile group name+ , cfillerTile :: Text -- ^ the filler wall group name+ , cdarkLegendTile :: Text -- ^ the dark place plan legend ground name+ , clitLegendTile :: Text -- ^ the lit place plan legend ground name+ , chiddenTile :: Text -- ^ the hidden tiles ground name } deriving Show -- No Eq and Ord to make extending it logically sound, see #53 @@ -49,6 +51,6 @@ (maxPlaceSizeX, maxPlaceSizeY) = (maxDice mx, maxDice my) xborder = if maxGridX == 1 then 5 else 3 yborder = if maxGridX == 1 then 5 else 3- in length cname <= 25+ in T.length cname <= 25 && (maxGridX * (xborder + maxPlaceSizeX) + 1 > cxsize || maxGridY * (yborder + maxPlaceSizeY) + 1 > cysize))
Game/LambdaHack/Content/FactionKind.hs view
@@ -4,16 +4,17 @@ ) where import Game.LambdaHack.Misc+import Data.Text (Text) -- | Faction properties that are fixed for a given kind of factions. data FactionKind = FactionKind { fsymbol :: !Char -- ^ a symbol- , fname :: !String -- ^ short description+ , fname :: !Text -- ^ short description , ffreq :: !Freqs -- ^ frequency within groups- , fAiSelected :: !String -- ^ Ai to use for the selected actor- , fAiIdle :: !String -- ^ Ai to use for idle actors- , fenemy :: ![String] -- ^ initially in war with these factions- , fally :: ![String] -- ^ initially allied with these factions+ , fAiSelected :: !Text -- ^ Ai to use for the selected actor+ , fAiIdle :: !Text -- ^ Ai to use for idle actors+ , fenemy :: ![Text] -- ^ initially in war with these factions+ , fally :: ![Text] -- ^ initially allied with these factions } deriving Show
Game/LambdaHack/Content/ItemKind.hs view
@@ -1,8 +1,12 @@+{-# LANGUAGE OverloadedStrings #-} -- | The type of kinds of weapons and treasure. module Game.LambdaHack.Content.ItemKind ( ItemKind(..), ivalidate ) where +import Data.Text (Text)+import qualified NLP.Miniutter.English as MU+ import Game.LambdaHack.Effect import Game.LambdaHack.Flavour import Game.LambdaHack.Random@@ -18,17 +22,17 @@ -- etc., so if we make ipower complex, the value computation gets complex too. -- | Item properties that are fixed for a given kind of items. data ItemKind = ItemKind- { isymbol :: !Char -- ^ map symbol- , iname :: !String -- ^ generic name- , ifreq :: !Freqs -- ^ frequency within groups- , iflavour :: ![Flavour] -- ^ possible flavours- , ieffect :: !Effect -- ^ the effect when activated- , icount :: !RollDeep -- ^ created in that quantify- , ipower :: !RollDeep -- ^ created with that power- , iverbApply :: !String -- ^ the verb for applying and possibly combat- , iverbProject :: !String -- ^ the verb for projecting- , iweight :: !Int -- ^ weight in grams- , itoThrow :: !Int -- ^ percentage bonus or malus to throw speed+ { isymbol :: !Char -- ^ map symbol+ , iname :: !Text -- ^ generic name+ , ifreq :: !Freqs -- ^ frequency within groups+ , iflavour :: ![Flavour] -- ^ possible flavours+ , ieffect :: !Effect -- ^ the effect when activated+ , icount :: !RollDeep -- ^ created in that quantify+ , ipower :: !RollDeep -- ^ created with that power+ , iverbApply :: !MU.Part -- ^ the verb for applying and possibly combat+ , iverbProject :: !MU.Part -- ^ the verb for projecting+ , iweight :: !Int -- ^ weight in grams+ , itoThrow :: !Int -- ^ percentage bonus or malus to throw speed } deriving Show -- No Eq and Ord to make extending it logically sound, see #53
Game/LambdaHack/Content/PlaceKind.hs view
@@ -4,16 +4,19 @@ ) where import qualified Data.List as L+import Data.Text (Text)+import qualified Data.Text as T+ import Game.LambdaHack.Misc -- | Parameters for the generation of small areas within a dungeon level. data PlaceKind = PlaceKind { psymbol :: Char -- ^ a symbol- , pname :: String -- ^ short description+ , pname :: Text -- ^ short description , pfreq :: Freqs -- ^ frequency within groups , pcover :: Cover -- ^ how to fill whole place based on the corner , pfence :: Fence -- ^ whether to fence the place with solid border- , ptopLeft :: [String] -- ^ plan of the top-left corner of the place+ , ptopLeft :: [Text] -- ^ plan of the top-left corner of the place } deriving Show -- No Eq and Ord to make extending it logically sound, see #53 @@ -42,5 +45,7 @@ -- Verify that the top-left corner map is rectangular and not empty. pvalidate :: [PlaceKind] -> [PlaceKind] pvalidate = L.filter (\ PlaceKind{..} ->- let dxcorner = case ptopLeft of [] -> 0 ; l : _ -> L.length l- in dxcorner /= 0 && L.any (/= dxcorner) (L.map L.length ptopLeft))+ let dxcorner = case ptopLeft of+ [] -> 0+ l : _ -> T.length l+ in dxcorner /= 0 && L.any (/= dxcorner) (L.map T.length ptopLeft))
Game/LambdaHack/Content/RuleKind.hs view
@@ -4,6 +4,7 @@ ) where import Data.Version+import Data.Text (Text) import Game.LambdaHack.PointXY import Game.LambdaHack.Content.TileKind@@ -26,17 +27,18 @@ -- whether one location is accessible from another. -- Precondition: the two locations are next to each other. data RuleKind = RuleKind- { rsymbol :: Char -- ^ a symbol- , rname :: String -- ^ short description- , rfreq :: Freqs -- ^ frequency within groups- , raccessible :: X -> Point -> TileKind -> Point -> TileKind -> Bool- , rtitle :: String -- ^ the title of the game- , rpathsDataFile :: FilePath -> IO FilePath -- ^ the path to data files- , rpathsVersion :: Version -- ^ the version of the game- , ritemMelee :: [Char] -- ^ symbols of melee weapons- , ritemProject :: [Char] -- ^ symbols of items AI can project- , rconfigDefault :: String -- ^ the default config file of the game- , rmainMenuArt :: String -- ^ the ASCII art for the Main Menu+ { rsymbol :: Char -- ^ a symbol+ , rname :: Text -- ^ short description+ , rfreq :: Freqs -- ^ frequency within groups+ , raccessible :: X -> Point -> TileKind -> Point -> TileKind -> Bool+ , rtitle :: Text -- ^ the title of the game+ , rpathsDataFile :: FilePath -> IO FilePath -- ^ the path to data files+ , rpathsVersion :: Version -- ^ the version of the game+ , ritemMelee :: [Char] -- ^ symbols of melee weapons+ , ritemProject :: [Char] -- ^ symbols of items AI can project+ , rcfgRulesDefault :: String -- ^ the default game rules config file+ , rcfgUIDefault :: String -- ^ the default UI settings config file+ , rmainMenuArt :: Text -- ^ the ASCII art for the Main Menu } -- | A dummy instance of the 'Show' class, to satisfy general requirments
Game/LambdaHack/Content/StrategyKind.hs view
@@ -3,13 +3,15 @@ ( StrategyKind(..), svalidate ) where +import Data.Text (Text)+ import Game.LambdaHack.Ability import Game.LambdaHack.Misc -- | Strategy properties that are fixed for a given kind of strategies. data StrategyKind = StrategyKind { ssymbol :: !Char -- ^ a symbol- , sname :: !String -- ^ short description+ , sname :: !Text -- ^ short description , sfreq :: !Freqs -- ^ frequency within groups , sabilities :: ![Ability] -- ^ abilities to pick from in roughly that order }
Game/LambdaHack/Content/TileKind.hs view
@@ -5,6 +5,7 @@ import qualified Data.List as L import qualified Data.Map as M+import Data.Text (Text) import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Color@@ -16,7 +17,7 @@ -- particular concrete tiles in the dungeon. data TileKind = TileKind { tsymbol :: !Char -- ^ map symbol- , tname :: !String -- ^ short description+ , tname :: !Text -- ^ short description , tfreq :: !Freqs -- ^ frequency within groups , tcolor :: !Color -- ^ map color , tcolor2 :: !Color -- ^ map color when not in FOV
Game/LambdaHack/Draw.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | Display game data on the screen using one of the available frontends -- (determined at compile time with cabal flags). {-# LANGUAGE CPP #-}@@ -9,6 +10,8 @@ import qualified Data.List as L import qualified Data.IntMap as IM import Data.Maybe+import Data.Text (Text)+import qualified Data.Text as T import Game.LambdaHack.Msg import qualified Game.LambdaHack.Color as Color@@ -29,9 +32,9 @@ import qualified Game.LambdaHack.Item as Item import Game.LambdaHack.Random import qualified Game.LambdaHack.Kind as Kind-import Game.LambdaHack.FOV import qualified Game.LambdaHack.Feature as F import Game.LambdaHack.Time+import Game.LambdaHack.Config -- | Color mode for the display. data ColorMode =@@ -74,8 +77,8 @@ (_, wealth) = calculateTotal coitem s damage = case Item.strongestSword cops bitems of Just sw -> case ieffect $ iokind $ Item.jkind sw of- Wound dice -> show dice ++ "+" ++ show (Item.jpower sw)- _ -> show (Item.jpower sw)+ Wound dice -> showT dice <> "+" <> showT (Item.jpower sw)+ _ -> showT (Item.jpower sw) Nothing -> "3d1" -- TODO; use the item 'fist' bl = fromMaybe [] $ bla lxsize lysize ceps bloc clocation dis pxy =@@ -142,31 +145,31 @@ _ -> Color.AttrChar a char seenN = 100 * lseen `div` lclear seenTxt | seenN == 100 = "all"- | otherwise = (reverse $ take 2 $ reverse $ " " ++ show seenN)- ++ "%"+ | otherwise = T.justifyRight 2 ' ' (showT seenN) <> "%" -- Indicate the actor is braced (was waiting last move). -- It's a useful feedback for the otherwise hard to observe -- 'wait' command. braceSign | braced mpl ltime = "{" | otherwise = " "- status =- take 31 (take 3 (show (Dungeon.levelNumber slid) ++ " ")- ++ ldesc ++ repeat ' ') ++- take 12 ("[" ++ seenTxt ++ " seen] ") ++- take 10 ("$: " ++ show wealth ++ repeat ' ') ++- take 12 ("Dmg: " ++ damage ++ repeat ' ') ++- take 14 (braceSign ++ "HP: " ++ show bhp ++- " (" ++ show (maxDice ahp) ++ ")" ++ repeat ' ')- toWidth :: Int -> String -> String- toWidth n x = take n (x ++ repeat ' ')+ lvlN = T.justifyLeft 2 ' ' (showT $ Dungeon.levelNumber slid)+ stats =+ T.justifyLeft 11 ' ' ("[" <> seenTxt <+> "seen]") <+>+ T.justifyLeft 9 ' ' ("$:" <+> showT wealth) <+>+ T.justifyLeft 11 ' ' ("Dmg:" <+> damage) <+>+ T.justifyLeft 13 ' ' (braceSign <> "HP:" <+> showT bhp+ <+> "(" <> showT (maxDice ahp) <> ")")+ widthForDesc = lxsize - T.length stats - T.length lvlN - 3+ status = lvlN <+> T.justifyLeft widthForDesc ' ' ldesc <+> stats+ toWidth :: Int -> Text -> Text+ toWidth n x = T.take n (T.justifyLeft n ' ' x) fLine y = let f l x = let !ac = dis (PointXY (x, y)) in ac : l in L.foldl' f [] [lxsize-1,lxsize-2..0] sfLevel = -- Fully evaluated. let f l y = let !line = fLine y in line : l in L.foldl' f [] [lysize-1,lysize-2..0]- sfTop = toWidth lxsize msgTop- sfBottom = toWidth lxsize $ fromMaybe status msgBottom+ sfTop = toWidth lxsize $ msgTop+ sfBottom = toWidth (lxsize - 1) $ fromMaybe status $ msgBottom in SingleFrame{..} -- | Render animations on top of the current screen frame.
Game/LambdaHack/DungeonState.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | Dungeon operations that require 'State', 'Kind.COps' -- or 'Config.Config' type. module Game.LambdaHack.DungeonState@@ -14,13 +15,14 @@ import qualified Data.IntMap as IM import Data.Maybe import Control.Monad+import Data.Text (Text) import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Point import Game.LambdaHack.Level import qualified Game.LambdaHack.Dungeon as Dungeon import Game.LambdaHack.Random-import qualified Game.LambdaHack.Config as Config+import Game.LambdaHack.Config import Game.LambdaHack.State import qualified Game.LambdaHack.Feature as F import qualified Game.LambdaHack.Tile as Tile@@ -34,6 +36,7 @@ import qualified Game.LambdaHack.Effect as Effect import Game.LambdaHack.Content.ItemKind import Game.LambdaHack.Time+import Game.LambdaHack.Msg convertTileMaps :: Rnd (Kind.Id TileKind) -> Int -> Int -> TileMapXY -> Rnd TileMap@@ -122,20 +125,20 @@ , ldesc = cname , lmeta = dmeta , lstairs = (su, sd)- , ltime = timeAdd timeTurn timeTurn -- just stepped into the dungeon+ , ltime = timeTurn , lclear , lseen = 0 } return level -matchGenerator :: Kind.Ops CaveKind -> Maybe String -> Rnd (Kind.Id CaveKind)+matchGenerator :: Kind.Ops CaveKind -> Maybe Text -> Rnd (Kind.Id CaveKind) matchGenerator Kind.Ops{opick} mname = opick (fromMaybe "dng" mname) (const True) -findGenerator :: Kind.COps -> Config.CP -> Int -> Int -> Rnd Level-findGenerator cops config k depth = do- let ln = "LambdaCave_" ++ show k- genName = Config.getOption config "dungeon" ln+findGenerator :: Kind.COps -> Config -> Int -> Int -> Rnd Level+findGenerator cops Config{configCaves} k depth = do+ let ln = "LambdaCave_" <> showT k+ genName = L.lookup ln configCaves ci <- matchGenerator (Kind.cocave cops) genName cave <- buildCave cops k depth ci buildLevel cops cave k depth@@ -148,20 +151,19 @@ } -- | Generate the dungeon for a new game.-generate :: Kind.COps -> Config.CP -> Rnd FreshDungeon-generate cops config =- let depth = Config.get config "dungeon" "depth"- gen :: R.StdGen -> Int -> (R.StdGen, (Dungeon.LevelId, Level))+generate :: Kind.COps -> Config -> Rnd FreshDungeon+generate cops config@Config{configDepth} =+ let gen :: R.StdGen -> Int -> (R.StdGen, (Dungeon.LevelId, Level)) gen g k = let (g1, g2) = R.split g- res = MState.evalState (findGenerator cops config k depth) g1+ res = MState.evalState (findGenerator cops config k configDepth) g1 in (g2, (Dungeon.levelDefault k, res)) con :: R.StdGen -> (FreshDungeon, R.StdGen)- con g = assert (depth >= 1 `blame` depth) $- let (gd, levels) = L.mapAccumL gen g [1..depth]+ con g = assert (configDepth >= 1 `blame` configDepth) $+ let (gd, levels) = L.mapAccumL gen g [1..configDepth] entryLevel = Dungeon.levelDefault 1 entryLoc = fst (lstairs (snd (head levels)))- freshDungeon = Dungeon.fromList levels depth+ freshDungeon = Dungeon.fromList levels configDepth in (FreshDungeon{..}, gd) in MState.state con
Game/LambdaHack/Effect.hs view
@@ -1,10 +1,14 @@+{-# LANGUAGE OverloadedStrings #-} -- | Effects of content on other content. No operation in this module -- involves the 'State' or 'Action' type. module Game.LambdaHack.Effect ( Effect(..), effectToSuffix, effectToBenefit ) where +import Data.Text (Text)+ import Game.LambdaHack.Random+import Game.LambdaHack.Msg -- TODO: document each constructor -- | All possible effects, some of them parameterized or dependent@@ -24,13 +28,13 @@ deriving (Show, Read, Eq, Ord) -- | Suffix to append to a basic content name, if the content causes the effect.-effectToSuffix :: Effect -> String+effectToSuffix :: Effect -> Text effectToSuffix NoEffect = "" effectToSuffix Heal = "of healing" effectToSuffix (Wound dice@(RollDice a b)) = if a == 0 && b == 0 then "of wounding"- else "(" ++ show dice ++ ")"+ else "(" <> showT dice <> ")" effectToSuffix Dominate = "of domination" effectToSuffix SummonFriend = "of aid calling" effectToSuffix SummonEnemy = "of summoning"
Game/LambdaHack/EffectAction.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE OverloadedStrings, ExtendedDefaultRules #-}+{-# OPTIONS_GHC -fno-warn-type-defaults #-} -- | The effectToAction function and all it depends on. -- This file should not depend on Actions.hs nor ItemAction.hs. -- TODO: Add an export list and document after it's rewritten according to #17.@@ -11,7 +13,9 @@ import qualified Data.IntMap as IM import qualified Data.Set as S import qualified Data.IntSet as IS-import Data.Monoid+import Data.Monoid (mempty)+import Data.Text (Text)+import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Action@@ -19,7 +23,6 @@ import Game.LambdaHack.ActorState import Game.LambdaHack.Content.ActorKind import Game.LambdaHack.Draw-import Game.LambdaHack.Grammar import Game.LambdaHack.Point import Game.LambdaHack.Item import Game.LambdaHack.Content.ItemKind@@ -30,7 +33,7 @@ import Game.LambdaHack.Random import Game.LambdaHack.State import Game.LambdaHack.Time-import qualified Game.LambdaHack.Config as Config+import Game.LambdaHack.Config import qualified Game.LambdaHack.Effect as Effect import qualified Game.LambdaHack.Kind as Kind import Game.LambdaHack.DungeonState@@ -38,6 +41,13 @@ import Game.LambdaHack.Animation (twirlSplash, blockHit, deathBody) import qualified Game.LambdaHack.Dungeon as Dungeon +default (Text)++-- | Sentences such as \"Dog barks loudly.\"+actorVerb :: Kind.Ops ActorKind -> Actor -> Text -> Text+actorVerb coactor a v =+ makeSentence [MU.SubjectVerbSg (partActor coactor a) (MU.Text v)]+ -- | Invoke pseudo-random computation with the generator kept in the state. rndToAction :: Rnd a -> Action a rndToAction r = do@@ -136,7 +146,7 @@ -- and the string part describes how the target reacted -- (not what the source did). eff :: Effect.Effect -> Int -> ActorId -> ActorId -> Int- -> Action (Bool, String)+ -> Action (Bool, Text) eff Effect.NoEffect _ _ _ _ = nullEffect eff Effect.Heal _ _source target power = do Kind.COps{coactor=coactor@Kind.Ops{okind}} <- getCOps@@ -147,7 +157,7 @@ else do void $ focusIfOurs target updateAnyActor target (addHp coactor power)- return (True, actorVerb coactor tm "feel" "better")+ return (True, actorVerb coactor tm "feel better") eff (Effect.Wound nDm) verbosity source target power = do Kind.COps{coactor} <- getCOps s <- get@@ -164,15 +174,14 @@ else -- Not as important, so let the player read the message -- about monster death while he watches the combat animation. if isProjectile s target- then actorVerb coactor tm "drop" "down"- else actorVerb coactor tm "die" ""+ then actorVerb coactor tm "drop down"+ else actorVerb coactor tm "die" | source == target = -- a potion of wounding, etc.- actorVerb coactor tm "feel" "wounded"+ actorVerb coactor tm "feel wounded" | verbosity <= 0 = "" | target == pl =- actorVerb coactor tm "lose" $- show (n + power) ++ "HP"- | otherwise = actorVerb coactor tm "hiss" "in pain"+ actorVerb coactor tm $ "lose" <+> showT (n + power) <> "HP"+ | otherwise = actorVerb coactor tm "hiss in pain" updateAnyActor target $ \ m -> m { bhp = newHP } -- Damage the target. return (True, msg) eff Effect.Dominate _ source target _power = do@@ -237,7 +246,7 @@ s2 <- get return $ if maybe Camping snd (squit s2) == Victor then (True, "")- else (True, actorVerb coactor tm "find" "a way upstairs")+ else (True, actorVerb coactor tm "find a way upstairs") eff Effect.Descend _ source target power = do tm <- gets (getActor target) s <- get@@ -249,9 +258,9 @@ s2 <- get return $ if maybe Camping snd (squit s2) == Victor then (True, "")- else (True, actorVerb coactor tm "find" "a way downstairs")+ else (True, actorVerb coactor tm "find a way downstairs") -nullEffect :: Action (Bool, String)+nullEffect :: Action (Bool, Text) nullEffect = return (False, "Nothing happens.") -- TODO: refactor with actorAttackActor.@@ -264,7 +273,10 @@ power = maxDeep $ ipower $ okind h2hKind h2h = Item h2hKind power Nothing 1 verb = iverbApply $ okind h2hKind- msg = actorVerbActor coactor sm verb tm "in a staircase accident"+ msg = makeSentence+ [ MU.SubjectVerbSg (partActor coactor sm) verb+ , partActor coactor tm+ , "in a staircase accident" ] msgAdd msg itemEffectAction 0 source target h2h False s <- get@@ -348,7 +360,7 @@ -- | The player leaves the dungeon. fleeDungeon :: Action () fleeDungeon = do- Kind.COps{coitem} <- getCOps+ Kind.COps{coitem=coitem@Kind.Ops{oname, ouniqGroup}} <- getCOps s <- get go <- displayYesNo "This is the way out. Really leave now?" recordHistory -- Prevent repeating the ending msgs.@@ -365,8 +377,12 @@ "This time try to grab some loot before escape!" when (not go2) $ abortWith "Here's your chance!" else do- let winMsg = "Congratulations, you won! Here's your loot, worth " ++- show total ++ " gold." -- TODO: use the name of the '$' item instead+ let currencyName = MU.Text $ oname $ ouniqGroup "currency"+ winMsg = makePhrase+ [ "Congratulations, you won!"+ , "Here's your loot, worth"+ , MU.NWs total currencyName+ , "." ] io <- itemOverlay True True items tryIgnore $ displayOverAbort winMsg io modify (\ st -> st {squit = Just (True, Victor)})@@ -397,15 +413,16 @@ Kind.COps{coitem=coitem@Kind.Ops{okind}} <- getCOps state <- get let ik = jkind i- obj = unwords $ tail $ words $ objectItem coitem state i- msg = "The " ++ obj ++ " turns out to be " kind = okind ik alreadyIdentified = L.length (iflavour kind) == 1 || ik `S.member` sdisco state unless alreadyIdentified $ do modify (updateDiscoveries (S.insert ik)) state2 <- get- msgAdd $ msg ++ objectItem coitem state2 i ++ "."+ let msg = makeSentence+ [ "the", MU.SubjectVerbSg (partItem coitem state i) "turn out to be"+ , MU.AW $ partItem coitem state2 i ]+ msgAdd msg -- | Make the actor controlled by the player. Switch level, if needed. -- False, if nothing to do. Should only be invoked as a direct result@@ -430,7 +447,7 @@ -- Don't continue an old run, if any. stopRunning -- Announce.- msgAdd $ capActor coactor pbody ++ " selected."+ msgAdd $ makeSentence [partActor coactor pbody, "selected"] msgAdd $ lookAt cops False True state lvl (bloc pbody) "" return True @@ -449,7 +466,8 @@ assert (n > 0) $ do cops <- getCOps newHeroId <- gets scounter- modify (\ state -> iterate (addHero cops loc) state !! n)+ configUI <- getConfigUI+ modify (\ state -> iterate (addHero cops loc configUI) state !! n) b <- focusIfOurs newHeroId assert (b `blame` (newHeroId, "player summons himself")) $ return ()@@ -479,13 +497,12 @@ ahs <- gets allHeroesAnyLevel pl <- gets splayer pbody <- gets getPlayerBody- config <- gets sconfig+ Config{configFirstDeathEnds} <- gets sconfig when (bhp pbody <= 0) $ do- msgAdd $ actorVerb coactor pbody "die" ""+ msgAdd $ actorVerb coactor pbody "die" go <- displayMore ColorBW "" recordHistory -- Prevent repeating the "die" msgs.- let firstDeathEnds = Config.get config "heroes" "firstDeathEnds"- bodyToCorpse = updateAnyActor pl $ \ body -> body {bsymbol = Just '%'}+ let bodyToCorpse = updateAnyActor pl $ \ body -> body {bsymbol = Just '%'} animateDeath = do diary <- getDiary s <- get@@ -495,7 +512,7 @@ animateDeath bodyToCorpse gameOver go- if firstDeathEnds+ if configFirstDeathEnds then animateGameOver else case L.filter (/= pl) ahs of [] -> animateGameOver@@ -521,7 +538,7 @@ slid <- gets slid modify (\ st -> st {squit = Just (False, Killed slid)}) when showEndingScreens $ do- Kind.COps{coitem} <- getCOps+ Kind.COps{coitem=coitem@Kind.Ops{oname, ouniqGroup}} <- getCOps s <- get dng <- gets sdungeon time <- gets stime@@ -540,8 +557,12 @@ "That is your name. 'Almost'." | otherwise = "Dead heroes make better legends."- loseMsg = failMsg ++ " You left " ++- show total ++ " gold and some junk." -- TODO: use the name of the '$' item instead+ currencyName = MU.Text $ oname $ ouniqGroup "currency"+ loseMsg = makePhrase+ [ failMsg+ , "You left"+ , MU.NWs total currencyName+ , "and some junk." ] if null items then modify (\ st -> st {squit = Just (True, Killed slid)}) else do@@ -554,14 +575,14 @@ itemOverlay ::Bool -> Bool -> [Item] -> Action [Overlay] itemOverlay sorted cheat is = do Kind.COps{coitem} <- getCOps- state <- get+ s <- get lysize <- gets (lysize . slevel)- let inv = L.map (\ i -> letterLabel (jletter i)- ++ objectItemCheat coitem cheat state i ++ " ")- ((if sorted- then L.sortBy (cmpLetterMaybe `on` jletter)- else id) is)- return $ splitOverlay lysize inv+ let items | sorted = L.sortBy (cmpLetterMaybe `on` jletter) is+ | otherwise = is+ pr i = makePhrase [ letterLabel (jletter i)+ , MU.NWs (jcount i) $ partItemCheat cheat coitem s i ]+ <> " "+ return $ splitOverlay lysize $ L.map pr items stopRunning :: Action () stopRunning = updatePlayerBody (\ p -> p { bdir = Nothing })@@ -582,19 +603,18 @@ let canSee = IS.member loc (totalVisible per) ihabitant | canSee = L.find (\ m -> bloc m == loc) (IM.elems hms) | otherwise = Nothing- monsterMsg =- maybe "" (\ m -> actorVerb coactor m "be" "here" ++ " ") ihabitant+ monsterMsg = maybe "" (\ m -> actorVerb coactor m "be here") ihabitant vis | not $ loc `IS.member` totalVisible per = " (not visible)" -- by party | actorReachesLoc pl loc per (Just pl) = "" | otherwise = " (not reachable)" -- by hero mode = case target of- TEnemy _ _ -> "[targeting monster" ++ vis ++ "] "- TLoc _ -> "[targeting location" ++ vis ++ "] "- TPath _ -> "[targeting path" ++ vis ++ "] "- TCursor -> "[targeting current" ++ vis ++ "] "+ TEnemy _ _ -> "[targeting monster" <> vis <> "]"+ TLoc _ -> "[targeting location" <> vis <> "]"+ TPath _ -> "[targeting path" <> vis <> "]"+ TCursor -> "[targeting current" <> vis <> "]" -- Show general info about current loc.- lookMsg = mode ++ lookAt cops True canSee state lvl loc monsterMsg+ lookMsg = mode <+> lookAt cops True canSee state lvl loc monsterMsg -- Check if there's something lying around at current loc. is = lvl `rememberAtI` loc io <- itemOverlay False False is
Game/LambdaHack/FOV.hs view
@@ -2,7 +2,7 @@ -- See <https://github.com/kosmikus/LambdaHack/wiki/Fov-and-los> -- for discussion. module Game.LambdaHack.FOV- ( FovMode(..), fullscan+ ( fullscan ) where import qualified Data.List as L@@ -18,16 +18,7 @@ import qualified Game.LambdaHack.Kind as Kind import Game.LambdaHack.Content.TileKind import qualified Game.LambdaHack.Tile as Tile---- TODO: should Blind really be a FovMode, or a modifier? Let's decide--- when other similar modifiers are added.--- | Field Of View scanning mode.-data FovMode =- Shadow -- ^ restrictive shadow casting- | Permissive -- ^ permissive FOV- | Digital Int -- ^ digital FOV with the given radius- | Blind -- ^ only feeling out adjacent tiles by touch- deriving Show+import Game.LambdaHack.Config -- | Perform a full scan for a given location. Returns the locations -- that are currently in the field of view. The Field of View
Game/LambdaHack/Feature.hs view
@@ -3,6 +3,8 @@ ( Feature(..) ) where +import Data.Text (Text)+ import Game.LambdaHack.Effect import Game.LambdaHack.Random @@ -16,7 +18,7 @@ | Hidden -- ^ triggered when the tile's secrecy becomes zero | Cause !Effect -- ^ causes the effect when triggered- | ChangeTo !String -- ^ transitions to any tile of the group when triggered+ | ChangeTo !Text -- ^ transitions to any tile of the group when triggered | Walkable -- ^ actors can walk through | Clear -- ^ actors can see through
Game/LambdaHack/Flavour.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | The appearance of in-game items, as communicated to the player. module Game.LambdaHack.Flavour ( -- * The @Flavour@ type@@ -10,6 +11,7 @@ import qualified Data.List as L import Data.Binary+import Data.Text (Text) import Game.LambdaHack.Color @@ -50,12 +52,12 @@ flavourToColor Flavour{baseColor} = baseColor -- | Construct the full name of a flavour.-flavourToName :: Flavour -> String+flavourToName :: Flavour -> Text flavourToName Flavour{..} | fancyName = colorToFancyName baseColor flavourToName Flavour{..} = colorToPlainName baseColor -- | Human-readable names, for item colors. The simple set.-colorToPlainName :: Color -> String+colorToPlainName :: Color -> Text colorToPlainName Black = "black" colorToPlainName Red = "red" colorToPlainName Green = "green"@@ -74,7 +76,7 @@ colorToPlainName BrWhite = "white" -- | Human-readable names, for item colors. The fancy set.-colorToFancyName :: Color -> String+colorToFancyName :: Color -> Text colorToFancyName Black = "smoky black" colorToFancyName Red = "apple red" colorToFancyName Green = "forest green"
− Game/LambdaHack/Grammar.hs
@@ -1,258 +0,0 @@--- | Construct English sentences from content.-module Game.LambdaHack.Grammar- ( -- * Grammar types- Verb, Object- -- * General operations- , capitalize, pluralise, addIndefinite, conjugate- -- * Objects from content- , objectItemCheat, objectItem, objectActor, capActor- -- * Sentences- , actorVerb, actorVerbItem, actorVerbActor, actorVerbExtraItem- -- * Scenery description- , lookAt- ) where--import Data.Char-import qualified Data.Set as S-import qualified Data.Map as M-import qualified Data.List as L-import Data.Maybe--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Point-import Game.LambdaHack.Item-import Game.LambdaHack.Actor-import Game.LambdaHack.Level-import Game.LambdaHack.State-import Game.LambdaHack.Content.ItemKind-import Game.LambdaHack.Content.ActorKind-import Game.LambdaHack.Effect-import Game.LambdaHack.Flavour-import qualified Game.LambdaHack.Kind as Kind---- | The type of verbs.-type Verb = String---- | The grammatical object type.-type Object = String---- | Nouns with irregular plural spelling.--- See http://en.wikipedia.org/wiki/English_plural.-irregularPlural :: M.Map String String-irregularPlural = M.fromList- [ ("canto" , "cantos")- , ("homo " , "homos")- , ("photo" , "photos")- , ("zero" , "zeros")- , ("piano" , "pianos")- , ("portico", "porticos")- , ("pro" , "pros")- , ("quarto" , "quartos")- , ("kimono" , "kimonos")- , ("calf" , "calves")- , ("leaf" , "leaves")- , ("knife" , "knives")- , ("life" , "lives")- , ("dwarf" , "dwarves")- , ("hoof" , "hooves")- , ("elf" , "elves")- , ("staff" , "staves")- , ("child" , "children")- , ("foot" , "feet")- , ("goose" , "geese")- , ("louse" , "lice")- , ("man" , "men")- , ("mouse" , "mice")- , ("tooth" , "teeth")- , ("woman" , "women")- ]---- | The list of words with identical singular and plural form.--- See http://en.wikipedia.org/wiki/English_plural.-noPlural :: S.Set String-noPlural = S.fromList- [ "buffalo"- , "deer"- , "moose"- , "sheep"- , "bison"- , "salmon"- , "pike"- , "trout"- , "swine"- , "aircraft"- , "watercraft"- , "spacecraft"- , "hovercraft"- , "information"- ]---- | Tests if a character is a vowel (@u@ is too hard, so is @eu@).-vowel :: Char -> Bool-vowel l = l `elem` "aeio"--compound :: Bool -> (String -> String) -> String -> String-compound modifyFirst f phrase =- let rev | modifyFirst = reverse- | otherwise = id- in case rev $ words phrase of- [] -> assert `failure` "compound: no words"- word : rest -> unwords $ rev $ f word : rest---- | Adds the plural (@s@, @es@, @ies@) suffix to a word.--- Used also for conjugation.--- See http://en.wikipedia.org/wiki/English_plural.-suffixS :: String -> String-suffixS = compound False singleSuffixS--singleSuffixS :: String -> String-singleSuffixS word = case L.reverse word of- 'h' : 'c' : _ -> word ++ "es"- 'h' : 's' : _ -> word ++ "es"- 'i' : 's' : _ -> word ++ "es"- 's' : _ -> word ++ "es"- 'z' : _ -> word ++ "es"- 'x' : _ -> word ++ "es"- 'j' : _ -> word ++ "es"- 'o' : l : _ | not (vowel l) -> init word ++ "es"- 'y' : l : _ | not (vowel l) -> init word ++ "ies"- _ -> word ++ "s"--pluralise :: Object -> Object-pluralise = compound True singlePluralise---- TODO: a suffix tree would be best, to catch ableman, seaman, etc.-singlePluralise :: Object -> Object-singlePluralise word =- if word `S.member` noPlural- then word- else case M.lookup word irregularPlural of- Just plural -> plural- Nothing -> suffixS word--conjugate :: String -> Verb -> Verb-conjugate "you" "be" = "are"-conjugate "You" "be" = "are"-conjugate _ "be" = "is"-conjugate "you" verb = verb-conjugate "You" verb = verb-conjugate _ verb = suffixS verb---- | Capitalize a string.-capitalize :: Object -> Object-capitalize [] = []-capitalize (c : cs) = toUpper c : cs---- | Add the indefinite article (@a@, @an@) to a word (@h@ is too hard).-addIndefinite :: Object -> Object-addIndefinite b = case b of- c : _ | vowel c -> "an " ++ b- _ -> "a " ++ b---- | Transform an object, adding a count and a plural suffix.-makeObject :: Int -> (Object -> Object) -> Object -> Object-makeObject 1 f obj = addIndefinite $ f obj-makeObject n f obj = show n ++ " " ++ f (pluralise obj)---- TODO: when there's more of the above, split and move to Utils/---- | How to refer to an item in object position of a sentence.--- If cheating is allowed, full identity of the object is revealed--- together with its flavour (e.g. at game over screen).-objectItemCheat :: Kind.Ops ItemKind -> Bool -> State -> Item -> Object-objectItemCheat coitem@Kind.Ops{okind} cheat state i =- let ik = jkind i- kind = okind ik- identified = L.length (iflavour kind) == 1 ||- ik `S.member` sdisco state- addSpace s = if s == "" then "" else " " ++ s- eff = effectToSuffix (ieffect kind)- pwr = if jpower i == 0 then "" else "(+" ++ show (jpower i) ++ ")"- adj name =- let known = name ++ addSpace eff ++ addSpace pwr- flavour = getFlavour coitem (sflavour state) ik- obscured = flavourToName flavour ++ " " ++ name- in if identified- then known- else if cheat- then flavourToName flavour ++ " " ++ known- else obscured- in makeObject (jcount i) adj (iname kind)---- | How to refer to an item in object position of a sentence.-objectItem :: Kind.Ops ItemKind -> State -> Item -> Object-objectItem coitem = objectItemCheat coitem False---- | How to refer to an actor in object position of a sentence.-objectActor :: Kind.Ops ActorKind -> Actor -> Object-objectActor Kind.Ops{oname} a =- fromMaybe (oname $ bkind a) (bname a)---- | Capitalized actor object.-capActor :: Kind.Ops ActorKind -> Actor -> Object-capActor coactor x = capitalize $ objectActor coactor x---- | Sentences such as \"Dog barks loudly.\"-actorVerb :: Kind.Ops ActorKind -> Actor -> Verb -> String -> String-actorVerb coactor a v extra =- let cactor = capActor coactor a- verb = conjugate cactor v- ending | null extra = "."- | otherwise = " " ++ extra ++ "."- in cactor ++ " " ++ verb ++ ending---- | Sentences such as \"Dog quaffs a red potion fast.\"-actorVerbItem :: Kind.COps -> State -> Actor -> Verb -> Item -> String- -> String-actorVerbItem Kind.COps{coactor, coitem} state a v i extra =- let ending | null extra = ""- | otherwise = " " ++ extra- in actorVerb coactor a v $- objectItem coitem state i ++ ending---- | Sentences such as \"Dog bites goblin furiously.\"-actorVerbActor :: Kind.Ops ActorKind -> Actor -> Verb -> Actor -> String- -> String-actorVerbActor coactor a v b extra =- let ending | null extra = ""- | otherwise = " " ++ extra- in actorVerb coactor a v $- objectActor coactor b ++ ending---- | Sentences such as \"Dog gulps down a red potion fast.\"-actorVerbExtraItem :: Kind.COps -> State -> Actor -> Verb -> String- -> Item -> String -> String-actorVerbExtraItem Kind.COps{coactor, coitem} state a v extra1 i extra2 =- assert (not $ null extra1) $- let ending | null extra2 = ""- | otherwise = " " ++ extra2- in actorVerb coactor a v $- extra1 ++ " " ++ objectItem coitem state i ++ ending---- | Produces a textual description of the terrain and items at an already--- explored location. Mute for unknown locations.--- The detailed variant is for use in the targeting mode.-lookAt :: Kind.COps -- ^ game content- -> Bool -- ^ detailed?- -> Bool -- ^ can be seen right now?- -> State -- ^ game state- -> Level -- ^ current level- -> Point -- ^ location to describe- -> String -- ^ an extra sentence to print- -> String-lookAt Kind.COps{coitem, cotile=Kind.Ops{oname}} detailed canSee s lvl loc msg- | detailed =- let tile = lvl `rememberAt` loc- name = capitalize $ oname tile- in name ++ ". " ++ msg ++ isd- | otherwise = msg ++ isd- where- is = lvl `rememberAtI` loc- prefixSee = if canSee then "You see " else "You remember "- isd = case is of- [] -> ""- [i] -> prefixSee ++ objectItem coitem s i ++ "."- [i,j] -> prefixSee ++ objectItem coitem s i ++ " and "- ++ objectItem coitem s j ++ "."- _ | detailed -> "Objects:"- _ -> "Objects here."
Game/LambdaHack/Item.hs view
@@ -28,6 +28,9 @@ import Data.Function import Data.Ord import Control.Monad+import Data.Text (Text)+import qualified Data.Text as T+import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Random@@ -37,6 +40,7 @@ import Game.LambdaHack.Flavour import qualified Game.LambdaHack.Kind as Kind import Game.LambdaHack.Effect+import Game.LambdaHack.Msg -- TODO: see the TODO about ipower in ItemKind. -- TODO: define type InvSymbol = Char and move all ops to another file.@@ -62,7 +66,7 @@ -- | Generate an item. newItem :: Kind.Ops ItemKind -> Int -> Int -> Rnd Item newItem cops@Kind.Ops{opick, okind} lvl depth = do- ikChosen <- opick "dng" (const True)+ ikChosen <- opick (T.pack "dng") (const True) let kind = okind ikChosen count <- rollDeep lvl depth (icount kind) if count == 0@@ -163,26 +167,26 @@ mergeLetter :: Maybe Char -> Maybe Char -> Maybe Char mergeLetter = mplus -letterRange :: [Char] -> String+letterRange :: [Char] -> Text letterRange ls = sectionBy (L.sortBy cmpLetter ls) Nothing where succLetter c d = ord d - ord c == 1 - sectionBy [] Nothing = ""+ sectionBy [] Nothing = T.empty sectionBy [] (Just (c,d)) = finish (c,d) sectionBy (x:xs) Nothing = sectionBy xs (Just (x,x)) sectionBy (x:xs) (Just (c,d)) | succLetter d x = sectionBy xs (Just (c,x))- | otherwise = finish (c,d) ++ sectionBy xs (Just (x,x))+ | otherwise = finish (c,d) <> sectionBy xs (Just (x,x)) - finish (c,d) | c == d = [c]- | succLetter c d = [c,d]- | otherwise = [c,'-',d]+ finish (c,d) | c == d = T.pack [c]+ | succLetter c d = T.pack $ [c, d]+ | otherwise = T.pack $ [c, '-', d] -letterLabel :: Maybe Char -> String-letterLabel Nothing = " "-letterLabel (Just c) = c : " - "+letterLabel :: Maybe Char -> MU.Part+letterLabel Nothing = MU.Text $ T.pack " "+letterLabel (Just c) = MU.Text $ T.pack $ c : " -" -- | Adds an item to a list of items, joining equal items. -- Also returns the joined item.
Game/LambdaHack/ItemAction.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE OverloadedStrings, ExtendedDefaultRules #-}+{-# OPTIONS_GHC -fno-warn-type-defaults #-} -- | Item UI code with the 'Action' type and everything it depends on -- that is not already in Action.hs and EffectAction.hs. -- This file should not depend on Actions.hs.@@ -11,11 +13,13 @@ import Data.Maybe import Data.Ord import qualified Data.IntSet as IS+import Data.Text (Text)+import qualified Data.Text as T+import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Action import Game.LambdaHack.Point-import Game.LambdaHack.Grammar import Game.LambdaHack.Item import qualified Game.LambdaHack.Key as K import Game.LambdaHack.Level@@ -27,7 +31,10 @@ import qualified Game.LambdaHack.Kind as Kind import Game.LambdaHack.Content.ItemKind import Game.LambdaHack.Time+import Game.LambdaHack.Msg +default (Text)+ -- TODO: When inventory is displayed, let TAB switch the player (without -- announcing that) and show the inventory of the new player. -- | Display inventory@@ -37,59 +44,65 @@ pbody <- gets getPlayerBody items <- gets getPlayerItem if L.null items- then abortWith $ actorVerb coactor pbody "be" "not carrying anything"+ then abortWith $ makeSentence+ [ MU.SubjectVerbSg (partActor coactor pbody) "be"+ , "not carrying anything" ] else do io <- itemOverlay True False items- displayOverlays (init $ actorVerb coactor pbody "be" "carrying:") "" io+ let blurb = makePhrase [MU.Capitalize $+ MU.SubjectVerbSg (partActor coactor pbody) "be carrying:"]+ displayOverlays blurb "" io -- | Let the player choose any item with a given group name. -- Note that this does not guarantee the chosen item belongs to the group, -- as the player can override the choice.-getGroupItem :: [Item] -- ^ all objects in question- -> Object -- ^ name of the group- -> [Char] -- ^ accepted item symbols- -> String -- ^ prompt- -> String -- ^ how to refer to the collection of objects+getGroupItem :: [Item] -- ^ all objects in question+ -> MU.Part -- ^ name of the group+ -> [Char] -- ^ accepted item symbols+ -> Text -- ^ prompt+ -> Text -- ^ how to refer to the collection of objects -> Action Item getGroupItem is object syms prompt packName = do Kind.COps{coitem=Kind.Ops{osymbol}} <- getCOps let choice i = osymbol (jkind i) `elem` syms- header = capitalize $ pluralise object+ header = makePhrase [MU.Capitalize (MU.Ws object)] getItem prompt choice header is packName applyGroupItem :: ActorId -- ^ actor applying the item (is on current level)- -> Verb -- ^ how the applying is called+ -> MU.Part -- ^ how the applying is called -> Item -- ^ the item to be applied -> Action () applyGroupItem actor verb item = do- cops <- getCOps+ Kind.COps{coactor, coitem} <- getCOps state <- get body <- gets (getActor actor) per <- getPerception -- only one item consumed, even if several in inventory let consumed = item { jcount = 1 }- msg = actorVerbItem cops state body verb consumed ""+ msg = makeSentence+ [ MU.SubjectVerbSg (partActor coactor body) verb+ , partItemNWs coitem state consumed ] loc = bloc body removeFromInventory actor consumed loc when (loc `IS.member` totalVisible per) $ msgAdd msg itemEffectAction 5 actor actor consumed False -playerApplyGroupItem :: Verb -> Object -> [Char] -> Action ()+playerApplyGroupItem :: MU.Part -> MU.Part -> [Char] -> Action () playerApplyGroupItem verb object syms = do Kind.COps{coitem=Kind.Ops{okind}} <- getCOps is <- gets getPlayerItem item <- getGroupItem is object syms- ("What to " ++ verb ++ "?") "in inventory"+ (makePhrase ["What to", verb MU.:> "?"]) "in inventory" pl <- gets splayer applyGroupItem pl (iverbApply $ okind $ jkind item) item projectGroupItem :: ActorId -- ^ actor projecting the item (is on current lvl) -> Point -- ^ target location of the projectile- -> Verb -- ^ how the projecting is called+ -> MU.Part -- ^ how the projecting is called -> Item -- ^ the item to be projected -> Action () projectGroupItem source tloc _verb item = do- cops@Kind.COps{coactor} <- getCOps+ cops@Kind.COps{coactor, coitem} <- getCOps state <- get sm <- gets (getActor source) per <- getPerception@@ -110,7 +123,9 @@ -- When projecting, the first turn is spent aiming. -- The projectile is seen one tile from the actor, giving a hint -- about the aim and letting the target evade.- msg = actorVerbItem cops state subject "aim" consumed ""+ msg = makeSentence+ [ MU.SubjectVerbSg (partActor coactor subject) "aim"+ , partItemNWs coitem state consumed ] -- TODO: AI should choose the best eps. eps = if source == pl then ceps else 0 -- Setting monster's projectiles time to player time ensures@@ -142,7 +157,7 @@ abortWith "blocked" when (svisible || projVis) $ msgAdd msg -playerProjectGroupItem :: Verb -> Object -> [Char] -> ActionFrame ()+playerProjectGroupItem :: MU.Part -> MU.Part -> [Char] -> ActionFrame () playerProjectGroupItem verb object syms = do ms <- gets hostileList lxsize <- gets (lxsize . slevel)@@ -152,7 +167,7 @@ then abortWith "You can't aim in melee." else playerProjectGI verb object syms -playerProjectGI :: Verb -> Object -> [Char] -> ActionFrame ()+playerProjectGI :: MU.Part -> MU.Part -> [Char] -> ActionFrame () playerProjectGI verb object syms = do state <- get pl <- gets splayer@@ -171,7 +186,7 @@ Kind.COps{coitem=Kind.Ops{okind}} <- getCOps is <- gets getPlayerItem item <- getGroupItem is object syms- ("What to " ++ verb ++ "?") "in inventory"+ (makePhrase ["What to", verb MU.:> "?"]) "in inventory" targeting <- gets (ctargeting . scursor) when (targeting == TgtAuto) $ endTargeting True projectGroupItem pl loc (iverbProject $ okind $ jkind item) item@@ -291,16 +306,16 @@ pbody <- gets getPlayerBody state <- get lxsize <- gets (lxsize . slevel)- let verb = "target"- targetMsg = case btarget pbody of+ let targetMsg = case btarget pbody of TEnemy a _ll -> if memActor a state- then objectActor coactor $ getActor a state+ then partActor coactor $ getActor a state else "a fear of the past"- TLoc loc -> "location " ++ showPoint lxsize loc+ TLoc loc -> MU.Text $ "location" <+> showPoint lxsize loc TPath _ -> "a path" TCursor -> "current cursor position continuously"- msgAdd $ actorVerb coactor pbody verb targetMsg+ msgAdd $ makeSentence+ [MU.SubjectVerbSg (partActor coactor pbody) "target", targetMsg] -- | Cancel something, e.g., targeting mode, resetting the cursor -- to the position of the player. Chosen target is not invalidated.@@ -328,7 +343,7 @@ dropItem :: Action () dropItem = do -- TODO: allow dropping a given number of identical items.- cops <- getCOps+ Kind.COps{coactor, coitem} <- getCOps pl <- gets splayer state <- get pbody <- gets getPlayerBody@@ -337,7 +352,9 @@ stack <- getAnyItem "What to drop?" ims "in inventory" let item = stack { jcount = 1 } removeOnlyFromInventory pl item (bloc pbody)- msgAdd (actorVerbItem cops state pbody "drop" item "")+ msgAdd $ makeSentence+ [ MU.SubjectVerbSg (partActor coactor pbody) "drop"+ , partItemNWs coitem state item ] modify (updateLevel (dropItemsAt [item] ploc)) -- TODO: this is a hack for dropItem, because removeFromInventory@@ -378,7 +395,7 @@ actorPickupItem :: ActorId -> Action () actorPickupItem actor = do- cops@Kind.COps{coitem} <- getCOps+ Kind.COps{coactor, coitem} <- getCOps state <- get pl <- gets splayer per <- getPerception@@ -397,11 +414,12 @@ let (ni, nitems) = joinItem (i { jletter = Just l }) bitems -- msg depends on who picks up and if a hero can perceive it if isPlayer- then msgAdd (letterLabel (jletter ni)- ++ objectItem coitem state ni ++ ".")+ then msgAdd $ makePhrase [ letterLabel (jletter ni)+ , partItemNWs coitem state ni ] else when perceived $- msgAdd $- actorVerbExtraItem cops state body "pick" "up" i ""+ msgAdd $ makeSentence+ [ MU.SubjectVerbSg (partActor coactor body) "pick up"+ , partItemNWs coitem state i ] removeFromLoc i loc >>= assert `trueM` (i, is, loc, "item is stuck") -- add item to actor's inventory:@@ -427,16 +445,16 @@ -- that messages are printed to the player only if the -- hero can perceive the action. --- TODO: you can drop an item already the floor, which works correctly,+-- TODO: you can drop an item already on the floor, which works correctly, -- but is weird and useless. -allObjectsName :: String+allObjectsName :: Text allObjectsName = "Objects" -- | Let the player choose any item from a list of items.-getAnyItem :: String -- ^ prompt+getAnyItem :: Text -- ^ prompt -> [Item] -- ^ all items in question- -> String -- ^ how to refer to the collection of items+ -> Text -- ^ how to refer to the collection of items -> Action Item getAnyItem prompt = getItem prompt (const True) allObjectsName @@ -444,11 +462,11 @@ -- | Let the player choose a single, preferably suitable, -- item from a list of items.-getItem :: String -- ^ prompt message- -> (Item -> Bool) -- ^ which items to consider suitable- -> String -- ^ how to describe suitable items- -> [Item] -- ^ all items in question- -> String -- ^ how to refer to the collection of items+getItem :: Text -- ^ prompt message+ -> (Item -> Bool) -- ^ which items to consider suitable+ -> Text -- ^ how to describe suitable items+ -> [Item] -- ^ all items in question+ -> Text -- ^ how to refer to the collection of items -> Action Item getItem prompt p ptext is0 isn = do lvl <- gets slevel@@ -462,9 +480,9 @@ bestFull = not $ null isp (bestMsg, bestKey) | bestFull =- let bestLetter = maybe "" (\ l -> ['(', l, ')']) $+ let bestLetter = maybe "" (\ l -> "(" <> T.singleton l <> ")") $ jletter $ L.maximumBy cmpItemLM isp- in (", RET" ++ bestLetter, [K.Return])+ in (", RET" <> bestLetter, [K.Return]) | otherwise = ("", []) cmpItemLM i1 i2 = cmpLetterMaybe (jletter i1) (jletter i2) keys ims =@@ -473,22 +491,22 @@ in zip ks $ repeat K.NoModifier choice ims = if null ims- then "[?" ++ floorMsg+ then "[?" <> floorMsg else let mls = mapMaybe jletter ims r = letterRange mls- in "[" ++ r ++ ", ?" ++ floorMsg ++ bestMsg+ in "[" <> r <> ", ?" <> floorMsg <> bestMsg ask = do when (L.null is0 && L.null tis) $ abortWith "Not carrying anything." perform INone perform itemDialogState = do let (ims, imsOver, msg) = case itemDialogState of- INone -> (isp, [], prompt ++ " ")- ISuitable -> (isp, isp, ptext ++ " " ++ isn ++ ". ")- IAll -> (is0, is0, allObjectsName ++ " " ++ isn ++ ". ")+ INone -> (isp, [], prompt)+ ISuitable -> (isp, isp, ptext <+> isn <> ".")+ IAll -> (is0, is0, allObjectsName <+> isn <> ".") io <- itemOverlay True False imsOver (command, modifier) <-- displayChoiceUI (msg ++ choice ims) io (keys ims)+ displayChoiceUI (msg <+> choice ims) io (keys ims) assert (modifier == K.NoModifier) $ case command of K.Char '?' -> case itemDialogState of@@ -503,5 +521,5 @@ in return $ fromJust mitem K.Return | bestFull -> return $ L.maximumBy cmpItemLM isp- k -> assert `failure` "perform: unexpected key: " ++ show k+ k -> assert `failure` "perform: unexpected key:" <+> showT k ask
Game/LambdaHack/Key.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | Frontend-independent keyboard input operations. module Game.LambdaHack.Key ( Key(..), handleDir, dirAllMoveKey@@ -7,9 +8,13 @@ import Prelude hiding (Left, Right) import qualified Data.List as L import qualified Data.Char as Char+import Data.Text (Text)+import qualified Data.Text as T+import Data.Binary import Game.LambdaHack.PointXY import Game.LambdaHack.Vector+import Game.LambdaHack.Msg -- TODO: if the file grows much larger, split it and move a part to Utils/ @@ -34,6 +39,46 @@ | Unknown !String -- ^ an unknown key, registered to warn the user deriving (Ord, Eq) +instance Binary Key where+ put Esc = putWord8 0+ put Return = putWord8 1+ put Space = putWord8 2+ put Tab = putWord8 3+ put BackTab = putWord8 4+ put PgUp = putWord8 5+ put PgDn = putWord8 6+ put Left = putWord8 7+ put Right = putWord8 8+ put Up = putWord8 9+ put Down = putWord8 10+ put End = putWord8 11+ put Begin = putWord8 12+ put Home = putWord8 13+ put (KP c) = putWord8 14 >> put c+ put (Char c) = putWord8 15 >> put c+ put (Unknown s) = putWord8 16 >> put s+ get = do+ tag <- getWord8+ case tag of+ 0 -> return Esc+ 1 -> return Return+ 2 -> return Space+ 3 -> return Tab+ 4 -> return BackTab+ 5 -> return PgUp+ 6 -> return PgDn+ 7 -> return Left+ 8 -> return Right+ 9 -> return Up+ 10 -> return Down+ 11 -> return End+ 12 -> return Begin+ 13 -> return Home+ 14 -> fmap KP get+ 15 -> fmap Char get+ 16 -> fmap Unknown get+ _ -> fail "no parse (Key)"+ -- | Our own encoding of modifiers. Incomplete. data Modifier = Control@@ -41,8 +86,8 @@ deriving (Ord, Eq) -- Common and terse names for keys.-showKey :: Key -> String-showKey (Char c) = [c]+showKey :: Key -> Text+showKey (Char c) = T.singleton c showKey Esc = "ESC" showKey Return = "RET" showKey Space = "SPACE"@@ -57,16 +102,16 @@ showKey End = "END" showKey Begin = "BEGIN" showKey Home = "HOME"-showKey (KP c) = "KEYPAD(" ++ [c] ++ ")"-showKey (Unknown s) = s+showKey (KP c) = "KEYPAD(" <> T.singleton c <> ")"+showKey (Unknown s) = T.pack s -- | Show a key with a modifier, if any.-showKM :: (Key, Modifier) -> String-showKM (key, Control) = "CTRL-" ++ showKey key+showKM :: (Key, Modifier) -> Text+showKM (key, Control) = "CTRL-" <> showKey key showKM (key, NoModifier) = showKey key instance Show Key where- show = showKey+ show = T.unpack . showKey dirViChar :: [Char] dirViChar = ['y', 'k', 'u', 'l', 'n', 'j', 'b', 'h']@@ -107,9 +152,9 @@ -- TODO: deduplicate -- | Binding of both sets of movement keys. moveBinding :: ((X -> Vector) -> a) -> ((X -> Vector) -> a)- -> [((Key, Modifier), (String, Bool, a))]+ -> [((Key, Modifier), (Text, Bool, a))] moveBinding move run =- let assign f (km, dir) = (km, ("", True, f dir))+ let assign f (km, dir) = (km, (T.empty, True, f dir)) rNoModifier = repeat NoModifier rControl = repeat Control in map (assign move) (zip (zip dirViMoveKey rNoModifier) movesWidth) ++
Game/LambdaHack/Kind.hs view
@@ -1,5 +1,6 @@--- | General content types and operations.+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes, ScopedTypeVariables, TypeFamilies #-}+-- | General content types and operations. module Game.LambdaHack.Kind ( -- * General content types Id, Speedup(..), Ops(..), COps(..), createOps, stdRuleset@@ -14,6 +15,10 @@ import qualified Data.Word as Word import qualified Data.Array.Unboxed as A import qualified Data.Ix as Ix+import Data.Text (Text)+import qualified Data.Text as T+import Game.LambdaHack.Msg+import Data.Maybe (fromMaybe) import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Utils.Frequency@@ -48,11 +53,11 @@ -- | Content operations for the content of type @a@. data Ops a = Ops { osymbol :: Id a -> Char -- ^ the symbol of a content element at id- , oname :: Id a -> String -- ^ the name of a content element at id+ , oname :: Id a -> Text -- ^ the name of a content element at id , okind :: Id a -> a -- ^ the content element at given id- , ouniqGroup :: String -> Id a -- ^ the id of the unique member of+ , ouniqGroup :: Text -> Id a -- ^ the id of the unique member of -- a singleton content group- , opick :: String -> (a -> Bool) -> Rnd (Id a)+ , opick :: Text -> (a -> Bool) -> Rnd (Id a) -- ^ pick a random id belonging to a group -- and satisfying a predicate , ofoldrWithKey :: forall b. (Id a -> a -> b -> b) -> b -> b@@ -69,34 +74,41 @@ kindAssocs = L.zip [0..] content kindMap :: IM.IntMap a kindMap = IM.fromDistinctAscList $ L.zip [0..] content- kindFreq :: M.Map String (Frequency (Id a, a))+ kindFreq :: M.Map Text (Frequency (Id a, a)) kindFreq = let tuples = [ (group, (n, (Id i, k))) | (i, k) <- kindAssocs , (group, n) <- getFreq k, n > 0 ] f m (group, nik) = M.insertWith (++) group [nik] m- lists = L.foldl' f M.empty tuples- nameFreq group = toFreq $ "opick ('" ++ group ++ "')"+ lists = L.foldl' f M.empty tuples+ nameFreq group = toFreq $ "opick ('" <> group <> "')" in M.mapWithKey nameFreq lists- okind (Id i) = kindMap IM.! fromEnum i- correct a = not (L.null (getName a)) && L.all ((> 0) . snd) (getFreq a)+ okind (Id i) = fromMaybe (assert `failure` (i, fromEnum i, kindMap))+ $ IM.lookup (fromEnum i) kindMap+ correct a = not (T.null (getName a)) && L.all ((> 0) . snd) (getFreq a) offenders = validate content in assert (allB correct content) $- assert (L.null offenders `blame` ("content not validated:", offenders)) $+ assert (L.null offenders `blame` ("content not validated: " :: Text,+ offenders)) $ Ops { osymbol = getSymbol . okind , oname = getName . okind , okind = okind , ouniqGroup = \ group ->- case runFrequency $ kindFreq M.! group of+ let freq = fromMaybe (assert `failure` (group, kindFreq))+ $ M.lookup group kindFreq+ in case runFrequency freq of [(n, (i, _))] | n > 0 -> i l -> assert `failure` l- , opick = \ group p -> frequency $ do- (i, k) <- kindFreq M.! group- breturn (p k) i- {- with MonadComprehensions:- frequency [ i | (i, k) <- kindFreq M.! group, p k ]- -}+ , opick = \ group p ->+ let freq = fromMaybe (assert `failure` (group, kindFreq))+ $ M.lookup group kindFreq+ in frequency $ do+ (i, k) <- freq+ breturn (p k) i+ {- with MonadComprehensions:+ frequency [ i | (i, k) <- kindFreq M.! group, p k ]+ -} , ofoldrWithKey = \ f z -> L.foldr (\ (i, a) -> f (Id i) a) z kindAssocs , obounds = let limits = let (i1, a1) = IM.findMin kindMap
Game/LambdaHack/Level.hs view
@@ -14,6 +14,7 @@ import Data.Binary import qualified Data.List as L import qualified Data.IntMap as IM+import Data.Text (Text) import Game.LambdaHack.Utils.Assert import Game.LambdaHack.PointXY@@ -57,8 +58,8 @@ , litem :: ItemMap -- ^ items on the ground , lmap :: TileMap -- ^ map tiles , lrmap :: TileMap -- ^ remembered map tiles- , ldesc :: String -- ^ level description for the player- , lmeta :: String -- ^ debug information from cave generation+ , ldesc :: Text -- ^ level description for the player+ , lmeta :: Text -- ^ debug information from cave generation , lstairs :: (Point, Point) -- ^ destination of the (up, down) stairs , ltime :: Time -- ^ date of the last activity on the level , lclear :: Int -- ^ total number of clear tiles
Game/LambdaHack/Misc.hs view
@@ -4,6 +4,7 @@ ) where import Control.Monad+import Data.Text (Text) -- | Level bounds. TODO: query terminal size instead and scroll view. normalLevelBound :: (Int, Int)@@ -13,10 +14,10 @@ divUp :: Int -> Int -> Int divUp n k = (n + k - 1) `div` k --- | For each group that the kind belongs to, denoted by a @String@ name+-- | For each group that the kind belongs to, denoted by a @Text@ name -- in the first component of a pair, the second component of a pair shows -- how common the kind is within the group.-type Freqs = [(String, Int)]+type Freqs = [(Text, Int)] -- | @breturn b a = [a | b]@ breturn :: MonadPlus m => Bool -> a -> m a
Game/LambdaHack/Msg.hs view
@@ -1,6 +1,9 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-orphans #-} -- | Game messages displayed on top of the screen for the player to read. module Game.LambdaHack.Msg- ( Msg, moreMsg, yesnoMsg, padMsg+ ( makePhrase, makeSentence+ , Msg, (<>), (<+>), showT, moreMsg, yesnoMsg, padMsg , Report, emptyReport, nullReport, singletonReport, addMsg , splitReport, renderReport , History, emptyHistory, singletonHistory, addReport, renderHistory@@ -13,13 +16,28 @@ import Data.Binary import qualified Data.ByteString.Char8 as BS import qualified Data.IntMap as IM+import Data.Text (Text)+import qualified Data.Text as T+import Data.Text.Encoding (decodeUtf8, encodeUtf8)+import NLP.Miniutter.English ((<>), (<+>), showT)+import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Misc import Game.LambdaHack.PointXY +-- | Re-exported English phrase creation functions, applied to default+-- irregular word sets.+makePhrase, makeSentence :: [MU.Part] -> Text+makePhrase = MU.makePhrase MU.defIrregular+makeSentence = MU.makeSentence MU.defIrregular+ -- | The type of a single message.-type Msg = String+type Msg = Text +instance Binary Text where+ put = put . encodeUtf8+ get = decodeUtf8 `fmap` get+ -- | The \"press something to see more\" mark. moreMsg :: Msg moreMsg = "--more-- "@@ -30,17 +48,15 @@ -- | Add spaces at the message end, for display overlayed over the level map. -- Also trims (does not wrap!) too long lines.-padMsg :: X -> String -> String+padMsg :: X -> Text -> Text padMsg w xs =- let len = length xs- rev = reverse xs+ let len = T.length xs in case compare w len of- LT -> reverse $ '$' : drop (len - w + 1) rev+ LT -> T.snoc (T.take (w - 1) xs) '$' EQ -> xs- GT -> case rev of- [] -> xs- ' ' : _ -> xs- _ -> reverse $ ' ' : rev+ GT -> if T.null xs || T.last xs == ' '+ then xs+ else T.snoc xs ' ' -- | The type of a set of messages to show at the screen at once. newtype Report = Report [(BS.ByteString, Int)]@@ -64,47 +80,46 @@ -- | Add message to the end of report. addMsg :: Report -> Msg -> Report-addMsg r "" = r+addMsg r m | T.null m = r addMsg (Report ((x, n) : xns)) y' | x == y = Report $ (y, n + 1) : xns- where y = BS.pack y'-addMsg (Report xns) y = Report $ (BS.pack y, 1) : xns+ where y = encodeUtf8 y'+addMsg (Report xns) y = Report $ (encodeUtf8 y, 1) : xns -- | Split a messages into chunks that fit in one line. -- We assume the width of the messages line is the same as of level map.-splitReport :: Report -> [String]+splitReport :: Report -> [Text] splitReport r = let w = fst normalLevelBound + 1- in splitString w $ renderReport r+ in splitText w $ renderReport r -- | Render a report as a (possibly very long) string.-renderReport ::Report -> String-renderReport (Report []) = ""-renderReport (Report [xn]) = renderRepetition xn+renderReport ::Report -> Text+renderReport (Report []) = T.empty renderReport (Report (xn : xs)) =- renderReport (Report xs) ++ " " ++ renderRepetition xn+ renderReport (Report xs) <+> renderRepetition xn -renderRepetition :: (BS.ByteString, Int) -> String-renderRepetition (s, 1) = BS.unpack s-renderRepetition (s, n) = BS.unpack s ++ "<x" ++ show n ++ ">"+renderRepetition :: (BS.ByteString, Int) -> Text+renderRepetition (s, 1) = decodeUtf8 s+renderRepetition (s, n) = decodeUtf8 s <> "<x" <> showT n <> ">" -- | Split a string into lines. Avoids ending the line with a character -- other than whitespace or punctuation. Space characters are removed -- from hte start, but never from the end of lines.-splitString :: X -> String -> [String]-splitString w xs = splitString' w $ dropWhile isSpace xs+splitText :: X -> Text -> [Text]+splitText w xs = splitText' w $ T.dropWhile isSpace xs -splitString' :: X -> String -> [String]-splitString' w xs+splitText' :: X -> Text -> [Text]+splitText' w xs | w <= 0 = [xs] -- border case, we cannot make progress- | w >= length xs = [xs] -- no problem, everything fits+ | w >= T.length xs = [xs] -- no problem, everything fits | otherwise =- let (pre, post) = splitAt w xs- (ppre, ppost) = break (`elem` " .,:;!?") $ reverse pre- testPost = dropWhile isSpace ppost- in if L.null testPost- then pre : splitString w post- else reverse ppost : splitString w (reverse ppre ++ post)+ let (pre, post) = T.splitAt w xs+ (ppre, ppost) = T.break (`elem` " .,:;!?") $ T.reverse pre+ testPost = T.dropWhile isSpace ppost+ in if T.null testPost+ then pre : splitText w post+ else T.reverse ppost : splitText w (T.reverse ppre <> post) -- | The history of reports. newtype History = History [Report]@@ -143,7 +158,7 @@ -- | A screenful of text lines. When displayed, they are trimmed, not wrapped -- and any lines below the lower screen edge are not visible.-type Overlay = [String]+type Overlay = [Text] -- | Split an overlay into overlays that fit on the screen. splitOverlay :: Y -> Overlay -> [Overlay]@@ -156,12 +171,12 @@ -- string by location. Takes the width and height of the display plus -- the string. Returns also the message to print at the top and bottom. stringByLocation :: X -> Y -> Overlay- -> (String, PointXY -> Maybe Char, Maybe String)-stringByLocation _ _ [] = ("", const Nothing, Nothing)+ -> (Text, PointXY -> Maybe Char, Maybe Text)+stringByLocation _ _ [] = (T.empty, const Nothing, Nothing) stringByLocation lxsize lysize (msgTop : ls) = let over = map (padMsg lxsize) $ take lysize ls m = IM.fromDistinctAscList $- zip [0..] (L.map (IM.fromList . zip [0..]) over)+ zip [0..] (L.map (IM.fromList . zip [0..] . T.unpack) over) msgBottom = case drop lysize ls of [] -> Nothing s : _ -> Just s
Game/LambdaHack/Perception.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | Actors perceiving other actors and the dungeon level. module Game.LambdaHack.Perception ( DungeonPerception, Perception@@ -11,7 +12,6 @@ import Data.Maybe import Control.Monad -import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Point import Game.LambdaHack.State import Game.LambdaHack.Level@@ -20,7 +20,7 @@ import Game.LambdaHack.Dungeon import Game.LambdaHack.Content.ActorKind import Game.LambdaHack.FOV-import qualified Game.LambdaHack.Config as Config+import Game.LambdaHack.Config import qualified Game.LambdaHack.Tile as Tile import qualified Game.LambdaHack.Kind as Kind import Game.LambdaHack.Content.TileKind@@ -123,23 +123,18 @@ , sdebug = DebugMode{smarkVision} } lvl@Level{lactor} =- let mode = Config.get sconfig "engine" "fovMode"- radius = let r = Config.get sconfig "engine" "fovRadius"- in if r < 1- then assert `failure`- "FOV radius is " ++ show r ++ ", should be >= 1"- else r+ let Config{configFovMode} = sconfig -- Perception for a player-controlled monster on the current level. mLocPer = if not (isAHero state splayer) && IM.member splayer lactor then let m = getPlayerBody state in Just (bloc m,- computeReachable cops radius mode smarkVision m lvl)+ computeReachable cops configFovMode smarkVision m lvl) else Nothing (mLoc, mPer) = (fmap fst mLocPer, fmap snd mLocPer) hs = IM.filter (\ m -> bfaction m == sfaction && not (bproj m)) lactor pers = IM.map (\ h ->- computeReachable cops radius mode smarkVision h lvl) hs+ computeReachable cops configFovMode smarkVision h lvl) hs locs = map bloc $ IM.elems hs lpers = maybeToList mPer ++ IM.elems pers reachable = PerceptionReachable $ IS.unions (map preachable lpers)@@ -184,20 +179,16 @@ -- | Reachable are all fields on an unblocked path from the hero position. -- The player's own position is considred reachable by him.-computeReachable :: Kind.COps -> Int -> String -> Maybe FovMode+computeReachable :: Kind.COps -> FovMode -> Maybe FovMode -> Actor -> Level -> PerceptionReachable computeReachable Kind.COps{cotile, coactor=Kind.Ops{okind}}- radius mode smarkVision actor lvl =+ configFovMode smarkVision actor lvl = let fovMode m = if not $ asight $ okind $ bkind m then Blind else case smarkVision of Just fm -> fm- Nothing -> case mode of- "shadow" -> Shadow- "permissive" -> Permissive- "digital" -> Digital radius- _ -> assert `failure` "Unknown FOV mode: " ++ show mode+ Nothing -> configFovMode ploc = bloc actor in PerceptionReachable $ IS.insert ploc $ IS.fromList $ fullscan cotile (fovMode actor) ploc lvl
Game/LambdaHack/Place.hs view
@@ -8,6 +8,8 @@ import qualified Data.Map as M import qualified Data.List as L import qualified Data.Set as S+import Data.Text (Text)+import qualified Data.Text as T import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Content.PlaceKind@@ -15,6 +17,7 @@ import Game.LambdaHack.Area import Game.LambdaHack.PointXY import Game.LambdaHack.Misc+import Game.LambdaHack.Msg() import Game.LambdaHack.Content.TileKind import Game.LambdaHack.Random import Game.LambdaHack.Content.CaveKind@@ -26,7 +29,7 @@ { qkind :: !(Kind.Id PlaceKind) , qarea :: !Area , qseen :: !Bool- , qlegend :: !String+ , qlegend :: !Text , qsolidFence :: !(Kind.Id TileKind) , qhollowFence :: !(Kind.Id TileKind) }@@ -66,7 +69,7 @@ let (x0, y0, x1, y1) = expandFence pfence r dx = x1 - x0 + 1 dy = y1 - y0 + 1- dxcorner = case ptopLeft of [] -> 0 ; l : _ -> L.length l+ dxcorner = case ptopLeft of [] -> 0 ; l : _ -> T.length l dycorner = L.length ptopLeft wholeOverlapped d dcorner = d > 1 && dcorner > 1 && (d - 1) `mod` (2 * (dcorner - 1)) == 0@@ -98,7 +101,7 @@ = assert (not (trivialArea r) `blame` r) $ do qsolidFence <- opick cfillerTile (const True) dark <- chanceDeep ln depth cdarkChance- qkind <- popick "rogue" (placeValid r)+ qkind <- popick (T.pack "rogue") (placeValid r) let kr = pokind qkind qlegend = if dark then cdarkLegendTile else clitLegendTile qseen = False@@ -110,7 +113,7 @@ return (digPlace place kr xlegend, place) -- | Roll a legend of a place plan: a map from plan symbols to tile kinds.-olegend :: Kind.Ops TileKind -> String -> Rnd (M.Map Char (Kind.Id TileKind))+olegend :: Kind.Ops TileKind -> Text -> Rnd (M.Map Char (Kind.Id TileKind)) olegend Kind.Ops{ofoldrWithKey, opick} group = let getSymbols _ tk acc = maybe acc (const $ S.insert (tsymbol tk) acc)@@ -143,6 +146,7 @@ FNone -> M.empty in M.union (M.map (legend M.!) $ tilePlace qarea kr) fence +-- TODO: use Text more instead of [Char]? -- | Create a place by tiling patterns. tilePlace :: Area -- ^ the area to fill -> PlaceKind -- ^ the place kind to construct@@ -154,7 +158,7 @@ fillInterior :: (forall a. Int -> [a] -> [a]) -> [(PointXY, Char)] fillInterior f = let tileInterior (y, row) = L.zip (fromX (x0, y)) $ f dx row- reflected = L.zip [y0..] $ f dy ptopLeft+ reflected = L.zip [y0..] $ f dy $ map T.unpack ptopLeft in L.concatMap tileInterior reflected tileReflect :: Int -> [a] -> [a] tileReflect d pat =
Game/LambdaHack/Point.hs view
@@ -10,7 +10,9 @@ import Game.LambdaHack.PointXY import Game.LambdaHack.VectorXY import Game.LambdaHack.Area+import Game.LambdaHack.Msg import Game.LambdaHack.Utils.Assert+import Data.Text (Text) -- | The type of locations on the 2D level map, heavily optimized. --@@ -29,8 +31,8 @@ type Point = Int -- | Print a point as a tuple of cartesian coordinates.-showPoint :: X -> Point -> String-showPoint lxsize = show . fromPoint lxsize+showPoint :: X -> Point -> Text+showPoint lxsize = showT . fromPoint lxsize -- | Conversion from cartesian coordinates to @Point@. toPoint :: X -> PointXY -> Point
Game/LambdaHack/State.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | Game state and persistent player diary types and operations. module Game.LambdaHack.State ( -- * Game state@@ -10,15 +11,20 @@ , updateCursor, updateTime, updateDiscoveries, updateLevel, updateDungeon -- * Player diary , Diary(..), defaultDiary+ -- * Textia; descriptions+ , lookAt, partItemCheat, partItem, partItemNWs -- * Debug flags , DebugMode(..), cycleMarkVision, toggleOmniscient ) where import qualified Data.Set as S import Data.Binary-import qualified Game.LambdaHack.Config as Config import qualified System.Random as R import System.Time+import Data.Text (Text)+import qualified Data.Text as T+import qualified NLP.Miniutter.English as MU+import qualified Data.List as L import Game.LambdaHack.Actor import Game.LambdaHack.Point@@ -26,10 +32,13 @@ import qualified Game.LambdaHack.Dungeon as Dungeon import Game.LambdaHack.Item import Game.LambdaHack.Msg-import Game.LambdaHack.FOV import Game.LambdaHack.Time import qualified Game.LambdaHack.Kind as Kind import Game.LambdaHack.Content.FactionKind+import Game.LambdaHack.Content.ItemKind+import Game.LambdaHack.Effect+import Game.LambdaHack.Flavour+import Game.LambdaHack.Config -- | The diary contains all the player data that carries over -- from game to game, even across playing sessions. That includes@@ -58,7 +67,7 @@ , slid :: Dungeon.LevelId -- ^ identifier of the current level , scounter :: Int -- ^ stores next actor index , srandom :: R.StdGen -- ^ current random generator- , sconfig :: Config.CP -- ^ this game's config (including initial RNG)+ , sconfig :: Config -- ^ this game's config (including initial RNG) , stakeTime :: Maybe Bool -- ^ last command unexpectedly took some time , squit :: Maybe (Bool, Status) -- ^ cause of game end/exit , sfaction :: Kind.Id FactionKind -- ^ our faction@@ -109,15 +118,15 @@ defaultDiary :: IO Diary defaultDiary = do dateTime <- getClockTime- let curDate = calendarTimeToString $ toUTCTime dateTime+ let curDate = MU.Text $ T.pack $ calendarTimeToString $ toUTCTime dateTime return Diary { sreport = emptyReport , shistory = singletonHistory $ singletonReport $- "Player diary started on " ++ curDate ++ "."+ makeSentence ["Player diary started on", curDate] } -- | Initial game state.-defaultState :: Config.CP -> Kind.Id FactionKind -> FlavourMap+defaultState :: Config -> Kind.Id FactionKind -> FlavourMap -> Dungeon.Dungeon -> Dungeon.LevelId -> Point -> R.StdGen -> State defaultState config sfaction flavour dng lid ploc g =@@ -254,3 +263,60 @@ 2 -> return Victor 3 -> return Restart _ -> fail "no parse (Status)"++-- TODO: probably move these somewhere++-- | The part of speech describing the item.+-- If cheating is allowed, full identity of the item is revealed+-- together with its flavour (e.g. at the game over screen).+partItemCheat :: Bool -> Kind.Ops ItemKind -> State -> Item -> MU.Part+partItemCheat cheat coitem@Kind.Ops{okind} state i =+ let ik = jkind i+ kind = okind ik+ identified = L.length (iflavour kind) == 1 ||+ ik `S.member` sdisco state+ eff = effectToSuffix (ieffect kind)+ pwr = if jpower i == 0+ then ""+ else "(+" <> showT (jpower i) <> ")"+ genericName = iname kind+ name = let fullName = genericName <+> eff <+> pwr+ flavour = getFlavour coitem (sflavour state) ik+ in if identified+ then fullName+ else flavourToName flavour+ <+> if cheat then fullName else genericName+ in MU.Text name++-- | The part of speech describing the item.+partItem :: Kind.Ops ItemKind -> State -> Item -> MU.Part+partItem = partItemCheat False++partItemNWs :: Kind.Ops ItemKind -> State -> Item -> MU.Part+partItemNWs coitem s i = MU.NWs (jcount i) $ partItem coitem s i++-- | Produces a textual description of the terrain and items at an already+-- explored location. Mute for unknown locations.+-- The detailed variant is for use in the targeting mode.+lookAt :: Kind.COps -- ^ game content+ -> Bool -- ^ detailed?+ -> Bool -- ^ can be seen right now?+ -> State -- ^ game state+ -> Level -- ^ current level+ -> Point -- ^ location to describe+ -> Text -- ^ an extra sentence to print+ -> Text+lookAt Kind.COps{coitem, cotile=Kind.Ops{oname}} detailed canSee s lvl loc msg+ | detailed =+ let tile = lvl `rememberAt` loc+ in makeSentence [MU.Text $ oname tile] <+> msg <+> isd+ | otherwise = msg <+> isd+ where+ is = lvl `rememberAtI` loc+ prefixSee = MU.Text $ if canSee then "you see" else "you remember"+ isd = case is of+ [] -> ""+ _ | length is <= 3 ->+ makeSentence [prefixSee, MU.WWandW $ map (partItemNWs coitem s) is]+ _ | detailed -> "Objects:"+ _ -> "Objects here."
Game/LambdaHack/Strategy.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | AI strategies to direct actors not controlled by the player. -- No operation in this module involves the 'State' or 'Action' type. module Game.LambdaHack.Strategy@@ -6,8 +7,10 @@ ) where import Control.Monad+import Data.Text (Text) import Game.LambdaHack.Utils.Frequency+import Game.LambdaHack.Msg -- | A strategy is a choice of (non-empty) frequency tables -- of possible actions.@@ -24,7 +27,7 @@ , (q, b) <- runFrequency y ] | x <- runStrategy m- , let name = "Strategy_bind (" ++ nameFrequency x ++ ")"]+ , let name = "Strategy_bind (" <> nameFrequency x <> ")"] instance MonadPlus Strategy where mzero = Strategy []@@ -73,9 +76,9 @@ bestVariant (Strategy (f : _)) = f -- | Overwrite the description of all frequencies within the strategy.-renameStrategy :: String -> Strategy a -> Strategy a+renameStrategy :: Text -> Strategy a -> Strategy a renameStrategy newName (Strategy fs) = Strategy $ map (renameFreq newName) fs -- | Like 'return', but pick a name of the single frequency.-returN :: String -> a -> Strategy a+returN :: Text -> a -> Strategy a returN name x = Strategy $ return $ uniformFreq name [x]
Game/LambdaHack/StrategyAction.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | AI strategy operations implemented with the 'Action' monad. module Game.LambdaHack.StrategyAction ( targetStrategy, strategy@@ -10,6 +11,7 @@ import Control.Monad import Control.Monad.State hiding (State, state) import Control.Arrow+import qualified Data.Text as T import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Ability (Ability)@@ -25,6 +27,7 @@ import Game.LambdaHack.Strategy import Game.LambdaHack.State import Game.LambdaHack.Action+import Game.LambdaHack.Msg import Game.LambdaHack.EffectAction import Game.LambdaHack.Actions import Game.LambdaHack.ItemAction@@ -154,9 +157,9 @@ -- set new direction updateAnyActor actor $ \ m -> m { bdir = Just (dir, 0) } -- perform action- tryWith (\ msg -> if null msg+ tryWith (\ msg -> if T.null msg then return ()- else assert `failure` (msg, "in AI")) $ do+ else assert `failure` msg <> "in AI") $ do -- If the following action aborts, we just advance the time and continue. -- TODO: ensure time is taken for other aborted actions in this file -- TODO: or just fail at each abort in AI code? or use tryWithFrame?@@ -253,7 +256,8 @@ Just [] -> bloc -- TODO Just (lbl:_) -> lbl throwFreq is multi =- [ (benefit * multi, projectGroupItem actor floc (iverbProject ik) i)+ [ (benefit * multi,+ projectGroupItem actor floc (iverbProject ik) i) | i <- is, let ik = iokind (jkind i), let benefit = - (1 + jpower i) * Effect.effectToBenefit (ieffect ik),
Game/LambdaHack/Turn.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | The main loop of the game, processing player and AI moves turn by turn. module Game.LambdaHack.Turn ( handleTurn ) where @@ -69,8 +70,8 @@ when (clipN == 1) regenerateLevelHP when (clipN == 3) generateMonster ptime <- gets (btime . getPlayerBody) -- time of player's next move- debug $ "handleTurn: time check. ptime = "- ++ show ptime ++ ", time = " ++ show time+ debug $ "handleTurn: time check. ptime ="+ <+> showT ptime <> ", time =" <+> showT time handleActors timeZero modify (updateTime (timeAdd timeClip)) endOrLoop handleTurn@@ -163,11 +164,11 @@ updateAnyActor actor $ \ m -> m { btarget } stateNew <- get let stratMove = strategy cops actor stateNew factionAbilities- debug $ "handleAI faction: " ++ fname faction- ++ ", symbol: " ++ show bsymbol- ++ ", loc: " ++ show bloc- ++ "\nhandleAI target: " ++ show stratTarget- ++ "\nhandleAI move: " ++ show stratMove+ debug $ "handleAI faction:" <+> fname faction+ <> ", symbol:" <+> showT bsymbol+ <> ", loc:" <+> showT bloc+ <> "\nhandleAI target:" <+> showT stratTarget+ <> "\nhandleAI move:" <+> showT stratMove -- Run the AI: choses an action from those given by the AI strategy. join $ rndToAction $ frequency $ bestVariant $ stratMove @@ -216,7 +217,7 @@ fr <- drawPrompt ColorFull "" return (timed, [Just fr]) else return (timed, frs)- Nothing -> let msgKey = "unknown command <" ++ K.showKM km ++ ">"+ Nothing -> let msgKey = "unknown command <" <> K.showKM km <> ">" in abortWith msgKey -- The command was aborted or successful and if the latter, -- possibly took some time.
Game/LambdaHack/Utils/Assert.hs view
@@ -23,8 +23,8 @@ in trace s False infix 1 `failure`--- | Like 'Prelude.undefined', but shows the source location--- and also the value to blame for the failure. To be used as in:+-- | Like 'error', but shows the source location and also+-- the value to blame for the failure. To be used as in: -- -- > assert `failure` ((x1, y1), (x2, y2), "designate a vertical line") failure :: Show a => (Bool -> b -> b) -> a -> b
Game/LambdaHack/Utils/Frequency.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | A list of items with relative frequencies of appearance. module Game.LambdaHack.Utils.Frequency ( -- * The @Frequency@ type@@ -12,13 +13,15 @@ import Control.Monad import qualified System.Random as R+import Data.Text (Text) import Game.LambdaHack.Utils.Assert+import Game.LambdaHack.Msg -- TODO: do not expose runFrequency -- | The frequency distribution type. data Frequency a = Frequency- { nameFrequency :: String -- ^ short description for debug, etc.+ { nameFrequency :: Text -- ^ short description for debug, etc. , runFrequency :: [(Int, a)] -- ^ give acces to raw frequency values } deriving Show@@ -26,17 +29,17 @@ instance Monad Frequency where return x = Frequency "return" [(1, x)] Frequency name xs >>= f =- Frequency ("bind (" ++ name ++ ")")+ Frequency ("bind (" <> name <> ")") [(p * q, y) | (p, x) <- xs , (q, y) <- runFrequency (f x) ] instance MonadPlus Frequency where mplus (Frequency xname xs) (Frequency yname ys) = let name = case (xs, ys) of- ([], []) -> []+ ([], []) -> "[]" ([], _ ) -> yname (_, []) -> xname- _ -> "(" ++ xname ++ ") ++ (" ++ yname ++ ")"+ _ -> "(" <> xname <> ") ++ (" <> yname <> ")" in Frequency name (xs ++ ys) mzero = Frequency "[]" [] @@ -44,40 +47,41 @@ fmap f (Frequency name xs) = Frequency name (map (\ (p, x) -> (p, f x)) xs) -- | Uniform discrete frequency distribution.-uniformFreq :: String -> [a] -> Frequency a+uniformFreq :: Text -> [a] -> Frequency a uniformFreq name = Frequency name . map (\ x -> (1, x)) -- | Takes a name and a list of frequencies and items -- into the frequency distribution.-toFreq :: String -> [(Int, a)] -> Frequency a+toFreq :: Text -> [(Int, a)] -> Frequency a toFreq = Frequency -- | Scale frequecy distribution, multiplying it -- by a positive integer constant. scaleFreq :: Show a => Int -> Frequency a -> Frequency a scaleFreq n (Frequency name xs) =- assert (n > 0 `blame` ("non-positive scale for " ++ name, n, xs)) $+ assert (n > 0 `blame` ("non-positive scale for" <+> name, n, xs)) $ Frequency name (map (\ (p, x) -> (n * p, x)) xs) -- | Change the description of the frequency.-renameFreq :: String -> Frequency a -> Frequency a+renameFreq :: Text -> Frequency a -> Frequency a renameFreq newName fr = fr {nameFrequency = newName} -- | Randomly choose an item according to the distribution. rollFreq :: Show a => Frequency a -> R.StdGen -> (a, R.StdGen) rollFreq (Frequency name []) _ =- assert `failure` ("choice from an empty frequency: " ++ name)+ assert `failure` ("choice from an empty frequency:" <+> name) rollFreq (Frequency name [(n, x)]) _ | n <= 0 =- assert `failure` ("singleton frequency with nothing to pick: " ++ name, n, x)+ assert `failure` ("singleton frequency with nothing to pick:" <+> name, n, x) rollFreq (Frequency _ [(_, x)]) g = (x, g) -- speedup rollFreq (Frequency name fs) g =- assert (sumf > 0 `blame` ("frequency with nothing to pick: " ++ name, fs)) $+ assert (sumf > 0 `blame` ("frequency with nothing to pick:" <+> name, fs)) $ (frec r fs, ng) where sumf = sum (map fst fs) (r, ng) = R.randomR (1, sumf) g frec :: Int -> [(Int, a)] -> a- frec m [] = assert `failure` ("impossible", name, fs, m)+ frec m [] = assert `failure`+ ("impossible" <+> name, fs, m) frec m ((n, x) : _) | m <= n = x frec m ((n, _) : xs) = frec (m - n) xs
LambdaHack.cabal view
@@ -1,13 +1,6 @@-cabal-version: >= 1.10 name: LambdaHack-version: 0.2.6-license: BSD3-license-file: LICENSE-tested-with: GHC == 7.2.2, GHC == 7.4.1, GHC == 7.6.1-data-files: LICENSE, CREDITS, PLAYING.md, README.md,- config.default, config.bot, scores-author: Andres Loeh, Mikolaj Konarski-maintainer: Mikolaj Konarski <mikolaj.konarski@funktory.com>+version: 0.2.6.5+synopsis: A roguelike game engine in early and active development description: This is an alpha release of LambdaHack, a game engine library for roguelike games of arbitrary theme, size and complexity,@@ -22,8 +15,16 @@ but the fundamental source of flexibility lies in the strict and type-safe separation of code and content. .- New in this release are the Main Menu and the improved- and configurable mode of squad combat.+ This is a minor release, primarily intended to fix broken+ haddock documentation on Hackage by disabling gtk2hs dependency+ under GHC 7.6.1 (if you use GHC 7.6.1 and gtk2hs compiles+ for you, please run 'cabal install -fgtk --reinstall').+ Changes since 0.2.6 are mostly unrelated to gameplay:+ strictly typed config files split into UI and rules;+ a switch from Text to String throughout the codebase;+ use of the external library 'miniutter' for English sentence+ generation.+ . Upcoming new features: playable monsters faction, more than two factions inhabiting the dungeon, AIvAI, PvP, improved ranged combat AI, dynamic light sources, explosions@@ -44,181 +45,205 @@ the module is the exclusive interface to the directory. No references to the modules in the directory are allowed except from the interface module.-synopsis: A roguelike game engine in early and active development homepage: http://github.com/kosmikus/LambdaHack bug-reports: http://github.com/kosmikus/LambdaHack/issues+license: BSD3+license-file: LICENSE+tested-with: GHC == 7.2.2, GHC == 7.4.1, GHC == 7.6.1+data-files: LICENSE, CREDITS, PLAYING.md, README.md,+ config.rules.default, config.ui.default, config.rules.bot,+ scores+author: Andres Loeh, Mikolaj Konarski+maintainer: Mikolaj Konarski <mikolaj.konarski@funktory.com> category: Game Engine build-type: Simple+cabal-version: >= 1.10 source-repository head- type: git- location: git://github.com/kosmikus/LambdaHack.git+ type: git+ location: git://github.com/kosmikus/LambdaHack.git -flag curses- description: pick the curses frontend- default: False+flag gtk+ description: enable the gtk frontend+ default: False flag vty- description: pick the vty frontend- default: False+ description: enable the vty frontend+ default: False +flag curses+ description: enable the curses frontend+ default: False+ flag std- description: pick the stdin/stdout frontend- default: False+ description: enable the stdin/stdout frontend+ default: False library- exposed-modules: Game.LambdaHack.Ability,- Game.LambdaHack.Action,- Game.LambdaHack.Action.ActionLift- Game.LambdaHack.Action.ConfigIO- Game.LambdaHack.Action.Frontend,- Game.LambdaHack.Action.Frontend.Chosen,- Game.LambdaHack.Action.HighScore,- Game.LambdaHack.Action.Save,- Game.LambdaHack.Actions,- Game.LambdaHack.Actor,- Game.LambdaHack.ActorState,- Game.LambdaHack.Animation,- Game.LambdaHack.Area,- Game.LambdaHack.AreaRnd,- Game.LambdaHack.Binding,- Game.LambdaHack.BindingAction,- Game.LambdaHack.CDefs,- Game.LambdaHack.Cave,- Game.LambdaHack.Color,- Game.LambdaHack.Command,- Game.LambdaHack.CommandAction,- Game.LambdaHack.Config,- Game.LambdaHack.Content.ActorKind,- Game.LambdaHack.Content.CaveKind,- Game.LambdaHack.Content.FactionKind,- Game.LambdaHack.Content.ItemKind,- Game.LambdaHack.Content.PlaceKind,- Game.LambdaHack.Content.RuleKind,- Game.LambdaHack.Content.StrategyKind,- Game.LambdaHack.Content.TileKind,- Game.LambdaHack.Draw,- Game.LambdaHack.Dungeon,- Game.LambdaHack.DungeonState,- Game.LambdaHack.Effect,- Game.LambdaHack.EffectAction,- Game.LambdaHack.Feature,- Game.LambdaHack.Flavour,- Game.LambdaHack.FOV,- Game.LambdaHack.FOV.Common,- Game.LambdaHack.FOV.Digital,- Game.LambdaHack.FOV.Permissive,- Game.LambdaHack.FOV.Shadow,- Game.LambdaHack.Grammar,- Game.LambdaHack.Item,- Game.LambdaHack.ItemAction,- Game.LambdaHack.Key,- Game.LambdaHack.Kind,- Game.LambdaHack.Level,- Game.LambdaHack.Misc,- Game.LambdaHack.Msg,- Game.LambdaHack.Perception,- Game.LambdaHack.Place,- Game.LambdaHack.Point,- Game.LambdaHack.PointXY,- Game.LambdaHack.Random,- Game.LambdaHack.Running,- Game.LambdaHack.State,- Game.LambdaHack.Strategy,- Game.LambdaHack.StrategyAction,- Game.LambdaHack.Tile,- Game.LambdaHack.Time,- Game.LambdaHack.Turn,- Game.LambdaHack.Utils.Assert,- Game.LambdaHack.Utils.File,- Game.LambdaHack.Utils.Frequency,- Game.LambdaHack.Utils.LQueue,- Game.LambdaHack.Vector- Game.LambdaHack.VectorXY- other-modules: Paths_LambdaHack- build-depends: ConfigFile >= 1.1.1 && < 2,- array >= 0.3.0.3 && < 1,- base >= 4 && < 5,- binary >= 0.5.0.2 && < 1,- bytestring >= 0.9.2 && < 1,- containers >= 0.4.1 && < 1,- directory >= 1.1.0.1 && < 2,- filepath >= 1.2.0.1 && < 2,- mtl >= 2.0.1 && < 3,- old-time >= 1.0.0.7 && < 2,- random >= 1.0.1 && < 2,- zlib >= 0.5.3.1 && < 1- default-language: Haskell2010+ exposed-modules: Game.LambdaHack.Ability,+ Game.LambdaHack.Action,+ Game.LambdaHack.Action.ActionLift+ Game.LambdaHack.Action.ConfigIO+ Game.LambdaHack.Action.Frontend,+ Game.LambdaHack.Action.Frontend.Chosen,+ Game.LambdaHack.Action.HighScore,+ Game.LambdaHack.Action.Save,+ Game.LambdaHack.Actions,+ Game.LambdaHack.Actor,+ Game.LambdaHack.ActorState,+ Game.LambdaHack.Animation,+ Game.LambdaHack.Area,+ Game.LambdaHack.AreaRnd,+ Game.LambdaHack.Binding,+ Game.LambdaHack.BindingAction,+ Game.LambdaHack.CDefs,+ Game.LambdaHack.Cave,+ Game.LambdaHack.Color,+ Game.LambdaHack.Command,+ Game.LambdaHack.CommandAction,+ Game.LambdaHack.Config,+ Game.LambdaHack.Content.ActorKind,+ Game.LambdaHack.Content.CaveKind,+ Game.LambdaHack.Content.FactionKind,+ Game.LambdaHack.Content.ItemKind,+ Game.LambdaHack.Content.PlaceKind,+ Game.LambdaHack.Content.RuleKind,+ Game.LambdaHack.Content.StrategyKind,+ Game.LambdaHack.Content.TileKind,+ Game.LambdaHack.Draw,+ Game.LambdaHack.Dungeon,+ Game.LambdaHack.DungeonState,+ Game.LambdaHack.Effect,+ Game.LambdaHack.EffectAction,+ Game.LambdaHack.Feature,+ Game.LambdaHack.Flavour,+ Game.LambdaHack.FOV,+ Game.LambdaHack.FOV.Common,+ Game.LambdaHack.FOV.Digital,+ Game.LambdaHack.FOV.Permissive,+ Game.LambdaHack.FOV.Shadow,+ Game.LambdaHack.Item,+ Game.LambdaHack.ItemAction,+ Game.LambdaHack.Key,+ Game.LambdaHack.Kind,+ Game.LambdaHack.Level,+ Game.LambdaHack.Misc,+ Game.LambdaHack.Msg,+ Game.LambdaHack.Perception,+ Game.LambdaHack.Place,+ Game.LambdaHack.Point,+ Game.LambdaHack.PointXY,+ Game.LambdaHack.Random,+ Game.LambdaHack.Running,+ Game.LambdaHack.State,+ Game.LambdaHack.Strategy,+ Game.LambdaHack.StrategyAction,+ Game.LambdaHack.Tile,+ Game.LambdaHack.Time,+ Game.LambdaHack.Turn,+ Game.LambdaHack.Utils.Assert,+ Game.LambdaHack.Utils.File,+ Game.LambdaHack.Utils.Frequency,+ Game.LambdaHack.Utils.LQueue,+ Game.LambdaHack.Vector+ Game.LambdaHack.VectorXY+ other-modules: Paths_LambdaHack+ build-depends: ConfigFile >= 1.1.1 && < 2,+ array >= 0.3.0.3 && < 1,+ base >= 4 && < 5,+ binary >= 0.5.0.2 && < 1,+ bytestring >= 0.9.2 && < 1,+ containers >= 0.4.1 && < 1,+ directory >= 1.1.0.1 && < 2,+ filepath >= 1.2.0.1 && < 2,+ miniutter >= 0.4.0 && < 2,+ mtl >= 2.0.1 && < 3,+ old-time >= 1.0.0.7 && < 2,+ random >= 1.0.1 && < 2,+ text >= 0.11.2.3 && < 1,+ zlib >= 0.5.3.1 && < 1++ default-language: Haskell2010 default-extensions: MonoLocalBinds, BangPatterns, RecordWildCards, NamedFieldPuns- other-extensions: MultiParamTypeClasses, RankNTypes, ScopedTypeVariables,- TypeFamilies, CPP- ghc-options: -Wall -fwarn-orphans -fwarn-tabs -fwarn-incomplete-uni-patterns -fwarn-incomplete-record-updates -fwarn-monomorphism-restriction -fwarn-unrecognised-pragmas- ghc-options: -fno-warn-auto-orphans -fno-warn-implicit-prelude- ghc-options: -fno-ignore-asserts -funbox-strict-fields+ other-extensions: CPP, MultiParamTypeClasses, RankNTypes,+ ScopedTypeVariables, TypeFamilies,+ OverloadedStrings, ExtendedDefaultRules+ ghc-options: -Wall -fwarn-orphans -fwarn-tabs -fwarn-incomplete-uni-patterns -fwarn-incomplete-record-updates -fwarn-monomorphism-restriction -fwarn-unrecognised-pragmas+ ghc-options: -fno-warn-auto-orphans -fno-warn-implicit-prelude+ ghc-options: -fno-ignore-asserts -funbox-strict-fields - if flag(curses) {- other-modules: Game.LambdaHack.Action.Frontend.Curses- build-depends: hscurses >= 1.4.1 && < 2- cpp-options: -DCURSES+ if flag(gtk) {+ other-modules: Game.LambdaHack.Action.Frontend.Gtk+ build-depends: gtk >= 0.12.1 && < 0.13+ cpp-options: -DGTK } else { if flag(vty) {- other-modules: Game.LambdaHack.Action.Frontend.Vty- build-depends: vty >= 4.7.0.6- cpp-options: -DVTY- } else { if flag(std) {- other-modules: Game.LambdaHack.Action.Frontend.Std- cpp-options: -DSTD+ other-modules: Game.LambdaHack.Action.Frontend.Vty+ build-depends: vty >= 4.7.0.6+ cpp-options: -DVTY+ } else { if flag(curses) {+ other-modules: Game.LambdaHack.Action.Frontend.Curses+ build-depends: hscurses >= 1.4.1 && < 2+ cpp-options: -DCURSES+-- a hack to work around gtk2hs problems on Hackage under GHC 7.6.1:+ } else { if flag(std) || impl(ghc == 7.6.1) {+ other-modules: Game.LambdaHack.Action.Frontend.Std+ cpp-options: -DSTD } else {- other-modules: Game.LambdaHack.Action.Frontend.Gtk- build-depends: gtk >= 0.12.1 && < 0.13- } } }+ other-modules: Game.LambdaHack.Action.Frontend.Gtk+ build-depends: gtk >= 0.12.1 && < 0.13+ } } } } executable LambdaHack- hs-source-dirs: LambdaHack- main-is: Main.hs- other-modules: Content.ActorKind,- Content.CaveKind,- Content.FactionKind,- Content.ItemKind,- Content.PlaceKind,- Content.RuleKind,- Content.StrategyKind,- Content.TileKind,- Multiline,- Paths_LambdaHack- build-depends: LambdaHack,- template-haskell >= 2.6 && < 3,+ hs-source-dirs: LambdaHack+ main-is: Main.hs+ other-modules: Content.ActorKind,+ Content.CaveKind,+ Content.FactionKind,+ Content.ItemKind,+ Content.PlaceKind,+ Content.RuleKind,+ Content.StrategyKind,+ Content.TileKind,+ Multiline,+ Paths_LambdaHack+ build-depends: LambdaHack,+ template-haskell >= 2.6 && < 3, - ConfigFile >= 1.1.1 && < 2,- array >= 0.3.0.3 && < 1,- base >= 4 && < 5,- binary >= 0.5.0.2 && < 1,- bytestring >= 0.9.2 && < 1,- containers >= 0.4.1 && < 1,- directory >= 1.1.0.1 && < 2,- filepath >= 1.2.0.1 && < 2,- mtl >= 2.0.1 && < 3,- old-time >= 1.0.0.7 && < 2,- random >= 1.0.1 && < 2,- zlib >= 0.5.3.1 && < 1- default-language: Haskell2010+ ConfigFile >= 1.1.1 && < 2,+ array >= 0.3.0.3 && < 1,+ base >= 4 && < 5,+ binary >= 0.5.0.2 && < 1,+ bytestring >= 0.9.2 && < 1,+ containers >= 0.4.1 && < 1,+ directory >= 1.1.0.1 && < 2,+ filepath >= 1.2.0.1 && < 2,+ miniutter >= 0.4.0 && < 2,+ mtl >= 2.0.1 && < 3,+ old-time >= 1.0.0.7 && < 2,+ random >= 1.0.1 && < 2,+ text >= 0.11.2.3 && < 1,+ zlib >= 0.5.3.1 && < 1++ default-language: Haskell2010 default-extensions: MonoLocalBinds, BangPatterns, RecordWildCards, NamedFieldPuns- other-extensions: CPP, QuasiQuotes- ghc-options: -Wall -fwarn-orphans -fwarn-tabs -fwarn-incomplete-uni-patterns -fwarn-incomplete-record-updates -fwarn-monomorphism-restriction -fwarn-unrecognised-pragmas- ghc-options: -fno-warn-auto-orphans -fno-warn-implicit-prelude- ghc-options: -fno-ignore-asserts -funbox-strict-fields- ghc-options: -threaded -with-rtsopts=-C0.005--- ghc-options: -with-rtsopts=-N -- eats all cores+ other-extensions: CPP, QuasiQuotes, OverloadedStrings, ExtendedDefaultRules+ ghc-options: -Wall -fwarn-orphans -fwarn-tabs -fwarn-incomplete-uni-patterns -fwarn-incomplete-record-updates -fwarn-monomorphism-restriction -fwarn-unrecognised-pragmas+ ghc-options: -fno-warn-auto-orphans -fno-warn-implicit-prelude+ ghc-options: -fno-ignore-asserts -funbox-strict-fields+ ghc-options: -threaded -with-rtsopts=-C0.005+-- ghc-options: -with-rtsopts=-N -- eats all cores executable DumbBot- hs-source-dirs: DumbBot- main-is: Main.hs- build-depends: base >= 4 && < 5,- random >= 1.0.1 && < 2- default-language: Haskell2010+ hs-source-dirs: DumbBot+ main-is: Main.hs+ build-depends: base >= 4 && < 5,+ random >= 1.0.1 && < 2++ default-language: Haskell2010 default-extensions: MonoLocalBinds, BangPatterns, RecordWildCards, NamedFieldPuns- ghc-options: -Wall -fwarn-orphans -fwarn-tabs -fwarn-incomplete-uni-patterns -fwarn-incomplete-record-updates -fwarn-monomorphism-restriction -fwarn-unrecognised-pragmas- ghc-options: -fno-warn-auto-orphans -fno-warn-implicit-prelude- ghc-options: -fno-ignore-asserts -funbox-strict-fields+ ghc-options: -Wall -fwarn-orphans -fwarn-tabs -fwarn-incomplete-uni-patterns -fwarn-incomplete-record-updates -fwarn-monomorphism-restriction -fwarn-unrecognised-pragmas+ ghc-options: -fno-warn-auto-orphans -fno-warn-implicit-prelude+ ghc-options: -fno-ignore-asserts -funbox-strict-fields
LambdaHack/Content/ActorKind.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | Monsters and heroes for LambdaHack. module Content.ActorKind ( cdefs ) where
LambdaHack/Content/CaveKind.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | Cave layouts for LambdaHack. module Content.CaveKind ( cdefs ) where
LambdaHack/Content/FactionKind.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | Game factions (heroes, enemies, NPCs, etc.) for LambdaHack. module Content.FactionKind ( cdefs ) where
LambdaHack/Content/ItemKind.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | Weapons and treasure for LambdaHack. module Content.ItemKind ( cdefs ) where @@ -15,9 +16,9 @@ , getFreq = ifreq , validate = ivalidate , content =- [amulet, dart, gem1, gem2, gem3, gold, harpoon, potion1, potion2, potion3, ring, scroll1, scroll2, scroll3, sword, wand, fist, foot, tentacle, weight]+ [amulet, dart, gem1, gem2, gem3, currency, harpoon, potion1, potion2, potion3, ring, scroll1, scroll2, scroll3, sword, wand, fist, foot, tentacle, weight] }-amulet, dart, gem1, gem2, gem3, gold, harpoon, potion1, potion2, potion3, ring, scroll1, scroll2, scroll3, sword, wand, fist, foot, tentacle, weight :: ItemKind+amulet, dart, gem1, gem2, gem3, currency, harpoon, potion1, potion2, potion3, ring, scroll1, scroll2, scroll3, sword, wand, fist, foot, tentacle, weight :: ItemKind gem, potion, scroll :: ItemKind -- generic templates @@ -71,10 +72,10 @@ gem3 = gem { icount = (RollDice 0 0, RollDice 1 3) -- appears on max depth }-gold = ItemKind+currency = ItemKind { isymbol = '$' , iname = "gold piece"- , ifreq = [("dng", 80)]+ , ifreq = [("dng", 80), ("currency", 1)] , iflavour = zipPlain [BrYellow] , ieffect = NoEffect , icount = (RollDice 0 0, RollDice 10 10)
LambdaHack/Content/PlaceKind.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | Rooms, halls and passages for LambdaHack. module Content.PlaceKind ( cdefs ) where
LambdaHack/Content/RuleKind.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE CPP, QuasiQuotes #-} -- | Game rules and assorted game setup data for LambdaHack. module Content.RuleKind ( cdefs ) where@@ -27,10 +28,10 @@ { rsymbol = 's' , rname = "standard LambdaHack ruleset" , rfreq = [("standard", 100)]- -- Check whether one location is accessible from another.- -- Precondition: the two locations are next to each other.- -- Apart of checking the target tile, we forbid diagonal movement- -- to and from doors.+ -- Check whether one location is accessible from another.+ -- Precondition: the two locations are next to each other.+ -- Apart of checking the target tile, we forbid diagonal movement+ -- to and from doors. , raccessible = \ lxsize sloc src tloc tgt -> F.Walkable `elem` tfeature tgt && not ((F.Closable `elem` tfeature src ||@@ -41,15 +42,18 @@ , rpathsVersion = Self.version , ritemMelee = ")" , ritemProject = "!?|/"- -- The string containing the default configuration- -- included from file config.default.- -- Warning: cabal does not detect that the default config is changed,- -- so touching this file is needed to reinclude config and recompile.+ -- The strings containing the default configuration files,+ -- included from files config.game.default and config.ui.default.+ -- Warning: cabal does not detect that the config files are changed,+ -- so touching them is needed to reinclude configs and recompile. -- Note: consider code.haskell.org/~dons/code/compiled-constants -- as soon as the config file grows very big.- , rconfigDefault = [multiline|-#include "../../config.default"+ , rcfgRulesDefault = [multiline|+#include "../../config.rules.default" |]+ , rcfgUIDefault = [multiline|+#include "../../config.ui.default"+|] -- ASCII art for the Main Menu. Only pure 7-bit ASCII characters are -- allowed. The picture should be exactly 24 rows by 80 columns, -- plus an extra frame of any charecters that is ignored for all purposes.@@ -65,7 +69,7 @@ -- The Main Menu is displayed dull white on black. -- TODO: Highlighted keybinding is in inverse video or bright white on grey -- background. The spaces that pad keybindings are not highlighted.- , rmainMenuArt = [multiline|+ , rmainMenuArt = [multiline| ---------------------------------------------------------------------------------- | | | |
LambdaHack/Content/StrategyKind.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | AI strategies for LambdaHack. module Content.StrategyKind ( cdefs ) where
LambdaHack/Content/TileKind.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- | Terrain tiles for LambdaHack. module Content.TileKind ( cdefs ) where
PLAYING.md view
@@ -154,7 +154,7 @@ -------------------- You win the game if you escape the dungeon alive. Your score is-the sum of all gold you've plundered plus 100gp for each gem.+the sum of all gold you've plundered plus 100 gold pieces for each gem. Only the loot in possession of the party members on the current level counts (the rest of the party is considered MIA).
README.md view
@@ -58,14 +58,14 @@ DumbBot 42 20000000 | LambdaHack > /tmp/log You may wish to tweak the game configuration file for the bot,-e.g., by helping it play longer, as in the supplied config.bot.+e.g., by helping it play longer, as in the supplied config.rules.bot. Compatibility notes ------------------- The current code was tested with GHC 7.6.1, but probably works with GHC > 7.2.-A [few tweaks] [7] are needed to compile with 7.0.+A [few tweaks] [6] are needed to compile with 7.0. If you are using the curses or vty frontends, numerical keypad may not work correctly depending on the versions
− config.bot
@@ -1,9 +0,0 @@-; Overriding config file options to help the bot.-; DumbBot 42 20000000 | LambdaHack > /tmp/log--[dungeon]-depth: 100--[heroes]-baseHP: 999999-extraHeroes: 1
− config.default
@@ -1,95 +0,0 @@-; ; This is a commented out copy of the default config file-; ; that is embedded in the binary.-; ; The game looks for the user config file that overrides some options-; ; in the ~/.LambdaHack/config file (or similar, depending on the OS).-; ; Warning: options are case-sensitive and only ';' comments are permitted.--; [commands]-; ; All commands are defined here, except movement, hero selection and debug.-; ;-; ; Interaction with the dungeon.-; c: TriggerDir { verb = "close", object = "door", feature = Closable }-; o: TriggerDir { verb = "open", object = "door", feature = Openable }-; less: TriggerTile { verb = "ascend", object = "level", feature = Cause Ascend }-; greater: TriggerTile { verb = "descend", object = "level", feature = Cause Descend }-; bracketleft: TgtAscend 1-; bracketright: TgtAscend (-1)-; braceleft: TgtAscend 10-; braceright: TgtAscend (-10)-; slash: TgtFloor-; asterisk: TgtEnemy-; plus: EpsIncr True-; minus: EpsIncr False-; Tab: HeroCycle-; ISO_Left_Tab: HeroBack-; ;-; ; Items.-; g: Pickup-; d: Drop-; i: Inventory-; q: Apply { verb = "quaff", object = "potion", syms = "!" }-; r: Apply { verb = "read", object = "scroll", syms = "?" }-; z: Project { verb = "zap", object = "wand", syms = "/" }-; t: Project { verb = "throw", object = "missile", syms = "|" }-; ;-; ; Saving, exiting and restarting the game.-; X: GameExit-; R: GameRestart-; S: GameSave-; ;-; ; Information for the player.-; P: History-; question: Help-; D: CfgDump-; ;-; ; General.-; KP_Begin: Wait-; Escape: Cancel-; Return: Accept-; space: Clear--; [dungeon]-; ; Fixed caves for the first levels, then randomly picked caves.-; LambdaCave_1: caveRogue-; LambdaCave_2: caveRogue-; LambdaCave_3: caveEmpty-; LambdaCave_10: caveNoise-; depth: 10--; [engine]-; fovMode: digital-; ;fovMode: permissive-; ;fovMode: shadow-; fovRadius: 12-; ;startingRandomGenerator: 42-; ;dungeonRandomGenerator: 42--; [files]-; ; Paths to various game files; relative to the save file directory.-; scoresFile: scores-; saveFile: save-; diaryFile : diary--; [heroes]-; HeroName_0: you-; HeroName_1: Haskell Alvin-; HeroName_2: Alonzo Barkley-; HeroName_3: Ernst Abraham-; HeroName_4: Samuel Saunders-; HeroName_5: Roger Robin-; baseHP: 50-; extraHeroes: 2-; firstDeathEnds: False-; faction: hero--; [macros]-; ; Handy with Vi keys:-; comma: g-; period: KP_Begin--; [monsters]-; smellTimeout: 100--; [ui]-; font: Terminus,Monospace normal normal normal normal 12-; historyMax: 5000
+ config.rules.bot view
@@ -0,0 +1,9 @@+; Overriding config file options to help the bot.+; DumbBot 42 20000000 | LambdaHack > /tmp/log++[dungeon]+depth: 100++[heroes]+baseHP: 999999+extraHeroes: 1
+ config.rules.default view
@@ -0,0 +1,30 @@+; ; This is a commented out copy of the default game rules config file+; ; that is embedded in the binary.+; ; A user config file can overrides these options. The game looks for it at+; ; ~/.LambdaHack/config.rules.ini (or a similar path, depending on the OS).+; ; Warning: options are case-sensitive and only ';' for comments is permitted.++; [dungeon]+; depth: 10++; [caves]+; ; Fixed caves for the first levels, then randomly picked caves.+; LambdaCave_1: caveRogue+; LambdaCave_2: caveRogue+; LambdaCave_3: caveEmpty+; LambdaCave_10: caveNoise++; [engine]+; fovMode: Digital 12+; ;fovMode: Permissive+; ;fovMode: Shadow+; ;startingRandomGenerator: 42+; ;dungeonRandomGenerator: 42+; smellTimeout: 100++; [heroes]+; baseHP: 50+; extraHeroes: 2+; firstDeathEnds: False+; ; faction "playable" means a random choice+; faction: hero
+ config.ui.default view
@@ -0,0 +1,72 @@+; ; This is a commented out copy of the default UI settings config file+; ; that is embedded in the binary.+; ; A user config file can overrides these options. The game looks for it at+; ; ~/.LambdaHack/config.ui.ini (or a similar path, depending on the OS).+; ; Warning: options are case-sensitive and only ';' for comments is permitted.++; [commands]+; ; All commands are defined here, except movement, hero selection and debug.+; ;+; ; Interaction with the dungeon.+; c: TriggerDir { verb = "close", object = "door", feature = Closable }+; o: TriggerDir { verb = "open", object = "door", feature = Openable }+; less: TriggerTile { verb = "ascend", object = "level", feature = Cause Ascend }+; greater: TriggerTile { verb = "descend", object = "level", feature = Cause Descend }+; bracketleft: TgtAscend 1+; bracketright: TgtAscend (-1)+; braceleft: TgtAscend 10+; braceright: TgtAscend (-10)+; slash: TgtFloor+; asterisk: TgtEnemy+; plus: EpsIncr True+; minus: EpsIncr False+; Tab: HeroCycle+; ISO_Left_Tab: HeroBack+; ;+; ; Items.+; g: Pickup+; d: Drop+; i: Inventory+; q: Apply { verb = "quaff", object = "potion", syms = "!" }+; r: Apply { verb = "read", object = "scroll", syms = "?" }+; z: Project { verb = "zap", object = "wand", syms = "/" }+; t: Project { verb = "throw", object = "missile", syms = "|" }+; ;+; ; Saving, exiting and restarting the game.+; X: GameExit+; R: GameRestart+; S: GameSave+; ;+; ; Information for the player.+; P: History+; question: Help+; D: CfgDump+; ;+; ; General.+; KP_Begin: Wait+; Escape: Cancel+; Return: Accept+; space: Clear++; [files]+; ; Names of various game files. They reside in ~/.LambdaHack or similar.+; scoresFile: scores+; saveFile: save+; diaryFile : diary++; [heroNames]+; HeroName_0: you+; HeroName_1: Haskell Alvin+; HeroName_2: Alonzo Barkley+; HeroName_3: Ernst Abraham+; HeroName_4: Samuel Saunders+; HeroName_5: Roger Robin++; [macros]+; ; Handy with Vi keys:+; comma: g+; period: KP_Begin++; [ui]+; font: Terminus,Monospace normal normal normal normal 12+; historyMax: 5000